Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Keypath filtering in notifications #1547

Merged
merged 27 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* None.

### Enhancements
* Added support for keypaths in `asFlow()` methods on objects. This makes it possible to control which properties will trigger change events, including properties on objects below the default nested limit of 4. (Issue [#661](https://github.com/realm/realm-kotlin/issues/661))
* Added support for keypaths in `asFlow()` methods on objects and queries. This makes it possible to control which properties will trigger change events, including properties on objects below the default nested limit of 4. (Issue [#661](https://github.com/realm/realm-kotlin/issues/661))
* Support for experimental K2-compilation with `kotlin.experimental.tryK2=true`. (Issue [#1483](https://github.com/realm/realm-kotlin/issues/1483))

### Fixed
* None.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ expect object RealmInterop {
): RealmObjectPointer?
fun realm_object_delete(obj: RealmObjectPointer)

fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer
fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer
fun realm_object_add_notification_callback(
obj: RealmObjectPointer,
keyPaths: RealmKeyPathArrayPointer?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ actual object RealmInterop {
return realmc.realm_dictionary_is_valid(dictionary.cptr())
}

actual fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
actual fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
val ptr = realmc.jni_realm_create_key_path_array(realm.cptr(), clazz.key, keyPaths.size, keyPaths.toTypedArray())
return LongPointerWrapper(ptr)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ import realm_wrapper.realm_http_header_t
import realm_wrapper.realm_http_request_method
import realm_wrapper.realm_http_request_t
import realm_wrapper.realm_http_response_t
import realm_wrapper.realm_key_path_array_t
import realm_wrapper.realm_link_t
import realm_wrapper.realm_list_t
import realm_wrapper.realm_object_id_t
Expand Down Expand Up @@ -1656,12 +1655,11 @@ actual object RealmInterop {
checkedBooleanResult(realm_wrapper.realm_object_delete(obj.cptr()))
}

actual fun create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
actual fun realm_create_key_paths_array(realm: RealmPointer, clazz: ClassKey, keyPaths: List<String>): RealmKeyPathArrayPointer {
memScoped {
val kps: CPointer<CPointerVarOf<CPointer<ByteVarOf<Byte>>>> = keyPaths.toCStringArray(this)
val kp = allocArray<CPointerVar<realm_key_path_array_t>>(1)
checkedBooleanResult(realm_wrapper.realm_create_key_path_array(realm.cptr(), clazz.key.toUInt(), keyPaths.size, kps, kp))
return CPointerWrapper(kp[0])
val userKeyPaths: CPointer<CPointerVarOf<CPointer<ByteVarOf<Byte>>>> = keyPaths.toCStringArray(this)
val keyPathPointer = realm_wrapper.realm_create_key_path_array(realm.cptr(), clazz.key.toUInt(), keyPaths.size, userKeyPaths)
return CPointerWrapper(keyPathPointer)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/external/core
rorbech marked this conversation as resolved.
Show resolved Hide resolved
Submodule core updated 65 files
+2 −1 .github/pull_request_template.md
+4 −0 .gitignore
+27 −1 CHANGELOG.md
+1 −1 Package.swift
+1 −0 bindgen/spec.yml
+2 −2 dependencies.list
+295 −0 doc/development/how-to-use-remote-baas-host.md
+182 −23 evergreen/config.yml
+0 −16 evergreen/config_overrides.json
+237 −0 evergreen/configure_baas_proxy.sh
+304 −214 evergreen/install_baas.sh
+4 −0 evergreen/proxy-network-faults.toxics
+5 −0 evergreen/proxy-nonideal-transfer.toxics
+160 −30 evergreen/setup_baas_host.sh
+149 −55 evergreen/setup_baas_host_local.sh
+263 −0 evergreen/setup_baas_proxy.sh
+21 −11 evergreen/wait_for_baas.sh
+7 −25 src/realm.h
+0 −1 src/realm/CMakeLists.txt
+0 −1 src/realm/alloc_slab.cpp
+11 −11 src/realm/collection.hpp
+0 −1 src/realm/group.cpp
+0 −1 src/realm/group_writer.cpp
+0 −1 src/realm/object-store/CMakeLists.txt
+52 −20 src/realm/object-store/c_api/notifications.cpp
+5 −5 src/realm/object-store/c_api/types.hpp
+0 −73 src/realm/object-store/keypath_helpers.cpp
+0 −16 src/realm/object-store/keypath_helpers.hpp
+7 −0 src/realm/object-store/object_store.cpp
+2 −0 src/realm/object-store/shared_realm.cpp
+3 −4 src/realm/object-store/sync/sync_session.cpp
+0 −1 src/realm/query_engine.hpp
+137 −129 src/realm/set.cpp
+0 −22 src/realm/set.hpp
+12 −7 src/realm/sync/client.cpp
+90 −111 src/realm/sync/noinst/client_impl_base.cpp
+29 −48 src/realm/sync/noinst/client_impl_base.hpp
+3 −7 src/realm/sync/noinst/client_reset.cpp
+1 −1 src/realm/sync/noinst/client_reset.hpp
+41 −83 src/realm/sync/noinst/client_reset_operation.cpp
+12 −50 src/realm/sync/noinst/client_reset_operation.hpp
+47 −53 src/realm/sync/subscriptions.cpp
+26 −23 src/realm/sync/subscriptions.hpp
+5 −1 src/realm/table.cpp
+2 −0 src/realm/table.hpp
+0 −49 src/realm/util/miscellaneous.hpp
+8 −0 src/realm/utilities.hpp
+16 −0 test/object-store/CMakeLists.txt
+1 −1 test/object-store/audit.cpp
+69 −31 test/object-store/c_api/c_api.cpp
+12 −0 test/object-store/main.cpp
+22 −28 test/object-store/sync/app.cpp
+16 −18 test/object-store/sync/client_reset.cpp
+24 −31 test/object-store/sync/flx_migration.cpp
+116 −58 test/object-store/sync/flx_sync.cpp
+98 −68 test/object-store/util/sync/baas_admin_api.cpp
+21 −12 test/object-store/util/sync/baas_admin_api.hpp
+1 −1 test/object-store/util/sync/flx_sync_harness.hpp
+58 −16 test/object-store/util/sync/sync_test_utils.cpp
+10 −5 test/object-store/util/sync/sync_test_utils.hpp
+4 −3 test/object-store/util/test_file.cpp
+3 −0 test/object-store/util/test_file.hpp
+387 −6 test/test_client_reset.cpp
+144 −34 test/test_set.cpp
+9 −0 test/util/test_path.hpp
7 changes: 4 additions & 3 deletions packages/jni-swig-stub/realm.i
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,9 @@ import static io.realm.kotlin.internal.interop.realm_errno_e.*;
/* This cleans up the memory we malloc'd before the function call */
%typemap(freearg) char ** {
int i;
for (i=0; i<size$argnum-1; i++)
free($1[i]);
for (i=0; i<size$argnum-1; i++) {
free($1[i]);
}
free($1);
}

Expand Down Expand Up @@ -454,7 +455,7 @@ import static io.realm.kotlin.internal.interop.realm_errno_e.*;
and vice versa */
%typemap(javain) char ** "$javainput"
%typemap(javaout) char ** {
return $jnicall;
return $jnicall;
}
// -- End

Expand Down
5 changes: 2 additions & 3 deletions packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1073,10 +1073,9 @@ jni_realm_create_key_path_array(const realm_t* realm,
int user_key_paths_count,
const char** user_key_paths)
{
realm_key_path_array_t* out = nullptr;
bool result = realm_create_key_path_array(realm, object_class_key, user_key_paths_count, user_key_paths, &out);
realm_key_path_array_t* result = realm_create_key_path_array(realm, object_class_key, user_key_paths_count, user_key_paths);
if (result) {
return out;
return result ;
} else {
auto env = get_env();
throw_last_error_as_java_exception(env);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ public fun BaseRealmObject.isValid(): Boolean = runIfManaged {
*
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @return a flow representing changes to the object.
* @throws UnsupportedOperationException if called on a live [RealmObject] or [EmbeddedRealmObject]
* from a write transaction ([Realm.write]) or on a [DynamicRealmObject] inside a migration
* ([AutomaticSchemaMigration.migrate]).
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun <T : BaseRealmObject> T.asFlow(keyPaths: List<String>? = null): Flow<ObjectChange<T>> = runIfManaged {
checkNotificationsAvailable()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ internal class SuspendableNotifier(
}

internal fun <T : CoreNotifiable<T, C>, C> registerObserver(flowable: Observable<T, C>, keyPaths: Pair<ClassKey, List<String>>?): Flow<C> {
val keypathsPtr: RealmKeyPathArrayPointer? = keyPaths?.let { RealmInterop.create_key_paths_array(realm.owner.realmReference.dbPointer, keyPaths.first, keyPaths.second) }
val keypathsPtr: RealmKeyPathArrayPointer? = keyPaths?.let { RealmInterop.realm_create_key_paths_array(realm.owner.realmReference.dbPointer, keyPaths.first, keyPaths.second) }
return callbackFlow {
val token: AtomicRef<Cancellable> =
kotlinx.atomicfu.atomic(NO_OP_NOTIFICATION_TOKEN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ internal class SingleQuery<E : BaseRealmObject> constructor(
} else {
oldHead = newHead
if (!oldHeadDeleted) {
newHead.asFlow()
newHead.asFlow(keyPaths)
} else {
newHead.asFlow().onStart { emit(DeletedObjectImpl()) }
newHead.asFlow(keyPaths).onStart { emit(DeletedObjectImpl()) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ public interface RealmElementQuery<T : BaseRealmObject> : Deleteable {
*
* **It is not allowed to call [asFlow] on queries generated from a [MutableRealm].**
*
* @param keyPath TODO
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
* @return a flow representing changes to the [RealmResults] resulting from running this query.
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun asFlow(keyPath: List<String>? = null): Flow<ResultsChange<T>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,13 @@ public interface RealmSingleQuery<T : BaseRealmObject> : Deleteable {
* the elements in a timely manner the coroutine scope will be cancelled with a
* [CancellationException].
*
* @param keyPaths TODO
* @param keyPaths An optional list of properties that defines when a change to the object will
* result in a change being emitted. Nested properties can be defined using a dotted
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
* @return a flow representing changes to the [RealmObject] or [EmbeddedRealmObject] resulting from
* running this query.
* @throws IllegalArgumentException if an invalid keypath is provided.
*/
public fun asFlow(keyPaths: List<String>? = null): Flow<SingleQueryChange<T>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ public interface RealmList<E> : MutableList<E>, Deleteable {
*
* @param keyPaths An optional list of properties that defines when a change to the object will
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
* result in a change being emitted. Nested properties can be defined using a dotted
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
* @return a flow representing changes to the list.
* @throws IllegalArgumentException if keypaths are provided for lists not containing objects.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public interface RealmMap<K, V> : MutableMap<K, V> {
*
* @param keyPaths An optional list of properties that defines when a change to the object will
rorbech marked this conversation as resolved.
Show resolved Hide resolved
* result in a change being emitted. Nested properties can be defined using a dotted
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @return a flow representing changes to the dictionary.
* @throws IllegalArgumentException if keypaths are provided for maps not containing objects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ public interface RealmSet<E> : MutableSet<E>, Deleteable {
*
* @param keyPaths An optional list of properties that defines when a change to the object will
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
* result in a change being emitted. Nested properties can be defined using a dotted
* syntex, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* syntax, e.g. `parent.child.name`. If no keypaths are provided, changes to all top-level
* properties and nested properties 4 levels down will trigger a change.
* @return a flow representing changes to the set.
* @throws IllegalArgumentException if keypaths are provided for sets not containing objects.
* @throws IllegalArgumentException if an invalid keypath is provided.
* @throws CancellationException if the stream produces changes faster than the consumer can
* consume them and results in a buffer overflow.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2946,6 +2946,86 @@ class QueryTests {
}
}

@Test
rorbech marked this conversation as resolved.
Show resolved Hide resolved
fun asFlow_results_withKeyPath() {
val channel = Channel<ResultsChange<QuerySample>>(1)
runBlocking {
val observer = async {
realm.query<QuerySample>()
.asFlow(listOf("stringField"))
.collect { results ->
assertNotNull(results)
channel.send(results)
}
}
channel.receiveOrFail().let { resultsChange ->
assertIs<InitialResults<*>>(resultsChange)
assertTrue(resultsChange.list.isEmpty())
}
val obj = realm.writeBlocking {
copyToRealm(QuerySample())
}
channel.receiveOrFail().let { resultsChange ->
assertIs<UpdatedResults<*>>(resultsChange)
assertEquals(1, resultsChange.list.size)
}
realm.writeBlocking {
// Should not trigger notification
findLatest(obj)!!.intField = 42
}
realm.writeBlocking {
// Should not trigger notification
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
findLatest(obj)!!.stringField = "update"
}
channel.receiveOrFail().let { resultsChange ->
assertIs<UpdatedResults<*>>(resultsChange)
cmelchior marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(1, resultsChange.list.size)
}
observer.cancel()
channel.close()
}
}

@Test
fun asFlow_objectBound_withKeyPath() {
val channel = Channel<SingleQueryChange<QuerySample>>(1)
runBlocking {
val observer = async {
realm.query<QuerySample>()
.first()
.asFlow(listOf("stringField"))
.collect { change ->
assertNotNull(change)
channel.send(change)
}
}
channel.receiveOrFail().let { objChange ->
assertIs<PendingObject<*>>(objChange)
}
val obj = realm.writeBlocking {
copyToRealm(QuerySample())
}
channel.receiveOrFail().let { objChange ->
assertIs<InitialObject<*>>(objChange)
}
realm.writeBlocking {
// Should not trigger notification
findLatest(obj)!!.intField = 42
}
realm.writeBlocking {
// Should trigger notification
findLatest(obj)!!.stringField = "update"
}
channel.receiveOrFail().let { objChange ->
assertIs<UpdatedObject<*>>(objChange)
assertEquals(1, objChange.changedFields.size)
assertEquals("stringField", objChange.changedFields.first())
}
observer.cancel()
channel.close()
}
}

// ----------------
// Coercion helpers
// ----------------
Expand Down
Loading