Skip to content

Commit

Permalink
RDART-969: Keypath filtering on collections (#1714)
Browse files Browse the repository at this point in the history
* Added basic tests for collections keypath filtering

* Various fixes

* Basic tests

* Added basic tests

* Added fix for realm objects collections

* Added changelog and added ticket

* Clean up

* Small correction

* Apply suggestions from code review

Co-authored-by: Kasper Overgård Nielsen <kasper.nielsen@mongodb.com>

* Fixed changelog

* Avoiding calling native for classkey

* Simplified code

* Various fixes

* Small fix

* Added changesFor to extension methods

* Corrected error

---------

Co-authored-by: Kasper Overgård Nielsen <kasper.nielsen@mongodb.com>
papafe and nielsenko authored Jun 14, 2024
1 parent 510a4fa commit 16f34e5
Showing 26 changed files with 1,470 additions and 115 deletions.
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -50,9 +50,23 @@

### Enhancements
* Report the originating error that caused a client reset to occur. (Core 14.9.0)
* Allow the realm package, and code generated by realm_generator to be included when building
* Allow the realm package, and code generated by realm_generator to be included when building
for web without breaking compilation. (Issue [#1374](https://github.com/realm/realm-dart/issues/1374),
PR [#1713](https://github.com/realm/realm-dart/pull/1713)). This does **not** imply that realm works on web!
* Added support for specifying key paths when listening to notifications on a collection with the `changesFor([List<String>? keyPaths])` method. Available on `RealmResults`, `RealmList`, `RealmSet`, and `RealmMap`. The key paths indicates what properties should raise a notification, if changed either directly or transitively.
```dart
@RealmModel()
class _Person {
late String name;
late int age;
late List<_Person> friends;
}
// ....
// Only changes to "age" or "friends" of any of the elements of the collection, together with changes to the collection itself, will raise a notification
realm.all<Person>().changesFor(["age", "friends"]).listen( .... )
```

### Fixed
* `Realm.writeAsync` did not handle async callbacks (`Future<T> Function()`) correctly. (Issue [#1667](https://github.com/realm/realm-dart/issues/1667))
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/list_handle.dart
Original file line number Diff line number Diff line change
@@ -28,5 +28,5 @@ abstract interface class ListHandle extends HandleBase {
void removeAt(int index);
ListHandle? resolveIn(RealmHandle frozenRealm);
ObjectHandle setEmbeddedAt(int index);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/map_handle.dart
Original file line number Diff line number Diff line change
@@ -27,5 +27,5 @@ abstract interface class MapHandle extends HandleBase {
ResultsHandle query(String query, List<Object?> args);
bool remove(String key);
MapHandle? resolveIn(RealmHandle frozenRealm);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
26 changes: 15 additions & 11 deletions packages/realm_dart/lib/src/handles/native/list_handle.dart
Original file line number Diff line number Diff line change
@@ -147,16 +147,20 @@ class ListHandle extends CollectionHandleBase<realm_list> implements intf.ListHa
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller) {
return NotificationTokenHandle(
realmLib.realm_list_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
nullptr,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((Arena arena) {
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);

return NotificationTokenHandle(
realmLib.realm_list_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
kpNative,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
});
}
}
26 changes: 15 additions & 11 deletions packages/realm_dart/lib/src/handles/native/map_handle.dart
Original file line number Diff line number Diff line change
@@ -178,17 +178,21 @@ class MapHandle extends CollectionHandleBase<realm_dictionary> implements intf.M
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller) {
return NotificationTokenHandle(
realmLib.realm_dictionary_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
nullptr,
Pointer.fromFunction(_mapChangeCallback),
),
root,
);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((Arena arena) {
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);

return NotificationTokenHandle(
realmLib.realm_dictionary_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
kpNative,
Pointer.fromFunction(_mapChangeCallback),
),
root,
);
});
}
}

24 changes: 2 additions & 22 deletions packages/realm_dart/lib/src/handles/native/object_handle.dart
Original file line number Diff line number Diff line change
@@ -136,9 +136,9 @@ class ObjectHandle extends RootedHandleBase<realm_object> implements intf.Object
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List<String>? keyPaths]) {
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((arena) {
final kpNative = buildAndVerifyKeyPath(keyPaths);
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);
return NotificationTokenHandle(
realmLib.realm_object_add_notification_callback(
pointer,
@@ -152,26 +152,6 @@ class ObjectHandle extends RootedHandleBase<realm_object> implements intf.Object
});
}

Pointer<realm_key_path_array> buildAndVerifyKeyPath(List<String>? keyPaths) {
return using((arena) {
if (keyPaths == null) {
return nullptr;
}

final length = keyPaths.length;
final keypathsNative = arena<Pointer<Char>>(length);

for (int i = 0; i < length; i++) {
keypathsNative[i] = keyPaths[i].toCharPtr(arena);
}
// TODO(kn):
// call to classKey getter involves a native call, which is not ideal
return realmLib.realm_create_key_path_array(root.pointer, classKey, length, keypathsNative).raiseLastErrorIfNull();
});
}

void verifyKeyPath(List<String>? keyPaths) => buildAndVerifyKeyPath(keyPaths);

@override
// equals handled by HandleBase<T>
// ignore: hash_and_equals
27 changes: 25 additions & 2 deletions packages/realm_dart/lib/src/handles/native/realm_handle.dart
Original file line number Diff line number Diff line change
@@ -42,9 +42,9 @@ class RealmHandle extends HandleBase<shared_realm> implements intf.RealmHandle {
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}

final configHandle = ConfigHandle.from(config);

return RealmHandle(realmLib
.realm_open(configHandle.pointer) //
.raiseLastErrorIfNull());
@@ -478,6 +478,29 @@ class RealmHandle extends HandleBase<shared_realm> implements intf.RealmHandle {
}
return result;
}

