From c3bad4f1230ec91528514316d340b8ad804e4b89 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 10 Jan 2024 21:46:34 +0100 Subject: [PATCH 01/18] Initial stab at collections in mixed --- common/lib/src/realm_types.dart | 45 ++++--- lib/src/list.dart | 15 +-- lib/src/map.dart | 9 +- lib/src/native/realm_bindings.dart | 17 +-- lib/src/native/realm_core.dart | 186 ++++++++++++++++++++++++---- lib/src/realm_object.dart | 5 + lib/src/set.dart | 17 +++ src/realm-core | 2 +- test/realm_value_test.dart | 189 ++++++++++++++++++++++++++++- test/realm_value_test.g.dart | 28 ++++- 10 files changed, 445 insertions(+), 68 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 5f57e59cf..53fcccfde 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -189,8 +189,13 @@ abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {} class RealmValue { final Object? value; Type get type => value.runtimeType; + T as() => value as T; // better for code completion + List asList() => as>(); + + Map asMap() => as>(); + // This is private, so user cannot accidentally construct an invalid instance const RealmValue._(this.value); @@ -206,25 +211,33 @@ class RealmValue { const RealmValue.decimal128(Decimal128 decimal) : this._(decimal); const RealmValue.uuid(Uuid uuid) : this._(uuid); const RealmValue.uint8List(Uint8List binary) : this._(binary); + const RealmValue.list(List list) : this._(list); + const RealmValue.map(Map map) : this._(map); /// Will throw [ArgumentError] factory RealmValue.from(Object? object) { - if (object == null || - object is bool || - object is String || - object is int || - object is Float || - object is double || - object is RealmObjectMarker || - object is DateTime || - object is ObjectId || - object is Decimal128 || - object is Uuid || - object is Uint8List) { - return RealmValue._(object); - } else { - throw ArgumentError.value(object, 'object', 'Unsupported type'); - } + return switch (object) { + Object? o + when o == null || + o is bool || + o is String || + o is int || + o is double || + o is RealmObjectMarker || + o is DateTime || + o is ObjectId || + o is Decimal128 || + o is Uuid || + o is Uint8List => + RealmValue._(o), + Map d => RealmValue.map(d), + Map d => RealmValue.map(d.map((key, value) => MapEntry(key, RealmValue.from(value)))), + List l => RealmValue.list(l), + List l => RealmValue.list(l.map((o) => RealmValue.from(o)).toList()), + Iterable i => RealmValue.list(i.toList()), + Iterable i => RealmValue.list(i.map((o) => RealmValue.from(o)).toList()), + _ => throw ArgumentError.value(object.runtimeType, 'object', 'Unsupported type'), + }; } @override diff --git a/lib/src/list.dart b/lib/src/list.dart index 9cce53358..190049af2 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -288,19 +288,14 @@ extension RealmListInternal on RealmList { return; } - if (value is RealmValue) { - value = value.value; + if (value is RealmValue && value.isCollection) { + realmCore.listAddCollectionAt(handle, realm, index, value, insert || index >= length); + return; } - if (value is RealmObject && !value.isManaged) { - realm.add(value, update: update); - } + realm.addUnmanagedRealmObjectFromValue(value, update); - if (insert || index >= length) { - realmCore.listInsertElementAt(handle, index, value); - } else { - realmCore.listSetElementAt(handle, index, value); - } + realmCore.listAddElementAt(handle, index, value, insert || index >= length); } on Exception catch (e) { throw RealmException("Error setting value at index $index. Error: $e"); } diff --git a/lib/src/map.dart b/lib/src/map.dart index 662dfffe7..121d5deab 100644 --- a/lib/src/map.dart +++ b/lib/src/map.dart @@ -261,13 +261,12 @@ extension RealmMapInternal on RealmMap { return; } - if (value is RealmValue) { - value = value.value; + if (value is RealmValue && value.isCollection) { + realmCore.mapInsertCollection(handle, realm, key, value); + return; } - if (value is RealmObject && !value.isManaged) { - realm.add(value, update: update); - } + realm.addUnmanagedRealmObjectFromValue(value, update); realmCore.mapInsertValue(handle, key, value); } on Exception catch (e) { diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index cc2ab7978..970085713 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -8608,7 +8608,7 @@ class RealmLibrary { late final _realm_set_clear = _realm_set_clearPtr.asFunction)>(); - bool realm_set_dictionary( + ffi.Pointer realm_set_dictionary( ffi.Pointer arg0, int arg1, ) { @@ -8620,10 +8620,11 @@ class RealmLibrary { late final _realm_set_dictionaryPtr = _lookup< ffi.NativeFunction< - ffi.Bool Function(ffi.Pointer, + ffi.Pointer Function(ffi.Pointer, realm_property_key_t)>>('realm_set_dictionary'); - late final _realm_set_dictionary = _realm_set_dictionaryPtr - .asFunction, int)>(); + late final _realm_set_dictionary = _realm_set_dictionaryPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, int)>(); /// Create an embedded object in a given property. /// @@ -8855,7 +8856,7 @@ class RealmLibrary { bool Function(ffi.Pointer, int, ffi.Pointer)>(); /// Create a collection in a given Mixed property. - bool realm_set_list( + ffi.Pointer realm_set_list( ffi.Pointer arg0, int arg1, ) { @@ -8867,10 +8868,10 @@ class RealmLibrary { late final _realm_set_listPtr = _lookup< ffi.NativeFunction< - ffi.Bool Function(ffi.Pointer, + ffi.Pointer Function(ffi.Pointer, realm_property_key_t)>>('realm_set_list'); - late final _realm_set_list = _realm_set_listPtr - .asFunction, int)>(); + late final _realm_set_list = _realm_set_listPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, int)>(); /// Install the default logger void realm_set_log_callback( diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index bd7acdfa2..4d89125d5 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -1054,17 +1054,23 @@ class _RealmCore { return using((Arena arena) { final realm_value = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_get_value(object.handle._pointer, propertyKey, realm_value)); - return realm_value.toDartValue(object.realm); + return realm_value.toDartValue(object.realm, () => _realmLib.realm_get_list(object.handle._pointer, propertyKey), + () => _realmLib.realm_get_dictionary(object.handle._pointer, propertyKey)); }); } void setProperty(RealmObjectBase object, int propertyKey, Object? value, bool isDefault) { - return using((Arena arena) { + using((Arena arena) { final realm_value = _toRealmValue(value, arena); _realmLib.invokeGetBool(() => _realmLib.realm_set_value(object.handle._pointer, propertyKey, realm_value.ref, isDefault)); }); } + void objectSetCollection(RealmObjectBase object, int propertyKey, RealmValue value) { + _createCollection(object.realm, value, () => _realmLib.realm_set_list(object.handle._pointer, propertyKey), + () => _realmLib.realm_set_dictionary(object.handle._pointer, propertyKey)); + } + String objectToString(RealmObjectBase object) { return _realmLib.realm_object_to_string(object.handle._pointer).cast().toRealmDartString(freeRealmMemory: true)!; } @@ -1246,7 +1252,8 @@ class _RealmCore { return using((Arena arena) { final realm_value = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_results_get(results.handle._pointer, index, realm_value)); - return realm_value.toDartValue(results.realm); + return realm_value.toDartValue(results.realm, () => _realmLib.realm_results_get_list(results.handle._pointer, index), + () => _realmLib.realm_results_get_dictionary(results.handle._pointer, index)); }); } @@ -1254,6 +1261,8 @@ class _RealmCore { return using((Arena arena) { final out_index = arena(); final out_found = arena(); + + // TODO: how should this behave for collections final realm_value = _toRealmValue(value, arena); _realmLib.invokeGetBool( () => _realmLib.realm_results_find( @@ -1421,22 +1430,21 @@ class _RealmCore { return using((Arena arena) { final realm_value = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_list_get(list.handle._pointer, index, realm_value)); - return realm_value.toDartValue(list.realm); + return realm_value.toDartValue( + list.realm, () => _realmLib.realm_list_get_list(list.handle._pointer, index), () => _realmLib.realm_list_get_dictionary(list.handle._pointer, index)); }); } - void listSetElementAt(RealmListHandle handle, int index, Object? value) { + void listAddElementAt(RealmListHandle handle, int index, Object? value, bool insert) { using((Arena arena) { final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => _realmLib.realm_list_set(handle._pointer, index, realm_value.ref)); + _realmLib.invokeGetBool(() => (insert ? _realmLib.realm_list_insert : _realmLib.realm_list_set)(handle._pointer, index, realm_value.ref)); }); } - void listInsertElementAt(RealmListHandle handle, int index, Object? value) { - using((Arena arena) { - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => _realmLib.realm_list_insert(handle._pointer, index, realm_value.ref)); - }); + void listAddCollectionAt(RealmListHandle handle, Realm realm, int index, RealmValue value, bool insert) { + _createCollection(realm, value, () => (insert ? _realmLib.realm_list_insert_list : _realmLib.realm_list_set_list)(handle._pointer, index), + () => (insert ? _realmLib.realm_list_insert_dictionary : _realmLib.realm_list_set_dictionary)(handle._pointer, index)); } RealmObjectHandle listSetEmbeddedObjectAt(Realm realm, RealmListHandle handle, int index) { @@ -1465,6 +1473,8 @@ class _RealmCore { return using((Arena arena) { final out_index = arena(); final out_found = arena(); + + // TODO: how should this behave for collections final realm_value = _toRealmValue(value, arena); _realmLib.invokeGetBool( () => _realmLib.realm_list_find( @@ -1505,13 +1515,15 @@ class _RealmCore { return using((Arena arena) { final realm_value = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_set_get(realmSet.handle._pointer, index, realm_value)); - final result = realm_value.toDartValue(realmSet.realm); + final result = realm_value.toDartValue( + realmSet.realm, () => throw RealmException('Sets cannot contain collections'), () => throw RealmException('Sets cannot contain collections')); return result; }); } bool realmSetFind(RealmSet realmSet, Object? value) { return using((Arena arena) { + // TODO: how should this behave for collections final realm_value = _toRealmValue(value, arena); final out_index = arena(); final out_found = arena(); @@ -1522,6 +1534,7 @@ class _RealmCore { bool realmSetErase(RealmSet realmSet, Object? value) { return using((Arena arena) { + // TODO: do we support sets containing mixed collections final realm_value = _toRealmValue(value, arena); final out_erased = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_set_erase(realmSet.handle._pointer, realm_value.ref, out_erased)); @@ -1585,7 +1598,8 @@ class _RealmCore { final out_found = arena(); _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_find(map.handle._pointer, key_value.ref, realm_value, out_found)); if (out_found.value) { - return realm_value.toDartValue(map.realm); + return realm_value.toDartValue(map.realm, () => _realmLib.realm_dictionary_get_list(map.handle._pointer, key_value.ref), + () => _realmLib.realm_dictionary_get_dictionary(map.handle._pointer, key_value.ref)); } return null; @@ -1625,9 +1639,10 @@ class _RealmCore { bool mapContainsValue(ManagedRealmMap map, Object? value) { return using((Arena arena) { - final key_value = _toRealmValue(value, arena); + // TODO: how should this behave for collections + final value_value = _toRealmValue(value, arena); final out_index = arena(); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_value(map.handle._pointer, key_value.ref, out_index)); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_value(map.handle._pointer, value_value.ref, out_index)); return out_index.value > -1; }); } @@ -1643,8 +1658,16 @@ class _RealmCore { void mapInsertValue(RealmMapHandle handle, String key, Object? value) { using((Arena arena) { final key_value = _toRealmValue(key, arena); - final realm_value = _toRealmValue(value, arena); - _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_insert(handle._pointer, key_value.ref, realm_value.ref, nullptr, nullptr)); + final value_value = _toRealmValue(value, arena); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_insert(handle._pointer, key_value.ref, value_value.ref, nullptr, nullptr)); + }); + } + + void mapInsertCollection(RealmMapHandle handle, Realm realm, String key, RealmValue value) { + using((Arena arena) { + final key_value = _toRealmValue(key, arena); + _createCollection(realm, value, () => _realmLib.realm_dictionary_insert_list(handle._pointer, key_value.ref), + () => _realmLib.realm_dictionary_insert_dictionary(handle._pointer, key_value.ref)); }); } @@ -2892,6 +2915,66 @@ class _RealmCore { final configHandle = _createConfig(config); _realmLib.invokeGetBool(() => _realmLib.realm_convert_with_config(realm.handle._pointer, configHandle._pointer, false)); } + + void _createCollection(Realm realm, RealmValue value, Pointer Function() createList, Pointer Function() createMap) { + CollectionHandleBase? collectionHandle; + try { + switch (value.collectionType) { + case realm_value_type.RLM_TYPE_LIST: + final listPointer = _realmLib.invokeGetPointer(createList); + final listHandle = RealmListHandle._(listPointer, realm.handle); + collectionHandle = listHandle; + + final list = realm.createList(listHandle, null); + for (final item in value.value as List) { + list.add(item); + } + case realm_value_type.RLM_TYPE_DICTIONARY: + final mapPointer = _realmLib.invokeGetPointer(createMap); + final mapHandle = RealmMapHandle._(mapPointer, realm.handle); + collectionHandle = mapHandle; + + final map = realm.createMap(mapHandle, null); + for (final kvp in (value.value as Map).entries) { + map[kvp.key] = kvp.value; + } + default: + throw RealmStateError('_createCollection invoked with type that is not list or map.'); + } + } finally { + collectionHandle?.release(); + } + } + + void _populateCollection(Realm realm, RealmValue content, Pointer Function() getList, Pointer Function() getMap) { + CollectionHandleBase? collectionHandle; + try { + switch (content.collectionType) { + case realm_value_type.RLM_TYPE_LIST: + final listPointer = _realmLib.invokeGetPointer(getList); + final listHandle = RealmListHandle._(listPointer, realm.handle); + collectionHandle = listHandle; + + final list = realm.createList(listHandle, null); + for (final item in content.value as List) { + list.add(item); + } + case realm_value_type.RLM_TYPE_DICTIONARY: + final mapPointer = _realmLib.invokeGetPointer(getMap); + final mapHandle = RealmMapHandle._(mapPointer, realm.handle); + collectionHandle = mapHandle; + + final map = realm.createMap(mapHandle, null); + for (final kvp in (content.value as Map).entries) { + map[kvp.key] = kvp.value; + } + default: + throw RealmStateError('listAddCollectionAt called with type that is not collection'); + } + } finally { + collectionHandle?.release(); + } + } } class LastError { @@ -3014,6 +3097,10 @@ abstract class RootedHandleBase extends HandleBase { } } +abstract class CollectionHandleBase extends RootedHandleBase { + CollectionHandleBase(RealmHandle root, Pointer pointer, int size) : super(root, pointer, size); +} + class SchemaHandle extends HandleBase { SchemaHandle._(Pointer pointer) : super(pointer, 24); @@ -3080,7 +3167,7 @@ class RealmResultsHandle extends RootedHandleBase { RealmResultsHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 872); } -class RealmListHandle extends RootedHandleBase { +class RealmListHandle extends CollectionHandleBase { RealmListHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 88); } @@ -3088,7 +3175,7 @@ class RealmSetHandle extends RootedHandleBase { RealmSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); } -class RealmMapHandle extends RootedHandleBase { +class RealmMapHandle extends CollectionHandleBase { RealmMapHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); // TODO: check size } @@ -3219,8 +3306,23 @@ extension _RealmLibraryEx on RealmLibrary { } } +T _toCollectionMethod(RealmValue value, {required T onMap, required T onList}) { + switch (value.collectionType) { + case realm_value_type.RLM_TYPE_LIST: + return onList; + case realm_value_type.RLM_TYPE_DICTIONARY: + return onMap; + default: + throw RealmStateError('_toCollectionRealmValue invoked with type that is not list or map.'); + } +} + Pointer _toRealmValue(Object? value, Allocator allocator) { final realm_value = allocator(); + if (value is RealmValue && value.isCollection) { + throw RealmError( + "Don't use _toPrimitiveValue if the value may contain collections. Use storeValue instead. This is a bug in the Realm Flutter SDK and should be reported to https://github.com/realm/realm-dart/issues/new"); + } _intoRealmValue(value, realm_value.ref, allocator); return realm_value; } @@ -3303,8 +3405,6 @@ void _intoRealmValue(Object? value, realm_value realm_value, Allocator allocator realm_value.values.timestamp.seconds = seconds; realm_value.values.timestamp.nanoseconds = nanoseconds; realm_value.type = realm_value_type.RLM_TYPE_TIMESTAMP; - } else if (value is RealmValue) { - return _intoRealmValue(value.value, realm_value, allocator); } else if (value is Decimal128) { realm_value.values.decimal128 = value.value; realm_value.type = realm_value_type.RLM_TYPE_DECIMAL128; @@ -3313,22 +3413,32 @@ void _intoRealmValue(Object? value, realm_value realm_value, Allocator allocator realm_value.values.binary.size = value.length; realm_value.values.binary.data = allocator(value.length); realm_value.values.binary.data.asTypedList(value.length).setAll(0, value); + } else if (value is RealmValue) { + if (value.type == List) { + realm_value.type = realm_value_type.RLM_TYPE_LIST; + } else if (value.type == Map) { + realm_value.type = realm_value_type.RLM_TYPE_DICTIONARY; + } else { + return _intoRealmValue(value.value, realm_value, allocator); + } } else { throw RealmException("Property type ${value.runtimeType} not supported"); } } extension on Pointer { - Object? toDartValue([Realm? realm]) { + Object? toDartValue(Realm realm, Pointer Function()? getList, Pointer Function()? getMap) { if (this == nullptr) { throw RealmException("Can not convert nullptr realm_value to Dart value"); } - return ref.toDartValue(realm); + return ref.toDartValue(realm: realm, getList: getList, getMap: getMap); } } extension on realm_value_t { - Object? toDartValue([Realm? realm]) { + Object? toPrimitiveValue() => toDartValue(realm: null, getList: null, getMap: null); + + Object? toDartValue({required Realm? realm, required Pointer Function()? getList, required Pointer Function()? getMap}) { switch (type) { case realm_value_type.RLM_TYPE_NULL: return null; @@ -3365,6 +3475,22 @@ extension on realm_value_t { case realm_value_type.RLM_TYPE_UUID: final listInt = values.uuid.bytes.toList(16); return Uuid.fromBytes(Uint8List.fromList(listInt).buffer); + case realm_value_type.RLM_TYPE_LIST: + if (getList == null || realm == null) { + throw RealmException('toDartValue called with a list argument but without a list getter'); + } + + final listPointer = _realmLib.invokeGetPointer(() => getList()); + final listHandle = RealmListHandle._(listPointer, realm.handle); + return realm.createList(listHandle, null); + case realm_value_type.RLM_TYPE_DICTIONARY: + if (getMap == null || realm == null) { + throw RealmException('toDartValue called with a list argument but without a list getter'); + } + + final mapPointer = _realmLib.invokeGetPointer(() => getMap()); + final mapHandle = RealmMapHandle._(mapPointer, realm.handle); + return realm.createMap(mapHandle, null); default: throw RealmException("realm_value_type $type not supported"); } @@ -3495,7 +3621,7 @@ extension on Pointer { final compensatingWrite = this[i]; final reason = compensatingWrite.reason.cast().toDartString(); final object_name = compensatingWrite.object_name.cast().toDartString(); - final primary_key = compensatingWrite.primary_key.toDartValue(); + final primary_key = compensatingWrite.primary_key.toPrimitiveValue(); compensatingWrites.add(CompensatingWriteInfo(object_name, reason, RealmValue.from(primary_key))); } return compensatingWrites; @@ -3691,3 +3817,13 @@ extension on realm_error { return LastError(error, message, user_code_error.toUserCodeError()); } } + +extension RealmValueInternal on RealmValue { + bool get isCollection => collectionType != null; + + int? get collectionType { + if (value is List && value is! Uint8List) return realm_value_type.RLM_TYPE_LIST; + if (value is Map) return realm_value_type.RLM_TYPE_DICTIONARY; + return null; + } +} diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 0741bc544..ce9a95567 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -244,6 +244,11 @@ class RealmCoreAccessor implements RealmAccessor { void set(RealmObjectBase object, String name, Object? value, {bool isDefault = false, bool update = false}) { final propertyMeta = metadata[name]; try { + if (value is RealmValue && value.isCollection) { + realmCore.objectSetCollection(object, propertyMeta.key, value); + return; + } + if (value is RealmList) { final handle = realmCore.getListProperty(object, propertyMeta.key); if (update) { diff --git a/lib/src/set.dart b/lib/src/set.dart index 069268a74..8d11e7fa5 100644 --- a/lib/src/set.dart +++ b/lib/src/set.dart @@ -107,6 +107,13 @@ abstract class RealmSet extends SetBase with RealmEntity i class UnmanagedRealmSet extends collection.DelegatingSet with RealmEntity implements RealmSet { UnmanagedRealmSet([Set? items]) : super(items ?? {}); + @override + bool add(T value) { + _throwOnRealmValueCollection(value); + + return super.add(value); + } + @override // ignore: unused_element RealmObjectMetadata? get _metadata => throw RealmError("Unmanaged RealmSets don't have metadata associated with them."); @@ -143,6 +150,8 @@ class ManagedRealmSet with RealmEntity, SetMixin implement @override bool add(T value) { + _throwOnRealmValueCollection(value); + if (_isManagedRealmObject(value)) { //It is valid to call `add` with managed objects already in the set. _ensureManagedByThis(value, "add"); @@ -381,3 +390,11 @@ extension RealmSetOfObject on RealmSet { return RealmResultsInternal.create(handle, realm, _metadata); } } + +extension on RealmSet { + void _throwOnRealmValueCollection(Object? value) { + if (value is RealmValue && value.isCollection) { + throw RealmStateError('Storing collections inside Set is not supported'); + } + } +} diff --git a/src/realm-core b/src/realm-core index 0e3267d9e..7eb56eae3 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 0e3267d9e44ea5b530cbcaee8018cc2c4769471c +Subproject commit 7eb56eae3ec97d4e9bf97a5852b52a6a475f2b34 diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 4d7fba048..051059080 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -32,9 +32,11 @@ class _TuckedIn { @RealmModel() class _AnythingGoes { - @Indexed() + // TODO: @Indexed() - depends on https://github.com/realm/realm-core/issues/7246 late RealmValue oneAny; late List manyAny; + late Map dictOfAny; + late Set setOfAny; } @RealmModel() @@ -254,4 +256,189 @@ Future main([List? args]) async { results = realm.query("oneAny IN \$0", [values]); expect(results.first.oneAny, realmValues.last); }); + + group('Collections in RealmValue', () { + test('Set throws', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); + final list = RealmValue.list([RealmValue.from(5)]); + final map = RealmValue.map({'a': RealmValue.from('abc')}); + + final obj = AnythingGoes(); + expect(() => obj.setOfAny.add(list), throws()); + expect(() => obj.setOfAny.add(map), throws()); + + realm.write(() => realm.add(obj)); + + realm.write(() { + expect(() => obj.setOfAny.add(list), throws()); + expect(() => obj.setOfAny.add(map), throws()); + }); + }); + + test('List get and set', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); + final list = RealmValue.list([RealmValue.from(5)]); + + final obj = AnythingGoes(oneAny: list); + expect(obj.oneAny.value, isA>()); + expect(obj.oneAny.asList().length, 1); + expect(obj.oneAny.asList().single.value, 5); + + realm.write(() { + realm.add(obj); + }); + + final foundObj = realm.all().single; + expect(foundObj.oneAny.value, isA>()); + final foundList = foundObj.oneAny.asList(); + expect(foundList.length, 1); + expect(foundList[0].value, 5); + + realm.write(() { + foundList.add(RealmValue.from('abc')); + }); + + expect(obj.oneAny.asList()[1].value, 'abc'); + }); + + test('Map get and set', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); + final map = RealmValue.from({'foo': 5}); + + final obj = AnythingGoes(oneAny: map); + expect(obj.oneAny.value, isA>()); + expect(obj.oneAny.asMap().length, 1); + expect(obj.oneAny.asMap()['foo']!.value, 5); + + realm.write(() { + realm.add(obj); + }); + + final foundObj = realm.all().single; + expect(foundObj.oneAny.value, isA>()); + final foundMap = foundObj.oneAny.asMap(); + expect(foundMap.length, 1); + expect(foundMap['foo']!.value, 5); + + realm.write(() { + foundMap['bar'] = RealmValue.from('abc'); + }); + + expect(obj.oneAny.asMap()['bar']!.value, 'abc'); + }); + + for (var isManaged in [true, false]) { + final managedString = isManaged ? 'managed' : 'unmanaged'; + RealmValue _persistAndFind(RealmValue rv, Realm realm) { + if (isManaged) { + realm.write(() { + realm.add(AnythingGoes(oneAny: rv)); + }); + + return realm.all().first.oneAny; + } + + return rv; + } + + test('List when $managedString works with all types', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); + final originalList = [ + null, + 1, + true, + 'string', + DateTime(1999, 3, 4, 5, 30, 23).toUtc(), + 2.3, + Decimal128.parse('1.23456789'), + ObjectId.fromHexString('5f63e882536de46d71877979'), + Uuid.fromString('3809d6d9-7618-4b3d-8044-2aa35fd02f31'), + Uint8List.fromList([1, 2, 0]), + Stuff(i: 123), + [5, 'abc'], + {'int': -10, 'string': 'abc'} + ]; + final foundValue = _persistAndFind(RealmValue.from(originalList), realm); + expect(foundValue.value, isA>()); + + final foundList = foundValue.asList(); + expect(foundList.length, originalList.length); + + // Last 3 elements are objects/collections, so they are treated specially + final primitiveCount = originalList.length - 3; + for (var i = 0; i < primitiveCount; i++) { + expect(foundList[i].value, originalList[i]); + } + + final storedObj = foundList[primitiveCount]; + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, isManaged); + expect(storedObj.as().i, 123); + + final storedList = foundList[primitiveCount + 1]; + expect(storedList.value, isA>()); + expect(storedList.asList().length, 2); + expect(storedList.asList()[0].value, 5); + expect(storedList.asList()[1].value, 'abc'); + + final storedDict = foundList[primitiveCount + 2]; + expect(storedDict.value, isA>()); + expect(storedDict.asMap().length, 2); + expect(storedDict.asMap()['int']!.value, -10); + expect(storedDict.asMap()['string']!.value, 'abc'); + expect(storedDict.asMap()['non-existent'], null); + }); + + test('Map when $managedString works with all types', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); + final originalMap = { + 'primitive_null': null, + 'primitive_int': 1, + 'primitive_bool': true, + 'primitive_string': 'string', + 'primitive_date': DateTime(1999, 3, 4, 5, 30, 23).toUtc(), + 'primitive_double': 2.3, + 'primitive_decimal': Decimal128.parse('1.23456789'), + 'primitive_objectId': ObjectId.fromHexString('5f63e882536de46d71877979'), + 'primitive_uuid': Uuid.fromString('3809d6d9-7618-4b3d-8044-2aa35fd02f31'), + 'primitive_binary': Uint8List.fromList([1, 2, 0]), + 'object': Stuff(i: 123), + 'list': [5, 'abc'], + 'map': {'int': -10, 'string': 'abc'} + }; + final foundValue = _persistAndFind(RealmValue.from(originalMap), realm); + expect(foundValue.value, isA>()); + + final foundMap = foundValue.asMap(); + expect(foundMap.length, foundMap.length); + + for (var key in originalMap.keys.where((k) => k.startsWith('primitive_'))) { + expect(foundMap[key]!.value, originalMap[key]); + } + + final storedObj = foundMap['object']!; + expect(storedObj.value, isA()); + expect(storedObj.as().isManaged, isManaged); + expect(storedObj.as().i, 123); + + final storedList = foundMap['list']!; + expect(storedList.value, isA>()); + expect(storedList.asList().length, 2); + expect(storedList.asList()[0].value, 5); + expect(storedList.asList()[1].value, 'abc'); + + final storedDict = foundMap['map']!; + expect(storedDict.value, isA>()); + expect(storedDict.asMap().length, 2); + expect(storedDict.asMap()['int']!.value, -10); + expect(storedDict.asMap()['string']!.value, 'abc'); + expect(storedDict.asMap()['non-existent'], null); + }); + } + }); } diff --git a/test/realm_value_test.g.dart b/test/realm_value_test.g.dart index f5596faae..3ea2926a8 100644 --- a/test/realm_value_test.g.dart +++ b/test/realm_value_test.g.dart @@ -51,10 +51,16 @@ class AnythingGoes extends _AnythingGoes AnythingGoes({ RealmValue oneAny = const RealmValue.nullValue(), Iterable manyAny = const [], + Set setOfAny = const {}, + Map dictOfAny = const {}, }) { RealmObjectBase.set(this, 'oneAny', oneAny); RealmObjectBase.set>( this, 'manyAny', RealmList(manyAny)); + RealmObjectBase.set>( + this, 'setOfAny', RealmSet(setOfAny)); + RealmObjectBase.set>( + this, 'dictOfAny', RealmMap(dictOfAny)); } AnythingGoes._(); @@ -72,6 +78,21 @@ class AnythingGoes extends _AnythingGoes set manyAny(covariant RealmList value) => throw RealmUnsupportedSetError(); + @override + RealmMap get dictOfAny => + RealmObjectBase.get(this, 'dictOfAny') + as RealmMap; + @override + set dictOfAny(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get setOfAny => + RealmObjectBase.get(this, 'setOfAny') as RealmSet; + @override + set setOfAny(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -85,10 +106,13 @@ class AnythingGoes extends _AnythingGoes RealmObjectBase.registerFactory(AnythingGoes._); return const SchemaObject( ObjectType.realmObject, AnythingGoes, 'AnythingGoes', [ - SchemaProperty('oneAny', RealmPropertyType.mixed, - optional: true, indexType: RealmIndexType.regular), + SchemaProperty('oneAny', RealmPropertyType.mixed, optional: true), SchemaProperty('manyAny', RealmPropertyType.mixed, optional: true, collectionType: RealmCollectionType.list), + SchemaProperty('dictOfAny', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('setOfAny', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.set), ]); } } From 41b814f3b786a87ff3d0a350559b139de92de7f0 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 10 Jan 2024 23:10:08 +0100 Subject: [PATCH 02/18] regenerate ffi bindings --- lib/src/native/realm_bindings.dart | 3 +-- src/realm-core | 2 +- test/realm_value_test.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index 970085713..8e0de8f90 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -12715,8 +12715,7 @@ abstract class realm_value_type { static const int RLM_TYPE_LINK = 10; static const int RLM_TYPE_UUID = 11; static const int RLM_TYPE_LIST = 12; - static const int RLM_TYPE_SET = 13; - static const int RLM_TYPE_DICTIONARY = 14; + static const int RLM_TYPE_DICTIONARY = 13; } final class realm_version_id extends ffi.Struct { diff --git a/src/realm-core b/src/realm-core index 7eb56eae3..e0657b57e 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 7eb56eae3ec97d4e9bf97a5852b52a6a475f2b34 +Subproject commit e0657b57e39353fe3f8125e4f41a9258f0afd46e diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 051059080..432f302b4 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -78,7 +78,7 @@ Future main([List? args]) async { test('Illegal value', () { final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); final realm = getRealm(config); - expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from([1, 2])))), throwsArgumentError); + expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(realm)))), throwsArgumentError); }); test('Embedded object not allowed in RealmValue', () { From beae67d0d8b7f4615d18f61d3c84b56826a3f1f2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 12 Jan 2024 16:07:43 +0100 Subject: [PATCH 03/18] Add more tests --- common/lib/src/realm_types.dart | 1 - lib/src/native/realm_core.dart | 55 +----- lib/src/realm_class.dart | 11 ++ src/realm-core | 2 +- test/realm_value_test.dart | 286 ++++++++++++++++++++++++++------ test/realm_value_test.g.dart | 3 +- 6 files changed, 250 insertions(+), 108 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 53fcccfde..f2ea0d1c5 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////////// -import 'dart:ffi'; import 'dart:math'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 4d89125d5..35ecc1906 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -2920,7 +2920,7 @@ class _RealmCore { CollectionHandleBase? collectionHandle; try { switch (value.collectionType) { - case realm_value_type.RLM_TYPE_LIST: + case RealmCollectionType.list: final listPointer = _realmLib.invokeGetPointer(createList); final listHandle = RealmListHandle._(listPointer, realm.handle); collectionHandle = listHandle; @@ -2929,7 +2929,7 @@ class _RealmCore { for (final item in value.value as List) { list.add(item); } - case realm_value_type.RLM_TYPE_DICTIONARY: + case RealmCollectionType.map: final mapPointer = _realmLib.invokeGetPointer(createMap); final mapHandle = RealmMapHandle._(mapPointer, realm.handle); collectionHandle = mapHandle; @@ -2945,36 +2945,6 @@ class _RealmCore { collectionHandle?.release(); } } - - void _populateCollection(Realm realm, RealmValue content, Pointer Function() getList, Pointer Function() getMap) { - CollectionHandleBase? collectionHandle; - try { - switch (content.collectionType) { - case realm_value_type.RLM_TYPE_LIST: - final listPointer = _realmLib.invokeGetPointer(getList); - final listHandle = RealmListHandle._(listPointer, realm.handle); - collectionHandle = listHandle; - - final list = realm.createList(listHandle, null); - for (final item in content.value as List) { - list.add(item); - } - case realm_value_type.RLM_TYPE_DICTIONARY: - final mapPointer = _realmLib.invokeGetPointer(getMap); - final mapHandle = RealmMapHandle._(mapPointer, realm.handle); - collectionHandle = mapHandle; - - final map = realm.createMap(mapHandle, null); - for (final kvp in (content.value as Map).entries) { - map[kvp.key] = kvp.value; - } - default: - throw RealmStateError('listAddCollectionAt called with type that is not collection'); - } - } finally { - collectionHandle?.release(); - } - } } class LastError { @@ -3306,17 +3276,6 @@ extension _RealmLibraryEx on RealmLibrary { } } -T _toCollectionMethod(RealmValue value, {required T onMap, required T onList}) { - switch (value.collectionType) { - case realm_value_type.RLM_TYPE_LIST: - return onList; - case realm_value_type.RLM_TYPE_DICTIONARY: - return onMap; - default: - throw RealmStateError('_toCollectionRealmValue invoked with type that is not list or map.'); - } -} - Pointer _toRealmValue(Object? value, Allocator allocator) { final realm_value = allocator(); if (value is RealmValue && value.isCollection) { @@ -3817,13 +3776,3 @@ extension on realm_error { return LastError(error, message, user_code_error.toUserCodeError()); } } - -extension RealmValueInternal on RealmValue { - bool get isCollection => collectionType != null; - - int? get collectionType { - if (value is List && value is! Uint8List) return realm_value_type.RLM_TYPE_LIST; - if (value is Map) return realm_value_type.RLM_TYPE_DICTIONARY; - return null; - } -} diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index fc7ea30f9..edac6d067 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; +import 'dart:typed_data'; import 'package:cancellation_token/cancellation_token.dart'; import 'package:collection/collection.dart'; @@ -1060,3 +1061,13 @@ class RealmAsyncOpenProgressNotificationsController implements ProgressNotificat _tokenHandle = null; } } + +extension RealmValueInternal on RealmValue { + bool get isCollection => collectionType != null; + + RealmCollectionType? get collectionType { + if (value is List && value is! Uint8List) return RealmCollectionType.list; + if (value is Map) return RealmCollectionType.map; + return null; + } +} diff --git a/src/realm-core b/src/realm-core index e0657b57e..a3bf8dcd0 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit e0657b57e39353fe3f8125e4f41a9258f0afd46e +Subproject commit a3bf8dcd074459bc3390602c0cb98f13e8faee47 diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 432f302b4..ba5295a07 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -32,7 +32,7 @@ class _TuckedIn { @RealmModel() class _AnythingGoes { - // TODO: @Indexed() - depends on https://github.com/realm/realm-core/issues/7246 + @Indexed() late RealmValue oneAny; late List manyAny; late Map dictOfAny; @@ -47,6 +47,11 @@ class _Stuff { Future main([List? args]) async { await setupTests(args); + Realm getMixedRealm() { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + return getRealm(config); + } + group('RealmValue', () { final now = DateTime.now().toUtc(); final values = [ @@ -66,8 +71,7 @@ Future main([List? args]) async { for (final x in values) { test('Roundtrip ${x.runtimeType} $x', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); expect(something.oneAny.type, x.runtimeType); expect(something.oneAny.value, x); @@ -76,14 +80,12 @@ Future main([List? args]) async { } test('Illegal value', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(realm)))), throwsArgumentError); }); test('Embedded object not allowed in RealmValue', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(TuckedIn())))), throwsArgumentError); }); @@ -222,8 +224,7 @@ Future main([List? args]) async { ]; test('Roundtrip', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final something = realm.write(() => realm.add(AnythingGoes(manyAny: values.map(RealmValue.from)))); expect(something.manyAny.map((e) => e.value), values); expect(something.manyAny, values.map(RealmValue.from)); @@ -245,8 +246,7 @@ Future main([List? args]) async { Uuid.v4(), Decimal128.fromInt(128), ]; - final config = Configuration.local([AnythingGoes.schema, Stuff.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final realmValues = values.map(RealmValue.from); realm.write(() => realm.add(AnythingGoes(manyAny: realmValues, oneAny: realmValues.last))); @@ -258,9 +258,35 @@ Future main([List? args]) async { }); group('Collections in RealmValue', () { + void expectMatches(RealmValue actual, Object? expected) { + switch (actual.collectionType) { + case RealmCollectionType.list: + expect(expected, isList); + final actualList = actual.asList(); + final expectedList = expected as List; + expect(actualList, hasLength(expectedList.length)); + for (var i = 0; i < expectedList.length; i++) { + expectMatches(actualList[i], expectedList[i]); + } + break; + case RealmCollectionType.map: + expect(expected, isMap); + final actualMap = actual.asMap(); + final expectedMap = expected as Map; + expect(actualMap, hasLength(expectedMap.length)); + for (String key in expectedMap.keys) { + expect(actualMap.containsKey(key), true); + expectMatches(actualMap[key]!, expectedMap[key]); + } + break; + default: + expect(actual, RealmValue.from(expected)); + break; + } + } + test('Set throws', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final list = RealmValue.list([RealmValue.from(5)]); final map = RealmValue.map({'a': RealmValue.from('abc')}); @@ -277,62 +303,98 @@ Future main([List? args]) async { }); test('List get and set', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); - final list = RealmValue.list([RealmValue.from(5)]); + final realm = getMixedRealm(); + final list = RealmValue.from([5]); - final obj = AnythingGoes(oneAny: list); + final obj = AnythingGoes(oneAny: list, manyAny: [list], dictOfAny: {'value': list}); expect(obj.oneAny.value, isA>()); expect(obj.oneAny.asList().length, 1); expect(obj.oneAny.asList().single.value, 5); + expect(obj.manyAny[0].value, isA>()); + expect(obj.manyAny[0].asList().length, 1); + expect(obj.manyAny[0].asList().single.value, 5); + + expect(obj.dictOfAny['value']!.value, isA>()); + expect(obj.dictOfAny['value']!.asList().length, 1); + expect(obj.dictOfAny['value']!.asList().single.value, 5); + realm.write(() { realm.add(obj); }); final foundObj = realm.all().single; expect(foundObj.oneAny.value, isA>()); - final foundList = foundObj.oneAny.asList(); - expect(foundList.length, 1); - expect(foundList[0].value, 5); + expect(foundObj.oneAny.asList().length, 1); + expect(foundObj.oneAny.asList()[0].value, 5); + + expect(foundObj.manyAny[0].value, isA>()); + expect(foundObj.manyAny[0].asList().length, 1); + expect(foundObj.manyAny[0].asList()[0].value, 5); + + expect(foundObj.dictOfAny['value']!.value, isA>()); + expect(foundObj.dictOfAny['value']!.asList().length, 1); + expect(foundObj.dictOfAny['value']!.asList()[0].value, 5); realm.write(() { - foundList.add(RealmValue.from('abc')); + foundObj.oneAny.asList().add(RealmValue.from('abc')); + foundObj.manyAny[0].asList().add(RealmValue.from('abc')); + foundObj.dictOfAny['value']!.asList().add(RealmValue.from('abc')); }); expect(obj.oneAny.asList()[1].value, 'abc'); + expect(obj.manyAny[0].asList()[1].value, 'abc'); + expect(obj.dictOfAny['value']!.asList()[1].value, 'abc'); }); test('Map get and set', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final map = RealmValue.from({'foo': 5}); - final obj = AnythingGoes(oneAny: map); + final obj = AnythingGoes(oneAny: map, manyAny: [map], dictOfAny: {'value': map}); expect(obj.oneAny.value, isA>()); expect(obj.oneAny.asMap().length, 1); expect(obj.oneAny.asMap()['foo']!.value, 5); + expect(obj.manyAny[0].value, isA>()); + expect(obj.manyAny[0].asMap().length, 1); + expect(obj.manyAny[0].asMap()['foo']!.value, 5); + + expect(obj.dictOfAny['value']!.value, isA>()); + expect(obj.dictOfAny['value']!.asMap().length, 1); + expect(obj.dictOfAny['value']!.asMap()['foo']!.value, 5); + realm.write(() { realm.add(obj); }); final foundObj = realm.all().single; expect(foundObj.oneAny.value, isA>()); - final foundMap = foundObj.oneAny.asMap(); - expect(foundMap.length, 1); - expect(foundMap['foo']!.value, 5); + expect(foundObj.oneAny.asMap().length, 1); + expect(foundObj.oneAny.asMap()['foo']!.value, 5); + + expect(foundObj.manyAny[0].value, isA>()); + expect(foundObj.manyAny[0].asMap().length, 1); + expect(foundObj.manyAny[0].asMap()['foo']!.value, 5); + + expect(foundObj.dictOfAny['value']!.value, isA>()); + expect(foundObj.dictOfAny['value']!.asMap().length, 1); + expect(foundObj.dictOfAny['value']!.asMap()['foo']!.value, 5); realm.write(() { - foundMap['bar'] = RealmValue.from('abc'); + foundObj.oneAny.asMap()['bar'] = RealmValue.from('abc'); + foundObj.manyAny[0].asMap()['bar'] = RealmValue.from('abc'); + foundObj.dictOfAny['value']!.asMap()['bar'] = RealmValue.from('abc'); }); expect(obj.oneAny.asMap()['bar']!.value, 'abc'); + expect(obj.manyAny[0].asMap()['bar']!.value, 'abc'); + expect(obj.dictOfAny['value']!.asMap()['bar']!.value, 'abc'); }); for (var isManaged in [true, false]) { final managedString = isManaged ? 'managed' : 'unmanaged'; - RealmValue _persistAndFind(RealmValue rv, Realm realm) { + RealmValue persistIfNecessary(RealmValue rv, Realm realm) { if (isManaged) { realm.write(() { realm.add(AnythingGoes(oneAny: rv)); @@ -344,9 +406,16 @@ Future main([List? args]) async { return rv; } + void writeIfNecessary(Realm realm, void Function() func) { + if (isManaged) { + realm.write(() => func()); + } else { + func(); + } + } + test('List when $managedString works with all types', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final originalList = [ null, 1, @@ -362,7 +431,7 @@ Future main([List? args]) async { [5, 'abc'], {'int': -10, 'string': 'abc'} ]; - final foundValue = _persistAndFind(RealmValue.from(originalList), realm); + final foundValue = persistIfNecessary(RealmValue.from(originalList), realm); expect(foundValue.value, isA>()); final foundList = foundValue.asList(); @@ -380,22 +449,31 @@ Future main([List? args]) async { expect(storedObj.as().i, 123); final storedList = foundList[primitiveCount + 1]; - expect(storedList.value, isA>()); - expect(storedList.asList().length, 2); - expect(storedList.asList()[0].value, 5); - expect(storedList.asList()[1].value, 'abc'); + expectMatches(storedList, [5, 'abc']); final storedDict = foundList[primitiveCount + 2]; - expect(storedDict.value, isA>()); - expect(storedDict.asMap().length, 2); - expect(storedDict.asMap()['int']!.value, -10); - expect(storedDict.asMap()['string']!.value, 'abc'); + expectMatches(storedDict, {'int': -10, 'string': 'abc'}); expect(storedDict.asMap()['non-existent'], null); }); + test('List when $managedString can be reassigned', () { + final realm = getMixedRealm(); + final obj = AnythingGoes(oneAny: RealmValue.from([true, 5.3])); + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + expectMatches(obj.oneAny, [true, 5.3]); + + writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); + expectMatches(obj.oneAny, 999); + + writeIfNecessary(realm, () => obj.oneAny = RealmValue.from({'int': -100})); + expectMatches(obj.oneAny, {'int': -100}); + }); + test('Map when $managedString works with all types', () { - final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); + final realm = getMixedRealm(); final originalMap = { 'primitive_null': null, 'primitive_int': 1, @@ -411,7 +489,7 @@ Future main([List? args]) async { 'list': [5, 'abc'], 'map': {'int': -10, 'string': 'abc'} }; - final foundValue = _persistAndFind(RealmValue.from(originalMap), realm); + final foundValue = persistIfNecessary(RealmValue.from(originalMap), realm); expect(foundValue.value, isA>()); final foundMap = foundValue.asMap(); @@ -427,18 +505,122 @@ Future main([List? args]) async { expect(storedObj.as().i, 123); final storedList = foundMap['list']!; - expect(storedList.value, isA>()); - expect(storedList.asList().length, 2); - expect(storedList.asList()[0].value, 5); - expect(storedList.asList()[1].value, 'abc'); + expectMatches(storedList, [5, 'abc']); final storedDict = foundMap['map']!; - expect(storedDict.value, isA>()); - expect(storedDict.asMap().length, 2); - expect(storedDict.asMap()['int']!.value, -10); - expect(storedDict.asMap()['string']!.value, 'abc'); - expect(storedDict.asMap()['non-existent'], null); + expectMatches(storedDict, {'int': -10, 'string': 'abc'}); + }); + + test('Map when $managedString can be reassigned', () { + final realm = getMixedRealm(); + final obj = AnythingGoes(oneAny: RealmValue.from({'bool': true, 'double': 5.3})); + if (isManaged) { + realm.write(() => realm.add(obj)); + } + + expectMatches(obj.oneAny, {'bool': true, 'double': 5.3}); + + writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); + expectMatches(obj.oneAny, 999); + + writeIfNecessary(realm, () => obj.oneAny = RealmValue.from([1.23456789])); + expectMatches(obj.oneAny, [1.23456789]); + }); + + test('RealmValue when $managedString can store complex struct', () { + final realm = getMixedRealm(); + final rv = persistIfNecessary( + RealmValue.from([ + {'0_bool': true, '0_double': 5.3}, + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + } + } + ]), + realm); + + expectMatches(rv, [ + {'0_bool': true, '0_double': 5.3}, + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + } + } + ]); + + writeIfNecessary(realm, () { + rv.asList().removeAt(0); + }); + + expectMatches(rv, [ + { + '1_int': 5, + '1_map': { + '2_decimal': Decimal128.fromDouble(0.1), + '2_list': [ + 'bla bla', + { + '3_dict': {'4_string': 'abc'} + } + ] + } + } + ]); + + writeIfNecessary(realm, () { + rv.asList()[0].asMap()['1_double'] = RealmValue.double(5.5); + rv.asList()[0].asMap().remove('1_map'); + rv.asList().add(RealmValue.bool(true)); + }); + + expectMatches(rv, [ + {'1_int': 5, '1_double': 5.5}, + true + ]); }); } + + test('List in RealmValue when unmanaged is same instance', () { + final list = [RealmValue.bool(true), RealmValue.string('abc')]; + final rv = RealmValue.list(list); + expect(identical(rv.asList(), list), true); + }); + + test('List in RealmValue when managed is different instance', () { + final list = [RealmValue.bool(true), RealmValue.string('abc')]; + final rv = RealmValue.list(list); + final realm = getMixedRealm(); + final obj = realm.write(() => realm.add(AnythingGoes(oneAny: rv))); + expect(identical(obj.oneAny.asList(), list), false); + }); + + test('Map in RealmValue when unmanaged is same instance', () { + final map = {'bool': RealmValue.bool(true), 'str': RealmValue.string('abc')}; + final rv = RealmValue.map(map); + expect(identical(rv.asMap(), map), true); + }); + + test('Map in RealmValue when managed is different instance', () { + final map = {'bool': RealmValue.bool(true), 'str': RealmValue.string('abc')}; + final rv = RealmValue.map(map); + final realm = getMixedRealm(); + final obj = realm.write(() => realm.add(AnythingGoes(oneAny: rv))); + expect(identical(obj.oneAny.asMap(), map), false); + }); }); } diff --git a/test/realm_value_test.g.dart b/test/realm_value_test.g.dart index 3ea2926a8..53c9c96de 100644 --- a/test/realm_value_test.g.dart +++ b/test/realm_value_test.g.dart @@ -106,7 +106,8 @@ class AnythingGoes extends _AnythingGoes RealmObjectBase.registerFactory(AnythingGoes._); return const SchemaObject( ObjectType.realmObject, AnythingGoes, 'AnythingGoes', [ - SchemaProperty('oneAny', RealmPropertyType.mixed, optional: true), + SchemaProperty('oneAny', RealmPropertyType.mixed, + optional: true, indexType: RealmIndexType.regular), SchemaProperty('manyAny', RealmPropertyType.mixed, optional: true, collectionType: RealmCollectionType.list), SchemaProperty('dictOfAny', RealmPropertyType.mixed, From 6cf7e50eaf4c7b726382156b2c506f8df090a656 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 12 Jan 2024 17:31:08 +0100 Subject: [PATCH 04/18] Add notification tests --- common/lib/src/realm_types.dart | 33 +++++----- lib/src/realm_class.dart | 8 +++ test/realm_value_test.dart | 112 ++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 18 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index f2ea0d1c5..b39aff477 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -191,10 +191,6 @@ class RealmValue { T as() => value as T; // better for code completion - List asList() => as>(); - - Map asMap() => as>(); - // This is private, so user cannot accidentally construct an invalid instance const RealmValue._(this.value); @@ -213,22 +209,23 @@ class RealmValue { const RealmValue.list(List list) : this._(list); const RealmValue.map(Map map) : this._(map); - /// Will throw [ArgumentError] + /// Constructs a RealmValue from an arbitrary object. Collections will be converted recursively as long + /// as all their values are compatible. + /// + /// Throws [ArgumentError] if any of the values inside the graph cannot be stored in a [RealmValue]. factory RealmValue.from(Object? object) { return switch (object) { - Object? o - when o == null || - o is bool || - o is String || - o is int || - o is double || - o is RealmObjectMarker || - o is DateTime || - o is ObjectId || - o is Decimal128 || - o is Uuid || - o is Uint8List => - RealmValue._(o), + null => RealmValue.nullValue(), + bool b => RealmValue.bool(b), + String text => RealmValue.string(text), + int i => RealmValue.int(i), + double d => RealmValue.double(d), + RealmObjectMarker o => RealmValue.realmObject(o), + DateTime d => RealmValue.dateTime(d), + ObjectId id => RealmValue.objectId(id), + Decimal128 decimal => RealmValue.decimal128(decimal), + Uuid uuid => RealmValue.uuid(uuid), + Uint8List binary => RealmValue.uint8List(binary), Map d => RealmValue.map(d), Map d => RealmValue.map(d.map((key, value) => MapEntry(key, RealmValue.from(value)))), List l => RealmValue.list(l), diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index edac6d067..965a271b6 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -1062,6 +1062,7 @@ class RealmAsyncOpenProgressNotificationsController implements ProgressNotificat } } +/// @nodoc extension RealmValueInternal on RealmValue { bool get isCollection => collectionType != null; @@ -1071,3 +1072,10 @@ extension RealmValueInternal on RealmValue { return null; } } + +// TODO: should this be here or should we move RealmList/RealmMap to common? +extension RealmValueCollections on RealmValue { + RealmList asList() => as>(); + + RealmMap asMap() => as>(); +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index ba5295a07..d0b4e0b52 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -622,5 +622,117 @@ Future main([List? args]) async { final obj = realm.write(() => realm.add(AnythingGoes(oneAny: rv))); expect(identical(obj.oneAny.asMap(), map), false); }); + + test('Notifications', () async { + final realm = getMixedRealm(); + final obj = AnythingGoes( + oneAny: RealmValue.from([ + 5, + { + 'foo': 'bar', + 'list': [10] + } + ])); + + realm.write(() { + realm.add(obj); + }); + + final List> parentChanges = []; + final subscription = obj.changes.listen((event) { + parentChanges.add(event); + }); + + final List> listChanges = []; + final listSubscription = obj.oneAny.asList().changes.listen((event) { + listChanges.add(event); + }); + + final List> mapChanges = []; + final mapSubscription = obj.oneAny.asList()[1].asMap().changes.listen((event) { + mapChanges.add(event); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + parentChanges.clear(); + listChanges.clear(); + mapChanges.clear(); + + realm.write(() { + obj.oneAny.asList().add(RealmValue.bool(true)); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(parentChanges, hasLength(1)); + expect(parentChanges[0].properties, ['oneAny']); + + expect(listChanges, hasLength(1)); + expect(listChanges[0].inserted, [2]); + expect(listChanges[0].deleted, isEmpty); + expect(listChanges[0].modified, isEmpty); + expect(listChanges[0].isCleared, false); + expect(listChanges[0].isCollectionDeleted, false); + + expect(mapChanges, hasLength(0)); + + realm.write(() { + obj.oneAny.asList()[1].asMap()['list'] = RealmValue.from([10]); + obj.oneAny.asList()[1].asMap()['new-value'] = RealmValue.from({'foo': 'bar'}); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(parentChanges, hasLength(2)); + expect(parentChanges[1].properties, ['oneAny']); + + // Collection changes are only emitted if the collection is directly modified + // but won't be emitted if an item inside the collection changes. In this case, + // we're modifying the dictionary inside the list, but not reassigning any list + // elements, so we shouldn't get a notification + expect(listChanges, hasLength(1)); + + expect(mapChanges, hasLength(1)); + expect(mapChanges[0].modified, ['list']); + expect(mapChanges[0].inserted, ['new-value']); + expect(mapChanges[0].deleted, isEmpty); + expect(mapChanges[0].isCleared, false); + expect(mapChanges[0].isCollectionDeleted, false); + + realm.write(() { + obj.oneAny.asList().removeAt(1); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + expect(parentChanges, hasLength(3)); + expect(parentChanges[2].properties, ['oneAny']); + + expect(listChanges, hasLength(2)); + expect(listChanges[1].inserted, isEmpty); + expect(listChanges[1].deleted, [1]); + expect(listChanges[1].modified, isEmpty); + expect(listChanges[1].isCleared, false); + expect(listChanges[1].isCollectionDeleted, false); + + expect(mapChanges, hasLength(2)); + expect(mapChanges[1].isCollectionDeleted, true); + + subscription.cancel(); + listSubscription.cancel(); + mapSubscription.cancel(); + + realm.write(() { + obj.oneAny = RealmValue.bool(false); + }); + + await Future.delayed(Duration(milliseconds: 20)); + + // Subscriptions have been canceled - shouldn't get more notifications + expect(parentChanges, hasLength(3)); + expect(listChanges, hasLength(2)); + expect(mapChanges, hasLength(2)); + }); }); } From b0e2c62c1c5d11517fdb7907cc9a2871d4209f99 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 12 Jan 2024 17:33:58 +0100 Subject: [PATCH 05/18] Return the types --- common/lib/src/realm_types.dart | 4 ++++ lib/src/realm_class.dart | 7 ------- test/realm_value_test.dart | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index b39aff477..85d093bb3 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -191,6 +191,10 @@ class RealmValue { T as() => value as T; // better for code completion + List asList() => as>(); + + Map asMap() => as>(); + // This is private, so user cannot accidentally construct an invalid instance const RealmValue._(this.value); diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 965a271b6..85045e58f 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -1072,10 +1072,3 @@ extension RealmValueInternal on RealmValue { return null; } } - -// TODO: should this be here or should we move RealmList/RealmMap to common? -extension RealmValueCollections on RealmValue { - RealmList asList() => as>(); - - RealmMap asMap() => as>(); -} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index d0b4e0b52..d85dfc38d 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -644,12 +644,12 @@ Future main([List? args]) async { }); final List> listChanges = []; - final listSubscription = obj.oneAny.asList().changes.listen((event) { + final listSubscription = obj.oneAny.as>().changes.listen((event) { listChanges.add(event); }); final List> mapChanges = []; - final mapSubscription = obj.oneAny.asList()[1].asMap().changes.listen((event) { + final mapSubscription = obj.oneAny.asList()[1].as>().changes.listen((event) { mapChanges.add(event); }); From 5965d1c6fa6a27b6eb075701af98e94be71c96c5 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 15 Jan 2024 16:42:34 +0100 Subject: [PATCH 06/18] Address some PR feedback --- common/lib/src/realm_types.dart | 10 ++++++---- lib/src/list.dart | 16 +++++++++++++++- lib/src/map.dart | 8 +++++++- lib/src/realm_class.dart | 21 +++++++++++++++++++++ test/realm_value_test.dart | 7 +++++-- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 85d093bb3..c8a19f6d7 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -189,11 +189,11 @@ class RealmValue { final Object? value; Type get type => value.runtimeType; - T as() => value as T; // better for code completion - - List asList() => as>(); + // TODO + // RealmValueType get realmValueType; - Map asMap() => as>(); + /// Casts [value] to [T]. An exception will be thrown if the value is not convertible to [T]. + T as() => value as T; // better for code completion // This is private, so user cannot accidentally construct an invalid instance const RealmValue._(this.value); @@ -248,6 +248,8 @@ class RealmValue { } return value == other.value; + + // TODO: talk to Claus/Ferdinando about equality of collections - we should try not to do deep eqaulity there } return value == other; diff --git a/lib/src/list.dart b/lib/src/list.dart index 190049af2..8a9714452 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -205,7 +205,13 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } class UnmanagedRealmList extends collection.DelegatingList with RealmEntity implements RealmList { - UnmanagedRealmList([Iterable? items]) : super(List.from(items ?? [])); + final List _base; + + UnmanagedRealmList([Iterable? items]) : this._(items is List ? items : List.from(items ?? [])); + + UnmanagedRealmList._(List items) + : _base = items, + super(items); @override RealmObjectMetadata? get _metadata => throw RealmException("Unmanaged lists don't have metadata associated with them."); @@ -224,6 +230,14 @@ class UnmanagedRealmList extends collection.DelegatingList @override Stream> get changes => throw RealmStateError("Unmanaged lists don't support changes"); + + @override + bool operator ==(Object? other) { + return _base == other; + } + + @override + int get hashCode => _base.hashCode; } // The query operations on lists, only work for list of objects (core restriction), diff --git a/lib/src/map.dart b/lib/src/map.dart index 121d5deab..ce1206638 100644 --- a/lib/src/map.dart +++ b/lib/src/map.dart @@ -49,7 +49,13 @@ abstract class RealmMap with RealmEntity implements MapBase extends collection.DelegatingMap with RealmEntity implements RealmMap { - UnmanagedRealmMap([Map? items]) : super(Map.from(items ?? {})); + final Map _base; + + UnmanagedRealmMap([Map? items]) : this._(items is Map ? items : Map.from(items ?? {})); + + UnmanagedRealmMap._(Map items) + : _base = items, + super(items); @override bool get isValid => true; diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 85045e58f..ba4845c47 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -1072,3 +1072,24 @@ extension RealmValueInternal on RealmValue { return null; } } + +/// Extensions on RealmValue providing convenience conversion operators +extension RealmValueConvenience on RealmValue { + /// Casts [value] to a List. It will throw an exception if [value] is not a list. + RealmList asList() { + if (value is RealmList) { + return as>(); + } + + return UnmanagedRealmList(as>()); + } + + /// Casts [value] to a Map. It will throw an exception if [value] is not a map. + RealmMap asMap() { + if (value is RealmMap) { + return as>(); + } + + return UnmanagedRealmMap(as>()); + } +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index d85dfc38d..29b3f880b 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -644,12 +644,12 @@ Future main([List? args]) async { }); final List> listChanges = []; - final listSubscription = obj.oneAny.as>().changes.listen((event) { + final listSubscription = obj.oneAny.asList().changes.listen((event) { listChanges.add(event); }); final List> mapChanges = []; - final mapSubscription = obj.oneAny.asList()[1].as>().changes.listen((event) { + final mapSubscription = obj.oneAny.asList()[1].asMap().changes.listen((event) { mapChanges.add(event); }); @@ -691,6 +691,9 @@ Future main([List? args]) async { // but won't be emitted if an item inside the collection changes. In this case, // we're modifying the dictionary inside the list, but not reassigning any list // elements, so we shouldn't get a notification + + // TODO: this is inconsistent with how lists behave today - talk to Claus/Ferdinando + // about the expectations expect(listChanges, hasLength(1)); expect(mapChanges, hasLength(1)); From dcbfba168125ccdc387a34e9129fd7fc426bd520 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 15 Jan 2024 20:55:24 +0100 Subject: [PATCH 07/18] Add RealmValueType --- common/lib/src/realm_types.dart | 76 +++++++++++++++++++++++++-------- lib/src/realm_class.dart | 1 + test/realm_value_test.dart | 73 ++++++++++++++++--------------- 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index c8a19f6d7..897028424 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -156,6 +156,48 @@ abstract class EmbeddedObjectMarker implements RealmObjectBaseMarker {} /// @nodoc abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {} +/// An enum describing the possible types that can be wrapped inside [RealmValue] +enum RealmValueType { + /// The [RealmValue] represents `null` + nullValue, + + /// The [RealmValue] represents a [bool] value + bool, + + /// The [RealmValue] represents a [String] value + string, + + /// The [RealmValue] represents an [int] value + int, + + /// The [RealmValue] represents a [double] value + double, + + /// The [RealmValue] represents a `RealmObject` instance value + object, + + /// The [RealmValue] represents an [ObjectId] value + objectId, + + /// The [RealmValue] represents a [DateTime] value + dateTime, + + /// The [RealmValue] represents a [Decimal128] value + decimal, + + /// The [RealmValue] represents an [Uuid] value + uuid, + + /// The [RealmValue] represents a binary ([Uint8List]) value + binary, + + /// The [RealmValue] represents a `List` + list, + + /// The [RealmValue] represents a `Map` + map, +} + /// A type that can represent any valid realm data type, except collections and embedded objects. /// /// You can use [RealmValue] to declare fields on realm models, in which case it must be non-nullable, @@ -187,31 +229,29 @@ abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {} /// ``` class RealmValue { final Object? value; - Type get type => value.runtimeType; - // TODO - // RealmValueType get realmValueType; + final RealmValueType type; /// Casts [value] to [T]. An exception will be thrown if the value is not convertible to [T]. T as() => value as T; // better for code completion // This is private, so user cannot accidentally construct an invalid instance - const RealmValue._(this.value); + const RealmValue._(this.value, this.type); - const RealmValue.nullValue() : this._(null); - const RealmValue.bool(bool b) : this._(b); - const RealmValue.string(String text) : this._(text); - const RealmValue.int(int i) : this._(i); - const RealmValue.double(double d) : this._(d); + const RealmValue.nullValue() : this._(null, RealmValueType.nullValue); + const RealmValue.bool(bool b) : this._(b, RealmValueType.bool); + const RealmValue.string(String text) : this._(text, RealmValueType.string); + const RealmValue.int(int i) : this._(i, RealmValueType.int); + const RealmValue.double(double d) : this._(d, RealmValueType.double); // TODO: RealmObjectMarker introduced to avoid dependency inversion. It would be better if we could use RealmObject directly. https://github.com/realm/realm-dart/issues/701 - const RealmValue.realmObject(RealmObjectMarker o) : this._(o); - const RealmValue.dateTime(DateTime timestamp) : this._(timestamp); - const RealmValue.objectId(ObjectId id) : this._(id); - const RealmValue.decimal128(Decimal128 decimal) : this._(decimal); - const RealmValue.uuid(Uuid uuid) : this._(uuid); - const RealmValue.uint8List(Uint8List binary) : this._(binary); - const RealmValue.list(List list) : this._(list); - const RealmValue.map(Map map) : this._(map); + const RealmValue.realmObject(RealmObjectMarker o) : this._(o, RealmValueType.object); + const RealmValue.dateTime(DateTime timestamp) : this._(timestamp, RealmValueType.dateTime); + const RealmValue.objectId(ObjectId id) : this._(id, RealmValueType.objectId); + const RealmValue.decimal128(Decimal128 decimal) : this._(decimal, RealmValueType.decimal); + const RealmValue.uuid(Uuid uuid) : this._(uuid, RealmValueType.uuid); + const RealmValue.binary(Uint8List binary) : this._(binary, RealmValueType.binary); + const RealmValue.list(List list) : this._(list, RealmValueType.list); + const RealmValue.map(Map map) : this._(map, RealmValueType.map); /// Constructs a RealmValue from an arbitrary object. Collections will be converted recursively as long /// as all their values are compatible. @@ -229,7 +269,7 @@ class RealmValue { ObjectId id => RealmValue.objectId(id), Decimal128 decimal => RealmValue.decimal128(decimal), Uuid uuid => RealmValue.uuid(uuid), - Uint8List binary => RealmValue.uint8List(binary), + Uint8List binary => RealmValue.binary(binary), Map d => RealmValue.map(d), Map d => RealmValue.map(d.map((key, value) => MapEntry(key, RealmValue.from(value)))), List l => RealmValue.list(l), diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index ba4845c47..49df40476 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -63,6 +63,7 @@ export 'package:realm_common/realm_common.dart' RealmStateError, RealmUnsupportedSetError, RealmValue, + RealmValueType, Uuid; // always expose with `show` to explicitly control the public API surface diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 29b3f880b..f5ffcf3ec 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -53,7 +53,6 @@ Future main([List? args]) async { } group('RealmValue', () { - final now = DateTime.now().toUtc(); final values = [ null, true, @@ -62,9 +61,9 @@ Future main([List? args]) async { 3.14, AnythingGoes(), Stuff(), - now, - ObjectId.fromTimestamp(now), - Uuid.v4(), + DateTime.utc(2024, 5, 3, 23, 11, 54), + ObjectId.fromHexString('64c13ab08edf48a008793cac'), + Uuid.fromString('7a459a5e-5eb6-45f6-9b72-8f794e324105'), Decimal128.fromDouble(128.128), Uint8List.fromList([1, 2, 0]) ]; @@ -73,7 +72,7 @@ Future main([List? args]) async { test('Roundtrip ${x.runtimeType} $x', () { final realm = getMixedRealm(); final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); - expect(something.oneAny.type, x.runtimeType); + expect(something.oneAny.value.runtimeType, x.runtimeType); expect(something.oneAny.value, x); expect(something.oneAny, RealmValue.from(x)); }); @@ -94,48 +93,44 @@ Future main([List? args]) async { final something = AnythingGoes(oneAny: RealmValue.from(x)); final value = something.oneAny.value; - // Uint8List can not be in the switch - if (something.oneAny.type == Uint8List(0).runtimeType) { - expect(value, isA()); - return; - } - switch (something.oneAny.type) { - case Null: + case RealmValueType.nullValue: expect(value, isA()); break; - case bool: + case RealmValueType.bool: expect(value, isA()); break; - case String: + case RealmValueType.string: expect(value, isA()); break; - case int: + case RealmValueType.int: expect(value, isA()); break; - case double: + case RealmValueType.double: expect(value, isA()); break; - case AnythingGoes: // RealmObject won't work with switch + case RealmValueType.object: expect(value, isA()); - break; - case Stuff: // RealmObject won't work with switch expect(value, isA()); break; - case DateTime: + case RealmValueType.dateTime: expect(value, isA()); break; - case ObjectId: + case RealmValueType.objectId: expect(value, isA()); break; - case Uuid: + case RealmValueType.uuid: expect(value, isA()); break; - case Decimal128: + case RealmValueType.decimal: expect(value, isA()); break; - default: - fail('${something.oneAny} not handled correctly in switch'); + case RealmValueType.binary: + expect(value, isA()); + break; + case RealmValueType.list: + case RealmValueType.map: + fail('List and map should not be tested here.'); } }); } @@ -146,29 +141,29 @@ Future main([List? args]) async { final value = something.oneAny.value; final type = something.oneAny.type; if (value == null) { - expect(type, Null); + expect(type, RealmValueType.nullValue); } else if (value is int) { - expect(type, int); + expect(type, RealmValueType.int); } else if (value is String) { - expect(type, String); + expect(type, RealmValueType.string); } else if (value is bool) { - expect(type, bool); + expect(type, RealmValueType.bool); } else if (value is double) { - expect(type, double); + expect(type, RealmValueType.double); } else if (value is DateTime) { - expect(type, DateTime); + expect(type, RealmValueType.dateTime); } else if (value is Uuid) { - expect(type, Uuid); + expect(type, RealmValueType.uuid); } else if (value is ObjectId) { - expect(type, ObjectId); + expect(type, RealmValueType.objectId); } else if (value is Decimal128) { - expect(type, Decimal128); + expect(type, RealmValueType.decimal); } else if (value is Uint8List) { - expect(type, Uint8List(0).runtimeType); + expect(type, RealmValueType.binary); } else if (value is AnythingGoes) { - expect(type, AnythingGoes); + expect(type, RealmValueType.object); } else if (value is Stuff) { - expect(type, Stuff); + expect(type, RealmValueType.object); } else { fail('$value not handled correctly in if-is'); } @@ -433,6 +428,7 @@ Future main([List? args]) async { ]; final foundValue = persistIfNecessary(RealmValue.from(originalList), realm); expect(foundValue.value, isA>()); + expect(foundValue.type, RealmValueType.list); final foundList = foundValue.asList(); expect(foundList.length, originalList.length); @@ -463,6 +459,7 @@ Future main([List? args]) async { realm.write(() => realm.add(obj)); } + expect(obj.oneAny.type, RealmValueType.list); expectMatches(obj.oneAny, [true, 5.3]); writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); @@ -491,6 +488,7 @@ Future main([List? args]) async { }; final foundValue = persistIfNecessary(RealmValue.from(originalMap), realm); expect(foundValue.value, isA>()); + expect(foundValue.type, RealmValueType.map); final foundMap = foundValue.asMap(); expect(foundMap.length, foundMap.length); @@ -518,6 +516,7 @@ Future main([List? args]) async { realm.write(() => realm.add(obj)); } + expect(obj.oneAny.type, RealmValueType.list); expectMatches(obj.oneAny, {'bool': true, 'double': 5.3}); writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); From 546a0189217f66ca55cce22edaf943f636411087 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 17 Jan 2024 15:11:03 +0100 Subject: [PATCH 08/18] Fix some tests --- common/lib/src/realm_types.dart | 22 ++++++---- lib/src/list.dart | 6 ++- lib/src/map.dart | 12 +++++- lib/src/realm_class.dart | 4 +- test/realm_map_test.dart | 2 +- test/realm_value_test.dart | 74 ++++++++++++++++++++++++++++----- 6 files changed, 97 insertions(+), 23 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 897028424..c41f8bdb4 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -161,8 +161,8 @@ enum RealmValueType { /// The [RealmValue] represents `null` nullValue, - /// The [RealmValue] represents a [bool] value - bool, + /// The [RealmValue] represents a [boolean] value + boolean, /// The [RealmValue] represents a [String] value string, @@ -195,7 +195,10 @@ enum RealmValueType { list, /// The [RealmValue] represents a `Map` - map, + map; + + /// Returns `true` if the enum value represents a collection - i.e. it's [list] or [map]. + bool get isCollection => this == RealmValueType.list || this == RealmValueType.map; } /// A type that can represent any valid realm data type, except collections and embedded objects. @@ -239,7 +242,7 @@ class RealmValue { const RealmValue._(this.value, this.type); const RealmValue.nullValue() : this._(null, RealmValueType.nullValue); - const RealmValue.bool(bool b) : this._(b, RealmValueType.bool); + const RealmValue.bool(bool b) : this._(b, RealmValueType.boolean); const RealmValue.string(String text) : this._(text, RealmValueType.string); const RealmValue.int(int i) : this._(i, RealmValueType.int); const RealmValue.double(double d) : this._(d, RealmValueType.double); @@ -282,21 +285,24 @@ class RealmValue { @override operator ==(Object? other) { + // We always return false when comparing two RealmValue collections. + if (type.isCollection) { + return false; + } + if (other is RealmValue) { if (value is Uint8List && other.value is Uint8List) { return ListEquality().equals(value as Uint8List, other.value as Uint8List); } - return value == other.value; - - // TODO: talk to Claus/Ferdinando about equality of collections - we should try not to do deep eqaulity there + return type == other.type && value == other.value; } return value == other; } @override - int get hashCode => value.hashCode; + int get hashCode => Object.hash(type, value); @override String toString() => 'RealmValue($value)'; diff --git a/lib/src/list.dart b/lib/src/list.dart index 8a9714452..ce4bdfb44 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -207,7 +207,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme class UnmanagedRealmList extends collection.DelegatingList with RealmEntity implements RealmList { final List _base; - UnmanagedRealmList([Iterable? items]) : this._(items is List ? items : List.from(items ?? [])); + UnmanagedRealmList([Iterable? items]) : this._(List.from(items ?? [])); UnmanagedRealmList._(List items) : _base = items, @@ -278,6 +278,10 @@ extension RealmListInternal on RealmList { RealmObjectMetadata? get metadata => asManaged()._metadata; + static RealmList createFromList(List items) { + return UnmanagedRealmList._(items); + } + static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); static void setValue(RealmListHandle handle, Realm realm, int index, Object? value, {bool update = false, bool insert = false}) { diff --git a/lib/src/map.dart b/lib/src/map.dart index ce1206638..b262e01a6 100644 --- a/lib/src/map.dart +++ b/lib/src/map.dart @@ -51,7 +51,7 @@ abstract class RealmMap with RealmEntity implements MapBase extends collection.DelegatingMap with RealmEntity implements RealmMap { final Map _base; - UnmanagedRealmMap([Map? items]) : this._(items is Map ? items : Map.from(items ?? {})); + UnmanagedRealmMap([Map? items]) : this._(Map.from(items ?? {})); UnmanagedRealmMap._(Map items) : _base = items, @@ -65,6 +65,14 @@ class UnmanagedRealmMap extends collection.DelegatingMap> get changes => throw RealmStateError("Unmanaged maps don't support changes"); + + @override + bool operator ==(Object? other) { + return _base == other; + } + + @override + int get hashCode => _base.hashCode; } class ManagedRealmMap with RealmEntity, MapMixin implements RealmMap { @@ -252,6 +260,8 @@ extension RealmMapInternal on RealmMap { RealmObjectMetadata? get metadata => asManaged()._metadata; + static RealmMap createFromMap(Map map) => UnmanagedRealmMap._(map); + static RealmMap create(RealmMapHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmMap._(handle, realm, metadata); diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 49df40476..1fb379292 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -1082,7 +1082,7 @@ extension RealmValueConvenience on RealmValue { return as>(); } - return UnmanagedRealmList(as>()); + return RealmListInternal.createFromList(as>()); } /// Casts [value] to a Map. It will throw an exception if [value] is not a map. @@ -1091,6 +1091,6 @@ extension RealmValueConvenience on RealmValue { return as>(); } - return UnmanagedRealmMap(as>()); + return RealmMapInternal.createFromMap(as>()); } } diff --git a/test/realm_map_test.dart b/test/realm_map_test.dart index 4e8c91f91..87b6f66cc 100644 --- a/test/realm_map_test.dart +++ b/test/realm_map_test.dart @@ -534,7 +534,7 @@ List> realmValueTestValues() => [ ('intKey', RealmValue.int(10)), ('boolKey', RealmValue.bool(true)), ('stringKey', RealmValue.string('abc')), - ('dataKey', RealmValue.uint8List(Uint8List.fromList([0, 1, 2]))), + ('dataKey', RealmValue.binary(Uint8List.fromList([0, 1, 2]))), ('dateKey', RealmValue.dateTime(DateTime.fromMillisecondsSinceEpoch(1616137641000).toUtc())), ('doubleKey', RealmValue.double(2.5)), ('decimalKey', RealmValue.decimal128(Decimal128.fromDouble(5.0))), diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index f5ffcf3ec..3d42838db 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -16,6 +16,7 @@ // //////////////////////////////////////////////////////////////////////////////// +import 'dart:collection'; import 'dart:typed_data'; import 'package:test/test.dart' hide test, throws; @@ -97,7 +98,7 @@ Future main([List? args]) async { case RealmValueType.nullValue: expect(value, isA()); break; - case RealmValueType.bool: + case RealmValueType.boolean: expect(value, isA()); break; case RealmValueType.string: @@ -110,8 +111,7 @@ Future main([List? args]) async { expect(value, isA()); break; case RealmValueType.object: - expect(value, isA()); - expect(value, isA()); + expect(value is AnythingGoes || value is Stuff, true); break; case RealmValueType.dateTime: expect(value, isA()); @@ -147,7 +147,7 @@ Future main([List? args]) async { } else if (value is String) { expect(type, RealmValueType.string); } else if (value is bool) { - expect(type, RealmValueType.bool); + expect(type, RealmValueType.boolean); } else if (value is double) { expect(type, RealmValueType.double); } else if (value is DateTime) { @@ -270,7 +270,7 @@ Future main([List? args]) async { final expectedMap = expected as Map; expect(actualMap, hasLength(expectedMap.length)); for (String key in expectedMap.keys) { - expect(actualMap.containsKey(key), true); + expect(actualMap.containsKey(key), true, reason: "Didn't find $key in the actual map"); expectMatches(actualMap[key]!, expectedMap[key]); } break; @@ -516,7 +516,7 @@ Future main([List? args]) async { realm.write(() => realm.add(obj)); } - expect(obj.oneAny.type, RealmValueType.list); + expect(obj.oneAny.type, RealmValueType.map); expectMatches(obj.oneAny, {'bool': true, 'double': 5.3}); writeIfNecessary(realm, () => obj.oneAny = RealmValue.from(999)); @@ -594,10 +594,64 @@ Future main([List? args]) async { }); } - test('List in RealmValue when unmanaged is same instance', () { + test('List inside RealmValue equality', () { + final realm = getMixedRealm(); + final originalList = [1]; + final managedValue = realm.write(() { + return realm.add(AnythingGoes(oneAny: RealmValue.from(originalList))).oneAny; + }); + + final unmanagedValue = RealmValue.from(originalList); + + expect(managedValue.type, RealmValueType.list); + expect(unmanagedValue.type, RealmValueType.list); + + expect(managedValue.asList().isManaged, true); + expect(unmanagedValue.asList().isManaged, false); + + expect(managedValue == unmanagedValue, false); + expect(unmanagedValue == managedValue, false); + expect(managedValue == managedValue, false); + expect(unmanagedValue == unmanagedValue, false); + + // ignore: unrelated_type_equality_checks + expect(managedValue == originalList, false); + + // ignore: unrelated_type_equality_checks + expect(unmanagedValue == originalList, false); + }); + + test('Map inside RealmValue equality', () { + final realm = getMixedRealm(); + final originalMap = {'foo': 'bar'}; + final managedValue = realm.write(() { + return realm.add(AnythingGoes(oneAny: RealmValue.from(originalMap))).oneAny; + }); + + final unmanagedValue = RealmValue.from(originalMap); + + expect(managedValue.type, RealmValueType.map); + expect(unmanagedValue.type, RealmValueType.map); + + expect(managedValue.asMap().isManaged, true); + expect(unmanagedValue.asMap().isManaged, false); + + expect(managedValue == unmanagedValue, false); + expect(unmanagedValue == managedValue, false); + expect(managedValue == managedValue, false); + expect(unmanagedValue == unmanagedValue, false); + + // ignore: unrelated_type_equality_checks + expect(managedValue == originalMap, false); + + // ignore: unrelated_type_equality_checks + expect(unmanagedValue == originalMap, false); + }); + + test('List in RealmValue when unmanaged is equal to original list', () { final list = [RealmValue.bool(true), RealmValue.string('abc')]; final rv = RealmValue.list(list); - expect(identical(rv.asList(), list), true); + expect(rv.asList() == list, true); }); test('List in RealmValue when managed is different instance', () { @@ -608,10 +662,10 @@ Future main([List? args]) async { expect(identical(obj.oneAny.asList(), list), false); }); - test('Map in RealmValue when unmanaged is same instance', () { + test('Map in RealmValue when unmanaged is equal to original map', () { final map = {'bool': RealmValue.bool(true), 'str': RealmValue.string('abc')}; final rv = RealmValue.map(map); - expect(identical(rv.asMap(), map), true); + expect(rv.asMap() == map, true); }); test('Map in RealmValue when managed is different instance', () { From b153064283eb409ad1a5292418541621a0bc4989 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 23 Jan 2024 16:43:12 +0100 Subject: [PATCH 09/18] Add query tests --- src/realm-core | 2 +- test/realm_value_test.dart | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/realm-core b/src/realm-core index a3bf8dcd0..c57a49542 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit a3bf8dcd074459bc3390602c0cb98f13e8faee47 +Subproject commit c57a495425a654b835da916e585afa1caa62fd76 diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 3d42838db..15445adcd 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -790,5 +790,64 @@ Future main([List? args]) async { expect(listChanges, hasLength(2)); expect(mapChanges, hasLength(2)); }); + + test('Queries', () { + final realm = getMixedRealm(); + + late AnythingGoes first; + late AnythingGoes second; + late AnythingGoes third; + + realm.write(() { + first = realm.add(AnythingGoes( + oneAny: RealmValue.from([ + 1, + 'a', + {'foo': 'bar'} + ]))); + + second = realm.add(AnythingGoes( + oneAny: RealmValue.from([ + 2, + {'foo': 'baz'} + ]))); + + third = realm.add(AnythingGoes( + oneAny: RealmValue.from([ + 3, + 'c', + { + 'foo': {'child': 5}, + 'bar': 10 + }, + 3.4 + ]))); + }); + + final listElementQuery = realm.query('oneAny[0] < 3'); + expect(listElementQuery, unorderedMatches([first, second])); + + // TODO: reenable when https://github.com/realm/realm-core/issues/7280 is fixed + // final listLengthQuery = realm.query('oneAny.@size > 3'); + // expect(listLengthQuery, unorderedMatches([third])); + + final listStarQuery = realm.query('oneAny[*] == 3.4'); + expect(listStarQuery, unorderedMatches([third])); + + // TODO: reenable when https://github.com/realm/realm-core/issues/7281 is fixed + // final typeQuery = realm.query("oneAny[2].@type == 'dictionary'"); + // expect(typeQuery, unorderedMatches([first, third])); + + // TODO: reenable when https://github.com/realm/realm-core/issues/7282 is fixed + // final dictionaryInListQuery = realm.query("oneAny[*].foo BEGINSWITH 'ba'"); + // expect(dictionaryInListQuery, unorderedMatches([first, second])); + + // TODO: reenable when https://github.com/realm/realm-core/issues/7283 is fixed + // final dictionaryKeysQuery = realm.query("ANY oneAny[*].foo.@keys == 'child'"); + // expect(dictionaryKeysQuery, unorderedMatches([third])); + + final noMatchesQuery = realm.query("oneAny[*].bar == 9"); + expect(noMatchesQuery, isEmpty); + }); }); } From da17de8bb46d6be2814543095bddee7f2a8a8c77 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 23 Jan 2024 23:43:24 +0100 Subject: [PATCH 10/18] Add tests for indexOf/contains; add tests for sets --- lib/src/list.dart | 14 ++- lib/src/map.dart | 12 ++- lib/src/native/realm_core.dart | 2 +- lib/src/realm_class.dart | 6 +- lib/src/realm_object.dart | 2 +- lib/src/results.dart | 11 +++ lib/src/set.dart | 3 +- test/realm_object_test.dart | 6 +- test/realm_value_test.dart | 153 ++++++++++++++++++++++++++++++++- 9 files changed, 192 insertions(+), 17 deletions(-) diff --git a/lib/src/list.dart b/lib/src/list.dart index ce4bdfb44..e97892538 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -97,6 +97,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme if (element is RealmObjectBase && !element.isManaged) { throw RealmStateError('Cannot call remove on a managed list with an element that is an unmanaged object'); } + final index = indexOf(element); if (index < 0) { return false; @@ -173,6 +174,17 @@ class ManagedRealmList with RealmEntity, ListMixin impleme if (element is RealmObjectBase && !element.isManaged) { throw RealmStateError('Cannot call indexOf on a managed list with an element that is an unmanaged object'); } + + if (element is RealmValue) { + if (element.type.isCollection) { + return -1; + } + + if (element.value is RealmObjectBase && !(element.value as RealmObjectBase).isManaged) { + return -1; + } + } + if (start < 0) start = 0; final index = realmCore.listFind(this, element); return index < start ? -1 : index; // to align with dart list semantics @@ -306,7 +318,7 @@ extension RealmListInternal on RealmList { return; } - if (value is RealmValue && value.isCollection) { + if (value is RealmValue && value.type.isCollection) { realmCore.listAddCollectionAt(handle, realm, index, value, insert || index >= length); return; } diff --git a/lib/src/map.dart b/lib/src/map.dart index b262e01a6..fc3c64bbc 100644 --- a/lib/src/map.dart +++ b/lib/src/map.dart @@ -186,8 +186,14 @@ class ManagedRealmMap with RealmEntity, MapMixin i return false; } - if (value is RealmValue && value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) { - return false; + if (value is RealmValue) { + if (value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) { + return false; + } + + if (value.type.isCollection) { + return false; + } } return realmCore.mapContainsValue(this, value); @@ -277,7 +283,7 @@ extension RealmMapInternal on RealmMap { return; } - if (value is RealmValue && value.isCollection) { + if (value is RealmValue && value.type.isCollection) { realmCore.mapInsertCollection(handle, realm, key, value); return; } diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 35ecc1906..5d207776b 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -3278,7 +3278,7 @@ extension _RealmLibraryEx on RealmLibrary { Pointer _toRealmValue(Object? value, Allocator allocator) { final realm_value = allocator(); - if (value is RealmValue && value.isCollection) { + if (value is RealmValue && value.type.isCollection) { throw RealmError( "Don't use _toPrimitiveValue if the value may contain collections. Use storeValue instead. This is a bug in the Realm Flutter SDK and should be reported to https://github.com/realm/realm-dart/issues/new"); } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 1fb379292..ccd7bcf52 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -1065,11 +1065,9 @@ class RealmAsyncOpenProgressNotificationsController implements ProgressNotificat /// @nodoc extension RealmValueInternal on RealmValue { - bool get isCollection => collectionType != null; - RealmCollectionType? get collectionType { - if (value is List && value is! Uint8List) return RealmCollectionType.list; - if (value is Map) return RealmCollectionType.map; + if (type == RealmValueType.list) return RealmCollectionType.list; + if (type == RealmValueType.map) return RealmCollectionType.map; return null; } } diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index ce9a95567..6f6704750 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -244,7 +244,7 @@ class RealmCoreAccessor implements RealmAccessor { void set(RealmObjectBase object, String name, Object? value, {bool isDefault = false, bool update = false}) { final propertyMeta = metadata[name]; try { - if (value is RealmValue && value.isCollection) { + if (value is RealmValue && value.type.isCollection) { realmCore.objectSetCollection(object, propertyMeta.key, value); return; } diff --git a/lib/src/results.dart b/lib/src/results.dart index 06db973e0..f881c790e 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -81,6 +81,17 @@ class RealmResults extends Iterable with RealmEntity imple if (element is RealmObjectBase && !element.isManaged) { throw RealmStateError('Cannot call $methodName on a results with an element that is an unmanaged object'); } + + if (element is RealmValue) { + if (element.type.isCollection) { + return -1; + } + + if (element.value is RealmObjectBase && !(element.value as RealmObjectBase).isManaged) { + return -1; + } + } + if (start < 0) start = 0; start += _skipOffset; final index = realmCore.resultsFind(this, element); diff --git a/lib/src/set.dart b/lib/src/set.dart index 8d11e7fa5..b1e9c0826 100644 --- a/lib/src/set.dart +++ b/lib/src/set.dart @@ -393,7 +393,8 @@ extension RealmSetOfObject on RealmSet { extension on RealmSet { void _throwOnRealmValueCollection(Object? value) { - if (value is RealmValue && value.isCollection) { + // There's no API in Core that would allow us to store a collection inside a Set. + if (value is RealmValue && value.type.isCollection) { throw RealmStateError('Storing collections inside Set is not supported'); } } diff --git a/test/realm_object_test.dart b/test/realm_object_test.dart index aea77660f..2a2247326 100644 --- a/test/realm_object_test.dart +++ b/test/realm_object_test.dart @@ -379,9 +379,6 @@ Future main([List? args]) async { // listProperty is mapped as `list-with-dashes` expect(json, contains('"list-with-dashes":')); - - // RemappedClass is mapped as `myRemappedClass` - expect(json, contains('"table": "class_myRemappedClass"')); }); test('Remapped class across different files works', () { @@ -394,8 +391,7 @@ Future main([List? args]) async { final json = obj.toJson(); // linkToAnotherClass is mapped as `property with spaces` - // RemappedClass is mapped as `myRemappedClass` - expect(json, contains('"property with spaces":{ "table": "class_myRemappedClass", "key": 0}')); + expect(json, contains('"property with spaces":0')); }); test('RealmObject read/write bool value with json', () { diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 15445adcd..399bc29d6 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////////// -import 'dart:collection'; import 'dart:typed_data'; import 'package:test/test.dart' hide test, throws; @@ -252,6 +251,29 @@ Future main([List? args]) async { expect(results.first.oneAny, realmValues.last); }); + test('Set with numeric values', () { + final realm = getMixedRealm(); + final values = [RealmValue.int(0), RealmValue.double(0.0), RealmValue.bool(false), RealmValue.decimal128(Decimal128.zero), RealmValue.nullValue()]; + final obj = realm.write(() => realm.add(AnythingGoes())..setOfAny.addAll(values)); + + expect(obj.setOfAny, unorderedMatches([RealmValue.int(0), RealmValue.bool(false), RealmValue.nullValue()])); + }); + + test('Set removes duplicates', () { + final realm = getMixedRealm(); + final values = [ + RealmValue.int(1), + RealmValue.nullValue(), + RealmValue.double(2.0), + RealmValue.string('abc'), + RealmValue.nullValue(), + RealmValue.string('abc') + ]; + final obj = realm.write(() => realm.add(AnythingGoes())..setOfAny.addAll(values)); + + expect(obj.setOfAny, unorderedMatches([RealmValue.int(1), RealmValue.double(2.0), RealmValue.nullValue(), RealmValue.string('abc')])); + }); + group('Collections in RealmValue', () { void expectMatches(RealmValue actual, Object? expected) { switch (actual.collectionType) { @@ -621,6 +643,60 @@ Future main([List? args]) async { expect(unmanagedValue == originalList, false); }); + test('List.indexOf for list', () { + final realm = getMixedRealm(); + final originalList = [1]; + final managedList = realm.write(() { + return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalList)])).manyAny; + }); + + final unmanagedList = [RealmValue.from(originalList)]; + + expect(managedList.isManaged, true); + + expect(managedList.indexOf(RealmValue.from(originalList)), -1); + expect(managedList.indexOf(managedList.first), -1); + expect(managedList.contains(RealmValue.from(originalList)), false); + expect(managedList.contains(managedList.first), false); + + expect(managedList.asResults().indexOf(RealmValue.from(originalList)), -1); + expect(managedList.asResults().indexOf(managedList.first), -1); + expect(managedList.asResults().contains(RealmValue.from(originalList)), false); + expect(managedList.asResults().contains(managedList.first), false); + + expect(unmanagedList.indexOf(RealmValue.from(originalList)), -1); + expect(unmanagedList.indexOf(unmanagedList.first), -1); + expect(unmanagedList.contains(RealmValue.from(originalList)), false); + expect(unmanagedList.contains(unmanagedList.first), false); + }); + + test('List.indexOf for map', () { + final realm = getMixedRealm(); + final originalMap = {'foo': 1}; + final managedList = realm.write(() { + return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalMap)])).manyAny; + }); + + final unmanagedList = [RealmValue.from(originalMap)]; + + expect(managedList.isManaged, true); + + expect(managedList.indexOf(RealmValue.from(originalMap)), -1); + expect(managedList.indexOf(managedList.first), -1); + expect(managedList.contains(RealmValue.from(originalMap)), false); + expect(managedList.contains(managedList.first), false); + + expect(managedList.asResults().indexOf(RealmValue.from(originalMap)), -1); + expect(managedList.asResults().indexOf(managedList.first), -1); + expect(managedList.asResults().contains(RealmValue.from(originalMap)), false); + expect(managedList.asResults().contains(managedList.first), false); + + expect(unmanagedList.indexOf(RealmValue.from(originalMap)), -1); + expect(unmanagedList.indexOf(unmanagedList.first), -1); + expect(unmanagedList.contains(RealmValue.from(originalMap)), false); + expect(unmanagedList.contains(unmanagedList.first), false); + }); + test('Map inside RealmValue equality', () { final realm = getMixedRealm(); final originalMap = {'foo': 'bar'}; @@ -648,6 +724,81 @@ Future main([List? args]) async { expect(unmanagedValue == originalMap, false); }); + test('Map.contains for list', () { + final realm = getMixedRealm(); + final originalList = [1]; + final managedMap = realm.write(() { + return realm.add(AnythingGoes(dictOfAny: {'foo': RealmValue.from(originalList)})).dictOfAny; + }); + + final unmanagedMap = {'foo': RealmValue.from(originalList)}; + + expect(managedMap.isManaged, true); + + expect(managedMap.containsValue(RealmValue.from(originalList)), false); + expect(managedMap.containsValue(managedMap.values.first), false); + + expect(managedMap.values.contains(RealmValue.from(originalList)), false); + expect(managedMap.values.contains(managedMap.values.first), false); + + expect(unmanagedMap.containsValue(RealmValue.from(originalList)), false); + expect(unmanagedMap.containsValue(unmanagedMap.values.first), false); + + expect(unmanagedMap.values.contains(RealmValue.from(originalList)), false); + expect(unmanagedMap.values.contains(managedMap.values.first), false); + }); + + test('Map.contains for map', () { + final realm = getMixedRealm(); + final originalMap = {'bar': 1}; + final managedMap = realm.write(() { + return realm.add(AnythingGoes(dictOfAny: {'foo': RealmValue.from(originalMap)})).dictOfAny; + }); + + final unmanagedMap = {'foo': RealmValue.from(originalMap)}; + + expect(managedMap.isManaged, true); + + expect(managedMap.containsValue(RealmValue.from(originalMap)), false); + expect(managedMap.containsValue(managedMap.values.first), false); + + expect(managedMap.values.contains(RealmValue.from(originalMap)), false); + expect(managedMap.values.contains(managedMap.values.first), false); + + expect(unmanagedMap.containsValue(RealmValue.from(originalMap)), false); + expect(unmanagedMap.containsValue(unmanagedMap.values.first), false); + + expect(unmanagedMap.values.contains(RealmValue.from(originalMap)), false); + expect(unmanagedMap.values.contains(managedMap.values.first), false); + }); + + test('Map.indexOf for map', () { + final realm = getMixedRealm(); + final originalMap = {'foo': 1}; + final managedList = realm.write(() { + return realm.add(AnythingGoes(manyAny: [RealmValue.from(originalMap)])).manyAny; + }); + + final unmanagedList = [RealmValue.from(originalMap)]; + + expect(managedList.isManaged, true); + + expect(managedList.indexOf(RealmValue.from(originalMap)), -1); + expect(managedList.indexOf(managedList.first), -1); + expect(managedList.contains(RealmValue.from(originalMap)), false); + expect(managedList.contains(managedList.first), false); + + expect(managedList.asResults().indexOf(RealmValue.from(originalMap)), -1); + expect(managedList.asResults().indexOf(managedList.first), -1); + expect(managedList.asResults().contains(RealmValue.from(originalMap)), false); + expect(managedList.asResults().contains(managedList.first), false); + + expect(unmanagedList.indexOf(RealmValue.from(originalMap)), -1); + expect(unmanagedList.indexOf(unmanagedList.first), -1); + expect(unmanagedList.contains(RealmValue.from(originalMap)), false); + expect(unmanagedList.contains(unmanagedList.first), false); + }); + test('List in RealmValue when unmanaged is equal to original list', () { final list = [RealmValue.bool(true), RealmValue.string('abc')]; final rv = RealmValue.list(list); From 454dd15631f4914e9b72b963a3bcc731944c95a4 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 24 Jan 2024 11:40:57 +0100 Subject: [PATCH 11/18] Create a symlink for test data inside tests --- flutter/realm_flutter/data | 1 - flutter/realm_flutter/tests/data | 1 + .../tests/data/realm_files/old-format.realm | Bin 4096 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 flutter/realm_flutter/data create mode 120000 flutter/realm_flutter/tests/data delete mode 100644 flutter/realm_flutter/tests/data/realm_files/old-format.realm diff --git a/flutter/realm_flutter/data b/flutter/realm_flutter/data deleted file mode 120000 index 4bfafef56..000000000 --- a/flutter/realm_flutter/data +++ /dev/null @@ -1 +0,0 @@ -../../test/data \ No newline at end of file diff --git a/flutter/realm_flutter/tests/data b/flutter/realm_flutter/tests/data new file mode 120000 index 000000000..9f64f7a1d --- /dev/null +++ b/flutter/realm_flutter/tests/data @@ -0,0 +1 @@ +../../../test/data \ No newline at end of file diff --git a/flutter/realm_flutter/tests/data/realm_files/old-format.realm b/flutter/realm_flutter/tests/data/realm_files/old-format.realm deleted file mode 100644 index 758ed862583b6d438cb46222004234d61b66a5dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeHGJ#Q015S`ie#Ty$-M#6%E;)-+#DJ7*ri9|u+0*Nv{!WAgSIh`*^h=LRpDUs6n zC%D69=&l1g7Zj9NQc$=-i6}5{*1NXDPe8kpJ$duqeE98#YGlt3wTw^i?>*YyCVGg| zBGTE>GVi`yuhJrUkrau;^}|6j8XfE%7!W=z_jD9k3eH%qpc1{$PnW5MQVH z=;iUy_V;~WqywCai7oJ9ti9g5xws(^Cx^^DVOJe`5Av-@uDxeTJ;q?=J_VCE9x?~ zv3uMkdJhtJ$jrvSfQa}Ob+Ed#uq7tR)pR2wVU+Qg-eJ|;xbVBgN;TFmCn6fSXx=KdzOerL!? zJ^bE*Crd-|=r4bL{1la6r=N8T7eHHvc=7H0iL7JwDSqDv-DO`NVcvveJg1c(s?Sx^ zWBLEI!yS+P;LEAzc%~~o*LC<(YWp0dB%I(ta3DAk90(2s2Z95^f#5)J;6HKTFWV=q AbpQYW From 06a8a6f2506cc54521127b125a6d59b4f9d254f2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 24 Jan 2024 15:29:58 +0100 Subject: [PATCH 12/18] Add a few more tests --- lib/src/native/realm_core.dart | 22 +++++++++ test/realm_value_test.dart | 85 +++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 5d207776b..354629278 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -3310,6 +3310,8 @@ void _intoRealmQueryArg(Object? value, Pointer realm_query_ar void _intoRealmValueHack(Object? value, realm_value realm_value, Allocator allocator) { if (value is GeoShape) { _intoRealmValue(value.toString(), realm_value, allocator); + } else if (value is RealmValueType) { + _intoRealmValue(value.toQueryArgString(), realm_value, allocator); } else { _intoRealmValue(value, realm_value, allocator); } @@ -3776,3 +3778,23 @@ extension on realm_error { return LastError(error, message, user_code_error.toUserCodeError()); } } + +extension on RealmValueType { + String toQueryArgString() { + return switch (this) { + RealmValueType.nullValue => 'null', + RealmValueType.boolean => 'bool', + RealmValueType.string => 'string', + RealmValueType.int => 'int', + RealmValueType.double => 'double', + RealmValueType.object => 'link', + RealmValueType.objectId => 'objectid', + RealmValueType.dateTime => 'date', + RealmValueType.decimal => 'decimal', + RealmValueType.uuid => 'uuid', + RealmValueType.binary => 'binary', + RealmValueType.list => 'array', + RealmValueType.map => 'object', + }; + } +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 399bc29d6..d189e090c 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -53,14 +53,12 @@ Future main([List? args]) async { } group('RealmValue', () { - final values = [ + final primitiveValues = [ null, true, 'text', 42, 3.14, - AnythingGoes(), - Stuff(), DateTime.utc(2024, 5, 3, 23, 11, 54), ObjectId.fromHexString('64c13ab08edf48a008793cac'), Uuid.fromString('7a459a5e-5eb6-45f6-9b72-8f794e324105'), @@ -68,7 +66,7 @@ Future main([List? args]) async { Uint8List.fromList([1, 2, 0]) ]; - for (final x in values) { + for (final x in primitiveValues) { test('Roundtrip ${x.runtimeType} $x', () { final realm = getMixedRealm(); final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); @@ -76,8 +74,52 @@ Future main([List? args]) async { expect(something.oneAny.value, x); expect(something.oneAny, RealmValue.from(x)); }); + + // TODO: reenable when https://github.com/realm/realm-core/pull/7288 is addressed + final queryArg = RealmValue.from(x); + test('Query @type == ${queryArg.type} $x', () { + final realm = getMixedRealm(); + realm.write(() { + // Add all values, we're going to query for just one of them. + for (final v in primitiveValues) { + realm.add(AnythingGoes(oneAny: RealmValue.from(v))); + } + realm.add(AnythingGoes(oneAny: RealmValue.from(Stuff()))); + }); + + final matches = realm.query(r'oneAny.@type == $0', [queryArg.type]); + expect(matches.length, 1); + expect(matches.single.oneAny.value, x); + expect(matches.single.oneAny.type, queryArg.type); + expect(matches.single.oneAny, queryArg); + }, skip: 'Depends on https://github.com/realm/realm-core/pull/7288'); } + test('Roundtrip object', () { + final stuff = Stuff(i: 123); + final realm = getMixedRealm(); + final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(stuff)))); + expect(something.oneAny.value.runtimeType, Stuff); + expect(something.oneAny.as().i, 123); + }); + + // TODO: reenable when https://github.com/realm/realm-core/pull/7288 is addressed + test('Query @type == object', () { + final realm = getMixedRealm(); + realm.write(() { + for (final v in primitiveValues) { + realm.add(AnythingGoes(oneAny: RealmValue.from(v))); + } + + realm.add(AnythingGoes(oneAny: RealmValue.from(Stuff(i: 123)))); + }); + + final matches = realm.query(r'oneAny.@type == $0', [RealmValueType.object]); + expect(matches.length, 1); + expect(matches.single.oneAny.as().i, 123); + expect(matches.single.oneAny.type, RealmValueType.object); + }, skip: 'Depends on https://github.com/realm/realm-core/pull/7288'); + test('Illegal value', () { final realm = getMixedRealm(); expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(realm)))), throwsArgumentError); @@ -88,7 +130,7 @@ Future main([List? args]) async { expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(TuckedIn())))), throwsArgumentError); }); - for (final x in values) { + for (final x in primitiveValues) { test('Switch $x', () { final something = AnythingGoes(oneAny: RealmValue.from(x)); final value = something.oneAny.value; @@ -134,7 +176,7 @@ Future main([List? args]) async { }); } - for (final x in values) { + for (final x in primitiveValues) { test('If-is $x', () { final something = AnythingGoes(oneAny: RealmValue.from(x)); final value = something.oneAny.value; @@ -827,6 +869,7 @@ Future main([List? args]) async { expect(identical(obj.oneAny.asMap(), map), false); }); + // TODO: reenable when https://github.com/realm/realm-core/issues/7270 is addressed test('Notifications', () async { final realm = getMixedRealm(); final obj = AnythingGoes( @@ -891,14 +934,12 @@ Future main([List? args]) async { expect(parentChanges, hasLength(2)); expect(parentChanges[1].properties, ['oneAny']); - // Collection changes are only emitted if the collection is directly modified - // but won't be emitted if an item inside the collection changes. In this case, - // we're modifying the dictionary inside the list, but not reassigning any list - // elements, so we shouldn't get a notification - - // TODO: this is inconsistent with how lists behave today - talk to Claus/Ferdinando - // about the expectations - expect(listChanges, hasLength(1)); + expect(listChanges, hasLength(2)); + expect(listChanges[1].inserted, isEmpty); + expect(listChanges[1].deleted, isEmpty); + expect(listChanges[1].modified, [1]); + expect(listChanges[1].isCleared, false); + expect(listChanges[1].isCollectionDeleted, false); expect(mapChanges, hasLength(1)); expect(mapChanges[0].modified, ['list']); @@ -916,12 +957,12 @@ Future main([List? args]) async { expect(parentChanges, hasLength(3)); expect(parentChanges[2].properties, ['oneAny']); - expect(listChanges, hasLength(2)); - expect(listChanges[1].inserted, isEmpty); - expect(listChanges[1].deleted, [1]); - expect(listChanges[1].modified, isEmpty); - expect(listChanges[1].isCleared, false); - expect(listChanges[1].isCollectionDeleted, false); + expect(listChanges, hasLength(3)); + expect(listChanges[2].inserted, isEmpty); + expect(listChanges[2].deleted, [1]); + expect(listChanges[2].modified, isEmpty); + expect(listChanges[2].isCleared, false); + expect(listChanges[2].isCollectionDeleted, false); expect(mapChanges, hasLength(2)); expect(mapChanges[1].isCollectionDeleted, true); @@ -938,9 +979,9 @@ Future main([List? args]) async { // Subscriptions have been canceled - shouldn't get more notifications expect(parentChanges, hasLength(3)); - expect(listChanges, hasLength(2)); + expect(listChanges, hasLength(3)); expect(mapChanges, hasLength(2)); - }); + }, skip: 'Depends on https://github.com/realm/realm-core/issues/7270'); test('Queries', () { final realm = getMixedRealm(); From 47c93089c6f0954c503d74d8f017820c5022f485 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Wed, 24 Jan 2024 15:31:38 +0100 Subject: [PATCH 13/18] Correct Core issue link --- test/realm_value_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index d189e090c..07673dcf3 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -75,7 +75,7 @@ Future main([List? args]) async { expect(something.oneAny, RealmValue.from(x)); }); - // TODO: reenable when https://github.com/realm/realm-core/pull/7288 is addressed + // TODO: reenable when https://github.com/realm/realm-core/issues/7289 is addressed final queryArg = RealmValue.from(x); test('Query @type == ${queryArg.type} $x', () { final realm = getMixedRealm(); @@ -92,7 +92,7 @@ Future main([List? args]) async { expect(matches.single.oneAny.value, x); expect(matches.single.oneAny.type, queryArg.type); expect(matches.single.oneAny, queryArg); - }, skip: 'Depends on https://github.com/realm/realm-core/pull/7288'); + }, skip: 'Depends on https://github.com/realm/realm-core/issues/7289'); } test('Roundtrip object', () { @@ -103,7 +103,7 @@ Future main([List? args]) async { expect(something.oneAny.as().i, 123); }); - // TODO: reenable when https://github.com/realm/realm-core/pull/7288 is addressed + // TODO: reenable when https://github.com/realm/realm-core/issues/7289 is addressed test('Query @type == object', () { final realm = getMixedRealm(); realm.write(() { @@ -118,7 +118,7 @@ Future main([List? args]) async { expect(matches.length, 1); expect(matches.single.oneAny.as().i, 123); expect(matches.single.oneAny.type, RealmValueType.object); - }, skip: 'Depends on https://github.com/realm/realm-core/pull/7288'); + }, skip: 'Depends on https://github.com/realm/realm-core/issues/7289'); test('Illegal value', () { final realm = getMixedRealm(); From 0491d8d52485f16ad6e9f85b83d6a927f2a1ae2c Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 25 Jan 2024 14:24:18 +0100 Subject: [PATCH 14/18] enable notifications tests --- src/realm-core | 2 +- test/realm_value_test.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/realm-core b/src/realm-core index c57a49542..3e810b2c1 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit c57a495425a654b835da916e585afa1caa62fd76 +Subproject commit 3e810b2c19e2a1b271dedbf6ca2f08ca636d7085 diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 07673dcf3..dd742fde4 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -869,7 +869,6 @@ Future main([List? args]) async { expect(identical(obj.oneAny.asMap(), map), false); }); - // TODO: reenable when https://github.com/realm/realm-core/issues/7270 is addressed test('Notifications', () async { final realm = getMixedRealm(); final obj = AnythingGoes( @@ -981,7 +980,7 @@ Future main([List? args]) async { expect(parentChanges, hasLength(3)); expect(listChanges, hasLength(3)); expect(mapChanges, hasLength(2)); - }, skip: 'Depends on https://github.com/realm/realm-core/issues/7270'); + }); test('Queries', () { final realm = getMixedRealm(); From bb360f1c98bcbdd04ea9efa11b1369f1ccac6d0b Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Fri, 26 Jan 2024 17:12:11 +0100 Subject: [PATCH 15/18] Enable some tests --- src/realm-core | 2 +- test/realm_value_test.dart | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/realm-core b/src/realm-core index 3e810b2c1..8caf13652 160000 --- a/src/realm-core +++ b/src/realm-core @@ -1 +1 @@ -Subproject commit 3e810b2c19e2a1b271dedbf6ca2f08ca636d7085 +Subproject commit 8caf13652ca5d187a3a2a2c81e480924a4d4a4ed diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index dd742fde4..85cd559db 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -75,7 +75,6 @@ Future main([List? args]) async { expect(something.oneAny, RealmValue.from(x)); }); - // TODO: reenable when https://github.com/realm/realm-core/issues/7289 is addressed final queryArg = RealmValue.from(x); test('Query @type == ${queryArg.type} $x', () { final realm = getMixedRealm(); @@ -92,7 +91,7 @@ Future main([List? args]) async { expect(matches.single.oneAny.value, x); expect(matches.single.oneAny.type, queryArg.type); expect(matches.single.oneAny, queryArg); - }, skip: 'Depends on https://github.com/realm/realm-core/issues/7289'); + }); } test('Roundtrip object', () { @@ -103,7 +102,6 @@ Future main([List? args]) async { expect(something.oneAny.as().i, 123); }); - // TODO: reenable when https://github.com/realm/realm-core/issues/7289 is addressed test('Query @type == object', () { final realm = getMixedRealm(); realm.write(() { @@ -118,7 +116,7 @@ Future main([List? args]) async { expect(matches.length, 1); expect(matches.single.oneAny.as().i, 123); expect(matches.single.oneAny.type, RealmValueType.object); - }, skip: 'Depends on https://github.com/realm/realm-core/issues/7289'); + }); test('Illegal value', () { final realm = getMixedRealm(); @@ -1018,16 +1016,14 @@ Future main([List? args]) async { final listElementQuery = realm.query('oneAny[0] < 3'); expect(listElementQuery, unorderedMatches([first, second])); - // TODO: reenable when https://github.com/realm/realm-core/issues/7280 is fixed - // final listLengthQuery = realm.query('oneAny.@size > 3'); - // expect(listLengthQuery, unorderedMatches([third])); + final listLengthQuery = realm.query('oneAny.@size > 3'); + expect(listLengthQuery, unorderedMatches([third])); final listStarQuery = realm.query('oneAny[*] == 3.4'); expect(listStarQuery, unorderedMatches([third])); - // TODO: reenable when https://github.com/realm/realm-core/issues/7281 is fixed - // final typeQuery = realm.query("oneAny[2].@type == 'dictionary'"); - // expect(typeQuery, unorderedMatches([first, third])); + final typeQuery = realm.query("oneAny[2].@type == 'dictionary'"); + expect(typeQuery, unorderedMatches([first, third])); // TODO: reenable when https://github.com/realm/realm-core/issues/7282 is fixed // final dictionaryInListQuery = realm.query("oneAny[*].foo BEGINSWITH 'ba'"); From 5d4764574da44c19d5f3620807332be5bc57fc66 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 29 Jan 2024 13:39:21 +0100 Subject: [PATCH 16/18] Re-generate ffi --- lib/src/native/realm_bindings.dart | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/src/native/realm_bindings.dart b/lib/src/native/realm_bindings.dart index 8e0de8f90..8fa89fbbf 100644 --- a/lib/src/native/realm_bindings.dart +++ b/lib/src/native/realm_bindings.dart @@ -1014,6 +1014,30 @@ class RealmLibrary { late final _realm_app_get_app_id = _realm_app_get_app_idPtr .asFunction Function(ffi.Pointer)>(); + /// Return the current base URL value used by the app. If the realm_app_update_base_url() is called, this + /// value will match the base_url value provided to that function when the update is complete. The value + /// provided by this function is undefined if the realm_app_update_base_url() operation is in progress, + /// since it will likely be the base_url value prior to realm_app_update_base_url() being called. + /// + /// @param app ptr to realm_app + /// @return The current base URL string used by the app + /// + /// Return value must be manually released with realm_free(). + ffi.Pointer realm_app_get_base_url( + ffi.Pointer app, + ) { + return _realm_app_get_base_url( + app, + ); + } + + late final _realm_app_get_base_urlPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('realm_app_get_base_url'); + late final _realm_app_get_base_url = _realm_app_get_base_urlPtr + .asFunction Function(ffi.Pointer)>(); + /// Get a cached realm_app_t* instance given an app id. out_app may be null if the app with this id hasn't been /// previously cached by calling realm_app_create_cached. /// @@ -1482,6 +1506,50 @@ class RealmLibrary { _realm_app_sync_client_wait_for_sessions_to_terminatePtr .asFunction)>(); + /// Update the URL used to communicate with the Realm server. This function will update the location + /// information used for http and websocket requests to the server. Once this operation has completed, + /// the new base_url value returned by realm_app_get_base_url() will match the base_url value provided + /// to this function. Any App requests performed while the base URl update is currently in progress + /// will continue to use the original base URL value. + /// + /// @param app ptr to realm_app + /// @param base_url The new base URL value to set as the Realm server URL - a null or empty string will + /// use the default base URL value + /// @param callback invoked once operation has completed + /// @return True if no error has been recorded, False otherwise + bool realm_app_update_base_url( + ffi.Pointer app, + ffi.Pointer base_url, + realm_app_void_completion_func_t callback, + ffi.Pointer userdata, + realm_free_userdata_func_t userdata_free, + ) { + return _realm_app_update_base_url( + app, + base_url, + callback, + userdata, + userdata_free, + ); + } + + late final _realm_app_update_base_urlPtr = _lookup< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + realm_app_void_completion_func_t, + ffi.Pointer, + realm_free_userdata_func_t)>>('realm_app_update_base_url'); + late final _realm_app_update_base_url = + _realm_app_update_base_urlPtr.asFunction< + bool Function( + ffi.Pointer, + ffi.Pointer, + realm_app_void_completion_func_t, + ffi.Pointer, + realm_free_userdata_func_t)>(); + /// Creates a user API key that can be used to authenticate as the current user. /// @return True if no error was recorded. False otherwise bool realm_app_user_apikey_provider_client_create_apikey( @@ -11865,6 +11933,7 @@ abstract class realm_errno { static const int RLM_ERR_WRONG_SYNC_TYPE = 1043; static const int RLM_ERR_SYNC_WRITE_NOT_ALLOWED = 1044; static const int RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH = 1045; + static const int RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR = 1046; static const int RLM_ERR_SYSTEM_ERROR = 1999; static const int RLM_ERR_LOGIC = 2000; static const int RLM_ERR_NOT_SUPPORTED = 2001; From 8241d20de99d8d1e8ea068da887c3da78dfa5bef Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 29 Jan 2024 14:08:34 +0100 Subject: [PATCH 17/18] Fix test --- test/results_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/results_test.dart b/test/results_test.dart index fffe93a73..aa2607c70 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -250,7 +250,7 @@ Future main([List? args]) async { var config = Configuration.local([Car.schema]); var realm = getRealm(config); realm.write(() => realm.add(Car("Audi"))); - expect(() => realm.all().query(r'make == $0', [1]), throws("Unsupported comparison between type")); + expect(() => realm.all().query(r'make == $0', [1]), throws("Cannot compare argument")); }); test('Results query with wrong argument types (bool for int) throws ', () { From 67f5e49eedcbe71498fad824cd0dd1bd698ca7ea Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 1 Feb 2024 14:58:56 +0100 Subject: [PATCH 18/18] Added a changelog entry --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca2de586..b73ee9598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## vNext-next (TBD) +### Breaking Changes +* `RealmValue.type` is now an enum of type `RealmValueType` rather than `Type`. If you need the runtime type of the value wrapped in `RealmValue`, use `RealmValue.value.runtimeType`. (Issue [#1505](https://github.com/realm/realm-dart/issues/1505)) +* Renamed `RealmValue.uint8List` constructor to `RealmValue.binary`. (PR [#1469](https://github.com/realm/realm-dart/pull/1469)) + ### Enhancements * Added `isCollectionDeleted` to `RealmListChanges`, `RealmSetChanges`, and `RealmMapChanges` which will be `true` if the parent object, containing the collection has been deleted. (Core 14.0.0) * Added `isCleared` to `RealmMapChanges` which will be `true` if the map has been cleared. (Core 14.0.0) @@ -14,6 +18,39 @@ realm.query('dogs[LAST].age = 5'); // Query all owners whose last dog is 5 years old realm.query('dogs[SIZE] = 10'); // Query all owners who have 10 dogs ``` +* Added support for storing lists and maps inside a `RealmValue` property. (Issue [#1504](https://github.com/realm/realm-dart/issues/1504)) + ```dart + class _Container { + late RealmValue anything; + } + + realm.write(() { + realm.add(Container(anything: RealmValue.from([1, 'foo', 3.14]))); + }); + + final container = realm.all().first; + + final list = container.anything.asList(); // will throw if cast is invalid + for (final item in containerValue) { + switch (item.type) { + case RealmValueType.int: + print('Integer: ${item.value as int}'); + break; + case RealmValueType.string: + print('String: ${item.value as String}'); + break; + case RealmValueType.double: + print('Double: ${item.value as double}'); + break; + } + } + + final subscription = list.changes.listen((event) { + // The list changed + }); + ``` +* Added `RealmValueType` enum that contains all the possible types that can be wrapped by a `RealmValue`. (PR [#1469](https://github.com/realm/realm-dart/pull/1469)) + ### Fixed * If you have more than 8388606 links pointing to one specific object, the program will crash. (Core 14.0.0)