diff --git a/CouchbaseLite.xcodeproj/project.pbxproj b/CouchbaseLite.xcodeproj/project.pbxproj index cde10426e..fdb21db10 100644 --- a/CouchbaseLite.xcodeproj/project.pbxproj +++ b/CouchbaseLite.xcodeproj/project.pbxproj @@ -1784,6 +1784,10 @@ AEC806BC2C89EA68001C9723 /* CBLArrayIndexConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = AEC806B62C89EA68001C9723 /* CBLArrayIndexConfiguration.m */; }; AEC806BD2C89EA68001C9723 /* CBLArrayIndexConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = AEC806B62C89EA68001C9723 /* CBLArrayIndexConfiguration.m */; }; AEC806BE2C89EA68001C9723 /* CBLArrayIndexConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = AEC806B52C89EA68001C9723 /* CBLArrayIndexConfiguration.h */; settings = {ATTRIBUTES = (Private, ); }; }; + AECA89782CAC765A00C7B6BE /* CBLIndexable+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */; }; + AECA89792CAC765B00C7B6BE /* CBLIndexable+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */; }; + AECA897A2CAC765C00C7B6BE /* CBLIndexable+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */; }; + AECA897B2CAC765D00C7B6BE /* CBLIndexable+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */; }; AECD5A162C0E21D900B1247E /* CBLIndexUpdater+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECD5A0F2C0E21D900B1247E /* CBLIndexUpdater+Internal.h */; }; AECD5A172C0E21D900B1247E /* CBLIndexUpdater+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = AECD5A0F2C0E21D900B1247E /* CBLIndexUpdater+Internal.h */; }; /* End PBXBuildFile section */ @@ -2807,6 +2811,7 @@ AE83D0832C0637ED0055D2CF /* CBLIndexUpdater.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CBLIndexUpdater.mm; sourceTree = ""; }; AEC806B52C89EA68001C9723 /* CBLArrayIndexConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CBLArrayIndexConfiguration.h; sourceTree = ""; }; AEC806B62C89EA68001C9723 /* CBLArrayIndexConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CBLArrayIndexConfiguration.m; sourceTree = ""; }; + AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CBLIndexable+Internal.h"; sourceTree = ""; }; AECD59F92C0E137200B1247E /* CBLQueryIndex+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CBLQueryIndex+Internal.h"; sourceTree = ""; }; AECD5A0F2C0E21D900B1247E /* CBLIndexUpdater+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CBLIndexUpdater+Internal.h"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -3601,6 +3606,7 @@ 27CDE75E207407280082D458 /* CBLDocumentChangeNotifier.h */, 27CDE75F207407280082D458 /* CBLDocumentChangeNotifier.mm */, 1A1612A8283DE8A200AA4987 /* CBLScope+Internal.h */, + AECA89652CAC714800C7B6BE /* CBLIndexable+Internal.h */, ); name = Database; sourceTree = ""; @@ -4376,6 +4382,7 @@ 93EB264521DF1AE40006FB88 /* CBLDocumentFlags.h in Headers */, 9383A5961F1EEFCD0083053D /* CBLQueryResult+Internal.h in Headers */, 934A27931F30E5CA003946A7 /* CBLBinaryExpression.h in Headers */, + AECA89792CAC765B00C7B6BE /* CBLIndexable+Internal.h in Headers */, 27D7219A1F8E97F400AA4458 /* CBLFleece.hh in Headers */, 9374A8A7201FC53600BA0D9E /* CBLReplicator+Backgrounding.h in Headers */, 932565A521ED13290092F4E0 /* CBLLogFileConfiguration.h in Headers */, @@ -4686,6 +4693,7 @@ 40FC1BD72B928A4F00394276 /* CBLCoreMLPredictiveModel.h in Headers */, 40FC1B502B92873000394276 /* CBLEncryptionKey.h in Headers */, 9343EFE4207D611600F19A89 /* CBLDatabase+Internal.h in Headers */, + AECA897A2CAC765C00C7B6BE /* CBLIndexable+Internal.h in Headers */, 40FC1BE02B928A4F00394276 /* CBLPredictiveIndex.h in Headers */, 9343EFE5207D611600F19A89 /* CBLDictionaryFragment.h in Headers */, 40FC1C1E2B928B5000394276 /* CBLVectorEncoding+Internal.h in Headers */, @@ -4890,6 +4898,7 @@ 9343F0FB207D61AB00F19A89 /* CBLQueryResultSet.h in Headers */, 9343F0FC207D61AB00F19A89 /* CBLDocumentFragment.h in Headers */, 9343F0FD207D61AB00F19A89 /* CBLQueryBuilder.h in Headers */, + AECA897B2CAC765D00C7B6BE /* CBLIndexable+Internal.h in Headers */, 40FC1BEE2B928A4F00394276 /* CBLQueryFunction+Vector.h in Headers */, 9343F0FF207D61AB00F19A89 /* CBLMutableArrayFragment.h in Headers */, 1AA2EE0328A682A800DEB47E /* CBLCollectionConfiguration+Swift.h in Headers */, @@ -4978,6 +4987,7 @@ 1AAFB66F284A260A00878453 /* CBLCollectionChangeObservable.h in Headers */, 9384D8401FC405D200FE89D8 /* CBLQueryFullTextFunction.h in Headers */, 1A3470E9266F69220042C6BA /* CBLIndexConfiguration+Internal.h in Headers */, + AECA89782CAC765A00C7B6BE /* CBLIndexable+Internal.h in Headers */, 933208141E77415E000D9993 /* CBLQueryExpression.h in Headers */, 935A58B621AFA34D009A29CB /* CBLDocumentReplication.h in Headers */, 1AEF0585283380D500D5DDEA /* CBLScope.h in Headers */, diff --git a/Objective-C/CBLArrayIndexConfiguration.m b/Objective-C/CBLArrayIndexConfiguration.m index 29a6025d8..02e2a43e1 100644 --- a/Objective-C/CBLArrayIndexConfiguration.m +++ b/Objective-C/CBLArrayIndexConfiguration.m @@ -32,7 +32,7 @@ - (instancetype) initWithPath: (NSString*) path expressions: @[@""]]; } else if ([expressions count] == 0) { [NSException raise: NSInvalidArgumentException format: - @"Expressions cannot be empty "]; + @"Empty expressions is not allowed, use nil instead"]; } self = [super initWithIndexType: kC4ArrayIndex diff --git a/Objective-C/CBLCollection.mm b/Objective-C/CBLCollection.mm index 2e19fa14f..d8a1af991 100644 --- a/Objective-C/CBLCollection.mm +++ b/Objective-C/CBLCollection.mm @@ -473,151 +473,6 @@ - (BOOL) setDocumentExpirationWithID: (NSString*)documentID } } -#pragma mark - Internal - -- (BOOL) checkIsValid: (NSError**)error { - BOOL valid = c4coll_isValid(_c4col); - if (!valid) { - if (error) - *error = CBLCollectionErrorNotOpen; - } - - return valid; -} - -- (BOOL) isValid { - return [self checkIsValid: nil]; -} - -- (BOOL) database: (CBLDatabase*)db isValid: (NSError**)error { - BOOL valid = db != nil; - if (!valid) { - if (error) - *error = CBLDatabaseErrorNotOpen; - } - - return valid; -} - -- (C4CollectionSpec) c4spec { - // Convert to cString directly instead of using CBLStringBytes to avoid - // stack-use-after-scope problem when a small string is kept in the - // _local stack based buffer of the CBLStringBytes - C4Slice name = c4str([_name cStringUsingEncoding: NSUTF8StringEncoding]); - C4Slice scopeName = c4str([_scope.name cStringUsingEncoding: NSUTF8StringEncoding]); - return { .name = name, .scope = scopeName }; -} - -- (BOOL) isEqual: (id)object { - if (self == object) - return YES; - - CBLCollection* other = $castIf(CBLCollection, object); - if (!other) - return NO; - - if (!(other && [self.name isEqual: other.name] && - [self.scope.name isEqual: other.scope.name] && - [self.database.path isEqual: other.database.path])) { - return NO; - } - - if (!self.isValid || !other.isValid) - return NO; - - return YES; -} - -- (id) addCollectionChangeListener: (void (^)(CBLCollectionChange*))listener - queue: (dispatch_queue_t)queue { - if (!_colChangeNotifier) { - _colChangeNotifier = [CBLChangeNotifier new]; - C4Error c4err = {}; - _colObs = c4dbobs_createOnCollection(_c4col, colObserverCallback, (__bridge void *)self, &c4err); - if (!_colObs) { - CBLWarn(Database, @"%@ Failed to create collection obs c4col=%p err=%d/%d", - self, _c4col, c4err.domain, c4err.code); - } - } - - return [_colChangeNotifier addChangeListenerWithQueue: queue listener: listener delegate: self]; -} - -static void colObserverCallback(C4CollectionObserver* obs, void* context) { - CBLCollection *c = (__bridge CBLCollection *)context; - dispatch_async(c.dispatchQueue, ^{ - [c postCollectionChanged]; - }); -} - -- (void) postCollectionChanged { - CBL_LOCK(_mutex) { - if (!_colObs || !_c4col) - return; - - const uint32_t kMaxChanges = 100u; - C4DatabaseChange changes[kMaxChanges]; - bool external = false; - C4CollectionObservation obs = {}; - NSMutableArray* docIDs = [NSMutableArray new]; - do { - // Read changes in batches of kMaxChanges: - obs = c4dbobs_getChanges(_colObs, changes, kMaxChanges); - if (obs.numChanges == 0 || external != obs.external || docIDs.count > 1000) { - if(docIDs.count > 0) { - CBLCollectionChange* change = [[CBLCollectionChange alloc] initWithCollection: self - documentIDs: docIDs - isExternal: external]; - [_colChangeNotifier postChange: change]; - docIDs = [NSMutableArray new]; - } - } - - external = obs.external; - for(uint32_t i = 0; i < obs.numChanges; i++) { - NSString *docID =slice2string(changes[i].docID); - [docIDs addObject: docID]; - } - c4dbobs_releaseChanges(changes, obs.numChanges); - } while(obs.numChanges > 0); - } -} - -- (void) removeToken: (id)token { - CBL_LOCK(_mutex) { - CBLChangeListenerToken* t = (CBLChangeListenerToken*)token; - if (t.context) - [self removeDocumentChangeListenerWithToken: token]; - else { - if ([_colChangeNotifier removeChangeListenerWithToken: token] == 0) { - c4dbobs_free(_colObs); - _colObs = nil; - _colChangeNotifier = nil; - } - } - } -} - -- (void) removeDocumentChangeListenerWithToken: (CBLChangeListenerToken*)token { - CBL_LOCK(_mutex) { - NSString* documentID = (NSString*)token.context; - CBLDocumentChangeNotifier* notifier = _docChangeNotifiers[documentID]; - if (notifier && [notifier removeChangeListenerWithToken: token] == 0) { - [notifier stop]; - [_docChangeNotifiers removeObjectForKey:documentID]; - } - } -} - -- (void) freeC4Observer { - c4dbobs_free(_colObs); - _colObs = nullptr; - _colChangeNotifier = nil; - - [_docChangeNotifiers.allValues makeObjectsPerformSelector: @selector(stop)]; - _docChangeNotifiers = nil; -} - #pragma mark - Document listener - (id) addDocumentChangeListenerWithDocumentID: documentID @@ -976,4 +831,171 @@ - (nullable CBLQueryIndex*) indexWithName: (nonnull NSString*)name } } +#pragma mark - Internal + +- (BOOL) checkIsValid: (NSError**)error { + BOOL valid = c4coll_isValid(_c4col); + if (!valid) { + if (error) + *error = CBLCollectionErrorNotOpen; + } + + return valid; +} + +- (BOOL) isValid { + return [self checkIsValid: nil]; +} + +- (BOOL) database: (CBLDatabase*)db isValid: (NSError**)error { + BOOL valid = db != nil; + if (!valid) { + if (error) + *error = CBLDatabaseErrorNotOpen; + } + + return valid; +} + +- (C4CollectionSpec) c4spec { + // Convert to cString directly instead of using CBLStringBytes to avoid + // stack-use-after-scope problem when a small string is kept in the + // _local stack based buffer of the CBLStringBytes + C4Slice name = c4str([_name cStringUsingEncoding: NSUTF8StringEncoding]); + C4Slice scopeName = c4str([_scope.name cStringUsingEncoding: NSUTF8StringEncoding]); + return { .name = name, .scope = scopeName }; +} + +- (BOOL) isEqual: (id)object { + if (self == object) + return YES; + + CBLCollection* other = $castIf(CBLCollection, object); + if (!other) + return NO; + + if (!(other && [self.name isEqual: other.name] && + [self.scope.name isEqual: other.scope.name] && + [self.database.path isEqual: other.database.path])) { + return NO; + } + + if (!self.isValid || !other.isValid) + return NO; + + return YES; +} + +- (id) addCollectionChangeListener: (void (^)(CBLCollectionChange*))listener + queue: (dispatch_queue_t)queue { + if (!_colChangeNotifier) { + _colChangeNotifier = [CBLChangeNotifier new]; + C4Error c4err = {}; + _colObs = c4dbobs_createOnCollection(_c4col, colObserverCallback, (__bridge void *)self, &c4err); + if (!_colObs) { + CBLWarn(Database, @"%@ Failed to create collection obs c4col=%p err=%d/%d", + self, _c4col, c4err.domain, c4err.code); + } + } + + return [_colChangeNotifier addChangeListenerWithQueue: queue listener: listener delegate: self]; +} + +static void colObserverCallback(C4CollectionObserver* obs, void* context) { + CBLCollection *c = (__bridge CBLCollection *)context; + dispatch_async(c.dispatchQueue, ^{ + [c postCollectionChanged]; + }); +} + +- (void) postCollectionChanged { + CBL_LOCK(_mutex) { + if (!_colObs || !_c4col) + return; + + const uint32_t kMaxChanges = 100u; + C4DatabaseChange changes[kMaxChanges]; + bool external = false; + C4CollectionObservation obs = {}; + NSMutableArray* docIDs = [NSMutableArray new]; + do { + // Read changes in batches of kMaxChanges: + obs = c4dbobs_getChanges(_colObs, changes, kMaxChanges); + if (obs.numChanges == 0 || external != obs.external || docIDs.count > 1000) { + if(docIDs.count > 0) { + CBLCollectionChange* change = [[CBLCollectionChange alloc] initWithCollection: self + documentIDs: docIDs + isExternal: external]; + [_colChangeNotifier postChange: change]; + docIDs = [NSMutableArray new]; + } + } + + external = obs.external; + for(uint32_t i = 0; i < obs.numChanges; i++) { + NSString *docID =slice2string(changes[i].docID); + [docIDs addObject: docID]; + } + c4dbobs_releaseChanges(changes, obs.numChanges); + } while(obs.numChanges > 0); + } +} + +- (void) removeToken: (id)token { + CBL_LOCK(_mutex) { + CBLChangeListenerToken* t = (CBLChangeListenerToken*)token; + if (t.context) + [self removeDocumentChangeListenerWithToken: token]; + else { + if ([_colChangeNotifier removeChangeListenerWithToken: token] == 0) { + c4dbobs_free(_colObs); + _colObs = nil; + _colChangeNotifier = nil; + } + } + } +} + +- (void) removeDocumentChangeListenerWithToken: (CBLChangeListenerToken*)token { + CBL_LOCK(_mutex) { + NSString* documentID = (NSString*)token.context; + CBLDocumentChangeNotifier* notifier = _docChangeNotifiers[documentID]; + if (notifier && [notifier removeChangeListenerWithToken: token] == 0) { + [notifier stop]; + [_docChangeNotifiers removeObjectForKey:documentID]; + } + } +} + +- (void) freeC4Observer { + c4dbobs_free(_colObs); + _colObs = nullptr; + _colChangeNotifier = nil; + + [_docChangeNotifiers.allValues makeObjectsPerformSelector: @selector(stop)]; + _docChangeNotifiers = nil; +} + +- (nullable NSArray*) _indexesInfo: (NSError**)error { + CBL_LOCK(_mutex) { + if (![self checkIsValid: error]) + return nil; + + C4Error err = {}; + C4SliceResult res = c4coll_getIndexesInfo(_c4col, &err); + if (err.code != 0){ + convertError(err, error); + return nil; + } + + FLDoc doc = FLDoc_FromResultData(res, kFLTrusted, nullptr, nullslice); + FLSliceResult_Release(res); + + NSArray* indexes = FLValue_GetNSObject(FLDoc_GetRoot(doc), nullptr); + FLDoc_Release(doc); + + return indexes; + } +} + @end diff --git a/Objective-C/CBLQueryIndex.mm b/Objective-C/CBLQueryIndex.mm index 3d14bd265..41554015a 100644 --- a/Objective-C/CBLQueryIndex.mm +++ b/Objective-C/CBLQueryIndex.mm @@ -82,4 +82,16 @@ - (id) mutex { return _mutex; } +- (NSString*) _unnestPathForIndex { + CBL_LOCK(_mutex) { + C4IndexOptions* opts = {}; + c4index_getOptions(_c4index, opts); + if (!opts->unnestPath) { + return [NSString stringWithUTF8String: opts->unnestPath]; + } else { + return nil; + } + } +} + @end diff --git a/Objective-C/Internal/CBLCollection+Internal.h b/Objective-C/Internal/CBLCollection+Internal.h index 2d9d340f7..2ff4ece1c 100644 --- a/Objective-C/Internal/CBLCollection+Internal.h +++ b/Objective-C/Internal/CBLCollection+Internal.h @@ -21,6 +21,8 @@ #import "CBLCollection.h" #import "CBLChangeListenerToken.h" #import "CBLDatabase.h" +#import "CBLIndexable+Internal.h" +#import "c4.h" #define CBLCollectionErrorNotOpen [NSError errorWithDomain: CBLErrorDomain \ code: CBLErrorNotOpen \ @@ -33,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface CBLCollection () +@interface CBLCollection () /** dispatch queue */ @property (readonly, nonatomic) dispatch_queue_t dispatchQueue; diff --git a/Objective-C/Internal/CBLIndexable+Internal.h b/Objective-C/Internal/CBLIndexable+Internal.h new file mode 100644 index 000000000..9db66452d --- /dev/null +++ b/Objective-C/Internal/CBLIndexable+Internal.h @@ -0,0 +1,31 @@ +// +// CBLIndexable+Internal.h +// CouchbaseLite +// +// 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. +// + +#import "CouchbaseLite/CBLIndexable.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol CBLIndexableInternal + +/** Return all index information about all indexes. */ +- (nullable NSArray*) _indexesInfo: (NSError**)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Objective-C/Internal/CBLQueryIndex+Internal.h b/Objective-C/Internal/CBLQueryIndex+Internal.h index 416df3f76..aed7724e9 100644 --- a/Objective-C/Internal/CBLQueryIndex+Internal.h +++ b/Objective-C/Internal/CBLQueryIndex+Internal.h @@ -34,6 +34,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype) initWithC4Index: (C4Index*) c4index name: (NSString*) name collection: (CBLCollection*) collection; + +- (NSString*) _unnestPathForIndex; @end NS_ASSUME_NONNULL_END diff --git a/Objective-C/Tests/CollectionTest.m b/Objective-C/Tests/CollectionTest.m index 7ec875d14..bf971cc20 100644 --- a/Objective-C/Tests/CollectionTest.m +++ b/Objective-C/Tests/CollectionTest.m @@ -25,14 +25,6 @@ @interface CollectionTest : CBLTestCase @implementation CollectionTest -- (void) setUp { - [super setUp]; -} - -- (void) tearDown { - [super tearDown]; -} - - (void) testGetNonExistingDoc { NSError* error = nil; CBLCollection* col = [self.db defaultCollection: &error]; diff --git a/Objective-C/Tests/UnnestArrayIndexTest.m b/Objective-C/Tests/UnnestArrayIndexTest.m new file mode 100644 index 000000000..232e12498 --- /dev/null +++ b/Objective-C/Tests/UnnestArrayIndexTest.m @@ -0,0 +1,97 @@ +// +// UnnestArrayIndexTest.m +// CBL_ObjC +// +// Created by Vlad Velicu on 01/10/2024. +// Copyright © 2024 Couchbase. All rights reserved. +// + +#import "CBLTestCase.h" +#import "CBLArrayIndexConfiguration.h" +#import "CBLCollection+Internal.h" +#import "CBLQueryIndex+Internal.h" + +@interface UnnestArrayIndexTest : CBLTestCase + +@end + +@implementation UnnestArrayIndexTest + +/** + https://github.com/couchbaselabs/couchbase-lite-api/blob/master/spec/tests/T0004-Unnest-Array-Index.md + */ + +/** + 1. TestArrayIndexConfigInvalidExpressions + Description + Test that creating an ArrayIndexConfiguration with invalid expressions which are an empty expressions or contain null. + Steps + 1. Create a ArrayIndexConfiguration object. + path: "contacts" + expressions: [] + 2. Check that an invalid arument exception is thrown. + */ + +- (void) testArrayIndexConfigInvalidExpressions { + [self expectException: NSInvalidArgumentException in:^{ + (void) [[CBLArrayIndexConfiguration alloc] initWithPath:@"contacts" expressions: @[]]; + }]; +} + +/** + 2. TestCreateArrayIndexWithPath + Description + Test that creating an array index with only path works as expected. + Steps + 1. Load profiles.json into the collection named "_default.profiles". + 2. Create a ArrayIndexConfiguration object. + path: "contacts" + expressions: null + 3. Create an array index named "contacts" in the profiles collection. + 4. Get index names from the profiles collection and check that the index named "contacts" exists. + 5. Get info of the index named "contacts" using an internal API and check that the index has path and expressions as configured. + */ + +- (void) testCreateArrayIndexWithPath { + NSError* err; + CBLCollection* profiles = [self.db createCollectionWithName: @"profiles" scope: nil error: &err]; + [self loadJSONResource: @"profiles_100" toCollection: profiles]; + + CBLArrayIndexConfiguration* config = [[CBLArrayIndexConfiguration alloc] initWithPath: @"contacts" expressions: nil]; + [profiles createIndexWithName: @"contacts" config: config error: &err]; + NSArray* indexes = [profiles _indexesInfo: nil]; + AssertEqual(indexes.count, 1u); + AssertEqualObjects(indexes[0][@"expr"], @""); + + //AssertEqual([[profiles indexWithName: @"contacts" error: &err] _unnestPathForIndex], @"contacts"); +} + +/** + 3. TestCreateArrayIndexWithPathAndExpressions + Description + Test that creating an array index with path and expressions works as expected. + Steps + 1. Load profiles.json into the collection named "_default.profiles". + 2. Create a ArrayIndexConfiguration object. + path: "contacts" + expressions: ["address.city", "address.state"] + 3. Create an array index named "contacts" in the profiles collection. + 4. Get index names from the profiles collection and check that the index named "contacts" exists. + 5. Get info of the index named "contacts" using an internal API and check that the index has path and expressions as configured. + */ +- (void) testCreateArrayIndexWithPathAndExpressions { + NSError* err; + CBLCollection* profiles = [self.db createCollectionWithName: @"profiles" scope: nil error: &err]; + [self loadJSONResource: @"profiles_100" toCollection: profiles]; + + CBLArrayIndexConfiguration* config = [[CBLArrayIndexConfiguration alloc] initWithPath: @"contacts" expressions: @[@"address.city", @"address.state"]]; + [profiles createIndexWithName: @"contacts" config: config error: &err]; + + NSArray* indexes = [profiles _indexesInfo: nil]; + AssertEqual(indexes.count, 1u); + AssertEqualObjects(indexes[0][@"expr"], @"address.city,address.state"); + + // AssertEqual([[profiles indexWithName: @"contacts" error: &err] _unnestPathForIndex], @"contacts"); +} + +@end