Pointer<realm_key_path_array> buildAndVerifyKeyPath(List<String>? keyPaths, int? classKey) {
if (keyPaths == null || classKey == null) {
return nullptr;
}

if (keyPaths.any((element) => element.isEmpty || element.trim().isEmpty)) {
throw RealmException("None of the key paths provided can be empty or consisting only of white spaces");
}

return using((arena) {
final length = keyPaths.length;
final keypathsNative = arena<Pointer<Char>>(length);
for (int i = 0; i < length; i++) {
keypathsNative[i] = keyPaths[i].toCharPtr(arena);
}

return realmLib.realm_create_key_path_array(pointer, classKey, length, keypathsNative).raiseLastErrorIfNull();
});
}

@override
void verifyKeyPath(List<String>? keyPaths, int? classKey) => buildAndVerifyKeyPath(keyPaths, classKey);
}

class CallbackTokenHandle extends RootedHandleBase<realm_callback_token> implements intf.CallbackTokenHandle {
25 changes: 14 additions & 11 deletions packages/realm_dart/lib/src/handles/native/results_handle.dart
Original file line number Diff line number Diff line change
@@ -114,16 +114,19 @@ class ResultsHandle extends RootedHandleBase<realm_results> implements intf.Resu
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller) {
return NotificationTokenHandle(
realmLib.realm_results_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
nullptr,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((Arena arena) {
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);
return NotificationTokenHandle(
realmLib.realm_results_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
kpNative,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
});
}
}
25 changes: 14 additions & 11 deletions packages/realm_dart/lib/src/handles/native/set_handle.dart
Original file line number Diff line number Diff line change
@@ -130,16 +130,19 @@ class SetHandle extends RootedHandleBase<realm_set> implements intf.SetHandle {
}

@override
NotificationTokenHandle subscribeForNotifications(NotificationsController controller) {
return NotificationTokenHandle(
realmLib.realm_set_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
nullptr,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey) {
return using((Arena arena) {
final kpNative = root.buildAndVerifyKeyPath(keyPaths, classKey);
return NotificationTokenHandle(
realmLib.realm_set_add_notification_callback(
pointer,
controller.toPersistentHandle(),
realmLib.addresses.realm_dart_delete_persistent_handle,
kpNative,
Pointer.fromFunction(collectionChangeCallback),
),
root,
);
});
}
}
4 changes: 1 addition & 3 deletions packages/realm_dart/lib/src/handles/object_handle.dart
Original file line number Diff line number Diff line change
@@ -38,9 +38,7 @@ abstract interface class ObjectHandle extends HandleBase {

ObjectHandle? resolveIn(RealmHandle frozenRealm);

NotificationTokenHandle subscribeForNotifications(NotificationsController controller, [List<String>? keyPaths]);

void verifyKeyPath(List<String>? keyPaths);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);

