Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBL-6674: Partial Index API and tests #1698

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/Couchbase.Lite.Shared/API/Query/FullTextIndexConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ namespace Couchbase.Lite.Query
/// </summary>
public sealed class FullTextIndexConfiguration : IndexConfiguration
{
#region Properties

/// <summary>
/// Gets whether or not to ignore accents when performing
Expand All @@ -42,13 +41,17 @@ public sealed class FullTextIndexConfiguration : IndexConfiguration
/// </summary>
public string Language { get; } = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;

/// <summary>
/// A predicate expression defining conditions for indexing documents.
/// Only documents satisfying the predicate are included, enabling partial indexes.
/// </summary>
public string? Where { get; set; }

internal override C4IndexOptions Options => new C4IndexOptions {
ignoreDiacritics = IgnoreAccents,
language = Language
language = Language,
where = Where
};
#endregion

#region Constructors

/// <summary>
/// Starts the creation of an index based on a full text search
Expand All @@ -58,10 +61,24 @@ public sealed class FullTextIndexConfiguration : IndexConfiguration
/// <param name="locale">The locale to use when performing full text searching</param>
/// <returns>The beginning of an FTS based index</returns>
public FullTextIndexConfiguration(string[] expressions, bool ignoreAccents = false,
string? locale = null)
: this(expressions, null, ignoreAccents, locale)
{
}

/// <summary>
/// Starts the creation of an index based on a full text search
/// </summary>
/// <param name="expressions">The expressions to use to create the index</param>
/// <param name="ignoreAccents">The boolean value to ignore accents when performing the full text search</param>
/// <param name="locale">The locale to use when performing full text searching</param>
/// <returns>The beginning of an FTS based index</returns>
public FullTextIndexConfiguration(string[] expressions, string? where = null, bool ignoreAccents = false,
string? locale = null)
: base(C4IndexType.FullTextIndex, expressions)
{
IgnoreAccents = ignoreAccents;
Where = where;
if (!string.IsNullOrEmpty(locale)) {
Language = locale!;
}
Expand All @@ -76,7 +93,5 @@ public FullTextIndexConfiguration(params string[] expressions)
: base(C4IndexType.FullTextIndex, expressions)
{
}

#endregion
}
}
32 changes: 24 additions & 8 deletions src/Couchbase.Lite.Shared/API/Query/ValueIndexConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,46 @@

using Couchbase.Lite.Internal.Query;
using LiteCore.Interop;
using System.Collections.Generic;
using System.Linq;

namespace Couchbase.Lite.Query
{
/// <summary>
/// An class for an index based on a simple property value
/// An class for an index based on one or more simple property values
/// </summary>
public sealed class ValueIndexConfiguration : IndexConfiguration
{
#region Properties
internal override C4IndexOptions Options => new C4IndexOptions();
#endregion
internal override C4IndexOptions Options => new C4IndexOptions
{
where = Where
};

#region Constructors
/// <summary>
/// A predicate expression defining conditions for indexing documents.
/// Only documents satisfying the predicate are included, enabling partial indexes.
/// </summary>
public string? Where { get; set; }

/// <summary>
/// Starts the creation of an index based on a simple property
/// Starts the creation of an index based on one or more simple property values
/// </summary>
/// <param name="expressions">The expressions to use to create the index</param>
/// <returns>The beginning of a value based index</returns>
public ValueIndexConfiguration(params string[] expressions)
: base(C4IndexType.ValueIndex, expressions)
{
}

#endregion
/// <summary>
/// Starts the creation of an index based on one or more simple property values,
/// and a predicate for enabling partial indexes.
/// </summary>
/// <param name="expressions">The expressions to use to create the index</param>
/// <param name="where">A where clause used to determine whether or not to include a particular doc</param>
public ValueIndexConfiguration(IEnumerable<string> expressions, string? where = null)
: base(C4IndexType.ValueIndex, expressions.ToArray())
{
Where = where;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Compile Include="$(MSBuildThisFileDirectory)MmapTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NotificationTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)P2PTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PartialIndexTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ScopesCollections.QueryTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ScopesCollections.ReplicationTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ReplicationTest.cs" />
Expand Down
2 changes: 1 addition & 1 deletion src/Couchbase.Lite.Tests.Shared/MmapTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public unsafe void TestDatabaseWithConfiguredMMap(bool useMmap)
var nativeConfig = TestNative.c4db_getConfig2(c4db!.RawDatabase);
var hasFlag = (nativeConfig->flags & C4DatabaseFlags.MmapDisabled) == C4DatabaseFlags.MmapDisabled;
hasFlag.Should().Be(!useMmap, "because the flag in LiteCore should match MmapEnabled (but flipped)");
}
#endif
}
}
}
118 changes: 118 additions & 0 deletions src/Couchbase.Lite.Tests.Shared/PartialIndexTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// PartialIndexTest.cs
//
// Copyright (c) 2024 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using Couchbase.Lite;
using Couchbase.Lite.Query;
using FluentAssertions;
using System.Linq;
using Xunit;
using Xunit.Abstractions;

namespace Test;

