diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad4670c9..f5e5c2e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,37 @@ * The `App(AppConfiguration)` constructor should only be used on the main isolate. Ideally, it should be called once as soon as your app launches. If you attempt to use it on a background isolate (as indicated by `Isolate.debugName` being different from `main`), a warning will be logged. * Added a new method - `App.getById` that allows you to obtain an already constructed app on a background isolate. (Issue [#1433](https://github.com/realm/realm-dart/issues/1433)) +* Added support for fields of type `Map` where `T` is any supported Realm type. You can define a model with a map like: + ```dart + @RealmModel() + class _LotsOfMaps { + late Map persons; + late Map bools; + late Map dateTimes; + late Map decimals; + late Map doubles; + late Map ints; + late Map objectIds; + late Map realmValues; + late Map strings; + late Map datas; + late Map uuids; + } + ``` + + The map keys may not contain `.` or start with `$`. (Issue [#685](https://github.com/realm/realm-dart/issues/685)) ### Fixed * Fixed warnings being emitted by the realm generator requesting that `xyz.g.dart` be included with `part 'xyz.g.dart';` for `xyz.dart` files that import `realm` but don't have realm models defined. Those should not need generated parts and including the part file would have resulted in an empty file with `// ignore_for_file: type=lint` being generated. (PR [#1443](https://github.com/realm/realm-dart/pull/1443)) * Updated the minimum required CMake version for Flutter on Linux to 3.19. (Issue [#1381](https://github.com/realm/realm-dart/issues/1381)) * Errors in user-provided client reset callbacks, such as `RecoverOrDiscardUnsyncedChangesHandler.onBeforeReset/onAfterDiscard` would not be correctly propagated and the client reset exception would contain a message like `A fatal error occurred during client reset: 'User-provided callback failed'` but no details about the actual error. Now `SyncError` has an `innerError` field which contains the original error thrown in the callback. (PR [#1447](https://github.com/realm/realm-dart/pull/1447)) +* Fixed a bug where the generator would not emit errors for invalid default values for collection properties. Default values for collection properties are not supported unless the default value is an empty collection. (PR [#1406](https://github.com/realm/realm-dart/pull/1406)) ### Compatibility * Realm Studio: 13.0.0 or later. ### Internal -* Using Core x.y.z. +* Using Core 13.24.0. ## 1.6.1 (2023-11-30) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index 7f48cc2a9..5f57e59cf 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -21,6 +21,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; import 'package:sane_uuid/uuid.dart'; +import 'package:collection/collection.dart'; Type _typeOf() => T; @@ -98,7 +99,20 @@ enum RealmCollectionType { list, set, _3, // ignore: unused_field, constant_identifier_names - dictionary, + map; + + String get plural { + switch (this) { + case RealmCollectionType.list: + return "lists"; + case RealmCollectionType.set: + return "sets"; + case RealmCollectionType.map: + return "maps"; + default: + return "none"; + } + } } /// A base class of all Realm errors. @@ -216,6 +230,10 @@ class RealmValue { @override operator ==(Object? other) { 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; } diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index e564797c4..744149334 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -55,9 +55,7 @@ extension DartTypeEx on DartType { RealmCollectionType get realmCollectionType { if (isDartCoreSet) return RealmCollectionType.set; if (isDartCoreList) return RealmCollectionType.list; - if (isDartCoreMap && (this as ParameterizedType).typeArguments.first == session.typeProvider.stringType) { - return RealmCollectionType.dictionary; - } + if (isDartCoreMap) return RealmCollectionType.map; return RealmCollectionType.none; } @@ -79,18 +77,14 @@ extension DartTypeEx on DartType { if (self is ParameterizedType) { final mapped = self.typeArguments.last.mappedType; if (self != mapped) { - final provider = session.typeProvider; if (self.isDartCoreList) { - final mappedList = provider.listType(mapped); - return PseudoType('Realm${mappedList.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedList.nullabilitySuffix); + return PseudoType('RealmList<${mapped.getDisplayString(withNullability: true)}>'); } if (self.isDartCoreSet) { - final mappedSet = provider.setType(mapped); - return PseudoType('Realm${mappedSet.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedSet.nullabilitySuffix); + return PseudoType('RealmSet<${mapped.getDisplayString(withNullability: true)}>'); } if (self.isDartCoreMap) { - final mappedMap = provider.mapType(self.typeArguments.first, mapped); - return PseudoType('Realm${mappedMap.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedMap.nullabilitySuffix); + return PseudoType('RealmMap<${mapped.getDisplayString(withNullability: true)}>'); } } } diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index 64605e071..a95b60a6b 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////////// import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/src/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; @@ -93,17 +94,6 @@ extension FieldElementEx on FieldElement { final indexed = indexedInfo; final backlink = backlinkInfo; - // Check for as-of-yet unsupported type - if (type.isDartCoreMap) { - throw RealmInvalidGenerationSourceError( - 'Field type not supported yet', - element: this, - primarySpan: typeSpan(span!.file), - primaryLabel: 'not yet supported', - todo: 'Avoid using $modelTypeName for now', - ); - } - // Validate primary key if (primaryKey != null) { if (indexed != null) { @@ -220,7 +210,7 @@ extension FieldElementEx on FieldElement { } else { // Validate collections and backlinks if (type.isRealmCollection || backlink != null) { - final typeDescription = type.isRealmCollection ? (type.isRealmSet ? 'sets' : 'collections') : 'backlinks'; + final typeDescription = type.isRealmCollection ? type.realmCollectionType.plural : 'backlinks'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( 'Realm $typeDescription cannot be nullable', @@ -231,40 +221,65 @@ extension FieldElementEx on FieldElement { ); } final itemType = type.basicType; - if (itemType.isRealmModel && itemType.isNullable) { - throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription', + final objectsShouldBeNullable = type.realmCollectionType == RealmCollectionType.map; + if (itemType.isRealmModel && itemType.isNullable != objectsShouldBeNullable) { + final requestedObjectType = objectsShouldBeNullable ? 'nullable' : 'non-nullable'; + final invalidObjectType = objectsShouldBeNullable ? 'non-nullable' : 'nullable'; + + throw RealmInvalidGenerationSourceError('Realm objects in $typeDescription must be $requestedObjectType', primarySpan: typeSpan(file), - primaryLabel: 'which has a nullable realm object element type', + primaryLabel: 'which has a $invalidObjectType realm object element type', element: this, - todo: 'Ensure element type is non-nullable'); + todo: 'Ensure element type is $requestedObjectType'); } - if (type.isRealmSet) { - final typeArgument = (type as ParameterizedType).typeArguments.first; - if (realmSetUnsupportedRealmTypes.contains(realmType)) { - throw RealmInvalidGenerationSourceError('$type is not supported', - primarySpan: typeSpan(file), - primaryLabel: 'Set element type is not supported', - element: this, - todo: 'Ensure set element type $typeArgument is a type supported by RealmSet.'); - } - - if (realmType == RealmPropertyType.mixed && typeArgument.isNullable) { - throw RealmInvalidGenerationSourceError('$type is not supported', - primarySpan: typeSpan(file), - primaryLabel: 'Set of nullable RealmValues is not supported', - element: this, - todo: 'Did you mean to use Set instead?'); - } - - final initExpression = initializerExpression; - if (initExpression != null) { - throw RealmInvalidGenerationSourceError('Default values for set are not supported.', - primarySpan: initializerExpressionSpan(file, initExpression), - primaryLabel: 'Remove the default value.', - element: this, - todo: 'Remove the default value for field $displayName.'); - } + if (realmType == RealmPropertyType.mixed && itemType.isNullable) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Nullable RealmValues are not supported', + element: this, + todo: 'Ensure the RealmValue type argument is non-nullable. RealmValue can hold null, but must not be nullable itself.'); + } + + if (itemType.isRealmCollection || itemType.realmType == RealmPropertyType.linkingObjects) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Collections of collections are not supported', + element: this, + todo: 'Ensure the collection element type $itemType is not Iterable.'); + } + + final initExpression = initializerExpression; + if (initExpression != null && !_isValidCollectionInitializer(initExpression)) { + throw RealmInvalidGenerationSourceError('Non-empty default values for $typeDescription are not supported.', + primarySpan: initializerExpressionSpan(file, initExpression), + primaryLabel: 'Remove the default value.', + element: this, + todo: 'Remove the default value for field $displayName or change it to be an empty collection.'); + } + + switch (type.realmCollectionType) { + case RealmCollectionType.map: + final keyType = (type as ParameterizedType).typeArguments.first; + if (!keyType.isDartCoreString || keyType.isNullable) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Non-String keys are not supported in maps', + element: this, + todo: 'Change the map key type to be String'); + } + break; + case RealmCollectionType.set: + if (itemType.realmObjectType == ObjectType.embeddedObject) { + throw RealmInvalidGenerationSourceError('$type is not supported', + primarySpan: typeSpan(file), + primaryLabel: 'Embedded objects in sets are not supported', + element: this, + todo: 'Change the collection element to be a non-embedded object'); + } + break; + default: + break; } } @@ -341,7 +356,7 @@ extension FieldElementEx on FieldElement { 'RealmValue fields cannot be nullable', primarySpan: typeSpan(file), primaryLabel: '$modelTypeName is nullable', - todo: 'Change type to ${modelType.asNonNullable}', + todo: 'Change type to RealmValue. RealmValue can hold null, but must not be nullable itself.', element: this, ); } @@ -368,4 +383,16 @@ extension FieldElementEx on FieldElement { ); } } + + bool _isValidCollectionInitializer(Expression initExpression) { + if (initExpression is AstNodeImpl) { + final astNode = initExpression as AstNodeImpl; + final elementsNode = astNode.namedChildEntities.where((e) => e.name == 'elements').singleOrNull; + final nodeValue = elementsNode?.value; + if (nodeValue is NodeList && nodeValue.isEmpty) { + return true; + } + } + return false; + } } diff --git a/generator/lib/src/realm_field_info.dart b/generator/lib/src/realm_field_info.dart index 0260b0365..1a5fc0616 100644 --- a/generator/lib/src/realm_field_info.dart +++ b/generator/lib/src/realm_field_info.dart @@ -43,8 +43,6 @@ class RealmFieldInfo { DartType get type => fieldElement.type; bool get isFinal => fieldElement.isFinal; - bool get isRealmCollection => type.isRealmCollection; - bool get isDartCoreSet => type.isDartCoreSet; bool get isLate => fieldElement.isLate; bool get hasDefaultValue => fieldElement.hasInitializer; bool get optional => type.basicType.isNullable || realmType == RealmPropertyType.mixed; @@ -53,6 +51,11 @@ class RealmFieldInfo { bool get isMixed => realmType == RealmPropertyType.mixed; bool get isComputed => isRealmBacklink; // only computed, so far + bool get isRealmCollection => type.isRealmCollection; + bool get isDartCoreList => type.isDartCoreList; + bool get isDartCoreSet => type.isDartCoreSet; + bool get isDartCoreMap => type.isDartCoreMap; + String get name => fieldElement.name; String get realmName => mapTo ?? name; @@ -61,17 +64,18 @@ class RealmFieldInfo { String get basicNonNullableMappedTypeName => type.basicType.asNonNullable.mappedName; String get basicRealmTypeName => - fieldElement.modelType.basicType.asNonNullable.element?.remappedRealmName ?? fieldElement.modelType.asNonNullable.basicMappedName; + fieldElement.modelType.basicType.asNonNullable.element?.remappedRealmName ?? fieldElement.modelType.basicType.asNonNullable.basicMappedName; String get modelTypeName => fieldElement.modelTypeName; String get mappedTypeName => fieldElement.mappedTypeName; String get initializer { - if (type.isDartCoreList) return ' = const []'; - if (isMixed && !type.isRealmCollection) return ' = const RealmValue.nullValue()'; + if (type.realmCollectionType == RealmCollectionType.list) return ' = const []'; + if (type.realmCollectionType == RealmCollectionType.set) return ' = const {}'; + if (type.realmCollectionType == RealmCollectionType.map) return ' = const {}'; + if (isMixed) return ' = const RealmValue.nullValue()'; if (hasDefaultValue) return ' = ${fieldElement.initializerExpression}'; - if (type.isDartCoreSet) return ' = const {}'; return ''; // no initializer } diff --git a/generator/lib/src/realm_model_info.dart b/generator/lib/src/realm_model_info.dart index 84843ae4c..21f64e57f 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -43,15 +43,17 @@ class RealmModelInfo { yield ''; } + // Constructor yield '$name('; { final required = allSettable.where((f) => f.isRequired || f.isPrimaryKey); yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); final notRequired = allSettable.where((f) => !f.isRequired && !f.isPrimaryKey); - final collections = fields.where((f) => f.isRealmCollection && !f.isDartCoreSet).toList(); + final lists = fields.where((f) => f.isDartCoreList).toList(); final sets = fields.where((f) => f.isDartCoreSet).toList(); - if (notRequired.isNotEmpty || collections.isNotEmpty || sets.isNotEmpty) { + final maps = fields.where((f) => f.isDartCoreMap).toList(); + if (notRequired.isNotEmpty || lists.isNotEmpty || sets.isNotEmpty || maps.isNotEmpty) { yield '{'; yield* notRequired.map((f) { if (f.type.isUint8List && f.hasDefaultValue) { @@ -59,8 +61,9 @@ class RealmModelInfo { } return '${f.mappedTypeName} ${f.name}${f.initializer},'; }); - yield* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},'); + yield* lists.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},'); yield* sets.map((c) => 'Set<${c.type.basicMappedName}> ${c.name}${c.initializer},'); + yield* maps.map((c) => 'Map ${c.name}${c.initializer},'); yield '}'; } @@ -82,32 +85,40 @@ class RealmModelInfo { return "RealmObjectBase.set(this, '${f.realmName}', ${f.name});"; }); - yield* collections.map((c) { + yield* lists.map((c) { return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; }); yield* sets.map((c) { return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; }); + + yield* maps.map((c) { + return "RealmObjectBase.set<${c.mappedTypeName}>(this, '${c.realmName}', ${c.mappedTypeName}(${c.name}));"; + }); } yield '}'; yield ''; yield '$name._();'; yield ''; + // Properties yield* fields.expand((f) => [ ...f.toCode(), '', ]); + // Changes yield '@override'; yield 'Stream> get changes => RealmObjectBase.getChanges<$name>(this);'; yield ''; + // Freeze yield '@override'; yield '$name freeze() => RealmObjectBase.freezeObject<$name>(this);'; yield ''; + // Schema yield 'static SchemaObject get schema => _schema ??= _initSchema();'; yield 'static SchemaObject? _schema;'; yield 'static SchemaObject _initSchema() {'; diff --git a/generator/test/error_test_data/dict_non_empty_initializer.dart b/generator/test/error_test_data/dict_non_empty_initializer.dart new file mode 100644 index 000000000..14f908828 --- /dev/null +++ b/generator/test/error_test_data/dict_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'dict_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final dictWithInitializer = {"a": 5}; +} diff --git a/generator/test/error_test_data/dict_non_empty_initializer.expected b/generator/test/error_test_data/dict_non_empty_initializer.expected new file mode 100644 index 000000000..d37b18643 --- /dev/null +++ b/generator/test/error_test_data/dict_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for maps are not supported. + +in: asset:pkg/test/error_test_data/dict_non_empty_initializer.dart:8:31 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final dictWithInitializer = {"a": 5}; + │ ^^^^^^^^ Remove the default value. + ╵ +Remove the default value for field dictWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/list_non_empty_initializer.dart b/generator/test/error_test_data/list_non_empty_initializer.dart new file mode 100644 index 000000000..69dee294a --- /dev/null +++ b/generator/test/error_test_data/list_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'list_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final listWithInitializer = [0]; +} diff --git a/generator/test/error_test_data/list_non_empty_initializer.expected b/generator/test/error_test_data/list_non_empty_initializer.expected new file mode 100644 index 000000000..e3ba0e158 --- /dev/null +++ b/generator/test/error_test_data/list_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for lists are not supported. + +in: asset:pkg/test/error_test_data/list_non_empty_initializer.dart:8:31 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final listWithInitializer = [0]; + │ ^^^ Remove the default value. + ╵ +Remove the default value for field listWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/map_unsupported.dart b/generator/test/error_test_data/map_unsupported.dart index d7b8eb47d..203422b10 100644 --- a/generator/test/error_test_data/map_unsupported.dart +++ b/generator/test/error_test_data/map_unsupported.dart @@ -4,5 +4,5 @@ import 'package:realm_common/realm_common.dart'; @RealmModel() class _Person { - late Map relatives; + late Map relatives; } diff --git a/generator/test/error_test_data/map_unsupported.expected b/generator/test/error_test_data/map_unsupported.expected index a570d65dc..15697d247 100644 --- a/generator/test/error_test_data/map_unsupported.expected +++ b/generator/test/error_test_data/map_unsupported.expected @@ -1,12 +1,12 @@ -Field type not supported yet +Map is not supported in: asset:pkg/test/error_test_data/map_unsupported.dart:7:8 ╷ 5 │ @RealmModel() 6 │ class _Person { │ ━━━━━━━ in realm model for 'Person' -7 │ late Map relatives; - │ ^^^^^^^^^^^^^^^^^^^^ not yet supported +7 │ late Map relatives; + │ ^^^^^^^^^^^^^^^^^^ Non-String keys are not supported in maps ╵ -Avoid using Map for now +Change the map key type to be String diff --git a/generator/test/error_test_data/nullable_list.expected b/generator/test/error_test_data/nullable_list.expected index 964176e97..98ec60e20 100644 --- a/generator/test/error_test_data/nullable_list.expected +++ b/generator/test/error_test_data/nullable_list.expected @@ -1,4 +1,4 @@ -Realm collections cannot be nullable +Realm lists cannot be nullable in: asset:pkg/test/error_test_data/nullable_list.dart:10:3 ╷ diff --git a/generator/test/error_test_data/nullable_list_elements.expected b/generator/test/error_test_data/nullable_list_elements.expected index a28c1fa37..a817a5789 100644 --- a/generator/test/error_test_data/nullable_list_elements.expected +++ b/generator/test/error_test_data/nullable_list_elements.expected @@ -1,4 +1,4 @@ -Nullable realm objects are not allowed in collections +Realm objects in lists must be non-nullable in: asset:pkg/test/error_test_data/nullable_list_elements.dart:14:8 ╷ diff --git a/generator/test/error_test_data/nullable_realm_value.expected b/generator/test/error_test_data/nullable_realm_value.expected index 111575679..5ec8abfef 100644 --- a/generator/test/error_test_data/nullable_realm_value.expected +++ b/generator/test/error_test_data/nullable_realm_value.expected @@ -8,5 +8,5 @@ in: asset:pkg/test/error_test_data/nullable_realm_value.dart:7:3 7 │ RealmValue? wrong; │ ^^^^^^^^^^^ RealmValue? is nullable ╵ -Change type to RealmValue +Change type to RealmValue. RealmValue can hold null, but must not be nullable itself. diff --git a/generator/test/error_test_data/set_non_empty_initializer.dart b/generator/test/error_test_data/set_non_empty_initializer.dart new file mode 100644 index 000000000..767bb4225 --- /dev/null +++ b/generator/test/error_test_data/set_non_empty_initializer.dart @@ -0,0 +1,9 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'set_non_empty_initializer.g.dart'; + +@RealmModel() +class _Bad { + late int x; + final setWithInitializer = {0}; +} diff --git a/generator/test/error_test_data/set_non_empty_initializer.expected b/generator/test/error_test_data/set_non_empty_initializer.expected new file mode 100644 index 000000000..648335f5d --- /dev/null +++ b/generator/test/error_test_data/set_non_empty_initializer.expected @@ -0,0 +1,13 @@ +Non-empty default values for sets are not supported. + +in: asset:pkg/test/error_test_data/set_non_empty_initializer.dart:8:30 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +8 │ final setWithInitializer = {0}; + │ ^^^ Remove the default value. + ╵ +Remove the default value for field setWithInitializer or change it to be an empty collection. + diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected index 1bb634c99..459592781 100644 --- a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected @@ -1,4 +1,4 @@ -Nullable realm objects are not allowed in sets +Realm objects in sets must be non-nullable in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart:10:8 ╷ diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected index 0f9d18fbc..2e9f837f5 100644 --- a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected @@ -7,7 +7,7 @@ in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue. │ ━━━━ in realm model for 'Bad' ... │ 10 │ late Set wrong1; - │ ^^^^^^^^^^^^^^^^ Set of nullable RealmValues is not supported + │ ^^^^^^^^^^^^^^^^ Nullable RealmValues are not supported ╵ -Did you mean to use Set instead? +Ensure the RealmValue type argument is non-nullable. RealmValue can hold null, but must not be nullable itself. diff --git a/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected b/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected index bff65897e..0cabfe0e4 100644 --- a/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected +++ b/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected @@ -1,4 +1,4 @@ -Default values for set are not supported. +Non-empty default values for sets are not supported. in: asset:pkg/test/error_test_data/unsupported_realm_set_with_default_values.dart:10:27 ╷ @@ -9,5 +9,5 @@ in: asset:pkg/test/error_test_data/unsupported_realm_set_with_default_values.dar 10 │ late Set wrong1 = {true, false}; │ ^^^^^^^^^^^^^ Remove the default value. ╵ -Remove the default value for field wrong1. +Remove the default value for field wrong1 or change it to be an empty collection. diff --git a/generator/test/good_test_data/all_types.dart b/generator/test/good_test_data/all_types.dart index fde9f6042..da84b8129 100644 --- a/generator/test/good_test_data/all_types.dart +++ b/generator/test/good_test_data/all_types.dart @@ -29,9 +29,9 @@ class _Bar { late Uuid uuid; @Ignored() var theMeaningOfEverything = 42; - var list = [0]; // list of ints with default value - // late Set set; // not supported yet - // late map = {}; // not supported yet + late List list; + late Set set; + late Map map; @Indexed() String? anOptionalString; diff --git a/generator/test/good_test_data/all_types.expected b/generator/test/good_test_data/all_types.expected index f6f114fa8..0ea68f69a 100644 --- a/generator/test/good_test_data/all_types.expected +++ b/generator/test/good_test_data/all_types.expected @@ -68,6 +68,8 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { RealmValue any = const RealmValue.nullValue(), Iterable list = const [], Iterable manyAny = const [], + Set set = const {}, + Map map = const {}, }) { if (!_defaultsSet) { _defaultsSet = RealmObjectBase.setDefaults({ @@ -89,6 +91,9 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { RealmObjectBase.set(this, 'decimal', decimal); RealmObjectBase.set>(this, 'list', RealmList(list)); RealmObjectBase.set>(this, 'manyAny', RealmList(manyAny)); + RealmObjectBase.set>(this, 'set', RealmSet(set)); + RealmObjectBase.set>( + this, 'map', RealmMap(map)); } Bar._(); @@ -148,6 +153,19 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { @override set list(covariant RealmList value) => throw RealmUnsupportedSetError(); + @override + RealmSet get set => + RealmObjectBase.get(this, 'set') as RealmSet; + @override + set set(covariant RealmSet value) => throw RealmUnsupportedSetError(); + + @override + RealmMap get map => + RealmObjectBase.get(this, 'map') as RealmMap; + @override + set map(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + @override String? get anOptionalString => RealmObjectBase.get(this, 'anOptionalString') as String?; @@ -212,6 +230,10 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject { SchemaProperty('uuid', RealmPropertyType.uuid, indexType: RealmIndexType.regular), SchemaProperty('list', RealmPropertyType.int, collectionType: RealmCollectionType.list), + SchemaProperty('set', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('map', RealmPropertyType.int, + collectionType: RealmCollectionType.map), SchemaProperty('anOptionalString', RealmPropertyType.string, optional: true, indexType: RealmIndexType.regular), SchemaProperty('any', RealmPropertyType.mixed, optional: true), diff --git a/generator/test/good_test_data/list_initialization.dart b/generator/test/good_test_data/list_initialization.dart index 66e6e8eb0..663825a27 100644 --- a/generator/test/good_test_data/list_initialization.dart +++ b/generator/test/good_test_data/list_initialization.dart @@ -5,4 +5,16 @@ import 'package:realm_common/realm_common.dart'; @RealmModel() class _Person { late List<_Person> children; + + final List initList = []; + final initListWithType = []; + final List initListConst = const []; + + final Set initSet = {}; + final initSetWithType = {}; + final Set initSetConst = const {}; + + final Map initMap = {}; + final initMapWithType = {}; + final Map initMapConst = const {}; } diff --git a/generator/test/good_test_data/list_initialization.expected b/generator/test/good_test_data/list_initialization.expected index 331621f54..d74f0890e 100644 --- a/generator/test/good_test_data/list_initialization.expected +++ b/generator/test/good_test_data/list_initialization.expected @@ -6,9 +6,35 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { Person({ Iterable children = const [], + Iterable initList = const [], + Iterable initListWithType = const [], + Iterable initListConst = const [], + Set initSet = const {}, + Set initSetWithType = const {}, + Set initSetConst = const {}, + Map initMap = const {}, + Map initMapWithType = const {}, + Map initMapConst = const {}, }) { RealmObjectBase.set>( this, 'children', RealmList(children)); + RealmObjectBase.set>( + this, 'initList', RealmList(initList)); + RealmObjectBase.set>( + this, 'initListWithType', RealmList(initListWithType)); + RealmObjectBase.set>( + this, 'initListConst', RealmList(initListConst)); + RealmObjectBase.set>(this, 'initSet', RealmSet(initSet)); + RealmObjectBase.set>( + this, 'initSetWithType', RealmSet(initSetWithType)); + RealmObjectBase.set>( + this, 'initSetConst', RealmSet(initSetConst)); + RealmObjectBase.set>( + this, 'initMap', RealmMap(initMap)); + RealmObjectBase.set>( + this, 'initMapWithType', RealmMap(initMapWithType)); + RealmObjectBase.set>( + this, 'initMapConst', RealmMap(initMapConst)); } Person._(); @@ -20,6 +46,70 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { set children(covariant RealmList value) => throw RealmUnsupportedSetError(); + @override + RealmList get initList => + RealmObjectBase.get(this, 'initList') as RealmList; + @override + set initList(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmList get initListWithType => + RealmObjectBase.get(this, 'initListWithType') as RealmList; + @override + set initListWithType(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmList get initListConst => + RealmObjectBase.get(this, 'initListConst') as RealmList; + @override + set initListConst(covariant RealmList value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSet => + RealmObjectBase.get(this, 'initSet') as RealmSet; + @override + set initSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSetWithType => + RealmObjectBase.get(this, 'initSetWithType') as RealmSet; + @override + set initSetWithType(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get initSetConst => + RealmObjectBase.get(this, 'initSetConst') as RealmSet; + @override + set initSetConst(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMap => + RealmObjectBase.get(this, 'initMap') as RealmMap; + @override + set initMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMapWithType => + RealmObjectBase.get(this, 'initMapWithType') + as RealmMap; + @override + set initMapWithType(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get initMapConst => + RealmObjectBase.get(this, 'initMapConst') as RealmMap; + @override + set initMapConst(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -34,7 +124,24 @@ class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { return const SchemaObject(ObjectType.realmObject, Person, 'Person', [ SchemaProperty('children', RealmPropertyType.object, linkTarget: 'Person', collectionType: RealmCollectionType.list), + SchemaProperty('initList', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initListWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initListConst', RealmPropertyType.int, + collectionType: RealmCollectionType.list), + SchemaProperty('initSet', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initSetWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initSetConst', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('initMap', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('initMapWithType', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('initMapConst', RealmPropertyType.int, + collectionType: RealmCollectionType.map), ]); } } - diff --git a/generator/test/good_test_data/map.dart b/generator/test/good_test_data/map.dart new file mode 100644 index 000000000..3d05b7c54 --- /dev/null +++ b/generator/test/good_test_data/map.dart @@ -0,0 +1,25 @@ +import 'dart:typed_data'; + +import 'package:realm_common/realm_common.dart'; + +//part 'map.g.dart'; + +@RealmModel() +class _LotsOfMaps { + late Map persons; + late Map bools; + late Map dateTimes; + late Map decimals; + late Map doubles; + late Map ints; + late Map objectIds; + late Map any; + late Map strings; + late Map binary; + late Map uuids; +} + +@RealmModel() +class _Person { + late String name; +} diff --git a/generator/test/good_test_data/map.expected b/generator/test/good_test_data/map.expected new file mode 100644 index 000000000..874fb0596 --- /dev/null +++ b/generator/test/good_test_data/map.expected @@ -0,0 +1,197 @@ +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class LotsOfMaps extends _LotsOfMaps + with RealmEntity, RealmObjectBase, RealmObject { + LotsOfMaps({ + Map persons = const {}, + Map bools = const {}, + Map dateTimes = const {}, + Map decimals = const {}, + Map doubles = const {}, + Map ints = const {}, + Map objectIds = const {}, + Map any = const {}, + Map strings = const {}, + Map binary = const {}, + Map uuids = const {}, + }) { + RealmObjectBase.set>( + this, 'persons', RealmMap(persons)); + RealmObjectBase.set>( + this, 'bools', RealmMap(bools)); + RealmObjectBase.set>( + this, 'dateTimes', RealmMap(dateTimes)); + RealmObjectBase.set>( + this, 'decimals', RealmMap(decimals)); + RealmObjectBase.set>( + this, 'doubles', RealmMap(doubles)); + RealmObjectBase.set>( + this, 'ints', RealmMap(ints)); + RealmObjectBase.set>( + this, 'objectIds', RealmMap(objectIds)); + RealmObjectBase.set>( + this, 'any', RealmMap(any)); + RealmObjectBase.set>( + this, 'strings', RealmMap(strings)); + RealmObjectBase.set>( + this, 'binary', RealmMap(binary)); + RealmObjectBase.set>( + this, 'uuids', RealmMap(uuids)); + } + + LotsOfMaps._(); + + @override + RealmMap get persons => + RealmObjectBase.get(this, 'persons') as RealmMap; + @override + set persons(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get bools => + RealmObjectBase.get(this, 'bools') as RealmMap; + @override + set bools(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get dateTimes => + RealmObjectBase.get(this, 'dateTimes') + as RealmMap; + @override + set dateTimes(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get decimals => + RealmObjectBase.get(this, 'decimals') + as RealmMap; + @override + set decimals(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get doubles => + RealmObjectBase.get(this, 'doubles') as RealmMap; + @override + set doubles(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get ints => + RealmObjectBase.get(this, 'ints') as RealmMap; + @override + set ints(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectIds => + RealmObjectBase.get(this, 'objectIds') + as RealmMap; + @override + set objectIds(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get any => + RealmObjectBase.get(this, 'any') + as RealmMap; + @override + set any(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get strings => + RealmObjectBase.get(this, 'strings') as RealmMap; + @override + set strings(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get binary => + RealmObjectBase.get(this, 'binary') + as RealmMap; + @override + set binary(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get uuids => + RealmObjectBase.get(this, 'uuids') as RealmMap; + @override + set uuids(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + LotsOfMaps freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(LotsOfMaps._); + return const SchemaObject( + ObjectType.realmObject, LotsOfMaps, 'LotsOfMaps', [ + SchemaProperty('persons', RealmPropertyType.object, + optional: true, linkTarget: 'Person', collectionType: RealmCollectionType.map), + SchemaProperty('bools', RealmPropertyType.bool, + collectionType: RealmCollectionType.map), + SchemaProperty('dateTimes', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.map), + SchemaProperty('decimals', RealmPropertyType.decimal128, + collectionType: RealmCollectionType.map), + SchemaProperty('doubles', RealmPropertyType.double, + collectionType: RealmCollectionType.map), + SchemaProperty('ints', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('objectIds', RealmPropertyType.objectid, + collectionType: RealmCollectionType.map), + SchemaProperty('any', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('strings', RealmPropertyType.string, + collectionType: RealmCollectionType.map), + SchemaProperty('binary', RealmPropertyType.binary, + collectionType: RealmCollectionType.map), + SchemaProperty('uuids', RealmPropertyType.uuid, + collectionType: RealmCollectionType.map), + ]); + } +} + +class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { + Person( + String name, + ) { + RealmObjectBase.set(this, 'name', name); + } + + Person._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Person freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Person._); + return const SchemaObject(ObjectType.realmObject, Person, 'Person', [ + SchemaProperty('name', RealmPropertyType.string), + ]); + } +} diff --git a/lib/src/collections.dart b/lib/src/collections.dart index 0226605c6..999806734 100644 --- a/lib/src/collections.dart +++ b/lib/src/collections.dart @@ -48,6 +48,15 @@ class CollectionChanges { const CollectionChanges(this.deletions, this.insertions, this.modifications, this.modificationsAfter, this.moves, this.isCleared); } +/// @nodoc +class MapChanges { + final List deletions; + final List insertions; + final List modifications; + + const MapChanges(this.deletions, this.insertions, this.modifications); +} + /// Describes the changes in a Realm collection since the last time the notification callback was invoked. class RealmCollectionChanges implements Finalizable { final RealmCollectionChangesHandle _handle; diff --git a/lib/src/list.dart b/lib/src/list.dart index 7e1e28e88..bf7533d8a 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -118,9 +118,7 @@ class ManagedRealmList with RealmEntity, ListMixin impleme late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); } else { targetMetadata = _metadata!; type = T; @@ -228,16 +226,14 @@ class UnmanagedRealmList extends collection.DelegatingList Stream> get changes => throw RealmStateError("Unmanaged lists don't support changes"); } -// The query operations on lists, as well as the ability to subscribe for notifications, -// only work for list of objects (core restriction), so we add these as an extension methods -// to allow the compiler to prevent misuse. +// The query operations on lists, only work for list of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmListOfObject on RealmList { /// Filters the list and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). /// /// Only works for lists of [RealmObject]s or [EmbeddedObject]s. /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { final handle = realmCore.queryList(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); diff --git a/lib/src/map.dart b/lib/src/map.dart new file mode 100644 index 000000000..6f79591d9 --- /dev/null +++ b/lib/src/map.dart @@ -0,0 +1,304 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:collection'; + +import 'package:collection/collection.dart' as collection; + +import 'dart:ffi'; + +import 'collections.dart'; +import 'native/realm_core.dart'; +import 'realm_object.dart'; +import 'realm_class.dart'; +import 'results.dart'; + +/// RealmMap is a collection that contains key-value pairs of . +abstract class RealmMap with RealmEntity implements MapBase, Finalizable { + /// Gets a value indicating whether this collection is still valid to use. + /// + /// Indicates whether the [Realm] instance hasn't been closed, + /// if it represents a to-many relationship + /// and it's parent object hasn't been deleted. + bool get isValid; + + /// Creates an unmanaged RealmMap from [items] + factory RealmMap(Map items) => UnmanagedRealmMap(items); + + /// Creates a frozen snapshot of this `RealmMap`. + RealmMap freeze(); + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes; +} + +class UnmanagedRealmMap extends collection.DelegatingMap with RealmEntity implements RealmMap { + UnmanagedRealmMap([Map? items]) : super(Map.from(items ?? {})); + + @override + bool get isValid => true; + + @override + RealmMap freeze() => throw RealmStateError("Unmanaged maps can't be frozen"); + + @override + Stream> get changes => throw RealmStateError("Unmanaged maps don't support changes"); +} + +class ManagedRealmMap with RealmEntity, MapMixin implements RealmMap { + final RealmMapHandle _handle; + + late final RealmObjectMetadata? _metadata; + + ManagedRealmMap._(this._handle, Realm realm, this._metadata) { + setRealm(realm); + } + + @override + int get length => realmCore.mapGetSize(handle); + + @override + T? remove(Object? key) { + if (key is! String) { + return null; + } + + final value = this[key]; + if (realmCore.mapRemoveKey(handle, key)) { + return value; + } + + return null; + } + + @override + T? operator [](Object? key) { + if (key is! String) { + return null; + } + + try { + var value = realmCore.mapGetElement(this, key); + if (value is RealmObjectHandle) { + late RealmObjectMetadata targetMetadata; + late Type type; + if (T == RealmValue) { + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + } else { + targetMetadata = _metadata!; + type = T; + } + value = realm.createObject(type, value, targetMetadata); + } + + if (T == RealmValue) { + // Maps must return `null` if attempting to access a non-existing key. Without this check, + // we'd return RealmValue(null) which is different. + if (value == null && !containsKey(key)) { + return null; + } + + value = RealmValue.from(value); + } + + return value as T?; + } on Exception catch (e) { + throw RealmException("Error getting value at key $key. Error: $e"); + } + } + + @override + void operator []=(String key, Object? value) => RealmMapInternal.setValue(handle, realm, key, value); + + /// Removes all objects from this map; the length of the map becomes zero. + /// The objects are not deleted from the realm, but are no longer referenced from this map. + @override + void clear() => realmCore.mapClear(handle); + + @override + bool get isValid => realmCore.mapIsValid(this); + + @override + RealmMap freeze() { + if (isFrozen) { + return this; + } + + final frozenRealm = realm.freeze(); + return frozenRealm.resolveMap(this)!; + } + + @override + Stream> get changes { + if (isFrozen) { + throw RealmStateError('Map is frozen and cannot emit changes'); + } + final controller = MapNotificationsController(asManaged()); + return controller.createStream(); + } + + @override + Iterable get keys => RealmResultsInternal.create(realmCore.mapGetKeys(this), realm, null); + + @override + Iterable get values => RealmResultsInternal.create(realmCore.mapGetValues(this), realm, metadata); + + @override + bool containsKey(Object? key) => key is String && realmCore.mapContainsKey(this, key); + + @override + bool containsValue(Object? value) { + if (value is! T?) { + return false; + } + + if (value is RealmObjectBase && !value.isManaged) { + return false; + } + + if (value is RealmValue && value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) { + return false; + } + + return realmCore.mapContainsValue(this, value); + } +} + +/// Describes the changes in a Realm map collection since the last time the notification callback was invoked. +class RealmMapChanges { + /// The collection being monitored for changes. + final RealmMap map; + + final RealmMapChangesHandle _handle; + MapChanges? _values; + + RealmMapChanges._(this._handle, this.map); + + MapChanges get _changes => _values ??= realmCore.getMapChanges(_handle); + + /// The keys of the map which have been removed. + List get deleted => _changes.deletions; + + /// The keys of the map which were added. + List get inserted => _changes.insertions; + + /// The keys of the map, whose corresponding values were modified in this version. + List get modified => _changes.modifications; +} + +// The query operations on maps only work for maps of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. +extension RealmMapOfObject on RealmMap { + /// Filters the map values and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). + /// + /// Only works for maps of [RealmObject]s or [EmbeddedObject]s. + /// + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. + RealmResults query(String query, [List arguments = const []]) { + final handle = realmCore.queryMap(asManaged(), query, arguments); + return RealmResultsInternal.create(handle, realm, metadata); + } +} + +/// @nodoc +extension RealmMapInternal on RealmMap { + @pragma('vm:never-inline') + void keepAlive() { + final self = this; + if (self is ManagedRealmMap) { + realm.keepAlive(); + self._handle.keepAlive(); + } + } + + ManagedRealmMap asManaged() => this is ManagedRealmMap ? this as ManagedRealmMap : throw RealmStateError('$this is not managed'); + + RealmMapHandle get handle { + final result = asManaged()._handle; + if (result.released) { + throw RealmClosedError('Cannot access a map that belongs to a closed Realm'); + } + + return result; + } + + RealmObjectMetadata? get metadata => asManaged()._metadata; + + static RealmMap create(RealmMapHandle handle, Realm realm, RealmObjectMetadata? metadata) => + ManagedRealmMap._(handle, realm, metadata); + + static void setValue(RealmMapHandle handle, Realm realm, String key, Object? value, {bool update = false}) { + try { + if (value is EmbeddedObject) { + if (value.isManaged) { + throw RealmError("Can't add to map an embedded object that is already managed"); + } + + final objHandle = realmCore.mapInsertEmbeddedObject(realm, handle, key); + realm.manageEmbedded(objHandle, value); + return; + } + + if (value is RealmValue) { + value = value.value; + } + + if (value is RealmObject && !value.isManaged) { + realm.add(value, update: update); + } + + realmCore.mapInsertValue(handle, key, value); + } on Exception catch (e) { + throw RealmException("Error setting value at key $key. Error: $e"); + } + } +} + +/// @nodoc +class MapNotificationsController extends NotificationsController { + final ManagedRealmMap map; + late final StreamController> streamController; + + MapNotificationsController(this.map); + + @override + RealmNotificationTokenHandle subscribe() { + return realmCore.subscribeMapNotifications(map, this); + } + + Stream> createStream() { + streamController = StreamController>(onListen: start, onCancel: stop); + return streamController.stream; + } + + @override + void onChanges(HandleBase changesHandle) { + if (changesHandle is! RealmMapChangesHandle) { + throw RealmError("Invalid changes handle. RealmMapChangesHandle expected"); + } + + final changes = RealmMapChanges._(changesHandle, map); + streamController.add(changes); + } + + @override + void onError(RealmError error) { + streamController.addError(error); + } +} diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 134d1db08..242e11df8 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -39,6 +39,7 @@ import '../configuration.dart'; import '../credentials.dart'; import '../init.dart'; import '../list.dart'; +import '../map.dart'; import '../migration.dart'; import '../realm_class.dart'; import '../realm_object.dart'; @@ -1201,6 +1202,29 @@ class _RealmCore { }); } + RealmResultsHandle queryMap(ManagedRealmMap target, String query, List args) { + return using((arena) { + final length = args.length; + final argsPointer = arena(length); + for (var i = 0; i < length; ++i) { + _intoRealmQueryArg(args[i], argsPointer.elementAt(i), arena); + } + + final results = mapGetValues(target); + final queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse_for_results( + results._pointer, + query.toCharPtr(arena), + length, + argsPointer, + ), + ), + target.realm.handle); + return _queryFindAll(queryHandle); + }); + } + RealmResultsHandle resultsFromList(RealmList list) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_to_results(list.handle._pointer)); return RealmResultsHandle._(pointer, list.realm.handle); @@ -1316,6 +1340,41 @@ class _RealmCore { }); } + MapChanges getMapChanges(RealmMapChangesHandle changes) { + return using((arena) { + final out_num_deletions = arena(); + final out_num_insertions = arena(); + final out_num_modifications = arena(); + _realmLib.realm_dictionary_get_changes( + changes._pointer, + out_num_deletions, + out_num_insertions, + out_num_modifications, + ); + + final deletionsCount = out_num_deletions != nullptr ? out_num_deletions.value : 0; + final insertionCount = out_num_insertions != nullptr ? out_num_insertions.value : 0; + final modificationCount = out_num_modifications != nullptr ? out_num_modifications.value : 0; + + final out_deletion_indexes = arena(deletionsCount); + final out_insertion_indexes = arena(insertionCount); + final out_modification_indexes = arena(modificationCount); + + _realmLib.realm_dictionary_get_changed_keys( + changes._pointer, + out_deletion_indexes, + out_num_deletions, + out_insertion_indexes, + out_num_insertions, + out_modification_indexes, + out_num_modifications, + ); + + return MapChanges(out_deletion_indexes.toStringList(deletionsCount), out_insertion_indexes.toStringList(insertionCount), + out_modification_indexes.toStringList(modificationCount)); + }); + } + _RealmLinkHandle _getObjectAsLink(RealmObjectBase object) { final realmLink = _realmLib.realm_object_as_link(object.handle._pointer); return _RealmLinkHandle._(realmLink); @@ -1488,6 +1547,98 @@ class _RealmCore { return RealmNotificationTokenHandle._(pointer, realmSet.realm.handle); } + int mapGetSize(RealmMapHandle handle) { + return using((Arena arena) { + final size = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_size(handle._pointer, size)); + return size.value; + }); + } + + bool mapRemoveKey(RealmMapHandle handle, String key) { + return using((Arena arena) { + final keyValue = _toRealmValue(key, arena); + final out_erased = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_erase(handle._pointer, keyValue.ref, out_erased)); + return out_erased.value; + }); + } + + Object? mapGetElement(RealmMap map, String key) { + return using((Arena arena) { + final realm_value = arena(); + final key_value = _toRealmValue(key, arena); + 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 null; + }); + } + + bool mapIsValid(RealmMap map) { + return _realmLib.realm_dictionary_is_valid(map.handle._pointer); + } + + void mapClear(RealmMapHandle mapHandle) { + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_clear(mapHandle._pointer)); + } + + RealmResultsHandle mapGetKeys(ManagedRealmMap map) { + return using((Arena arena) { + final out_size = arena(); + final out_keys = arena>(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_get_keys(map.handle._pointer, out_size, out_keys)); + return RealmResultsHandle._(out_keys.value, map.realm.handle); + }); + } + + RealmResultsHandle mapGetValues(ManagedRealmMap map) { + final result = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_to_results(map.handle._pointer)); + return RealmResultsHandle._(result, map.realm.handle); + } + + bool mapContainsKey(ManagedRealmMap map, String key) { + return using((Arena arena) { + final key_value = _toRealmValue(key, arena); + final out_found = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_key(map.handle._pointer, key_value.ref, out_found)); + return out_found.value; + }); + } + + bool mapContainsValue(ManagedRealmMap map, Object? value) { + return using((Arena arena) { + final key_value = _toRealmValue(value, arena); + final out_index = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_contains_value(map.handle._pointer, key_value.ref, out_index)); + return out_index.value > -1; + }); + } + + RealmObjectHandle mapInsertEmbeddedObject(Realm realm, RealmMapHandle handle, String key) { + return using((Arena arena) { + final realm_value = _toRealmValue(key, arena); + final ptr = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_insert_embedded(handle._pointer, realm_value.ref)); + return RealmObjectHandle._(ptr, realm.handle); + }); + } + + 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)); + }); + } + + RealmMapHandle getMapProperty(RealmObjectBase object, int propertyKey) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_dictionary(object.handle._pointer, propertyKey)); + return RealmMapHandle._(pointer, object.realm.handle); + } + bool _equals(HandleBase first, HandleBase second) { return _realmLib.realm_equals(first._pointer.cast(), second._pointer.cast()); } @@ -1569,6 +1720,31 @@ class _RealmCore { } } + static void map_change_callback(Pointer userdata, Pointer data) { + NotificationsController? controller = userdata.toObject(); + if (controller == null) { + return; + } + + if (data == nullptr) { + controller.onError(RealmError("Invalid notifications data received")); + return; + } + + try { + final clonedData = _realmLib.realm_clone(data.cast()); + if (clonedData == nullptr) { + controller.onError(RealmError("Error while cloning notifications data")); + return; + } + + final changesHandle = RealmMapChangesHandle._(clonedData.cast()); + controller.onChanges(changesHandle); + } catch (e) { + controller.onError(RealmError("Error handling change notifications. Error: $e")); + } + } + RealmNotificationTokenHandle subscribeResultsNotifications(RealmResults results, NotificationsController controller) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_add_notification_callback( results.handle._pointer, @@ -1605,6 +1781,18 @@ class _RealmCore { return RealmNotificationTokenHandle._(pointer, object.realm.handle); } + RealmNotificationTokenHandle subscribeMapNotifications(RealmMap map, NotificationsController controller) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_dictionary_add_notification_callback( + map.handle._pointer, + controller.toWeakHandle(), + nullptr, + nullptr, + Pointer.fromFunction(map_change_callback), + )); + + return RealmNotificationTokenHandle._(pointer, map.realm.handle); + } + bool getObjectChangesIsDeleted(RealmObjectChangesHandle handle) { return _realmLib.realm_object_changes_is_deleted(handle._pointer); } @@ -2512,6 +2700,14 @@ class _RealmCore { }); } + RealmMapHandle? resolveMap(ManagedRealmMap map, Realm frozenRealm) { + return using((Arena arena) { + final resultPtr = arena>(); + _realmLib.invokeGetBool(() => _realmLib.realm_dictionary_resolve_in(map.handle._pointer, frozenRealm.handle._pointer, resultPtr)); + return resultPtr == nullptr ? null : RealmMapHandle._(resultPtr.value, frozenRealm.handle); + }); + } + static void _app_api_key_completion_callback(Pointer userdata, Pointer apiKey, Pointer error) { final Completer? completer = userdata.toObject(isPersistent: true); if (completer == null) { @@ -2880,6 +3076,10 @@ class RealmSetHandle extends RootedHandleBase { RealmSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); } +class RealmMapHandle extends RootedHandleBase { + RealmMapHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); // TODO: check size +} + class _RealmQueryHandle extends RootedHandleBase { _RealmQueryHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 256); } @@ -2896,6 +3096,10 @@ class RealmCollectionChangesHandle extends HandleBase RealmCollectionChangesHandle._(Pointer pointer) : super(pointer, 256); } +class RealmMapChangesHandle extends HandleBase { + RealmMapChangesHandle._(Pointer pointer) : super(pointer, 256); +} + class RealmObjectChangesHandle extends HandleBase { RealmObjectChangesHandle._(Pointer pointer) : super(pointer, 256); } @@ -3175,6 +3379,18 @@ extension on Pointer { } } +extension on Pointer { + List toStringList(int count) { + final result = List.filled(count, ''); + for (var i = 0; i < count; i++) { + final str_value = elementAt(i).ref.values.string; + result[i] = str_value.data.cast().toRealmDartString(length: str_value.size)!; + } + + return result; + } +} + extension on Pointer { T? toObject({bool isPersistent = false}) { assert(this != nullptr, "Pointer is null"); diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 1845cd5bc..fc7ea30f9 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -34,6 +34,7 @@ import 'scheduler.dart'; import 'session.dart'; import 'subscription.dart'; import 'set.dart'; +import 'map.dart'; export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, TimeoutCancellationToken, CancelledException; export 'package:realm_common/realm_common.dart' @@ -108,6 +109,7 @@ export "configuration.dart" export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider; export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExtension; export 'set.dart' show RealmSet, RealmSetChanges, RealmSetOfObject; +export 'map.dart' show RealmMap, RealmMapChanges, RealmMapOfObject; export 'migration.dart' show Migration; export 'realm_object.dart' show @@ -737,6 +739,10 @@ extension RealmInternal on Realm { return RealmSetInternal.create(handle, this, metadata); } + RealmMap createMap(RealmMapHandle handle, RealmObjectMetadata? metadata) { + return RealmMapInternal.create(handle, this, metadata); + } + List getPropertyNames(Type type, List propertyKeys) { final metadata = _metadata.getByType(type); final result = []; @@ -807,6 +813,15 @@ extension RealmInternal on Realm { return createSet(handle, set.metadata); } + RealmMap? resolveMap(ManagedRealmMap map) { + final handle = realmCore.resolveMap(map, this); + if (handle == null) { + return null; + } + + return createMap(handle, map.metadata); + } + static MigrationRealm getMigrationRealm(Realm realm) => MigrationRealm._(realm); bool get isInMigration => _isInMigration; @@ -949,11 +964,11 @@ class RealmMetadata { RealmObjectMetadata? getByClassKeyIfExists(int key) => _classKeyMap[key]; - Tuple getByClassKey(int key) { + (Type type, RealmObjectMetadata meta) getByClassKey(int key) { final meta = _classKeyMap[key]; if (meta != null) { final type = _typeMap.entries.firstWhereOrNull((e) => e.value.classKey == key)?.key ?? RealmObjectBase; - return Tuple(type, meta); + return (type, meta); } throw RealmError("Object with classKey $key not found in the current Realm's schema."); } diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index 670ad89ec..6fd91b088 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -28,6 +28,7 @@ import 'native/realm_core.dart'; import 'realm_class.dart'; import 'results.dart'; import 'set.dart'; +import 'map.dart'; typedef DartDynamic = dynamic; @@ -145,73 +146,95 @@ class RealmCoreAccessor implements RealmAccessor { Object? get(RealmObjectBase object, String name) { try { final propertyMeta = metadata[name]; - if (propertyMeta.collectionType == RealmCollectionType.list) { - if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { - final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); - final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; - final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); - return RealmResultsInternal.create(handle, object.realm, sourceMeta); - } - - final handle = realmCore.getListProperty(object, propertyMeta.key); - final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - if (propertyMeta.propertyType == RealmPropertyType.mixed) { - return object.realm.createList(handle, metadata); - } - - // listMetadata is not null when we have list of RealmObjects. If the API was - // called with a generic object arg - get we construct a list of - // RealmObjects since we don't know the type of the object. - if (listMetadata != null && _isTypeGenericObject()) { - switch (listMetadata.schema.baseType) { - case ObjectType.realmObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.embeddedObject: - return object.realm.createList(handle, listMetadata); - case ObjectType.asymmetricObject: - return object.realm.createList(handle, listMetadata); - default: - throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + switch (propertyMeta.collectionType) { + case RealmCollectionType.list: + if (propertyMeta.propertyType == RealmPropertyType.linkingObjects) { + final sourceMeta = object.realm.metadata.getByName(propertyMeta.objectType!); + final sourceProperty = sourceMeta[propertyMeta.linkOriginProperty!]; + final handle = realmCore.getBacklinks(object, sourceMeta.classKey, sourceProperty.key); + return RealmResultsInternal.create(handle, object.realm, sourceMeta); } - } - return object.realm.createList(handle, listMetadata); - } - if (propertyMeta.collectionType == RealmCollectionType.set) { - final handle = realmCore.getSetProperty(object, propertyMeta.key); - final setMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - return RealmSetInternal.create(handle, object.realm, setMetadata); - } + final handle = realmCore.getListProperty(object, propertyMeta.key); + final listMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); - var value = realmCore.getProperty(object, propertyMeta.key); - - if (value is RealmObjectHandle) { - final meta = object.realm.metadata; - final typeName = propertyMeta.objectType; + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + return object.realm.createList(handle, metadata); + } - late Type type; - late RealmObjectMetadata targetMetadata; + // listMetadata is not null when we have list of RealmObjects. If the API was + // called with a generic object arg - get we construct a list of + // RealmObjects since we don't know the type of the object. + if (listMetadata != null && _isTypeGenericObject()) { + switch (listMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.embeddedObject: + return object.realm.createList(handle, listMetadata); + case ObjectType.asymmetricObject: + return object.realm.createList(handle, listMetadata); + default: + throw RealmError('List of ${listMetadata.schema.baseType} is not supported yet'); + } + } + return object.realm.createList(handle, listMetadata); + case RealmCollectionType.set: + final handle = realmCore.getSetProperty(object, propertyMeta.key); + final setMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + return RealmSetInternal.create(handle, object.realm, setMetadata); + case RealmCollectionType.map: + final handle = realmCore.getMapProperty(object, propertyMeta.key); + final mapMetadata = propertyMeta.objectType == null ? null : object.realm.metadata.getByName(propertyMeta.objectType!); + + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + return object.realm.createMap(handle, metadata); + } - if (propertyMeta.propertyType == RealmPropertyType.mixed) { - final tuple = meta.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; - } else { - // If we have an object but the user called the API without providing a generic - // arg, we construct a RealmObject since we don't know the type of the object. - type = _isTypeGenericObject() ? RealmObjectBase : T; - targetMetadata = typeName != null ? meta.getByName(typeName) : meta.getByType(type); - } + // mapMetadata is not null when we have map of RealmObjects. If the API was + // called with a generic object arg - get we construct a map of + // RealmObjects since we don't know the type of the object. + if (mapMetadata != null && _isTypeGenericObject()) { + switch (mapMetadata.schema.baseType) { + case ObjectType.realmObject: + return object.realm.createMap(handle, mapMetadata); + case ObjectType.embeddedObject: + return object.realm.createMap(handle, mapMetadata); + case ObjectType.asymmetricObject: + return object.realm.createMap(handle, mapMetadata); + default: + throw RealmError('Map of ${mapMetadata.schema.baseType} is not supported yet'); + } + } + return object.realm.createMap(handle, mapMetadata); + default: + var value = realmCore.getProperty(object, propertyMeta.key); + + if (value is RealmObjectHandle) { + final meta = object.realm.metadata; + final typeName = propertyMeta.objectType; + + late Type type; + late RealmObjectMetadata targetMetadata; + + if (propertyMeta.propertyType == RealmPropertyType.mixed) { + (type, targetMetadata) = meta.getByClassKey(realmCore.getClassKey(value)); + } else { + // If we have an object but the user called the API without providing a generic + // arg, we construct a RealmObject since we don't know the type of the object. + type = _isTypeGenericObject() ? RealmObjectBase : T; + targetMetadata = typeName != null ? meta.getByName(typeName) : meta.getByType(type); + } + + value = object.realm.createObject(type, value, targetMetadata); + } - value = object.realm.createObject(type, value, targetMetadata); - } + if (T == RealmValue) { + value = RealmValue.from(value); + } - if (T == RealmValue) { - value = RealmValue.from(value); + return value; } - - return value; } on Exception catch (e) { throw RealmException("Error getting property ${metadata._realmObjectTypeName}.$name Error: $e"); } @@ -221,29 +244,20 @@ 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 RealmList) { + if (value is RealmList) { final handle = realmCore.getListProperty(object, propertyMeta.key); - if (update) realmCore.listClear(handle); - for (var i = 0; i < value.length; i++) { - RealmListInternal.setValue(handle, object.realm, i, value[i], update: update); + if (update) { + realmCore.listClear(handle); } - return; - } - if (value is EmbeddedObject) { - if (value.isManaged) { - throw RealmError("Can't set an embedded object that is already managed"); + for (var i = 0; i < value.length; i++) { + RealmListInternal.setValue(handle, object.realm, i, value[i], update: update); } - - final handle = realmCore.createEmbeddedObject(object, propertyMeta.key); - object.realm.manageEmbedded(handle, value, update: update); return; } - object.realm.addUnmanagedRealmObjectFromValue(value, update); - //TODO: set from ManagedRealmList is not supported yet - if (value is UnmanagedRealmSet) { + if (value is RealmSet) { final handle = realmCore.getSetProperty(object, propertyMeta.key); if (update) { realmCore.realmSetClear(handle); @@ -263,6 +277,30 @@ class RealmCoreAccessor implements RealmAccessor { return; } + if (value is RealmMap) { + final handle = realmCore.getMapProperty(object, propertyMeta.key); + if (update) { + realmCore.mapClear(handle); + } + + for (var kvp in value.entries) { + RealmMapInternal.setValue(handle, object.realm, kvp.key, kvp.value, update: update); + } + return; + } + + if (value is EmbeddedObject) { + if (value.isManaged) { + throw RealmError("Can't set an embedded object that is already managed"); + } + + final handle = realmCore.createEmbeddedObject(object, propertyMeta.key); + object.realm.manageEmbedded(handle, value, update: update); + return; + } + + object.realm.addUnmanagedRealmObjectFromValue(value, update); + if (propertyMeta.isPrimaryKey && !isInMigration) { final currentValue = realmCore.getProperty(object, propertyMeta.key); if (currentValue != value) { @@ -490,8 +528,8 @@ extension EmbeddedObjectExtension on EmbeddedObject { } final parent = realmCore.getEmbeddedParent(this); - final metadata = realm.metadata.getByClassKey(parent.item2); - return realm.createObject(metadata.item1, parent.item1, metadata.item2); + final (type, metadata) = realm.metadata.getByClassKey(parent.item2); + return realm.createObject(type, parent.item1, metadata); } } diff --git a/lib/src/results.dart b/lib/src/results.dart index 0c28f6eed..06db973e0 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -50,13 +50,30 @@ class RealmResults extends Iterable with RealmEntity imple /// Returns the element of type `T` at the specified [index]. @override T elementAt(int index) { - if (this is RealmResults) { - final handle = realmCore.resultsGetObjectAt(this, _skipOffset + index); - final accessor = RealmCoreAccessor(metadata, realm.isInMigration); - return RealmObjectInternal.create(T, realm, handle, accessor) as T; - } else { - return realmCore.resultsGetElementAt(this, _skipOffset + index) as T; + // TODO: this is identical to list[] - consider refactoring to combine them. + if (index < 0 || index >= length) { + throw RangeError.range(index, 0, length - 1); } + + var value = realmCore.resultsGetElementAt(this, _skipOffset + index); + + if (value is RealmObjectHandle) { + late RealmObjectMetadata targetMetadata; + late Type type; + if (T == RealmValue) { + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + } else { + targetMetadata = _metadata!; + type = T; + } + value = realm.createObject(type, value, targetMetadata); + } + + if (T == RealmValue) { + value = RealmValue.from(value); + } + + return value as T; } @pragma('vm:prefer-inline') diff --git a/lib/src/set.dart b/lib/src/set.dart index 4be7c6d17..3e6edf62f 100644 --- a/lib/src/set.dart +++ b/lib/src/set.dart @@ -166,9 +166,7 @@ class ManagedRealmSet with RealmEntity, SetMixin implement late RealmObjectMetadata targetMetadata; late Type type; if (T == RealmValue) { - final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value)); - type = tuple.item1; - targetMetadata = tuple.item2; + (type, targetMetadata) = realm.metadata.getByClassKey(realmCore.getClassKey(value)); } else { targetMetadata = _metadata!; // will be null for RealmValue, so defer until here type = T; @@ -367,16 +365,14 @@ class RealmSetNotificationsController extends NotificationsCo } } -// The query operations on sets, as well as the ability to subscribe for notifications, -// only work for sets of objects (core restriction), so we add these as an extension methods -// to allow the compiler to prevent misuse. +// The query operations on sets only work for sets of objects (core restriction), +// so we add these as an extension methods to allow the compiler to prevent misuse. extension RealmSetOfObject on RealmSet { /// Filters the set and returns a new [RealmResults] according to the provided [query] (with optional [arguments]). /// /// Only works for sets of [RealmObject]s or [EmbeddedObject]s. /// - /// The Realm Dart and Realm Flutter SDKs supports querying based on a language inspired by [NSPredicate](https://academy.realm.io/posts/nspredicate-cheatsheet/) - /// and [Predicate Programming Guide.](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html#//apple_ref/doc/uid/TP40001789) + /// For more details about the syntax of the Realm Query Language, refer to the documentation: https://www.mongodb.com/docs/realm/realm-query-language/. RealmResults query(String query, [List arguments = const []]) { final handle = realmCore.querySet(asManaged(), query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); diff --git a/test/backlinks_test.dart b/test/backlinks_test.dart index 4e77bbdaf..f4b8bf0e2 100644 --- a/test/backlinks_test.dart +++ b/test/backlinks_test.dart @@ -28,7 +28,7 @@ class _Source { String name = 'source'; @MapTo('et mål') // to throw a curve ball.. _Target? oneTarget; - List<_Target> manyTargets = []; + late List<_Target> manyTargets; } @RealmModel() diff --git a/test/geospatial_test.dart b/test/geospatial_test.dart index 9d207e7ec..290048347 100644 --- a/test/geospatial_test.dart +++ b/test/geospatial_test.dart @@ -30,7 +30,7 @@ part 'geospatial_test.g.dart'; @RealmModel(ObjectType.embeddedObject) class _Location { final String type = 'Point'; - final List coordinates = const [0, 0]; + late final List coordinates; double get lon => coordinates[0]; set lon(double value) => coordinates[0] = value; @@ -62,7 +62,7 @@ void createRestaurants(Realm realm) { @RealmModel() class _LocationList { - final locations = <_Location>[]; + late final List<_Location> locations; @override String toString() => '[${locations.join(', ')}]'; diff --git a/test/realm_map_test.dart b/test/realm_map_test.dart new file mode 100644 index 000000000..56ac0b458 --- /dev/null +++ b/test/realm_map_test.dart @@ -0,0 +1,966 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:test/test.dart' hide test, throws; +import '../lib/realm.dart'; + +import 'test.dart'; + +part 'realm_map_test.g.dart'; + +@RealmModel() +class _Car { + @PrimaryKey() + late String make; + late String? color; +} + +@RealmModel(ObjectType.embeddedObject) +class _EmbeddedValue { + late int intValue; +} + +@RealmModel() +class _TestRealmMaps { + @PrimaryKey() + late int key; + + late Map boolMap; + late Map intMap; + late Map stringMap; + late Map doubleMap; + late Map dateTimeMap; + late Map objectIdMap; + late Map uuidMap; + late Map binaryMap; + late Map decimalMap; + + late Map nullableBoolMap; + late Map nullableIntMap; + late Map nullableStringMap; + late Map nullableDoubleMap; + late Map nullableDateTimeMap; + late Map nullableObjectIdMap; + late Map nullableUuidMap; + late Map nullableBinaryMap; + late Map nullableDecimalMap; + + late Map objectsMap; + late Map embeddedMap; + + late Map mixedMap; +} + +class TestCaseData { + final T Function(T) _cloneFunc; + final bool Function(T?, T?) _equalityFunc; + + final T _sampleValue; + + final List<(String key, T value)> _initialValues; + + List<(String key, T value)> get initialValues => _initialValues.map((kvp) => (kvp.$1, _cloneFunc(kvp.$2))).toList(); + + T get sampleValue => _cloneFunc(_sampleValue); + + TestCaseData(this._sampleValue, {bool Function(T?, T?)? equalityFunc, List<(String key, T value)> initialValues = const [], T Function(T)? cloneFunc}) + : _equalityFunc = equalityFunc ?? ((a, b) => a == b), + _cloneFunc = cloneFunc ?? ((v) => v), + _initialValues = initialValues; + + void seed(Map target, {Iterable<(String key, T value)>? values}) { + _writeIfNecessary(target, () { + target.clear(); + for (var (key, value) in values ?? initialValues) { + target[key] = value; + } + }); + } + + void assertEquivalent(Map target) { + final reference = _getReferenceMap(); + _isEquivalent(target, reference); + } + + void assertContainsKey(Map target) { + for (final (key, _) in initialValues) { + expect(target.containsKey(key), true, reason: 'expected to find $key'); + } + + expect(target.containsKey(Uuid.v4().toString()), false); + } + + void assertKeys(Map target) { + expect(target.keys, unorderedEquals(initialValues.map((e) => e.$1))); + } + + void assertValues(Map target) { + expect(target.values.length, initialValues.length); + final actualValues = target.values; + for (final (_, value) in initialValues) { + expect(actualValues.where((element) => _equalityFunc(element, value)).length, greaterThanOrEqualTo(1)); // values may be duplicates + } + + // Test in the other direction in case we have duplicates + for (final value in actualValues) { + expect(initialValues.where((element) => _equalityFunc(element.$2, value)).length, greaterThanOrEqualTo(1)); // values may be duplicates + } + } + + void assertEntries(Map target) { + final reference = _getReferenceMap(); + for (final kvp in target.entries) { + expect(reference.containsKey(kvp.key), true); + expect(_equalityFunc(reference[kvp.key], target[kvp.key]), true); + + reference.remove(kvp.key); + } + + expect(reference, isEmpty); + } + + void assertAccessor(Map target) { + for (final (key, value) in initialValues) { + expect(_equalityFunc(target[key], value), true); + } + + expect(target[Uuid.v4().toString()], null); + } + + void assertSet(Map target) { + var expectedLength = target.length; + + if (target.isNotEmpty) { + final key = target.keys.first; + _writeIfNecessary(target, () { + target[key] = sampleValue; + }); + + expect(target.containsKey(key), true); + expect(_equalityFunc(target[key], sampleValue), true); + expect(target.length, expectedLength); + } + + final newKey = Uuid.v4().toString(); + _writeIfNecessary(target, () { + target[newKey] = sampleValue; + }); + + expectedLength++; + + expect(target.containsKey(newKey), true); + expect(_equalityFunc(target[newKey], sampleValue), true); + expect(target.length, expectedLength); + } + + void assertRemove(Map target) { + seed(target); + + var expectedLength = target.length; + + if (target.isNotEmpty) { + final kvp = target.entries.last; + final removedValue = _writeIfNecessary(target, () => target.remove(kvp.key)); + expectedLength--; + + expect(removedValue, kvp.value); + expect(target.containsKey(kvp.key), false); + expect(target.length, expectedLength); + } + + final newKey = Uuid.v4().toString(); + final removedValue = _writeIfNecessary(target, () => target.remove(newKey)); + + expect(removedValue, null); + expect(target.containsKey(newKey), false); + expect(target.length, expectedLength); + } + + (String key, T value) _getDifferentValue(Map collection, T valueToCompare) { + for (final kvp in collection.entries) { + if (!_areValuesEqual(kvp.value, valueToCompare)) { + return (kvp.key, kvp.value); + } + } + + throw StateError('Could not find a different value'); + } + + void _isEquivalent(Map actual, Map expected) { + expect(actual, hasLength(expected.length)); + for (final kvp in expected.entries) { + final actualEntry = actual.entries.firstWhereOrNull((element) => element.key == kvp.key); + expect(actualEntry, isNotNull, reason: 'expect actual to contain ${kvp.key}'); + final actualValue = actual[kvp.key]; + expect(_equalityFunc(actualValue, kvp.value), true, reason: 'expected $actualValue == ${kvp.value}'); + } + } + + bool _areValuesEqual(T first, T second) { + if (first == second) { + return true; + } + + if (first is Uint8List && second is Uint8List) { + return IterableEquality().equals(first, second); + } + + return false; + } + + U _writeIfNecessary(Map collection, U Function() writeAction) { + Transaction? transaction; + try { + if (collection is RealmMap && collection.isManaged) { + transaction = collection.realm.beginWrite(); + } + + final result = writeAction(); + + transaction?.commit(); + + return result; + } catch (e) { + transaction?.rollback(); + rethrow; + } + } + + Map _getReferenceMap() => {for (var v in initialValues) v.$1: v.$2}; + + @override + String toString() { + return _initialValues.map((kvp) => '${kvp.$1}-${kvp.$2}').join(', '); + } +} + +List> boolTestValues() => [ + TestCaseData(true), + TestCaseData(true, initialValues: [('a', true)]), + TestCaseData(false, initialValues: [('b', false)]), + TestCaseData(true, initialValues: [('a', false), ('b', true)]), + TestCaseData(false, initialValues: [('a', true), ('b', false), ('c', true)]), + ]; + +List> nullableBoolTestValues() => [ + TestCaseData(true), + TestCaseData(true, initialValues: [('a', true)]), + TestCaseData(true, initialValues: [('b', false)]), + TestCaseData(false, initialValues: [('c', null)]), + TestCaseData(true, initialValues: [('a', false), ('b', true)]), + TestCaseData(null, initialValues: [('a', true), ('b', false), ('c', null)]), + ]; + +List> intTestCases() => [ + TestCaseData(123456789), + TestCaseData(123456789, initialValues: [('123', 123)]), + TestCaseData(123456789, initialValues: [('123', -123)]), + TestCaseData(123456789, initialValues: [('a', 1), ('b', 1), ('c', 1)]), + TestCaseData(123456789, initialValues: [('a', 1), ('b', 2), ('c', 3)]), + TestCaseData(123456789, initialValues: [('a', -0x8000000000000000), ('z', 0x7FFFFFFFFFFFFFFF)]), + TestCaseData(123456789, initialValues: [('a', -0x8000000000000000), ('zero', 0), ('one', 1), ('z', 0x7FFFFFFFFFFFFFFF)]), + ]; + +List> nullableIntTestCases() => [ + TestCaseData(1234), + TestCaseData(null, initialValues: [('123', 123)]), + TestCaseData(1234, initialValues: [('123', -123)]), + TestCaseData(1234, initialValues: [('null', null)]), + TestCaseData(1234, initialValues: [('null1', null), ('null2', null), ('null3', null)]), + TestCaseData(null, initialValues: [('a', 1), ('b', null), ('c', 3)]), + TestCaseData(1234, initialValues: [('a', -0x8000000000000000), ('m', null), ('z', 0x7FFFFFFFFFFFFFFF)]), + TestCaseData(1234, initialValues: [('a', -0x8000000000000000), ('zero', 0), ('null', null), ('one', 1), ('z', 0x7FFFFFFFFFFFFFFF)]), + ]; + +List> stringTestValues() => [ + TestCaseData(''), + TestCaseData('', initialValues: [('123', 'abc')]), + TestCaseData('', initialValues: [('a', 'AbCdEfG'), ('b', 'HiJklMn'), ('c', 'OpQrStU')]), + TestCaseData('', initialValues: [('a', 'vwxyz'), ('b', ''), ('c', ' ')]), + TestCaseData('', initialValues: [('a', ''), ('z', 'aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz')]), + TestCaseData('', initialValues: [('a', ''), ('z', 'lorem ipsum'), ('zero', '-1234567890'), ('one', 'lololo')]), + ]; + +List> nullableStringTestValues() => [ + TestCaseData(null), + TestCaseData(null, initialValues: [('123', 'abc')]), + TestCaseData('', initialValues: [('null', null)]), + TestCaseData('', initialValues: [('null1', null), ('null2', null)]), + TestCaseData('', initialValues: [('a', 'AbCdEfG'), ('b', null), ('c', 'OpQrStU')]), + TestCaseData(null, initialValues: [('a', 'vwxyz'), ('b', null), ('c', ''), ('d', ' ')]), + TestCaseData('', initialValues: [('a', ''), ('m', null), ('z', 'aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz')]), + TestCaseData('', initialValues: [('a', ''), ('zero', 'lorem ipsum'), ('null', null), ('one', '-1234567890'), ('z', 'lololo')]), + ]; + +List> doubleTestValues() => [ + TestCaseData(789.123), + TestCaseData(789.123, initialValues: [('123', 123.123)]), + TestCaseData(789.123, initialValues: [('123', -123.456)]), + TestCaseData(789.123, initialValues: [('a', 1.1), ('b', 1.1), ('c', 1.1)]), + TestCaseData(789.123, initialValues: [('a', 1), ('b', 2.2), ('c', 3.3)]), + TestCaseData(789.123, + initialValues: [('a', 1), ('b', 2.2), ('c', 3.3), ('d', 4385948963486946854968945789458794538793438693486934869.238593285932859238952398)]), + TestCaseData(789.123, initialValues: [('a', -double.maxFinite), ('z', double.maxFinite)]), + TestCaseData(789.123, initialValues: [('a', -double.maxFinite), ('zero', 0.0), ('one', 1.1), ('z', double.maxFinite)]), + ]; + +List> nullableDoubleTestValues() => [ + TestCaseData(-123.789), + TestCaseData(-123.789, initialValues: [('123', 123.123)]), + TestCaseData(null, initialValues: [('123', -123.456)]), + TestCaseData(-123.789, initialValues: [('null', null)]), + TestCaseData(-123.789, initialValues: [('null1', null), ('null2', null)]), + TestCaseData(-123.789, initialValues: [('a', 1), ('b', null), ('c', 3.3)]), + TestCaseData(null, + initialValues: [('a', 1), ('b', null), ('c', 3.3), ('d', 4385948963486946854968945789458794538793438693486934869.238593285932859238952398)]), + TestCaseData(-123.789, initialValues: [('a', -double.maxFinite), ('m', null), ('z', double.maxFinite)]), + TestCaseData(-123.789, initialValues: [('a', -double.maxFinite), ('zero', 0), ('null', null), ('one', 1.1), ('z', double.maxFinite)]), + ]; + +List> decimal128TestValues() => [ + TestCaseData(Decimal128.parse('1.5')), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('123', Decimal128.parse('123.123'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('123', Decimal128.parse('-123.456'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('a', Decimal128.parse('1.1')), ('b', Decimal128.parse('1.1')), ('c', Decimal128.parse('1.1'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [('a', Decimal128.parse('1')), ('b', Decimal128.parse('2.2')), ('c', Decimal128.parse('3.3'))]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('1')), + ('b', Decimal128.parse('2.2')), + ('c', Decimal128.parse('3.3')), + ('d', Decimal128.parse('43859489538793438693486934869.238436346943634634634634634634634634634593285932859238952398')) + ]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('a1', Decimal128.parse('-79228162514264337593543950335')), + ('z', Decimal128.parse('79228162514264337593543950335')), + ('z1', Decimal128.parse('79228162514264337593543950335')) + ]), + TestCaseData(Decimal128.parse('1.5'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('zero', Decimal128.parse('0')), + ('one', Decimal128.parse('1.1')), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + ]; + +List> nullableDecimal128TestValues() => [ + TestCaseData(null), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('123', Decimal128.parse('123.123'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('123', Decimal128.parse('-123.456'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('null', null)]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [('a', Decimal128.parse('1')), ('b', null), ('c', Decimal128.parse('3.3'))]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('1')), + ('b', null), + ('c', Decimal128.parse('3.3')), + ('d', Decimal128.parse('43859489538793438693486934869.238436346943634634634634634634634634634593285932859238952398')) + ]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('a1', Decimal128.parse('-79228162514264337593543950335')), + ('m', null), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + TestCaseData(Decimal128.parse('-9.7'), initialValues: [ + ('a', Decimal128.parse('-79228162514264337593543950335')), + ('zero', Decimal128.parse('0')), + ('null', null), + ('one', Decimal128.parse('1.1')), + ('z', Decimal128.parse('79228162514264337593543950335')) + ]), + ]; + +DateTime date0 = DateTime(0).toUtc(); +DateTime date1 = DateTime(1999, 3, 4, 5, 30, 23).toUtc(); +DateTime date2 = DateTime(2030, 1, 3, 9, 25, 34).toUtc(); + +List> dateTimeTestValues() => [ + TestCaseData(DateTime.now().toUtc()), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date1), ('b', date1), ('c', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date0), ('b', date1), ('c', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('z', date2)]), + TestCaseData(DateTime.now().toUtc(), + initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('zero', date1), ('one', date2), ('z', date2)]), + ]; + +List> nullableDateTimeTestValues() => [ + TestCaseData(null), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date1)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('123', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('null', null)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date0), ('b', null), ('c', date2)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', date2), ('b', null), ('c', date1), ('d', date0)]), + TestCaseData(DateTime.now().toUtc(), initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('m', null), ('z', date2)]), + TestCaseData(DateTime.now().toUtc(), + initialValues: [('a', DateTime.fromMillisecondsSinceEpoch(0).toUtc()), ('zero', date1), ('null', null), ('one', date2), ('z', date2)]), + ]; + +ObjectId objectId0 = ObjectId.fromValues(987654321, 5, 1); +ObjectId objectId1 = ObjectId.fromValues(0, 0, 0); +ObjectId objectId2 = ObjectId.fromValues(987654321, 5, 2); +ObjectId objectId3 = ObjectId.fromValues(55555, 123, 1); + +List> objectIdTestValues() => [ + TestCaseData(ObjectId()), + TestCaseData(ObjectId(), initialValues: [('123', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('123', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId1), ('b', objectId1), ('c', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('b', objectId1), ('c', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('z', objectId3)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('zero', objectId1), ('one', objectId2), ('z', objectId3)]), + ]; + +List> nullableObjectIdTestValues() => [ + TestCaseData(ObjectId()), + TestCaseData(ObjectId(), initialValues: [('123', objectId1)]), + TestCaseData(ObjectId(), initialValues: [('123', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('null', null)]), + TestCaseData(ObjectId(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(null, initialValues: [('a', objectId0), ('b', null), ('c', objectId2)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId2), ('b', null), ('c', objectId1), ('d', objectId0)]), + TestCaseData(ObjectId(), initialValues: [('a', objectId0), ('m', null), ('z', objectId3)]), + TestCaseData(null, initialValues: [('a', objectId0), ('zero', objectId1), ('null', null), ('one', objectId2), ('z', objectId3)]), + ]; + +Uuid uuid0 = Uuid.fromString('48f11f3a-7609-471f-b7ab-81c20c723ed9'); +Uuid uuid1 = Uuid.fromString('957ba4de-3966-46f6-b19f-242996608a8b'); +Uuid uuid2 = Uuid.fromString('081924e2-8e62-4af1-bc9c-e1a7fc365d84'); +Uuid uuid3 = Uuid.fromString('0bef5993-7480-4862-abdc-160bb364d1f3'); + +List> uuidTestValues() => [ + TestCaseData(Uuid.v4()), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid1), ('b', uuid1), ('c', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('b', uuid1), ('c', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('z', uuid3)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('zero', uuid1), ('one', uuid2), ('z', uuid3)]), + ]; + +List> nullableUuidTestValues() => [ + TestCaseData(Uuid.v4()), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid1)]), + TestCaseData(Uuid.v4(), initialValues: [('123', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('null', null)]), + TestCaseData(Uuid.v4(), initialValues: [('null1', null), ('null2', null)]), + TestCaseData(null, initialValues: [('a', uuid0), ('b', null), ('c', uuid2)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid2), ('b', null), ('c', uuid1), ('d', uuid0)]), + TestCaseData(Uuid.v4(), initialValues: [('a', uuid0), ('m', null), ('z', uuid3)]), + TestCaseData(null, initialValues: [('a', uuid0), ('zero', uuid1), ('null', null), ('one', uuid2), ('z', uuid3)]), + ]; + +Uint8List byteArray0 = Uint8List.fromList([1, 2, 3]); +Uint8List byteArray1 = Uint8List.fromList([4, 5, 6]); +Uint8List byteArray2 = Uint8List.fromList([7, 8, 9]); + +List> byteArrayTestValues() => [ + TestCaseData(Uint8List.fromList([1, 2, 3]), equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray1), ('b', byteArray1), ('c', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray0), ('b', byteArray1), ('c', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', Uint8List.fromList([0])), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', byteArray0), + ('zero', byteArray1), + ('one', byteArray2), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + ]; + +List> nullableByteArrayTestValues() => [ + TestCaseData(null), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray1)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('123', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('null', null)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), initialValues: [('null1', null), ('null2', null)], equalityFunc: IterableEquality().equals), + TestCaseData(null, initialValues: [('a', byteArray0), ('b', null), ('c', byteArray2)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [('a', byteArray2), ('b', null), ('c', byteArray1), ('d', byteArray0)], equalityFunc: IterableEquality().equals), + TestCaseData(Uint8List.fromList([1, 2, 3]), + initialValues: [ + ('a', byteArray0), + ('m', null), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + TestCaseData(null, + initialValues: [ + ('a', byteArray0), + ('zero', byteArray1), + ('null', null), + ('one', byteArray2), + ('z', Uint8List.fromList([255])) + ], + equalityFunc: IterableEquality().equals), + ]; + +List> realmValueTestValues() => [ + TestCaseData(RealmValue.string('sampleValue'), initialValues: [ + ('nullKey', RealmValue.nullValue()), + ('intKey', RealmValue.int(10)), + ('boolKey', RealmValue.bool(true)), + ('stringKey', RealmValue.string('abc')), + ('dataKey', RealmValue.uint8List(Uint8List.fromList([0, 1, 2]))), + ('dateKey', RealmValue.dateTime(DateTime.fromMillisecondsSinceEpoch(1616137641000).toUtc())), + ('doubleKey', RealmValue.double(2.5)), + ('decimalKey', RealmValue.decimal128(Decimal128.fromDouble(5.0))), + ('objectIdKey', RealmValue.objectId(ObjectId.fromHexString('5f63e882536de46d71877979'))), + ('guidKey', RealmValue.from(Uuid.fromString('F2952191-A847-41C3-8362-497F92CB7D24'))), + ('objectKey', RealmValue.from(Car('Honda'))) + ]) + ]; + +List> _embeddedObjectTestValues() => [ + TestCaseData(null), + TestCaseData(null, + initialValues: [('123', EmbeddedValue(1))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('123', EmbeddedValue(1))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('null', null)], equalityFunc: (a, b) => a?.intValue == b?.intValue, cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('null1', null), ('null2', null)], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + TestCaseData(EmbeddedValue(999), + initialValues: [('a', EmbeddedValue(1)), ('null', null), ('z', EmbeddedValue(2))], + equalityFunc: (a, b) => a?.intValue == b?.intValue, + cloneFunc: (a) => a == null ? null : EmbeddedValue(a.intValue)), + ]; + +@isTest +void testUnmanaged(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T unmanaged: $testData', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + await runTestsCore(testData, map, expectManaged: false); + + expect(() => map.freeze(), throwsA(isA())); + }); +} + +@isTest +void testManaged(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T managed: $testData', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(testObject); + }); + + final managedMap = accessor(testObject); + expect(identical(map, managedMap), false); + + await runTestsCore(testData, managedMap, expectManaged: true); + + final frozen = managedMap.freeze(); + expect(frozen.isFrozen, true); + + final newKey = Uuid.v4().toString(); + realm.write(() { + managedMap[newKey] = testData.sampleValue; + }); + + expect(frozen.length, managedMap.length - 1); + expect(frozen[newKey], null); + expect(frozen.containsKey(newKey), false); + expect(() => frozen.changes, throwsA(isA())); + }); +} + +@isTest +testNotifications(RealmMap Function(TestRealmMaps) accessor, TestCaseData testData) { + test('$T notifications', () async { + final testObject = TestRealmMaps(0); + final map = accessor(testObject); + + testData.seed(map); + + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + realm.add(testObject); + }); + + // final managedMap = accessor(testObject); + // await runManagedNotificationTests(testData, managedMap); + }); + + test('$T key notifications', () async { + // TODO: for some reason, we don't appear to be getting key notifications + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + final map = realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + return accessor(testObject); + }); + + final keysResults = map.keys as RealmResults; + expectLater( + keysResults.changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map['b'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + }, skip: 'Key notifications are not working'); + + test('$T value notifications', () async { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + final map = realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + return accessor(testObject); + }); + + final valueResults = map.values as RealmResults; + expectLater( + valueResults.changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map['b'] = testData.sampleValue; + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + }); +} + +Future runTestsCore(TestCaseData testData, RealmMap map, {required bool expectManaged}) async { + expect(map.isManaged, expectManaged); + expect(map.isValid, true); + + testData.assertEquivalent(map); + testData.assertContainsKey(map); + testData.assertKeys(map); + testData.assertValues(map); + testData.assertEntries(map); + testData.assertAccessor(map); + testData.assertSet(map); + testData.assertRemove(map); +} + +Future runManagedNotificationTests(TestCaseData testData, RealmMap map) async { + final insertedKey = Uuid.v4().toString(); + final (keyToUpdate, _) = testData._getDifferentValue(map, testData.sampleValue); + + final changes = >[]; + final subscription = map.changes.listen((change) { + changes.add(change); + }); + + var expectedCallbacks = 0; + + Future> waitForChanges(({List inserted, List modified, List deleted}) expected) async { + expectedCallbacks++; + + map.realm.refresh(); + + await waitForCondition(() => changes.length == expectedCallbacks); + final result = changes[expectedCallbacks - 1]; + expect(result.inserted, expected.inserted); + expect(result.modified, expected.modified); + expect(result.deleted, expected.deleted); + + return result; + } + + // Initial callback + await waitForChanges((inserted: [], modified: [], deleted: [])); + + // Insert + map.realm.write(() { + map[insertedKey] = testData.sampleValue; + }); + + await waitForChanges((inserted: [insertedKey], modified: [], deleted: [])); + + // Modify + map.realm.write(() { + map[keyToUpdate] = testData.sampleValue; + }); + + await waitForChanges((inserted: [], modified: [keyToUpdate], deleted: [])); + + // Delete + map.realm.write(() { + map.remove(keyToUpdate); + }); + + await waitForChanges((inserted: [], modified: [], deleted: [keyToUpdate])); + + // Stop listening + subscription.cancel(); + + expect(changes.length, expectedCallbacks); + + map.realm.write(() { + map[Uuid.v4().toString()] = testData.sampleValue; + }); + + map.realm.refresh(); + + // We shouldn't have received a notification + expect(changes.length, expectedCallbacks); +} + +@isTest +void runTests(List> Function() testGetter, RealmMap Function(TestRealmMaps) accessor) { + group('$T test cases', () { + for (var test in testGetter()) { + testUnmanaged(accessor, test); + testManaged(accessor, test); + } + }); + + group('notifications', () { + testNotifications(accessor, testGetter().last); + + test('key notifications', () {}); + }); +} + +final List<({String key, String errorFragment})> invalidKeys = [ + (key: '.', errorFragment: "must not contain '.'"), + (key: '\$', errorFragment: "must not start with '\$'"), + (key: '\$foo', errorFragment: "must not start with '\$'"), + (key: 'foo.bar', errorFragment: "must not contain '.'"), + (key: 'foo.', errorFragment: "must not contain '.'") +]; + +Future main([List? args]) async { + await setupTests(args); + + group('key validation', () { + for (final testData in invalidKeys) { + test('Invalid key: ${testData.key}', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + expect(() => testObject.stringMap[testData.key] = 'value', + throwsA(isA().having((e) => e.message, 'message', contains(testData.errorFragment)))); + + final unmanaged = TestRealmMaps(1); + unmanaged.stringMap[testData.key] = 'value'; + + expect(() => realm.add(unmanaged), throwsA(isA().having((e) => e.message, 'message', contains(testData.errorFragment)))); + }); + }); + } + + for (final key in [r'a$', r'a$$$$$$$$$', r'_$_$_']) { + test('key may contain \$: $key', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + + realm.write(() { + final testObject = realm.add(TestRealmMaps(0)); + testObject.stringMap[key] = 'value'; + }); + }); + } + }); + + group('queries', () { + test('invalid predicate', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expect(() => map.query('invalid predicate'), throwsA(isA().having((e) => e.message, 'message', contains('Invalid predicate')))); + }); + + test('invalid number of arguments', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expect(() => map.query(r'make = $0'), + throwsA(isA().having((e) => e.message, 'message', contains('Request for argument at index 0 but no arguments are provided')))); + }); + + test('unmanaged dictionary throws', () { + final map = TestRealmMaps(0).objectsMap; + expect(() => map.query('query'), throwsA(isA())); + }); + + test('can be filtered', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + final filtered = map.query(r'make BEGINSWITH $0', ['A']); + realm.write(() { + map['a'] = Car('Acura'); + map['b'] = Car('BMW'); + map['c'] = Car('Astra'); + }); + + expect(filtered.length, 2); + expect(filtered.firstWhereOrNull((element) => element.make == 'BMW'), isNull); + }); + + test('can be sorted', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + final filtered = map.query(r'make BEGINSWITH[c] $0 SORT(make desc)', ['A']); + realm.write(() { + map['a'] = Car('aaaa'); + map['b'] = Car('azzz'); + map['c'] = Car('abbb'); + }); + + expect(filtered.length, 3); + expect(filtered.map((e) => e.make), ['azzz', 'abbb', 'aaaa']); + }); + + test('raises notifications', () { + final config = Configuration.local([TestRealmMaps.schema, Car.schema, EmbeddedValue.schema]); + final realm = getRealm(config); + final map = realm.write(() => realm.add(TestRealmMaps(0))).objectsMap; + + expectLater( + map.query('TRUEPREDICATE SORT(make asc)').changes, + emitsInOrder([ + isA>().having((ch) => ch.inserted, 'inserted', []), // always an empty event on subscription + isA>().having((ch) => ch.inserted, 'inserted', [0]), + isA>().having((ch) => ch.modified, 'modified', [0]), + isA>().having((ch) => ch.inserted, 'inserted', [1]), + isA>().having((ch) => ch.deleted, 'deleted', [0]), + ])); + + realm.write(() { + map['a'] = Car('aaaa'); + }); + realm.refresh(); + + realm.write(() { + map['a']!.color = 'some color'; + }); + realm.refresh(); + + realm.write(() { + map['b'] = Car('bbbb'); + }); + realm.refresh(); + + realm.write(() { + map.remove('a'); + }); + realm.refresh(); + }); + }); + + runTests(boolTestValues, (e) => e.boolMap); + runTests(nullableBoolTestValues, (e) => e.nullableBoolMap); + + runTests(intTestCases, (e) => e.intMap); + runTests(nullableIntTestCases, (e) => e.nullableIntMap); + + runTests(stringTestValues, (e) => e.stringMap); + runTests(nullableStringTestValues, (e) => e.nullableStringMap); + + runTests(doubleTestValues, (e) => e.doubleMap); + runTests(nullableDoubleTestValues, (e) => e.nullableDoubleMap); + + runTests(decimal128TestValues, (e) => e.decimalMap); + runTests(nullableDecimal128TestValues, (e) => e.nullableDecimalMap); + + runTests(dateTimeTestValues, (e) => e.dateTimeMap); + runTests(nullableDateTimeTestValues, (e) => e.nullableDateTimeMap); + + runTests(objectIdTestValues, (e) => e.objectIdMap); + runTests(nullableObjectIdTestValues, (e) => e.nullableObjectIdMap); + + runTests(uuidTestValues, (e) => e.uuidMap); + runTests(nullableUuidTestValues, (e) => e.nullableUuidMap); + + runTests(byteArrayTestValues, (e) => e.binaryMap); + runTests(nullableByteArrayTestValues, (e) => e.nullableBinaryMap); + + runTests(realmValueTestValues, (e) => e.mixedMap); + + runTests(_embeddedObjectTestValues, (e) => e.embeddedMap); +} diff --git a/test/realm_map_test.g.dart b/test/realm_map_test.g.dart new file mode 100644 index 000000000..3acadf1f7 --- /dev/null +++ b/test/realm_map_test.g.dart @@ -0,0 +1,375 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'realm_map_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { + Car( + String make, { + String? color, + }) { + RealmObjectBase.set(this, 'make', make); + RealmObjectBase.set(this, 'color', color); + } + + Car._(); + + @override + String get make => RealmObjectBase.get(this, 'make') as String; + @override + set make(String value) => RealmObjectBase.set(this, 'make', value); + + @override + String? get color => RealmObjectBase.get(this, 'color') as String?; + @override + set color(String? value) => RealmObjectBase.set(this, 'color', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Car freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Car._); + return const SchemaObject(ObjectType.realmObject, Car, 'Car', [ + SchemaProperty('make', RealmPropertyType.string, primaryKey: true), + SchemaProperty('color', RealmPropertyType.string, optional: true), + ]); + } +} + +class EmbeddedValue extends _EmbeddedValue + with RealmEntity, RealmObjectBase, EmbeddedObject { + EmbeddedValue( + int intValue, + ) { + RealmObjectBase.set(this, 'intValue', intValue); + } + + EmbeddedValue._(); + + @override + int get intValue => RealmObjectBase.get(this, 'intValue') as int; + @override + set intValue(int value) => RealmObjectBase.set(this, 'intValue', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + EmbeddedValue freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(EmbeddedValue._); + return const SchemaObject( + ObjectType.embeddedObject, EmbeddedValue, 'EmbeddedValue', [ + SchemaProperty('intValue', RealmPropertyType.int), + ]); + } +} + +class TestRealmMaps extends _TestRealmMaps + with RealmEntity, RealmObjectBase, RealmObject { + TestRealmMaps( + int key, { + Map boolMap = const {}, + Map intMap = const {}, + Map stringMap = const {}, + Map doubleMap = const {}, + Map dateTimeMap = const {}, + Map objectIdMap = const {}, + Map uuidMap = const {}, + Map binaryMap = const {}, + Map decimalMap = const {}, + Map nullableBoolMap = const {}, + Map nullableIntMap = const {}, + Map nullableStringMap = const {}, + Map nullableDoubleMap = const {}, + Map nullableDateTimeMap = const {}, + Map nullableObjectIdMap = const {}, + Map nullableUuidMap = const {}, + Map nullableBinaryMap = const {}, + Map nullableDecimalMap = const {}, + Map objectsMap = const {}, + Map embeddedMap = const {}, + Map mixedMap = const {}, + }) { + RealmObjectBase.set(this, 'key', key); + RealmObjectBase.set>( + this, 'boolMap', RealmMap(boolMap)); + RealmObjectBase.set>(this, 'intMap', RealmMap(intMap)); + RealmObjectBase.set>( + this, 'stringMap', RealmMap(stringMap)); + RealmObjectBase.set>( + this, 'doubleMap', RealmMap(doubleMap)); + RealmObjectBase.set>( + this, 'dateTimeMap', RealmMap(dateTimeMap)); + RealmObjectBase.set>( + this, 'objectIdMap', RealmMap(objectIdMap)); + RealmObjectBase.set>( + this, 'uuidMap', RealmMap(uuidMap)); + RealmObjectBase.set>( + this, 'binaryMap', RealmMap(binaryMap)); + RealmObjectBase.set>( + this, 'decimalMap', RealmMap(decimalMap)); + RealmObjectBase.set>( + this, 'nullableBoolMap', RealmMap(nullableBoolMap)); + RealmObjectBase.set>( + this, 'nullableIntMap', RealmMap(nullableIntMap)); + RealmObjectBase.set>( + this, 'nullableStringMap', RealmMap(nullableStringMap)); + RealmObjectBase.set>( + this, 'nullableDoubleMap', RealmMap(nullableDoubleMap)); + RealmObjectBase.set>( + this, 'nullableDateTimeMap', RealmMap(nullableDateTimeMap)); + RealmObjectBase.set>( + this, 'nullableObjectIdMap', RealmMap(nullableObjectIdMap)); + RealmObjectBase.set>( + this, 'nullableUuidMap', RealmMap(nullableUuidMap)); + RealmObjectBase.set>( + this, 'nullableBinaryMap', RealmMap(nullableBinaryMap)); + RealmObjectBase.set>( + this, 'nullableDecimalMap', RealmMap(nullableDecimalMap)); + RealmObjectBase.set>( + this, 'objectsMap', RealmMap(objectsMap)); + RealmObjectBase.set>( + this, 'embeddedMap', RealmMap(embeddedMap)); + RealmObjectBase.set>( + this, 'mixedMap', RealmMap(mixedMap)); + } + + TestRealmMaps._(); + + @override + int get key => RealmObjectBase.get(this, 'key') as int; + @override + set key(int value) => RealmObjectBase.set(this, 'key', value); + + @override + RealmMap get boolMap => + RealmObjectBase.get(this, 'boolMap') as RealmMap; + @override + set boolMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get intMap => + RealmObjectBase.get(this, 'intMap') as RealmMap; + @override + set intMap(covariant RealmMap value) => throw RealmUnsupportedSetError(); + + @override + RealmMap get stringMap => + RealmObjectBase.get(this, 'stringMap') as RealmMap; + @override + set stringMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get doubleMap => + RealmObjectBase.get(this, 'doubleMap') as RealmMap; + @override + set doubleMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get dateTimeMap => + RealmObjectBase.get(this, 'dateTimeMap') as RealmMap; + @override + set dateTimeMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectIdMap => + RealmObjectBase.get(this, 'objectIdMap') as RealmMap; + @override + set objectIdMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get uuidMap => + RealmObjectBase.get(this, 'uuidMap') as RealmMap; + @override + set uuidMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get binaryMap => + RealmObjectBase.get(this, 'binaryMap') as RealmMap; + @override + set binaryMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get decimalMap => + RealmObjectBase.get(this, 'decimalMap') + as RealmMap; + @override + set decimalMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableBoolMap => + RealmObjectBase.get(this, 'nullableBoolMap') as RealmMap; + @override + set nullableBoolMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableIntMap => + RealmObjectBase.get(this, 'nullableIntMap') as RealmMap; + @override + set nullableIntMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableStringMap => + RealmObjectBase.get(this, 'nullableStringMap') + as RealmMap; + @override + set nullableStringMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDoubleMap => + RealmObjectBase.get(this, 'nullableDoubleMap') + as RealmMap; + @override + set nullableDoubleMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDateTimeMap => + RealmObjectBase.get(this, 'nullableDateTimeMap') + as RealmMap; + @override + set nullableDateTimeMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableObjectIdMap => + RealmObjectBase.get(this, 'nullableObjectIdMap') + as RealmMap; + @override + set nullableObjectIdMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableUuidMap => + RealmObjectBase.get(this, 'nullableUuidMap') as RealmMap; + @override + set nullableUuidMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableBinaryMap => + RealmObjectBase.get(this, 'nullableBinaryMap') + as RealmMap; + @override + set nullableBinaryMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get nullableDecimalMap => + RealmObjectBase.get(this, 'nullableDecimalMap') + as RealmMap; + @override + set nullableDecimalMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get objectsMap => + RealmObjectBase.get(this, 'objectsMap') as RealmMap; + @override + set objectsMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get embeddedMap => + RealmObjectBase.get(this, 'embeddedMap') + as RealmMap; + @override + set embeddedMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + RealmMap get mixedMap => + RealmObjectBase.get(this, 'mixedMap') as RealmMap; + @override + set mixedMap(covariant RealmMap value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + TestRealmMaps freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(TestRealmMaps._); + return const SchemaObject( + ObjectType.realmObject, TestRealmMaps, 'TestRealmMaps', [ + SchemaProperty('key', RealmPropertyType.int, primaryKey: true), + SchemaProperty('boolMap', RealmPropertyType.bool, + collectionType: RealmCollectionType.map), + SchemaProperty('intMap', RealmPropertyType.int, + collectionType: RealmCollectionType.map), + SchemaProperty('stringMap', RealmPropertyType.string, + collectionType: RealmCollectionType.map), + SchemaProperty('doubleMap', RealmPropertyType.double, + collectionType: RealmCollectionType.map), + SchemaProperty('dateTimeMap', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.map), + SchemaProperty('objectIdMap', RealmPropertyType.objectid, + collectionType: RealmCollectionType.map), + SchemaProperty('uuidMap', RealmPropertyType.uuid, + collectionType: RealmCollectionType.map), + SchemaProperty('binaryMap', RealmPropertyType.binary, + collectionType: RealmCollectionType.map), + SchemaProperty('decimalMap', RealmPropertyType.decimal128, + collectionType: RealmCollectionType.map), + SchemaProperty('nullableBoolMap', RealmPropertyType.bool, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableIntMap', RealmPropertyType.int, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableStringMap', RealmPropertyType.string, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDoubleMap', RealmPropertyType.double, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDateTimeMap', RealmPropertyType.timestamp, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableObjectIdMap', RealmPropertyType.objectid, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableUuidMap', RealmPropertyType.uuid, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableBinaryMap', RealmPropertyType.binary, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('nullableDecimalMap', RealmPropertyType.decimal128, + optional: true, collectionType: RealmCollectionType.map), + SchemaProperty('objectsMap', RealmPropertyType.object, + optional: true, + linkTarget: 'Car', + collectionType: RealmCollectionType.map), + SchemaProperty('embeddedMap', RealmPropertyType.object, + optional: true, + linkTarget: 'EmbeddedValue', + collectionType: RealmCollectionType.map), + SchemaProperty('mixedMap', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.map), + ]); + } +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 0f34ebdcc..4d7fba048 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -69,11 +69,7 @@ Future main([List? args]) async { final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); expect(something.oneAny.type, x.runtimeType); expect(something.oneAny.value, x); - if (x is Uint8List) { - expect(something.oneAny, isNot(RealmValue.from(x))); - } else { - expect(something.oneAny, RealmValue.from(x)); - } + expect(something.oneAny, RealmValue.from(x)); }); } diff --git a/test/results_test.dart b/test/results_test.dart index 9a1115b28..fffe93a73 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -132,7 +132,7 @@ Future main([List? args]) async { final cars = realm.all(); - expect(() => cars[0], throws("Requested index 0 calling get() on Results when empty")); + expect(() => cars[0], throws()); }); test('Results iteration test', () { @@ -961,11 +961,11 @@ Future main([List? args]) async { final rit = results.iterator; // you are not supposed to call current before first moveNext - expect(() => rit.current, throwsA(isA())); + expect(() => rit.current, throwsA(isA())); expect(rit.moveNext(), isTrue); expect(rit.moveNext(), isFalse); // you are not supposed to call current, if moveNext return false - expect(() => rit.current, throwsA(isA())); + expect(() => rit.current, throwsA(isA())); }); test('RealmResults.indexOf', () { diff --git a/test/test.dart b/test/test.dart index fc0847859..592046525 100644 --- a/test/test.dart +++ b/test/test.dart @@ -245,7 +245,7 @@ class _Player { @RealmModel() class _Game { - final winnerByRound = <_Player>[]; // null means no winner yet + final winnerByRound = <_Player>[]; int get rounds => winnerByRound.length; }