@override
// equals handled by HandleBase<T>
2 changes: 2 additions & 0 deletions packages/realm_dart/lib/src/handles/realm_handle.dart
Original file line number Diff line number Diff line change
@@ -64,6 +64,8 @@ abstract interface class RealmHandle extends HandleBase {
Map<String, RealmPropertyMetadata> getPropertiesMetadata(int classKey, String? primaryKeyName);

RealmObjectMetadata getObjectMetadata(SchemaObject schema);

void verifyKeyPath(List<String> keyPaths, int? classKey);
}

abstract class CallbackTokenHandle extends HandleBase {}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/results_handle.dart
Original file line number Diff line number Diff line change
@@ -24,5 +24,5 @@ abstract interface class ResultsHandle extends HandleBase {
ResultsHandle resolveIn(RealmHandle realmHandle);

Object? elementAt(Realm realm, int index);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
2 changes: 1 addition & 1 deletion packages/realm_dart/lib/src/handles/set_handle.dart
Original file line number Diff line number Diff line change
@@ -30,5 +30,5 @@ abstract interface class SetHandle extends HandleBase {

SetHandle? resolveIn(RealmHandle frozenRealm);

NotificationTokenHandle subscribeForNotifications(NotificationsController controller);
NotificationTokenHandle subscribeForNotifications(NotificationsController controller, List<String>? keyPaths, int? classKey);
}
34 changes: 29 additions & 5 deletions packages/realm_dart/lib/src/list.dart
Original file line number Diff line number Diff line change
@@ -195,11 +195,18 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
RealmResults<T> asResults() => RealmResultsInternal.create<T>(handle.asResults(), realm, metadata);

@override
Stream<RealmListChanges<T>> get changes {
Stream<RealmListChanges<T>> get changes => _changesFor(null);

Stream<RealmListChanges<T>> _changesFor([List<String>? keyPaths]) {
if (isFrozen) {
throw RealmStateError('List is frozen and cannot emit changes');
}
final controller = ListNotificationsController<T>(asManaged());

if (keyPaths != null && _metadata == null) {
throw RealmStateError('Key paths can be used only with collections of Realm objects');
}

final controller = ListNotificationsController<T>(asManaged(), keyPaths);
return controller.createStream();
}
}
@@ -238,7 +245,7 @@ class UnmanagedRealmList<T extends Object?> extends collection.DelegatingList<T>
int get hashCode => _base.hashCode;
}

// The query operations on lists, only work for list of objects (core restriction),
// Query operations and keypath filtering on lists only work for list of objects (core restriction),
// so we add these as an extension methods to allow the compiler to prevent misuse.
extension RealmListOfObject<T extends RealmObjectBase> on RealmList<T> {
/// Filters the list and returns a new [RealmResults] according to the provided [query] (with optional [arguments]).
@@ -250,6 +257,17 @@ extension RealmListOfObject<T extends RealmObjectBase> on RealmList<T> {
final handle = asManaged().handle.query(query, arguments);
return RealmResultsInternal.create<T>(handle, realm, _metadata);
}

/// Allows listening for changes when the contents of this collection changes on one of the provided [keyPaths].
/// If [keyPaths] is null, default notifications will be raised (same as [RealmList.change]).
/// If [keyPaths] is an empty list, only notifications related to the collection itself will be raised (such as adding or removing elements).
Stream<RealmListChanges<T>> changesFor([List<String>? keyPaths]) {
if (!isManaged) {
throw RealmStateError("Unmanaged lists don't support changes");
}

return (this as ManagedRealmList<T>)._changesFor(keyPaths);
}
}

/// @nodoc
@@ -323,12 +341,18 @@ class RealmListChanges<T extends Object?> extends RealmCollectionChanges {
class ListNotificationsController<T extends Object?> extends NotificationsController {
final ManagedRealmList<T> list;
late final StreamController<RealmListChanges<T>> streamController;
List<String>? keyPaths;

ListNotificationsController(this.list);
ListNotificationsController(this.list, [List<String>? keyPaths]) {
if (keyPaths != null) {
this.keyPaths = keyPaths;
list.realm.handle.verifyKeyPath(keyPaths, list._metadata?.classKey);
}
}

@override
NotificationTokenHandle subscribe() {
return list.handle.subscribeForNotifications(this);
return list.handle.subscribeForNotifications(this, keyPaths, list._metadata?.classKey);
}

Stream<RealmListChanges<T>> createStream() {
Loading

0 comments on commit 16f34e5

Please sign in to comment.