diff --git a/src/Couchbase.Lite.Shared/API/Query/FullTextIndexConfiguration.cs b/src/Couchbase.Lite.Shared/API/Query/FullTextIndexConfiguration.cs index c8eb16095..3bb3105c0 100644 --- a/src/Couchbase.Lite.Shared/API/Query/FullTextIndexConfiguration.cs +++ b/src/Couchbase.Lite.Shared/API/Query/FullTextIndexConfiguration.cs @@ -28,7 +28,6 @@ namespace Couchbase.Lite.Query /// public sealed class FullTextIndexConfiguration : IndexConfiguration { - #region Properties /// /// Gets whether or not to ignore accents when performing @@ -42,13 +41,17 @@ public sealed class FullTextIndexConfiguration : IndexConfiguration /// public string Language { get; } = CultureInfo.CurrentCulture.TwoLetterISOLanguageName; + /// + /// A predicate expression defining conditions for indexing documents. + /// Only documents satisfying the predicate are included, enabling partial indexes. + /// + public string? Where { get; set; } + internal override C4IndexOptions Options => new C4IndexOptions { ignoreDiacritics = IgnoreAccents, - language = Language + language = Language, + where = Where }; - #endregion - - #region Constructors /// /// Starts the creation of an index based on a full text search @@ -58,10 +61,24 @@ public sealed class FullTextIndexConfiguration : IndexConfiguration /// The locale to use when performing full text searching /// The beginning of an FTS based index public FullTextIndexConfiguration(string[] expressions, bool ignoreAccents = false, + string? locale = null) + : this(expressions, null, ignoreAccents, locale) + { + } + + /// + /// Starts the creation of an index based on a full text search + /// + /// The expressions to use to create the index + /// The boolean value to ignore accents when performing the full text search + /// The locale to use when performing full text searching + /// The beginning of an FTS based index + 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!; } @@ -76,7 +93,5 @@ public FullTextIndexConfiguration(params string[] expressions) : base(C4IndexType.FullTextIndex, expressions) { } - - #endregion } } diff --git a/src/Couchbase.Lite.Shared/API/Query/ValueIndexConfiguration.cs b/src/Couchbase.Lite.Shared/API/Query/ValueIndexConfiguration.cs index dd1e6af78..44b8b1a1d 100644 --- a/src/Couchbase.Lite.Shared/API/Query/ValueIndexConfiguration.cs +++ b/src/Couchbase.Lite.Shared/API/Query/ValueIndexConfiguration.cs @@ -18,30 +18,46 @@ using Couchbase.Lite.Internal.Query; using LiteCore.Interop; +using System.Collections.Generic; +using System.Linq; namespace Couchbase.Lite.Query { /// - /// An class for an index based on a simple property value + /// An class for an index based on one or more simple property values /// public sealed class ValueIndexConfiguration : IndexConfiguration { - #region Properties - internal override C4IndexOptions Options => new C4IndexOptions(); - #endregion + internal override C4IndexOptions Options => new C4IndexOptions + { + where = Where + }; - #region Constructors + /// + /// A predicate expression defining conditions for indexing documents. + /// Only documents satisfying the predicate are included, enabling partial indexes. + /// + public string? Where { get; set; } /// - /// 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 /// /// The expressions to use to create the index - /// The beginning of a value based index public ValueIndexConfiguration(params string[] expressions) : base(C4IndexType.ValueIndex, expressions) { } - #endregion + /// + /// Starts the creation of an index based on one or more simple property values, + /// and a predicate for enabling partial indexes. + /// + /// The expressions to use to create the index + /// A where clause used to determine whether or not to include a particular doc + public ValueIndexConfiguration(IEnumerable expressions, string? where = null) + : base(C4IndexType.ValueIndex, expressions.ToArray()) + { + Where = where; + } } } diff --git a/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems b/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems index d13d1b6da..93b31716c 100644 --- a/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems +++ b/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems @@ -27,6 +27,7 @@ + diff --git a/src/Couchbase.Lite.Tests.Shared/MmapTest.cs b/src/Couchbase.Lite.Tests.Shared/MmapTest.cs index 7bc77f8d8..9aa27d22f 100644 --- a/src/Couchbase.Lite.Tests.Shared/MmapTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/MmapTest.cs @@ -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 + } } } \ No newline at end of file diff --git a/src/Couchbase.Lite.Tests.Shared/PartialIndexTest.cs b/src/Couchbase.Lite.Tests.Shared/PartialIndexTest.cs new file mode 100644 index 000000000..e39871f50 --- /dev/null +++ b/src/Couchbase.Lite.Tests.Shared/PartialIndexTest.cs @@ -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) + { + } + + /// + /// 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. + /// + [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"); + } + + /// + /// 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.". + /// + [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"); + } +} \ No newline at end of file diff --git a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs index 4085050d9..5621b63f1 100644 --- a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs @@ -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() { diff --git a/src/LiteCore/src/LiteCore.Shared/Interop/C4IndexTypes_defs.cs b/src/LiteCore/src/LiteCore.Shared/Interop/C4IndexTypes_defs.cs index 537c56299..60282891e 100644 --- a/src/LiteCore/src/LiteCore.Shared/Interop/C4IndexTypes_defs.cs +++ b/src/LiteCore/src/LiteCore.Shared/Interop/C4IndexTypes_defs.cs @@ -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. @@ -115,6 +115,7 @@ internal unsafe partial struct C4IndexOptions private IntPtr _stopWords; private IntPtr _unnestPath; public C4VectorIndexOptions vector; + private IntPtr _where; public string? language { @@ -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); + } + } } } diff --git a/src/LiteCore/src/LiteCore.Shared/Interop/C4Query.cs b/src/LiteCore/src/LiteCore.Shared/Interop/C4Query.cs index 744245164..a5715014b 100644 --- a/src/LiteCore/src/LiteCore.Shared/Interop/C4Query.cs +++ b/src/LiteCore/src/LiteCore.Shared/Interop/C4Query.cs @@ -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); + } } }