diff --git a/.vscode/launch.json b/.vscode/launch.json index d0a8fba74..78c7de3e8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,6 +52,15 @@ "program": "test/realm_test.dart", "cwd": "${workspaceFolder}", "args": ["--name", ""] - } + }, + { + "name": "Debug Generator Tests", + "type": "dart", + "request": "launch", + // "program": "test/good_test.dart", + "program": "test/error_test.dart", + "args": ["-p", "vm", "-n", "unsupported_realm_set_with_default_values.dart"], + "cwd": "generator", + }, ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e4d5699..d9b714354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,10 @@ **This project is in Release Candidate stage.** ### Enhancements -* None +* Add supoprt for Realm set data type ([#1102](https://github.com/realm/realm-dart/pull/1102)) ### Fixed -* None +* Added an error for default values for Realm object references in the Realm generator ([#1102](https://github.com/realm/realm-dart/pull/1102)) ### Compatibility * Realm Studio: 13.0.0 or later. diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index af9cfcd79..2a55e063c 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -69,6 +69,7 @@ enum RealmCollectionType { none, list, set, + _3, // ignore: unused_field, constant_identifier_names dictionary, } diff --git a/flutter/realm_flutter/tests/test_driver/realm_test.dart b/flutter/realm_flutter/tests/test_driver/realm_test.dart index 45b9a2ab6..5d4a7c4a5 100644 --- a/flutter/realm_flutter/tests/test_driver/realm_test.dart +++ b/flutter/realm_flutter/tests/test_driver/realm_test.dart @@ -20,6 +20,7 @@ import '../test/session_test.dart' as session_test; import '../test/subscription_test.dart' as subscription_test; import '../test/user_test.dart' as user_test; import '../test/client_reset_test.dart' as client_reset_test; +import '../test/realm_set_test.dart' as realm_set_test; Future main(List args) async { final Completer completer = Completer(); @@ -41,6 +42,7 @@ Future main(List args) async { await subscription_test.main(args); await user_test.main(args); await client_reset_test.main(args); + await realm_set_test.main(args); tearDown(() { if (Invoker.current?.liveTest.state.result == test_api.Result.error || Invoker.current?.liveTest.state.result == test_api.Result.failure) { diff --git a/generator/README.md b/generator/README.md index c5afe50b0..d8f57750d 100644 --- a/generator/README.md +++ b/generator/README.md @@ -60,12 +60,6 @@ class _Car { # Debugging -* On first use .dart_tool/build/entrypoint/build.dart needs to be generated with pub run build_runer build - -* Use a terminal to launch a debuggee with command - - ``` - dart run --observe --pause-isolates-on-start --enable-vm-service:5858/127.0.0.1 --disable-service-auth-codes .dart_tool/build/entrypoint/build.dart build - ``` +* Use the Debug Generator Tests launch config in VS Code. The launch config also has an arg for filtering specifc tests. ##### The "Dart" name and logo and the "Flutter" name and logo are trademarks owned by Google. \ No newline at end of file diff --git a/generator/lib/src/dart_type_ex.dart b/generator/lib/src/dart_type_ex.dart index 19334791f..8c1c6d35f 100644 --- a/generator/lib/src/dart_type_ex.dart +++ b/generator/lib/src/dart_type_ex.dart @@ -32,6 +32,7 @@ extension DartTypeEx on DartType { bool get isRealmValue => const TypeChecker.fromRuntime(RealmValue).isAssignableFromType(this); bool get isRealmCollection => realmCollectionType != RealmCollectionType.none; + bool get isRealmSet => realmCollectionType == RealmCollectionType.set; bool get isRealmModel => element2 != null ? realmModelChecker.annotationsOfExact(element2!).isNotEmpty : false; bool get isNullable => session.typeSystem.isNullable(this); @@ -110,7 +111,6 @@ extension DartTypeEx on DartType { if (isExactly()) return RealmPropertyType.binary; if (isRealmValue) return RealmPropertyType.mixed; if (isExactly()) return RealmPropertyType.timestamp; - if (isExactly()) return RealmPropertyType.float; if (isDartCoreNum || isDartCoreDouble) return RealmPropertyType.double; if (isExactly()) return RealmPropertyType.decimal128; if (isRealmModel) return RealmPropertyType.object; diff --git a/generator/lib/src/field_element_ex.dart b/generator/lib/src/field_element_ex.dart index ca041525b..59ccb2642 100644 --- a/generator/lib/src/field_element_ex.dart +++ b/generator/lib/src/field_element_ex.dart @@ -37,6 +37,8 @@ import 'session.dart'; import 'type_checkers.dart'; extension FieldElementEx on FieldElement { + static const realmSetUnsupportedRealmTypes = [RealmPropertyType.binary, RealmPropertyType.linkingObjects]; + FieldDeclaration get declarationAstNode => getDeclarationFromElement(this)!.node.parent!.parent as FieldDeclaration; AnnotationValue? get ignoredInfo => annotationInfoOfExact(ignoredChecker); @@ -59,6 +61,14 @@ extension FieldElementEx on FieldElement { [span!], ); + FileSpan? initializerExpressionSpan(SourceFile file, Expression initializerExpression) => ExpandedContextSpan( + ExpandedContextSpan( + (initializerExpression).span(file), + [span!], + ), + [span!], + ); + DartType get modelType => typeAnnotation?.type?.nullIfDynamic ?? initializerExpression?.staticType ?? PseudoType(typeAnnotation.toString()); String get modelTypeName => modelType.getDisplayString(withNullability: true); @@ -82,9 +92,7 @@ extension FieldElementEx on FieldElement { final backlink = backlinkInfo; // Check for as-of-yet unsupported type - if (type.isDartCoreSet || // - type.isDartCoreMap || - type.isExactly()) { + if (type.isDartCoreMap || type.isExactly()) { throw RealmInvalidGenerationSourceError( 'Field type not supported yet', element: this, @@ -186,7 +194,7 @@ extension FieldElementEx on FieldElement { } else { // Validate collections and backlinks if (type.isRealmCollection || backlink != null) { - final typeDescription = type.isRealmCollection ? 'collections' : 'backlinks'; + final typeDescription = type.isRealmCollection ? (type.isRealmSet ? 'sets' : 'collections') : 'backlinks'; if (type.isNullable) { throw RealmInvalidGenerationSourceError( 'Realm $typeDescription cannot be nullable', @@ -204,6 +212,34 @@ extension FieldElementEx on FieldElement { element: this, todo: 'Ensure element type is non-nullable'); } + + 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.'); + } + } } // Validate backlinks @@ -260,6 +296,17 @@ extension FieldElementEx on FieldElement { element: this, ); } + + final intiExpression = initializerExpression; + if (intiExpression != null) { + throw RealmInvalidGenerationSourceError( + 'Realm object references should not have default values', + primarySpan: initializerExpressionSpan(file, intiExpression), + primaryLabel: ' Remove the default value', + todo: 'Remove the default value for field "$displayName"', + element: this, + ); + } } // Validate mixed (RealmValue) diff --git a/generator/lib/src/realm_field_info.dart b/generator/lib/src/realm_field_info.dart index b49bd0982..0906b62ae 100644 --- a/generator/lib/src/realm_field_info.dart +++ b/generator/lib/src/realm_field_info.dart @@ -44,6 +44,7 @@ class RealmFieldInfo { 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; @@ -68,8 +69,9 @@ class RealmFieldInfo { String get initializer { if (type.isDartCoreList) return ' = const []'; - if (isMixed) return ' = const RealmValue.nullValue()'; + if (isMixed && !type.isRealmCollection) 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 8fbb6231c..cabf8133a 100644 --- a/generator/lib/src/realm_model_info.dart +++ b/generator/lib/src/realm_model_info.dart @@ -48,11 +48,13 @@ class RealmModelInfo { yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); final notRequired = allSettable.where((f) => !f.isRequired && !f.isPrimaryKey); - final collections = fields.where((f) => f.isRealmCollection).toList(); - if (notRequired.isNotEmpty || collections.isNotEmpty) { + final collections = fields.where((f) => f.isRealmCollection && !f.isDartCoreSet).toList(); + final sets = fields.where((f) => f.isDartCoreSet).toList(); + if (notRequired.isNotEmpty || collections.isNotEmpty || sets.isNotEmpty) { yield '{'; yield* notRequired.map((f) => '${f.mappedTypeName} ${f.name}${f.initializer},'); yield* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},'); + yield* sets.map((c) => 'Set<${c.type.basicMappedName}> ${c.name}${c.initializer},'); yield '}'; } @@ -73,6 +75,10 @@ class RealmModelInfo { yield* collections.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 '}'; yield ''; diff --git a/generator/test/error_test.dart b/generator/test/error_test.dart index b3e3fefd3..c6b9a7e99 100644 --- a/generator/test/error_test.dart +++ b/generator/test/error_test.dart @@ -6,16 +6,17 @@ void main() { const directory = 'test/error_test_data'; getListOfTestFiles(directory).forEach((inputFile, outputFile) { executeTest(getTestName(inputFile), () async { + final expectedContent = await readFileAsErrorFormattedString(directory, outputFile); expectLater( generatorTestBuilder(directory, inputFile), throwsA( isA().having( (e) => e.format(), - 'format()', - await readFileAsErrorFormattedString(directory, outputFile), + '', + LinesEqualsMatcher(expectedContent), ), ), ); }); }); -} +} \ No newline at end of file diff --git a/generator/test/error_test_data/nullable_set.dart b/generator/test/error_test_data/nullable_set.dart new file mode 100644 index 000000000..792bb11f2 --- /dev/null +++ b/generator/test/error_test_data/nullable_set.dart @@ -0,0 +1,11 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'nullable_set.g.dart'; + +@RealmModel() +class _Bad { + @PrimaryKey() + late int id; + + Set? wrong; +} diff --git a/generator/test/error_test_data/nullable_set.expected b/generator/test/error_test_data/nullable_set.expected new file mode 100644 index 000000000..09e6a9829 --- /dev/null +++ b/generator/test/error_test_data/nullable_set.expected @@ -0,0 +1,11 @@ +Realm sets cannot be nullable + +in: asset:pkg/test/error_test_data/nullable_set.dart:10:3 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +10 │ Set? wrong; + │ ^^^^^^^^^ is nullable + ╵ diff --git a/generator/test/error_test_data/set_unsupported.dart b/generator/test/error_test_data/realm_object_reference_default_values.dart similarity index 52% rename from generator/test/error_test_data/set_unsupported.dart rename to generator/test/error_test_data/realm_object_reference_default_values.dart index 97f739ab8..69dfa72fe 100644 --- a/generator/test/error_test_data/set_unsupported.dart +++ b/generator/test/error_test_data/realm_object_reference_default_values.dart @@ -1,8 +1,9 @@ -import 'package:realm_common/realm_common.dart'; +import 'dart:typed_data'; -//part 'set_unsupported.g.dart'; +import 'package:realm_common/realm_common.dart'; @RealmModel() class _Person { - late Set<_Person> children; + int x = 0; + late _Person? parent = Person(); } diff --git a/generator/test/error_test_data/realm_object_reference_default_values.expected b/generator/test/error_test_data/realm_object_reference_default_values.expected new file mode 100644 index 000000000..67ccdd392 --- /dev/null +++ b/generator/test/error_test_data/realm_object_reference_default_values.expected @@ -0,0 +1,13 @@ +Realm object references should not have default values + +in: asset:pkg/test/error_test_data/realm_object_reference_default_values.dart:8:26 + ╷ +5 │ @RealmModel() +6 │ class _Person { + │ ━━━━━━━ in realm model for 'Person' +... │ +8 │ late _Person? parent = Person(); + │ ^^^^^^^^ Remove the default value + ╵ +Remove the default value for field "parent" + diff --git a/generator/test/error_test_data/set_unsupported.expected b/generator/test/error_test_data/set_unsupported.expected deleted file mode 100644 index 9c53795d5..000000000 --- a/generator/test/error_test_data/set_unsupported.expected +++ /dev/null @@ -1,12 +0,0 @@ -Field type not supported yet - -in: asset:pkg/test/error_test_data/set_unsupported.dart:7:8 - ╷ -5 │ @RealmModel() -6 │ class _Person { - │ ━━━━━━━ in realm model for 'Person' -7 │ late Set<_Person> children; - │ ^^^^^^^^^^^^ not yet supported - ╵ -Avoid using Set<_Person> for now - diff --git a/generator/test/error_test_data/unsupported_non_realm_type_set.dart b/generator/test/error_test_data/unsupported_non_realm_type_set.dart new file mode 100644 index 000000000..d9d45e7d0 --- /dev/null +++ b/generator/test/error_test_data/unsupported_non_realm_type_set.dart @@ -0,0 +1,11 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'unsupported_non_realm_type_set.g.dart'; + +@RealmModel() +class _Bad { + @PrimaryKey() + late int id; + + late Set wrong; +} \ No newline at end of file diff --git a/generator/test/error_test_data/unsupported_non_realm_type_set.expected b/generator/test/error_test_data/unsupported_non_realm_type_set.expected new file mode 100644 index 000000000..f3f695ef3 --- /dev/null +++ b/generator/test/error_test_data/unsupported_non_realm_type_set.expected @@ -0,0 +1,13 @@ +Not a realm type + +in: asset:pkg/test/error_test_data/unsupported_non_realm_type_set.dart:10:8 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +10 │ late Set wrong; + │ ^^^^^^^^^^^^^^^^^^^^ Set is not a realm model type + ╵ +Remove the invalid field or add an @Ignored annotation on 'wrong'. + diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart new file mode 100644 index 000000000..1ca2250c0 --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart @@ -0,0 +1,11 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'unsupported_realm_set_of_nullable_realmvalue.g.dart'; + +@RealmModel() +class _Bad { + @PrimaryKey() + late int id; + + late Set<_Bad?> wrong1; +} 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 new file mode 100644 index 000000000..1bb634c99 --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.expected @@ -0,0 +1,13 @@ +Nullable realm objects are not allowed in sets + +in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmobject.dart:10:8 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +10 │ late Set<_Bad?> wrong1; + │ ^^^^^^^^^^ which has a nullable realm object element type + ╵ +Ensure element type is non-nullable + diff --git a/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.dart b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.dart new file mode 100644 index 000000000..dbc9214cd --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.dart @@ -0,0 +1,11 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'unsupported_realm_set_of_nullable_realmvalue.g.dart'; + +@RealmModel() +class _Bad { + @PrimaryKey() + late int id; + + late Set wrong1; +} 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 new file mode 100644 index 000000000..0f9d18fbc --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.expected @@ -0,0 +1,13 @@ +Set is not supported + +in: asset:pkg/test/error_test_data/unsupported_realm_set_of_nullable_realmvalue.dart:10:8 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +10 │ late Set wrong1; + │ ^^^^^^^^^^^^^^^^ Set of nullable RealmValues is not supported + ╵ +Did you mean to use Set instead? + diff --git a/generator/test/error_test_data/unsupported_realm_set_with_default_values.dart b/generator/test/error_test_data/unsupported_realm_set_with_default_values.dart new file mode 100644 index 000000000..656bc3bdc --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_with_default_values.dart @@ -0,0 +1,11 @@ +import 'package:realm_common/realm_common.dart'; + +//part 'unsupported_realm_set_of_nullable_realmvalue.g.dart'; + +@RealmModel() +class _Bad { + @PrimaryKey() + late int id; + + late Set wrong1 = {true, false }; +} 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 new file mode 100644 index 000000000..bff65897e --- /dev/null +++ b/generator/test/error_test_data/unsupported_realm_set_with_default_values.expected @@ -0,0 +1,13 @@ +Default values for set are not supported. + +in: asset:pkg/test/error_test_data/unsupported_realm_set_with_default_values.dart:10:27 + ╷ +5 │ @RealmModel() +6 │ class _Bad { + │ ━━━━ in realm model for 'Bad' +... │ +10 │ late Set wrong1 = {true, false}; + │ ^^^^^^^^^^^^^ Remove the default value. + ╵ +Remove the default value for field wrong1. + diff --git a/generator/test/good_test_data/all_types.dart b/generator/test/good_test_data/all_types.dart index 1c62d26e8..4f86173a8 100644 --- a/generator/test/good_test_data/all_types.dart +++ b/generator/test/good_test_data/all_types.dart @@ -50,4 +50,4 @@ class _PrimitiveTypes { late DateTime dateProp; late double doubleProp; late ObjectId objectIdProp; -} +} \ No newline at end of file diff --git a/generator/test/good_test_data/realm_set.dart b/generator/test/good_test_data/realm_set.dart new file mode 100644 index 000000000..462824d74 --- /dev/null +++ b/generator/test/good_test_data/realm_set.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:realm_common/realm_common.dart'; + +@RealmModel() +class _Car { + @PrimaryKey() + late String make; +} + +@RealmModel() +class _RealmSets { + @PrimaryKey() + late int key; + + late Set boolSet; + late Set nullableBoolSet; + + late Set intSet; + late Set nullableintSet; + + late Set stringSet; + late Set nullablestringSet; + + late Set doubleSet; + late Set nullabledoubleSet; + + late Set dateTimeSet; + late Set nullabledateTimeSet; + + late Set objectIdSet; + late Set nullableobjectIdSet; + + late Set uuidSet; + late Set nullableuuidSet; + + late Set realmValueSet; + late Set<_Car> realmObjectsSet; +} diff --git a/generator/test/good_test_data/realm_set.expected b/generator/test/good_test_data/realm_set.expected new file mode 100644 index 000000000..3bb5a2962 --- /dev/null +++ b/generator/test/good_test_data/realm_set.expected @@ -0,0 +1,261 @@ +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { + Car( + String make, + ) { + RealmObjectBase.set(this, 'make', make); + } + + Car._(); + + @override + String get make => RealmObjectBase.get(this, 'make') as String; + @override + set make(String value) => RealmObjectBase.set(this, 'make', 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), + ]); + } +} + +class RealmSets extends _RealmSets + with RealmEntity, RealmObjectBase, RealmObject { + RealmSets( + int key, { + Set boolSet = const {}, + Set nullableBoolSet = const {}, + Set intSet = const {}, + Set nullableintSet = const {}, + Set stringSet = const {}, + Set nullablestringSet = const {}, + Set doubleSet = const {}, + Set nullabledoubleSet = const {}, + Set dateTimeSet = const {}, + Set nullabledateTimeSet = const {}, + Set objectIdSet = const {}, + Set nullableobjectIdSet = const {}, + Set uuidSet = const {}, + Set nullableuuidSet = const {}, + Set realmValueSet = const {}, + Set realmObjectsSet = const {}, + }) { + RealmObjectBase.set(this, 'key', key); + RealmObjectBase.set>( + this, 'boolSet', RealmSet(boolSet)); + RealmObjectBase.set>( + this, 'nullableBoolSet', RealmSet(nullableBoolSet)); + RealmObjectBase.set>(this, 'intSet', RealmSet(intSet)); + RealmObjectBase.set>( + this, 'nullableintSet', RealmSet(nullableintSet)); + RealmObjectBase.set>( + this, 'stringSet', RealmSet(stringSet)); + RealmObjectBase.set>( + this, 'nullablestringSet', RealmSet(nullablestringSet)); + RealmObjectBase.set>( + this, 'doubleSet', RealmSet(doubleSet)); + RealmObjectBase.set>( + this, 'nullabledoubleSet', RealmSet(nullabledoubleSet)); + RealmObjectBase.set>( + this, 'dateTimeSet', RealmSet(dateTimeSet)); + RealmObjectBase.set>( + this, 'nullabledateTimeSet', RealmSet(nullabledateTimeSet)); + RealmObjectBase.set>( + this, 'objectIdSet', RealmSet(objectIdSet)); + RealmObjectBase.set>( + this, 'nullableobjectIdSet', RealmSet(nullableobjectIdSet)); + RealmObjectBase.set>( + this, 'uuidSet', RealmSet(uuidSet)); + RealmObjectBase.set>( + this, 'nullableuuidSet', RealmSet(nullableuuidSet)); + RealmObjectBase.set>( + this, 'realmValueSet', RealmSet(realmValueSet)); + RealmObjectBase.set>( + this, 'realmObjectsSet', RealmSet(realmObjectsSet)); + } + + RealmSets._(); + + @override + int get key => RealmObjectBase.get(this, 'key') as int; + @override + set key(int value) => RealmObjectBase.set(this, 'key', value); + + @override + RealmSet get boolSet => + RealmObjectBase.get(this, 'boolSet') as RealmSet; + @override + set boolSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableBoolSet => + RealmObjectBase.get(this, 'nullableBoolSet') as RealmSet; + @override + set nullableBoolSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get intSet => + RealmObjectBase.get(this, 'intSet') as RealmSet; + @override + set intSet(covariant RealmSet value) => throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableintSet => + RealmObjectBase.get(this, 'nullableintSet') as RealmSet; + @override + set nullableintSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get stringSet => + RealmObjectBase.get(this, 'stringSet') as RealmSet; + @override + set stringSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullablestringSet => + RealmObjectBase.get(this, 'nullablestringSet') + as RealmSet; + @override + set nullablestringSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get doubleSet => + RealmObjectBase.get(this, 'doubleSet') as RealmSet; + @override + set doubleSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullabledoubleSet => + RealmObjectBase.get(this, 'nullabledoubleSet') + as RealmSet; + @override + set nullabledoubleSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get dateTimeSet => + RealmObjectBase.get(this, 'dateTimeSet') as RealmSet; + @override + set dateTimeSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullabledateTimeSet => + RealmObjectBase.get(this, 'nullabledateTimeSet') + as RealmSet; + @override + set nullabledateTimeSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get objectIdSet => + RealmObjectBase.get(this, 'objectIdSet') as RealmSet; + @override + set objectIdSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableobjectIdSet => + RealmObjectBase.get(this, 'nullableobjectIdSet') + as RealmSet; + @override + set nullableobjectIdSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get uuidSet => + RealmObjectBase.get(this, 'uuidSet') as RealmSet; + @override + set uuidSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableuuidSet => + RealmObjectBase.get(this, 'nullableuuidSet') as RealmSet; + @override + set nullableuuidSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get realmValueSet => + RealmObjectBase.get(this, 'realmValueSet') + as RealmSet; + @override + set realmValueSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get realmObjectsSet => + RealmObjectBase.get(this, 'realmObjectsSet') as RealmSet; + @override + set realmObjectsSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + RealmSets freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(RealmSets._); + return const SchemaObject(ObjectType.realmObject, RealmSets, 'RealmSets', [ + SchemaProperty('key', RealmPropertyType.int, primaryKey: true), + SchemaProperty('boolSet', RealmPropertyType.bool, + collectionType: RealmCollectionType.set), + SchemaProperty('nullableBoolSet', RealmPropertyType.bool, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('intSet', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('nullableintSet', RealmPropertyType.int, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('stringSet', RealmPropertyType.string, + collectionType: RealmCollectionType.set), + SchemaProperty('nullablestringSet', RealmPropertyType.string, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('doubleSet', RealmPropertyType.double, + collectionType: RealmCollectionType.set), + SchemaProperty('nullabledoubleSet', RealmPropertyType.double, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('dateTimeSet', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.set), + SchemaProperty('nullabledateTimeSet', RealmPropertyType.timestamp, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('objectIdSet', RealmPropertyType.objectid, + collectionType: RealmCollectionType.set), + SchemaProperty('nullableobjectIdSet', RealmPropertyType.objectid, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('uuidSet', RealmPropertyType.uuid, + collectionType: RealmCollectionType.set), + SchemaProperty('nullableuuidSet', RealmPropertyType.uuid, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('realmValueSet', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('realmObjectsSet', RealmPropertyType.object, + linkTarget: 'Car', collectionType: RealmCollectionType.set), + ]); + } +} \ No newline at end of file diff --git a/generator/test/test_util.dart b/generator/test/test_util.dart index d2621eeaf..8f4a93de7 100644 --- a/generator/test/test_util.dart +++ b/generator/test/test_util.dart @@ -38,9 +38,10 @@ Future> getInputFileAsset(String inputFilePath) async { /// A special equality matcher for strings. class LinesEqualsMatcher extends Matcher { + String expected; late final List expectedLines; - LinesEqualsMatcher(String expected) { + LinesEqualsMatcher(this.expected) { expectedLines = expected.split("\n"); } @@ -50,26 +51,46 @@ class LinesEqualsMatcher extends Matcher { @override // ignore: strict_raw_type bool matches(dynamic actual, Map matchState) { - final actualValue = utf8.decode(actual as List); + var actualValue = ""; + if (actual is List) { + actualValue = utf8.decode(actual); + } else if (actual is String) { + actualValue = actual; + } else { + actualValue = actual.toString(); + } + final actualLines = actualValue.split("\n"); + final result = _matches(actualLines, matchState); + if (!result) { + print("\nGenerator Failed\n"); + print("Expected ======================================================================================================\n$expected\n======================================================================================================\n"); + print("Actual ======================================================================================================\n$actualValue\n======================================================================================================\n"); + } + + return result; + } + + bool _matches(List actualLines, Map matchState) { for (var i = 0; i < expectedLines.length - 1; i++) { if (i >= actualLines.length) { matchState["Error"] = "Difference at line ${i + 1}. \nExpected: ${expectedLines[i]}.\n Actual: empty"; + return false; } - + if (expectedLines[i] != actualLines[i]) { matchState["Error"] = "Difference at line ${i + 1}. \nExpected: ${expectedLines[i]}.\n Actual: ${actualLines[i]}"; return false; } } - + if (actualLines.length != expectedLines.length) { matchState["Error"] = "Different number of lines. \nExpected: ${expectedLines.length}\nActual: ${actualLines.length}"; return false; } - + return true; } @@ -100,7 +121,7 @@ Future readFileAsErrorFormattedString(String directoryName, String outpu var file = File(_path.join(Directory.current.path, '$directoryName/$outputFilePath')); String content = await file.readAsString(encoding: utf8); if (Platform.isWindows) { - var macToWinSymbols = {'╷': ',', '━': '=', '╵': '\'', '│': '|', '─': '-', '┌': ',', '└': '\''}; + final macToWinSymbols = {'╷': ',', '━': '=', '╵': '\'', '│': '|', '─': '-', '┌': ',', '└': '\''}; macToWinSymbols.forEach((key, value) { content = content.replaceAll(key, value); }); @@ -117,4 +138,4 @@ String _stringReplacements(String content) { String getTestName(String file) { return _path.basename(file); -} +} \ No newline at end of file diff --git a/lib/src/list.dart b/lib/src/list.dart index 69d6b9056..f0684900c 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -47,6 +47,8 @@ abstract class RealmList with RealmEntity implements List, RealmResults asResults(); factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); + + /// Creates an unmanaged RealmList from [items] factory RealmList(Iterable items) => UnmanagedRealmList(items); /// Creates a frozen snapshot of this `RealmList`. @@ -125,9 +127,11 @@ class ManagedRealmList with RealmEntity, ListMixin impleme } value = realm.createObject(type, value, targetMetadata); } + if (T == RealmValue) { value = RealmValue.from(value); } + return value as T; } on Exception catch (e) { throw RealmException("Error getting value at index $index. Error: $e"); @@ -307,7 +311,7 @@ extension RealmListInternal on RealmList { } } -/// Describes the changes in a Realm results collection since the last time the notification callback was invoked. +/// Describes the changes in a Realm list collection since the last time the notification callback was invoked. class RealmListChanges extends RealmCollectionChanges { /// The collection being monitored for changes. final RealmList list; diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 0bed4f35e..417dc98d1 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -44,6 +44,7 @@ import '../scheduler.dart'; import '../session.dart'; import '../subscription.dart'; import '../user.dart'; +import '../set.dart'; import 'realm_bindings.dart'; late RealmLibrary _realmLib; @@ -1176,6 +1177,95 @@ class _RealmCore { _realmLib.invokeGetBool(() => _realmLib.realm_list_clear(listHandle._pointer)); } + RealmSetHandle getSetProperty(RealmObjectBase object, int propertyKey) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_get_set(object.handle._pointer, propertyKey)); + return RealmSetHandle._(pointer, object.realm.handle); + } + + bool realmSetInsert(RealmSetHandle handle, Object? value) { + return using((Arena arena) { + final realm_value = _toRealmValue(value, arena); + final out_index = arena(); + final out_inserted = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_set_insert(handle._pointer, realm_value.ref, out_index, out_inserted)); + return out_inserted.value; + }); + } + + Object? realmSetGetElementAt(RealmSet realmSet, int index) { + return using((Arena arena) { + final realm_value = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_set_get(realmSet.handle._pointer, index, realm_value)); + final result = realm_value.toDartValue(realmSet.realm); + return result; + }); + } + + bool realmSetFind(RealmSet realmSet, Object? value) { + return using((Arena arena) { + final realm_value = _toRealmValue(value, arena); + final out_index = arena(); + final out_found = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_set_find(realmSet.handle._pointer, realm_value.ref, out_index, out_found)); + return out_found.value; + }); + } + + bool realmSetErase(RealmSet realmSet, Object? value) { + return using((Arena arena) { + final realm_value = _toRealmValue(value, arena); + final out_erased = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_set_erase(realmSet.handle._pointer, realm_value.ref, out_erased)); + return out_erased.value; + }); + } + + void realmSetClear(RealmSetHandle handle) { + _realmLib.invokeGetBool(() => _realmLib.realm_set_clear(handle._pointer)); + } + + int realmSetSize(RealmSet realmSet) { + return using((Arena arena) { + final out_size = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_set_size(realmSet.handle._pointer, out_size)); + return out_size.value; + }); + } + + bool realmSetIsValid(RealmSet realmSet) { + return _realmLib.realm_set_is_valid(realmSet.handle._pointer); + } + + void realmSetAssign(RealmSetHandle realmSet, List values) { + return using((Arena arena) { + final len = values.length; + final valuesPtr = arena.allocate(len); + for (var i = 0; i < len; i++) { + final value = values[i]; + final valPtr = valuesPtr.elementAt(i); + _intoRealmValue(value, valPtr, arena); + } + + _realmLib.invokeGetBool(() => _realmLib.realm_set_assign(realmSet._pointer, valuesPtr, len)); + }); + } + + void realmSetRemoveAll(RealmSet realmSet) { + _realmLib.invokeGetBool(() => _realmLib.realm_set_remove_all(realmSet.handle._pointer)); + } + + RealmNotificationTokenHandle subscribeSetNotifications(RealmSet realmSet, NotificationsController controller) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_set_add_notification_callback( + realmSet.handle._pointer, + controller.toWeakHandle(), + nullptr, + nullptr, + Pointer.fromFunction(collection_change_callback), + )); + + return RealmNotificationTokenHandle._(pointer, realmSet.realm.handle); + } + bool _equals(HandleBase first, HandleBase second) { return _realmLib.realm_equals(first._pointer.cast(), second._pointer.cast()); } @@ -2526,6 +2616,10 @@ class RealmListHandle extends RootedHandleBase { RealmListHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 88); } +class RealmSetHandle extends RootedHandleBase { + RealmSetHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 96); +} + class _RealmQueryHandle extends RootedHandleBase { _RealmQueryHandle._(Pointer pointer, RealmHandle root) : super(root, pointer, 256); } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 14202ec11..6925a0bd4 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -32,6 +32,7 @@ import 'results.dart'; import 'scheduler.dart'; import 'session.dart'; import 'subscription.dart'; +import 'set.dart'; export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, CancelledException; export 'package:realm_common/realm_common.dart' @@ -92,6 +93,7 @@ export "configuration.dart" SyncSessionError; export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider; export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExtension; +export 'set.dart' show RealmSet, RealmSetChanges; export 'migration.dart' show Migration; export 'realm_object.dart' show @@ -684,6 +686,16 @@ extension RealmInternal on Realm { static MigrationRealm getMigrationRealm(Realm realm) => MigrationRealm._(realm); bool get isInMigration => _isInMigration; + + void addUnmanagedRealmObjectFromValue(Object? value, bool update) { + if (value is RealmObject && !value.isManaged) { + add(value, update: update); + } + + if (value is RealmValue) { + addUnmanagedRealmObjectFromValue(value.value, update); + } + } } /// @nodoc diff --git a/lib/src/realm_object.dart b/lib/src/realm_object.dart index ea0eeb070..fe42bb73d 100644 --- a/lib/src/realm_object.dart +++ b/lib/src/realm_object.dart @@ -27,6 +27,7 @@ import 'list.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; import 'results.dart'; +import 'set.dart'; typedef DartDynamic = dynamic; @@ -175,6 +176,11 @@ class RealmCoreAccessor implements RealmAccessor { return object.realm.createList(handle, listMetadata); } + if (propertyMeta.collectionType == RealmCollectionType.set) { + final handle = realmCore.getSetProperty(object, propertyMeta.key); + return RealmSetInternal.create(handle, object.realm, metadata); + } + var value = realmCore.getProperty(object, propertyMeta.key); if (value is RealmObjectHandle) { @@ -231,15 +237,27 @@ class RealmCoreAccessor implements RealmAccessor { return; } - if (value is RealmObject && !value.isManaged) { - object.realm.add(value, update: update); - } + object.realm.addUnmanagedRealmObjectFromValue(value, update); + + //TODO: set from ManagedRealmList is not supported yet + if (value is UnmanagedRealmSet) { + final handle = realmCore.getSetProperty(object, propertyMeta.key); + if (update) { + realmCore.realmSetClear(handle); + } + + // TODO: use realmSetAssign when available in C-API + // https://github.com/realm/realm-core/issues/6209 + //realmCore.realmSetAssign(handle, value.toList()); + for (var element in value) { + object.realm.addUnmanagedRealmObjectFromValue(element, update); - if (value is RealmValue) { - final v = value.value; - if (v is RealmObject && !v.isManaged) { - object.realm.add(v, update: update); + final result = realmCore.realmSetInsert(handle, element); + if (!result) { + throw RealmException("Error while adding value $element in RealmSet"); + } } + return; } if (propertyMeta.isPrimaryKey && !isInMigration) { diff --git a/lib/src/set.dart b/lib/src/set.dart new file mode 100644 index 000000000..4281722e9 --- /dev/null +++ b/lib/src/set.dart @@ -0,0 +1,341 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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:core'; +import 'dart:async'; +import 'dart:collection'; +import 'dart:ffi'; + +import 'package:collection/collection.dart' as collection; + +import 'native/realm_core.dart'; +import 'realm_class.dart'; +import 'realm_object.dart'; +import 'collections.dart'; + +/// RealmSet is a collection that contains no duplicate elements. +abstract class RealmSet extends SetBase with RealmEntity implements Finalizable { + RealmObjectMetadata? _metadata; + + /// 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 RealmSet from [items] + factory RealmSet(Set items) => UnmanagedRealmSet(items.toSet()); + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes; + + /// Returns the element of type `T` at the specified [index]. + /// + /// Note that elements in a RealmSet move around arbitrarily when other elements are + /// inserted/removed. + @override + T elementAt(int index) => super.elementAt(index); + + /// Whether [value] is in the set. + /// ```dart + /// final characters = {'A', 'B', 'C'}; + /// final containsB = characters.contains('B'); // true + /// final containsD = characters.contains('D'); // false + /// ``` + @override + bool contains(covariant T element); //Note: explicitly overriding contains() to change parameter type + + /// If an object equal to [object] is in the set, return it. + /// + /// Checks whether [object] is in the set, like [contains], and if so, + /// returns the object in the set, otherwise returns `null`. + /// + /// If the equality relation used by the set is not identity, + /// then the returned object may not be *identical* to [object]. + /// Some set implementations may not be able to implement this method. + /// If the [contains] method is computed, + /// rather than being based on an actual object instance, + /// then there may not be a specific object instance representing the + /// set element. + /// ```dart + /// final characters = {'A', 'B', 'C'}; + /// final containsB = characters.lookup('B'); + /// print(containsB); // B + /// final containsD = characters.lookup('D'); + /// print(containsD); // null + /// ``` + @override + T? lookup(covariant T element); //Note: explicitly overriding lookup() to change parameter type + + /// Removes [value] from the set. + /// + /// Returns `true` if [value] was in the set, and `false` if not. + /// The method has no effect if [value] was not in the set. + /// ```dart + /// final characters = {'A', 'B', 'C'}; + /// final didRemoveB = characters.remove('B'); // true + /// final didRemoveD = characters.remove('D'); // false + /// print(characters); // {A, C} + /// ``` + @override + bool remove(covariant T value); //Note: explicitly overriding remove() to change parameter type + + /// Clear the set and if it is a set of Realm objects, delete all realm objects from the realm. + void deleteAll(); +} + +class UnmanagedRealmSet extends collection.DelegatingSet with RealmEntity implements RealmSet { + UnmanagedRealmSet([Set? items]) : super(items ?? {}); + + @override + RealmObjectMetadata? get _metadata => throw RealmException("Unmanaged RealmSets don't have metadata associated with them."); + + @override + set _metadata(RealmObjectMetadata? _) => throw RealmException("Unmanaged RealmSets don't have metadata associated with them."); + + @override + bool get isValid => true; + + @override + Stream> get changes => throw RealmStateError("Unmanaged RealmSets don't support changes"); + + @override + void deleteAll() => super.clear(); +} + +class ManagedRealmSet with RealmEntity, SetMixin implements RealmSet { + final RealmSetHandle _handle; + + ManagedRealmSet._(this._handle, Realm realm, this._metadata) { + setRealm(realm); + } + + @override + late final RealmObjectMetadata? _metadata; + + @override + bool get isValid => realmCore.realmSetIsValid(this); + + @override + bool add(T value) { + if (_isManagedRealmObject(value)) { + //It is valid to call `add` with managed objects already in the set. + _ensureManagedByThis(value, "add"); + } else { + // might be updating an existing realm object + realm.addUnmanagedRealmObjectFromValue(value, false); + } + + return realmCore.realmSetInsert(_handle, value); + } + + @override + T elementAt(int index) { + if (index < 0) { + throw RealmException("Index out of range $index"); + } + + try { + var value = realmCore.realmSetGetElementAt(this, index); + if (value is RealmObjectHandle) { + RealmObjectMetadata targetMetadata = _metadata!; + Type type = T; + if (T == RealmValue) { + final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value)); + type = tuple.item1; + targetMetadata = tuple.item2; + } + + value = realm.createObject(type, value, targetMetadata); + } + + if (T == RealmValue) { + return RealmValue.from(value) as T; + } + + return value as T; + } on Exception catch (e) { + throw RealmException("Error getting value at index $index. Error: $e"); + } + } + + @override + bool contains(covariant T element) { + if (!_isManagedRealmObject(element)) { + return false; + } + + _ensureManagedByThis(element, "contains"); + + return realmCore.realmSetFind(this, element); + } + + @override + T? lookup(covariant T element) { + return contains(element) ? element : null; + } + + @override + bool remove(covariant T value) { + if (!_isManagedRealmObject(value)) { + return false; + } + + _ensureManagedByThis(value, "remove"); + + return realmCore.realmSetErase(this, value); + } + + @override + Iterator get iterator => _RealmSetIterator(this); + + @override + Set toSet() => {...this}; + + @override + void clear() => realmCore.realmSetClear(_handle); + + @override + void deleteAll() => realmCore.realmSetRemoveAll(this); + + @override + int get length => realmCore.realmSetSize(this); + + @override + Stream> get changes { + final controller = RealmSetNotificationsController(asManaged()); + return controller.createStream(); + } + + void _ensureManagedByThis(covariant T element, String action) { + Object? value = element; + if (element is RealmValue && element.value is RealmObject) { + value = element.value; + } + + if (value is! RealmObject) { + return; + } + + RealmObject realmObject = value; + + if (realmObject.realm != realm) { + if (realmObject.isFrozen) { + throw RealmError('Cannot invoke "$action" because the object is managed by a frozen Realm'); + } + + throw RealmError('Cannot invoke "$action" because the object is managed by another Realm instance'); + } + } + + bool _isManagedRealmObject(Object? object) { + if (object is RealmObject) { + if (!object.isManaged) { + return false; + } + } + + if (object is RealmValue) { + return _isManagedRealmObject(object.value); + } + + return true; + } +} + +/// @nodoc +extension RealmSetInternal on RealmSet { + ManagedRealmSet asManaged() => this is ManagedRealmSet ? this as ManagedRealmSet : throw RealmStateError('$this is not managed'); + + RealmSetHandle get handle { + final result = asManaged()._handle; + if (result.released) { + throw RealmClosedError('Cannot access a RealmSet that belongs to a closed Realm'); + } + + return result; + } + + static RealmSet create(RealmSetHandle handle, Realm realm, RealmObjectMetadata? metadata) => + ManagedRealmSet._(handle, realm, metadata); +} + +class _RealmSetIterator implements Iterator { + final RealmSet _set; + int _index; + T? _current; + + _RealmSetIterator(this._set) : _index = -1; + + @override + T get current => _current as T; + + @override + bool moveNext() { + _index++; + if (_index >= _set.length) { + _current = null; + return false; + } + _current = _set.elementAt(_index); + + return true; + } +} + +/// Describes the changes in a Realm set collection since the last time the notification callback was invoked. +class RealmSetChanges extends RealmCollectionChanges { + /// The collection being monitored for changes. + final RealmSet set; + + RealmSetChanges._(super.handle, this.set); +} + +/// @nodoc +class RealmSetNotificationsController extends NotificationsController { + final ManagedRealmSet set; + late final StreamController> streamController; + + RealmSetNotificationsController(this.set); + + @override + RealmNotificationTokenHandle subscribe() { + return realmCore.subscribeSetNotifications(set, this); + } + + Stream> createStream() { + streamController = StreamController>(onListen: start, onPause: stop, onResume: start, onCancel: stop); + return streamController.stream; + } + + @override + void onChanges(HandleBase changesHandle) { + if (changesHandle is! RealmCollectionChangesHandle) { + throw RealmError("Invalid changes handle. RealmCollectionChangesHandle expected"); + } + + final changes = RealmSetChanges._(changesHandle, set); + streamController.add(changes); + } + + @override + void onError(RealmError error) { + streamController.addError(error); + } +} diff --git a/test/realm_set_test.dart b/test/realm_set_test.dart new file mode 100644 index 000000000..1d791626a --- /dev/null +++ b/test/realm_set_test.dart @@ -0,0 +1,571 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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:typed_data'; + +import 'package:test/test.dart' hide test, throws; +import '../lib/realm.dart'; + +import 'test.dart'; + +part 'realm_set_test.g.dart'; + +class _NullableBool {} + +class _NullableInt {} + +class _NullableString {} + +class _NullableDouble {} + +class _NullableDateTime {} + +class _NullableObjectId {} + +class _NullableUuid {} + +class _NullableObjects {} + +/// When changing update also `setByType` +List supportedTypes = [ + bool, + int, + String, + double, + DateTime, + ObjectId, + Uuid, + RealmValue, + RealmObject, + _NullableBool, + _NullableInt, + _NullableString, + _NullableDouble, + _NullableDateTime, + _NullableObjectId, + _NullableUuid +]; + +@RealmModel() +class _Car { + @PrimaryKey() + late String make; +} + +@RealmModel() +class _TestRealmSets { + @PrimaryKey() + late int key; + + late Set boolSet; + late Set intSet; + late Set stringSet; + late Set doubleSet; + late Set dateTimeSet; + late Set objectIdSet; + late Set uuidSet; + late Set mixedSet; + late Set<_Car> objectsSet; + + late Set nullableBoolSet; + late Set nullableIntSet; + late Set nullableStringSet; + late Set nullableDoubleSet; + late Set nullableDateTimeSet; + late Set nullableObjectIdSet; + late Set nullableUuidSet; + + /// When changing update also `supportedTypes` + Sets setByType(Type type) { + switch (type) { + case bool: + return Sets(boolSet as RealmSet, [true, false]); + case int: + return Sets(intSet as RealmSet, [-1, 0, 1]); + case String: + return Sets(stringSet as RealmSet, ['Tesla', 'VW', 'Audi']); + case double: + return Sets(doubleSet as RealmSet, [-1.1, 0.1, 1.1, 2.2, 3.3, 3.14]); + case DateTime: + return Sets(dateTimeSet as RealmSet, [DateTime(2023).toUtc(), DateTime(1981).toUtc()]); + case ObjectId: + return Sets(objectIdSet as RealmSet, [ObjectId.fromTimestamp(DateTime(2023).toUtc()), ObjectId.fromTimestamp(DateTime(1981).toUtc())]); + case Uuid: + return Sets(uuidSet as RealmSet, [Uuid.fromString("12345678123456781234567812345678"), Uuid.fromString("82345678123456781234567812345678")]); + case RealmValue: + return Sets(mixedSet as RealmSet, [RealmValue.nullValue(), RealmValue.int(1), RealmValue.realmObject(Car("Tesla"))], + (realm, value) => realm.find((value as Car).make)); + case RealmObject: + return Sets(objectsSet as RealmSet, [Car("Tesla"), Car("VW"), Car("Audi")], (realm, value) => realm.find((value as Car).make)); + case _NullableBool: + return Sets(nullableBoolSet as RealmSet, [...setByType(bool).values, null]); + case _NullableInt: + return Sets(nullableIntSet as RealmSet, [...setByType(int).values, null]); + case _NullableString: + return Sets(nullableStringSet as RealmSet, [...setByType(String).values, null]); + case _NullableDouble: + return Sets(nullableDoubleSet as RealmSet, [...setByType(double).values, null]); + case _NullableDateTime: + return Sets(nullableDateTimeSet as RealmSet, [...setByType(DateTime).values, null]); + case _NullableObjectId: + return Sets(nullableObjectIdSet as RealmSet, [...setByType(ObjectId).values, null]); + case _NullableUuid: + return Sets(nullableUuidSet as RealmSet, [...setByType(Uuid).values, null]); + default: + throw RealmError("Unsupported type $type"); + } + } + + List values(Type type) { + return setByType(type).values; + } + + List getValuesOrManagedValues(Realm realm, Type type) { + Sets set = setByType(type); + if (!set.values.any((element) => element is RealmObject || element is RealmValue)) { + return set.values; + } + + return set.values.map((value) { + if (value is RealmValue && value.value is! RealmObject) { + return value; + } + + return _getManagedValue(set, realm, value); + }).toList(); + } + + Object? _getManagedValue(Sets set, Realm realm, Object? value) { + if (value is RealmValue) { + return RealmValue.from(_getManagedValue(set, realm, value.value)); + } + + RealmObject? realmValue = set.getRealmObject!(realm, value as RealmObject); + return realmValue; + } +} + +class Sets { + final RealmSet set; + final List values; + RealmObject? Function(Realm realm, RealmObject value)? getRealmObject = (realm, value) => value; + + Sets(this.set, this.values, [this.getRealmObject]); +} + +Future main([List? args]) async { + await setupTests(args); + + for (var type in supportedTypes) { + test('RealmSet<$type> unmanged set add', () { + final testSet = TestRealmSets(1); + final set = testSet.setByType(type).set; + final values = testSet.values(type); + + set.add(values.first); + expect(set.length, equals(1)); + expect(set.contains(values.first), true); + }); + + test('RealmSet<$type> unmanged set remove', () { + final testSet = TestRealmSets(1); + final set = testSet.setByType(type).set; + final values = testSet.values(type); + + set.add(values.first); + expect(set.length, equals(1)); + expect(set.contains(values.first), true); + + set.remove(values.first); + expect(set.length, equals(0)); + expect(set.contains(values.first), false); + }); + + test('RealmSet<$type>.elementAt on an unmanged RealmSet', () { + final testSet = TestRealmSets(1); + final set = testSet.setByType(type).set; + final values = testSet.values(type); + + set.add(values.first); + expect(set.length, equals(1)); + expect(set.elementAt(0), values.first); + }); + + test('RealmSet<$type> creation', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + + realm.write(() { + realm.add(testSet); + }); + + expect(realm.find(1), isNotNull); + + testSet = realm.find(1)!; + var set = testSet.setByType(type).set; + + expect(set.length, equals(0)); + }); + + test('RealmSet<$type> create from unmanaged', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + for (var value in values) { + set.add(value); + } + + realm.write(() { + realm.add(testSet); + }); + + testSet = realm.find(1)!; + set = testSet.setByType(type).set; + expect(set.length, equals(values.length)); + values = testSet.getValuesOrManagedValues(realm, type); + + for (var value in values) { + expect(set.contains(value), true); + } + }); + + test('RealmSet<$type> contains', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + realm.write(() { + realm.add(testSet); + }); + + set = testSet.setByType(type).set; + + expect(set.contains(values.first), false); + + realm.write(() { + set.add(values.first); + }); + + testSet = realm.find(1)!; + set = testSet.setByType(type).set; + expect(set.contains(values.first), true); + }); + + test('RealmSet<$type> add', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + + var values = testSet.values(type); + + realm.write(() { + realm.add(testSet); + var set = testSet.setByType(type).set; + expect(set.add(values.first), true); + + //adding an already existing value is a no operation + expect(set.add(values.first), false); + }); + + var set = testSet.setByType(type).set; + // values = testSet.values(type); + values = testSet.getValuesOrManagedValues(realm, type); + + expect(set.contains(values.first), true); + }); + + test('RealmSet<$type> remove', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + + realm.write(() { + realm.add(testSet); + }); + + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + realm.write(() { + set.add(values.first); + }); + + expect(set.length, 1); + + realm.write(() { + expect(set.remove(values.first), true); + + //removing a value not in the set should return false. + expect(set.remove(values.first), false); + }); + + expect(set.length, 0); + }); + + test('RealmSet<$type> length', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + realm.write(() { + realm.add(testSet); + }); + + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + expect(set.length, 0); + + realm.write(() { + set.add(values.first); + }); + + expect(set.length, 1); + + realm.write(() { + for (var value in values) { + set.add(value); + } + }); + + expect(set.length, values.length); + }); + + test('RealmSet<$type> elementAt', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + var values = testSet.values(type); + + realm.write(() { + realm.add(testSet); + var set = testSet.setByType(type).set; + set.add(values.first); + }); + + var set = testSet.setByType(type).set; + + expect(() => set.elementAt(-1), throws("Index out of range")); + expect(() => set.elementAt(800), throws("Requested index 800 greater than max 0")); + expect(set.elementAt(0), values[0]); + }); + + test('RealmSet<$type> lookup', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + realm.write(() { + realm.add(testSet); + }); + + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + expect(set.lookup(values.first), null); + + realm.write(() { + set.add(values.first); + }); + + expect(set.lookup(values.first), values.first); + }); + + test('RealmSet<$type> toSet', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + var set = testSet.setByType(type).set; + var values = testSet.values(type); + set.add(values.first); + + realm.write(() { + realm.add(testSet); + }); + + set = testSet.setByType(type).set; + + final newSet = set.toSet(); + expect(newSet != set, true); + newSet.add(values[1]); + expect(newSet.length, 2); + expect(set.length, 1); + }); + + test('RealmSet<$type> clear', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final testSet = TestRealmSets(1); + var values = testSet.values(type); + + realm.write(() { + realm.add(testSet); + var set = testSet.setByType(type).set; + for (var value in values) { + set.add(value); + } + }); + + var set = testSet.setByType(type).set; + + expect(set.length, values.length); + + realm.write(() { + set.clear(); + }); + + expect(set.length, 0); + }); + + test('RealmSet<$type> iterator', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + var set = testSet.setByType(type).set; + var values = testSet.values(type); + + for (var value in values) { + set.add(value); + } + + realm.write(() { + realm.add(testSet); + }); + + set = testSet.setByType(type).set; + expect(set.length, equals(values.length)); + + for (var element in set) { + expect(values.contains(element), true); + } + }); + + test('RealmSet<$type> notifications', () async { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1); + realm.write(() => realm.add(testSet)); + + var set = testSet.setByType(type).set; + var values = testSet.setByType(type).values; + + var state = 0; + final maxSate = 2; + final subscription = set.changes.listen((changes) { + if (state == 0) { + expect(changes.inserted.isEmpty, true); + expect(changes.modified.isEmpty, true); + expect(changes.deleted.isEmpty, true); + expect(changes.newModified.isEmpty, true); + expect(changes.moved.isEmpty, true); + } else if (state == 1) { + expect(changes.inserted, [0]); //new object at index 0 + expect(changes.modified.isEmpty, true); + expect(changes.deleted.isEmpty, true); + expect(changes.newModified.isEmpty, true); + expect(changes.moved.isEmpty, true); + } else if (state == 2) { + expect(changes.inserted.isEmpty, true); //new object at index 0 + expect(changes.modified.isEmpty, true); + expect(changes.deleted, [0]); + expect(changes.newModified.isEmpty, true); + expect(changes.moved.isEmpty, true); + } + state++; + }); + + await Future.delayed(Duration(milliseconds: 20)); + realm.write(() { + set.add(values.first); + }); + + await Future.delayed(Duration(milliseconds: 20)); + realm.write(() { + set.remove(values.first); + }); + + expect(state, maxSate); + + await Future.delayed(Duration(milliseconds: 20)); + subscription.cancel(); + + await Future.delayed(Duration(milliseconds: 20)); + }); + } + + test('RealmSet deleteAll', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + var testSet = TestRealmSets(1)..objectsSet.addAll([Car("Tesla"), Car("Audi")]); + + realm.write(() { + realm.add(testSet); + }); + + expect(realm.find(1), isNotNull); + + testSet = realm.find(1)!; + expect(testSet.objectsSet.length, 2); + expect(realm.all().length, 2); + + realm.write(() { + testSet.objectsSet.deleteAll(); + }); + + expect(testSet.objectsSet.length, 0); + expect(realm.all().length, 0); + }); + + test('RealmSet add a set of already managed objects', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + realm.write(() { + realm.addAll([Car("Tesla"), Car("Audi")]); + }); + + var testSet = TestRealmSets(1)..objectsSet.addAll(realm.all()); + + realm.write(() { + realm.add(testSet); + }); + + expect(realm.find(1), isNotNull); + + testSet = realm.find(1)!; + expect(testSet.objectsSet.length, 2); + expect(realm.all().length, 2); + }); +} diff --git a/test/realm_set_test.g.dart b/test/realm_set_test.g.dart new file mode 100644 index 000000000..c5000b1f1 --- /dev/null +++ b/test/realm_set_test.g.dart @@ -0,0 +1,265 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'realm_set_test.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { + Car( + String make, + ) { + RealmObjectBase.set(this, 'make', make); + } + + Car._(); + + @override + String get make => RealmObjectBase.get(this, 'make') as String; + @override + set make(String value) => RealmObjectBase.set(this, 'make', 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), + ]); + } +} + +class TestRealmSets extends _TestRealmSets + with RealmEntity, RealmObjectBase, RealmObject { + TestRealmSets( + int key, { + Set boolSet = const {}, + Set intSet = const {}, + Set stringSet = const {}, + Set doubleSet = const {}, + Set dateTimeSet = const {}, + Set objectIdSet = const {}, + Set uuidSet = const {}, + Set mixedSet = const {}, + Set objectsSet = const {}, + Set nullableBoolSet = const {}, + Set nullableIntSet = const {}, + Set nullableStringSet = const {}, + Set nullableDoubleSet = const {}, + Set nullableDateTimeSet = const {}, + Set nullableObjectIdSet = const {}, + Set nullableUuidSet = const {}, + }) { + RealmObjectBase.set(this, 'key', key); + RealmObjectBase.set>( + this, 'boolSet', RealmSet(boolSet)); + RealmObjectBase.set>(this, 'intSet', RealmSet(intSet)); + RealmObjectBase.set>( + this, 'stringSet', RealmSet(stringSet)); + RealmObjectBase.set>( + this, 'doubleSet', RealmSet(doubleSet)); + RealmObjectBase.set>( + this, 'dateTimeSet', RealmSet(dateTimeSet)); + RealmObjectBase.set>( + this, 'objectIdSet', RealmSet(objectIdSet)); + RealmObjectBase.set>( + this, 'uuidSet', RealmSet(uuidSet)); + RealmObjectBase.set>( + this, 'mixedSet', RealmSet(mixedSet)); + RealmObjectBase.set>( + this, 'objectsSet', RealmSet(objectsSet)); + RealmObjectBase.set>( + this, 'nullableBoolSet', RealmSet(nullableBoolSet)); + RealmObjectBase.set>( + this, 'nullableIntSet', RealmSet(nullableIntSet)); + RealmObjectBase.set>( + this, 'nullableStringSet', RealmSet(nullableStringSet)); + RealmObjectBase.set>( + this, 'nullableDoubleSet', RealmSet(nullableDoubleSet)); + RealmObjectBase.set>( + this, 'nullableDateTimeSet', RealmSet(nullableDateTimeSet)); + RealmObjectBase.set>( + this, 'nullableObjectIdSet', RealmSet(nullableObjectIdSet)); + RealmObjectBase.set>( + this, 'nullableUuidSet', RealmSet(nullableUuidSet)); + } + + TestRealmSets._(); + + @override + int get key => RealmObjectBase.get(this, 'key') as int; + @override + set key(int value) => RealmObjectBase.set(this, 'key', value); + + @override + RealmSet get boolSet => + RealmObjectBase.get(this, 'boolSet') as RealmSet; + @override + set boolSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get intSet => + RealmObjectBase.get(this, 'intSet') as RealmSet; + @override + set intSet(covariant RealmSet value) => throw RealmUnsupportedSetError(); + + @override + RealmSet get stringSet => + RealmObjectBase.get(this, 'stringSet') as RealmSet; + @override + set stringSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get doubleSet => + RealmObjectBase.get(this, 'doubleSet') as RealmSet; + @override + set doubleSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get dateTimeSet => + RealmObjectBase.get(this, 'dateTimeSet') as RealmSet; + @override + set dateTimeSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get objectIdSet => + RealmObjectBase.get(this, 'objectIdSet') as RealmSet; + @override + set objectIdSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get uuidSet => + RealmObjectBase.get(this, 'uuidSet') as RealmSet; + @override + set uuidSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get mixedSet => + RealmObjectBase.get(this, 'mixedSet') as RealmSet; + @override + set mixedSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get objectsSet => + RealmObjectBase.get(this, 'objectsSet') as RealmSet; + @override + set objectsSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableBoolSet => + RealmObjectBase.get(this, 'nullableBoolSet') as RealmSet; + @override + set nullableBoolSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableIntSet => + RealmObjectBase.get(this, 'nullableIntSet') as RealmSet; + @override + set nullableIntSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableStringSet => + RealmObjectBase.get(this, 'nullableStringSet') + as RealmSet; + @override + set nullableStringSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableDoubleSet => + RealmObjectBase.get(this, 'nullableDoubleSet') + as RealmSet; + @override + set nullableDoubleSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableDateTimeSet => + RealmObjectBase.get(this, 'nullableDateTimeSet') + as RealmSet; + @override + set nullableDateTimeSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableObjectIdSet => + RealmObjectBase.get(this, 'nullableObjectIdSet') + as RealmSet; + @override + set nullableObjectIdSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + RealmSet get nullableUuidSet => + RealmObjectBase.get(this, 'nullableUuidSet') as RealmSet; + @override + set nullableUuidSet(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + TestRealmSets freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(TestRealmSets._); + return const SchemaObject( + ObjectType.realmObject, TestRealmSets, 'TestRealmSets', [ + SchemaProperty('key', RealmPropertyType.int, primaryKey: true), + SchemaProperty('boolSet', RealmPropertyType.bool, + collectionType: RealmCollectionType.set), + SchemaProperty('intSet', RealmPropertyType.int, + collectionType: RealmCollectionType.set), + SchemaProperty('stringSet', RealmPropertyType.string, + collectionType: RealmCollectionType.set), + SchemaProperty('doubleSet', RealmPropertyType.double, + collectionType: RealmCollectionType.set), + SchemaProperty('dateTimeSet', RealmPropertyType.timestamp, + collectionType: RealmCollectionType.set), + SchemaProperty('objectIdSet', RealmPropertyType.objectid, + collectionType: RealmCollectionType.set), + SchemaProperty('uuidSet', RealmPropertyType.uuid, + collectionType: RealmCollectionType.set), + SchemaProperty('mixedSet', RealmPropertyType.mixed, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('objectsSet', RealmPropertyType.object, + linkTarget: 'Car', collectionType: RealmCollectionType.set), + SchemaProperty('nullableBoolSet', RealmPropertyType.bool, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableIntSet', RealmPropertyType.int, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableStringSet', RealmPropertyType.string, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableDoubleSet', RealmPropertyType.double, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableDateTimeSet', RealmPropertyType.timestamp, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableObjectIdSet', RealmPropertyType.objectid, + optional: true, collectionType: RealmCollectionType.set), + SchemaProperty('nullableUuidSet', RealmPropertyType.uuid, + optional: true, collectionType: RealmCollectionType.set), + ]); + } +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 49db5ccac..7aa98bb1b 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -42,7 +42,9 @@ class _Stuff { int i = 42; } -void main() { +Future main([List? args]) async { + await setupTests(args); + group('RealmValue', () { final now = DateTime.now().toUtc(); final values = [ @@ -58,11 +60,10 @@ void main() { Uuid.v4(), ]; - final config = Configuration.inMemory([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); - final realm = getRealm(config); - for (final x in values) { test('Roundtrip ${x.runtimeType} $x', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); final something = realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(x)))); expect(something.oneAny.type, x.runtimeType); expect(something.oneAny.value, x); @@ -71,10 +72,14 @@ void main() { } test('Illegal value', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from([1, 2])))), throwsArgumentError); }); test('Embedded object not allowed in RealmValue', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from(TuckedIn())))), throwsArgumentError); }); @@ -198,10 +203,12 @@ void main() { Uuid.v4(), ]; - final config = Configuration.inMemory([AnythingGoes.schema, Stuff.schema]); + final config = Configuration.local([AnythingGoes.schema, Stuff.schema]); final realm = getRealm(config); test('Roundtrip', () { + final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); + final realm = getRealm(config); final something = realm.write(() => realm.add(AnythingGoes(manyAny: values.map(RealmValue.from)))); expect(something.manyAny.map((e) => e.value), values); expect(something.manyAny, values.map(RealmValue.from));