[ImplementsTestSpec("T0007-Partial-Index", "1.0.3")]
public class PartialIndexTest : TestCase
{
public PartialIndexTest(ITestOutputHelper output) : base(output)
{
}

/// <summary>
/// Test that a partial value index is successfully created.
///
/// Steps
/// 1. Create a partial value index named "numIndex" in the default collection.
/// - expression: "num"
/// - where: "type = 'number'"
/// 2. Check that the index is successfully created.
/// 3. Create a query object with an SQL++ string:
/// - SELECt* FROM _ WHERE type = 'number' AND num > 1000
/// 4. Get the query plan from the query object and check that the plan contains
/// "USING INDEX numIndex" string.
/// 5. Create a query object with an SQL++ string:
/// - SELECt* FROM _ WHERE type = 'foo' AND num > 1000
/// 6. Get the query plan from the query object and check that the plan doesn't contain
/// "USING INDEX numIndex" string.
/// </summary>
[Fact]
public void TestCreatePartialValueIndex()
{
// Step 1
var indexConfig = new ValueIndexConfiguration(["num"], "type = 'number'");
DefaultCollection.CreateIndex("numIndex", indexConfig);

// Step 2
DefaultCollection.GetIndexes().Should().Contain("numIndex", "because the index was just created");

// Step 3
using var partialQuery = Db.CreateQuery("SELECT * FROM _ WHERE type = 'number' AND num > 1000");

// Step 4
partialQuery.Explain().Should().Contain("USING INDEX numIndex", "because the partial index should be applied to this query");

// Step 5
using var nonPartialQuery = Db.CreateQuery("SELECT * FROM _ WHERE type = 'foo' AND num > 1000");

// Step 6
nonPartialQuery.Explain().Should().NotContain("USING INDEX numIndex", "because the partial index should not be applied to this query");
}

/// <summary>
/// Test that a partial full text index is successfully created.
///
/// Steps
/// 1. Create following two documents with the following bodies in the default collection.
/// - { "content" : "Couchbase Lite is a database." }
/// - { "content" : "Couchbase Lite is a NoSQL syncable database." }
/// 2. Create a partial full text index named "contentIndex" in the default collection.
/// - expression: "content"
/// - where: "length(content) > 30"
/// 3. Check that the index is successfully created.
/// 4. Create a query object with an SQL++ string:
/// - SELECt content FROM _ WHERE match(contentIndex, "database")
/// 5. Execute the query and check that:
/// - There is one result returned
/// - The returned content is "Couchbase Lite is a NoSQL syncable database.".
/// </summary>
[Fact]
public void TestCreatePartialFullTextIndex()
{
// Step 1
using var doc1 = new MutableDocument();
using var doc2 = new MutableDocument();
doc1.SetString("content", "Couchbase Lite is a database.");
doc2.SetString("content", "Couchbase Lite is a NoSQL syncable database.");
DefaultCollection.Save(doc1);
DefaultCollection.Save(doc2);

// Step 2
var indexConfig = new FullTextIndexConfiguration(["content"], "length(content) > 30");
DefaultCollection.CreateIndex("contentIndex", indexConfig);

// Step 3
DefaultCollection.GetIndexes().Should().Contain("contentIndex", "because the index was just created");

// Step 4
using var query = Db.CreateQuery("SELECT content FROM _ WHERE match(contentIndex, 'database')");

// Step 5
var results = query.Execute().ToList();
results.Should().HaveCount(1, "because only one document matches the partial index criteria");
results[0].GetString("content").Should().Be("Couchbase Lite is a NoSQL syncable database.", "because this is the document that matches the query");
}
}
2 changes: 1 addition & 1 deletion src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2102,7 +2102,7 @@ public void TestDisposeRunningReplicator()
stoppedWait.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue("because otherwise the replicator didn't stop");
}

#if __IOS__ && !SANITY_ONLY
#if __IOS__ && !MACCATALYST && !SANITY_ONLY
[SkippableFact]
public void TestSwitchBackgroundForeground()
{
Expand Down
14 changes: 13 additions & 1 deletion src/LiteCore/src/LiteCore.Shared/Interop/C4IndexTypes_defs.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//
// C4IndexTypes_defs.cs
//
// Copyright (c) 2024 Couchbase, Inc All rights reserved.
// Copyright (c) 2025 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -115,6 +115,7 @@ internal unsafe partial struct C4IndexOptions
private IntPtr _stopWords;
private IntPtr _unnestPath;
public C4VectorIndexOptions vector;
private IntPtr _where;

public string? language
{
Expand Down Expand Up @@ -168,6 +169,17 @@ public string? unnestPath
Marshal.FreeHGlobal(old);
}
}

public string? where
{
get {
return Marshal.PtrToStringAnsi(_where);
}
set {
var old = Interlocked.Exchange(ref _where, Marshal.StringToHGlobalAnsi(value));
Marshal.FreeHGlobal(old);
}
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/LiteCore/src/LiteCore.Shared/Interop/C4Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public void Dispose()
if (old != IntPtr.Zero) {
Marshal.FreeHGlobal(old);
}

old = Interlocked.Exchange(ref _where, IntPtr.Zero);
if(old != IntPtr.Zero) {
Marshal.FreeHGlobal(old);
}
}
}

Expand Down