diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9217de23..fb19a297ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements * Added `SyncConfiguration.initialSubscriptions` which describes the initial subscription configuration that was passed when constructing the `SyncConfiguration`. ([#8548](https://github.com/realm/realm-swift/issues/8548)) +* Added support for storing nested collections (List and Map not ManagedSet) in a `AnyRealmValue`. + ```swift + class MixedObject: Object { + @Persisted var anyValue: AnyRealmValue + } + + // You can build a AnyRealmValue from a Swift's Dictionary. + let dictionary: Dictionary = ["key1": .string("hello"), "key2": .bool(false)] + + // You can build a AnyRealmValue from a Swift's Map + // and nest a collection within another collection. + let list: Array = [.int(12), .double(14.17), AnyRealmValue.fromDictionary(dictionary)] + + let realm = realmWithTestPath() + try realm.write { + let obj = MixedObject() + obj.anyValue = AnyRealmValue.fromArray(list) + realm.add(o) + } + ``` +* Added new operators to Swift's Query API for supporting querying nested collections. + ```swift + realm.objects(MixedObject.self).where { $0.anyValue[0][0][1] == .double(76.54) } + ``` + + The `.all` operator allows looking up in all keys or indexes. + ```swift + realm.objects(MixedObject.self).where { $0.anyValue["key"].all == .bool(false) } + ``` ### Fixed * ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?) diff --git a/Package.swift b/Package.swift index accb03a8dc..ca7c30138f 100644 --- a/Package.swift +++ b/Package.swift @@ -153,7 +153,7 @@ let package = Package( targets: ["Realm", "RealmSwift"]), ], dependencies: [ - .package(url: "https://github.com/realm/realm-core.git", exact: coreVersion) + .package(url: "https://github.com/realm/realm-core.git", branch:"je/nested-collection-accessor") ], targets: [ .target( diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index 5d9f090e83..82b6715fd5 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -350,6 +350,7 @@ AC3B33AE29DC6CEE0042F3A0 /* RLMLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3B33AB29DC6CEE0042F3A0 /* RLMLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3B33AF29DC6CEE0042F3A0 /* RLMLogger.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3B33AC29DC6CEE0042F3A0 /* RLMLogger.mm */; }; AC3B33B029DC6CEE0042F3A0 /* RLMLogger_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3B33AD29DC6CEE0042F3A0 /* RLMLogger_Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + AC5300722BD03D4A00BF5950 /* MixedCollectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5300712BD03D4900BF5950 /* MixedCollectionTest.swift */; }; AC7825B92ACD90BE007ABA4B /* Geospatial.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC7825B82ACD90BE007ABA4B /* Geospatial.swift */; }; AC7825BD2ACD90DA007ABA4B /* RLMGeospatial.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC7825BA2ACD90DA007ABA4B /* RLMGeospatial.mm */; }; AC7825BF2ACD90DA007ABA4B /* RLMGeospatial.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7825BC2ACD90DA007ABA4B /* RLMGeospatial.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -900,6 +901,7 @@ AC3B33AB29DC6CEE0042F3A0 /* RLMLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMLogger.h; sourceTree = ""; }; AC3B33AC29DC6CEE0042F3A0 /* RLMLogger.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMLogger.mm; sourceTree = ""; }; AC3B33AD29DC6CEE0042F3A0 /* RLMLogger_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMLogger_Private.h; sourceTree = ""; }; + AC5300712BD03D4900BF5950 /* MixedCollectionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixedCollectionTest.swift; sourceTree = ""; }; AC7825B82ACD90BE007ABA4B /* Geospatial.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Geospatial.swift; sourceTree = ""; }; AC7825BA2ACD90DA007ABA4B /* RLMGeospatial.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMGeospatial.mm; sourceTree = ""; }; AC7825BB2ACD90DA007ABA4B /* RLMGeospatial_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RLMGeospatial_Private.hpp; sourceTree = ""; }; @@ -1414,6 +1416,7 @@ 5D6610001BE98D880021E04F /* ListTests.swift */, 3F1D8D75265B075000593ABA /* MapTests.swift */, 5D6610011BE98D880021E04F /* MigrationTests.swift */, + AC5300712BD03D4900BF5950 /* MixedCollectionTest.swift */, 3F4E0FF82654765C008B8C0B /* ModernKVOTests.swift */, 3F4E100E2655CA33008B8C0B /* ModernObjectAccessorTests.swift */, 3FA5E94C266064C4008F1345 /* ModernObjectCreationTests.swift */, @@ -2612,6 +2615,7 @@ 5D6610161BE98D880021E04F /* ListTests.swift in Sources */, 3F1D8D77265B075100593ABA /* MapTests.swift in Sources */, 3F8824FD1E5E335000586B35 /* MigrationTests.swift in Sources */, + AC5300722BD03D4A00BF5950 /* MixedCollectionTest.swift in Sources */, 3F4E0FF92654765C008B8C0B /* ModernKVOTests.swift in Sources */, 3F4E10102655CA33008B8C0B /* ModernObjectAccessorTests.swift in Sources */, 3FA5E94D266064C4008F1345 /* ModernObjectCreationTests.swift in Sources */, diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index ea68afc070..106c44ea11 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -80,6 +80,8 @@ private func bsonType(_ type: PropertyType) -> String { case .objectId: return "objectId" case .string: return "string" case .linkingObjects: return "linkingObjects" + default: // Types must be added when tested for sync + fatalError() } } diff --git a/Realm/RLMAccessor.hpp b/Realm/RLMAccessor.hpp index 30d42492eb..2d45e27875 100644 --- a/Realm/RLMAccessor.hpp +++ b/Realm/RLMAccessor.hpp @@ -135,6 +135,7 @@ class RLMAccessorContext : public RLMStatelessAccessorContext { RLMAccessorContext(RLMObjectBase *parentObject, const realm::Property *property = nullptr); RLMAccessorContext(RLMObjectBase *parentObject, realm::ColKey); RLMAccessorContext(RLMClassInfo& info); + RLMAccessorContext(RLMClassInfo& parentInfo, RLMClassInfo& info); // The property currently being accessed; needed for KVO things for boxing // List and Results diff --git a/Realm/RLMAccessor.mm b/Realm/RLMAccessor.mm index db7598738e..9d6ce1e84e 100644 --- a/Realm/RLMAccessor.mm +++ b/Realm/RLMAccessor.mm @@ -33,9 +33,11 @@ #import "RLMSwiftProperty.h" #import "RLMUUID_Private.hpp" #import "RLMUtil.hpp" +#import "RLMValue.h" -#import +#import #import +#import #import #import @@ -126,17 +128,19 @@ void setValueOrNull(__unsafe_unretained RLMObjectBase *const obj, ColKey col, RLMVerifyInWriteTransaction(obj); RLMTranslateError([&] { - if (value) { - if constexpr (std::is_same_v) { - obj->_row.set(col, RLMObjcToMixed(value, obj->_realm, realm::CreatePolicy::SetLink)); - } - else { + if constexpr (std::is_same_v) { + realm::Object o(obj->_realm->_realm, *obj->_info->objectSchema, obj->_row); + RLMAccessorContext ctx(obj); + RLMProperty *property = obj->_info->propertyForTableColumn(col); + o.set_property_value(ctx, getProperty(obj, property).name, value ?: NSNull.null); + } else { + if (value) { RLMStatelessAccessorContext ctx; obj->_row.set(col, ctx.unbox(value)); } - } - else { - obj->_row.set_null(col); + else { + obj->_row.set_null(col); + } } }); } @@ -379,6 +383,9 @@ id managedGetter(RLMProperty *prop, const char *type) { }; case RLMPropertyTypeUUID: return makeWrapperGetter(index, prop.optional); + case RLMPropertyTypeDictionary: + case RLMPropertyTypeList: + REALM_UNREACHABLE(); } } @@ -404,6 +411,13 @@ void kvoSetValue(__unsafe_unretained RLMObjectBase *const obj, NSUInteger index, setValue(obj, key, static_cast(value)); } +template<> +void kvoSetValue>(__unsafe_unretained RLMObjectBase *const obj, NSUInteger index, id value) { + RLMVerifyInWriteTransaction(obj); + auto& prop = getProperty(obj, index); + setValue(obj, prop.column_key, static_cast>(value)); +} + template id makeSetter(__unsafe_unretained RLMProperty *const prop) { if (prop.isPrimary) { @@ -454,6 +468,9 @@ id managedSetter(RLMProperty *prop, const char *type) { case RLMPropertyTypeObjectId: return makeSetter(prop); case RLMPropertyTypeDecimal128: return makeSetter(prop); case RLMPropertyTypeUUID: return makeSetter(prop); + case RLMPropertyTypeDictionary: + case RLMPropertyTypeList: + REALM_UNREACHABLE(); } } @@ -821,13 +838,8 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16 , _parentObject(obj) , _parentObjectInfo(&parent._info) , _colKey(property.column_key) -{ -} - -RLMAccessorContext::RLMAccessorContext(RLMClassInfo& info) -: _realm(info.realm), _info(info) -{ -} + { + } RLMAccessorContext::RLMAccessorContext(__unsafe_unretained RLMObjectBase *const parent, const realm::Property *prop) @@ -850,6 +862,18 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16 { } +RLMAccessorContext::RLMAccessorContext(RLMClassInfo& info) +: _realm(info.realm), _info(info) +{ +} + +RLMAccessorContext::RLMAccessorContext(RLMClassInfo& parentInfo, RLMClassInfo& info) +: _realm(info.realm) +, _info(info) +, _parentObjectInfo(&parentInfo) +{ +} + id RLMAccessorContext::defaultValue(__unsafe_unretained NSString *const key) { if (!_defaultValues) { _defaultValues = RLMDefaultValuesForObjectSchema(_info.rlmObjectSchema); @@ -890,15 +914,17 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16 } id RLMAccessorContext::box(realm::Mixed v) { - return RLMMixedToObjc(v, _realm, &_info); + auto property = (currentProperty) ? currentProperty : _info.propertyForTableColumn(_colKey); + return RLMMixedToObjc(v, _realm, &_info, property, _parentObject); } id RLMAccessorContext::box(realm::List&& l) { REALM_ASSERT(_parentObjectInfo); - REALM_ASSERT(currentProperty); + auto property = currentProperty ? currentProperty : _info.propertyForTableColumn(_colKey); + REALM_ASSERT(property); return [[RLMManagedArray alloc] initWithBackingCollection:std::move(l) parentInfo:_parentObjectInfo - property:currentProperty]; + property:property]; } id RLMAccessorContext::box(realm::object_store::Set&& s) { @@ -911,10 +937,11 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16 id RLMAccessorContext::box(realm::object_store::Dictionary&& d) { REALM_ASSERT(_parentObjectInfo); - REALM_ASSERT(currentProperty); + auto property = currentProperty ? currentProperty : _info.propertyForTableColumn(_colKey); + REALM_ASSERT(property); return [[RLMManagedDictionary alloc] initWithBackingCollection:std::move(d) parentInfo:_parentObjectInfo - property:currentProperty]; + property:property]; } id RLMAccessorContext::box(realm::Object&& o) { @@ -1100,7 +1127,7 @@ static auto toOptional(__unsafe_unretained id const value) { try { realm::Object::create(*this, _realm->_realm, *_info.objectSchema, - (id)value, policy, existingKey, outObj); + realm::util::any_cast(value), policy, existingKey, outObj); } catch (std::exception const& e) { @throw RLMException(e); diff --git a/Realm/RLMArray.h b/Realm/RLMArray.h index b2c6c16643..ea79242a15 100644 --- a/Realm/RLMArray.h +++ b/Realm/RLMArray.h @@ -17,9 +17,11 @@ //////////////////////////////////////////////////////////////////////////// #import +#import RLM_HEADER_AUDIT_BEGIN(nullability, sendability) +@protocol RLMValue; @class RLMObject, RLMResults; /** @@ -55,7 +57,7 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) object. Instead, you can call the mutation methods on the `RLMArray` directly. */ -@interface RLMArray : NSObject +@interface RLMArray : NSObject #pragma mark - Properties diff --git a/Realm/RLMArray.mm b/Realm/RLMArray.mm index ae34c64a71..b5e8f02348 100644 --- a/Realm/RLMArray.mm +++ b/Realm/RLMArray.mm @@ -69,7 +69,7 @@ - (instancetype)initWithObjectType:(RLMPropertyType)type optional:(BOOL)optional - (void)setParent:(RLMObjectBase *)parentObject property:(RLMProperty *)property { _parentObject = parentObject; - _key = property.name; + _property = property; _isLegacyProperty = property.isLegacy; } @@ -192,9 +192,9 @@ static void changeArray(__unsafe_unretained RLMArray *const ar, if (RLMObjectBase *parent = ar->_parentObject) { NSIndexSet *indexes = is(); - [parent willChange:kind valuesAtIndexes:indexes forKey:ar->_key]; + [parent willChange:kind valuesAtIndexes:indexes forKey:ar->_property.name]; f(); - [parent didChange:kind valuesAtIndexes:indexes forKey:ar->_key]; + [parent didChange:kind valuesAtIndexes:indexes forKey:ar->_property.name]; } else { f(); @@ -608,7 +608,7 @@ - (NSString *)descriptionWithMaxDepth:(NSUInteger)depth { #pragma mark - Key Path Strings - (NSString *)propertyKey { - return _key; + return _property.name; } @end diff --git a/Realm/RLMArray_Private.hpp b/Realm/RLMArray_Private.hpp index 332b83cd7b..4af07080fc 100644 --- a/Realm/RLMArray_Private.hpp +++ b/Realm/RLMArray_Private.hpp @@ -39,7 +39,7 @@ class RLMObservationInfo; BOOL _optional; @public // The name of the property which this RLMArray represents - NSString *_key; + RLMProperty *_property; __weak RLMObjectBase *_parentObject; } @end diff --git a/Realm/RLMCollection.mm b/Realm/RLMCollection.mm index 6826e302db..e7d2c813a1 100644 --- a/Realm/RLMCollection.mm +++ b/Realm/RLMCollection.mm @@ -45,6 +45,8 @@ @implementation RLMFastEnumerator { RLMRealm *_realm; RLMClassInfo *_info; + RLMClassInfo *_parentInfo; + RLMProperty *_property; // A pointer to either _snapshot or a Results from the source collection, // to avoid having to copy the Results when not in a write transaction @@ -58,11 +60,15 @@ @implementation RLMFastEnumerator { - (instancetype)initWithBackingCollection:(realm::object_store::Collection const&)backingCollection collection:(id)collection - classInfo:(RLMClassInfo&)info { + classInfo:(RLMClassInfo&)info + parentInfo:(RLMClassInfo&)parentInfo + property:(RLMProperty *)property { self = [super init]; if (self) { _info = &info; _realm = _info->realm; + _parentInfo = &parentInfo; + _property = property; if (_realm.inWriteTransaction) { _snapshot = backingCollection.as_results().snapshot(); @@ -79,11 +85,15 @@ - (instancetype)initWithBackingCollection:(realm::object_store::Collection const - (instancetype)initWithBackingDictionary:(realm::object_store::Dictionary const&)backingDictionary dictionary:(RLMManagedDictionary *)dictionary - classInfo:(RLMClassInfo&)info { + classInfo:(RLMClassInfo&)info + parentInfo:(RLMClassInfo&)parentInfo + property:(RLMProperty *)property { self = [super init]; if (self) { _info = &info; _realm = _info->realm; + _parentInfo = &parentInfo; + _property = property; if (_realm.inWriteTransaction) { _snapshot = backingDictionary.get_keys().snapshot(); @@ -146,10 +156,19 @@ - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state NSUInteger batchCount = 0, count = state->extra[1]; @autoreleasepool { - RLMAccessorContext ctx(*_info); - for (NSUInteger index = state->state; index < count && batchCount < len; ++index) { - _strongBuffer[batchCount] = _results->get(ctx, index); - batchCount++; + if (!_parentInfo) { + RLMAccessorContext ctx = RLMAccessorContext(*_info); + for (NSUInteger index = state->state; index < count && batchCount < len; ++index) { + _strongBuffer[batchCount] = _results->get(ctx, index); + batchCount++; + } + } else { + RLMAccessorContext ctx = RLMAccessorContext(*_parentInfo, *_info); + ctx.currentProperty = _property; + for (NSUInteger index = state->state; index < count && batchCount < len; ++index) { + _strongBuffer[batchCount] = _results->get(ctx, index); + batchCount++; + } } } diff --git a/Realm/RLMCollection_Private.hpp b/Realm/RLMCollection_Private.hpp index 565dfa84ae..aee0e28517 100644 --- a/Realm/RLMCollection_Private.hpp +++ b/Realm/RLMCollection_Private.hpp @@ -63,11 +63,15 @@ RLM_DIRECT_MEMBERS @interface RLMFastEnumerator : NSObject - (instancetype)initWithBackingCollection:(realm::object_store::Collection const&)backingCollection collection:(id)collection - classInfo:(RLMClassInfo&)info; + classInfo:(RLMClassInfo&)info + parentInfo:(RLMClassInfo&)parentInfo + property:(RLMProperty *)property; - (instancetype)initWithBackingDictionary:(realm::object_store::Dictionary const&)backingDictionary dictionary:(RLMManagedDictionary *)dictionary - classInfo:(RLMClassInfo&)info; + classInfo:(RLMClassInfo&)info + parentInfo:(RLMClassInfo&)parentInfo + property:(RLMProperty *)property; - (instancetype)initWithResults:(realm::Results&)results collection:(id)collection diff --git a/Realm/RLMConstants.h b/Realm/RLMConstants.h index 0bc8db972c..f2f32158eb 100644 --- a/Realm/RLMConstants.h +++ b/Realm/RLMConstants.h @@ -84,6 +84,8 @@ typedef RLM_CLOSED_ENUM(int32_t, RLMPropertyType) { RLMPropertyTypeAny = 9, /** Dates: `NSDate` */ RLMPropertyTypeDate = 4, + RLMPropertyTypeObjectId = 10, + RLMPropertyTypeDecimal128 = 11, #pragma mark - Linked object types @@ -92,8 +94,12 @@ typedef RLM_CLOSED_ENUM(int32_t, RLMPropertyType) { /** Realm linking objects. See [Realm Models](https://www.mongodb.com/docs/realm/sdk/swift/fundamentals/relationships/#inverse-relationship) for more information. */ RLMPropertyTypeLinkingObjects = 8, - RLMPropertyTypeObjectId = 10, - RLMPropertyTypeDecimal128 = 11 +#pragma mark - Collection types + + /** Dictionary: `RLMDictionary`, `Map` (Swift) */ + RLMPropertyTypeDictionary = 512, + /** Set: `RLMArray`, `List` (Swift) */ + RLMPropertyTypeList = 128 }; #pragma mark - Notification Constants diff --git a/Realm/RLMDictionary.h b/Realm/RLMDictionary.h index 9c8cac5386..e4dca92833 100644 --- a/Realm/RLMDictionary.h +++ b/Realm/RLMDictionary.h @@ -17,9 +17,11 @@ //////////////////////////////////////////////////////////////////////////// #import +#import RLM_HEADER_AUDIT_BEGIN(nullability, sendability) +@protocol RLMValue; @class RLMObject, RLMResults, RLMDictionaryChange; /** @@ -49,7 +51,7 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) key-value observing compliant when the `RLMDictionary` is attached to a managed `RLMObject` (`RLMDictionary`s on unmanaged `RLMObject`s will never become invalidated). */ -@interface RLMDictionary: NSObject +@interface RLMDictionary: NSObject #pragma mark - Properties diff --git a/Realm/RLMDictionary.mm b/Realm/RLMDictionary.mm index 650a011e68..2d7dd96ca5 100644 --- a/Realm/RLMDictionary.mm +++ b/Realm/RLMDictionary.mm @@ -67,7 +67,7 @@ - (instancetype)initWithObjectType:(RLMPropertyType)type optional:(BOOL)optional - (void)setParent:(RLMObjectBase *)parentObject property:(RLMProperty *)property { _parentObject = parentObject; - _key = property.name; + _property = property; _isLegacyProperty = property.isLegacy; } @@ -133,9 +133,9 @@ static void changeDictionary(__unsafe_unretained RLMDictionary *const dictionary dictionary->_backingCollection = [NSMutableDictionary new]; } if (RLMObjectBase *parent = dictionary->_parentObject) { - [parent willChangeValueForKey:dictionary->_key]; + [parent willChangeValueForKey:dictionary->_property.name]; f(); - [parent didChangeValueForKey:dictionary->_key]; + [parent didChangeValueForKey:dictionary->_property.name]; } else { f(); @@ -425,7 +425,7 @@ - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath #pragma mark - Key Path Strings - (NSString *)propertyKey { - return _key; + return _property.name; } #pragma mark - Methods unsupported on unmanaged RLMDictionary instances diff --git a/Realm/RLMDictionary_Private.hpp b/Realm/RLMDictionary_Private.hpp index f294dbacdb..5e7b02a964 100644 --- a/Realm/RLMDictionary_Private.hpp +++ b/Realm/RLMDictionary_Private.hpp @@ -39,7 +39,7 @@ class RLMObservationInfo; BOOL _optional; @public // The name of the property which this RLMDictionary represents - NSString *_key; + RLMProperty *_property; __weak RLMObjectBase *_parentObject; } @end diff --git a/Realm/RLMManagedArray.mm b/Realm/RLMManagedArray.mm index 44aee52a6f..fba4e9c210 100644 --- a/Realm/RLMManagedArray.mm +++ b/Realm/RLMManagedArray.mm @@ -20,6 +20,7 @@ #import "RLMAccessor.hpp" #import "RLMCollection_Private.hpp" +#import "RLMDictionary_Private.hpp" #import "RLMObjectSchema_Private.hpp" #import "RLMObjectStore.h" #import "RLMObject_Private.hpp" @@ -33,6 +34,7 @@ #import "RLMThreadSafeReference_Private.hpp" #import "RLMUtil.hpp" +#import #import #import #import @@ -76,24 +78,15 @@ - (RLMManagedArray *)initWithBackingCollection:(realm::List)list REALM_ASSERT(list.get_realm() == _realm->_realm); _backingList = std::move(list); _ownerInfo = parentInfo; + _property = property; if (property.type == RLMPropertyTypeObject) _objectInfo = &parentInfo->linkTargetType(property.index); else _objectInfo = _ownerInfo; - _key = property.name; } return self; } -- (RLMManagedArray *)initWithParent:(__unsafe_unretained RLMObjectBase *const)parentObject - property:(__unsafe_unretained RLMProperty *const)property { - __unsafe_unretained RLMRealm *const realm = parentObject->_realm; - auto col = parentObject->_info->tableColumn(property); - return [self initWithBackingCollection:realm::List(realm->_realm, parentObject->_row, col) - parentInfo:parentObject->_info - property:property]; -} - - (RLMManagedArray *)initWithParent:(realm::Obj)parent property:(__unsafe_unretained RLMProperty *const)property parentInfo:(RLMClassInfo&)info { @@ -103,6 +96,13 @@ - (RLMManagedArray *)initWithParent:(realm::Obj)parent property:property]; } +- (RLMManagedArray *)initWithParent:(__unsafe_unretained RLMObjectBase *const)parentObject + property:(__unsafe_unretained RLMProperty *const)property { + return [self initWithParent:parentObject->_row + property:property + parentInfo:*parentObject->_info]; +} + void RLMValidateArrayObservationKey(__unsafe_unretained NSString *const keyPath, __unsafe_unretained RLMArray *const array) { if (![keyPath isEqualToString:RLMInvalidatedKey]) { @@ -141,7 +141,7 @@ static void changeArray(__unsafe_unretained RLMManagedArray *const ar, ar->_backingList.get_parent_object_key(), *ar->_ownerInfo); if (obsInfo) { - tracker.willChange(obsInfo, ar->_key, kind, is()); + tracker.willChange(obsInfo, ar->_property.name, kind, is()); } translateErrors(f); @@ -198,9 +198,19 @@ - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state } - (id)objectAtIndex:(NSUInteger)index { - return translateErrors([&] { - RLMAccessorContext context(*_objectInfo); - return _backingList.get(context, index); + return translateErrors([&]() -> id { + realm::Mixed mixed = _backingList.get_any(index); + if (mixed.is_type(realm::type_Dictionary)) { + return [[RLMManagedDictionary alloc] initWithBackingCollection:_backingList.get_dictionary(index) parentInfo:_ownerInfo property:_property]; + } + else if (mixed.is_type(realm::type_List)) { + return [[RLMManagedArray alloc] initWithBackingCollection:_backingList.get_list(index) parentInfo:_ownerInfo property:_property]; + } + else { + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; + return _backingList.get(context, index); + } }); } @@ -211,7 +221,8 @@ static void RLMInsertObject(RLMManagedArray *ar, id object, NSUInteger index) { } changeArray(ar, NSKeyValueChangeInsertion, index, ^{ - RLMAccessorContext context(*ar->_objectInfo); + RLMAccessorContext context(*ar->_ownerInfo, *ar->_objectInfo); + context.currentProperty = ar->_property; ar->_backingList.insert(context, index, object); }); } @@ -227,7 +238,8 @@ - (void)insertObject:(id)object atIndex:(NSUInteger)index { - (void)insertObjects:(id)objects atIndexes:(NSIndexSet *)indexes { changeArray(self, NSKeyValueChangeInsertion, indexes, ^{ NSUInteger index = [indexes firstIndex]; - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; for (id obj in objects) { RLMArrayValidateMatchingObjectType(self, obj); _backingList.insert(context, index, obj); @@ -252,7 +264,8 @@ - (void)removeObjectsAtIndexes:(NSIndexSet *)indexes { - (void)addObjectsFromArray:(NSArray *)array { changeArray(self, NSKeyValueChangeInsertion, NSMakeRange(self.count, array.count), ^{ - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; for (id obj in array) { RLMArrayValidateMatchingObjectType(self, obj); _backingList.add(context, obj); @@ -276,7 +289,8 @@ - (void)replaceAllObjectsWithObjects:(NSArray *)objects { return; } changeArray(self, NSKeyValueChangeInsertion, NSMakeRange(0, objects.count), ^{ - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; _backingList.assign(context, objects); }); } @@ -284,7 +298,8 @@ - (void)replaceAllObjectsWithObjects:(NSArray *)objects { - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object { RLMArrayValidateMatchingObjectType(self, object); changeArray(self, NSKeyValueChangeReplacement, index, ^{ - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; if (index >= _backingList.size()) { @throw RLMException(@"Index %llu is out of bounds (must be less than %llu).", (unsigned long long)index, (unsigned long long)_backingList.size()); @@ -314,7 +329,8 @@ - (void)exchangeObjectAtIndex:(NSUInteger)index1 withObjectAtIndex:(NSUInteger)i - (NSUInteger)indexOfObject:(id)object { RLMArrayValidateMatchingObjectType(self, object); return translateErrors([&] { - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; return RLMConvertNotFound(_backingList.find(context, object)); }); } @@ -350,7 +366,8 @@ - (id)valueForKey:(NSString *)key { - (void)setValue:(id)value forKey:(NSString *)key { if ([key isEqualToString:@"self"]) { RLMArrayValidateMatchingObjectType(self, value); - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; translateErrors([&] { for (size_t i = 0, count = _backingList.size(); i < count; ++i) { _backingList.set(context, i, value); @@ -439,7 +456,8 @@ - (NSArray *)objectsAtIndexes:(NSIndexSet *)indexes { size_t c = self.count; NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:indexes.count]; NSUInteger i = [indexes firstIndex]; - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; while (i != NSNotFound) { // Given KVO relies on `objectsAtIndexes` we need to make sure // that no out of bounds exceptions are generated. This disallows us to mirror @@ -484,7 +502,9 @@ - (RLMFastEnumerator *)fastEnumerator { return translateErrors([&] { return [[RLMFastEnumerator alloc] initWithBackingCollection:_backingList collection:self - classInfo:*_objectInfo]; + classInfo:*_objectInfo + parentInfo:*_ownerInfo + property:_property]; }); } @@ -495,9 +515,9 @@ - (BOOL)isFrozen { - (instancetype)resolveInRealm:(RLMRealm *)realm { auto& parentInfo = _ownerInfo->resolve(realm); return translateErrors([&] { - return [[self.class alloc] initWithBackingCollection:_backingList.freeze(realm->_realm) + return [[RLMManagedArray alloc] initWithBackingCollection:_backingList.freeze(realm->_realm) parentInfo:&parentInfo - property:parentInfo.rlmObjectSchema[_key]]; + property:parentInfo.rlmObjectSchema[_property.name]]; }); } @@ -529,7 +549,7 @@ - (instancetype)thaw { - (RLMManagedArrayHandoverMetadata *)objectiveCMetadata { RLMManagedArrayHandoverMetadata *metadata = [[RLMManagedArrayHandoverMetadata alloc] init]; metadata.parentClassName = _ownerInfo->rlmObjectSchema.className; - metadata.key = _key; + metadata.key = _property.name; return metadata; } diff --git a/Realm/RLMManagedDictionary.mm b/Realm/RLMManagedDictionary.mm index fa3c1ec7e2..c09c72289c 100644 --- a/Realm/RLMManagedDictionary.mm +++ b/Realm/RLMManagedDictionary.mm @@ -19,6 +19,7 @@ #import "RLMDictionary_Private.hpp" #import "RLMAccessor.hpp" +#import "RLMArray_Private.hpp" #import "RLMCollection_Private.hpp" #import "RLMObjectSchema_Private.hpp" #import "RLMObjectStore.h" @@ -32,6 +33,7 @@ #import "RLMThreadSafeReference_Private.hpp" #import "RLMUtil.hpp" +#import #import #import #import @@ -94,6 +96,7 @@ @implementation RLMManagedDictionary { RLMRealm *_realm; RLMClassInfo *_objectInfo; RLMClassInfo *_ownerInfo; + RLMProperty *_property; std::unique_ptr _observationInfo; } @@ -102,6 +105,11 @@ - (RLMManagedDictionary *)initWithBackingCollection:(realm::object_store::Dictio property:(__unsafe_unretained RLMProperty *const)property { if (property.type == RLMPropertyTypeObject) self = [self initWithObjectClassName:property.objectClassName keyType:property.dictionaryKeyType]; + else if (property.type == RLMPropertyTypeAny) + // Because the property is type mixed and we don't know if it will contain a dictionary when the schema + // is created, we set RLMPropertyTypeString by default. + // If another type is used for the dictionary key in a mixed dictionary context, this will thrown by core. + self = [self initWithObjectType:property.type optional:property.optional keyType:RLMPropertyTypeString]; else self = [self initWithObjectType:property.type optional:property.optional keyType:property.dictionaryKeyType]; if (self) { @@ -113,20 +121,11 @@ - (RLMManagedDictionary *)initWithBackingCollection:(realm::object_store::Dictio _objectInfo = &parentInfo->linkTargetType(property.index); else _objectInfo = _ownerInfo; - _key = property.name; + _property = property; } return self; } -- (RLMManagedDictionary *)initWithParent:(__unsafe_unretained RLMObjectBase *const)parentObject - property:(__unsafe_unretained RLMProperty *const)property { - __unsafe_unretained RLMRealm *const realm = parentObject->_realm; - auto col = parentObject->_info->tableColumn(property); - return [self initWithBackingCollection:realm::object_store::Dictionary(realm->_realm, parentObject->_row, col) - parentInfo:parentObject->_info - property:property]; -} - - (RLMManagedDictionary *)initWithParent:(realm::Obj)parent property:(__unsafe_unretained RLMProperty *const)property parentInfo:(RLMClassInfo&)info { @@ -136,6 +135,13 @@ - (RLMManagedDictionary *)initWithParent:(realm::Obj)parent property:property]; } +- (RLMManagedDictionary *)initWithParent:(__unsafe_unretained RLMObjectBase *const)parentObject + property:(__unsafe_unretained RLMProperty *const)property { + return [self initWithParent:parentObject->_row + property:property + parentInfo:*parentObject->_info]; +} + void RLMDictionaryValidateObservationKey(__unsafe_unretained NSString *const keyPath, __unsafe_unretained RLMDictionary *const dictionary) { if (![keyPath isEqualToString:RLMInvalidatedKey]) { @@ -176,7 +182,7 @@ static void changeDictionary(__unsafe_unretained RLMManagedDictionary *const dic dict->_backingCollection.get_parent_object_key(), *dict->_ownerInfo); if (obsInfo) { - tracker.willChange(obsInfo, dict->_key); + tracker.willChange(obsInfo, dict->_property.name); } translateErrors(f); @@ -244,18 +250,29 @@ - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state - (nullable id)objectForKey:(id)key { return translateErrors([&]() -> id { [self.realm verifyThread]; - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; if (auto value = _backingCollection.try_get_any(context.unbox(key))) { - return context.box(*value); + if (value->is_type(realm::type_Dictionary)) { + return [[RLMManagedDictionary alloc] initWithBackingCollection:_backingCollection.get_dictionary(context.unbox(key)) parentInfo:_ownerInfo property:_property]; + } + else if (value->is_type(realm::type_List)) { + return [[RLMManagedArray alloc] initWithBackingCollection:_backingCollection.get_list(context.unbox(key)) parentInfo:_ownerInfo property:_property]; + } + else { + return context.box(*value); + } } + return nil; }); } - (void)setObject:(id)obj forKey:(id)key { changeDictionary(self, ^{ - RLMAccessorContext c(*_objectInfo); - _backingCollection.insert(c, c.unbox(RLMDictionaryKey(self, key)), + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; + _backingCollection.insert(context, context.unbox(RLMDictionaryKey(self, key)), RLMDictionaryValue(self, obj)); }); } @@ -267,7 +284,8 @@ - (void)removeAllObjects { } - (void)removeObjectsForKeys:(NSArray *)keyArray { - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; changeDictionary(self, [&] { for (id key in keyArray) { _backingCollection.try_erase(context.unbox(key)); @@ -277,17 +295,19 @@ - (void)removeObjectsForKeys:(NSArray *)keyArray { - (void)removeObjectForKey:(id)key { changeDictionary(self, ^{ - RLMAccessorContext context(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; _backingCollection.try_erase(context.unbox(key)); }); } - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block { - RLMAccessorContext c(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; BOOL stop = false; @autoreleasepool { for (auto&& [key, value] : _backingCollection) { - block(c.box(key), c.box(value), &stop); + block(context.box(key), [self objectForKey:context.box(key)], &stop); if (stop) { break; } @@ -306,12 +326,13 @@ - (void)mergeDictionary:(id)dictionary clear:(bool)clear { } changeDictionary(self, ^{ - RLMAccessorContext c(*_objectInfo); + RLMAccessorContext context(*_ownerInfo, *_objectInfo); + context.currentProperty = _property; if (clear) { _backingCollection.remove_all(); } [dictionary enumerateKeysAndObjectsUsingBlock:[&](id key, id value, BOOL *) { - _backingCollection.insert(c, c.unbox(RLMDictionaryKey(self, key)), + _backingCollection.insert(context, context.unbox(RLMDictionaryKey(self, key)), RLMDictionaryValue(self, value)); }]; }); @@ -443,7 +464,10 @@ - (RLMFastEnumerator *)fastEnumerator { return translateErrors([&] { return [[RLMFastEnumerator alloc] initWithBackingDictionary:_backingCollection dictionary:self - classInfo:*_objectInfo]; + classInfo:*_objectInfo + parentInfo:*_ownerInfo + property:_property + ]; }); } @@ -454,9 +478,9 @@ - (BOOL)isFrozen { - (instancetype)resolveInRealm:(RLMRealm *)realm { auto& parentInfo = _ownerInfo->resolve(realm); return translateErrors([&] { - return [[self.class alloc] initWithBackingCollection:_backingCollection.freeze(realm->_realm) + return [[RLMManagedDictionary alloc] initWithBackingCollection:_backingCollection.freeze(realm->_realm) parentInfo:&parentInfo - property:parentInfo.rlmObjectSchema[_key]]; + property:parentInfo.rlmObjectSchema[_property.name]]; }); } @@ -519,7 +543,7 @@ void operator()(realm::DictionaryChangeSet const& changes) { - (RLMManagedCollectionHandoverMetadata *)objectiveCMetadata { RLMManagedCollectionHandoverMetadata *metadata = [[RLMManagedCollectionHandoverMetadata alloc] init]; metadata.parentClassName = _ownerInfo->rlmObjectSchema.className; - metadata.key = _key; + metadata.key = _property.name; return metadata; } diff --git a/Realm/RLMManagedSet.mm b/Realm/RLMManagedSet.mm index bc03584e2c..90408219e0 100644 --- a/Realm/RLMManagedSet.mm +++ b/Realm/RLMManagedSet.mm @@ -74,11 +74,11 @@ - (RLMManagedSet *)initWithBackingCollection:(realm::object_store::Set)set REALM_ASSERT(set.get_realm() == _realm->_realm); _backingSet = std::move(set); _ownerInfo = parentInfo; + _property = property; if (property.type == RLMPropertyTypeObject) _objectInfo = &parentInfo->linkTargetType(property.index); else _objectInfo = _ownerInfo; - _key = property.name; } return self; } @@ -138,7 +138,7 @@ static void changeSet(__unsafe_unretained RLMManagedSet *const set, set->_backingSet.get_parent_object_key(), *set->_ownerInfo); if (obsInfo) { - tracker.willChange(obsInfo, set->_key); + tracker.willChange(obsInfo, set->_property.name); } translateErrors(f); @@ -482,7 +482,9 @@ - (RLMFastEnumerator *)fastEnumerator { return translateErrors([&] { return [[RLMFastEnumerator alloc] initWithBackingCollection:_backingSet collection:self - classInfo:*_objectInfo]; + classInfo:*_objectInfo + parentInfo:*_ownerInfo + property:_property]; }); } @@ -499,7 +501,7 @@ - (instancetype)resolveInRealm:(RLMRealm *)realm { return translateErrors([&] { return [[self.class alloc] initWithBackingCollection:_backingSet.freeze(realm->_realm) parentInfo:&parentInfo - property:parentInfo.rlmObjectSchema[_key]]; + property:parentInfo.rlmObjectSchema[_property.name]]; }); } @@ -531,7 +533,7 @@ - (instancetype)thaw { - (RLMManagedSetHandoverMetadata *)objectiveCMetadata { RLMManagedSetHandoverMetadata *metadata = [[RLMManagedSetHandoverMetadata alloc] init]; metadata.parentClassName = _ownerInfo->rlmObjectSchema.className; - metadata.key = _key; + metadata.key = _property.name; return metadata; } diff --git a/Realm/RLMProperty_Private.h b/Realm/RLMProperty_Private.h index 6a87ced632..29700221ed 100644 --- a/Realm/RLMProperty_Private.h +++ b/Realm/RLMProperty_Private.h @@ -56,6 +56,10 @@ static inline NSString *RLMTypeToString(RLMPropertyType type) { return @"object id"; case RLMPropertyTypeUUID: return @"uuid"; + case RLMPropertyTypeDictionary: + return @"dictionary"; + case RLMPropertyTypeList: + return @"array"; } return @"Unknown"; } diff --git a/Realm/RLMQueryUtil.hpp b/Realm/RLMQueryUtil.hpp index 12df76e67f..46e4acedc5 100644 --- a/Realm/RLMQueryUtil.hpp +++ b/Realm/RLMQueryUtil.hpp @@ -24,6 +24,7 @@ namespace realm { class Group; class Query; class SortDescriptor; + struct PathElement; } @class RLMObjectSchema, RLMProperty, RLMSchema, RLMSortDescriptor; @@ -37,3 +38,4 @@ realm::Query RLMPredicateToQuery(NSPredicate *predicate, RLMObjectSchema *object // return property - throw for invalid column name RLMProperty *RLMValidatedProperty(RLMObjectSchema *objectSchema, NSString *columnName); +void RLMGetPathElements(std::vector &paths, NSExpression *expression); diff --git a/Realm/RLMQueryUtil.mm b/Realm/RLMQueryUtil.mm index ec3922ae3c..d6fff7e688 100644 --- a/Realm/RLMQueryUtil.mm +++ b/Realm/RLMQueryUtil.mm @@ -30,6 +30,7 @@ #import #import #import +#import #import #import #import @@ -1131,6 +1132,9 @@ void convert_null(T&& value, Fn&& fn) { column.resolve>(), value); }); + case RLMPropertyTypeDictionary: + case RLMPropertyTypeList: + break; } } @@ -1328,7 +1332,7 @@ KeyPath key_path_from_string(RLMSchema *schema, RLMObjectSchema *objectSchema, N ColumnReference QueryBuilder::column_reference_from_key_path(KeyPath&& kp, bool isAggregate) { - if (isAggregate && !kp.containsToManyRelationship) { + if (isAggregate && !kp.containsToManyRelationship && kp.property.type != RLMPropertyTypeAny) { throwException(@"Invalid predicate", @"Aggregate operations can only be used on key paths that include an collection property"); } else if (!isAggregate && kp.containsToManyRelationship) { @@ -1500,6 +1504,9 @@ void get_collection_type(__unsafe_unretained RLMProperty *prop, Fn&& fn) { case RLMPropertyTypeObject: case RLMPropertyTypeLinkingObjects: return add_numeric_constraint(type, operatorType, column.resolve().count(), rhsValue); + case RLMPropertyTypeDictionary: + case RLMPropertyTypeList: + break; } } case CollectionOperation::Minimum: @@ -1684,23 +1691,27 @@ bool is_self_value_for_key_path_function_expression(NSExpression *expression) void QueryBuilder::apply_map_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression, NSComparisonPredicateOptions options, NSPredicateOperatorType operatorType, NSExpression *right) { + std::vector pathElements; + RLMGetPathElements(pathElements, functionExpression); + NSString *keyPath; - NSString *mapKey; - if (functionExpression.operand.expressionType == NSKeyPathExpressionType) { - NSExpression *mapItems = [functionExpression.arguments firstObject]; - NSExpression *linkCol = [[functionExpression.operand arguments] firstObject]; - NSExpression *mapCol = [mapItems.arguments firstObject]; - mapKey = [mapItems.arguments[1] constantValue]; - keyPath = [NSString stringWithFormat:@"%@.%@", linkCol.keyPath, mapCol.keyPath]; - } else { - keyPath = [functionExpression.arguments.firstObject keyPath]; - mapKey = [functionExpression.arguments[1] constantValue]; + NSExpression *keyPathExpression = functionExpression; + for (int i = 0; i < pathElements.size(); i++) { + if (keyPathExpression.arguments[0].expressionType == NSKeyPathExpressionType) { + keyPath = [NSString stringWithFormat:@"%@", keyPathExpression.arguments[0]]; + } else { + keyPathExpression = keyPathExpression.arguments[0]; + } } - ColumnReference collectionColumn = column_reference_from_key_path(key_path_from_string(m_schema, objectSchema, keyPath), true); - RLMPrecondition(collectionColumn.property().dictionary, @"Invalid predicate", - @"Invalid keypath '%@': only dictionaries support subscript predicates.", functionExpression); - add_mixed_constraint(operatorType, options, std::move(collectionColumn.resolve().key(mapKey.UTF8String)), right.constantValue); + + if (collectionColumn.property().type == RLMPropertyTypeAny && !collectionColumn.property().dictionary) { + add_mixed_constraint(operatorType, options, std::move(collectionColumn.resolve().path(pathElements)), right.constantValue); + } else { + RLMPrecondition(collectionColumn.property().dictionary, @"Invalid predicate", + @"Invalid keypath '%@': only dictionaries and mixed support subscript predicates.", functionExpression); + add_mixed_constraint(operatorType, options, std::move(collectionColumn.resolve().key(pathElements[0].get_key())), right.constantValue); + } } void QueryBuilder::apply_function_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression, @@ -1881,3 +1892,30 @@ bool is_self_value_for_key_path_function_expression(NSExpression *expression) @"Property '%@' not found in object of type '%@'", columnName, desc.className); return prop; } + +void RLMGetPathElements(std::vector &paths, NSExpression *expression) { + for (int i = 0; i < 2; i++) { + if ((expression.arguments.count > 1) && (expression.arguments[i].expressionType == NSFunctionExpressionType)) { + RLMGetPathElements(paths, expression.arguments[0]); + } else { + if (expression.arguments[i].expressionType == NSConstantValueExpressionType) { + id value = [expression.arguments[i] constantValue]; + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *index = (NSNumber *)value; + if (index) { + paths.push_back(PathElement{[index intValue]}); + } + } else if ([value isKindOfClass:[NSString class]]) { + NSString *key = (NSString *)value; + if ([key isEqual:@"all"]) { + paths.push_back(PathElement{}); + break; + } + if (key) { + paths.push_back(PathElement{key.UTF8String}); + } + } + } + } + } +} diff --git a/Realm/RLMSet.h b/Realm/RLMSet.h index fead2e0051..70fde81e6e 100644 --- a/Realm/RLMSet.h +++ b/Realm/RLMSet.h @@ -514,7 +514,7 @@ __attribute__((warn_unused_result)); /** `-[RLMSet init]` is not available because `RLMSet`s cannot be created directly. - ``RLMSet` properties on `RLMObject`s are lazily created when accessed. + `RLMSet` properties on `RLMObject`s are lazily created when accessed. */ - (instancetype)init __attribute__((unavailable("RLMSets cannot be created directly"))); diff --git a/Realm/RLMSet.mm b/Realm/RLMSet.mm index bfe44dfd49..975f105371 100644 --- a/Realm/RLMSet.mm +++ b/Realm/RLMSet.mm @@ -70,7 +70,7 @@ - (instancetype)initWithObjectType:(RLMPropertyType)type optional:(BOOL)optional - (void)setParent:(RLMObjectBase *)parentObject property:(RLMProperty *)property { _parentObject = parentObject; - _key = property.name; + _property = property; _isLegacyProperty = property.isLegacy; } @@ -240,9 +240,9 @@ static void changeSet(__unsafe_unretained RLMSet *const set, } if (RLMObjectBase *parent = set->_parentObject) { - [parent willChangeValueForKey:set->_key]; + [parent willChangeValueForKey:set->_property.name]; f(); - [parent didChangeValueForKey:set->_key]; + [parent didChangeValueForKey:set->_property.name]; } else { f(); @@ -484,7 +484,7 @@ void RLMSetValidateMatchingObjectType(__unsafe_unretained RLMSet *const set, #pragma mark - Key Path Strings - (NSString *)propertyKey { - return _key; + return _property.name; } #pragma mark - Methods unsupported on unmanaged RLMSet instances diff --git a/Realm/RLMSet_Private.hpp b/Realm/RLMSet_Private.hpp index 0fde608cc8..cb20d276e2 100644 --- a/Realm/RLMSet_Private.hpp +++ b/Realm/RLMSet_Private.hpp @@ -41,7 +41,7 @@ class RLMObservationInfo; BOOL _optional; @public // The name of the property which this RLMSet represents - NSString *_key; + RLMProperty *_property; __weak RLMObjectBase *_parentObject; } @end diff --git a/Realm/RLMSwiftValueStorage.mm b/Realm/RLMSwiftValueStorage.mm index 80cfc33456..b7e04fc287 100644 --- a/Realm/RLMSwiftValueStorage.mm +++ b/Realm/RLMSwiftValueStorage.mm @@ -72,7 +72,7 @@ void attach(__unsafe_unretained RLMObjectBase *const obj, NSString *property) { : _realm(obj->_realm) , _object(obj->_realm->_realm, *obj->_info->objectSchema, obj->_row) , _columnName(prop.columnName.UTF8String) - , _ctx(*obj->_info) + , _ctx(obj, obj->_info->objectSchema->property_for_name(prop.columnName.UTF8String)) { } diff --git a/Realm/RLMUtil.hpp b/Realm/RLMUtil.hpp index 2fe906e274..57f48f3639 100644 --- a/Realm/RLMUtil.hpp +++ b/Realm/RLMUtil.hpp @@ -203,13 +203,17 @@ static inline void RLMNSStringToStdString(std::string &out, NSString *in) { options:0 range:{0, in.length} remainingRange:nullptr]; out.resize(size); } - -realm::Mixed RLMObjcToMixed(__unsafe_unretained id value, - __unsafe_unretained RLMRealm *realm=nil, +realm::Mixed RLMObjcToMixed(__unsafe_unretained id const value, + __unsafe_unretained RLMRealm *const realm=nil, realm::CreatePolicy createPolicy={}); +realm::Mixed RLMObjcToMixedPrimitives(__unsafe_unretained id const value, + __unsafe_unretained RLMRealm *const realm, + realm::CreatePolicy createPolicy); id RLMMixedToObjc(realm::Mixed const& value, __unsafe_unretained RLMRealm *realm=nil, - RLMClassInfo *classInfo=nullptr); + RLMClassInfo *classInfo=nullptr, + RLMProperty *property=nullptr, + realm::Obj obj={}); realm::Decimal128 RLMObjcToDecimal128(id value); realm::UUID RLMObjcToUUID(__unsafe_unretained id const value); diff --git a/Realm/RLMUtil.mm b/Realm/RLMUtil.mm index f10cd2fbde..8a4149c004 100644 --- a/Realm/RLMUtil.mm +++ b/Realm/RLMUtil.mm @@ -21,7 +21,7 @@ #import "RLMArray_Private.hpp" #import "RLMAccessor.hpp" #import "RLMDecimal128_Private.hpp" -#import "RLMDictionary_Private.h" +#import "RLMDictionary_Private.hpp" #import "RLMError_Private.hpp" #import "RLMObjectId_Private.hpp" #import "RLMObjectSchema_Private.hpp" @@ -37,6 +37,7 @@ #import "RLMValue.h" #import +#import #import #include @@ -181,6 +182,9 @@ static BOOL validateValue(__unsafe_unretained id const value, case RLMPropertyTypeUUID: return [value isKindOfClass:[NSUUID class]] || ([value isKindOfClass:[NSString class]] && realm::UUID::is_valid_string([value UTF8String])); + case RLMPropertyTypeDictionary: + case RLMPropertyTypeList: + REALM_UNREACHABLE(); } @throw RLMException(@"Invalid RLMPropertyType specified"); } @@ -289,6 +293,11 @@ void RLMValidateValueForProperty(__unsafe_unretained id const obj, if (prop.type == RLMPropertyTypeObject && !validateObjects) { return; } + + if (prop.type == RLMPropertyTypeAny) { + return; + } + if (RLMIsObjectValidForProperty(obj, prop)) { return; } @@ -401,20 +410,34 @@ BOOL RLMIsRunningInPlayground() { } REALM_ASSERT([v conformsToProtocol:@protocol(RLMValue)]); } + + switch ([v rlm_valueType]) { + case RLMPropertyTypeList: + return realm::Mixed(0, realm::CollectionType::List); + case RLMPropertyTypeDictionary: + return realm::Mixed(0, realm::CollectionType::Dictionary); + default: + return RLMObjcToMixedPrimitives(v, realm, createPolicy); + } +} - RLMPropertyType type = [v rlm_valueType]; +realm::Mixed RLMObjcToMixedPrimitives(__unsafe_unretained id const value, + __unsafe_unretained RLMRealm *const realm, + realm::CreatePolicy createPolicy) { + RLMPropertyType type = [value rlm_valueType]; return switch_on_type(static_cast(type), realm::util::overload{[&](realm::Obj*) { - // The RLMObjectBase may be unmanaged and therefor has no RLMClassInfo attached. + // The RLMObjectBase may be unmanaged and therefore has no RLMClassInfo attached. // So we fetch from the Realm instead. // If the Object is managed use it's RLMClassInfo instead so we do not have to do a // lookup in the table of schemas. - RLMObjectBase *objBase = v; + RLMObjectBase *objBase = value; RLMAccessorContext c{objBase->_info ? *objBase->_info : realm->_info[objBase->_objectSchema.className]}; - auto obj = c.unbox(v, createPolicy); + auto obj = c.unbox(value, createPolicy); return obj.is_valid() ? realm::Mixed(obj) : realm::Mixed(); }, [&](auto t) { RLMStatelessAccessorContext c; - return realm::Mixed(c.unbox>(v)); + auto mixed = realm::Mixed(c.unbox>(value)); + return mixed; }, [&](realm::Mixed*) -> realm::Mixed { REALM_UNREACHABLE(); }}); @@ -422,7 +445,9 @@ BOOL RLMIsRunningInPlayground() { id RLMMixedToObjc(realm::Mixed const& mixed, __unsafe_unretained RLMRealm *realm, - RLMClassInfo *classInfo) { + RLMClassInfo *classInfo, + RLMProperty *property, + realm::Obj obj) { if (mixed.is_null()) { return NSNull.null; } @@ -453,6 +478,11 @@ id RLMMixedToObjc(realm::Mixed const& mixed, } case realm::type_UUID: return [[NSUUID alloc] initWithRealmUUID:mixed.get()]; + case realm::type_Dictionary: + return [[RLMManagedDictionary alloc] initWithParent:obj property:property parentInfo:*classInfo]; + case realm::type_List: + return [[RLMManagedArray alloc] initWithParent:obj property:property parentInfo:*classInfo]; + break; default: @throw RLMException(@"Invalid data type for RLMPropertyTypeAny property."); } diff --git a/Realm/RLMValue.h b/Realm/RLMValue.h index ac1f10ee45..a132a46883 100644 --- a/Realm/RLMValue.h +++ b/Realm/RLMValue.h @@ -22,6 +22,8 @@ #import #import #import +#import +#import #pragma mark RLMValue @@ -52,6 +54,10 @@ `RLMObject `RLMObjectId` `RLMDecimal128` + `RLMDictionary` + `RLMArray` + `NSArray` + `NSDictionary` */ @protocol RLMValue @@ -95,3 +101,11 @@ /// :nodoc: @interface RLMObjectId (RLMValue) @end + +/// :nodoc: +@interface NSDictionary (RLMValue) +@end + +/// :nodoc: +@interface NSArray (RLMValue) +@end diff --git a/Realm/RLMValue.mm b/Realm/RLMValue.mm index ec36455728..f6aa7c0a75 100644 --- a/Realm/RLMValue.mm +++ b/Realm/RLMValue.mm @@ -122,3 +122,39 @@ - (RLMPropertyType)rlm_valueType { } @end + +#pragma mark Dictionary + +@implementation NSDictionary (RLMValue) + +- (RLMPropertyType)rlm_valueType { + return RLMPropertyTypeDictionary; +} + +@end + +@implementation RLMDictionary (RLMValue) + +- (RLMPropertyType)rlm_valueType { + return RLMPropertyTypeDictionary; +} + +@end + +#pragma mark Array + +@implementation NSArray (RLMValue) + +- (RLMPropertyType)rlm_valueType { + return RLMPropertyTypeList; +} + +@end + +@implementation RLMArray (RLMValue) + +- (RLMPropertyType)rlm_valueType { + return RLMPropertyTypeList; +} + +@end diff --git a/Realm/Tests/KVOTests.mm b/Realm/Tests/KVOTests.mm index 3b2e90f8cf..4770f20501 100644 --- a/Realm/Tests/KVOTests.mm +++ b/Realm/Tests/KVOTests.mm @@ -506,14 +506,14 @@ - (void)testRemoveObserver { XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]); XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col" context:context2]); XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context1]); - + XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]); XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context2]); XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]); XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col"]); XCTAssertThrows([obj removeObserver:self forKeyPath:@"int32Col"]); - + XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context1]); XCTAssertNoThrow([obj addObserver:self forKeyPath:@"int32Col" options:0 context:context2]); XCTAssertNoThrow([obj removeObserver:self forKeyPath:@"int32Col" context:context1]); @@ -1707,6 +1707,64 @@ - (void)testObserveArrayCount { [mutator addObject:obj]; AssertChanged(r, @0, @1); } + +- (void)testMixedCollectionKVC { + KVOObject *obj = [self createObject]; + NSDictionary *d = @{ @"key2" : @"hello2", + @"key3" : @YES, + @"key4" : @123, + @"key5" : @456.789 }; + + NSArray *a = @[ @"hello2", @YES, @123, @456.789 ]; + + { + KVORecorder r(self, obj, @"anyCol"); + obj.anyCol = d; + AssertCollectionChanged(); + } + + { + KVORecorder r(self, obj, @"anyCol"); + [obj setValue:d forKey:@"anyCol"]; + AssertCollectionChanged(); + [obj setValue:nil forKey:@"anyCol"]; + AssertCollectionChanged(); + } + + { + KVORecorder r(self, obj, @"anyCol"); + obj.anyCol = a; + AssertCollectionChanged(); + } + + { + KVORecorder r(self, obj, @"anyCol"); + [obj setValue:a forKey:@"anyCol"]; + AssertCollectionChanged(); + [obj setValue:nil forKey:@"anyCol"]; + AssertCollectionChanged(); + } + + if (![obj respondsToSelector:@selector(setObject:forKeyedSubscript:)]) { + return; + } + + { + KVORecorder r(self, obj, @"anyCol"); + obj[@"anyCol"] = d; + AssertCollectionChanged(); + obj[@"anyCol"] = nil; + AssertCollectionChanged(); + } + + { + KVORecorder r(self, obj, @"anyCol"); + obj[@"anyCol"] = a; + AssertCollectionChanged(); + obj[@"anyCol"] = nil; + AssertCollectionChanged(); + } +} @end // Run tests on an unmanaged RLMObject instance diff --git a/Realm/Tests/NotificationTests.m b/Realm/Tests/NotificationTests.m index b061d6295f..056756e8a9 100644 --- a/Realm/Tests/NotificationTests.m +++ b/Realm/Tests/NotificationTests.m @@ -262,7 +262,7 @@ - (NSInteger)row { } @end -static RLMCollectionChange *getChange(RLMTestCase *self, void (^block)(RLMRealm *)) { +static RLMCollectionChange *getChange(RLMTestCase *self, void (^block)(RLMRealm *realm)) { [self prepare]; __block bool first = true; @@ -292,7 +292,7 @@ - (NSInteger)row { } static void ExpectChange(id self, NSArray *deletions, NSArray *insertions, - NSArray *modifications, void (^block)(RLMRealm *)) { + NSArray *modifications, void (^block)(RLMRealm *realm)) { RLMCollectionChange *changes = getChange(self, block); XCTAssertNotNil(changes); if (!changes) { @@ -1727,3 +1727,362 @@ - (void)testModifyUnobservedKeyPathArrayProperty { } @end + +@interface MixedCollectionChangesetTests : RLMTestCase +@end + +@implementation MixedCollectionChangesetTests { + MixedObject *_obj; +} +- (void)setUp { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm transactionWithBlock:^{ + [realm deleteAllObjects]; + _obj = [MixedObject createInRealm:realm withValue:@{ @"anyCol": @"initial" }]; + }]; +} + +- (void)tearDown { + _obj = nil; + [super tearDown]; +} + +- (NSDictionary *)testDictionary { + return @{ @"key2" : @"hello2", + @"key3" : @YES, + @"key4" : @123, + @"key5" : @456.789 }; +} + +- (NSArray *)testArray { + return @[ @"hello2", @YES, @123, @456.789 ]; +} + +- (void)prepare { +} + +- (RLMResults *)query { + return MixedObject.allObjects; +} + +static void ExpectMixedCollectionObjectChange(id self, MixedObject *obj, void (^block)(void)) { + __block XCTestExpectation *expectation = nil; + + RLMNotificationToken *token = [obj addNotificationBlock:^(BOOL deleted, NSArray *changes, NSError *error) { + XCTAssertFalse(deleted); + XCTAssertNil(error); + XCTAssertEqual(changes.count, 1); + [expectation fulfill]; + }]; + + RLMRealm *realm = [RLMRealm defaultRealm]; + expectation = [self expectationWithDescription:@"collections_mixed_notifications"]; + [realm beginWriteTransaction]; + block(); + [realm commitWriteTransaction]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + [token invalidate]; +} + +static void ExpectMixedDictionaryChange(id self, NSArray *deletions, NSArray *insertions, + NSArray *modifications, void (^block)(void)) { + __block XCTestExpectation *expectation = nil; + + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + RLMNotificationToken *token = [dictionary addNotificationBlock:^(RLMDictionary *dictionary, RLMDictionaryChange *changes, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(dictionary); + if (!changes) { + return; + } + + XCTAssertEqualObjects(deletions, changes.deletions); + XCTAssertEqualObjects(insertions, changes.insertions); + XCTAssertEqualObjects(modifications, changes.modifications); + + [expectation fulfill]; + }]; + + RLMRealm *realm = [RLMRealm defaultRealm]; + expectation = [self expectationWithDescription:@"collections_mixed_notifications"]; + [realm beginWriteTransaction]; + block(); + [realm commitWriteTransaction]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + [token invalidate]; +} + +static void ExpectMixedArrayChange(id self, NSArray *deletions, NSArray *insertions, + NSArray *modifications, void (^block)(void)) { + __block XCTestExpectation *expectation = nil; + + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + RLMNotificationToken *token = [array addNotificationBlock:^(RLMArray *array, RLMCollectionChange *changes, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(array); + if (!changes) { + return; + } + + XCTAssertEqualObjects(deletions, changes.deletions); + XCTAssertEqualObjects(insertions, changes.insertions); + XCTAssertEqualObjects(modifications, changes.modifications); + [expectation fulfill]; + }]; + + RLMRealm *realm = [RLMRealm defaultRealm]; + expectation = [self expectationWithDescription:@"collections_mixed_notifications"]; + [realm beginWriteTransaction]; + block(); + [realm commitWriteTransaction]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + [token invalidate]; +} + +- (void)testAddDictionaryToMixed { + NSDictionary *testDictionary = [self testDictionary]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testDictionary; + }); +} + +- (void)testAddArrayToMixed { + NSArray *testArray = [self testArray]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testArray; + }); +} + +- (void)testUpdateDictionaryToMixed { + NSDictionary *testDictionary = [self testDictionary]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testDictionary; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMDictionary *dictionary = (RLMDictionary *)_obj.anyCol; + dictionary[@"key2"] = @NO; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMDictionary *dictionary = (RLMDictionary *)_obj.anyCol; + dictionary[@"key4"] = [NSNull null]; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMDictionary *dictionary = (RLMDictionary *)_obj.anyCol; + [dictionary removeAllObjects]; + }); +} + +- (void)testUpdateAndRemoveArrayToMixed { + NSArray *testArray = [self testArray]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testArray; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMArray *array = (RLMArray *)_obj.anyCol; + [array addObject:@NO]; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMArray *array = (RLMArray *)_obj.anyCol; + array[3] = @"hey"; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMArray *array = (RLMArray *)_obj.anyCol; + [array removeObjectAtIndex:2]; + }); + + ExpectMixedCollectionObjectChange(self, _obj, ^{ + RLMArray *array = (RLMArray *)_obj.anyCol; + [array removeAllObjects]; + }); +} + +- (void)testAddToMixedObservationWithKeypath { + __block XCTestExpectation *expectation = nil; + RLMNotificationToken *token = [_obj addNotificationBlock:^(BOOL deleted, NSArray *changes, NSError *error) { + XCTAssertFalse(deleted); + XCTAssertNil(error); + XCTAssertEqual(changes.count, 1); + [expectation fulfill]; + } keyPaths:@[ @"anyCol" ]]; + + RLMRealm *realm = [RLMRealm defaultRealm]; + expectation = [self expectationWithDescription:@"collections_mixed_notifications"]; + [realm beginWriteTransaction]; + _obj.anyCol = @{ @"key2": @987.321 }; + [realm commitWriteTransaction]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + [token invalidate]; +} + +- (void)testAddToMixedObservationReplaceCollection { + NSDictionary *testDictionary = [self testDictionary]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testDictionary; + }); + + NSArray *testArray = [self testArray]; + ExpectMixedCollectionObjectChange(self, _obj, ^{ + _obj.anyCol = testArray; + }); +} + +- (void)testMixedDictionaryChangesInResults { + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + mixedObject.anyCol = @{ @"key": @987.321 }; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + NSLog(@"%@", mixedObject.anyCol); + RLMDictionary *dict = (RLMDictionary *)mixedObject.anyCol; + dict[@"key"] = @NO; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMDictionary *dict = (RLMDictionary *)mixedObject.anyCol; + dict[@"key1"] = @"newvalue"; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMDictionary *dict = (RLMDictionary *)mixedObject.anyCol; + dict[@"key1"] = nil; + }); +} + +- (void)testMixedArrayChangesInResults { + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + mixedObject.anyCol = @[ @987.321 ]; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + array[0] = @NO; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array addObject:@"newvalue"]; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array insertObject:@765 atIndex:1]; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array removeLastObject]; + }); + + ExpectChange(self, @[], @[], @[@0], ^(RLMRealm *realm) { + MixedObject *mixedObject = [[MixedObject allObjectsInRealm:realm] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array removeAllObjects]; + }); +} + +- (void)testMixedDictionaryCollectionChanges { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + _obj.anyCol = @{ @"key2": @987.321 }; + [realm commitWriteTransaction]; + + ExpectMixedDictionaryChange(self, @[@"key2"], @[@"key1", @"key3"], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + mixedObject.anyCol = @{ @"key1": @YES, @"key3": @987.321 }; + }); + + ExpectMixedDictionaryChange(self, @[], @[@"key4"], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + dictionary[@"key4"] = @NO; + }); + + ExpectMixedDictionaryChange(self, @[@"key4"], @[], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + dictionary[@"key4"] = nil; + }); + + ExpectMixedDictionaryChange(self, @[], @[], @[@"key1"], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + dictionary[@"key1"] = @345; + }); + + ExpectMixedDictionaryChange(self, @[@"key1"], @[], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + [dictionary removeObjectForKey:@"key1"]; + }); + + ExpectMixedDictionaryChange(self, @[@"key3"], @[], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)mixedObject.anyCol; + [dictionary removeAllObjects]; + }); +} + +- (void)testMixedArrayCollectionChanges { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm beginWriteTransaction]; + _obj.anyCol = @[ @987.321 ]; + [realm commitWriteTransaction]; + + ExpectMixedArrayChange(self, @[@0], @[@0, @1], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + mixedObject.anyCol = @[ @YES, @987.321 ]; + }); + + ExpectMixedArrayChange(self, @[], @[@2], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array addObject: @NO]; + }); + + ExpectMixedArrayChange(self, @[], @[@1], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array insertObject:@"hey" atIndex:1]; + }); + + ExpectMixedArrayChange(self, @[], @[], @[@0], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + array[0] = [RLMObjectId objectId]; + }); + + ExpectMixedArrayChange(self, @[@3], @[], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array removeLastObject]; + }); + + ExpectMixedArrayChange(self, @[@0, @1, @2], @[], @[], ^{ + MixedObject *mixedObject = [[MixedObject allObjects] firstObject]; + RLMArray *array = (RLMArray *)mixedObject.anyCol; + [array removeAllObjects]; + }); +} + +@end diff --git a/Realm/Tests/ObjectTests.m b/Realm/Tests/ObjectTests.m index 521b7d7c63..d410bbd7d1 100644 --- a/Realm/Tests/ObjectTests.m +++ b/Realm/Tests/ObjectTests.m @@ -785,7 +785,7 @@ - (void)testCreateInRealmValidationForDictionary { } if ([keyToInvalidate isEqualToString:@"anyCol"]) { - obj = self; + continue; } invalidInput[keyToInvalidate] = obj; diff --git a/Realm/Tests/QueryTests.m b/Realm/Tests/QueryTests.m index 12996db4a6..7e4bc43732 100644 --- a/Realm/Tests/QueryTests.m +++ b/Realm/Tests/QueryTests.m @@ -3805,9 +3805,9 @@ - (void)testDictionaryQueryKeySubscriptWithObjectCol { - (void)testDictionarySubscriptThrowsException { RLMRealm *realm = [self realm]; RLMAssertThrowsWithReason(([realm objects:@"ArrayPropertyObject" where:@"array['invalid'] = NULL"]), - @"Invalid keypath 'array[\"invalid\"]': only dictionaries support subscript predicates."); + @"Invalid keypath 'array[\"invalid\"]': only dictionaries and mixed support subscript predicates."); RLMAssertThrowsWithReason(([realm objects:@"SetPropertyObject" where:@"set['invalid'] = NULL"]), - @"Invalid keypath 'set[\"invalid\"]': only dictionaries support subscript predicates."); + @"Invalid keypath 'set[\"invalid\"]': only dictionaries and mixed support subscript predicates."); RLMAssertThrowsWithReason(([realm objects:@"OwnerObject" where:@"dog['dogName'] = NULL"]), @"Aggregate operations can only be used on key paths that include an collection property"); } diff --git a/Realm/Tests/RLMValueTests.m b/Realm/Tests/RLMValueTests.m index 797dd8e0ba..9e83ab1aca 100644 --- a/Realm/Tests/RLMValueTests.m +++ b/Realm/Tests/RLMValueTests.m @@ -19,6 +19,12 @@ #import "RLMTestCase.h" #import +@interface NotRealmObject : NSObject +@end + +@implementation NotRealmObject +@end + @interface RLMValueTests : RLMTestCase @end @@ -66,6 +72,16 @@ - (void)testDecimal128Type { XCTAssertEqual(v.rlm_valueType, RLMPropertyTypeDecimal128); } +- (void)testDictionaryType { + NSDictionary *dictionary = @{ @"key1" : @"hello" }; + XCTAssertEqual(dictionary.rlm_valueType, RLMPropertyTypeDictionary); +} + +- (void)testArrayType { + NSArray *array = @[ @"hello", @123456 ]; + XCTAssertEqual(array.rlm_valueType, RLMPropertyTypeList); +} + #pragma mark - Comparison - (void)testNumberEquals { @@ -144,6 +160,44 @@ - (void)testDecimal128Equals { XCTAssertNotEqual(v1, [RLMDecimal128 decimalWithNumber:@456.123]); } +- (void)testDictionaryAnyEquals { + NSDictionary *dictionary = @{ @"key2" : @"hello2", + @"key3" : @YES, + @"key4" : @123, + @"key5" : @456.789, + @"key6" : [NSData dataWithBytes:"hey" length:3], + @"key7" : [NSDate date], + @"key8" : [[MixedObject alloc] init], + @"key9" : [RLMObjectId objectId], + @"key10" : [RLMDecimal128 decimalWithNumber:@123.456] }; + id v1 = dictionary; + id v2 = dictionary; + XCTAssertEqual(v1, dictionary); + XCTAssertEqual(v2, dictionary); + XCTAssertEqual(v1.rlm_valueType, RLMPropertyTypeDictionary); + XCTAssertEqual(v2.rlm_valueType, RLMPropertyTypeDictionary); + XCTAssertEqual(v1, v2); +} + +- (void)testArrayAnyEquals { + NSArray *array = @[ @"hello2", + @YES, + @123, + @456.789, + [NSData dataWithBytes:"hey" length:3], + [NSDate date], + [[MixedObject alloc] init], + [RLMObjectId objectId], + [RLMDecimal128 decimalWithNumber:@123.456] ]; + id v1 = array; + id v2 = array; + XCTAssertEqual(v1, array); + XCTAssertEqual(v2, array); + XCTAssertEqual(v1.rlm_valueType, RLMPropertyTypeList); + XCTAssertEqual(v2.rlm_valueType, RLMPropertyTypeList); + XCTAssertEqual(v1, v2); +} + #pragma mark - Managed Values - (void)testCreateManagedObjectManagedChild { @@ -169,12 +223,64 @@ - (void)testCreateManagedObjectManagedChild { XCTAssertTrue([((StringObject *)mo1.anyCol).stringCol isEqualToString:so.stringCol]); XCTAssertEqual(mo1.anyCol.rlm_valueType, RLMPropertyTypeObject); XCTAssertTrue([((StringObject *)mo1.anyArray.firstObject).stringCol isEqualToString:so.stringCol]); + XCTAssertEqual([StringObject allObjectsInRealm:r].count, 1U); } -- (void)testCreateManagedObjectUnmanagedChild { +- (void)testCreateManagedObjectInAnyDictionary { StringObject *so = [[StringObject alloc] init]; so.stringCol = @"hello"; + + RLMRealm *r = [self realmWithTestPath]; + [r beginWriteTransaction]; + [r addObject:so]; + + NSDictionary *dictionary = @{ @"key1" : so }; + MixedObject *mo0 = [MixedObject createInRealm:r withValue:@[dictionary, @[dictionary]]]; + + MixedObject *mo1 = [[MixedObject alloc] init]; + mo1.anyCol = dictionary; + [mo1.anyArray addObject:dictionary]; + [r commitWriteTransaction]; + + XCTAssertNotNil(mo0.anyCol); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo0.anyCol)[@"key1"]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo0.anyArray.firstObject)[@"key1"]).stringCol isEqualToString:so.stringCol]); + + XCTAssertNotNil(mo1.anyCol); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo1.anyCol)[@"key1"]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo1.anyArray[0])[@"key1"]).stringCol isEqualToString:so.stringCol]); +} +- (void)testCreateManagedObjectInAnyArray { + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + + RLMRealm *r = [self realmWithTestPath]; + [r beginWriteTransaction]; + [r addObject:so]; + + NSArray *array = @[so]; + MixedObject *mo0 = [MixedObject createInRealm:r withValue:@[array, @[array]]]; + + MixedObject *mo1 = [[MixedObject alloc] init]; + mo1.anyCol = array; + [mo1.anyArray addObject:array]; + [r commitWriteTransaction]; + + XCTAssertNotNil(mo0.anyCol); + XCTAssertTrue([((StringObject *)((NSArray *)mo0.anyCol)[0]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSArray *)mo0.anyArray.firstObject)[0]).stringCol isEqualToString:so.stringCol]); + + XCTAssertNotNil(mo1.anyCol); + XCTAssertTrue([((StringObject *)((NSArray *)mo1.anyCol)[0]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSArray *)mo1.anyArray[0])[0]).stringCol isEqualToString:so.stringCol]); +} + + +- (void)testCreateManagedObjectUnmanagedChild { + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + StringObject *so1 = [[StringObject alloc] init]; so1.stringCol = @"hello2"; @@ -202,6 +308,68 @@ - (void)testCreateManagedObjectUnmanagedChild { XCTAssertTrue([((StringObject *)mo1.anyArray[0]).stringCol isEqualToString:so.stringCol]); } +- (void)testCreateManagedObjectUnmanagedChildInAnyDictionary { + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + + StringObject *so1 = [[StringObject alloc] init]; + so1.stringCol = @"hello2"; + + RLMRealm *r = [self realmWithTestPath]; + [r beginWriteTransaction]; + + NSDictionary *dictionary = @{ @"key1" : so }; + MixedObject *mo0 = [MixedObject createInRealm:r withValue:@[dictionary, @[dictionary]]]; + + MixedObject *mo1 = [[MixedObject alloc] init]; + [mo1.anyArray addObject:dictionary]; + [r addObject:mo1]; + [r commitWriteTransaction]; + + XCTAssertThrows(mo1.anyCol = so1); + [r beginWriteTransaction]; + mo1.anyCol = @{ @"key2" : so1 }; + + XCTAssertNotNil(mo0.anyCol); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo0.anyCol)[@"key1"]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo0.anyArray.firstObject)[@"key1"]).stringCol isEqualToString:so.stringCol]); + + XCTAssertNotNil(mo1.anyCol); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo1.anyCol)[@"key2"]).stringCol isEqualToString:so1.stringCol]); + XCTAssertTrue([((StringObject *)((NSDictionary *)mo1.anyArray[0])[@"key1"]).stringCol isEqualToString:so.stringCol]); +} + +- (void)testCreateManagedObjectUnmanagedChildInAnyArray { + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + + StringObject *so1 = [[StringObject alloc] init]; + so1.stringCol = @"hello2"; + + RLMRealm *r = [self realmWithTestPath]; + [r beginWriteTransaction]; + + NSArray *array = @[so]; + MixedObject *mo0 = [MixedObject createInRealm:r withValue:@[array, @[array]]]; + + MixedObject *mo1 = [[MixedObject alloc] init]; + [mo1.anyArray addObject:array]; + [r addObject:mo1]; + [r commitWriteTransaction]; + + XCTAssertThrows(mo1.anyCol = so1); + [r beginWriteTransaction]; + mo1.anyCol = @[so1]; + + XCTAssertNotNil(mo0.anyCol); + XCTAssertTrue([((StringObject *)((NSArray *)mo0.anyCol)[0]).stringCol isEqualToString:so.stringCol]); + XCTAssertTrue([((StringObject *)((NSArray *)mo0.anyArray.firstObject)[0]).stringCol isEqualToString:so.stringCol]); + + XCTAssertNotNil(mo1.anyCol); + XCTAssertTrue([((StringObject *)((NSArray *)mo1.anyCol)[0]).stringCol isEqualToString:so1.stringCol]); + XCTAssertTrue([((StringObject *)((NSArray *)mo1.anyArray[0])[0]).stringCol isEqualToString:so.stringCol]); +} + // difference between adding object and not! - (void)testCreateManagedObject { RLMRealm *r = [self realmWithTestPath]; @@ -321,6 +489,102 @@ - (void)testCreateManagedDecimal128 { XCTAssertEqual(mo.anyCol.rlm_valueType, RLMPropertyTypeDecimal128); } +- (void)testCreateManagedDictionary { + RLMRealm *r = [self realmWithTestPath]; + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + RLMObjectId *oid = [RLMObjectId objectId]; + NSDate *d = [NSDate date]; + NSDictionary *d1 = @{ @"key2" : @"hello2", + @"key3" : @YES, + @"key4" : @123, + @"key5" : @456.789, + @"key6" : [NSData dataWithBytes:"hey" length:3], + @"key7" : d, + @"key8" : so, + @"key9" : oid, + @"key10" : [RLMDecimal128 decimalWithNumber:@123.456] }; + NSDictionary *d2 = @{ @"key1" : @"hello" }; + [r beginWriteTransaction]; + [MixedObject createInRealm:r withValue:@[d1, @[d1, d2]]]; + [r commitWriteTransaction]; + + XCTAssertEqual([MixedObject allObjectsInRealm:r].count, 1U); + MixedObject *result = [[MixedObject allObjectsInRealm:r] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)result.anyCol; + XCTAssertTrue([dictionary[@"key2"] isEqual:@"hello2"]); + XCTAssertTrue([dictionary[@"key3"] isEqual:@YES]); + XCTAssertTrue([dictionary[@"key4"] isEqual:@123]); + XCTAssertTrue([dictionary[@"key5"] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)dictionary[@"key6"] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)dictionary[@"key7"]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)dictionary[@"key8"]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)dictionary[@"key9"]) isEqual:oid]); + XCTAssertTrue([dictionary[@"key10"] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMDictionary *dictionary1 = (RLMDictionary *)result.anyArray.firstObject; + XCTAssertTrue([dictionary1[@"key2"] isEqual:@"hello2"]); + XCTAssertTrue([dictionary1[@"key3"] isEqual:@YES]); + XCTAssertTrue([dictionary1[@"key4"] isEqual:@123]); + XCTAssertTrue([dictionary1[@"key5"] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)dictionary1[@"key6"] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)dictionary1[@"key7"]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)dictionary1[@"key8"]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)dictionary1[@"key9"]) isEqual:oid]); + XCTAssertTrue([dictionary1[@"key10"] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMDictionary *dictionary2 = (RLMDictionary *)result.anyArray.lastObject; + XCTAssertTrue([dictionary2[@"key1"] isEqual:@"hello"]); +} + +- (void)testCreateManagedArray { + RLMRealm *r = [self realmWithTestPath]; + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + RLMObjectId *oid = [RLMObjectId objectId]; + NSDate *d = [NSDate date]; + NSArray *d1 = @[ @"hello2", + @YES, + @123, + @456.789, + [NSData dataWithBytes:"hey" length:3], + d, + so, + oid, + [RLMDecimal128 decimalWithNumber:@123.456] ]; + NSArray *d2 = @[@"hello"]; + [r beginWriteTransaction]; + [MixedObject createInRealm:r withValue:@[d1, @[d1, d2]]]; + [r commitWriteTransaction]; + + XCTAssertEqual([MixedObject allObjectsInRealm:r].count, 1U); + MixedObject *result = [[MixedObject allObjectsInRealm:r] firstObject]; + RLMArray *array = ((RLMArray *)result.anyCol); + XCTAssertTrue([array[0] isEqual:@"hello2"]); + XCTAssertTrue([array[1] isEqual:@YES]); + XCTAssertTrue([array[2] isEqual:@123]); + XCTAssertTrue([array[3] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)array[4] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)array[5]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)array[6]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)array[7]) isEqual:oid]); + XCTAssertTrue([array[8] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMArray *array1 = (RLMArray *)result.anyArray.firstObject; + XCTAssertTrue([array1[0] isEqual:@"hello2"]); + XCTAssertTrue([array1[1] isEqual:@YES]); + XCTAssertTrue([array1[2] isEqual:@123]); + XCTAssertTrue([array1[3] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)array1[4] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)array1[5]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)array1[6]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)array1[7]) isEqual:oid]); + XCTAssertTrue([array1[8] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMArray *array2 = (RLMArray *)result.anyArray.lastObject; + XCTAssertTrue([array2[0] isEqual:@"hello"]); +} + #pragma mark - Add Managed Values - (void)testAddManagedObject { @@ -463,6 +727,196 @@ - (void)testAddManagedDecimal128 { XCTAssertEqual(mo.anyCol.rlm_valueType, RLMPropertyTypeDecimal128); } +- (void)testAddManagedDictionary { + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + RLMObjectId *oid = [RLMObjectId objectId]; + NSDate *d = [NSDate date]; + NSDictionary *d1 = @{ @"key2" : @"hello2", + @"key3" : @YES, + @"key4" : @123, + @"key5" : @456.789, + @"key6" : [NSData dataWithBytes:"hey" length:3], + @"key7" : d, + @"key8" : so, + @"key9" : oid, + @"key10" : [RLMDecimal128 decimalWithNumber:@123.456] }; + NSDictionary *d2 = @{ @"key1" : @"hello" }; + MixedObject *mo = [[MixedObject alloc] init]; + mo.anyCol = d1; + [mo.anyArray addObjects:@[d1, d2]]; + + RLMRealm *r = [self realmWithTestPath]; + [r beginWriteTransaction]; + [r addObject:mo]; + [r commitWriteTransaction]; + + XCTAssertEqual([MixedObject allObjectsInRealm:r].count, 1U); + MixedObject *result = [[MixedObject allObjectsInRealm:r] firstObject]; + RLMDictionary *dictionary = (RLMDictionary *)result.anyCol; + XCTAssertTrue([dictionary[@"key2"] isEqual:@"hello2"]); + XCTAssertTrue([dictionary[@"key3"] isEqual:@YES]); + XCTAssertTrue([dictionary[@"key4"] isEqual:@123]); + XCTAssertTrue([dictionary[@"key5"] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)dictionary[@"key6"] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)dictionary[@"key7"]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)dictionary[@"key8"]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)dictionary[@"key9"]) isEqual:oid]); + XCTAssertTrue([dictionary[@"key10"] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMDictionary *dictionary1 = (RLMDictionary *)result.anyArray.firstObject; + XCTAssertTrue([dictionary1[@"key2"] isEqual:@"hello2"]); + XCTAssertTrue([dictionary1[@"key3"] isEqual:@YES]); + XCTAssertTrue([dictionary1[@"key4"] isEqual:@123]); + XCTAssertTrue([dictionary1[@"key5"] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)dictionary1[@"key6"] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)dictionary1[@"key7"]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)dictionary1[@"key8"]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)dictionary1[@"key9"]) isEqual:oid]); + XCTAssertTrue([dictionary1[@"key10"] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMDictionary *dictionary2 = (RLMDictionary *)result.anyArray.lastObject; + XCTAssertTrue([dictionary2[@"key1"] isEqual:@"hello"]); +} + +- (void)testAddManagedArray { + RLMRealm *r = [self realmWithTestPath]; + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + RLMObjectId *oid = [RLMObjectId objectId]; + NSDate *d = [NSDate date]; + NSArray *d1 = @[ @"hello2", + @YES, + @123, + @456.789, + [NSData dataWithBytes:"hey" length:3], + d, + so, + oid, + [RLMDecimal128 decimalWithNumber:@123.456] ]; + NSArray *d2 = @[@"hello"]; + MixedObject *mo = [[MixedObject alloc] init]; + mo.anyCol = d1; + [mo.anyArray addObjects:@[d1, d2]]; + + [r beginWriteTransaction]; + [r addObject:mo]; + [r commitWriteTransaction]; + + XCTAssertEqual([MixedObject allObjectsInRealm:r].count, 1U); + MixedObject *result = [[MixedObject allObjectsInRealm:r] firstObject]; + RLMArray *array = ((RLMArray *)result.anyCol); + XCTAssertTrue([array[0] isEqual:@"hello2"]); + XCTAssertTrue([array[1] isEqual:@YES]); + XCTAssertTrue([array[2] isEqual:@123]); + XCTAssertTrue([array[3] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)array[4] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)array[5]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)array[6]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)array[7]) isEqual:oid]); + XCTAssertTrue([array[8] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMArray *array1 = (RLMArray *)result.anyArray.firstObject; + XCTAssertTrue([array1[0] isEqual:@"hello2"]); + XCTAssertTrue([array1[1] isEqual:@YES]); + XCTAssertTrue([array1[2] isEqual:@123]); + XCTAssertTrue([array1[3] isEqual:@456.789]); + XCTAssertTrue([[[NSString alloc] initWithData:(NSData *)array1[4] encoding:NSUTF8StringEncoding] isEqual:@"hey"]); + XCTAssertEqualWithAccuracy(((NSDate *)array1[5]).timeIntervalSince1970, d.timeIntervalSince1970, 1.0); + XCTAssertTrue([((StringObject *)array1[6]).stringCol isEqual:@"hello"]); + XCTAssertTrue([((RLMObjectId *)array1[7]) isEqual:oid]); + XCTAssertTrue([array1[8] isEqual:[RLMDecimal128 decimalWithNumber:@123.456]]); + + RLMArray *array2 = (RLMArray *)result.anyArray.lastObject; + XCTAssertTrue([array2[0] isEqual:@"hello"]); +} + +- (void)testDeleteAndUpdateValuesInManagedDictionary { + RLMRealm *r = [self realmWithTestPath]; + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + NSDictionary *d1 = @{ @"key2" : @"hello2", + @"key3" : @YES}; + NSDictionary *d2 = @{ @"key1" : @"hello" }; + MixedObject *mo = [[MixedObject alloc] init]; + mo.anyCol = d1; + [mo.anyArray addObjects:@[d1, d2]]; + + [r beginWriteTransaction]; + [r addObject:mo]; + [r commitWriteTransaction]; + + RLMDictionary *dictionary = (RLMDictionary *)mo.anyCol; + XCTAssertTrue([dictionary[@"key2"] isEqual:@"hello2"]); + XCTAssertTrue([dictionary[@"key3"] isEqual:@YES]); + + RLMDictionary *dictionary2 = (RLMDictionary *)mo.anyArray.lastObject; + XCTAssertTrue([dictionary2[@"key1"] isEqual:@"hello"]); + + [r beginWriteTransaction]; + dictionary[@"key2"] = @1234; + dictionary2[@"key1"] = @123.456; + [r commitWriteTransaction]; + + XCTAssertTrue([dictionary[@"key2"] isEqual:@1234]); + XCTAssertTrue([dictionary2[@"key1"] isEqual:@123.456]); + + [r beginWriteTransaction]; + [dictionary removeObjectForKey:@"key3"]; + [dictionary2 removeObjectForKey:@"key1"]; + [r commitWriteTransaction]; + + XCTAssertNil(dictionary[@"key3"]); + XCTAssertNil(dictionary2[@"key1"]); +} + +- (void)testDeleteAndUpdateValuesInArray { + RLMRealm *r = [self realmWithTestPath]; + StringObject *so = [[StringObject alloc] init]; + so.stringCol = @"hello"; + RLMObjectId *oid = [RLMObjectId objectId]; + NSDate *d = [NSDate date]; + NSArray *d1 = @[ @"hello2", + @YES]; + NSArray *d2 = @[@"hello"]; + MixedObject *mo = [[MixedObject alloc] init]; + mo.anyCol = d1; + [mo.anyArray addObjects:@[d1, d2]]; + + [r beginWriteTransaction]; + [r addObject:mo]; + [r commitWriteTransaction]; + + XCTAssertEqual([MixedObject allObjectsInRealm:r].count, 1U); + MixedObject *result = [[MixedObject allObjectsInRealm:r] firstObject]; + RLMArray *array = (RLMArray *)result.anyCol; + XCTAssertTrue([array[0] isEqual:@"hello2"]); + XCTAssertTrue([array[1] isEqual:@YES]); + XCTAssertEqual(array.count, 2U); + + RLMArray *array2 = (RLMArray *)mo.anyArray.lastObject; + XCTAssertTrue([array2[0] isEqual:@"hello"]); + + [r beginWriteTransaction]; + array[0] = @1234; + array2[0] = @123.456; + [r commitWriteTransaction]; + + XCTAssertTrue([array[0] isEqual:@1234]); + XCTAssertTrue([array2[0] isEqual:@123.456]); + XCTAssertEqual(array.count, 2U); + XCTAssertEqual(array2.count, 1U); + + [r beginWriteTransaction]; + [array removeObjectAtIndex:0]; + [array2 removeObjectAtIndex:0]; + [r commitWriteTransaction]; + + XCTAssertFalse([array[0] isEqual:@1234]); + XCTAssertEqual(array.count, 1U); + XCTAssertEqual(array2.count, 0U); +} + #pragma mark - Dynamic Object Accessor - (void)testDynamicObjectAccessor { @@ -520,5 +974,4 @@ - (void)testDynamicSchema { [self waitForExpectations:@[ex] timeout:1.0]; (void)[[MixedObject allObjectsInRealm:realm2].firstObject anyCol]; } - @end diff --git a/Realm/Tests/Swift/SwiftDynamicTests.swift b/Realm/Tests/Swift/SwiftDynamicTests.swift index b005a3f490..f9ab880314 100644 --- a/Realm/Tests/Swift/SwiftDynamicTests.swift +++ b/Realm/Tests/Swift/SwiftDynamicTests.swift @@ -156,4 +156,48 @@ class SwiftRLMDynamicTests: RLMTestCase { XCTAssertTrue((robj2["objectCol"] as! RLMObject)["stringCol"] as! String == "string") XCTAssertTrue((robj2["mixedObjectCol"] as! RLMObject)["anyCol"] as! String == "string") } + + func testDynamicTypesMixedCollection_objc() { + let obj1 = AllTypesObject.values(5, + stringObject: StringObject(value: ["newString"]), + mixedObject: MixedObject(value: [["string", 45, false]]))! + let obj2 = AllTypesObject.values(2, + stringObject: StringObject(value: ["newString1"]), + mixedObject: MixedObject(value: [["key1": "string", "key2": true, "key3": 12.12]]))! + let obj3 = AllTypesObject.values(2, + stringObject: StringObject(value: ["newString1"]), + mixedObject: MixedObject(value: [["key1": ["string", true, ["key3": 12.12]]]]))! + + autoreleasepool { + // open realm in autoreleasepool to create tables and then dispose + let realm = self.realmWithTestPath() + realm.beginWriteTransaction() + _ = AllTypesObject.create(in: realm, withValue: obj1) + _ = AllTypesObject.create(in: realm, withValue: obj2) + _ = AllTypesObject.create(in: realm, withValue: obj3) + try! realm.commitWriteTransaction() + } + + let dyrealm = realm(withTestPathAndSchema: nil) + let results = dyrealm.allObjects(AllTypesObject.className()) + XCTAssertEqual(results.count, UInt(3)) + let robj1 = results[0] + let robj2 = results[1] + let robj3 = results[2] + + // Mixed List + XCTAssertTrue(((robj1["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedArray)[0] as! String == "string") + XCTAssertTrue(((robj1["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedArray)[1] as! Int == 45) + XCTAssertTrue(((robj1["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedArray)[2] as! Bool == false) + + // Mixed Dictionary + XCTAssertTrue(((robj2["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key1"] as! String == "string") + XCTAssertTrue(((robj2["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key2"] as! Bool == true) + XCTAssertTrue(((robj2["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key3"] as! Double == 12.12) + + // Mixed Dictionary + XCTAssertTrue((((robj3["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key1"] as! RLMManagedArray)[0] as! String == "string") + XCTAssertTrue((((robj3["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key1"] as! RLMManagedArray)[1] as! Bool == true) + XCTAssertTrue(((((robj3["mixedObjectCol"] as! RLMObject)["anyCol"] as! RLMManagedDictionary)["key1"] as! RLMManagedArray)[2] as! RLMManagedDictionary)["key3"] as! Double == 12.12) + } } diff --git a/RealmSwift/AnyRealmValue.swift b/RealmSwift/AnyRealmValue.swift index 3ca489585e..2ac145f9d2 100644 --- a/RealmSwift/AnyRealmValue.swift +++ b/RealmSwift/AnyRealmValue.swift @@ -21,6 +21,8 @@ import Foundation import Realm /// A enum for storing and retrieving values associated with an `AnyRealmValue` property. +/// `AnyRealmValue` can be as well a collection (List, Dictionary) of `AnyRealmValue`, meaning that you can have +/// nested collections inside a `AnyRealmValue`. public enum AnyRealmValue: Hashable { /// Represents `nil` case none @@ -46,6 +48,10 @@ public enum AnyRealmValue: Hashable { case decimal128(Decimal128) /// A UUID type. case uuid(UUID) + /// Dictionary type. + case dictionary(Map) + /// List type. + case list(List) /// Returns an `Int` if that is what the stored value is, otherwise `nil`. public var intValue: Int? { @@ -139,6 +145,22 @@ public enum AnyRealmValue: Hashable { return o as? T } + /// Returns a `Map` if that is what the stored value is, otherwise `nil`. + public var dictionaryValue: Map? { + guard case let .dictionary(d) = self else { + return nil + } + return d + } + + /// Returns a `List` if that is what the stored value is, otherwise `nil`. + public var listValue: List? { + guard case let .list(l) = self else { + return nil + } + return l + } + /// Returns a `DynamicObject` if the stored value is an `Object`, otherwise `nil`. /// /// Note: This allows access to an object stored in `AnyRealmValue` where you may not have @@ -156,4 +178,28 @@ public enum AnyRealmValue: Hashable { public init() { self = .none } + + /// Returns a `AnyRealmValue` storing a `Map`. + /// + /// - Parameter dictionary: A Swift's dictionary of `AnyRealmValue` values. + /// - Returns: Returns an `AnyRealmValue` storing a `Map`. + public static func fromDictionary(_ dictionary: Dictionary) -> AnyRealmValue { + let map = Map() + dictionary.forEach { (key, value) in + map.setValue(value, forKey: key) + } + return AnyRealmValue.dictionary(map) + } + + /// Returns a `AnyRealmValue` storing a `List`. + /// + /// - Parameter array: A Swift's array of `AnyRealmValue`. + /// - Returns: Returns a `AnyRealmValue` storing a `List`. + public static func fromArray(_ array: Array) -> AnyRealmValue { + let list = List() + array.forEach { item in + list.append(item) + } + return AnyRealmValue.list(list) + } } diff --git a/RealmSwift/List.swift b/RealmSwift/List.swift index 905dc79c48..a065a35b2e 100644 --- a/RealmSwift/List.swift +++ b/RealmSwift/List.swift @@ -361,6 +361,14 @@ extension List: MutableCollection { } } +// MARK: - Hashable + +extension List: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(rlmArray) + } +} + // MARK: - Codable extension List: Decodable where Element: Decodable { diff --git a/RealmSwift/Map.swift b/RealmSwift/Map.swift index 2f5d51a4a7..d0000085c6 100644 --- a/RealmSwift/Map.swift +++ b/RealmSwift/Map.swift @@ -779,6 +779,14 @@ public final class Map: RLMSwiftColle } } +// MARK: - Hashable + +extension Map: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(keys) + } +} + // MARK: - Codable extension Map: Decodable where Key: Decodable, Value: Decodable { diff --git a/RealmSwift/Object.swift b/RealmSwift/Object.swift index ab98a5b132..5f6c642d46 100644 --- a/RealmSwift/Object.swift +++ b/RealmSwift/Object.swift @@ -760,6 +760,8 @@ public final class DynamicObject: Object { return array.isOptional ? List(collection: array) : List(collection: array) case .object: return List(collection: array) + default: + fatalError() } } @@ -791,6 +793,8 @@ public final class DynamicObject: Object { return set.isOptional ? MutableSet(collection: set) : MutableSet(collection: set) case .object: return MutableSet(collection: set) + default: + fatalError() } } @@ -822,6 +826,8 @@ public final class DynamicObject: Object { return dictionary.isOptional ? Map(objc: dictionary) : Map(objc: dictionary) case .object: return Map(objc: dictionary) + default: + fatalError() } } } diff --git a/RealmSwift/ObjectiveCSupport+AnyRealmValue.swift b/RealmSwift/ObjectiveCSupport+AnyRealmValue.swift index 94fdad29a6..d3390cbf37 100644 --- a/RealmSwift/ObjectiveCSupport+AnyRealmValue.swift +++ b/RealmSwift/ObjectiveCSupport+AnyRealmValue.swift @@ -50,6 +50,10 @@ public extension ObjectiveCSupport { return u as NSUUID case let .object(o): return o + case let .dictionary(d): + return d._rlmObjcValue as? RLMDictionary + case let .list(l): + return l._rlmObjcValue as? RLMArray default: return nil } @@ -120,6 +124,17 @@ public extension ObjectiveCSupport { return .none } return .object(val) + case RLMPropertyType.dictionary: + guard let val = value as? RLMDictionary else { + return .none + } + let d = Map(objc: val) + return AnyRealmValue.dictionary(d) + case RLMPropertyType.list: + guard let val = value as? RLMArray else { + return .none + } + return AnyRealmValue.list(List(collection: val)) default: return .none } diff --git a/RealmSwift/Query.swift b/RealmSwift/Query.swift index 95b7429196..05a2952283 100644 --- a/RealmSwift/Query.swift +++ b/RealmSwift/Query.swift @@ -289,6 +289,23 @@ extension Query where T == Bool { } } +// MARK: Mixed + +extension Query where T == AnyRealmValue { + /// :nodoc: + public subscript(position: Int) -> Query { + .init(appendKeyPath("[\(position)]", options: [.isPath])) + } + /// :nodoc: + public subscript(key: String) -> Query { + .init(appendKeyPath("['\(key)']", options: [.isPath])) + } + /// Query all indexes or keys in a mixed nested collecttion. + public var all: Query { + .init(appendKeyPath("['all']", options: [.isPath])) + } +} + // MARK: OptionalProtocol extension Query where T: OptionalProtocol { @@ -971,7 +988,12 @@ private func buildPredicate(_ root: QueryNode, subqueryCount: Int = 0) -> (Strin if options.contains(.requiresAny) { formatStr.append("ANY ") } - formatStr.append(kp.joined(separator: ".")) + + if options.contains(.isPath) { + formatStr.append(kp.joined()) + } else { + formatStr.append(kp.joined(separator: ".")) + } case .not(let child): if case .keyPath = child, isNewNode { @@ -1017,6 +1039,7 @@ private struct KeyPathOptions: OptionSet { static let isCollection = KeyPathOptions(rawValue: 1) static let requiresAny = KeyPathOptions(rawValue: 2) + static let isPath = KeyPathOptions(rawValue: 4) } private struct SubqueryRewriter { diff --git a/RealmSwift/Tests/AnyRealmValueTests.swift b/RealmSwift/Tests/AnyRealmValueTests.swift index b7e09e4dbd..e66d91507f 100644 --- a/RealmSwift/Tests/AnyRealmValueTests.swift +++ b/RealmSwift/Tests/AnyRealmValueTests.swift @@ -641,6 +641,10 @@ class AnyRealmValueMutableSetTests: AnyRea XCTAssertEqual(kvc as! String, s) case let .uuid(u): XCTAssertEqual(kvc as! UUID, u) + case let .dictionary(d): + XCTAssertEqual(kvc as! Map, d) + case let .list(l): + XCTAssertEqual(kvc as! List, l) } assertThrows(mutableSet.value(forKey: "not self"), named: "NSUnknownKeyException") @@ -937,6 +941,10 @@ class AnyRealmValueMapTests: AnyRealmValue XCTAssertEqual(kvc as! String, s) case let .uuid(u): XCTAssertEqual(kvc as! UUID, u) + case let .dictionary(d): + XCTAssertEqual(kvc as! Map, d) + case let .list(l): + XCTAssertEqual(kvc as! List, l) } } diff --git a/RealmSwift/Tests/KVOTests.swift b/RealmSwift/Tests/KVOTests.swift index 3664d20b67..20194d37ea 100644 --- a/RealmSwift/Tests/KVOTests.swift +++ b/RealmSwift/Tests/KVOTests.swift @@ -53,6 +53,7 @@ class SwiftKVOObject: Object { let otherFloatCol = RealmProperty() let otherDoubleCol = RealmProperty() let otherBoolCol = RealmProperty() + let otherAnyCol = RealmProperty() @objc dynamic var optStringCol: String? @objc dynamic var optBinaryCol: Data? @objc dynamic var optDateCol: Date? @@ -71,6 +72,7 @@ class SwiftKVOObject: Object { let arrayDate = List() let arrayDecimal = List() let arrayObjectId = List() + let arrayAny = List() let arrayOptBool = List() let arrayOptInt8 = List() @@ -97,6 +99,7 @@ class SwiftKVOObject: Object { let setDate = MutableSet() let setDecimal = MutableSet() let setObjectId = MutableSet() + let setAny = MutableSet() let setOptBool = MutableSet() let setOptInt8 = MutableSet() @@ -123,6 +126,7 @@ class SwiftKVOObject: Object { let mapDate = Map() let mapDecimal = Map() let mapObjectId = Map() + let mapAny = Map() let mapOptBool = Map() let mapOptInt8 = Map() @@ -456,6 +460,21 @@ class KVOTests: TestCase { } } + func testCollectionInMixedKVO() { + let (obj, obs) = getObject(SwiftKVOObject()) + + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value = AnyRealmValue.fromDictionary([ + "key1": .int(1234)]) } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.dictionaryValue?["key1"] = .string("hello") } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.dictionaryValue?["key1"] = nil } + + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value = AnyRealmValue.fromArray([.bool(true)]) } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.listValue?[0] = .float(123.456) } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.listValue?.append(.bool(true)) } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.listValue?.insert(.date(Date()), at: 1) } + observeSetChange(obs, "otherAnyCol") { obj.otherAnyCol.value.listValue?.remove(at: 0) } + } + func testTypedObservation() { let (obj, obs) = getObject(SwiftKVOObject()) diff --git a/RealmSwift/Tests/MixedCollectionTest.swift b/RealmSwift/Tests/MixedCollectionTest.swift new file mode 100644 index 0000000000..d20ec0a636 --- /dev/null +++ b/RealmSwift/Tests/MixedCollectionTest.swift @@ -0,0 +1,851 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// 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 XCTest +import RealmSwift + +class MixedCollectionTest: TestCase { + func testAnyMixedDictionary() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + let d = Date.now + let oid = ObjectId.generate() + let uuid = UUID() + + func assertObject(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"], .string("hello")) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key2"], .bool(false)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key3"], .int(234)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key4"], .float(789.123)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key5"], .double(12345.678901)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key6"], .data("a".data(using: .utf8)!)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key7"], .date(d)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key9"], .objectId(oid)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key10"], .uuid(uuid)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key11"], .decimal128(Decimal128(number: 567))) + + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"]?.stringValue, "hello") + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key2"]?.boolValue, false) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key3"]?.intValue, 234) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key4"]?.floatValue, 789.123) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key5"]?.doubleValue, 12345.678901) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key6"]?.dataValue, "a".data(using: .utf8)!) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key7"]?.dateValue, d) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key8"]?.object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key9"]?.objectIdValue, oid) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key10"]?.uuidValue, uuid) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key11"]?.decimal128Value, Decimal128(number: 567)) + } + + let dictionary: Dictionary = [ + "key1": .string("hello"), + "key2": .bool(false), + "key3": .int(234), + "key4": .float(789.123), + "key5": .double(12345.678901), + "key6": .data("a".data(using: .utf8)!), + "key7": .date(d), + "key8": .object(so), + "key9": .objectId(oid), + "key10": .uuid(uuid), + "key11": .decimal128(Decimal128(number: 567)) + ] + + let dictionary1: Dictionary = [ + "key": .none + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + assertObject(o) + // Unmanaged update + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary1) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key"], AnyRealmValue.none) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key"], AnyRealmValue.none) + + // Update managed object + try realm.write { + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary1) + } + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key"], AnyRealmValue.none) + + // Create managed object + try realm.write { + let object = realm.create(AnyRealmTypeObject.self, value: [ "anyValue": dictionary ]) + assertObject(object) + } + + // Results + let result = realm.objects(AnyRealmTypeObject.self).last + XCTAssertNotNil(result) + assertObject(result!) + } + + func testAnyMixedDictionaryUpdateAndDelete() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + + let dictionary: Dictionary = [ + "key1": .string("hello"), + "key2": .bool(false), + "key3": .int(234), + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"], .string("hello")) + + // Unmanaged Update + o.anyValue.value.dictionaryValue?["key1"] = .object(so) + o.anyValue.value.dictionaryValue?["key2"] = .data("a".data(using: .utf8)!) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"]?.object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key2"], .data("a".data(using: .utf8)!)) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"]?.object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key2"], .data("a".data(using: .utf8)!)) + + // Update managed object + try realm.write { + o.anyValue.value.dictionaryValue?["key1"] = .double(12345.678901) + o.anyValue.value.dictionaryValue?["key2"] = .float(789.123) + } + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key1"], .double(12345.678901)) + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key2"], .float(789.123)) + + let result = realm.objects(AnyRealmTypeObject.self).last + XCTAssertNotNil(result) + // Delete + try realm.write { + result?.anyValue.value.dictionaryValue?["key1"] = nil + result?.anyValue.value.dictionaryValue?["key2"] = nil + } + XCTAssertNil(result?.anyValue.value.dictionaryValue?["key1"]) + XCTAssertNil(result?.anyValue.value.dictionaryValue?["key2"]) + } + + func testAnyMixedNestedDictionary() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + + func assertDictionary1(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.dictionaryValue?["key2"]?.dictionaryValue?["key3"]?.object(SwiftStringObject.self)?.stringCol, "hello") + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key4"]?.boolValue, false) + } + + func assertDictionary2(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.dictionaryValue?["key10"]?.dictionaryValue?["key11"]?.dictionaryValue?["key12"]?.dictionaryValue?["key1"]?.dictionaryValue?["key2"]?.dictionaryValue?["key3"]?.object(SwiftStringObject.self)?.stringCol, "hello") + } + + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary([ "key3": .object(so) ]) + let subDict3: AnyRealmValue = AnyRealmValue.fromDictionary([ "key2": subDict2 ]) + let subDict4: AnyRealmValue = AnyRealmValue.fromDictionary([ "key1": subDict3 ]) + let dictionary1: Dictionary = [ + "key0": subDict4, + "key4": .bool(false) + ] + + let subDict5: AnyRealmValue = AnyRealmValue.fromDictionary([ "key12": subDict4 ]) + let subDict6: AnyRealmValue = AnyRealmValue.fromDictionary([ "key11": subDict5 ]) + let dictionary2: Dictionary = [ + "key10": subDict6, + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary2) + // Unamanged Read + assertDictionary2(o) + + // Unmanaged update + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary1) + // Update assert + assertDictionary1(o) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + // Add assert + assertDictionary1(o) + + // Update managed object + try realm.write { + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary2) + } + // Update assert + assertDictionary2(o) + + try realm.write { + let d = [ "key0": [ "key1": [ "key2": [ "key3": AnyRealmValue.object(so)]]], + "key4": AnyRealmValue.bool(false)] + let object = realm.create(AnyRealmTypeObject.self, value: [ "anyValue": d]) + assertDictionary1(object) + } + + // Results + let result = realm.objects(AnyRealmTypeObject.self).last + + // Results assert + XCTAssertNotNil(result) + assertDictionary1(result!) + } + + func testAnyMixedList() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + let d = Date.now + let oid = ObjectId.generate() + let uuid = UUID() + + func assertObject(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.listValue?[0], .string("hello")) + XCTAssertEqual(o.anyValue.value.listValue?[1], .bool(false)) + XCTAssertEqual(o.anyValue.value.listValue?[2], .int(234)) + XCTAssertEqual(o.anyValue.value.listValue?[3], .float(789.123)) + XCTAssertEqual(o.anyValue.value.listValue?[4], .double(12345.678901)) + XCTAssertEqual(o.anyValue.value.listValue?[5], .data("a".data(using: .utf8)!)) + XCTAssertEqual(o.anyValue.value.listValue?[6], .date(d)) + XCTAssertEqual(o.anyValue.value.listValue?[8], .objectId(oid)) + XCTAssertEqual(o.anyValue.value.listValue?[9], .uuid(uuid)) + XCTAssertEqual(o.anyValue.value.listValue?[10], .decimal128(Decimal128(number: 567))) + + XCTAssertEqual(o.anyValue.value.listValue?[0].stringValue, "hello") + XCTAssertEqual(o.anyValue.value.listValue?[1].boolValue, false) + XCTAssertEqual(o.anyValue.value.listValue?[2].intValue, 234) + XCTAssertEqual(o.anyValue.value.listValue?[3].floatValue, 789.123) + XCTAssertEqual(o.anyValue.value.listValue?[4].doubleValue, 12345.678901) + XCTAssertEqual(o.anyValue.value.listValue?[5].dataValue, "a".data(using: .utf8)!) + XCTAssertEqual(o.anyValue.value.listValue?[6].dateValue, d) + XCTAssertEqual(o.anyValue.value.listValue?[7].object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.listValue?[8].objectIdValue, oid) + XCTAssertEqual(o.anyValue.value.listValue?[9].uuidValue, uuid) + XCTAssertEqual(o.anyValue.value.listValue?[10].decimal128Value, Decimal128(number: 567)) + } + + let list: Array = [ + .string("hello"), + .bool(false), + .int(234), + .float(789.123), + .double(12345.678901), + .data("a".data(using: .utf8)!), + .date(d), + .object(so), + .objectId(oid), + .uuid(uuid), + .decimal128(Decimal128(number: 567)) + ] + + let list1: Array = [ + .none + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + let l = AnyRealmValue.fromArray(list) + o.anyValue.value = l + assertObject(o) + // Unmanaged update + o.anyValue.value = AnyRealmValue.fromArray(list1) + XCTAssertEqual(o.anyValue.value.listValue?[0], AnyRealmValue.none) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + XCTAssertEqual(o.anyValue.value.listValue?[0], AnyRealmValue.none) + + // Update managed object + try realm.write { + o.anyValue.value = AnyRealmValue.fromArray(list1) + } + XCTAssertEqual(o.anyValue.value.listValue?[0], AnyRealmValue.none) + + try realm.write { + let object = realm.create(AnyRealmTypeObject.self, value: [ "anyValue": list ]) + assertObject(object) + } + + // Results + let result = realm.objects(AnyRealmTypeObject.self).last + XCTAssertNotNil(result) + assertObject(result!) + } + + func testAnyMixedListUpdateAndDelete() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + + let list: Array = [ + .string("hello"), + .bool(false), + .int(234), + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromArray(list) + XCTAssertEqual(o.anyValue.value.listValue?[0], .string("hello")) + + // Unmanaged Update + o.anyValue.value.listValue?[0] = .object(so) + o.anyValue.value.listValue?[1] = .data("a".data(using: .utf8)!) + XCTAssertEqual(o.anyValue.value.listValue?[0].object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.listValue?[1], .data("a".data(using: .utf8)!)) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + XCTAssertEqual(o.anyValue.value.listValue?[0].object(SwiftStringObject.self)?.stringCol, so.stringCol) + XCTAssertEqual(o.anyValue.value.listValue?[1], .data("a".data(using: .utf8)!)) + + // Update managed object + try realm.write { + o.anyValue.value.listValue?[0] = .double(12345.678901) + o.anyValue.value.listValue?[1] = .float(789.123) + } + XCTAssertEqual(o.anyValue.value.listValue?[0], .double(12345.678901)) + XCTAssertEqual(o.anyValue.value.listValue?[1], .float(789.123)) + + let result = realm.objects(AnyRealmTypeObject.self).last + XCTAssertNotNil(result) + XCTAssertEqual(result?.anyValue.value.listValue?.count, 3) + + // Delete + try realm.write { + result?.anyValue.value.listValue?.remove(at: 0) + result?.anyValue.value.listValue?.remove(at: 0) + } + XCTAssertNotEqual(result?.anyValue.value.listValue?[0], .double(12345.678901)) + XCTAssertEqual(result?.anyValue.value.listValue?[0], .int(234)) + XCTAssertEqual(result?.anyValue.value.listValue?.count, 1) + } + + func testAnyMixedNestedArray() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + + func assertArray1(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.listValue?[0].listValue?[0].listValue?[0].listValue?[0].object(SwiftStringObject.self)?.stringCol, "hello") + XCTAssertEqual(o.anyValue.value.listValue?[1].boolValue, false) + } + + func assertArray2(_ o: AnyRealmTypeObject) { + XCTAssertEqual(o.anyValue.value.listValue?[0].listValue?[0].listValue?[0].listValue?[0].listValue?[0].listValue?[0].object(SwiftStringObject.self)?.stringCol, "hello") + } + + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .object(so) ]) + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ subArray2 ]) + let subArray4: AnyRealmValue = AnyRealmValue.fromArray([ subArray3 ]) + let array1: Array = [ + subArray4, .bool(false) + ] + + let subArray5: AnyRealmValue = AnyRealmValue.fromArray([ subArray4 ]) + let subArray6: AnyRealmValue = AnyRealmValue.fromArray([ subArray5 ]) + let array2: Array = [ + subArray6 + ] + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromArray(array2) + // Unamanged Read + assertArray2(o) + + // Unmanaged update + o.anyValue.value = AnyRealmValue.fromArray(array1) + // Update assert + assertArray1(o) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + // Add assert + assertArray1(o) + + // Update managed object + try realm.write { + o.anyValue.value = AnyRealmValue.fromArray(array2) + } + // Update assert + assertArray2(o) + + try realm.write { + let d = [[[[ AnyRealmValue.object(so)]]], AnyRealmValue.bool(false)] + let object = realm.create(AnyRealmTypeObject.self, value: [ "anyValue": d]) + assertArray1(object) + } + + // Results + let result = realm.objects(AnyRealmTypeObject.self).last + XCTAssertNotNil(result) + assertArray1(result!) + } + + func testMixedNestedCollection() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .object(so) ]) + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary([ "key1": subArray2 ]) + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ subArray2, subDict2]) + let subDict3: AnyRealmValue = AnyRealmValue.fromDictionary([ "key2": subArray3 ]) + let subArray4: AnyRealmValue = AnyRealmValue.fromArray([ subDict3 ]) + let subDict4: AnyRealmValue = AnyRealmValue.fromDictionary([ "key3": subArray4 ]) + let subArray5: AnyRealmValue = AnyRealmValue.fromArray([ subDict4 ]) + let subDict5: AnyRealmValue = AnyRealmValue.fromDictionary([ "key4": subArray5 ]) + let subArray6: AnyRealmValue = AnyRealmValue.fromArray([ subDict5 ]) + let subDict6: AnyRealmValue = AnyRealmValue.fromDictionary([ "key5": subArray6 ]) + let dictionary: Dictionary = [ + "key0": subDict6, + ] + + func assertMixed(_ o: AnyRealmTypeObject) { + let baseNested: List? = o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key5"]?.listValue?[0].dictionaryValue?["key4"]?.listValue?[0].dictionaryValue?["key3"]?.listValue + let nested1: String? = baseNested?[0].dictionaryValue?["key2"]?.listValue?[0].listValue?[0].object(SwiftStringObject.self)?.stringCol + XCTAssertEqual(nested1, "hello") + let nested2: String? = baseNested?[0].dictionaryValue?["key2"]?.listValue?[1].dictionaryValue?["key1"]?.listValue?[0].object(SwiftStringObject.self)?.stringCol + XCTAssertEqual(nested2, "hello") + } + + let o = AnyRealmTypeObject() + // Unmanaged Set + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + // Unamanged Read + assertMixed(o) + + // Unmanaged update + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + // Update assert + assertMixed(o) + + // Add mixed collection to object + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + // Add assert + assertMixed(o) + + // Update managed object + try realm.write { + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + } + // Update assert + assertMixed(o) + + try realm.write { + let d = ["key0": ["key5": [["key4": [["key3": [["key2": [[AnyRealmValue.object(so)], ["key1": [AnyRealmValue.object(so)]]]]]]]]]]] + let object = realm.create(AnyRealmTypeObject.self, value: [ "anyValue": d]) + assertMixed(object) + } + + // Results + let result = realm.objects(AnyRealmTypeObject.self).last + + // Results assert + XCTAssertNotNil(result) + assertMixed(result!) + } + + func testMixedCollectionComparision() throws { + let so = SwiftStringObject() + so.stringCol = "hello" + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .object(so) ]) + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary([ "key1": subArray2 ]) + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ subArray2, subDict2]) + let subDict3: AnyRealmValue = AnyRealmValue.fromDictionary([ "key2": subArray3 ]) + let subArray4: AnyRealmValue = AnyRealmValue.fromArray([ subDict3 ]) + let subDict4: AnyRealmValue = AnyRealmValue.fromDictionary([ "key3": subArray4 ]) + let subArray5: AnyRealmValue = AnyRealmValue.fromArray([ subDict4 ]) + let subDict5: AnyRealmValue = AnyRealmValue.fromDictionary([ "key4": subArray5 ]) + let subArray6: AnyRealmValue = AnyRealmValue.fromArray([ subDict5 ]) + let subDict6: AnyRealmValue = AnyRealmValue.fromDictionary([ "key5": subArray6 ]) + let dictionary: Dictionary = [ + "key0": subDict6, + ] + + XCTAssertEqual(AnyRealmValue.fromDictionary([:]), AnyRealmValue.fromDictionary([:])) // Empty mixed lists should be equal + XCTAssertEqual(AnyRealmValue.fromDictionary(dictionary), AnyRealmValue.fromDictionary(dictionary)) // Unamanged mixed list should be equal + + var dictionary3 = dictionary + dictionary3["newKey"] = .bool(false) + XCTAssertNotEqual(AnyRealmValue.fromDictionary(dictionary), AnyRealmValue.fromDictionary(dictionary3)) + + let mixedObject = AnyRealmTypeObject() + mixedObject.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + XCTAssertEqual(mixedObject.anyValue.value, AnyRealmValue.fromDictionary(dictionary)) + + let mixedObject2 = mixedObject + XCTAssertEqual(mixedObject2.anyValue.value, AnyRealmValue.fromDictionary(dictionary)) + + XCTAssertEqual(mixedObject.anyValue.value, mixedObject2.anyValue.value) + XCTAssertEqual(mixedObject.anyValue, mixedObject2.anyValue, "instances should be equal by `==` operator") + XCTAssertTrue(mixedObject.isEqual(mixedObject2), "instances should be equal by `isEqual` method") + + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue, mixedObject.anyValue.value.dictionaryValue) + + let realm = realmWithTestPath() + try realm.write { + realm.add(mixedObject) + realm.add(mixedObject2) + } + + XCTAssertEqual(mixedObject.anyValue.value, mixedObject2.anyValue.value) + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key0"], mixedObject2.anyValue.value.dictionaryValue?["key0"]) + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key5"]?.listValue?[0], mixedObject2.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key5"]?.listValue?[0]) + + let mixedObject3 = AnyRealmTypeObject() + mixedObject3.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + + try realm.write { + realm.add(mixedObject3) + } + + XCTAssertNotEqual(mixedObject.anyValue.value, mixedObject3.anyValue.value) + XCTAssertNotEqual(mixedObject.anyValue.value.dictionaryValue?["key0"], mixedObject3.anyValue.value.dictionaryValue?["key0"]) + XCTAssertNotEqual(mixedObject.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key5"]?.listValue?[0], mixedObject3.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key5"]?.listValue?[0]) + } + + + func testMixedCollectionObjectNotifications() throws { + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ .int(3) ]) + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ subArray3 ]) + let subDict1: AnyRealmValue = AnyRealmValue.fromDictionary([ "key1": subArray2 ]) + let dictionary: Dictionary = [ + "key0": subDict1, + ] + + func expectChange(_ name: String) -> ((ObjectChange) -> Void) { + let exp = expectation(description: "Object changes for mixed collections") + return { change in + if case .change(_, let properties) = change { + XCTAssertEqual(properties.count, 1) + if let prop = properties.first { + XCTAssertEqual(prop.name, name) + } + } else { + XCTFail("expected .change, got \(change)") + } + exp.fulfill() + } + } + + func assertObjectNotification(_ object: Object, block: @escaping () -> Void) { + let token = object.observe(expectChange("anyValue")) + let realm = realmWithTestPath() + try! realm.write { + block() + } + + waitForExpectations(timeout: 2) + token.invalidate() + } + + let o = AnyRealmTypeObject() + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + + assertObjectNotification(o, block: { + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.listValue?[0].listValue?[0] = .bool(true) + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.listValue?.append(.float(33.33)) + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.listValue?.removeLast() + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.listValue?.removeAll() + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key2"] = .string("nowhere") + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key2"] = nil + }) + + assertObjectNotification(o, block: { + o.anyValue.value.dictionaryValue?.removeAll() + }) + } + + func testMixedCollectionDictionaryNotifications() throws { + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary([ "key5": .float(43) ]) + let subDict1: AnyRealmValue = AnyRealmValue.fromDictionary([ "key1": subDict2 ]) + let dictionary: Dictionary = [ + "key0": subDict1, + "key3": .decimal128(Decimal128(1)), + ] + + func expectChanges(_ deletions: [String], _ insertions: [String], _ modifications: [String]) -> ((RealmMapChange>) -> Void) { + let exp = expectation(description: "Dictionary changes for mixed collections") + return { change in + switch change { + case .initial: + break + case .update(_, deletions: let d, insertions: let i, modifications: let m): + XCTAssertEqual(d.count, deletions.count) + XCTAssertEqual(d, deletions) + XCTAssertEqual(i.count, insertions.count) + XCTAssertEqual(i, insertions) + XCTAssertEqual(m.count, modifications.count) + XCTAssertEqual(m, modifications) + exp.fulfill() + case .error(let error): + XCTFail("Unexpected error \(error)") + } + } + } + + func assertDictionaryNotification(_ dictionary: Map?, deletions: [String], insertions: [String], modifications: [String], block: @escaping () -> Void) { + let token = dictionary?.observe(expectChanges(deletions, insertions, modifications)) + let realm = realmWithTestPath() + try! realm.write { + block() + } + + waitForExpectations(timeout: 2) + token?.invalidate() + } + + let o = AnyRealmTypeObject() + o.anyValue.value = AnyRealmValue.fromDictionary(dictionary) + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + + assertDictionaryNotification(o.anyValue.value.dictionaryValue, deletions: [], insertions: ["key2"], modifications: [], block: { + o.anyValue.value.dictionaryValue?["key2"] = AnyRealmValue.fromDictionary(dictionary) + }) + + assertDictionaryNotification(o.anyValue.value.dictionaryValue, deletions: [], insertions: ["key10"], modifications: [], block: { + o.anyValue.value.dictionaryValue?["key10"] = AnyRealmValue.fromDictionary(dictionary) + }) + + assertDictionaryNotification(o.anyValue.value.dictionaryValue?["key10"]?.dictionaryValue, deletions: [], insertions: [], modifications: ["key0"], block: { + o.anyValue.value.dictionaryValue?["key10"]?.dictionaryValue?["key0"] = .string("new") + }) + + assertDictionaryNotification(o.anyValue.value.dictionaryValue, deletions: ["key3"], insertions: [], modifications: [], block: { + o.anyValue.value.dictionaryValue?["key3"] = nil + }) + + assertDictionaryNotification(o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.dictionaryValue, deletions: [], insertions: ["key6"], modifications: [], block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.dictionaryValue?["key6"] = .date(Date()) + }) + + assertDictionaryNotification(o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.dictionaryValue, deletions: ["key5", "key6"], insertions: [], modifications: [], block: { + o.anyValue.value.dictionaryValue?["key0"]?.dictionaryValue?["key1"]?.dictionaryValue?.removeAll() + }) + } + + func testMixedCollectionArrayNotifications() throws { + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .float(43), .string("lunch"), .double(12.34) ]) + let subArray1: AnyRealmValue = AnyRealmValue.fromArray([ subArray2, .bool(false) ]) + let array: Array = [ + subArray1, .decimal128(Decimal128(1)), + ] + + func expectChanges(_ deletions: [Int], _ insertions: [Int], _ modifications: [Int]) -> ((RealmCollectionChange>) -> Void) { + let exp = expectation(description: "Dictionary changes for mixed collections") + return { change in + switch change { + case .initial: + break + case .update(_, deletions: let d, insertions: let i, modifications: let m): + XCTAssertEqual(d.count, deletions.count) + XCTAssertEqual(d, deletions) + XCTAssertEqual(i.count, insertions.count) + XCTAssertEqual(i, insertions) + XCTAssertEqual(m.count, modifications.count) + XCTAssertEqual(m, modifications) + exp.fulfill() + case .error(let error): + XCTFail("Unexpected error \(error)") + } + } + } + + func assertDictionaryNotification(_ list: List?, deletions: [Int], insertions: [Int], modifications: [Int], block: @escaping () -> Void) { + let token = list?.observe(expectChanges(deletions, insertions, modifications)) + let realm = realmWithTestPath() + try! realm.write { + block() + } + + waitForExpectations(timeout: 2) + token?.invalidate() + } + + let o = AnyRealmTypeObject() + o.anyValue.value = AnyRealmValue.fromArray(array) + let realm = realmWithTestPath() + try realm.write { + realm.add(o) + } + + assertDictionaryNotification(o.anyValue.value.listValue, deletions: [], insertions: [2], modifications: [], block: { + o.anyValue.value.listValue?.append(AnyRealmValue.fromArray(array)) + }) + + assertDictionaryNotification(o.anyValue.value.listValue?[0].listValue, deletions: [], insertions: [], modifications: [1], block: { + o.anyValue.value.listValue?[0].listValue?[1] = .objectId(ObjectId.generate()) + }) + + assertDictionaryNotification(o.anyValue.value.listValue?[0].listValue?[0].listValue, deletions: [2], insertions: [], modifications: [], block: { + o.anyValue.value.listValue?[0].listValue?[0].listValue?.removeLast() + }) + + assertDictionaryNotification(o.anyValue.value.listValue?[0].listValue?[0].listValue, deletions: [0, 1], insertions: [], modifications: [], block: { + o.anyValue.value.listValue?[0].listValue?[0].listValue?.removeAll() + }) + } + + func testReassignToMixedList() throws { + let list = AnyRealmValue.fromArray([.bool(true), AnyRealmValue.fromDictionary(["key": .int(12)]), .float(13.12)]) + + let mixedObject = AnyRealmTypeObject() + mixedObject.anyValue.value = list + + let realm = realmWithTestPath() + try realm.write { + realm.add(mixedObject) + } + XCTAssertEqual(mixedObject.anyValue.value.listValue?[1].dictionaryValue?["key"], .int(12)) + + try realm.write { + mixedObject.anyValue.value.listValue?.append(AnyRealmValue.fromArray([.double(20.20), .string("hello")])) + } + XCTAssertEqual(mixedObject.anyValue.value.listValue?[3].listValue?[0], .double(20.20)) + + try realm.write { + let listItem = mixedObject.anyValue.value.listValue?[0] + mixedObject.anyValue.value.listValue?.append(listItem!) + } + XCTAssertEqual(mixedObject.anyValue.value.listValue?[4], .bool(true)) + + try realm.write { + let listItem = mixedObject.anyValue.value.listValue?[3] + mixedObject.anyValue.value.listValue?.append(listItem!) + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.listValue?[4].listValue?[0], .double(20.20)) + + try realm.write { + let listItem = mixedObject.anyValue.value.listValue?[3] + mixedObject.anyValue.value.listValue?[3] = listItem! + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.listValue?[3].listValue?[0], .double(20.20)) + + try realm.write { + mixedObject.anyValue.value.listValue?[1] = AnyRealmValue.fromDictionary(["new-key": .int(3)]) + } + XCTAssertEqual(mixedObject.anyValue.value.listValue?[1].dictionaryValue?["new-key"], .int(3)) + + try realm.write { + let listItem = mixedObject.anyValue.value.listValue?[1] + mixedObject.anyValue.value.listValue?.append(listItem!) + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.listValue?[3].dictionaryValue?["new-key"], .int(3)) + + try realm.write { + let listItem = mixedObject.anyValue.value.listValue?[1] + mixedObject.anyValue.value.listValue?[3] = listItem! + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.listValue?[3].dictionaryValue?["new-key"], .int(3)) + } + + func testReassignToMixedDictionary() throws { + let dictionary = AnyRealmValue.fromDictionary(["key1": .bool(true), "key2": AnyRealmValue.fromDictionary(["key4": .int(12)]), "key3": .float(13.12)]) + + let mixedObject = AnyRealmTypeObject() + mixedObject.anyValue.value = dictionary + + let realm = realmWithTestPath() + try realm.write { + realm.add(mixedObject) + } + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key2"]?.dictionaryValue?["key4"], .int(12)) + + try realm.write { + mixedObject.anyValue.value.dictionaryValue?["key4"] = AnyRealmValue.fromDictionary(["new-key": .int(3)]) + } + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key4"]?.dictionaryValue?["new-key"], .int(3)) + + try realm.write { + let dictItem = mixedObject.anyValue.value.dictionaryValue?["key1"] + mixedObject.anyValue.value.dictionaryValue?["key5"] = dictItem + } + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key5"], .bool(true)) + + try realm.write { + let dictItem = mixedObject.anyValue.value.dictionaryValue?["key2"] + mixedObject.anyValue.value.dictionaryValue?["key6"] = dictItem + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key6"]?.dictionaryValue?["key4"], .int(12)) + + try realm.write { + mixedObject.anyValue.value.dictionaryValue?["key7"] = AnyRealmValue.fromArray([.string("hello"), .double(20.20)]) + } + XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key7"]?.listValue?[0], .string("hello")) + + try realm.write { + let dictItem = mixedObject.anyValue.value.dictionaryValue?["key7"] + mixedObject.anyValue.value.dictionaryValue?["key2"] = dictItem + } + // TODO: Self-assignment - this doesn't work due to https://github.com/realm/realm-core/issues/7422 +// XCTAssertEqual(mixedObject.anyValue.value.dictionaryValue?["key2"]?.listValue?[0], .string("hello")) + } +} diff --git a/RealmSwift/Tests/ModernKVOTests.swift b/RealmSwift/Tests/ModernKVOTests.swift index 611854c076..0d8ca9afc6 100644 --- a/RealmSwift/Tests/ModernKVOTests.swift +++ b/RealmSwift/Tests/ModernKVOTests.swift @@ -367,6 +367,21 @@ class ModernKVOTests: TestCase { } } + func testCollectionInMixedKVO() { + let (obj, obs) = getObject(ModernAllTypesObject()) + + observeSetChange(obs, "anyCol") { obj.anyCol = AnyRealmValue.fromDictionary([ + "key1": .int(1234)]) } + observeSetChange(obs, "anyCol") { obj.anyCol.dictionaryValue?["key1"] = .string("hello") } + observeSetChange(obs, "anyCol") { obj.anyCol.dictionaryValue?["key1"] = nil } + + observeSetChange(obs, "anyCol") { obj.anyCol = AnyRealmValue.fromArray([ .int(1234)]) } + observeSetChange(obs, "anyCol") { obj.anyCol.listValue?[0] = .float(123.456) } + observeSetChange(obs, "anyCol") { obj.anyCol.listValue?.append(.bool(true)) } + observeSetChange(obs, "anyCol") { obj.anyCol.listValue?.insert(.date(Date()), at: 1) } + observeSetChange(obs, "anyCol") { obj.anyCol.listValue?.remove(at: 0) } + } + func testReadSharedSchemaFromObservedObject() { let obj = ModernAllTypesObject() obj.addObserver(self, forKeyPath: "boolCol", options: [.old, .new], context: nil) diff --git a/RealmSwift/Tests/ObjectCreationTests.swift b/RealmSwift/Tests/ObjectCreationTests.swift index 4797cec4b3..483e9fdb92 100644 --- a/RealmSwift/Tests/ObjectCreationTests.swift +++ b/RealmSwift/Tests/ObjectCreationTests.swift @@ -70,54 +70,6 @@ class ObjectCreationTests: TestCase { SwiftOptionalDefaultValuesObject.defaultValues(), boolObjectValue: true) } - func testInitWithDictionary() { - // dictionary with all values specified - let baselineValues: [String: Any] = - ["boolCol": true, - "intCol": 1, - "int8Col": 1 as Int8, - "int16Col": 1 as Int16, - "int32Col": 1 as Int32, - "int64Col": 1 as Int64, - "floatCol": 1.1 as Float, - "doubleCol": 11.1, - "stringCol": "b", - "binaryCol": "b".data(using: String.Encoding.utf8)!, - "dateCol": Date(timeIntervalSince1970: 2), - "decimalCol": 3 as Decimal128, - "objectIdCol": ObjectId.generate(), - "objectCol": SwiftBoolObject(value: [true]), - "uuidCol": UUID(uuidString: "137decc8-b300-4954-a233-f89909f4fd89")!, - "anyCol": AnyRealmValue.string("hello"), - "arrayCol": [SwiftBoolObject(value: [true]), SwiftBoolObject()], - "setCol": [SwiftBoolObject(value: [true]), SwiftBoolObject()], - "mapCol": ["trueVal": SwiftBoolObject(value: [true]), "falseVal": SwiftBoolObject(value: [false])] - ] - - // test with valid dictionary literals - let props = try! Realm().schema["SwiftObject"]!.properties - for propNum in 0..()] case .linkingObjects: fatalError("not supported") case .UUID: return ["invalid"] + default: + fatalError() } } } diff --git a/RealmSwift/Tests/QueryTests.swift b/RealmSwift/Tests/QueryTests.swift index 1e3e7cd86a..1145ff25e2 100644 --- a/RealmSwift/Tests/QueryTests.swift +++ b/RealmSwift/Tests/QueryTests.swift @@ -59,10 +59,10 @@ class QueryTests: TestCase { override func setUp() { realm = inMemoryRealm("QueryTests") try! realm.write { - let objModernCollectionsOfEnums = ModernCollectionsOfEnums() let objCustomPersistableCollections = CustomPersistableCollections() - let objModernAllTypesObject = ModernAllTypesObject() + let objModernCollectionsOfEnums = ModernCollectionsOfEnums() let objAllCustomPersistableTypes = AllCustomPersistableTypes() + let objModernAllTypesObject = ModernAllTypesObject() objModernAllTypesObject.boolCol = false objModernAllTypesObject.intCol = 3 @@ -420,10 +420,10 @@ class QueryTests: TestCase { objCustomPersistableCollections.mapOptUuid["foo"] = UUIDWrapper(persistedValue: UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!) objCustomPersistableCollections.mapOptUuid["bar"] = UUIDWrapper(persistedValue: UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09f")!) - realm.add(objModernCollectionsOfEnums) realm.add(objAllCustomPersistableTypes) - realm.add(objModernAllTypesObject) + realm.add(objModernCollectionsOfEnums) realm.add(objCustomPersistableCollections) + realm.add(objModernAllTypesObject) } } @@ -435,22 +435,22 @@ class QueryTests: TestCase { realm.beginWrite() realm.deleteAll() - let parentLinkToModernCollectionsOfEnums = realm.create(LinkToModernCollectionsOfEnums.self) - let childrenModernCollectionsOfEnums = [ModernCollectionsOfEnums(), ModernCollectionsOfEnums(), ModernCollectionsOfEnums()] - parentLinkToModernCollectionsOfEnums.list.append(objectsIn: childrenModernCollectionsOfEnums) - let parentLinkToCustomPersistableCollections = realm.create(LinkToCustomPersistableCollections.self) let childrenCustomPersistableCollections = [CustomPersistableCollections(), CustomPersistableCollections(), CustomPersistableCollections()] parentLinkToCustomPersistableCollections.list.append(objectsIn: childrenCustomPersistableCollections) - let parentLinkToModernAllTypesObject = realm.create(LinkToModernAllTypesObject.self) - let childrenModernAllTypesObject = [ModernAllTypesObject(), ModernAllTypesObject(), ModernAllTypesObject()] - parentLinkToModernAllTypesObject.list.append(objectsIn: childrenModernAllTypesObject) + let parentLinkToModernCollectionsOfEnums = realm.create(LinkToModernCollectionsOfEnums.self) + let childrenModernCollectionsOfEnums = [ModernCollectionsOfEnums(), ModernCollectionsOfEnums(), ModernCollectionsOfEnums()] + parentLinkToModernCollectionsOfEnums.list.append(objectsIn: childrenModernCollectionsOfEnums) let parentLinkToAllCustomPersistableTypes = realm.create(LinkToAllCustomPersistableTypes.self) let childrenAllCustomPersistableTypes = [AllCustomPersistableTypes(), AllCustomPersistableTypes(), AllCustomPersistableTypes()] parentLinkToAllCustomPersistableTypes.list.append(objectsIn: childrenAllCustomPersistableTypes) + let parentLinkToModernAllTypesObject = realm.create(LinkToModernAllTypesObject.self) + let childrenModernAllTypesObject = [ModernAllTypesObject(), ModernAllTypesObject(), ModernAllTypesObject()] + parentLinkToModernAllTypesObject.list.append(objectsIn: childrenModernAllTypesObject) + initForKeypathCollectionAggregates(childrenModernAllTypesObject, \.intCol) initForKeypathCollectionAggregates(childrenModernAllTypesObject, \.int8Col) @@ -986,6 +986,328 @@ class QueryTests: TestCase { } } + func testEqualAnyRealmList() { + let circleObject = self.circleObject + let object = objects()[0] + let list = List() + list.removeAll() + list.append(.none) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.none) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.int(123)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.int(123)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.bool(true)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.bool(true)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.float(123.456)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.float(123.456)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.double(123.456)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.double(123.456)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.string("FooBar")) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.string("FooBar")) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.data(Data(count: 64))) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.data(Data(count: 64))) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.date(Date(timeIntervalSince1970: 1000000))) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.date(Date(timeIntervalSince1970: 1000000))) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.object(circleObject)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.object(circleObject)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.objectId(ObjectId("61184062c1d8f096a3695046"))) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.objectId(ObjectId("61184062c1d8f096a3695046"))) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.decimal128(123.456)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.decimal128(123.456)) + return $0.anyCol == .list(list) + } + list.removeAll() + list.append(.uuid(UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!)) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(.uuid(UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!)) + return $0.anyCol == .list(list) + } + } + + func testEqualAnyRealmDictionary() { + let circleObject = self.circleObject + let object = objects()[0] + let dictionary = Map() + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.none + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.none + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.int(123) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.int(123) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.bool(true) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.bool(true) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.float(123.456) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.float(123.456) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.double(123.456) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.double(123.456) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.string("FooBar") + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.string("FooBar") + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.data(Data(count: 64)) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.data(Data(count: 64)) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.date(Date(timeIntervalSince1970: 1000000)) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.date(Date(timeIntervalSince1970: 1000000)) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.object(circleObject) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.object(circleObject) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.objectId(ObjectId("61184062c1d8f096a3695046")) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.objectId(ObjectId("61184062c1d8f096a3695046")) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.decimal128(123.456) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.decimal128(123.456) + return $0.anyCol == .dictionary(dictionary) + } + dictionary.removeAll() + dictionary["key"] = AnyRealmValue.uuid(UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!) + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue.uuid(UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!) + return $0.anyCol == .dictionary(dictionary) + } + } + + func testNestedAnyRealmList() { + let object = objects()[0] + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .string("john"), .bool(false) ]) + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ subArray2, .double(76.54) ]) + let subArray4: AnyRealmValue = AnyRealmValue.fromArray([ subArray3, .int(99)]) + let array: Array = [ + subArray4, .none + ] + + setAnyRealmValueCol(with: AnyRealmValue.fromArray(array), object: object) + assertQuery("(anyCol[1] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol[1] == .none + } + + assertQuery("(anyCol['all'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol.all == .none + } + + assertQuery("(anyCol[0][1] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] == .int(99) + } + + assertQuery("(anyCol[0]['all'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0].all == .int(99) + } + + assertQuery("(anyCol[0][0][1] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol[0][0][1] == .double(76.54) + } + + assertQuery("(anyCol[0][0]['all'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol[0][0].all == .double(76.54) + } + + assertQuery("(anyCol[0][0][0][0] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol[0][0][0][0] == .string("john") + } + + assertQuery("(anyCol[0][0][0][1] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol[0][0][0][1] == .bool(false) + } + + assertQuery("(anyCol[0][0][0]['all'] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol[0][0][0].all == .string("john") + } + + assertQuery("(anyCol[0][0][0]['all'] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol[0][0][0].all == .bool(false) + } + + assertQuery("(anyCol[0][1] >= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] >= .int(99) + } + + assertQuery("(anyCol[0][1] <= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] <= .int(99) + } + + assertQuery("(anyCol[0][1] != %@)", AnyRealmValue.int(99), count: 0) { + $0.anyCol[0][1] != .int(99) + } + } + + func testNestedAnyRealmDictionary() { + let object = objects()[0] + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary(["key6": .string("john"), "key7": .bool(false)]) + let subDict3: AnyRealmValue = AnyRealmValue.fromDictionary(["key4": subDict2, "key5": .double(76.54)]) + let subDict4: AnyRealmValue = AnyRealmValue.fromDictionary(["key2": subDict3, "key3": .int(99)]) + let dict: Dictionary = [ + "key0": subDict4, "key1": .none + ] + + setAnyRealmValueCol(with: AnyRealmValue.fromDictionary(dict), object: object) + assertQuery("(anyCol['key1'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol["key1"] == .none + } + + assertQuery("(anyCol['all'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol.all == .none + } + + assertQuery("(anyCol['key0']['key3'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"]["key3"] == .int(99) + } + + assertQuery("(anyCol['key0']['all'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all == .int(99) + } + + assertQuery("(anyCol['key0']['key2']['key5'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol["key0"]["key2"]["key5"] == .double(76.54) + } + + assertQuery("(anyCol['key0']['key2']['all'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol["key0"]["key2"].all == .double(76.54) + } + + assertQuery("(anyCol['key0']['key2']['key4']['key6'] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol["key0"]["key2"]["key4"]["key6"] == .string("john") + } + + assertQuery("(anyCol['key0']['key2']['key4']['key7'] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol["key0"]["key2"]["key4"]["key7"] == .bool(false) + } + + assertQuery("(anyCol['key0']['all'] >= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all >= .int(99) + } + + assertQuery("(anyCol['key0']['all'] <= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all <= .int(99) + } + + assertQuery("(anyCol['key0']['key3'] != %@)", AnyRealmValue.int(99), count: 0) { + $0.anyCol["key0"]["key3"] != .int(99) + } + } + func testEqualObject() { let nestedObject = ModernAllTypesObject() let object = objects().first! @@ -3685,23 +4007,17 @@ class QueryTests: TestCase { func testCollectionFromProperty() { try! realm.write { - let objModernCollectionsOfEnums = realm.objects(ModernCollectionsOfEnums.self).first! - _ = realm.create(LinkToModernCollectionsOfEnums.self, value: [ - "list": [objModernCollectionsOfEnums], - "set": [objModernCollectionsOfEnums], - "map": ["foo": objModernCollectionsOfEnums] - ]) let objCustomPersistableCollections = realm.objects(CustomPersistableCollections.self).first! _ = realm.create(LinkToCustomPersistableCollections.self, value: [ "list": [objCustomPersistableCollections], "set": [objCustomPersistableCollections], "map": ["foo": objCustomPersistableCollections] ]) - let objModernAllTypesObject = realm.objects(ModernAllTypesObject.self).first! - _ = realm.create(LinkToModernAllTypesObject.self, value: [ - "list": [objModernAllTypesObject], - "set": [objModernAllTypesObject], - "map": ["foo": objModernAllTypesObject] + let objModernCollectionsOfEnums = realm.objects(ModernCollectionsOfEnums.self).first! + _ = realm.create(LinkToModernCollectionsOfEnums.self, value: [ + "list": [objModernCollectionsOfEnums], + "set": [objModernCollectionsOfEnums], + "map": ["foo": objModernCollectionsOfEnums] ]) let objAllCustomPersistableTypes = realm.objects(AllCustomPersistableTypes.self).first! _ = realm.create(LinkToAllCustomPersistableTypes.self, value: [ @@ -3709,6 +4025,12 @@ class QueryTests: TestCase { "set": [objAllCustomPersistableTypes], "map": ["foo": objAllCustomPersistableTypes] ]) + let objModernAllTypesObject = realm.objects(ModernAllTypesObject.self).first! + _ = realm.create(LinkToModernAllTypesObject.self, value: [ + "list": [objModernAllTypesObject], + "set": [objModernAllTypesObject], + "map": ["foo": objModernAllTypesObject] + ]) } func test( @@ -9476,10 +9798,10 @@ private protocol LinkToTestObject: Object { var set: MutableSet { get } var map: Map { get } } -extension LinkToModernCollectionsOfEnums: LinkToTestObject {} extension LinkToCustomPersistableCollections: LinkToTestObject {} -extension LinkToModernAllTypesObject: LinkToTestObject {} +extension LinkToModernCollectionsOfEnums: LinkToTestObject {} extension LinkToAllCustomPersistableTypes: LinkToTestObject {} +extension LinkToModernAllTypesObject: LinkToTestObject {} private protocol QueryValue { static func queryValues() -> [Self] diff --git a/RealmSwift/Tests/QueryTests.swift.gyb b/RealmSwift/Tests/QueryTests.swift.gyb index 3213a68308..ccefa35077 100644 --- a/RealmSwift/Tests/QueryTests.swift.gyb +++ b/RealmSwift/Tests/QueryTests.swift.gyb @@ -469,6 +469,156 @@ class QueryTests: TestCase { % end } + func testEqualAnyRealmList() { + let circleObject = self.circleObject + let object = objects()[0] + let list = List() + % for value in anyRealmValues: + list.removeAll() + list.append(${value.value(0)}) + setAnyRealmValueCol(with: .list(list), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.list(list), count: 1) { + let list = List() + list.append(${value.value(0)}) + return $0.anyCol == .list(list) + } + % end + } + + func testEqualAnyRealmDictionary() { + let circleObject = self.circleObject + let object = objects()[0] + let dictionary = Map() + % for value in anyRealmValues: + dictionary.removeAll() + dictionary["key"] = AnyRealmValue${value.value(0)} + setAnyRealmValueCol(with: .dictionary(dictionary), object: object) + assertQuery("(anyCol == %@)", AnyRealmValue.dictionary(dictionary), count: 1) { + let dictionary = Map() + dictionary["key"] = AnyRealmValue${value.value(0)} + return $0.anyCol == .dictionary(dictionary) + } + % end + } + + func testNestedAnyRealmList() { + let object = objects()[0] + let subArray2: AnyRealmValue = AnyRealmValue.fromArray([ .string("john"), .bool(false) ]) + let subArray3: AnyRealmValue = AnyRealmValue.fromArray([ subArray2, .double(76.54) ]) + let subArray4: AnyRealmValue = AnyRealmValue.fromArray([ subArray3, .int(99)]) + let array: Array = [ + subArray4, .none + ] + + setAnyRealmValueCol(with: AnyRealmValue.fromArray(array), object: object) + assertQuery("(anyCol[1] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol[1] == .none + } + + assertQuery("(anyCol['all'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol.all == .none + } + + assertQuery("(anyCol[0][1] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] == .int(99) + } + + assertQuery("(anyCol[0]['all'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0].all == .int(99) + } + + assertQuery("(anyCol[0][0][1] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol[0][0][1] == .double(76.54) + } + + assertQuery("(anyCol[0][0]['all'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol[0][0].all == .double(76.54) + } + + assertQuery("(anyCol[0][0][0][0] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol[0][0][0][0] == .string("john") + } + + assertQuery("(anyCol[0][0][0][1] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol[0][0][0][1] == .bool(false) + } + + assertQuery("(anyCol[0][0][0]['all'] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol[0][0][0].all == .string("john") + } + + assertQuery("(anyCol[0][0][0]['all'] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol[0][0][0].all == .bool(false) + } + + assertQuery("(anyCol[0][1] >= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] >= .int(99) + } + + assertQuery("(anyCol[0][1] <= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol[0][1] <= .int(99) + } + + assertQuery("(anyCol[0][1] != %@)", AnyRealmValue.int(99), count: 0) { + $0.anyCol[0][1] != .int(99) + } + } + + func testNestedAnyRealmDictionary() { + let object = objects()[0] + let subDict2: AnyRealmValue = AnyRealmValue.fromDictionary(["key6": .string("john"), "key7": .bool(false)]) + let subDict3: AnyRealmValue = AnyRealmValue.fromDictionary(["key4": subDict2, "key5": .double(76.54)]) + let subDict4: AnyRealmValue = AnyRealmValue.fromDictionary(["key2": subDict3, "key3": .int(99)]) + let dict: Dictionary = [ + "key0": subDict4, "key1": .none + ] + + setAnyRealmValueCol(with: AnyRealmValue.fromDictionary(dict), object: object) + assertQuery("(anyCol['key1'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol["key1"] == .none + } + + assertQuery("(anyCol['all'] == %@)", AnyRealmValue.none, count: 1) { + $0.anyCol.all == .none + } + + assertQuery("(anyCol['key0']['key3'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"]["key3"] == .int(99) + } + + assertQuery("(anyCol['key0']['all'] == %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all == .int(99) + } + + assertQuery("(anyCol['key0']['key2']['key5'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol["key0"]["key2"]["key5"] == .double(76.54) + } + + assertQuery("(anyCol['key0']['key2']['all'] == %@)", AnyRealmValue.double(76.54), count: 1) { + $0.anyCol["key0"]["key2"].all == .double(76.54) + } + + assertQuery("(anyCol['key0']['key2']['key4']['key6'] == %@)", AnyRealmValue.string("john"), count: 1) { + $0.anyCol["key0"]["key2"]["key4"]["key6"] == .string("john") + } + + assertQuery("(anyCol['key0']['key2']['key4']['key7'] == %@)", AnyRealmValue.bool(false), count: 1) { + $0.anyCol["key0"]["key2"]["key4"]["key7"] == .bool(false) + } + + assertQuery("(anyCol['key0']['all'] >= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all >= .int(99) + } + + assertQuery("(anyCol['key0']['all'] <= %@)", AnyRealmValue.int(99), count: 1) { + $0.anyCol["key0"].all <= .int(99) + } + + assertQuery("(anyCol['key0']['key3'] != %@)", AnyRealmValue.int(99), count: 0) { + $0.anyCol["key0"]["key3"] != .int(99) + } + } + func testEqualObject() { let nestedObject = ModernAllTypesObject() let object = objects().first!