From 5ea131072828d89ec92e0f2e0fd1c2f39f44e696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 9 Sep 2022 14:56:34 +0200 Subject: [PATCH] Allow results of primitives. Moves results of objects specific functionality to extension method to avoid misuse. --- .vscode/settings.json | 1 + lib/src/list.dart | 21 +++++---- lib/src/native/realm_core.dart | 15 ++++++- lib/src/realm_class.dart | 4 +- lib/src/results.dart | 82 ++++++++++++++++++++++------------ test/results_test.dart | 12 +++++ test/subscription_test.dart | 14 +++--- 7 files changed, 103 insertions(+), 46 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a4900236..7fda08abe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "dart.lineLength": 160, "cSpell.words": [ "apikeys", + "BEGINSWITH", "bson", "deallocated", "deleter", diff --git a/lib/src/list.dart b/lib/src/list.dart index 98a37dbca..705a99472 100644 --- a/lib/src/list.dart +++ b/lib/src/list.dart @@ -42,6 +42,8 @@ abstract class RealmList with RealmEntityMixin implements Lis /// and it's parent object hasn't been deleted. bool get isValid; + RealmResults get asResults; + factory RealmList._(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => ManagedRealmList._(handle, realm, metadata); factory RealmList(Iterable items) => UnmanagedRealmList(items); @@ -164,6 +166,9 @@ class ManagedRealmList with RealmEntityMixin, ListMixin im final frozenRealm = realm.freeze(); return frozenRealm.resolveList(this)!; } + + @override + RealmResults get asResults => RealmResultsInternal.createFromList(handle, realm, _metadata); } class UnmanagedRealmList extends collection.DelegatingList with RealmEntityMixin implements RealmList { @@ -177,6 +182,9 @@ class UnmanagedRealmList extends collection.DelegatingList @override RealmList freeze() => throw RealmStateError("Unmanaged lists can't be frozen"); + + @override + RealmResults get asResults => throw RealmStateError("Unmanaged lists can't be converted to results"); } // The query operations on lists, as well as the ability to subscribe for notifications, @@ -190,8 +198,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 []]) { - final managedList = asManaged(); - final handle = realmCore.queryList(managedList, query, arguments); + final handle = realmCore.queryList(asManaged, query, arguments); return RealmResultsInternal.create(handle, realm, _metadata); } @@ -200,9 +207,7 @@ extension RealmListOfObject> on RealmList { if (isFrozen) { throw RealmStateError('List is frozen and cannot emit changes'); } - - final managedList = asManaged(); - final controller = ListNotificationsController(managedList); + final controller = ListNotificationsController(asManaged); return controller.createStream(); } } @@ -218,11 +223,11 @@ extension RealmListInternal on RealmList { } } - ManagedRealmList asManaged() => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); + ManagedRealmList get asManaged => this is ManagedRealmList ? this as ManagedRealmList : throw RealmStateError('$this is not managed'); - RealmListHandle get handle => asManaged()._handle; + RealmListHandle get handle => asManaged._handle; - RealmObjectMetadata? get metadata => asManaged()._metadata; + RealmObjectMetadata? get metadata => asManaged._metadata; static RealmList create(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList._(handle, realm, metadata); diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index b1315d60c..3a5255a87 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -795,7 +795,20 @@ class _RealmCore { }); } - RealmObjectHandle getObjectAt(RealmResults results, int index) { + RealmResultsHandle resultsFromList(RealmListHandle listHandle) { + final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_list_to_results(listHandle._pointer)); + return RealmResultsHandle._(pointer); + } + + Object? resultsGetElementAt(RealmResults results, int index) { + return using((Arena arena) { + final realm_value = arena(); + _realmLib.invokeGetBool(() => _realmLib.realm_results_get(results.handle._pointer, index, realm_value)); + return realm_value.toDartValue(results.realm); + }); + } + + RealmObjectHandle resultsGetObjectAt(RealmResults results, int index) { final pointer = _realmLib.invokeGetPointer(() => _realmLib.realm_results_get_object(results.handle._pointer, index)); return RealmObjectHandle._(pointer); } diff --git a/lib/src/realm_class.dart b/lib/src/realm_class.dart index f4e3fa0d7..285593125 100644 --- a/lib/src/realm_class.dart +++ b/lib/src/realm_class.dart @@ -76,7 +76,7 @@ export 'credentials.dart' show Credentials, AuthProviderType, EmailPasswordAuthP export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges; export 'realm_object.dart' show RealmEntityMixin, RealmObjectMixin, RealmException, UserCallbackException, RealmObject, RealmObjectChanges, DynamicRealmObject; export 'realm_property.dart'; -export 'results.dart' show RealmResults, RealmResultsChanges; +export 'results.dart' show RealmResults, RealmResultsChanges, RealmResultsOfObject; export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirection, ProgressMode, SyncProgress, ConnectionStateChange; export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet; export 'user.dart' show User, UserState, UserIdentity; @@ -580,7 +580,7 @@ class DynamicRealm { RealmResults> all(String className) { final metadata = _realm._metadata.getByName(className); final handle = realmCore.findAll(_realm, metadata.key); - return RealmResultsInternal.create(handle, _realm, metadata); + return RealmResultsInternal.create>(handle, _realm, metadata); } /// Fast lookup for a [RealmObject] of type [className] with the specified [primaryKey]. diff --git a/lib/src/results.dart b/lib/src/results.dart index 7376bbe49..9203a84e9 100644 --- a/lib/src/results.dart +++ b/lib/src/results.dart @@ -15,15 +15,17 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// - import 'dart:async'; import 'dart:collection' as collection; import 'dart:ffi'; +import 'package:realm_common/realm_common.dart'; + import 'collections.dart'; import 'native/realm_core.dart'; import 'realm_class.dart'; import 'realm_object.dart'; +import 'type_utils.dart'; /// Instances of this class are live collections and will update as new elements are either /// added to or deleted from the Realm that match the underlying query. @@ -33,7 +35,7 @@ class RealmResults extends collection.IterableBase with Re final RealmObjectMetadata? _metadata; final RealmResultsHandle _handle; - final _supportsSnapshot = [] is List; + final _supportsSnapshot = isSubtype>(); RealmResults._(this._handle, Realm realm, this._metadata) { setRealm(realm); @@ -41,17 +43,13 @@ class RealmResults extends collection.IterableBase with Re /// Returns the element of type `T` at the specified [index]. T operator [](int index) { - final handle = realmCore.getObjectAt(this, index); - return RealmObjectInternal.create(realm, handle, _metadata!); - } - - /// Returns a new [RealmResults] filtered according to the provided query. - /// - /// 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 args = const []]) { - final handle = realmCore.queryResults(this, query, args); - return RealmResultsInternal.create(handle, realm, _metadata); + final meta = _metadata; + if (meta != null) { + final handle = realmCore.resultsGetObjectAt(this, index); + return RealmObjectInternal.create(realm, handle, meta); + } else { + return realmCore.resultsGetElementAt(this, index) as T; + } } /// `true` if the `Results` collection is empty. @@ -73,24 +71,42 @@ class RealmResults extends collection.IterableBase with Re @override int get length => realmCore.getResultsCount(this); - /// Allows listening for changes when the contents of this collection changes. - Stream> get changes { + /// Creates a frozen snapshot of this query. + RealmResults freeze() { if (isFrozen) { - throw RealmStateError('Results are frozen and cannot emit changes'); + return this; } + final frozenRealm = realm.freeze(); + return frozenRealm.resolveResults(this); + } +} - final controller = ResultsNotificationsController(this); - return controller.createStream(); +// The query operations on results, as well as the ability to subscribe for notifications, +// only work for results of objects (core restriction), so we add these as an extension methods +// to allow the compiler to prevent misuse. +extension RealmResultsOfObject on RealmResults { + RealmResults snapshot() { + final handle = realmCore.resultsSnapshot(this); + return RealmResults._(handle, realm, _metadata); } - /// Creates a frozen snapshot of this query. - RealmResults freeze() { + /// Returns a new [RealmResults] filtered according to the provided query. + /// + /// 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 args = const []]) { + final handle = realmCore.queryResults(this, query, args); + return RealmResultsInternal.create(handle, realm, _metadata); + } + + /// Allows listening for changes when the contents of this collection changes. + Stream> get changes { if (isFrozen) { - return this; + throw RealmStateError('Results are frozen and cannot emit changes'); } - final frozenRealm = realm.freeze(); - return frozenRealm.resolveResults(this); + final controller = ResultsNotificationsController(this); + return controller.createStream(); } } @@ -104,11 +120,21 @@ extension RealmResultsInternal on RealmResults { RealmResultsHandle get handle => _handle; - RealmObjectMetadata? get metadata => _metadata; - - static RealmResults create(RealmResultsHandle handle, Realm realm, RealmObjectMetadata? metadata) { - return RealmResults._(handle, realm, metadata); - } + RealmObjectMetadata get metadata => _metadata!; + + static RealmResults create( + RealmResultsHandle handle, + Realm realm, + RealmObjectMetadata? metadata, + ) => + RealmResults._(handle, realm, metadata); + + static RealmResults createFromList( + RealmListHandle handle, + Realm realm, + RealmObjectMetadata? metadata, + ) => + RealmResults._(realmCore.resultsFromList(handle), realm, metadata); } /// Describes the changes in a Realm results collection since the last time the notification callback was invoked. diff --git a/test/results_test.dart b/test/results_test.dart index e099fc922..85e3f7871 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -549,4 +549,16 @@ Future main([List? args]) async { expect(realm.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), isNot([alice, carol])); expect(realm.query("name CONTAINS 'a'").query("name CONTAINS 'l'"), [carol]); }); + + test('Results of primitives', () { + var config = Configuration.local([Player.schema, Game.schema]); + var realm = getRealm(config); + + final scores = [-1, null, 0, 1]; + final alice = Player('Alice', scoresByRound: scores); + realm.write(() => realm.add(alice)); + + expect(alice.scoresByRound, scores); + expect(alice.scoresByRound.asResults, scores); + }); } diff --git a/test/subscription_test.dart b/test/subscription_test.dart index 3995d4b2f..aadabd8d7 100644 --- a/test/subscription_test.dart +++ b/test/subscription_test.dart @@ -295,18 +295,18 @@ Future main([List? args]) async { ObjectId newOid() => ObjectId.fromBytes(randomBytes(12)); - final oids = {}; + final objectIds = {}; const max = 1000; subscriptions.update((mutableSubscriptions) { - oids.addAll([ + objectIds.addAll([ for (int i = 0; i < max; ++i) mutableSubscriptions.add(realm.query(r'_id == $0', [newOid()])).id ]); }); - expect(oids.length, max); // no collisions + expect(objectIds.length, max); // no collisions expect(subscriptions.length, max); for (final sub in subscriptions) { - expect(sub.id, isIn(oids)); + expect(sub.id, isIn(objectIds)); } }); @@ -499,7 +499,7 @@ Future main([List? args]) async { expect(() => realm.write(() => realm.add(Task(ObjectId()))), throws("no flexible sync subscription has been created")); }); - testSubscriptions('Subscription on unqueryable field sould throw', (realm) async { + testSubscriptions('Subscription on non-queryable field should throw', (realm) async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.add(realm.all()); }); @@ -513,7 +513,7 @@ Future main([List? args]) async { isCompleted: false, durationInMinutes: 10, ), - Event(ObjectId(), name: "Some other eveent", isCompleted: true, durationInMinutes: 60), + Event(ObjectId(), name: "Some other event", isCompleted: true, durationInMinutes: 60), ]); }); @@ -550,7 +550,7 @@ Future main([List? args]) async { realm.addAll([ Event(ObjectId(), name: "NPMG Event", isCompleted: true, durationInMinutes: 30), Event(ObjectId(), name: "NPMG Meeting", isCompleted: false, durationInMinutes: 10), - Event(ObjectId(), name: "Some other eveent", isCompleted: true, durationInMinutes: 60), + Event(ObjectId(), name: "Some other event", isCompleted: true, durationInMinutes: 60), ]); });