From 553740fda103127d1ba573d9aeced760fe4e2b9b Mon Sep 17 00:00:00 2001 From: Desislava Stefanova <95419820+desistefanova@users.noreply.github.com> Date: Thu, 3 Aug 2023 18:40:36 +0300 Subject: [PATCH] Query lists arguments (#1346) * Query list parameter implemented Fixes #755 Fixes #1084 --- CHANGELOG.md | 3 + lib/src/list.dart | 2 +- lib/src/native/realm_core.dart | 98 +++++++++++++++++--------- lib/src/realm_class.dart | 2 +- lib/src/set.dart | 16 +++++ test/embedded_test.dart | 17 +++++ test/list_test.dart | 30 ++++++++ test/realm_set_test.dart | 31 +++++++++ test/realm_set_test.g.dart | 12 +++- test/realm_value_test.dart | 30 +++++++- test/results_test.dart | 121 +++++++++++++++++++++++++++++++++ 11 files changed, 322 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf5c96c0..c342f081d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Enhancements * Support ReamSet.freeze() ([#1342](https://github.com/realm/realm-dart/pull/1342)) +* Added support for query on `RealmSet`. ([#1346](https://github.com/realm/realm-dart/pull/1346)) +* Support for passing `List`, `Set` or `Iterable` arguments to queries with `IN`-operators. ([#1346](https://github.com/realm/realm-dart/pull/1346)) + ### Fixed * Fixed an early unlock race condition during client reset callbacks. ([#1335](https://github.com/realm/realm-dart/pull/1335)) diff --git a/lib/src/list.dart b/lib/src/list.dart index 6fecdeba1..7e1e28e88 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -238,7 +238,7 @@ extension RealmListOfObject on RealmList { /// /// 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) - RealmResults query(String query, [List arguments = const []]) { + 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/native/realm_core.dart b/lib/src/native/realm_core.dart index c8b4233bc..d8d924f1e 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -1101,7 +1101,7 @@ class _RealmCore { } } - RealmResultsHandle queryList(RealmList target, String query, List args) { + RealmResultsHandle queryList(RealmList target, String query, List args) { return using((arena) { final length = args.length; final argsPointer = arena(length); @@ -1122,6 +1122,27 @@ class _RealmCore { }); } + RealmResultsHandle querySet(RealmSet 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 queryHandle = _RealmQueryHandle._( + _realmLib.invokeGetPointer( + () => _realmLib.realm_query_parse_for_set( + target.handle._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); @@ -2893,7 +2914,7 @@ extension _RealmLibraryEx on RealmLibrary { Pointer _toRealmValue(Object? value, Allocator allocator) { final realm_value = allocator(); - _intoRealmValue(value, realm_value, allocator); + _intoRealmValue(value, realm_value.ref, allocator); return realm_value; } @@ -2901,51 +2922,62 @@ const int _microsecondsPerSecond = 1000 * 1000; const int _nanosecondsPerMicrosecond = 1000; void _intoRealmQueryArg(Object? value, Pointer realm_query_arg, Allocator allocator) { - realm_query_arg.ref.arg = allocator(); - realm_query_arg.ref.nb_args = 1; - realm_query_arg.ref.is_list = false; - _intoRealmValue(value, realm_query_arg.ref.arg, allocator); + if (value is Iterable) { + realm_query_arg.ref.nb_args = value.length; + realm_query_arg.ref.is_list = true; + realm_query_arg.ref.arg = allocator(value.length); + int i = 0; + for (var item in value) { + _intoRealmValue(item, realm_query_arg.ref.arg[i], allocator); + i++; + } + } else { + realm_query_arg.ref.arg = allocator(); + realm_query_arg.ref.nb_args = 1; + realm_query_arg.ref.is_list = false; + _intoRealmValue(value, realm_query_arg.ref.arg.ref, allocator); + } } -void _intoRealmValue(Object? value, Pointer realm_value, Allocator allocator) { +void _intoRealmValue(Object? value, realm_value realm_value, Allocator allocator) { if (value == null) { - realm_value.ref.type = realm_value_type.RLM_TYPE_NULL; + realm_value.type = realm_value_type.RLM_TYPE_NULL; } else if (value is RealmObjectBase) { // when converting a RealmObjectBase to realm_value.link we assume the object is managed final link = realmCore._getObjectAsLink(value); - realm_value.ref.values.link.target = link.targetKey; - realm_value.ref.values.link.target_table = link.classKey; - realm_value.ref.type = realm_value_type.RLM_TYPE_LINK; + realm_value.values.link.target = link.targetKey; + realm_value.values.link.target_table = link.classKey; + realm_value.type = realm_value_type.RLM_TYPE_LINK; } else if (value is int) { - realm_value.ref.values.integer = value; - realm_value.ref.type = realm_value_type.RLM_TYPE_INT; + realm_value.values.integer = value; + realm_value.type = realm_value_type.RLM_TYPE_INT; } else if (value is bool) { - realm_value.ref.values.boolean = value; - realm_value.ref.type = realm_value_type.RLM_TYPE_BOOL; + realm_value.values.boolean = value; + realm_value.type = realm_value_type.RLM_TYPE_BOOL; } else if (value is String) { String string = value; final units = utf8.encode(string); final result = allocator(units.length); final Uint8List nativeString = result.asTypedList(units.length); nativeString.setAll(0, units); - realm_value.ref.values.string.data = result.cast(); - realm_value.ref.values.string.size = units.length; - realm_value.ref.type = realm_value_type.RLM_TYPE_STRING; + realm_value.values.string.data = result.cast(); + realm_value.values.string.size = units.length; + realm_value.type = realm_value_type.RLM_TYPE_STRING; } else if (value is double) { - realm_value.ref.values.dnum = value; - realm_value.ref.type = realm_value_type.RLM_TYPE_DOUBLE; + realm_value.values.dnum = value; + realm_value.type = realm_value_type.RLM_TYPE_DOUBLE; } else if (value is ObjectId) { final bytes = value.bytes; for (var i = 0; i < 12; i++) { - realm_value.ref.values.object_id.bytes[i] = bytes[i]; + realm_value.values.object_id.bytes[i] = bytes[i]; } - realm_value.ref.type = realm_value_type.RLM_TYPE_OBJECT_ID; + realm_value.type = realm_value_type.RLM_TYPE_OBJECT_ID; } else if (value is Uuid) { final bytes = value.bytes.asUint8List(); for (var i = 0; i < 16; i++) { - realm_value.ref.values.uuid.bytes[i] = bytes[i]; + realm_value.values.uuid.bytes[i] = bytes[i]; } - realm_value.ref.type = realm_value_type.RLM_TYPE_UUID; + realm_value.type = realm_value_type.RLM_TYPE_UUID; } else if (value is DateTime) { final microseconds = value.toUtc().microsecondsSinceEpoch; final seconds = microseconds ~/ _microsecondsPerSecond; @@ -2953,19 +2985,19 @@ void _intoRealmValue(Object? value, Pointer realm_value, Allocato if (microseconds < 0 && nanoseconds != 0) { nanoseconds = nanoseconds - _nanosecondsPerMicrosecond * _microsecondsPerSecond; } - realm_value.ref.values.timestamp.seconds = seconds; - realm_value.ref.values.timestamp.nanoseconds = nanoseconds; - realm_value.ref.type = realm_value_type.RLM_TYPE_TIMESTAMP; + realm_value.values.timestamp.seconds = seconds; + realm_value.values.timestamp.nanoseconds = nanoseconds; + realm_value.type = realm_value_type.RLM_TYPE_TIMESTAMP; } else if (value is RealmValue) { return _intoRealmValue(value.value, realm_value, allocator); } else if (value is Decimal128) { - realm_value.ref.values.decimal128 = value.value; - realm_value.ref.type = realm_value_type.RLM_TYPE_DECIMAL128; + realm_value.values.decimal128 = value.value; + realm_value.type = realm_value_type.RLM_TYPE_DECIMAL128; } else if (value is Uint8List) { - realm_value.ref.type = realm_value_type.RLM_TYPE_BINARY; - realm_value.ref.values.binary.size = value.length; - realm_value.ref.values.binary.data = allocator(value.length); - realm_value.ref.values.binary.data.asTypedList(value.length).setAll(0, value); + realm_value.type = realm_value_type.RLM_TYPE_BINARY; + realm_value.values.binary.size = value.length; + realm_value.values.binary.data = allocator(value.length); + realm_value.values.binary.data.asTypedList(value.length).setAll(0, value); } else { throw RealmException("Property type ${value.runtimeType} not supported"); } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index 8dc6743f0..c016da5c0 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -92,7 +92,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 'set.dart' show RealmSet, RealmSetChanges, RealmSetOfObject; export 'migration.dart' show Migration; export 'realm_object.dart' show diff --git a/lib/src/set.dart b/lib/src/set.dart index ed2cccae2..4be7c6d17 100644 --- a/lib/src/set.dart +++ b/lib/src/set.dart @@ -366,3 +366,19 @@ class RealmSetNotificationsController extends NotificationsCo streamController.addError(error); } } + +// 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. +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) + RealmResults query(String query, [List arguments = const []]) { + final handle = realmCore.querySet(asManaged(), query, arguments); + return RealmResultsInternal.create(handle, realm, _metadata); + } +} diff --git a/test/embedded_test.dart b/test/embedded_test.dart index 9162dc684..656c52180 100644 --- a/test/embedded_test.dart +++ b/test/embedded_test.dart @@ -16,6 +16,8 @@ // //////////////////////////////////////////////////////////////////////////////// +import 'dart:typed_data'; + import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; @@ -850,6 +852,21 @@ Future main([List? args]) async { expect(parent.recursiveList[0].parent, null); }); + + test('Query embedded objects list with list argument with different type of values', () { + final realm = getLocalRealm(); + final realmObject = realm.write(() { + return realm.add(ObjectWithEmbedded('', list: [ + AllTypesEmbedded('text1', false, DateTime.now(), 1.1, ObjectId(), Uuid.v4(), 1, Decimal128.one, nullableDecimalProp: Decimal128.fromDouble(3.3)), + AllTypesEmbedded('text2', true, DateTime.now(), 2.2, ObjectId(), Uuid.v4(), 2, Decimal128.ten), + AllTypesEmbedded('text3', true, DateTime.now(), 3.3, ObjectId(), Uuid.v4(), 3, Decimal128.infinity), + ])); + }); + final results = realmObject.list.query(r"nullableDecimalProp IN $0 || stringProp IN $0", [ + ['text1', null, 2.2, 3] // Searching by different type of values and null + ]); + expect(results.length, 3); + }); } extension on RealmObjectBase { diff --git a/test/list_test.dart b/test/list_test.dart index 29d1b4183..7644396aa 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -1238,4 +1238,34 @@ Future main([List? args]) async { realm.write(() => team.players.clear()); expect(playersAsResults.length, 0); }); + + test('Query on RealmList with IN-operator', () { + final config = Configuration.local([Team.schema, Person.schema]); + final realm = getRealm(config); + + final team = realm.write(() => realm.add(Team('team', players: [ + Person('Paul'), + Person('John'), + Person('Alex'), + ]))); + + final result = team.players.query(r'name IN $0', [ + ['Paul', 'Alex'] + ]); + expect(result.length, 2); + }); + + test('Query on RealmList allows null in arguments', () { + final config = Configuration.local([School.schema, Student.schema]); + final realm = getRealm(config); + + final school = realm.write(() => realm.add(School('primary school 1', branches: [ + School('131', city: "NY city"), + School('144'), + School('128'), + ]))); + + final result = school.branches.query(r'city = $0', [null]); + expect(result.length, 2); + }); } diff --git a/test/realm_set_test.dart b/test/realm_set_test.dart index e1b9a72a1..ac85116d7 100644 --- a/test/realm_set_test.dart +++ b/test/realm_set_test.dart @@ -70,6 +70,7 @@ List supportedTypes = [ class _Car { @PrimaryKey() late String make; + late String? color; } @RealmModel() @@ -800,4 +801,34 @@ Future main([List? args]) async { if (count > 1) fail('Should only receive one event'); } }); + + test('Query on RealmSet with IN-operator', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final cars = [Car("Tesla"), Car("Ford"), Car("Audi")]; + final testSets = TestRealmSets(1)..objectsSet.addAll(cars); + + realm.write(() { + realm.add(testSets); + }); + final result = testSets.objectsSet.query(r'make IN $0', [ + ['Tesla', 'Audi'] + ]); + expect(result.length, 2); + }); + + test('Query on RealmSet allows null in arguments', () { + var config = Configuration.local([TestRealmSets.schema, Car.schema]); + var realm = getRealm(config); + + final cars = [Car("Tesla", color: "black"), Car("Ford"), Car("Audi")]; + final testSets = TestRealmSets(1)..objectsSet.addAll(cars); + + realm.write(() { + realm.add(testSets); + }); + var result = testSets.objectsSet.query(r'color = $0', [null]); + expect(result.length, 2); + }); } diff --git a/test/realm_set_test.g.dart b/test/realm_set_test.g.dart index 559b7a168..8aea06a0f 100644 --- a/test/realm_set_test.g.dart +++ b/test/realm_set_test.g.dart @@ -8,9 +8,11 @@ part of 'realm_set_test.dart'; class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { Car( - String make, - ) { + String make, { + String? color, + }) { RealmObjectBase.set(this, 'make', make); + RealmObjectBase.set(this, 'color', color); } Car._(); @@ -20,6 +22,11 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { @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); @@ -33,6 +40,7 @@ class Car extends _Car with RealmEntity, RealmObjectBase, RealmObject { RealmObjectBase.registerFactory(Car._); return const SchemaObject(ObjectType.realmObject, Car, 'Car', [ SchemaProperty('make', RealmPropertyType.string, primaryKey: true), + SchemaProperty('color', RealmPropertyType.string, optional: true), ]); } } diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index a1f32e288..0f34ebdcc 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -223,9 +223,6 @@ Future main([List? args]) async { Decimal128.fromInt(128), ]; - 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); @@ -234,4 +231,31 @@ Future main([List? args]) async { expect(something.manyAny, values.map(RealmValue.from)); }); }); + + test('Query with list of realm values in arguments', () { + final now = DateTime.now().toUtc(); + final values = [ + null, + true, + 'text', + 42, + 3.14, + AnythingGoes(), + Stuff(), + now, + ObjectId.fromTimestamp(now), + Uuid.v4(), + Decimal128.fromInt(128), + ]; + final config = Configuration.local([AnythingGoes.schema, Stuff.schema]); + final realm = getRealm(config); + final realmValues = values.map(RealmValue.from); + realm.write(() => realm.add(AnythingGoes(manyAny: realmValues, oneAny: realmValues.last))); + + var results = realm.query("manyAny IN \$0", [values]); + expect(results.first.manyAny, realmValues); + + results = realm.query("oneAny IN \$0", [values]); + expect(results.first.oneAny, realmValues.last); + }); } diff --git a/test/results_test.dart b/test/results_test.dart index d75b75599..829376b8b 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -18,6 +18,8 @@ // ignore_for_file: unused_local_variable +import 'dart:typed_data'; + import 'package:test/test.dart' hide test, throws; import '../lib/realm.dart'; import 'test.dart'; @@ -790,4 +792,123 @@ Future main([List? args]) async { expect(() => realm.all().single, throws('Too many elements')); expect(() => realm.all().single, throws('Too many elements')); }); + + test('Query with argument lists of different types and null', () { + final id_1 = ObjectId(); + final id_2 = ObjectId(); + final uid_1 = Uuid.v4(); + final uid_2 = Uuid.v4(); + final date_1 = DateTime.now().add(const Duration(days: 1)); + final date_2 = DateTime.now().add(const Duration(days: 2)); + final text_1 = generateRandomUnicodeString(); + + final config = Configuration.local([AllTypes.schema]); + Realm realm = getRealm(config); + realm.write(() => realm.addAll([ + AllTypes(text_1, false, DateTime.now(), 1.1, id_1, uid_1, 1, Decimal128.one, binaryProp: Uint8List.fromList([1, 2])), + AllTypes('text2', true, date_1, 2.2, id_2, uid_2, 2, Decimal128.ten), + AllTypes('text3', true, date_2, 3.3, ObjectId(), Uuid.v4(), 3, Decimal128.infinity, binaryProp: Uint8List.fromList([3, 4])), + ])); + + void queryWithListArg(String propName, Object? argument, {int expected = 0}) { + final results = realm.query("$propName IN \$0", [argument]); + expect(results.length, expected); + } + + queryWithListArg("stringProp", [null, text_1, 'text3'], expected: 2); + queryWithListArg("nullableStringProp", [null, 'text2'], expected: 3); + + queryWithListArg("boolProp", [false, true, null], expected: 3); + queryWithListArg("nullableBoolProp", [null], expected: 3); + + queryWithListArg("dateProp", [date_1, null, date_2], expected: 2); + queryWithListArg("nullableDateProp", [null], expected: 3); + + queryWithListArg("doubleProp", [1.1, null, 3.3], expected: 2); + queryWithListArg("nullableDoubleProp", [null], expected: 3); + + queryWithListArg("objectIdProp", [id_1, id_2, null], expected: 2); + queryWithListArg("nullableObjectIdProp", [null], expected: 3); + + queryWithListArg("uuidProp", [uid_1, uid_2, null], expected: 2); + queryWithListArg("nullableUuidProp", [null], expected: 3); + + queryWithListArg("intProp", [1, 2, null], expected: 2); + queryWithListArg("nullableIntProp", [null], expected: 3); + + queryWithListArg("decimalProp", [Decimal128.one, null], expected: 1); + queryWithListArg("nullableDecimalProp", [null], expected: 3); + + queryWithListArg( + "binaryProp", + [ + Uint8List.fromList([1, 2]), + null, + Uint8List(16) + ], + expected: 2); + + queryWithListArg("nullableBinaryProp", [null], expected: 3); + }); + + test('Query with list, sets and iterable arguments', () { + final config = Configuration.local([Person.schema]); + Realm realm = getRealm(config); + realm.write(() => realm.addAll([ + Person('Ani'), + Person('Teddy'), + Person('Poly'), + ])); + + final listOfNames = ['Ani', 'Teddy']; + var result = realm.query(r'name IN $0', [listOfNames]); + expect(result.length, 2); + + final setOfNames = {'Poly', 'Teddy'}; + result = realm.query(r'name IN $0', [setOfNames]); + expect(result.length, 2); + + final iterableNames = result.map((e) => e.name); + result = realm.query(r'name IN $0', [iterableNames]); + expect(result.length, 2); + + result = realm.query(r'name IN $0 || name IN $1 || name IN $2', [listOfNames, setOfNames, iterableNames]); + expect(result.length, 3); + }); + + test('Query with ANY, ALL and NONE operators with iterable arguments', () { + final config = Configuration.local([School.schema, Student.schema]); + final realm = getRealm(config); + + realm.write(() => realm.addAll([ + School('primary school 1', branches: [ + School('131', city: "NY city"), + School('144', city: "Garden city"), + School('128'), + ]), + School('secondary school 1', students: [ + Student(1, name: 'NP'), + Student(2, name: 'KR'), + ]), + ])); + + var result = realm.query(r'ANY $0 IN branches.city', [ + ["NY city"] + ]); + expect(result.length, 1); + expect(result.first.name, 'primary school 1'); + + result = realm.query(r'ALL $0 IN branches.city', [ + ["NY city", "Garden city", null] + ]); + expect(result.length, 1); + expect(result.first.name, 'primary school 1'); + + result = realm.query(r'students.@count > $0 && NONE $1 IN students.name', [ + 0, + {'Non-existing name', null} + ]); + expect(result.length, 1); + expect(result.first.name, 'secondary school 1'); + }); }