From 5abe1e9ffbba5728e0a739b03e1c31379aecdc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 19 Sep 2023 14:38:29 +0200 Subject: [PATCH 01/12] Add support for collections in mixed properties (#1429) --- CHANGELOG.md | 5 +- .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmEnums.kt | 3 + .../kotlin/internal/interop/RealmInterop.kt | 31 + .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmInterop.kt | 96 ++- .../kotlin/internal/interop/ValueType.kt | 5 +- .../kotlin/internal/interop/ErrorCode.kt | 2 +- .../kotlin/internal/interop/RealmEnums.kt | 6 +- .../kotlin/internal/interop/RealmInterop.kt | 92 +- .../io/realm/kotlin/test/CinteropTest.kt | 5 +- packages/external/core | 2 +- packages/jni-swig-stub/realm.i | 3 +- .../kotlin/io/realm/kotlin/ext/RealmAnyExt.kt | 77 ++ .../kotlin/internal/CollectionOperator.kt | 1 - .../io/realm/kotlin/internal/Converters.kt | 294 +++---- .../io/realm/kotlin/internal/Notifiable.kt | 3 + .../io/realm/kotlin/internal/RealmAnyImpl.kt | 11 + .../kotlin/internal/RealmListInternal.kt | 216 ++++- .../realm/kotlin/internal/RealmMapInternal.kt | 339 ++++++-- .../kotlin/internal/RealmObjectHelper.kt | 315 ++++--- .../kotlin/internal/RealmObjectReference.kt | 2 +- .../realm/kotlin/internal/RealmResultsImpl.kt | 20 +- .../realm/kotlin/internal/RealmSetInternal.kt | 212 ++++- .../io/realm/kotlin/internal/RealmUtils.kt | 4 +- .../kotlin/internal/SuspendableNotifier.kt | 13 +- .../kotlin/internal/query/ScalarQuery.kt | 172 ++-- .../kotlin/serializers/RealmKSerializers.kt | 16 + .../kotlin/io/realm/kotlin/types/RealmAny.kt | 142 +++- .../kotlin/mongodb/internal/BsonEncoder.kt | 1 + .../kotlin/entities/JsonStyleRealmObject.kt | 30 + .../common/RealmAnyNestedCollectionTests.kt | 793 ++++++++++++++++++ .../realm/kotlin/test/common/RealmAnyTests.kt | 20 + .../test/common/RealmDictionaryTests.kt | 18 + .../kotlin/test/common/RealmListTests.kt | 33 + .../realm/kotlin/test/common/RealmSetTests.kt | 25 +- .../kotlin/test/common/SerializationTests.kt | 80 +- .../dynamic/DynamicMutableRealmObjectTests.kt | 266 +++++- .../common/dynamic/DynamicRealmObjectTests.kt | 175 +++- .../BacklinksNotificationsTests.kt | 2 +- ...ealmAnyNestedCollectionNotificationTest.kt | 108 +++ ...ealmAnyNestedDictionaryNotificationTest.kt | 268 ++++++ .../RealmAnyNestedListNotificationTest.kt | 254 ++++++ .../RealmAnyNestedSetNotificationTest.kt | 249 ++++++ .../RealmDictionaryNotificationsTests.kt | 2 +- .../RealmListNotificationsTests.kt | 2 +- .../RealmObjectNotificationsTests.kt | 2 +- .../RealmSetNotificationsTests.kt | 2 +- .../utils/RealmEntityNotificationTests.kt | 2 +- .../test/mongodb/common/SyncedRealmTests.kt | 43 + 50 files changed, 3829 insertions(+), 637 deletions(-) create mode 100644 packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/JsonStyleRealmObject.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedCollectionNotificationTest.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 130171b81f..a483e7a2a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ ## 1.12.0-SNAPSHOT (YYYY-MM-DD) +This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it. Downgrading to a previous file format is not possible. + ### Breaking Changes * None. ### Enhancements +* Support for RealmLists, RealmSets and RealmDictionaries in `RealmAny`. This is only supported in the local database, Device Sync support will come in a future release. (Issue [#1434](https://github.com/realm/realm-kotlin/issues/1434)) * Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. ([#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed @@ -24,7 +27,7 @@ * Minimum Android SDK: 16. ### Internal -* None. +* Updated to Realm Core `next-major`, commit 0da737b699bf4bcfc1a3772385cd49cd9eb9cad9. ## 1.11.1 (2023-09-07) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 31db69c4c8..1d00e55cae 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -179,7 +179,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_MAINTENANCE_IN_PROGRESS, RLM_ERR_USERPASS_TOKEN_INVALID, RLM_ERR_INVALID_SERVER_RESPONSE, - REALM_ERR_APP_SERVER_ERROR, + RLM_ERR_APP_SERVER_ERROR, RLM_ERR_CALLBACK, RLM_ERR_UNKNOWN; diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt index c5bd4a968c..3970840586 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -95,4 +95,7 @@ expect enum class ValueType { RLM_TYPE_OBJECT_ID, RLM_TYPE_LINK, RLM_TYPE_UUID, + RLM_TYPE_LIST, + RLM_TYPE_SET, + RLM_TYPE_DICTIONARY, } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 1cd6c153c9..776dd654c5 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -289,6 +289,9 @@ expect object RealmInterop { isDefault: Boolean ) fun realm_set_embedded(obj: RealmObjectPointer, key: PropertyKey): RealmObjectPointer + fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer + fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer + fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) fun realm_object_get_parent( obj: RealmObjectPointer, @@ -300,10 +303,19 @@ expect object RealmInterop { fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue + fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer + fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer + fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) fun realm_list_insert_embedded(list: RealmListPointer, index: Long): RealmObjectPointer // Returns the element previously at the specified position fun realm_list_set(list: RealmListPointer, index: Long, inputTransport: RealmValue) + fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer + fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer + fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer + fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer + fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer + fun realm_list_set_dictionary(list: RealmListPointer, index: Long): RealmMapPointer // Returns the newly inserted element as the previous embedded element is automatically delete // by this operation @@ -335,10 +347,23 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue + fun realm_dictionary_find_set( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmSetPointer + fun realm_dictionary_find_list( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmListPointer + fun realm_dictionary_find_dictionary( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmMapPointer fun MemAllocator.realm_dictionary_get( dictionary: RealmMapPointer, pos: Int ): Pair + fun MemAllocator.realm_dictionary_insert( dictionary: RealmMapPointer, mapKey: RealmValue, @@ -360,6 +385,9 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue + fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer + fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer + fun realm_dictionary_insert_dictionary(dictionary: RealmMapPointer, mapKey: RealmValue): RealmMapPointer fun realm_dictionary_get_keys(dictionary: RealmMapPointer): RealmResultsPointer fun realm_dictionary_resolve_in( dictionary: RealmMapPointer, @@ -424,6 +452,9 @@ expect object RealmInterop { // FIXME OPTIMIZE Get many fun MemAllocator.realm_results_get(results: RealmResultsPointer, index: Long): RealmValue + fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer + fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer + fun realm_results_get_dictionary(results: RealmResultsPointer, index: Long): RealmMapPointer fun realm_results_delete_all(results: RealmResultsPointer) fun realm_get_object(realm: RealmPointer, link: Link): RealmObjectPointer diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 28bdbb8cb0..ec721b3ed4 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -176,7 +176,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno_e.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno_e.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno_e.RLM_ERR_INVALID_SERVER_RESPONSE), - REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno_e.RLM_ERR_APP_SERVER_ERROR), + RLM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno_e.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno_e.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno_e.RLM_ERR_UNKNOWN); diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index c2b2a07d8f..db31131558 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH +import io.realm.kotlin.internal.interop.RealmInterop.cptr import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AuthProvider import io.realm.kotlin.internal.interop.sync.CoreConnectionState @@ -463,6 +464,19 @@ actual object RealmInterop { return LongPointerWrapper(realmc.realm_set_embedded(obj.cptr(), key.key)) } + actual fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer { + realmc.realm_set_set(obj.cptr(), key.key) + return realm_get_set(obj, key) + } + actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { + realmc.realm_set_list(obj.cptr(), key.key) + return realm_get_list(obj, key) + } + actual fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer { + realmc.realm_set_dictionary(obj.cptr(), key.key) + return realm_get_dictionary(obj, key) + } + actual fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) { realmc.realm_object_add_int(obj.cptr(), key.key, value) } @@ -519,6 +533,13 @@ actual object RealmInterop { realmc.realm_list_get(list.cptr(), index, struct) return RealmValue(struct) } + actual fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer = + LongPointerWrapper(realmc.realm_list_get_set(list.cptr(), index)) + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = + LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index)) + + actual fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer = + LongPointerWrapper(realmc.realm_list_get_dictionary(list.cptr(), index)) actual fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) { realmc.realm_list_insert(list.cptr(), index, transport.value) @@ -527,6 +548,24 @@ actual object RealmInterop { actual fun realm_list_insert_embedded(list: RealmListPointer, index: Long): RealmObjectPointer { return LongPointerWrapper(realmc.realm_list_insert_embedded(list.cptr(), index)) } + actual fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer { + return LongPointerWrapper(realmc.realm_list_insert_set(list.cptr(), index)) + } + actual fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer { + return LongPointerWrapper(realmc.realm_list_insert_list(list.cptr(), index)) + } + actual fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { + return LongPointerWrapper(realmc.realm_list_insert_dictionary(list.cptr(), index)) + } + actual fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer { + return LongPointerWrapper(realmc.realm_list_set_set(list.cptr(), index)) + } + actual fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer { + return LongPointerWrapper(realmc.realm_list_set_list(list.cptr(), index)) + } + actual fun realm_list_set_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { + return LongPointerWrapper(realmc.realm_list_set_dictionary(list.cptr(), index)) + } actual fun realm_list_set( list: RealmListPointer, @@ -683,6 +722,25 @@ actual object RealmInterop { realmc.realm_dictionary_find(dictionary.cptr(), mapKey.value, struct, found) return RealmValue(struct) } + actual fun realm_dictionary_find_set( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmSetPointer { + return LongPointerWrapper(realmc.realm_dictionary_get_set(dictionary.cptr(), mapKey.value)) + } + + actual fun realm_dictionary_find_list( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmListPointer { + return LongPointerWrapper(realmc.realm_dictionary_get_list(dictionary.cptr(), mapKey.value)) + } + actual fun realm_dictionary_find_dictionary( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmMapPointer { + return LongPointerWrapper(realmc.realm_dictionary_get_dictionary(dictionary.cptr(), mapKey.value)) + } actual fun MemAllocator.realm_dictionary_get( dictionary: RealmMapPointer, @@ -751,6 +809,17 @@ actual object RealmInterop { } ) } + actual fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer { + return LongPointerWrapper(realmc.realm_dictionary_insert_set(dictionary.cptr(), mapKey.value)) + } + + actual fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer { + return LongPointerWrapper(realmc.realm_dictionary_insert_list(dictionary.cptr(), mapKey.value)) + } + + actual fun realm_dictionary_insert_dictionary(dictionary: RealmMapPointer, mapKey: RealmValue): RealmMapPointer { + return LongPointerWrapper(realmc.realm_dictionary_insert_dictionary(dictionary.cptr(), mapKey.value)) + } actual fun realm_dictionary_get_keys(dictionary: RealmMapPointer): RealmResultsPointer { val size = LongArray(1) @@ -886,7 +955,11 @@ actual object RealmInterop { val deletionCount = LongArray(1) val modificationCount = LongArray(1) val movesCount = LongArray(1) + // Not exposed in SDK yet, but could be used to provide optimized notifications when + // collections are cleared. + // https://github.com/realm/realm-kotlin/issues/1498 val collectionWasCleared = BooleanArray(1) + val collectionWasDeleted = BooleanArray(1) realmc.realm_collection_changes_get_num_changes( change.cptr(), @@ -894,7 +967,8 @@ actual object RealmInterop { insertionCount, modificationCount, movesCount, - collectionWasCleared + collectionWasCleared, + collectionWasDeleted, ) val insertionIndices: LongArray = initIndicesArray(insertionCount) @@ -978,16 +1052,22 @@ actual object RealmInterop { val deletions = longArrayOf(0) val insertions = longArrayOf(0) val modifications = longArrayOf(0) + val collectionWasDeleted = BooleanArray(1) realmc.realm_dictionary_get_changes( change.cptr(), deletions, insertions, - modifications + modifications, + collectionWasDeleted, ) val deletionStructs = realmc.new_valueArray(deletions[0].toInt()) val insertionStructs = realmc.new_valueArray(insertions[0].toInt()) val modificationStructs = realmc.new_valueArray(modifications[0].toInt()) + // Not exposed in SDK yet, but could be used to provide optimized notifications when + // collections are cleared. + // https://github.com/realm/realm-kotlin/issues/1498 + val collectionWasCleared = booleanArrayOf(false) realmc.realm_dictionary_get_changed_keys( change.cptr(), deletionStructs, @@ -995,7 +1075,8 @@ actual object RealmInterop { insertionStructs, insertions, modificationStructs, - modifications + modifications, + collectionWasCleared ) // TODO optimize - integrate within mem allocator? @@ -1768,6 +1849,15 @@ actual object RealmInterop { return RealmValue(value) } + actual fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer = + LongPointerWrapper(realmc.realm_results_get_set(results.cptr(), index)) + + actual fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer = + LongPointerWrapper(realmc.realm_results_get_list(results.cptr(), index)) + + actual fun realm_results_get_dictionary(results: RealmResultsPointer, index: Long): RealmMapPointer = + LongPointerWrapper(realmc.realm_results_get_dictionary(results.cptr(), index)) + actual fun realm_get_object(realm: RealmPointer, link: Link): RealmObjectPointer { return LongPointerWrapper(realmc.realm_get_object(realm.cptr(), link.classKey.key, link.objKey)) } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ValueType.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ValueType.kt index d0363e037d..8bdceac38e 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ValueType.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ValueType.kt @@ -28,7 +28,10 @@ actual enum class ValueType(override val nativeValue: Int) : NativeEnumerated { RLM_TYPE_DECIMAL128(realm_value_type_e.RLM_TYPE_DECIMAL128), RLM_TYPE_OBJECT_ID(realm_value_type_e.RLM_TYPE_OBJECT_ID), RLM_TYPE_LINK(realm_value_type_e.RLM_TYPE_LINK), - RLM_TYPE_UUID(realm_value_type_e.RLM_TYPE_UUID); + RLM_TYPE_UUID(realm_value_type_e.RLM_TYPE_UUID), + RLM_TYPE_LIST(realm_value_type_e.RLM_TYPE_LIST), + RLM_TYPE_SET(realm_value_type_e.RLM_TYPE_SET), + RLM_TYPE_DICTIONARY(realm_value_type_e.RLM_TYPE_DICTIONARY); companion object { fun from(nativeValue: Int): ValueType = values().find { diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index aadd8db464..644636308c 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -180,7 +180,7 @@ actual enum class ErrorCode( RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno.RLM_ERR_INVALID_SERVER_RESPONSE), - REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno.RLM_ERR_APP_SERVER_ERROR), + RLM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno.RLM_ERR_UNKNOWN); diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt index 9f5849c1f8..b992b9816c 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -115,7 +115,11 @@ actual enum class ValueType( RLM_TYPE_DECIMAL128(realm_value_type_e.RLM_TYPE_DECIMAL128), RLM_TYPE_OBJECT_ID(realm_value_type_e.RLM_TYPE_OBJECT_ID), RLM_TYPE_LINK(realm_value_type_e.RLM_TYPE_LINK), - RLM_TYPE_UUID(realm_value_type_e.RLM_TYPE_UUID); + RLM_TYPE_UUID(realm_value_type_e.RLM_TYPE_UUID), + RLM_TYPE_SET(realm_value_type_e.RLM_TYPE_SET), + RLM_TYPE_LIST(realm_value_type_e.RLM_TYPE_LIST), + RLM_TYPE_DICTIONARY(realm_value_type_e.RLM_TYPE_DICTIONARY), + ; companion object { fun from(nativeValue: realm_value_type): ValueType = values().find { diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 4418168802..2993e9e801 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -946,6 +946,19 @@ actual object RealmInterop { return CPointerWrapper(realm_wrapper.realm_set_embedded(obj.cptr(), key.key)) } + actual fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer { + checkedBooleanResult(realm_wrapper.realm_set_set(obj.cptr(), key.key)) + return realm_get_set(obj, key) + } + actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { + checkedBooleanResult(realm_wrapper.realm_set_list(obj.cptr(), key.key)) + return realm_get_list(obj, key) + } + actual fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer { + checkedBooleanResult(realm_wrapper.realm_set_dictionary(obj.cptr(), key.key)) + return realm_get_dictionary(obj, key) + } + actual fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) { checkedBooleanResult(realm_wrapper.realm_object_add_int(obj.cptr(), key.key, value)) } @@ -998,6 +1011,14 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer = + CPointerWrapper(realm_wrapper.realm_list_get_set(list.cptr(), index.toULong())) + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = + CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong())) + + actual fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer = + CPointerWrapper(realm_wrapper.realm_list_get_dictionary(list.cptr(), index.toULong())) + actual fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) { checkedBooleanResult( realm_wrapper.realm_list_insert( @@ -1007,6 +1028,24 @@ actual object RealmInterop { ) ) } + actual fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer { + return CPointerWrapper(realm_wrapper.realm_list_insert_set(list.cptr(), index.toULong())) + } + actual fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer { + return CPointerWrapper(realm_wrapper.realm_list_insert_list(list.cptr(), index.toULong())) + } + actual fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { + return CPointerWrapper(realm_wrapper.realm_list_insert_dictionary(list.cptr(), index.toULong())) + } + actual fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer { + return CPointerWrapper(realm_wrapper.realm_list_set_set(list.cptr(), index.toULong())) + } + actual fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer { + return CPointerWrapper(realm_wrapper.realm_list_set_list(list.cptr(), index.toULong())) + } + actual fun realm_list_set_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { + return CPointerWrapper(realm_wrapper.realm_list_set_dictionary(list.cptr(), index.toULong())) + } actual fun realm_list_insert_embedded(list: RealmListPointer, index: Long): RealmObjectPointer { return CPointerWrapper(realm_wrapper.realm_list_insert_embedded(list.cptr(), index.toULong())) @@ -1222,6 +1261,26 @@ actual object RealmInterop { } } + actual fun realm_dictionary_find_set( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmSetPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_get_set(dictionary.cptr(), mapKey.value.readValue())) + } + + actual fun realm_dictionary_find_list( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmListPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_get_list(dictionary.cptr(), mapKey.value.readValue())) + } + actual fun realm_dictionary_find_dictionary( + dictionary: RealmMapPointer, + mapKey: RealmValue + ): RealmMapPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_get_dictionary(dictionary.cptr(), mapKey.value.readValue())) + } + actual fun MemAllocator.realm_dictionary_get( dictionary: RealmMapPointer, pos: Int @@ -1343,6 +1402,18 @@ actual object RealmInterop { return RealmValue(outputStruct) } + actual fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_insert_set(dictionary.cptr(), mapKey.value.readValue())) + } + + actual fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_insert_list(dictionary.cptr(), mapKey.value.readValue())) + } + + actual fun realm_dictionary_insert_dictionary(dictionary: RealmMapPointer, mapKey: RealmValue): RealmMapPointer { + return CPointerWrapper(realm_wrapper.realm_dictionary_insert_dictionary(dictionary.cptr(), mapKey.value.readValue())) + } + actual fun realm_dictionary_get_keys(dictionary: RealmMapPointer): RealmResultsPointer { memScoped { val size = alloc() @@ -1598,6 +1669,15 @@ actual object RealmInterop { return RealmValue(value) } + actual fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer = + CPointerWrapper(realm_wrapper.realm_results_get_set(results.cptr(), index.toULong())) + + actual fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer = + CPointerWrapper(realm_wrapper.realm_results_get_list(results.cptr(), index.toULong())) + + actual fun realm_results_get_dictionary(results: RealmResultsPointer, index: Long): RealmMapPointer = + CPointerWrapper(realm_wrapper.realm_results_get_dictionary(results.cptr(), index.toULong())) + actual fun realm_get_object(realm: RealmPointer, link: Link): RealmObjectPointer { val ptr = checkedPointerResult( realm_wrapper.realm_get_object( @@ -1818,6 +1898,7 @@ actual object RealmInterop { val modificationCount = allocArray(1) val movesCount = allocArray(1) val collectionWasErased = alloc() + val collectionWasDeleted = alloc() realm_wrapper.realm_collection_changes_get_num_changes( change.cptr(), @@ -1825,7 +1906,8 @@ actual object RealmInterop { insertionCount, modificationCount, movesCount, - collectionWasErased.ptr + collectionWasErased.ptr, + collectionWasDeleted.ptr, ) val deletionIndices = initArray(deletionCount) @@ -1907,12 +1989,15 @@ actual object RealmInterop { val deletions = allocArray(1) val insertions = allocArray(1) val modifications = allocArray(1) + val collectionWasCleared = alloc() + val collectionWasDeleted = alloc() realm_wrapper.realm_dictionary_get_changes( change.cptr(), deletions, insertions, - modifications + modifications, + collectionWasDeleted.ptr, ) val deletionStructs = allocArray(deletions[0].toInt()) val insertionStructs = allocArray(insertions[0].toInt()) @@ -1925,7 +2010,8 @@ actual object RealmInterop { insertionStructs, insertions, modificationStructs, - modifications + modifications, + collectionWasCleared.ptr, ) val deletedKeys = (0 until deletions[0].toInt()).map { diff --git a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt index 01fd6fec06..ab55391880 100644 --- a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt +++ b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt @@ -246,6 +246,9 @@ class CinteropTest { } .toIntArray() + val unmappedErrors = coreErrorNativeValues + .filter { ErrorCode.of(it) == null } + val errorCodeValues = coreErrorNativeValues .map { ErrorCode.of(it) @@ -254,7 +257,7 @@ class CinteropTest { .toSet() // validate that all error codes are mapped - assertEquals(coreErrorNativeValues.size, errorCodeValues.size) + assertEquals(coreErrorNativeValues.size, errorCodeValues.size, "Unmapped error codes: $unmappedErrors") } } diff --git a/packages/external/core b/packages/external/core index c258e2681b..0da737b699 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit c258e2681bca5fb33bbd23c112493817b43bfa86 +Subproject commit 0da737b699bf4bcfc1a3772385cd49cd9eb9cad9 diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 02880c1df5..74bec2f26a 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -356,7 +356,8 @@ bool realm_object_is_valid(const realm_object_t*); // bool output parameter %apply bool* OUTPUT { bool* out_found, bool* did_create, bool* did_delete_realm, bool* out_inserted, bool* erased, bool* out_erased, bool* did_refresh, bool* did_run, - bool* found, bool* out_collection_was_cleared, bool* did_compact }; + bool* found, bool* out_collection_was_cleared, bool* did_compact, + bool* collection_was_cleared, bool* out_collection_was_deleted, bool* out_was_deleted}; // uint64_t output parameter for realm_get_num_versions %apply int64_t* OUTPUT { uint64_t* out_versions_count }; diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt index 67ef4a7827..b40d27ae9b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt @@ -1,8 +1,13 @@ package io.realm.kotlin.ext +import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmUUID +import org.mongodb.kbson.Decimal128 +import org.mongodb.kbson.ObjectId /** * Creates an unmanaged `RealmAny` instance from a [BaseRealmObject] value. @@ -11,3 +16,75 @@ import io.realm.kotlin.types.RealmObject */ public inline fun RealmAny.asRealmObject(): T = asRealmObject(T::class) + +/** + * Create a [RealmAny] encapsulating the [value] argument. + * + * This corresponds to calling [RealmAny.create]-variant with the specific typed non-null argument. + * + * @param value the value that should be wrapped in a [RealmAny]. + * @return a [RealmAny] wrapping the [value] argument, or `null` if [value] is null. + */ +@Suppress("ComplexMethod") +public fun realmAnyOf(value: Any?): RealmAny? { + return when (value) { + (value == null) -> null + is Boolean -> RealmAny.create(value) + is Byte -> RealmAny.create(value) + is Char -> RealmAny.create(value) + is Short -> RealmAny.create(value) + is Int -> RealmAny.create(value) + is Long -> RealmAny.create(value) + is Float -> RealmAny.create(value) + is Double -> RealmAny.create(value) + is String -> RealmAny.create(value) + is Decimal128 -> RealmAny.create(value) + is ObjectId -> RealmAny.create(value) + is ByteArray -> RealmAny.create(value) + is RealmInstant -> RealmAny.create(value) + is RealmUUID -> RealmAny.create(value) + is RealmObject -> RealmAny.create(value) + is DynamicRealmObject -> RealmAny.create(value) + is Set<*> -> RealmAny.create(value.map { realmAnyOf(it) }.toRealmSet()) + is List<*> -> RealmAny.create(value.map { realmAnyOf(it) }.toRealmList()) + is Map<*, *> -> RealmAny.create( + value.map { (mapKey, mapValue) -> + try { + mapKey as String + } catch (e: ClassCastException) { + throw IllegalArgumentException("Cannot create a RealmAny from a map with non-string key, found '${mapKey?.let { it::class.simpleName } ?: "null"}'") + } to realmAnyOf(mapValue) + }.toRealmDictionary() + ) + is RealmAny -> value + else -> throw IllegalArgumentException("Cannot create RealmAny from '$value'") + } +} + +/** + * Create a [RealmAny] containing a [RealmSet] of all arguments wrapped as [RealmAny]s. + * @param values elements of the set. + * + * See [RealmAny.create] for [RealmSet] constraints and examples of usage. + */ +public fun realmAnySetOf(vararg values: Any?): RealmAny = + RealmAny.create(values.map { realmAnyOf(it) }.toRealmSet()) + +/** + * Create a [RealmAny] containing a [RealmList] of all arguments wrapped as [RealmAny]s. + * @param values elements of the set. + * + * See [RealmAny.create] for [RealmList] constraints and examples of usage. + */ +public fun realmAnyListOf(vararg values: Any?): RealmAny = + RealmAny.create(values.map { realmAnyOf(it) }.toRealmList()) + +/** + * Create a [RealmAny] containing a [RealmDictionary] with all argument values wrapped as + * [RealmAnys]s. + * @param values entries of the dictionary. + * + * See [RealmAny.create] for [RealmDictionaries] constraints and examples of usage. + */ +public fun realmAnyDictionaryOf(vararg values: Pair): RealmAny = + RealmAny.create(values.map { (key, value) -> key to realmAnyOf(value) }.toRealmDictionary()) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt index 286847e1af..01a55091f4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt @@ -22,7 +22,6 @@ import io.realm.kotlin.internal.interop.NativePointer internal interface CollectionOperator { val mediator: Mediator val realmReference: RealmReference - val valueConverter: RealmValueConverter val nativePointer: NativePointer } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt index 7100685660..e33d020ab9 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt @@ -22,15 +22,17 @@ import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.internal.interop.MemTrackingAllocator +import io.realm.kotlin.internal.interop.RealmListPointer +import io.realm.kotlin.internal.interop.RealmMapPointer import io.realm.kotlin.internal.interop.RealmObjectInterop import io.realm.kotlin.internal.interop.RealmQueryArgument import io.realm.kotlin.internal.interop.RealmQueryArgumentList import io.realm.kotlin.internal.interop.RealmQueryListArgument import io.realm.kotlin.internal.interop.RealmQuerySingleArgument +import io.realm.kotlin.internal.interop.RealmSetPointer import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.Timestamp import io.realm.kotlin.internal.interop.ValueType -import io.realm.kotlin.internal.platform.realmObjectCompanionOrNull import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.ObjectId import io.realm.kotlin.types.RealmAny @@ -105,60 +107,113 @@ public inline fun realmValueToRealmUUID(transport: RealmValue): RealmUUID = Real public inline fun realmValueToDecimal128(transport: RealmValue): Decimal128 = transport.getDecimal128Array().let { Decimal128.fromIEEE754BIDEncoding(it[1], it[0]) } +@Suppress("ComplexMethod", "NestedBlockDepth", "LongParameterList") internal inline fun realmValueToRealmAny( - transport: RealmValue, - mediator: Mediator, - owner: RealmReference, - issueDynamicObject: Boolean = false -): RealmAny? { - return realmValueToRealmAny(transport, mediator, owner, issueDynamicObject, false) -} - -@Suppress("ComplexMethod", "NestedBlockDepth") -internal inline fun realmValueToRealmAny( - transport: RealmValue, + realmValue: RealmValue, + parent: RealmObjectReference<*>?, mediator: Mediator, owner: RealmReference, issueDynamicObject: Boolean, issueDynamicMutableObject: Boolean, + getSetFunction: () -> RealmSetPointer = { error("Cannot handled embedded sets") }, + getListFunction: () -> RealmListPointer = { error("Cannot handled embedded lists") }, + getDictionaryFunction: () -> RealmMapPointer = { error("Cannot handled embedded dictionaries") }, ): RealmAny? { - return when (transport.isNull()) { + return when (realmValue.isNull()) { true -> null - false -> when (val type = transport.getType()) { + false -> when (val type = realmValue.getType()) { ValueType.RLM_TYPE_NULL -> null - ValueType.RLM_TYPE_INT -> RealmAny.create(transport.getLong()) - ValueType.RLM_TYPE_BOOL -> RealmAny.create(transport.getBoolean()) - ValueType.RLM_TYPE_STRING -> RealmAny.create(transport.getString()) - ValueType.RLM_TYPE_BINARY -> RealmAny.create(transport.getByteArray()) - ValueType.RLM_TYPE_TIMESTAMP -> RealmAny.create(RealmInstantImpl(transport.getTimestamp())) - ValueType.RLM_TYPE_FLOAT -> RealmAny.create(transport.getFloat()) - ValueType.RLM_TYPE_DOUBLE -> RealmAny.create(transport.getDouble()) - ValueType.RLM_TYPE_DECIMAL128 -> RealmAny.create(realmValueToDecimal128(transport)) + ValueType.RLM_TYPE_INT -> RealmAny.create(realmValue.getLong()) + ValueType.RLM_TYPE_BOOL -> RealmAny.create(realmValue.getBoolean()) + ValueType.RLM_TYPE_STRING -> RealmAny.create(realmValue.getString()) + ValueType.RLM_TYPE_BINARY -> RealmAny.create(realmValue.getByteArray()) + ValueType.RLM_TYPE_TIMESTAMP -> RealmAny.create(RealmInstantImpl(realmValue.getTimestamp())) + ValueType.RLM_TYPE_FLOAT -> RealmAny.create(realmValue.getFloat()) + ValueType.RLM_TYPE_DOUBLE -> RealmAny.create(realmValue.getDouble()) + ValueType.RLM_TYPE_DECIMAL128 -> RealmAny.create(realmValueToDecimal128(realmValue)) ValueType.RLM_TYPE_OBJECT_ID -> - RealmAny.create(BsonObjectId(transport.getObjectIdBytes())) - ValueType.RLM_TYPE_UUID -> RealmAny.create(RealmUUIDImpl(transport.getUUIDBytes())) + RealmAny.create(BsonObjectId(realmValue.getObjectIdBytes())) + ValueType.RLM_TYPE_UUID -> RealmAny.create(RealmUUIDImpl(realmValue.getUUIDBytes())) ValueType.RLM_TYPE_LINK -> { if (issueDynamicObject) { val clazz = when (issueDynamicMutableObject) { true -> DynamicMutableRealmObject::class false -> DynamicRealmObject::class } - val realmObject = realmValueToRealmObject(transport, clazz, mediator, owner) + val realmObject = realmValueToRealmObject(realmValue, clazz, mediator, owner) RealmAny.create(realmObject!!) } else { val clazz = owner.schemaMetadata - .get(transport.getLink().classKey) + .get(realmValue.getLink().classKey) ?.clazz ?: throw IllegalArgumentException("The object class is not present in the current schema - are you using an outdated schema version?") - val realmObject = realmValueToRealmObject(transport, clazz, mediator, owner) + val realmObject = realmValueToRealmObject(realmValue, clazz, mediator, owner) RealmAny.create(realmObject!! as RealmObject, clazz as KClass) } } + ValueType.RLM_TYPE_SET -> { + val nativePointer = getSetFunction() + val operator = realmAnySetOperator(mediator, owner, nativePointer, issueDynamicObject, issueDynamicMutableObject) + return RealmAny.create(ManagedRealmSet(parent, nativePointer, operator)) + } + ValueType.RLM_TYPE_LIST -> { + val nativePointer = getListFunction() + val operator = realmAnyListOperator(mediator, owner, nativePointer, issueDynamicObject, issueDynamicMutableObject) + RealmAny.create(ManagedRealmList(parent, nativePointer, operator)) + } + ValueType.RLM_TYPE_DICTIONARY -> { + val nativePointer = getDictionaryFunction() + val operator = realmAnyMapOperator(mediator, owner, nativePointer, issueDynamicObject, issueDynamicMutableObject) + RealmAny.create(ManagedRealmDictionary(parent, nativePointer, operator)) + } else -> throw IllegalArgumentException("Unsupported type: ${type.name}") } } } +@Suppress("LongParameterList") +internal fun MemTrackingAllocator.realmAnyHandler( + value: RealmAny?, + primitiveValueAsRealmValueHandler: (RealmValue) -> T = { throw IllegalArgumentException("Operation not support for primitive values") }, + referenceAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for objects") }, + setAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for sets") }, + listAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for lists") }, + dictionaryAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for dictionaries") }, +): T { + return when (value?.type) { + null -> + primitiveValueAsRealmValueHandler(nullTransport()) + + io.realm.kotlin.types.RealmAny.Type.INT, + io.realm.kotlin.types.RealmAny.Type.BOOL, + io.realm.kotlin.types.RealmAny.Type.STRING, + io.realm.kotlin.types.RealmAny.Type.BINARY, + io.realm.kotlin.types.RealmAny.Type.TIMESTAMP, + io.realm.kotlin.types.RealmAny.Type.FLOAT, + io.realm.kotlin.types.RealmAny.Type.DOUBLE, + io.realm.kotlin.types.RealmAny.Type.DECIMAL128, + io.realm.kotlin.types.RealmAny.Type.OBJECT_ID, + io.realm.kotlin.types.RealmAny.Type.UUID -> + primitiveValueAsRealmValueHandler(realmAnyPrimitiveToRealmValue(value)) + + io.realm.kotlin.types.RealmAny.Type.OBJECT -> { + referenceAsRealmAnyHandler(value) + } + + io.realm.kotlin.types.RealmAny.Type.SET -> { + setAsRealmAnyHandler(value) + } + + io.realm.kotlin.types.RealmAny.Type.LIST -> { + listAsRealmAnyHandler(value) + } + + io.realm.kotlin.types.RealmAny.Type.DICTIONARY -> { + dictionaryAsRealmAnyHandler(value) + } + } +} + /** * Composite converters that combines a [PublicConverter] and a [StorageTypeConverter] into a * [RealmValueConverter]. @@ -338,20 +393,36 @@ internal val primitiveTypeConverters: Map, RealmValueConverter<*>> = // Dynamic default primitive value converter to translate primary keys and query arguments to RealmValues @Suppress("NestedBlockDepth") internal object RealmValueArgumentConverter { - fun MemTrackingAllocator.kAnyToRealmValue(value: Any?): RealmValue { + fun MemTrackingAllocator.kAnyToPrimaryKeyRealmValue(value: Any?): RealmValue { return value?.let { value -> - when (value) { - is RealmObject -> { - realmObjectTransport(realmObjectToRealmReferenceOrError(value)) + primitiveTypeConverters[value::class]?.let { converter -> + with(converter as RealmValueConverter) { + publicToRealmValue(value) } - is RealmAny -> realmAnyToRealmValue(value) - else -> { - primitiveTypeConverters[value::class]?.let { converter -> - with(converter as RealmValueConverter) { - publicToRealmValue(value) + } ?: throw IllegalArgumentException("Cannot use object '$value' of type '${value::class.simpleName}' as primary key argument") + } ?: nullTransport() + } + + fun MemTrackingAllocator.kAnyToRealmValueWithoutImport(value: Any?): RealmValue { + return value?.let { value -> + try { + when (value) { + is RealmObject -> { + realmObjectTransport(realmObjectToRealmReferenceOrError(value)) + } + is RealmAny -> + realmAnyToRealmValueWithoutImport(value) + else -> { + primitiveTypeConverters[value::class]?.let { converter -> + with(converter as RealmValueConverter) { + publicToRealmValue(value) + } } - } ?: throw IllegalArgumentException("Cannot use object '$value' of type '${value::class.simpleName}' as query argument") + ?: throw IllegalArgumentException("Cannot convert primitive type '$value' of type '${value::class.simpleName}' as query argument") + } } + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid query argument: ${e.message}", e) } } ?: nullTransport() } @@ -363,7 +434,7 @@ internal object RealmValueArgumentConverter { RealmQueryListArgument( allocRealmValueList(value.size).apply { value.mapIndexed { index: Int, element: Any? -> - set(index, kAnyToRealmValue(element)) + set(index, kAnyToRealmValueWithoutImport(element)) } } ) @@ -374,7 +445,7 @@ internal object RealmValueArgumentConverter { RealmQueryListArgument( allocRealmValueList(args.size).apply { args.mapIndexed { index: Int, element: Any? -> - set(index, kAnyToRealmValue(element)) + set(index, kAnyToRealmValueWithoutImport(element)) } } ) @@ -384,10 +455,10 @@ internal object RealmValueArgumentConverter { is GeoPolygon -> { // Hack support for geospatial arguments until we have propert C-API support. // See https://github.com/realm/realm-core/pull/6934 - RealmQuerySingleArgument(kAnyToRealmValue(value.toString())) + RealmQuerySingleArgument(kAnyToRealmValueWithoutImport(value.toString())) } else -> { - RealmQuerySingleArgument(kAnyToRealmValue(value)) + RealmQuerySingleArgument(kAnyToRealmValueWithoutImport(value)) } } @@ -402,25 +473,6 @@ internal object RealmValueArgumentConverter { } } -// Realm object converter that also imports (copyToRealm) objects when setting it -internal fun realmObjectConverter( - clazz: KClass, - mediator: Mediator, - realmReference: RealmReference -): RealmValueConverter { - return object : PassThroughPublicConverter() { - // TODO OPTIMIZE We could lookup the companion and keep a reference to - // `companion.newInstance` method to avoid repeated mediator lookups in Link.toRealmObject() - override fun fromRealmValue(realmValue: RealmValue): T? = - realmValueToRealmObject(realmValue, clazz, mediator, realmReference) - - override fun MemTrackingAllocator.toRealmValue(value: T?): RealmValue = - realmObjectTransport( - value?.let { realmObjectToRealmReferenceOrError(it) as RealmObjectInterop } - ) - } -} - /** * Tries to convert a [RealmValue] into a [RealmAny], it handles the cases for all primitive types * and leaves the other cases to an else block. @@ -443,103 +495,23 @@ internal inline fun RealmValue.asPrimitiveRealmAnyOrElse( else -> elseBlock() } -@Suppress("OVERRIDE_BY_INLINE", "NestedBlockDepth") -internal fun realmAnyConverter( - mediator: Mediator, - realmReference: RealmReference, - issueDynamicObject: Boolean = false, - issueDynamicMutableObject: Boolean = false -): RealmValueConverter { - return object : PassThroughPublicConverter() { - override inline fun fromRealmValue(realmValue: RealmValue): RealmAny? = - realmValue.asPrimitiveRealmAnyOrElse { - when (val type = realmValue.getType()) { - ValueType.RLM_TYPE_LINK -> { - val link = realmValue.getLink() - val clazz = if (issueDynamicObject) { - if (issueDynamicMutableObject) { - DynamicMutableRealmObject::class - } else { - DynamicRealmObject::class - } - } else { - realmReference.schemaMetadata - .get(link.classKey) - ?.clazz - ?: throw IllegalArgumentException("The object class is not present in the current schema - are you using an outdated schema version?") - } - val internalObject = mediator.createInstanceOf(clazz) - val obj = internalObject.link( - realmReference, - mediator, - clazz, - link - ) - when (issueDynamicObject) { - true -> when (issueDynamicMutableObject) { - true -> RealmAny.create(obj as DynamicMutableRealmObject) - else -> RealmAny.create(obj as DynamicRealmObject) - } - - false -> RealmAny.create( - obj as RealmObject, - clazz as KClass - ) - } - } - - else -> throw IllegalArgumentException("Invalid type '$type' for RealmValue.") - } - } - - override inline fun MemTrackingAllocator.toRealmValue(value: RealmAny?): RealmValue { - return realmAnyToRealmValueWithObjectImport( - value, - mediator, - realmReference, - issueDynamicObject, - ) - } - } -} - /** - * Used for converting values to query arguments. Importing objects isn't allowed here. - */ -internal inline fun MemTrackingAllocator.realmAnyToRealmValueWithObjectImport( - value: RealmAny?, - mediator: Mediator, - realmReference: RealmReference, - issueDynamicObject: Boolean = false -): RealmValue { - return when (value) { - null -> nullTransport() - else -> when (value.type) { - RealmAny.Type.OBJECT -> { - val obj = when (issueDynamicObject) { - true -> value.asRealmObject() - false -> value.asRealmObject() - } - val objRef = realmObjectToRealmReferenceWithImport(obj, mediator, realmReference) - realmObjectTransport(objRef as RealmObjectInterop) - } - else -> realmAnyPrimitiveToRealmValue(value) - } - } -} - -/** - * Used for converting RealmAny values to RealmValues suitable for query arguments. + * Used for converting RealmAny values to RealmValues suitable for query arguments and primary keys. * Importing objects isn't allowed here. */ -internal inline fun MemTrackingAllocator.realmAnyToRealmValue(value: RealmAny?): RealmValue { +internal inline fun MemTrackingAllocator.realmAnyToRealmValueWithoutImport(value: RealmAny?): RealmValue { return when (value) { null -> nullTransport() else -> when (value.type) { + // We shouldn't be able to land here for primary key arguments! RealmAny.Type.OBJECT -> { val objRef = realmObjectToRealmReferenceOrError(value.asRealmObject()) realmObjectTransport(objRef) } + RealmAny.Type.SET, + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> + throw IllegalArgumentException("Cannot pass unmanaged collections as input argument") else -> realmAnyPrimitiveToRealmValue(value) } } @@ -576,6 +548,12 @@ internal inline fun realmValueToRealmObject( } } +internal fun MemTrackingAllocator.realmObjectToRealmValue(value: BaseRealmObject?): RealmValue { + return realmObjectTransport( + value?.let { realmObjectToRealmReferenceOrError(it) as RealmObjectInterop } + ) +} + // Will return a managed realm object reference or null. If the object is unmanaged it will be // imported according to the update policy. If the object is an outdated object it will throw an // error. @@ -634,23 +612,5 @@ internal inline fun realmObjectToRealmReferenceOrError( // Returns a converter fixed to convert objects of the given type in the context of the given mediator/realm internal fun converter( - clazz: KClass, - mediator: Mediator, - realmReference: RealmReference -): RealmValueConverter { - return if (realmObjectCompanionOrNull(clazz) != null || clazz in setOf>( - DynamicRealmObject::class, - DynamicMutableRealmObject::class - ) - ) { - realmObjectConverter( - clazz as KClass, - mediator, - realmReference - ) as RealmValueConverter - } else if (clazz == RealmAny::class) { - realmAnyConverter(mediator, realmReference) as RealmValueConverter - } else { - primitiveTypeConverters.getValue(clazz) as RealmValueConverter - } -} + clazz: KClass +): RealmValueConverter = primitiveTypeConverters.getValue(clazz) as RealmValueConverter diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Notifiable.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Notifiable.kt index 8ce71fd03c..60cac46c82 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Notifiable.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Notifiable.kt @@ -135,4 +135,7 @@ internal interface CoreNotifiable : Notifiable, Observable, Ve // Default implementation as all Observables are just thawing themselves. override fun notifiable(): Notifiable = this override fun coreObservable(liveRealm: LiveRealm): CoreNotifiable? = thaw(liveRealm.realmReference) + + // Checks if the underlying native pointer points still points to a valid object. + fun isValid(): Boolean } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt index 0a2791fab8..69fdb8c003 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt @@ -18,8 +18,11 @@ package io.realm.kotlin.internal import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.RealmUUID import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 @@ -90,6 +93,14 @@ internal class RealmAnyImpl constructor( return clazz.cast(getValue) } + override fun asSet(): RealmSet = getValue(RealmAny.Type.SET) as RealmSet + + override fun asList(): RealmList = + getValue(RealmAny.Type.LIST) as RealmList + + override fun asDictionary(): RealmDictionary = + getValue(RealmAny.Type.DICTIONARY) as RealmDictionary + private fun getValue(type: RealmAny.Type): Any { if (this.type != type) { throw IllegalStateException("RealmAny type mismatch, wanted a '${type.name}' but the instance is a '${this.type.name}'.") diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index 2b4d12221b..b395db913e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -18,6 +18,8 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.Versioned +import io.realm.kotlin.dynamic.DynamicRealmObject +import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.internal.RealmValueArgumentConverter.convertToQueryArgs import io.realm.kotlin.internal.interop.Callback import io.realm.kotlin.internal.interop.ClassKey @@ -28,6 +30,7 @@ import io.realm.kotlin.internal.interop.RealmInterop.realm_list_set_embedded import io.realm.kotlin.internal.interop.RealmListPointer import io.realm.kotlin.internal.interop.RealmNotificationTokenPointer import io.realm.kotlin.internal.interop.RealmObjectInterop +import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.getterScope import io.realm.kotlin.internal.interop.inputScope import io.realm.kotlin.internal.query.ObjectBoundQuery @@ -38,7 +41,9 @@ import io.realm.kotlin.notifications.internal.InitialListImpl import io.realm.kotlin.notifications.internal.UpdatedListImpl import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow import kotlin.reflect.KClass @@ -67,7 +72,7 @@ internal class UnmanagedRealmList( * Implementation for managed lists, backed by Realm. */ internal class ManagedRealmList( - internal val parent: RealmObjectReference<*>, + internal val parent: RealmObjectReference<*>?, internal val nativePointer: RealmListPointer, val operator: ListOperator, ) : AbstractMutableList(), RealmList, InternalDeleteable, CoreNotifiable, ListChange>, Versioned by operator.realmReference { @@ -140,7 +145,7 @@ internal class ManagedRealmList( RealmListChangeFlow(scope) // TODO from LifeCycle interface - internal fun isValid(): Boolean = + override fun isValid(): Boolean = !nativePointer.isReleased() && RealmInterop.realm_list_is_valid(nativePointer) override fun delete() = RealmInterop.realm_list_remove_all(nativePointer) @@ -179,6 +184,12 @@ internal fun ManagedRealmList.query( throw IllegalArgumentException(e.message, e.cause) } } + // parent is only available for lists with an object as an immediate parent (contrary to nested + // collections). + // Nested collections are only supported for RealmAny-values and are therefore + // outside of the BaseRealmObject bound for the generic type parameters, so we should never be + // able to reach here for nested collections of RealmAny. + if (parent == null) error("Cannot perform subqueries on non-object lists") return ObjectBoundQuery( parent, ObjectQuery( @@ -245,7 +256,7 @@ internal interface ListOperator : CollectionOperator { internal class PrimitiveListOperator( override val mediator: Mediator, override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, + val realmValueConverter: RealmValueConverter, override val nativePointer: RealmListPointer ) : ListOperator { @@ -253,7 +264,7 @@ internal class PrimitiveListOperator( override fun get(index: Int): E { return getterScope { val transport = realm_list_get(nativePointer, index.toLong()) - with(valueConverter) { + with(realmValueConverter) { realmValueToPublic(transport) as E } } @@ -266,7 +277,7 @@ internal class PrimitiveListOperator( cache: UnmanagedToManagedObjectCache ) { inputScope { - with(valueConverter) { + with(realmValueConverter) { val transport = publicToRealmValue(element) RealmInterop.realm_list_add(nativePointer, index.toLong(), transport) } @@ -282,7 +293,7 @@ internal class PrimitiveListOperator( ): E { return get(index).also { inputScope { - with(valueConverter) { + with(realmValueConverter) { val transport = publicToRealmValue(element) RealmInterop.realm_list_set(nativePointer, index.toLong(), transport) } @@ -294,13 +305,166 @@ internal class PrimitiveListOperator( realmReference: RealmReference, nativePointer: RealmListPointer ): ListOperator = - PrimitiveListOperator(mediator, realmReference, valueConverter, nativePointer) + PrimitiveListOperator(mediator, realmReference, realmValueConverter, nativePointer) } -internal abstract class BaseRealmObjectListOperator( +internal fun realmAnyListOperator( + mediator: Mediator, + realm: RealmReference, + nativePointer: RealmListPointer, + issueDynamicObject: Boolean = false, + issueDynamicMutableObject: Boolean = false, +): RealmAnyListOperator = RealmAnyListOperator( + mediator, + realm, + nativePointer, + issueDynamicObject = issueDynamicObject, + issueDynamicMutableObject = issueDynamicMutableObject +) + +@Suppress("LongParameterList") +internal class RealmAnyListOperator( + override val mediator: Mediator, + override val realmReference: RealmReference, + override val nativePointer: RealmListPointer, + val updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + val cache: UnmanagedToManagedObjectCache = mutableMapOf(), + val issueDynamicObject: Boolean, + val issueDynamicMutableObject: Boolean +) : ListOperator { + + @Suppress("UNCHECKED_CAST") + override fun get(index: Int): RealmAny? { + return getterScope { + val transport = realm_list_get(nativePointer, index.toLong()) + return realmValueToRealmAny( + transport, null, mediator, realmReference, + issueDynamicObject, + issueDynamicMutableObject, + { RealmInterop.realm_list_get_set(nativePointer, index.toLong()) }, + { RealmInterop.realm_list_get_list(nativePointer, index.toLong()) }, + { RealmInterop.realm_list_get_dictionary(nativePointer, index.toLong()) } + ) + } + } + + override fun insert( + index: Int, + element: RealmAny?, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectCache + ) { + inputScope { + realmAnyHandler( + value = element, + primitiveValueAsRealmValueHandler = { realmValue: RealmValue -> + RealmInterop.realm_list_add(nativePointer, index.toLong(), realmValue) + }, + referenceAsRealmAnyHandler = { realmValue: RealmAny -> + val obj = when (issueDynamicObject) { + true -> realmValue.asRealmObject() + false -> realmValue.asRealmObject() + } + val objRef = + realmObjectToRealmReferenceWithImport(obj, mediator, realmReference, updatePolicy, cache) + RealmInterop.realm_list_add(nativePointer, index.toLong(), realmObjectTransport(objRef)) + }, + setAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_insert_set(nativePointer, index.toLong()) + val operator = realmAnySetOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.addAll(realmValue.asSet(), updatePolicy, cache) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_insert_list(nativePointer, index.toLong()) + val operator = realmAnyListOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.insertAll(0, realmValue.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_insert_dictionary(nativePointer, index.toLong()) + val operator = + realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) + operator.putAll(realmValue.asDictionary(), updatePolicy, cache) + } + ) + } + } + + @Suppress("UNCHECKED_CAST") + override fun set( + index: Int, + element: RealmAny?, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectCache + ): RealmAny? { + return get(index).also { + inputScope { + realmAnyHandler( + value = element, + primitiveValueAsRealmValueHandler = { realmValue: RealmValue -> + RealmInterop.realm_list_set(nativePointer, index.toLong(), realmValue) + }, + referenceAsRealmAnyHandler = { realmValue -> + val objRef = + realmObjectToRealmReferenceWithImport(realmValue.asRealmObject(), mediator, realmReference, updatePolicy, cache) + RealmInterop.realm_list_set(nativePointer, index.toLong(), realmObjectTransport(objRef)) + }, + setAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) + val nativePointer = RealmInterop.realm_list_set_set(nativePointer, index.toLong()) + val operator = realmAnySetOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.addAll(realmValue.asSet(), updatePolicy, cache) + }, + listAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) + val nativePointer = RealmInterop.realm_list_set_list(nativePointer, index.toLong()) + val operator = realmAnyListOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.insertAll(0, realmValue.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) + val nativePointer = RealmInterop.realm_list_set_dictionary(nativePointer, index.toLong()) + val operator = + realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) + operator.putAll(realmValue.asDictionary(), updatePolicy, cache) + } + ) + } + } + } + + override fun copy( + realmReference: RealmReference, + nativePointer: RealmListPointer + ): ListOperator = + RealmAnyListOperator(mediator, realmReference, nativePointer, issueDynamicObject = issueDynamicObject, issueDynamicMutableObject = issueDynamicMutableObject) +} + +internal abstract class BaseRealmObjectListOperator ( override val mediator: Mediator, override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, override val nativePointer: RealmListPointer, val clazz: KClass, val classKey: ClassKey, @@ -310,21 +474,18 @@ internal abstract class BaseRealmObjectListOperator( override fun get(index: Int): E { return getterScope { val transport = realm_list_get(nativePointer, index.toLong()) - with(valueConverter) { - realmValueToPublic(transport) as E - } + realmValueToRealmObject(transport, clazz, mediator, realmReference) as E } } } -internal class RealmObjectListOperator( +internal class RealmObjectListOperator( mediator: Mediator, realmReference: RealmReference, - converter: RealmValueConverter, nativePointer: RealmListPointer, clazz: KClass, classKey: ClassKey, -) : BaseRealmObjectListOperator(mediator, realmReference, converter, nativePointer, clazz, classKey) { +) : BaseRealmObjectListOperator(mediator, realmReference, nativePointer, clazz, classKey) { override fun insert( index: Int, @@ -361,11 +522,9 @@ internal class RealmObjectListOperator( cache ) val transport = realmObjectTransport(objRef as RealmObjectInterop) - with(valueConverter) { - val originalValue = get(index) - RealmInterop.realm_list_set(nativePointer, index.toLong(), transport) - originalValue - } + val originalValue = get(index) + RealmInterop.realm_list_set(nativePointer, index.toLong(), transport) + originalValue } } @@ -373,12 +532,9 @@ internal class RealmObjectListOperator( realmReference: RealmReference, nativePointer: RealmListPointer ): ListOperator { - val converter: RealmValueConverter = - converter(clazz, mediator, realmReference) as CompositeConverter return RealmObjectListOperator( mediator, realmReference, - converter, nativePointer, clazz, classKey @@ -389,11 +545,10 @@ internal class RealmObjectListOperator( internal class EmbeddedRealmObjectListOperator( mediator: Mediator, realmReference: RealmReference, - converter: RealmValueConverter, nativePointer: RealmListPointer, clazz: KClass, classKey: ClassKey, -) : BaseRealmObjectListOperator(mediator, realmReference, converter, nativePointer, clazz, classKey) { +) : BaseRealmObjectListOperator(mediator, realmReference, nativePointer, clazz, classKey) { @Suppress("UNCHECKED_CAST") override fun insert( @@ -423,11 +578,9 @@ internal class EmbeddedRealmObjectListOperator( // return null as this is not allowed for lists with non-nullable elements, so just return // the newly created object even though it goes against the list API. val embedded = realm_list_set_embedded(nativePointer, index.toLong()) - with(valueConverter) { - val newEmbeddedRealmObject = realmValueToPublic(embedded) as BaseRealmObject - RealmObjectHelper.assign(newEmbeddedRealmObject, element, updatePolicy, cache) - newEmbeddedRealmObject as E - } + val newEmbeddedRealmObject = realmValueToRealmObject(embedded, clazz, mediator, realmReference) as E + RealmObjectHelper.assign(newEmbeddedRealmObject, element, updatePolicy, cache) + newEmbeddedRealmObject } } @@ -435,12 +588,9 @@ internal class EmbeddedRealmObjectListOperator( realmReference: RealmReference, nativePointer: RealmListPointer ): EmbeddedRealmObjectListOperator { - val converter: RealmValueConverter = - converter(clazz, mediator, realmReference) as CompositeConverter return EmbeddedRealmObjectListOperator( mediator, realmReference, - converter, nativePointer, clazz, classKey diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt index 884f771f72..f68187d336 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.Versioned +import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.isManaged import io.realm.kotlin.internal.RealmValueArgumentConverter.convertToQueryArgs @@ -35,6 +36,7 @@ import io.realm.kotlin.internal.interop.RealmMapPointer import io.realm.kotlin.internal.interop.RealmNotificationTokenPointer import io.realm.kotlin.internal.interop.RealmObjectInterop import io.realm.kotlin.internal.interop.RealmResultsPointer +import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.getterScope import io.realm.kotlin.internal.interop.inputScope import io.realm.kotlin.internal.query.ObjectBoundQuery @@ -49,6 +51,7 @@ import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmMap +import io.realm.kotlin.types.RealmObject import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow import kotlin.reflect.KClass @@ -58,7 +61,7 @@ import kotlin.reflect.KClass // ---------------------------------------------------------------------- internal abstract class ManagedRealmMap constructor( - internal val parent: RealmObjectReference<*>, + internal val parent: RealmObjectReference<*>?, internal val nativePointer: RealmMapPointer, val operator: MapOperator ) : AbstractMutableMap(), RealmMap, CoreNotifiable, MapChange>, Flowable> { @@ -109,7 +112,7 @@ internal abstract class ManagedRealmMap constructor( ): RealmNotificationTokenPointer = RealmInterop.realm_dictionary_add_notification_callback(nativePointer, callback) - internal fun isValid(): Boolean = + override fun isValid(): Boolean = !nativePointer.isReleased() && RealmInterop.realm_dictionary_is_valid(nativePointer) // TODO add equals and hashCode and tests for those. Observe this constrain @@ -127,6 +130,12 @@ internal fun ManagedRealmMap.query( val mapValues = values as RealmMapValues<*, *> RealmInterop.realm_query_parse_for_results(mapValues.resultsPointer, query, queryArgs) } + // parent is only available for lists with an object as an immediate parent (contrary to nested + // collections). + // Nested collections are only supported for RealmAny-values and are therefore + // outside of the BaseRealmObject bound for the generic type parameters, so we should never be + // able to reach here for nested collections of RealmAny. + if (parent == null) error("Cannot perform subqueries on non-object dictionaries") return ObjectBoundQuery( parent, ObjectQuery( @@ -211,14 +220,7 @@ internal interface MapOperator : CollectionOperator { } @Suppress("UNCHECKED_CAST") - fun getValue(resultsPointer: RealmResultsPointer, index: Int): V? { - return getterScope { - with(valueConverter) { - val transport = realm_results_get(resultsPointer, index.toLong()) - realmValueToPublic(transport) - } as V - } - } + fun getValue(resultsPointer: RealmResultsPointer, index: Int): V? @Suppress("UNCHECKED_CAST") fun getKey(resultsPointer: RealmResultsPointer, index: Int): K { @@ -282,7 +284,7 @@ internal interface MapOperator : CollectionOperator { internal open class PrimitiveMapOperator constructor( override val mediator: Mediator, override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, + val realmValueConverter: RealmValueConverter, override val keyConverter: RealmValueConverter, override val nativePointer: RealmMapPointer ) : MapOperator { @@ -297,7 +299,7 @@ internal open class PrimitiveMapOperator constructor( ): Pair { return inputScope { val keyTransport = with(keyConverter) { publicToRealmValue(key) } - with(valueConverter) { + with(realmValueConverter) { val valueTransport = publicToRealmValue(value) realm_dictionary_insert( nativePointer, @@ -313,7 +315,7 @@ internal open class PrimitiveMapOperator constructor( override fun eraseInternal(key: K): Pair { return inputScope { val keyTransport = with(keyConverter) { publicToRealmValue(key) } - with(valueConverter) { + with(realmValueConverter) { realm_dictionary_erase(nativePointer, keyTransport).let { Pair(realmValueToPublic(it.first), it.second) } @@ -327,19 +329,28 @@ internal open class PrimitiveMapOperator constructor( realm_dictionary_get(nativePointer, position) .let { val key = with(keyConverter) { realmValueToPublic(it.first) } - val value = with(valueConverter) { realmValueToPublic(it.second) } + val value = with(realmValueConverter) { realmValueToPublic(it.second) } Pair(key, value) } as Pair } } + override fun getValue(resultsPointer: RealmResultsPointer, index: Int): V? { + return getterScope { + with(realmValueConverter) { + val transport = realm_results_get(resultsPointer, index.toLong()) + realmValueToPublic(transport) + } as V + } + } + override fun getInternal(key: K): V? { // Even though we are getting a value we need to free the data buffers of the string we // send down to Core, so we need to use an inputScope. return inputScope { val keyTransport = with(keyConverter) { publicToRealmValue(key) } val valueTransport = realm_dictionary_find(nativePointer, keyTransport) - with(valueConverter) { realmValueToPublic(valueTransport) } + with(realmValueConverter) { realmValueToPublic(valueTransport) } } } @@ -347,7 +358,8 @@ internal open class PrimitiveMapOperator constructor( // Even though we are getting a value we need to free the data buffers of the string values // we send down to Core, so we need to use an inputScope. return inputScope { - with(valueConverter) { + // FIXME This could potentially import an object? + with(realmValueConverter) { RealmInterop.realm_dictionary_contains_value( nativePointer, publicToRealmValue(value) @@ -366,22 +378,44 @@ internal open class PrimitiveMapOperator constructor( realmReference: RealmReference, nativePointer: RealmMapPointer ): MapOperator = - PrimitiveMapOperator(mediator, realmReference, valueConverter, keyConverter, nativePointer) + PrimitiveMapOperator(mediator, realmReference, realmValueConverter, keyConverter, nativePointer) } -internal class RealmAnyMapOperator constructor( +internal fun realmAnyMapOperator( mediator: Mediator, - realmReference: RealmReference, - valueConverter: RealmValueConverter, - keyConverter: RealmValueConverter, - nativePointer: RealmMapPointer -) : PrimitiveMapOperator( + realm: RealmReference, + nativePointer: RealmMapPointer, + issueDynamicObject: Boolean = false, + issueDynamicMutableObject: Boolean = false, +): RealmAnyMapOperator = RealmAnyMapOperator( mediator, - realmReference, - valueConverter, - keyConverter, - nativePointer -) { + realm, + converter(String::class), + nativePointer, + issueDynamicObject, + issueDynamicMutableObject +) +@Suppress("LongParameterList") +internal class RealmAnyMapOperator constructor( + override val mediator: Mediator, + override val realmReference: RealmReference, + override val keyConverter: RealmValueConverter, + override val nativePointer: RealmMapPointer, + private val issueDynamicObject: Boolean, + private val issueDynamicMutableObject: Boolean +) : MapOperator { + + override var modCount: Int = 0 + + override fun eraseInternal(key: K): Pair { + return inputScope { + val keyTransport = with(keyConverter) { publicToRealmValue(key) } + realm_dictionary_erase(nativePointer, keyTransport).let { + Pair(realmAny(it.first, keyTransport), it.second) + } + } + } + override fun containsValueInternal(value: RealmAny?): Boolean { // Unmanaged objects are never found in a managed dictionary if (value?.type == RealmAny.Type.OBJECT) { @@ -391,21 +425,143 @@ internal class RealmAnyMapOperator constructor( // Even though we are getting a value we need to free the data buffers of the string values // we send down to Core, so we need to use an inputScope. return inputScope { - with(valueConverter) { - RealmInterop.realm_dictionary_contains_value( - nativePointer, - publicToRealmValue(value) - ) - } + RealmInterop.realm_dictionary_contains_value( + nativePointer, + realmAnyToRealmValueWithoutImport(value) + ) + } + } + + @Suppress("UNCHECKED_CAST") + override fun getEntryInternal(position: Int): Pair { + return getterScope { + realm_dictionary_get(nativePointer, position) + .let { + val keyTransport: K = with(keyConverter) { realmValueToPublic(it.first) as K } + return keyTransport to getInternal(keyTransport) + } + } + } + + override fun getValue(resultsPointer: RealmResultsPointer, index: Int): RealmAny? { + return getterScope { + val transport = realm_results_get(resultsPointer, index.toLong()) + realmValueToRealmAny( + realmValue = transport, + parent = null, + mediator = mediator, + owner = realmReference, + issueDynamicObject = issueDynamicObject, + issueDynamicMutableObject = issueDynamicMutableObject, + getSetFunction = { RealmInterop.realm_results_get_set(resultsPointer, index.toLong()) }, + getListFunction = { RealmInterop.realm_results_get_list(resultsPointer, index.toLong()) }, + getDictionaryFunction = { RealmInterop.realm_results_get_dictionary(resultsPointer, index.toLong()) }, + ) + } + } + + override fun copy( + realmReference: RealmReference, + nativePointer: RealmMapPointer + ): MapOperator = + RealmAnyMapOperator(mediator, realmReference, keyConverter, nativePointer, issueDynamicObject, issueDynamicMutableObject) + + override fun areValuesEqual(expected: RealmAny?, actual: RealmAny?): Boolean { + return expected == actual + } + + override fun getInternal(key: K): RealmAny? { + return inputScope { + val keyTransport: RealmValue = with(keyConverter) { publicToRealmValue(key) } + val valueTransport: RealmValue = realm_dictionary_find(nativePointer, keyTransport) + realmAny(valueTransport, keyTransport) + } + } + + private fun realmAny( + valueTransport: RealmValue, + keyTransport: RealmValue + ) = realmValueToRealmAny( + valueTransport, null, mediator, realmReference, + issueDynamicObject, + issueDynamicMutableObject, + { RealmInterop.realm_dictionary_find_set(nativePointer, keyTransport) }, + { RealmInterop.realm_dictionary_find_list(nativePointer, keyTransport) } + ) { RealmInterop.realm_dictionary_find_dictionary(nativePointer, keyTransport) } + + override fun insertInternal( + key: K, + value: RealmAny?, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectCache + ): Pair { + return inputScope { + val keyTransport = with(keyConverter) { publicToRealmValue(key) } + return realmAnyHandler( + value, + primitiveValueAsRealmValueHandler = { + realm_dictionary_insert(nativePointer, keyTransport, it).let { result -> + realmAny(result.first, keyTransport) to result.second + } + }, + referenceAsRealmAnyHandler = { + val obj = when (issueDynamicObject) { + true -> it.asRealmObject() + false -> it.asRealmObject() + } + val objRef = realmObjectToRealmReferenceWithImport(obj, mediator, realmReference, updatePolicy, cache) + val transport = realmObjectTransport(objRef as RealmObjectInterop) + realm_dictionary_insert(nativePointer, keyTransport, transport).let { result -> + realmAny(result.first, keyTransport) to result.second + } + }, + setAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) + val previous = getInternal(key) + val nativePointer = RealmInterop.realm_dictionary_insert_set(nativePointer, keyTransport) + val operator = realmAnySetOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.addAll(realmValue.asSet(), updatePolicy, cache) + previous to true + }, + listAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) + val previous = getInternal(key) + val nativePointer = RealmInterop.realm_dictionary_insert_list(nativePointer, keyTransport) + val operator = realmAnyListOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.insertAll(0, realmValue.asList(), updatePolicy, cache) + previous to true + }, + dictionaryAsRealmAnyHandler = { realmValue -> + // Have to clear existing elements for core to know if we are updating with a new collection + realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) + val previous = getInternal(key) + val nativePointer = RealmInterop.realm_dictionary_insert_dictionary(nativePointer, keyTransport) + val operator = + realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) + operator.putAll(realmValue.asDictionary(), updatePolicy, cache) + previous to true + } + ) } } } @Suppress("LongParameterList") -internal abstract class BaseRealmObjectMapOperator constructor( +internal abstract class BaseRealmObjectMapOperator constructor( override val mediator: Mediator, override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, override val keyConverter: RealmValueConverter, override val nativePointer: RealmMapPointer, val clazz: KClass, @@ -462,6 +618,13 @@ internal abstract class BaseRealmObjectMapOperator constructor( } as V? } + override fun getValue(resultsPointer: RealmResultsPointer, index: Int): V? { + return getterScope { + val transport = realm_results_get(resultsPointer, index.toLong()) + realmValueToRealmObject(transport, clazz, mediator, realmReference) + } + } + override fun containsValueInternal(value: V): Boolean { value?.also { // Unmanaged objects are never found in a managed dictionary @@ -471,12 +634,10 @@ internal abstract class BaseRealmObjectMapOperator constructor( // Even though we are getting a value we need to free the data buffers of the string we // send down to Core, so we need to use an inputScope. return inputScope { - with(valueConverter) { - RealmInterop.realm_dictionary_contains_value( - nativePointer, - publicToRealmValue(value) - ) - } + RealmInterop.realm_dictionary_contains_value( + nativePointer, + realmObjectToRealmValue(value) + ) } } @@ -496,8 +657,7 @@ internal abstract class BaseRealmObjectMapOperator constructor( ): MapOperator = RealmObjectMapOperator( mediator, realmReference, - converter(clazz, mediator, realmReference), - converter(String::class, mediator, realmReference) as RealmValueConverter, + converter(String::class) as RealmValueConverter, nativePointer, clazz, classKey @@ -505,10 +665,9 @@ internal abstract class BaseRealmObjectMapOperator constructor( } @Suppress("LongParameterList") -internal class RealmObjectMapOperator constructor( +internal class RealmObjectMapOperator constructor( mediator: Mediator, realmReference: RealmReference, - valueConverter: RealmValueConverter, keyConverter: RealmValueConverter, nativePointer: RealmMapPointer, clazz: KClass, @@ -516,7 +675,6 @@ internal class RealmObjectMapOperator constructor( ) : BaseRealmObjectMapOperator( mediator, realmReference, - valueConverter, keyConverter, nativePointer, clazz, @@ -561,7 +719,6 @@ internal class RealmObjectMapOperator constructor( internal class EmbeddedRealmObjectMapOperator constructor( mediator: Mediator, realmReference: RealmReference, - valueConverter: RealmValueConverter, keyConverter: RealmValueConverter, nativePointer: RealmMapPointer, clazz: KClass, @@ -569,7 +726,6 @@ internal class EmbeddedRealmObjectMapOperator constructo ) : BaseRealmObjectMapOperator( mediator, realmReference, - valueConverter, keyConverter, nativePointer, clazz, @@ -601,11 +757,9 @@ internal class EmbeddedRealmObjectMapOperator constructo // We cannot return the old object as it is deleted when losing its parent so just // return the newly created object even though it goes against the API val embedded = realm_dictionary_insert_embedded(nativePointer, keyTransport) - with(valueConverter) { - val newEmbeddedRealmObject = realmValueToPublic(embedded) as BaseRealmObject - RealmObjectHelper.assign(newEmbeddedRealmObject, value, updatePolicy, cache) - Pair(newEmbeddedRealmObject, true) - } + val newEmbeddedRealmObject = realmValueToRealmObject(embedded, clazz, mediator, realmReference) as V + RealmObjectHelper.assign(newEmbeddedRealmObject, value, updatePolicy, cache) + Pair(newEmbeddedRealmObject, true) } as Pair } } @@ -639,30 +793,38 @@ internal class UnmanagedRealmDictionary( } internal class ManagedRealmDictionary constructor( - parent: RealmObjectReference<*>, + parent: RealmObjectReference<*>?, nativePointer: RealmMapPointer, operator: MapOperator -) : ManagedRealmMap(parent, nativePointer, operator), RealmDictionary, Versioned by operator.realmReference { +) : ManagedRealmMap(parent, nativePointer, operator), + RealmDictionary, + Versioned by operator.realmReference { override fun freeze(frozenRealm: RealmReference): ManagedRealmDictionary? { - return RealmInterop.realm_dictionary_resolve_in(nativePointer, frozenRealm.dbPointer)?.let { - ManagedRealmDictionary(parent, it, operator.copy(frozenRealm, it)) - } + return RealmInterop.realm_dictionary_resolve_in(nativePointer, frozenRealm.dbPointer) + ?.let { + ManagedRealmDictionary(parent, it, operator.copy(frozenRealm, it)) + } } override fun changeFlow(scope: ProducerScope>): ChangeFlow, MapChange> = RealmDictonaryChangeFlow(scope) override fun thaw(liveRealm: RealmReference): ManagedRealmDictionary? { - return RealmInterop.realm_dictionary_resolve_in(nativePointer, liveRealm.dbPointer)?.let { - ManagedRealmDictionary(parent, it, operator.copy(liveRealm, it)) - } + return RealmInterop.realm_dictionary_resolve_in(nativePointer, liveRealm.dbPointer) + ?.let { + ManagedRealmDictionary(parent, it, operator.copy(liveRealm, it)) + } } override fun toString(): String { - val owner = parent.className - val version = parent.owner.version().version - val objKey = RealmInterop.realm_object_get_key(parent.objectPointer).key + val (owner, version, objKey) = parent?.run { + Triple( + className, + owner.version().version, + RealmInterop.realm_object_get_key(objectPointer).key + ) + } ?: Triple("null", operator.realmReference.version().version, "null") return "RealmDictionary{size=$size,owner=$owner,objKey=$objKey,version=$version}" } @@ -671,7 +833,8 @@ internal class ManagedRealmDictionary constructor( internal class RealmDictonaryChangeFlow(scope: ProducerScope>) : ChangeFlow, MapChange>(scope) { - override fun initial(frozenRef: ManagedRealmMap): MapChange = InitialDictionaryImpl(frozenRef) + override fun initial(frozenRef: ManagedRealmMap): MapChange = + InitialDictionaryImpl(frozenRef) override fun update( frozenRef: ManagedRealmMap, @@ -681,7 +844,8 @@ internal class RealmDictonaryChangeFlow(scope: ProducerScope = DeletedDictionaryImpl(UnmanagedRealmDictionary()) + override fun delete(): MapChange = + DeletedDictionaryImpl(UnmanagedRealmDictionary()) } // ---------------------------------------------------------------------- @@ -694,7 +858,7 @@ internal class RealmDictonaryChangeFlow(scope: ProducerScope constructor( private val keysPointer: RealmResultsPointer, private val operator: MapOperator, - private val parent: RealmObjectReference<*> + private val parent: RealmObjectReference<*>? ) : AbstractMutableSet() { override val size: Int @@ -710,9 +874,13 @@ internal class KeySet constructor( } override fun toString(): String { - val owner = parent.className - val version = parent.owner.version().version - val objKey = RealmInterop.realm_object_get_key(parent.objectPointer).key + val (owner, version, objKey) = parent?.run { + Triple( + className, + owner.version().version, + RealmInterop.realm_object_get_key(parent.objectPointer).key + ) + } ?: Triple("null", operator.realmReference.version().version, "null") return "RealmDictionary.keys{size=$size,owner=$owner,objKey=$objKey,version=$version}" } @@ -740,7 +908,7 @@ internal class KeySet constructor( internal class RealmMapValues constructor( internal val resultsPointer: RealmResultsPointer, private val operator: MapOperator, - private val parent: RealmObjectReference<*> + private val parent: RealmObjectReference<*>? ) : AbstractMutableCollection() { override val size: Int @@ -817,9 +985,13 @@ internal class RealmMapValues constructor( } override fun toString(): String { - val owner = parent.className - val version = parent.owner.version().version - val objKey = RealmInterop.realm_object_get_key(parent.objectPointer).key + val (owner, version, objKey) = parent?.run { + Triple( + className, + owner.version().version, + RealmInterop.realm_object_get_key(parent.objectPointer).key + ) + } ?: Triple("null", operator.realmReference.owner.version(), "null") return "RealmDictionary.values{size=$size,owner=$owner,objKey=$objKey,version=$version}" } @@ -935,7 +1107,7 @@ internal abstract class RealmMapGenericIterator( internal class RealmMapEntrySetImpl constructor( private val nativePointer: RealmMapPointer, private val operator: MapOperator, - private val parent: RealmObjectReference<*> + private val parent: RealmObjectReference<*>? ) : AbstractMutableSet>(), RealmMapEntrySet { override val size: Int @@ -956,7 +1128,10 @@ internal class RealmMapEntrySetImpl constructor( @Suppress("UNCHECKED_CAST") override fun getNext(position: Int): MutableMap.MutableEntry { val pair = operator.getEntry(position) - return ManagedRealmMapEntry(pair.first, operator) as MutableMap.MutableEntry + return ManagedRealmMapEntry( + pair.first, + operator + ) as MutableMap.MutableEntry } } @@ -974,9 +1149,13 @@ internal class RealmMapEntrySetImpl constructor( } override fun toString(): String { - val owner = parent.className - val version = parent.owner.version().version - val objKey = RealmInterop.realm_object_get_key(parent.objectPointer).key + val (owner, version, objKey) = parent?.run { + Triple( + className, + owner.version().version, + RealmInterop.realm_object_get_key(parent.objectPointer).key + ) + } ?: Triple("null", operator.realmReference.owner.version(), "null") return "RealmDictionary.entries{size=$size,owner=$owner,objKey=$objKey,version=$version}" } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt index 8351ee4be2..13367aa2ca 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt @@ -190,7 +190,9 @@ internal object RealmObjectHelper { internal inline fun setValueByKey( obj: RealmObjectReference, key: PropertyKey, - value: Any? + value: Any?, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectCache = mutableMapOf() ) { // TODO optimize: avoid this by creating the scope in the accessor via the compiler plugin // See comment in AccessorModifierIrGeneration.modifyAccessor about this. @@ -223,26 +225,45 @@ internal object RealmObjectHelper { ) is MutableRealmInt -> setValueTransportByKey(obj, key, longTransport(value.get())) is RealmAny -> { - val converter = if (value.type == RealmAny.Type.OBJECT) { - when ((value as RealmAnyImpl<*>).clazz) { - DynamicRealmObject::class -> - realmAnyConverter(obj.mediator, obj.owner, true) - DynamicMutableRealmObject::class -> - realmAnyConverter( - obj.mediator, - obj.owner, - issueDynamicObject = true, - issueDynamicMutableObject = true - ) - else -> - realmAnyConverter(obj.mediator, obj.owner) + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { realmValue -> + setValueTransportByKey( + obj, + key, + realmValue + ) + }, + referenceAsRealmAnyHandler = { realmValue -> + setObjectByKey(obj, key, realmValue.asRealmObject(), updatePolicy, cache) + }, + setAsRealmAnyHandler = { realmValue -> + RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) + val nativePointer = RealmInterop.realm_set_set(obj.objectPointer, key) + val operator = realmAnySetOperator( + obj.mediator, + obj.owner, + nativePointer, + false, + false + ) + operator.addAll(value.asSet(), updatePolicy, cache) + }, + listAsRealmAnyHandler = { realmValue -> + RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) + val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) + val operator = + realmAnyListOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.insertAll(0, value.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) + val nativePointer = RealmInterop.realm_set_dictionary(obj.objectPointer, key) + val operator = + realmAnyMapOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.putAll(value.asDictionary(), updatePolicy, cache) } - } else { - realmAnyConverter(obj.mediator, obj.owner) - } - with(converter) { - setValueTransportByKey(obj, key, publicToRealmValue(value)) - } + ) } else -> throw IllegalArgumentException("Unsupported value for transport: $value") } @@ -255,69 +276,83 @@ internal object RealmObjectHelper { internal inline fun getString( obj: RealmObjectReference, propertyName: String - ): String? = getterScope { getValue(obj, propertyName)?.let { realmValueToString(it) } } + ): String? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToString(it) } } internal inline fun getLong( obj: RealmObjectReference, propertyName: String - ): Long? = getterScope { getValue(obj, propertyName)?.let { realmValueToLong(it) } } + ): Long? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToLong(it) } } internal inline fun getBoolean( obj: RealmObjectReference, propertyName: String - ): Boolean? = getterScope { getValue(obj, propertyName)?.let { realmValueToBoolean(it) } } + ): Boolean? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToBoolean(it) } } internal inline fun getFloat( obj: RealmObjectReference, propertyName: String - ): Float? = getterScope { getValue(obj, propertyName)?.let { realmValueToFloat(it) } } + ): Float? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToFloat(it) } } internal inline fun getDouble( obj: RealmObjectReference, propertyName: String - ): Double? = getterScope { getValue(obj, propertyName)?.let { realmValueToDouble(it) } } + ): Double? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToDouble(it) } } internal inline fun getDecimal128( obj: RealmObjectReference, propertyName: String - ): Decimal128? = getterScope { getValue(obj, propertyName)?.let { realmValueToDecimal128(it) } } + ): Decimal128? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToDecimal128(it) } } internal inline fun getInstant( obj: RealmObjectReference, propertyName: String ): RealmInstant? = - getterScope { getValue(obj, propertyName)?.let { realmValueToRealmInstant(it) } } + getterScope { getRealmValue(obj, propertyName)?.let { realmValueToRealmInstant(it) } } internal inline fun getObjectId( obj: RealmObjectReference, propertyName: String - ): BsonObjectId? = getterScope { getValue(obj, propertyName)?.let { realmValueToObjectId(it) } } + ): BsonObjectId? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToObjectId(it) } } internal inline fun getUUID( obj: RealmObjectReference, propertyName: String - ): RealmUUID? = getterScope { getValue(obj, propertyName)?.let { realmValueToRealmUUID(it) } } + ): RealmUUID? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToRealmUUID(it) } } internal inline fun getByteArray( obj: RealmObjectReference, propertyName: String - ): ByteArray? = getterScope { getValue(obj, propertyName)?.let { realmValueToByteArray(it) } } + ): ByteArray? = getterScope { getRealmValue(obj, propertyName)?.let { realmValueToByteArray(it) } } internal inline fun getRealmAny( obj: RealmObjectReference, propertyName: String ): RealmAny? = getterScope { - getValue(obj, propertyName) - ?.let { realmValueToRealmAny(it, obj.mediator, obj.owner) } + val key = obj.propertyInfoOrThrow(propertyName).key + getRealmValueFromKey(obj, key) + ?.let { + realmValueToRealmAny( + it, obj, obj.mediator, obj.owner, + false, + false, + { RealmInterop.realm_get_set(obj.objectPointer, key) }, + { RealmInterop.realm_get_list(obj.objectPointer, key) } + ) { RealmInterop.realm_get_dictionary(obj.objectPointer, key) } + } } - internal inline fun MemAllocator.getValue( + internal inline fun MemAllocator.getRealmValue( obj: RealmObjectReference, propertyName: String, + ): RealmValue? = getRealmValueFromKey(obj, obj.propertyInfoOrThrow(propertyName).key) + + internal inline fun MemAllocator.getRealmValueFromKey( + obj: RealmObjectReference, + propertyKey: PropertyKey ): RealmValue? { val realmValue = realm_get_value( obj.objectPointer, - obj.propertyInfoOrThrow(propertyName).key + propertyKey ) return when (realmValue.isNull()) { true -> null @@ -347,7 +382,7 @@ internal object RealmObjectHelper { obj: RealmObjectReference, propertyName: String ): ManagedMutableRealmInt? { - val converter = converter(Long::class, obj.mediator, obj.owner) + val converter = converter(Long::class) val propertyKey = obj.propertyInfoOrThrow(propertyName).key // In order to be able to use Kotlin's nullability handling baked into the accessor we need @@ -434,32 +469,31 @@ internal object RealmObjectHelper { CollectionOperatorType.PRIMITIVE -> PrimitiveListOperator( mediator, realm, - converter(clazz, mediator, realm) as CompositeConverter, + converter(clazz) as CompositeConverter, listPtr ) - CollectionOperatorType.REALM_ANY -> PrimitiveListOperator( + CollectionOperatorType.REALM_ANY -> RealmAnyListOperator( mediator, realm, - realmAnyConverter(mediator, realm, issueDynamicObject, issueDynamicMutableObject), - listPtr + listPtr, + issueDynamicObject = issueDynamicObject, + issueDynamicMutableObject = issueDynamicMutableObject ) as ListOperator CollectionOperatorType.REALM_OBJECT -> { val classKey: ClassKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey RealmObjectListOperator( mediator, realm, - converter(clazz, mediator, realm) as CompositeConverter, listPtr, - clazz, + clazz as KClass, classKey, - ) + ) as ListOperator } CollectionOperatorType.EMBEDDED_OBJECT -> { val classKey: ClassKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey EmbeddedRealmObjectListOperator( mediator, realm, - converter(clazz, mediator, realm) as RealmValueConverter, listPtr, clazz as KClass, classKey, @@ -525,25 +559,25 @@ internal object RealmObjectHelper { CollectionOperatorType.PRIMITIVE -> PrimitiveSetOperator( mediator, realm, - converter(clazz, mediator, realm), + converter(clazz), setPtr ) - CollectionOperatorType.REALM_ANY -> PrimitiveSetOperator( + CollectionOperatorType.REALM_ANY -> RealmAnySetOperator( mediator, realm, - realmAnyConverter(mediator, realm, issueDynamicObject, issueDynamicMutableObject), - setPtr + setPtr, + issueDynamicObject, + issueDynamicMutableObject ) as SetOperator CollectionOperatorType.REALM_OBJECT -> { val classKey: ClassKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey RealmObjectSetOperator( mediator, realm, - converter(clazz, mediator, realm), setPtr, - clazz, + clazz as KClass, classKey - ) + ) as SetOperator } else -> throw IllegalArgumentException("Unsupported collection type: ${operatorType.name}") @@ -610,36 +644,34 @@ internal object RealmObjectHelper { CollectionOperatorType.PRIMITIVE -> PrimitiveMapOperator( mediator, realm, - converter(clazz, mediator, realm), - converter(String::class, mediator, realm), + converter(clazz), + converter(String::class), dictionaryPtr ) CollectionOperatorType.REALM_ANY -> RealmAnyMapOperator( mediator, realm, - realmAnyConverter(mediator, realm, issueDynamicObject, issueDynamicMutableObject), - converter(String::class, mediator, realm), - dictionaryPtr + converter(String::class), + dictionaryPtr, + issueDynamicObject, issueDynamicMutableObject ) as MapOperator CollectionOperatorType.REALM_OBJECT -> { val classKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey RealmObjectMapOperator( mediator, realm, - converter(clazz, mediator, realm), - converter(String::class, mediator, realm), + converter(String::class), dictionaryPtr, - clazz, + clazz as KClass, classKey - ) + ) as MapOperator } CollectionOperatorType.EMBEDDED_OBJECT -> { val classKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey EmbeddedRealmObjectMapOperator( mediator, realm, - converter(clazz, mediator, realm) as RealmValueConverter, - converter(String::class, mediator, realm), + converter(String::class), dictionaryPtr, clazz as KClass, classKey @@ -785,6 +817,16 @@ internal object RealmObjectHelper { ) } } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { + val value = accessor.get(source) + setValueByKey( + target.realmObjectReference!!, + property.key, + value, + updatePolicy, + cache + ) + } else -> { val getterValue = accessor.get(source) accessor.set(target, getterValue) @@ -898,12 +940,16 @@ internal object RealmObjectHelper { obj.owner ) RealmAny::class -> realmValueToRealmAny( - transport, - obj.mediator, - obj.owner, - true, - issueDynamicMutableObject - ) + realmValue = transport, + parent = obj, + mediator = obj.mediator, + owner = obj.owner, + issueDynamicObject = true, + issueDynamicMutableObject = issueDynamicMutableObject, + getSetFunction = { RealmInterop.realm_get_set(obj.objectPointer, propertyInfo.key) }, + getListFunction = { RealmInterop.realm_get_list(obj.objectPointer, propertyInfo.key) }, + ) { RealmInterop.realm_get_dictionary(obj.objectPointer, propertyInfo.key) } + else -> with(primitiveTypeConverters.getValue(clazz)) { realmValueToPublic(transport) } @@ -1045,67 +1091,100 @@ internal object RealmObjectHelper { } } when (propertyMetadata.collectionType) { - CollectionType.RLM_COLLECTION_TYPE_NONE -> when (propertyMetadata.type) { - PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { - if (obj.owner.schemaMetadata[propertyMetadata.linkTarget]!!.isEmbeddedRealmObject) { - setEmbeddedRealmObjectByKey( - obj, - propertyMetadata.key, - value as BaseRealmObject?, - updatePolicy, - cache - ) - } else { - setObjectByKey( - obj, - propertyMetadata.key, - value as BaseRealmObject?, - updatePolicy, - cache - ) - } - } - PropertyType.RLM_PROPERTY_TYPE_MIXED -> { - val realmAnyValue = value as RealmAny? - when (realmAnyValue?.type) { - RealmAny.Type.OBJECT -> { - val objValue = value?.let { - val objectClass = ((it as RealmAnyImpl<*>).clazz) as KClass - if (objectClass == DynamicRealmObject::class || objectClass == DynamicMutableRealmObject::class) { - value.asRealmObject() - } else { - throw IllegalArgumentException("Dynamic RealmAny fields only support DynamicRealmObjects or DynamicMutableRealmObjects.") - } - } - val managedObj = realmObjectWithImport( - objValue, - obj.mediator, - obj.owner, + CollectionType.RLM_COLLECTION_TYPE_NONE -> { + val key = propertyMetadata.key + when (propertyMetadata.type) { + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { + if (obj.owner.schemaMetadata[propertyMetadata.linkTarget]!!.isEmbeddedRealmObject) { + setEmbeddedRealmObjectByKey( + obj, + key, + value as BaseRealmObject?, updatePolicy, cache - )!! + ) + } else { setObjectByKey( obj, - propertyMetadata.key, - managedObj, + key, + value as BaseRealmObject?, updatePolicy, cache ) } - else -> inputScope { - val transport = - realmAnyToRealmValueWithObjectImport(value, obj.mediator, obj.owner) - setValueTransportByKey(obj, propertyMetadata.key, transport) + } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { + val realmAnyValue = value as RealmAny? + when (realmAnyValue?.type) { + RealmAny.Type.OBJECT -> { + val objValue = value?.let { + val objectClass = ((it as RealmAnyImpl<*>).clazz) as KClass + if (objectClass == DynamicRealmObject::class || objectClass == DynamicMutableRealmObject::class) { + value.asRealmObject() + } else { + throw IllegalArgumentException("Dynamic RealmAny fields only support DynamicRealmObjects or DynamicMutableRealmObjects.") + } + } + val managedObj = realmObjectWithImport( + objValue, + obj.mediator, + obj.owner, + updatePolicy, + cache + )!! + setObjectByKey( + obj, + key, + managedObj, + updatePolicy, + cache + ) + } + else -> inputScope { + if (value == null) { + setValueTransportByKey(obj, key, nullTransport()) + } else { + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { realmValue -> setValueTransportByKey(obj, key, realmValue) }, + referenceAsRealmAnyHandler = { realmValue -> + setObjectByKey(obj, key, realmValue.asRealmObject(), updatePolicy, cache) + }, + setAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_set(obj.objectPointer, key) + val operator = realmAnySetOperator( + obj.mediator, + obj.owner, + nativePointer, + true, + ) + operator.addAll(value.asSet(), updatePolicy, cache) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) + val operator = + realmAnyListOperator(obj.mediator, obj.owner, nativePointer, true) + operator.insertAll(0, value.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_dictionary(obj.objectPointer, key) + val operator = + realmAnyMapOperator(obj.mediator, obj.owner, nativePointer, true) + operator.putAll(value.asDictionary(), updatePolicy, cache) + } + ) + } + } } } - } - else -> { - val converter = primitiveTypeConverters.getValue(clazz) - .let { converter -> converter as RealmValueConverter } - inputScope { - with(converter) { - val realmValue = publicToRealmValue(value) - setValueTransportByKey(obj, propertyMetadata.key, realmValue) + else -> { + val converter = primitiveTypeConverters.getValue(clazz) + .let { converter -> converter as RealmValueConverter } + inputScope { + with(converter) { + val realmValue = publicToRealmValue(value) + setValueTransportByKey(obj, key, realmValue) + } } } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectReference.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectReference.kt index e282065338..938b575d91 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectReference.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectReference.kt @@ -141,7 +141,7 @@ public class RealmObjectReference( objectPointer.let { RealmInterop.realm_object_delete(it) } } - internal fun isValid(): Boolean { + override fun isValid(): Boolean { val ptr = objectPointer return if (ptr != null) { !ptr.isReleased() && RealmInterop.realm_object_is_valid(ptr) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmResultsImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmResultsImpl.kt index 87dab43f86..dc0cf94f80 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmResultsImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmResultsImpl.kt @@ -35,7 +35,6 @@ import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.query.RealmResults import io.realm.kotlin.query.TRUE_PREDICATE import io.realm.kotlin.types.BaseRealmObject -import io.realm.kotlin.types.RealmObject import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow import kotlin.reflect.KClass @@ -57,13 +56,6 @@ internal class RealmResultsImpl constructor( private val mode: Mode = Mode.RESULTS, ) : AbstractList(), RealmResults, InternalDeleteable, CoreNotifiable, ResultsChange>, RealmStateHolder { - @Suppress("UNCHECKED_CAST") - private val converter = realmObjectConverter( - clazz as KClass, - mediator, - realm - ) as RealmValueConverter - internal enum class Mode { // FIXME Needed to make working with @LinkingObjects easier. EMPTY, // RealmResults that is always empty. @@ -74,10 +66,12 @@ internal class RealmResultsImpl constructor( get() = RealmInterop.realm_results_count(nativePointer).toInt() override fun get(index: Int): E = getterScope { - with(converter) { - val transport = realm_results_get(nativePointer, index.toLong()) - realmValueToPublic(transport) - } as E + realmValueToRealmObject( + realm_results_get(nativePointer, index.toLong()), + clazz, + mediator, + realm + ) as E } override fun query(query: String, vararg args: Any?): RealmQuery = inputScope { @@ -143,7 +137,7 @@ internal class RealmResultsImpl constructor( override fun realmState(): RealmState = realm - internal fun isValid(): Boolean { + override fun isValid(): Boolean { return !nativePointer.isReleased() && !realm.isClosed() } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt index c61098a768..c9513db479 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt @@ -18,6 +18,9 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.Versioned +import io.realm.kotlin.dynamic.DynamicRealmObject +import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.isManaged import io.realm.kotlin.internal.RealmValueArgumentConverter.convertToQueryArgs import io.realm.kotlin.internal.interop.Callback import io.realm.kotlin.internal.interop.ClassKey @@ -27,6 +30,7 @@ import io.realm.kotlin.internal.interop.RealmInterop.realm_set_get import io.realm.kotlin.internal.interop.RealmNotificationTokenPointer import io.realm.kotlin.internal.interop.RealmObjectInterop import io.realm.kotlin.internal.interop.RealmSetPointer +import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.ValueType import io.realm.kotlin.internal.interop.getterScope import io.realm.kotlin.internal.interop.inputScope @@ -38,6 +42,8 @@ import io.realm.kotlin.notifications.internal.InitialSetImpl import io.realm.kotlin.notifications.internal.UpdatedSetImpl import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmSet import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow @@ -46,7 +52,7 @@ import kotlin.reflect.KClass /** * Implementation for unmanaged sets, backed by a [MutableSet]. */ -internal class UnmanagedRealmSet( +public class UnmanagedRealmSet( private val backingSet: MutableSet = mutableSetOf() ) : RealmSet, InternalDeleteable, MutableSet by backingSet { override fun asFlow(): Flow> { @@ -68,7 +74,8 @@ internal class UnmanagedRealmSet( * Implementation for managed sets, backed by Realm. */ internal class ManagedRealmSet constructor( - internal val parent: RealmObjectReference<*>, + // Rework to allow RealmAny + internal val parent: RealmObjectReference<*>?, internal val nativePointer: RealmSetPointer, val operator: SetOperator ) : AbstractMutableSet(), RealmSet, InternalDeleteable, CoreNotifiable, SetChange>, Versioned by operator.realmReference { @@ -188,7 +195,7 @@ internal class ManagedRealmSet constructor( RealmInterop.realm_set_remove_all(nativePointer) } - internal fun isValid(): Boolean { + override fun isValid(): Boolean { return !nativePointer.isReleased() && RealmInterop.realm_set_is_valid(nativePointer) } } @@ -206,6 +213,12 @@ internal fun ManagedRealmSet.query( queryArgs ) } + // parent is only available for lists with an object as an immediate parent (contrary to nested + // collections). + // Nested collections are only supported for RealmAny-values and are therefore + // outside of the BaseRealmObject bound for the generic type parameters, so we should never be + // able to reach here for nested collections of RealmAny. + if (parent == null) error("Cannot perform subqueries on non-object sets") return ObjectBoundQuery( parent, ObjectQuery( @@ -233,7 +246,6 @@ internal interface SetOperator : CollectionOperator { updatePolicy: UpdatePolicy = UpdatePolicy.ALL, cache: UnmanagedToManagedObjectCache = mutableMapOf() ): Boolean { - realmReference.checkClosed() return addInternal(element, updatePolicy, cache) .also { modCount++ } } @@ -276,13 +288,11 @@ internal interface SetOperator : CollectionOperator { modCount++ } + fun removeInternal(element: E): Boolean fun remove(element: E): Boolean { - return inputScope { - with(valueConverter) { - val transport = publicToRealmValue(element) - RealmInterop.realm_set_erase(nativePointer, transport) - } - }.also { modCount++ } + return removeInternal(element).also { + modCount++ + } } fun removeAll(elements: Collection): Boolean { @@ -296,10 +306,103 @@ internal interface SetOperator : CollectionOperator { fun copy(realmReference: RealmReference, nativePointer: RealmSetPointer): SetOperator } +internal fun realmAnySetOperator( + mediator: Mediator, + realm: RealmReference, + nativePointer: RealmSetPointer, + issueDynamicObject: Boolean = false, + issueDynamicMutableObject: Boolean = false, +): RealmAnySetOperator = RealmAnySetOperator( + mediator, + realm, + nativePointer, + issueDynamicObject, + issueDynamicMutableObject +) + +internal class RealmAnySetOperator( + override val mediator: Mediator, + override val realmReference: RealmReference, + override val nativePointer: RealmSetPointer, + val issueDynamicObject: Boolean, + val issueDynamicMutableObject: Boolean +) : SetOperator { + + override var modCount: Int = 0 + + @Suppress("UNCHECKED_CAST") + override fun get(index: Int): RealmAny? { + return getterScope { + val transport = realm_set_get(nativePointer, index.toLong()) + return realmValueToRealmAny( + transport, null, mediator, realmReference, + issueDynamicObject, + issueDynamicMutableObject, + { error("Set should never container sets") }, + { error("Set should never container lists") } + ) { error("Set should never container dictionaries") } + } + } + + override fun addInternal( + element: RealmAny?, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectCache + ): Boolean { + return inputScope { + realmAnyHandler( + value = element, + primitiveValueAsRealmValueHandler = { realmValue: RealmValue -> + RealmInterop.realm_set_insert(nativePointer, realmValue) + }, + referenceAsRealmAnyHandler = { realmValue -> + val obj = when (issueDynamicObject) { + true -> realmValue.asRealmObject() + false -> realmValue.asRealmObject() + } + val objRef = + realmObjectToRealmReferenceWithImport(obj, mediator, realmReference, updatePolicy, cache) + RealmInterop.realm_set_insert(nativePointer, realmObjectTransport(objRef)) + }, + setAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections") }, + listAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, + dictionaryAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, + ) + } + } + + override fun removeInternal(element: RealmAny?): Boolean { + if (element?.type == RealmAny.Type.OBJECT) { + if (!element.asRealmObject().isManaged()) return false + } + return inputScope { + val transport = realmAnyToRealmValueWithoutImport(element) + RealmInterop.realm_set_erase(nativePointer, transport) + } + } + + override fun contains(element: RealmAny?): Boolean { + // Unmanaged objects are never found in a managed dictionary + if (element?.type == RealmAny.Type.OBJECT) { + if (!element.asRealmObject().isManaged()) return false + } + return inputScope { + val transport = realmAnyToRealmValueWithoutImport(element) + RealmInterop.realm_set_find(nativePointer, transport) + } + } + + override fun copy( + realmReference: RealmReference, + nativePointer: RealmSetPointer + ): SetOperator = + RealmAnySetOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) +} + internal class PrimitiveSetOperator( override val mediator: Mediator, override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, + val realmValueConverter: RealmValueConverter, override val nativePointer: RealmSetPointer ) : SetOperator { @@ -308,7 +411,7 @@ internal class PrimitiveSetOperator( @Suppress("UNCHECKED_CAST") override fun get(index: Int): E { return getterScope { - with(valueConverter) { + with(realmValueConverter) { val transport = realm_set_get(nativePointer, index.toLong()) realmValueToPublic(transport) } as E @@ -321,16 +424,25 @@ internal class PrimitiveSetOperator( cache: UnmanagedToManagedObjectCache ): Boolean { return inputScope { - with(valueConverter) { + with(realmValueConverter) { val transport = publicToRealmValue(element) RealmInterop.realm_set_insert(nativePointer, transport) } } } + override fun removeInternal(element: E): Boolean { + return inputScope { + with(realmValueConverter) { + val transport = publicToRealmValue(element) + RealmInterop.realm_set_erase(nativePointer, transport) + } + } + } + override fun contains(element: E): Boolean { return inputScope { - with(valueConverter) { + with(realmValueConverter) { val transport = publicToRealmValue(element) RealmInterop.realm_set_find(nativePointer, transport) } @@ -341,17 +453,30 @@ internal class PrimitiveSetOperator( realmReference: RealmReference, nativePointer: RealmSetPointer ): SetOperator = - PrimitiveSetOperator(mediator, realmReference, valueConverter, nativePointer) + PrimitiveSetOperator(mediator, realmReference, realmValueConverter, nativePointer) } -internal class RealmObjectSetOperator constructor( - override val mediator: Mediator, - override val realmReference: RealmReference, - override val valueConverter: RealmValueConverter, - override val nativePointer: RealmSetPointer, - val clazz: KClass, +internal class RealmObjectSetOperator : SetOperator { + + override val mediator: Mediator + override val realmReference: RealmReference + override val nativePointer: RealmSetPointer + val clazz: KClass val classKey: ClassKey -) : SetOperator { + + constructor( + mediator: Mediator, + realmReference: RealmReference, + nativePointer: RealmSetPointer, + clazz: KClass, + classKey: ClassKey + ) { + this.mediator = mediator + this.realmReference = realmReference + this.nativePointer = nativePointer + this.clazz = clazz + this.classKey = classKey + } override var modCount: Int = 0 @@ -376,27 +501,34 @@ internal class RealmObjectSetOperator constructor( @Suppress("UNCHECKED_CAST") override fun get(index: Int): E { return getterScope { - with(valueConverter) { - realm_set_get(nativePointer, index.toLong()) - .let { transport -> - when (ValueType.RLM_TYPE_NULL) { - transport.getType() -> null - else -> realmValueToPublic(transport) - } - } as E - } + realm_set_get(nativePointer, index.toLong()) + .let { transport -> + when (ValueType.RLM_TYPE_NULL) { + transport.getType() -> null + else -> realmValueToRealmObject(transport, clazz, mediator, realmReference) + } + } as E + } + } + + override fun removeInternal(element: E): Boolean { + // Unmanaged objects are never found in a managed set + element?.also { + if (!(it as RealmObjectInternal).isManaged()) return false + } + return inputScope { + val transport = realmObjectToRealmValue(element as BaseRealmObject?) + RealmInterop.realm_set_erase(nativePointer, transport) } } override fun contains(element: E): Boolean { + // Unmanaged objects are never found in a managed set + element?.also { + if (!(it as RealmObjectInternal).isManaged()) return false + } return inputScope { - val objRef = realmObjectToRealmReferenceWithImport( - element as BaseRealmObject?, - mediator, - realmReference, - UpdatePolicy.ALL, - mutableMapOf() - ) + val objRef = realmObjectToRealmReferenceOrError(element as BaseRealmObject?) val transport = realmObjectTransport(objRef as RealmObjectInterop) RealmInterop.realm_set_find(nativePointer, transport) } @@ -406,9 +538,7 @@ internal class RealmObjectSetOperator constructor( realmReference: RealmReference, nativePointer: RealmSetPointer ): SetOperator { - val converter = - converter(clazz, mediator, realmReference) as CompositeConverter - return RealmObjectSetOperator(mediator, realmReference, converter, nativePointer, clazz, classKey) + return RealmObjectSetOperator(mediator, realmReference, nativePointer, clazz, classKey) } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt index 702dd178fa..eea159ef51 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt @@ -23,7 +23,7 @@ import io.realm.kotlin.VersionId import io.realm.kotlin.ext.isManaged import io.realm.kotlin.ext.isValid import io.realm.kotlin.internal.RealmObjectHelper.assign -import io.realm.kotlin.internal.RealmValueArgumentConverter.kAnyToRealmValue +import io.realm.kotlin.internal.RealmValueArgumentConverter.kAnyToPrimaryKeyRealmValue import io.realm.kotlin.internal.dynamic.DynamicUnmanagedRealmObject import io.realm.kotlin.internal.interop.ClassKey import io.realm.kotlin.internal.interop.ObjectKey @@ -203,7 +203,7 @@ internal fun copyToRealm( realmReference, element::class, className, - kAnyToRealmValue(primaryKey), + kAnyToPrimaryKeyRealmValue(primaryKey), updatePolicy ) } catch (e: IllegalStateException) { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt index 3d8bfb49e4..7f758af61d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -103,7 +103,7 @@ internal class SuspendableNotifier( // notifications on newer objects. realm.refresh() val observable = flowable.notifiable() - val lifeRef = observable.coreObservable(realm) + val lifeRef: CoreNotifiable? = observable.coreObservable(realm) val changeFlow = observable.changeFlow(this@callbackFlow) // Only emit events during registration if the observed entity is already deleted // (lifeRef == null) as there is no guarantee when the first callback is delivered @@ -118,7 +118,16 @@ internal class SuspendableNotifier( override fun onChange(change: RealmChangesPointer) { // Notifications need to be delivered with the version they where created on, otherwise // the fine-grained notification data might be out of sync. - val frozenObservable = lifeRef.freeze(realm.gcTrackedSnapshot()) + // TODO Currently verifying that lifeRef is still valid to indicate + // if it was actually deleted. This is only a problem for + // collections as they seemed to be freezable from a delete + // reference (contrary to other objects that returns null from + // freeze). An `out_collection_was_deleted` flag was added to the + // change object, which would probably be the way to go, but + // requires rework of our change set build infrastructure. + val frozenObservable: T? = if (lifeRef.isValid()) + lifeRef.freeze(realm.gcTrackedSnapshot()) + else null changeFlow.emit(frozenObservable, change) } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ScalarQuery.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ScalarQuery.kt index 43e9964d93..3bc8bd498a 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ScalarQuery.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ScalarQuery.kt @@ -18,12 +18,16 @@ package io.realm.kotlin.internal.query import io.realm.kotlin.TypedRealm import io.realm.kotlin.dynamic.DynamicRealm +import io.realm.kotlin.internal.Decimal128Converter +import io.realm.kotlin.internal.DoubleConverter +import io.realm.kotlin.internal.FloatConverter +import io.realm.kotlin.internal.IntConverter import io.realm.kotlin.internal.Mediator import io.realm.kotlin.internal.Notifiable import io.realm.kotlin.internal.Observable +import io.realm.kotlin.internal.RealmInstantConverter import io.realm.kotlin.internal.RealmReference import io.realm.kotlin.internal.RealmResultsImpl -import io.realm.kotlin.internal.RealmValueConverter import io.realm.kotlin.internal.interop.ClassKey import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop @@ -33,10 +37,8 @@ import io.realm.kotlin.internal.interop.RealmInterop.realm_results_sum import io.realm.kotlin.internal.interop.RealmQueryPointer import io.realm.kotlin.internal.interop.RealmResultsPointer import io.realm.kotlin.internal.interop.RealmValue -import io.realm.kotlin.internal.interop.ValueType import io.realm.kotlin.internal.interop.getterScope -import io.realm.kotlin.internal.primitiveTypeConverters -import io.realm.kotlin.internal.realmAnyConverter +import io.realm.kotlin.internal.realmValueToRealmAny import io.realm.kotlin.internal.schema.PropertyMetadata import io.realm.kotlin.notifications.ResultsChange import io.realm.kotlin.query.RealmQuery @@ -50,7 +52,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import org.mongodb.kbson.BsonDecimal128 -import org.mongodb.kbson.Decimal128 import kotlin.reflect.KClass /** @@ -107,9 +108,9 @@ internal class CountQuery constructor( * Type-bound query linked to a property. Unlike [CountQuery] this is executed at a table level * rather than at a column level. */ -internal interface TypeBoundQuery { +internal interface TypeBoundQuery { val propertyMetadata: PropertyMetadata - val converter: RealmValueConverter<*> + val converter: (RealmValue) -> T? } /** @@ -126,15 +127,20 @@ internal class MinMaxQuery constructor( override val propertyMetadata: PropertyMetadata, private val type: KClass, private val queryType: AggregatorQueryType -) : BaseScalarQuery(realmReference, queryPointer, mediator, classKey, clazz), TypeBoundQuery, RealmScalarNullableQuery { +) : BaseScalarQuery(realmReference, queryPointer, mediator, classKey, clazz), TypeBoundQuery, RealmScalarNullableQuery { - override val converter: RealmValueConverter<*> = when (propertyMetadata.type) { - PropertyType.RLM_PROPERTY_TYPE_INT -> primitiveTypeConverters[Long::class]!! - PropertyType.RLM_PROPERTY_TYPE_FLOAT -> primitiveTypeConverters[Float::class]!! - PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> primitiveTypeConverters[Double::class]!! - PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> primitiveTypeConverters[Decimal128::class]!! - PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> primitiveTypeConverters[RealmInstant::class]!! - PropertyType.RLM_PROPERTY_TYPE_MIXED -> realmAnyConverter(mediator, realmReference) + @Suppress("ExplicitItLambdaParameter") + override val converter: (RealmValue) -> T? = when (propertyMetadata.type) { + PropertyType.RLM_PROPERTY_TYPE_INT -> { it -> IntConverter.fromRealmValue(it)?.let { coerceLong(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> { it -> FloatConverter.fromRealmValue(it)?.let { coerceFloat(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> { it -> DoubleConverter.fromRealmValue(it)?.let { coerceDouble(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> { it -> RealmInstantConverter.fromRealmValue(it) as T? } + PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> { it -> Decimal128Converter.fromRealmValue(it) as T? } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { it -> + // Mixed fields rely on updated realmReference to resolve objects, so postpone + // conversion until values are resolved to unity immediate and async results + error("Mixed values should be aggregated elsewhere") + } else -> throw IllegalArgumentException("Conversion not possible between '$type' and '${type.simpleName}'.") } @@ -143,7 +149,7 @@ internal class MinMaxQuery constructor( queryTypeValidator(propertyMetadata, type, validateTimestamp = true) } - override fun find(): T? = findFromResults(RealmInterop.realm_query_find_all(queryPointer)) + override fun find(): T? = findFromResults(RealmInterop.realm_query_find_all(queryPointer), realmReference) override fun asFlow(): Flow { realmReference.checkClosed() @@ -160,7 +166,7 @@ internal class MinMaxQuery constructor( // e.g. when computing MAX on a RealmAny property when the MAX value is a RealmObject private fun findFromResults( resultsPointer: RealmResultsPointer, - updatedRealmReference: RealmReference? = null + realmReference: RealmReference ): T? = getterScope { val transport = when (queryType) { AggregatorQueryType.MIN -> realm_results_min(resultsPointer, propertyMetadata.key) @@ -171,11 +177,11 @@ internal class MinMaxQuery constructor( @Suppress("UNCHECKED_CAST") when (type) { // Asynchronous aggregations require a converter with an updated realm reference - RealmAny::class -> when (updatedRealmReference) { - null -> converter - else -> realmAnyConverter(mediator, updatedRealmReference) - }.realmValueToPublic(transport) - else -> coerceType(converter, propertyMetadata.name, type, transport) + RealmAny::class -> + realmValueToRealmAny( + transport, null, mediator, realmReference, false, false, + ) as T? + else -> converter(transport) } as T? } } @@ -193,16 +199,17 @@ internal class SumQuery constructor( clazz: KClass, override val propertyMetadata: PropertyMetadata, private val type: KClass -) : BaseScalarQuery(realmReference, queryPointer, mediator, classKey, clazz), TypeBoundQuery, RealmScalarQuery { +) : BaseScalarQuery(realmReference, queryPointer, mediator, classKey, clazz), TypeBoundQuery, RealmScalarQuery { - // RealmAny SUMs are computed as Decimal128 and Float fields return Double - override val converter: RealmValueConverter<*> = when (propertyMetadata.type) { - PropertyType.RLM_PROPERTY_TYPE_INT -> primitiveTypeConverters[Long::class]!! - PropertyType.RLM_PROPERTY_TYPE_FLOAT -> primitiveTypeConverters[Double::class]!! - PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> primitiveTypeConverters[Double::class]!! - PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> primitiveTypeConverters[RealmInstant::class]!! + @Suppress("ExplicitItLambdaParameter") + override val converter: (RealmValue) -> T? = when (propertyMetadata.type) { + PropertyType.RLM_PROPERTY_TYPE_INT -> { it -> IntConverter.fromRealmValue(it)?.let { coerceLong(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> { it -> DoubleConverter.fromRealmValue(it)?.let { coerceDouble(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> { it -> DoubleConverter.fromRealmValue(it)?.let { coerceDouble(propertyMetadata.name, it, type) } as T? } + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> { it -> RealmInstantConverter.fromRealmValue(it) as T? } PropertyType.RLM_PROPERTY_TYPE_DECIMAL128, - PropertyType.RLM_PROPERTY_TYPE_MIXED -> primitiveTypeConverters[Decimal128::class]!! + PropertyType.RLM_PROPERTY_TYPE_MIXED -> + { it -> Decimal128Converter.fromRealmValue(it) as T? } else -> throw IllegalArgumentException("Conversion not possible between '$type' and '${type.simpleName}'.") } @@ -222,12 +229,7 @@ internal class SumQuery constructor( } private fun findFromResults(resultsPointer: RealmResultsPointer): T = getterScope { - val transport = realm_results_sum(resultsPointer, propertyMetadata.key) - - when (type) { - RealmAny::class -> converter.realmValueToPublic(transport) - else -> coerceType(converter, propertyMetadata.name, type, transport) - } + converter(realm_results_sum(resultsPointer, propertyMetadata.key)) } as T } @@ -269,69 +271,43 @@ private fun queryTypeValidator( } } -/** - * Converts a value in the form of "storage type", i.e. type of the transport object produced by the - * C-API, to a user-specified type in the query, i.e. "coerced type". - */ -@Suppress("ComplexMethod") -// TODO optimize: try to move this to query construction -private fun coerceType( - converter: RealmValueConverter<*>, - propertyName: String, - coercedType: KClass, - transport: RealmValue -): T? { - return when (transport.getType()) { - ValueType.RLM_TYPE_NULL -> null - // Core INT can be coerced to any numeric as long as Kotlin supports it - ValueType.RLM_TYPE_INT -> { - val storageTypeValue = converter.realmValueToPublic(transport) as Long? - when (coercedType) { - Short::class -> storageTypeValue?.toShort() - Int::class -> storageTypeValue?.toInt() - Byte::class -> storageTypeValue?.toByte() - Char::class -> storageTypeValue?.toInt()?.toChar() - Long::class -> storageTypeValue - Double::class -> storageTypeValue?.toDouble() - Float::class -> storageTypeValue?.toFloat() - else -> throw IllegalArgumentException("Cannot coerce type of property '$propertyName' to '${coercedType.simpleName}'.") - } - } - // Core FLOAT can be coerced to any numeric as long as Kotlin supports it - ValueType.RLM_TYPE_FLOAT -> { - val storageTypeValue = converter.realmValueToPublic(transport) as Float? - when (coercedType) { - Short::class -> storageTypeValue?.toInt()?.toShort() - Int::class -> storageTypeValue?.toInt() - Byte::class -> storageTypeValue?.toInt()?.toByte() - Char::class -> storageTypeValue?.toInt()?.toChar() - Long::class -> storageTypeValue?.toInt()?.toLong() - Double::class -> storageTypeValue?.toDouble() - Float::class -> storageTypeValue - else -> throw IllegalArgumentException("Cannot coerce type of property '$$propertyName' to '${coercedType.simpleName}'.") - } - } - // Core DOUBLE can be coerced to any numeric as long as Kotlin supports it - ValueType.RLM_TYPE_DOUBLE -> { - val storageTypeValue = converter.realmValueToPublic(transport) as Double? - when (coercedType) { - Short::class -> storageTypeValue?.toInt()?.toShort() - Int::class -> storageTypeValue?.toInt() - Byte::class -> storageTypeValue?.toInt()?.toByte() - Char::class -> storageTypeValue?.toInt()?.toChar() - Long::class -> storageTypeValue?.toInt()?.toLong() - Double::class -> storageTypeValue - Float::class -> storageTypeValue?.toFloat() - else -> throw IllegalArgumentException("Cannot coerce type of property '$propertyName' to '${coercedType.simpleName}'.") - } - } - // Core TIMESTAMP cannot be coerced to any type other than RealmInstant - ValueType.RLM_TYPE_DECIMAL128, - ValueType.RLM_TYPE_TIMESTAMP -> { - converter.realmValueToPublic(transport) - } +internal fun coerceLong(propertyName: String, value: Long, coercedType: KClass<*>): Any { + return when (coercedType) { + Short::class -> value.toShort() + Int::class -> value.toInt() + Byte::class -> value.toByte() + Char::class -> value.toInt().toChar() + Long::class -> value + Double::class -> value.toDouble() + Float::class -> value.toFloat() else -> throw IllegalArgumentException("Cannot coerce type of property '$propertyName' to '${coercedType.simpleName}'.") - } as T? + } +} + +internal fun coerceFloat(propertyName: String, value: Float, coercedType: KClass<*>): Any { + return when (coercedType) { + Short::class -> value.toInt().toShort() + Int::class -> value.toInt() + Byte::class -> value.toInt().toByte() + Char::class -> value.toInt().toChar() + Long::class -> value.toInt().toLong() + Double::class -> value.toDouble() + Float::class -> value + else -> throw IllegalArgumentException("Cannot coerce type of property '$$propertyName' to '${coercedType.simpleName}'.") + } +} + +internal fun coerceDouble(propertyName: String, value: Double, coercedType: KClass<*>): Any { + return when (coercedType) { + Short::class -> value.toInt().toShort() + Int::class -> value.toInt() + Byte::class -> value.toInt().toByte() + Char::class -> value.toInt().toChar() + Long::class -> value.toInt().toLong() + Double::class -> value + Float::class -> value.toFloat() + else -> throw IllegalArgumentException("Cannot coerce type of property '$$propertyName' to '${coercedType.simpleName}'.") + } } private fun KClass<*>.isNumeric(): Boolean { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt index 4d0b19277e..664a3e2db6 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt @@ -30,6 +30,7 @@ import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.RealmUUID +import kotlinx.serialization.Contextual import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer @@ -351,11 +352,19 @@ public object RealmAnyKSerializer : KSerializer { @Serializable(RealmUUIDKSerializer::class) var uuid: RealmUUID? = null var realmObject: RealmObject? = null + + @Contextual + var set: RealmSet? = null + @Contextual + var list: RealmList? = null + @Contextual + var dictionary: RealmDictionary? = null } private val serializer = SerializableRealmAny.serializer() override val descriptor: SerialDescriptor = serializer.descriptor + @Suppress("ComplexMethod") override fun deserialize(decoder: Decoder): RealmAny { return decoder.decodeSerializableValue(serializer).let { when (Type.valueOf(it.type)) { @@ -370,10 +379,14 @@ public object RealmAnyKSerializer : KSerializer { Type.OBJECT_ID -> RealmAny.create(it.objectId!!) Type.UUID -> RealmAny.create(it.uuid!!) Type.OBJECT -> RealmAny.create(it.realmObject!!) + Type.SET -> RealmAny.create(it.set!!) + Type.LIST -> RealmAny.create(it.list!!) + Type.DICTIONARY -> RealmAny.create(it.dictionary!!) } } } + @Suppress("ComplexMethod") override fun serialize(encoder: Encoder, value: RealmAny) { encoder.encodeSerializableValue( serializer, @@ -393,6 +406,9 @@ public object RealmAnyKSerializer : KSerializer { ) Type.UUID -> uuid = value.asRealmUUID() Type.OBJECT -> realmObject = value.asRealmObject() + Type.SET -> set = value.asSet() + Type.LIST -> list = value.asList() + Type.DICTIONARY -> dictionary = value.asDictionary() } } ) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt index f1343b23e1..6eccbd8016 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt @@ -60,6 +60,9 @@ import kotlin.reflect.KClass * OBJECT_ID -> doSomething(realmAny.asObjectId()) * REALM_UUID -> doSomething(realmAny.asRealmUUID()) * REALM_OBJECT -> doSomething(realmAny.asRealmObject()) + * SET -> doSomething(realmAny.asSet()) + * LIST -> doSomething(realmAny.asList()) + * DICTIONARY -> doSomething(realmAny.asDictionary()) * } * ``` * [Short], [Int], [Byte], [Char] and [Long] values are converted internally to `int64_t` values. @@ -82,6 +85,28 @@ import kotlin.reflect.KClass * ``` * `RealmAny` cannot store [EmbeddedRealmObject]s. * + * `RealmAny` can contain other collections of [RealmAny]. This means that you can build nested + * collections inside a `RealmAny`-field. The only constraint is that sets cannot contain other + * collections, so must be at the leaf of such nested hierarchies. + * ``` + * realmObjct.realmAnyField = realmAnyListOf( + * // Primitive values can be added in collections + * 1, + * // Sets are allowed but cannot contain nested collection types + * "realmSetOf(1), + * // Lists and dictionaries can contain other nested collection types + * realmListOf( + * realmSetOf(), + * realmListOf(), + * realmDictionaryOf() + * ), + * realmDictionaryOf( + * "key1" to realmSetOf(), + * "key2" to realmListOf(), + * "key3" to realmDictionaryOf()) + * ) + * ``` + * * [DynamicRealmObject]s and [DynamicMutableRealmObject]s can be used inside `RealmAny` with * the corresponding [create] function for `DynamicRealmObject`s and with [asRealmObject] using * either `DynamicRealmObject` or `DynamicMutableRealmObject` as the generic parameter. @@ -112,7 +137,7 @@ public interface RealmAny { * Supported Realm data types that can be stored in a `RealmAny` instance. */ public enum class Type { - INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT + INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT, SET, LIST, DICTIONARY; } /** @@ -233,6 +258,24 @@ public interface RealmAny { */ public fun asRealmObject(clazz: KClass): T + /** + * Returns the value from this `RealmAny` as a [RealmSet] containing new [RealmAny]s. + * @throws [IllegalStateException] if the stored value is not a set. + */ + public fun asSet(): RealmSet + + /** + * Returns the value from this `RealmAny` as a [RealmList] containing new [RealmAny]s. + * @throws [IllegalStateException] if the stored value is not a list. + */ + public fun asList(): RealmList + + /** + * Returns the value from this `RealmAny` as a [RealmDictionary] containing new [RealmAny]s. + * @throws [IllegalStateException] if the stored value is not a dictionary. + */ + public fun asDictionary(): RealmDictionary + /** * Two [RealmAny] instances are equal if and only if their types and contents are the equal. */ @@ -343,5 +386,102 @@ public interface RealmAny { */ public fun create(realmObject: DynamicRealmObject): RealmAny = RealmAnyImpl(Type.OBJECT, DynamicRealmObject::class, realmObject) + + /** + * Creates an unmanaged `RealmAny` instance from a [RealmSet] of [RealmAny] values. + * + * To create a [RealmAny] containing a [RealmSet] of arbitrary values wrapped in [RealmAny]s + * use the [io.realm.kotlin.ext.realmAnySetOf]. + * + * **NOTE:** Realm does not support to having other collections ([RealmSet], [RealmList] and + * [RealmDictionary]) in a [RealmSet]. These kind of structures can be built in a [RealmAny] + * but will fail if imported to realm. + * + * Example: + * ``` + * class SampleObject() : RealmObject { + * val realmAnyField: RealmAny? = null + * } + * val realmObject = copyToRealm(SampleObject()) + * + * // Sets with non-collection types can be built and imported into realm. + * realmObject.realmAnyField = realmAnySetOf(1, "Realm", realmObject) + * + * // Sets with collection types can be built but cannot be imported into realm. + * val setsWithCollections = realmAnySetOf(realmSetOf(), realmListOf(), realmDictionaryOf()) + * realmObject.realmAnyField = setsWithCollections // Will throw IllegalArgumentExcception + * ``` + */ + public fun create(value: RealmSet): RealmAny = + RealmAnyImpl(Type.SET, RealmAny::class, value) + + /** + * Creates an unmanaged `RealmAny` instance from a [RealmList] of [RealmAny] values. + * + * To create a [RealmAny] containing a [RealmList] of arbitrary values wrapped in [RealmAny]s + * use the [io.realm.kotlin.ext.realmAnyListOf]. + * + * A `RealmList` can contain all [RealmAny] types, also other collection types: + * ``` + * class SampleObject() : RealmObject { + * val realmAnyField: RealmAny? = null + * } + * val realmObject = copyToRealm(SampleObject()) + * + * // Lists can contain other collection types, including [RealmSet]s. + * realmObject.realmAnyField = realmAnyListOf( + * // Primitive values + * 1, + * // Sets are allowed but cannot contain nested collection types + * realmSetOf(1), + * // Lists and dictionaries can contain other collection types + * realmListOf( + * realmSetOf(), + * realmListOf(), + * realmDictionaryOf() + * ), + * realmDictionaryOf( + * "key1" to realmSetOf(), + * "key2" to realmListOf(), + * "key3" to realmDictioneryOf()) + * ) + * ``` + */ + public fun create(value: RealmList): RealmAny = + RealmAnyImpl(Type.LIST, RealmAny::class, value) + + /** + * Creates an unmanaged `RealmAny` instance from a [RealmDictionary] of [RealmAny] values. + * + * To create a [RealmAny] containing a [RealmDictionary] of arbitrary values wrapped in + * [RealmAny]s use the [io.realm.kotlin.ext.realmAnyDictionaryOf]. + * + * A `RealmDictionery` can contain all [RealmAny] types, also other collection types: + * ``` + * class SampleObject() : RealmObject { + * val realmAnyField: RealmAny? = null + * } + * val realmObject = copyToRealm(SampleObject()) + * + * // Dictionaries can contain other collection types, including [RealmSet]s. + * realmObjct.realmAnyField = realmAnyDictionaryOf( + * "int" to 5, + * // Sets are allowed but cannot contain nested collection types + * "set" to "realmSetOf(1), + * // Lists and dictionaries can contain other nested collection types + * "list" to realmListOf( + * realmSetOf(), + * realmListOf(), + * realmDictionaryOf() + * ), + * "dictionary" to realmDictionaryOf( + * "key1" to realmSetOf(), + * "key2" to realmListOf(), + * "key3" to realmDictionaryOf()) + * ) + * ``` + */ + public fun create(value: RealmDictionary): RealmAny = + RealmAnyImpl(Type.DICTIONARY, RealmAny::class, value) } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/BsonEncoder.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/BsonEncoder.kt index f30dc83ecb..c676fbbbcf 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/BsonEncoder.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/BsonEncoder.kt @@ -246,6 +246,7 @@ internal object BsonEncoder { RealmAny.Type.OBJECT_ID -> asObjectId() RealmAny.Type.UUID -> asRealmUUID() RealmAny.Type.OBJECT -> asRealmObject() + else -> TODO("Unsupported type $type") } ) diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/JsonStyleRealmObject.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/JsonStyleRealmObject.kt new file mode 100644 index 0000000000..d54855248e --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/JsonStyleRealmObject.kt @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package io.realm.kotlin.entities + +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PersistedName +import io.realm.kotlin.types.annotations.PrimaryKey + +class JsonStyleRealmObject(id: String) : RealmObject { + constructor() : this("JsonStyleRealmObject") + @PrimaryKey + @PersistedName("_id") + var id: String = id + var value: RealmAny? = null +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt new file mode 100644 index 0000000000..84057002cc --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -0,0 +1,793 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.entities.Sample +import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmAnyDictionaryOf +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnySetOf +import io.realm.kotlin.ext.realmDictionaryOf +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.test.common.utils.assertFailsWithMessage +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmAny +import org.mongodb.kbson.ObjectId +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class RealmAnyNestedCollectionTests { + + private lateinit var configBuilder: RealmConfiguration.Builder + private lateinit var configuration: RealmConfiguration + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configBuilder = RealmConfiguration.Builder( + setOf( + JsonStyleRealmObject::class, + Sample::class, + ) + ).directory(tmpDir) + configuration = configBuilder.build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun setInRealmAny_copyToRealm() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + realm.write { + val instance = JsonStyleRealmObject().apply { + value = RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ) + } + copyToRealm(instance) + } + val managedSample: Sample = realm.query().find().single() + val anyValue: RealmAny = realm.query().find().single().value!! + assertEquals(RealmAny.Type.SET, anyValue.type) + anyValue.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + } + + @Test + fun setInRealmAny_assignment() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + realm.write { + val managedInstance = copyToRealm(JsonStyleRealmObject()) + managedInstance.value = RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ) + } + val managedSample: Sample = realm.query().find().single() + val anyValue: RealmAny = realm.query().find().single().value!! + anyValue.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + } + + @Test + fun setInRealmAny_throwsOnNestedCollections_copyToRealm() = runBlocking { + realm.write { + JsonStyleRealmObject(ObjectId().toString()).apply { + value = + RealmAny.create(realmSetOf(RealmAny.create(realmListOf(RealmAny.create(5))))) + }.let { + assertFailsWithMessage("Sets cannot contain other collections") { + copyToRealm(it) + } + } + JsonStyleRealmObject(ObjectId().toString()).apply { + value = RealmAny.create( + realmSetOf( + RealmAny.create(realmDictionaryOf("key" to RealmAny.create(5))) + ) + ) + }.let { + assertFailsWithMessage("Sets cannot contain other collections") { + copyToRealm(it) + } + } + } + } + + @Test + fun setInRealmAny_throwsOnNestedCollections_add() = runBlocking { + realm.write { + val instance = copyToRealm( + JsonStyleRealmObject().apply { value = RealmAny.create(realmSetOf()) } + ) + val set = instance.value!!.asSet() + + val realmAnyList = RealmAny.create(realmListOf()) + assertFailsWithMessage("Sets cannot contain other collections") { + set.add(realmAnyList) + } + + val realmAnyDictionary = RealmAny.create(realmDictionaryOf()) + assertFailsWithMessage("Sets cannot contain other collections") { + set.add(realmAnyDictionary) + } + } + } + + @Test + fun listInRealmAny_copyToRealm() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + realm.write { + JsonStyleRealmObject().apply { + value = RealmAny.create( + realmListOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ) + }.let { + copyToRealm(it) + } + } + val instance = realm.query().find().single() + val anyValue: RealmAny = instance.value!! + assertEquals(RealmAny.Type.LIST, anyValue.type) + anyValue.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + } + + @Test + fun nestedCollectionsInList_copyToRealm() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + realm.write { + JsonStyleRealmObject().apply { + value = RealmAny.create( + realmListOf( + // Primitive values + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + // Embedded list + RealmAny.create( + realmListOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + // Embedded set + RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + // Embedded map + RealmAny.create( + realmDictionaryOf( + "keyInt" to RealmAny.create(5), + "keyString" to RealmAny.create("Realm"), + "keyObject" to RealmAny.create(sample) + ) + ), + ) + ) + }.let { + copyToRealm(it) + } + } + val instance = realm.query().find().single() + val anyValue: RealmAny = instance.value!! + val managedSample: Sample = realm.query().find().single() + assertEquals(RealmAny.Type.LIST, anyValue.type) + + // Assert structure + anyValue.asList().let { + assertEquals(RealmAny.create(5), it[0]) + assertEquals(RealmAny.create("Realm"), it[1]) + assertEquals("SAMPLE", it[2]!!.asRealmObject().stringField) + it[3]!!.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + it[4]!!.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + it[5]!!.asDictionary().toMutableMap().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + } + + @Test + fun nestedCollectionsInList_add() = runBlocking { + realm.write { + val sample = copyToRealm(Sample().apply { stringField = "SAMPLE" }) + val instance = + copyToRealm(JsonStyleRealmObject().apply { value = RealmAny.create(realmListOf()) }) + instance.value!!.asList().run { + add(RealmAny.create(5)) + add(RealmAny.create("Realm")) + add(RealmAny.create(sample)) + // Embedded list + add( + RealmAny.create( + realmListOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + ) + // Embedded set + add( + RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + ) + // Embedded map + add( + RealmAny.create( + realmDictionaryOf( + "keyInt" to RealmAny.create(5), + "keyString" to RealmAny.create("Realm"), + "keyObject" to RealmAny.create(sample) + ) + ), + ) + } + } + val anyList: RealmAny = realm.query().find().single().value!! + val managedSample: Sample = realm.query().find().single() + anyList.asList().let { + assertEquals(RealmAny.create(5), it[0]) + assertEquals(RealmAny.create("Realm"), it[1]) + assertEquals("SAMPLE", it[2]!!.asRealmObject().stringField) + it[3]!!.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + it[4]!!.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + it[5]!!.asDictionary().toMutableMap().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + } + + @Test + fun nestedCollectionsInList_set() = runBlocking { + realm.write { + val sample = copyToRealm(Sample().apply { stringField = "SAMPLE" }) + val instance = + copyToRealm( + JsonStyleRealmObject().apply { + value = RealmAny.create( + realmListOf( + RealmAny.create(1), + RealmAny.create(1), + RealmAny.create(1), + RealmAny.create(1), + ) + ) + } + ) + instance.value!!.asList().run { + // Embedded list + set( + 0, + RealmAny.create( + realmListOf( + RealmAny.create(5), + RealmAny.create(sample), + ) + ), + ) + // Embedded set + set( + 1, + RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + ) + // Embedded map + set( + 2, + RealmAny.create( + realmDictionaryOf( + "keyInt" to RealmAny.create(5), + "keyString" to RealmAny.create("Realm"), + "keyObject" to RealmAny.create(sample) + ) + ), + ) + } + } + + val anyValue3: RealmAny = realm.query().find().single().value!! + val managedSample: Sample = realm.query().find().single() + anyValue3.asList().let { + it[0]!!.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals("SAMPLE", embeddedList[1]!!.asRealmObject().stringField) + } + it[1]!!.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + it[2]!!.asDictionary().toMutableMap().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + } + + @Test + fun nestedCollectionsInList_set_invalidatesOldElement() = runBlocking { + realm.write { + val instance = copyToRealm(JsonStyleRealmObject()) + instance.value = realmAnyListOf(realmAnyListOf(5)) + + // Store local reference to existing list + var nestedList = instance.value!!.asList()[0]!!.asList() + // Accessing returns excepted value 5 + assertEquals(5, nestedList[0]!!.asInt()) + + // Overwriting exact list with new list + instance.value!!.asList()[0] = realmAnyListOf(7) + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + + nestedList = instance.value!!.asList()[0]!!.asList() + assertEquals(7, nestedList[0]!!.asInt()) + + // Overwriting root entry + instance.value = null + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + + // Recreating list doesn't bring things back to shape + instance.value = realmAnyListOf(realmAnyListOf(8)) + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + } + } + + @Test + fun dictionaryInRealmAny_copyToRealm() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + // Import + realm.write { + // Normal realm link/object reference + JsonStyleRealmObject().apply { + // Assigning dictionary with nested lists and dictionaries + value = RealmAny.create( + realmDictionaryOf( + "keyInt" to RealmAny.create(5), + "keySet" to RealmAny.create( + realmSetOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample), + ) + ), + "keyList" to RealmAny.create( + realmListOf( + RealmAny.create(5), + RealmAny.create("Realm"), + RealmAny.create(sample) + ) + ), + "keyDictionary" to RealmAny.create( + realmDictionaryOf( + "keyInt" to RealmAny.create(5), + "keyString" to RealmAny.create("Realm"), + "keyObject" to RealmAny.create(sample) + ) + ), + ) + ) + }.let { + copyToRealm(it) + } + } + + val jsonStyleRealmObject: JsonStyleRealmObject = + realm.query().find().single() + val anyValue: RealmAny = jsonStyleRealmObject.value!! + assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) + val managedSample: Sample = realm.query().find().single() + anyValue.asDictionary().run { + assertEquals(4, size) + assertEquals(5, get("keyInt")!!.asInt()) + get("keySet")!!.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + get("keyList")!!.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + get("keyDictionary")!!.asDictionary().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + } + @Test + fun dictionaryInRealmAny_values() = runBlocking { + val sample = Sample().apply { stringField = "SAMPLE" } + // Import + realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + // Assigning dictionary with nested lists and dictionaries + value = realmAnyDictionaryOf( + "keySet" to realmAnySetOf(5, "Realm", sample), + "keyList" to realmAnyListOf(5, "Realm", sample), + "keyDictionary" to realmAnyDictionaryOf( + "keyInt" to 5, + "keyString" to "Realm", + "keyObject" to sample, + ), + ) + } + ) + } + + val managedSample: Sample = realm.query().find().single() + val jsonStyleRealmObject: JsonStyleRealmObject = + realm.query().find().single() + val anyValue: RealmAny = jsonStyleRealmObject.value!! + assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) + anyValue.asDictionary().values.run { + assertEquals(3, size) + forEach { value -> + when (value?.type) { + RealmAny.Type.SET -> { + value.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + } + RealmAny.Type.LIST -> { + value.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + } + RealmAny.Type.DICTIONARY -> { + value.asDictionary().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + else -> {} // NO-OP Only testing for nested collections in here + } + } + } + } + + @Test + fun dictionaryInRealmAny_put() = runBlocking { + // Import + realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + // Assigning dictionary with nested lists and dictionaries + value = RealmAny.create(realmDictionaryOf()) + } + ) + query().find().single().value!!.asDictionary().run { + val sample = copyToRealm(Sample().apply { stringField = "SAMPLE" }) + put("keyInt", RealmAny.create(5)) + put("keySet", realmAnySetOf(5, "Realm", sample)) + put("keyList", realmAnyListOf(5, "Realm", sample)) + put( + "keyDictionary", + realmAnyDictionaryOf( + "keyInt" to 5, + "keyString" to "Realm", + "keyObject" to sample, + ), + ) + } + } + + val managedSample: Sample = realm.query().find().single() + val jsonStyleRealmObject: JsonStyleRealmObject = + realm.query().find().single() + val anyValue: RealmAny = jsonStyleRealmObject.value!! + assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) + anyValue.asDictionary().run { + assertEquals(4, size) + assertEquals(5, get("keyInt")!!.asInt()) + get("keySet")!!.asSet().let { embeddedSet -> + assertEquals(3, embeddedSet.size) + assertTrue { embeddedSet.contains(RealmAny.create(5)) } + assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } + assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } + } + get("keyList")!!.asList().let { embeddedList -> + assertEquals(RealmAny.create(5), embeddedList[0]) + assertEquals(RealmAny.create("Realm"), embeddedList[1]) + assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) + } + get("keyDictionary")!!.asDictionary().let { embeddedDict -> + assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) + assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) + assertEquals( + "SAMPLE", + embeddedDict["keyObject"]!!.asRealmObject().stringField + ) + } + } + } + + @Test + fun nestedCollectionsInDictionary_put_invalidatesOldElement() = runBlocking { + realm.write { + val instance = copyToRealm( + JsonStyleRealmObject().apply { + value = RealmAny.create( + realmDictionaryOf("key" to RealmAny.create(realmListOf(RealmAny.create(5)))) + ) + } + ) + // Store local reference to existing list + var nestedList = instance.value!!.asDictionary()["key"]!!.asList() + // Accessing returns excepted value 5 + assertEquals(5, nestedList[0]!!.asInt()) + // Overwriting exact list with new list + instance.value!!.asDictionary()["key"] = realmAnyListOf(7) + + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + + // Getting updated reference to embedded list + nestedList = instance.value!!.asDictionary()["key"]!!.asList() + assertEquals(7, nestedList[0]!!.asInt()) + + // Overwriting root entry + instance.value = null + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + } + } + + @Test + fun updateMixed_invalidatesOldElement() = runBlocking { + realm.write { + val instance = copyToRealm(JsonStyleRealmObject()) + instance.value = RealmAny.create(realmListOf(RealmAny.create(5))) + + // Store local reference to existing list + val nestedList = instance.value!!.asList() + // Accessing returns excepted value 5 + nestedList[0]!!.asInt() + + // Overwriting with new list + instance.value = realmAnyListOf(7) + // Accessing original orphaned list return 7 from the new instance, but expected ILLEGAL_STATE_EXCEPTION["List is no longer valid"] + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + + // Overwriting with null value + instance.value = null + // Throws excepted ILLEGAL_STATE_EXCEPTION["List is no longer valid"] + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + + // Updating to a new list + instance.value = realmAnyListOf(7) + // Accessing original orphaned list return 7 from the new instance again, but expected ILLEGAL_STATE_EXCEPTION["List is no longer valid"] + assertFailsWithMessage("List is no longer valid") { + nestedList[0] + } + } + } + + @Test + fun query_ThrowsOnNestedCollectionArguments() { + assertFailsWithMessage("Invalid query argument: Cannot pass unmanaged collections as input argument") { + realm.query("value == $0", RealmAny.create(realmSetOf())) + } + assertFailsWithMessage("Invalid query argument: Cannot pass unmanaged collections as input argument") { + realm.query("value == $0", RealmAny.create(realmListOf())) + } + assertFailsWithMessage("Invalid query argument: Cannot pass unmanaged collections as input argument") { + realm.query("value == $0", RealmAny.create(realmDictionaryOf())) + } + } + + @Test + fun query() = runBlocking { + realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + id = "SET" + value = realmAnySetOf(1, 2, 3) + } + ) + copyToRealm( + JsonStyleRealmObject().apply { + id = "LIST" + value = realmAnyListOf(4, 5, 6) + } + ) + copyToRealm( + JsonStyleRealmObject().apply { + id = "DICT" + value = realmAnyDictionaryOf( + "key1" to 7, + "key2" to 8, + "key3" to 9, + ) + } + ) + copyToRealm( + JsonStyleRealmObject().apply { + id = "EMBEDDED" + value = realmAnyListOf( + setOf(1, 2, 3), + listOf(4, 5, 6), + mapOf( + "key1" to 7, + "key2" to 8, + "key3" to listOf(9), + ) + ) + } + ) + } + + assertEquals(4, realm.query().find().size) + + // Matching lists + realm.query("value[0] == 4").find().single().run { + assertEquals("LIST", id) + } + realm.query("value[*] == 4").find().single().run { + assertEquals("LIST", id) + } + + // Matching dictionaries + realm.query("value.key1 == 7").find().single().run { + assertEquals("DICT", id) + } + realm.query("value['key1'] == 7").find().single().run { + assertEquals("DICT", id) + } + realm.query("value[*] == 7").find().single().run { + assertEquals("DICT", id) + } + assertEquals(0, realm.query("value.unknown == 3").find().size) + realm.query("value.@keys == 'key1'").find().single().run { + assertEquals("DICT", id) + } + assertEquals(0, realm.query("value.@keys == 'unknown'").find().size) + + // None + assertTrue { realm.query("value[*] == 10").find().isEmpty() } + + // Matching across all elements and in nested structures + realm.query("value[*][*] == 4").find().single().run { + assertEquals("EMBEDDED", id) + } + realm.query("value[*][*] == 7").find().single().run { + assertEquals("EMBEDDED", id) + } + realm.query("value[*].@keys == 'key1'").find().single().run { + assertEquals("EMBEDDED", id) + } + realm.query("value[*].key3[0] == 9").find().single().run { + assertEquals("EMBEDDED", id) + } + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt index 843fbd29c2..fbf1e7d0a6 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt @@ -26,6 +26,9 @@ import io.realm.kotlin.entities.embedded.EmbeddedParent import io.realm.kotlin.entities.embedded.embeddedSchema import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmDictionaryOf +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.notifications.DeletedObject import io.realm.kotlin.notifications.InitialObject @@ -438,6 +441,23 @@ class RealmAnyTests { assertEquals(1, realm.query().count().find()) } + @Test + fun importWithDuplicateReference() = runBlocking { + val child = realm.write { + Sample().apply { stringField = "CHILD" } + } + realm.write { + val parent = Sample().apply { + nullableRealmAnyField = RealmAny.create(child) + nullableRealmAnySetField = realmSetOf(RealmAny.create(child)) + nullableRealmAnyListField = realmListOf(RealmAny.create(child)) + nullableRealmAnyDictionaryField = realmDictionaryOf("key" to RealmAny.create(child)) + } + copyToRealm(parent) + } + assertEquals(1, realm.query("stringField = 'CHILD'").find().size) + } + private fun assertCoreIntValuesAreTheSame( fromInt: RealmAny, fromLong: RealmAny, diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt index 4c903effe0..b98e34baba 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt @@ -1323,6 +1323,21 @@ class RealmDictionaryTests : EmbeddedObjectCollectionQueryTests { Unit } + @Test + fun contains_unmanagedArgs() = runBlocking { + val frozenObject = realm.write { + val liveObject = copyToRealm(RealmDictionaryContainer()) + assertEquals(1, query().find().size) + assertFalse(liveObject.nullableObjectDictionaryField.containsValue(RealmDictionaryContainer())) + assertFalse(liveObject.nullableRealmAnyDictionaryField.containsValue(RealmAny.create(RealmDictionaryContainer()))) + assertEquals(1, query().find().size) + liveObject + } + // Verify that we can also call this on frozen instances + assertFalse(frozenObject.nullableObjectDictionaryField.containsValue(RealmDictionaryContainer())) + assertFalse(frozenObject.nullableRealmAnyDictionaryField.containsValue(RealmAny.create(RealmDictionaryContainer()))) + } + private fun getCloseableRealm(): Realm = RealmConfiguration.Builder(schema = dictionarySchema) .directory(tmpDir) @@ -3063,6 +3078,9 @@ internal class RealmAnyDictionaryTester( assertEquals(expectedObj.stringField, assertNotNull(actualObj).stringField) } null -> assertNull(actualValue) + RealmAny.Type.SET, + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> {} // Tested separately in RealmAnyNestedCollectionTests } } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index 4e1bcf0b8f..eeea0b30b4 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -632,6 +632,35 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { Unit } + @Test + fun contains_unmanagedArgs() = runBlocking { + val frozenObject = realm.write { + val liveObject = copyToRealm(RealmListContainer()) + assertEquals(1, query().find().size) + assertFalse(liveObject.objectListField.contains(RealmListContainer())) + assertFalse(liveObject.nullableRealmAnyListField.contains(RealmAny.create(RealmListContainer()))) + assertEquals(1, query().find().size) + liveObject + } + // Verify that we can also call this on frozen instances + assertFalse(frozenObject.objectListField.contains(RealmListContainer())) + assertFalse(frozenObject.nullableRealmAnyListField.contains(RealmAny.create(RealmListContainer()))) + } + + @Test + fun remove_unmanagedArgs() = runBlocking { + val frozenObject = realm.write { + val liveObject = copyToRealm(RealmListContainer()) + assertEquals(1, query().find().size) + assertFalse(liveObject.objectListField.remove(RealmListContainer())) + assertFalse(liveObject.nullableRealmAnyListField.remove(RealmAny.create(RealmListContainer()))) + assertEquals(1, query().find().size) + liveObject + } + assertFalse(frozenObject.objectListField.contains(RealmListContainer())) + assertFalse(frozenObject.nullableRealmAnyListField.contains(RealmAny.create(RealmListContainer()))) + } + private fun getCloseableRealm(): Realm = RealmConfiguration.Builder(schema = listTestSchema) .directory(tmpDir) @@ -1316,6 +1345,10 @@ internal class RealmAnyListTester constructor( expected.asRealmObject().stringField, actual.asRealmObject().stringField ) + // FIXME Should we rather test nested collections somewhere else? + RealmAny.Type.SET -> TODO() + RealmAny.Type.LIST -> TODO() + RealmAny.Type.DICTIONARY -> TODO() } } else if (expected != null || actual != null) { fail("One of the RealmAny values is null, expected = $expected, actual = $actual") diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt index 437ff45ebc..225f18f490 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt @@ -637,6 +637,22 @@ class RealmSetTests : CollectionQueryTests { Unit } + @Test + fun dontImportUnmanagedArgsToNonImportingMethods() = runBlocking { + val frozenObject = realm.write { + val liveObject = copyToRealm(RealmSetContainer()) + assertEquals(1, query().find().size) + assertFalse(liveObject.objectSetField.contains(RealmSetContainer())) + assertFalse(liveObject.nullableRealmAnySetField.contains(RealmAny.create(RealmSetContainer()))) + assertFalse(liveObject.objectSetField.remove(RealmSetContainer())) + assertFalse(liveObject.nullableRealmAnySetField.remove(RealmAny.create(RealmSetContainer()))) + assertEquals(1, query().find().size) + liveObject + } + assertFalse(frozenObject.objectSetField.contains(RealmSetContainer())) + assertFalse(frozenObject.nullableRealmAnySetField.contains(RealmAny.create(RealmSetContainer()))) + } + private fun getCloseableRealm(): Realm = RealmConfiguration.Builder(schema = setOf(RealmSetContainer::class)) .directory(tmpDir) @@ -800,9 +816,6 @@ internal abstract class ManagedSetTester( } override fun removeAll() { - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // Ignore RealmObject: structural equality cannot be assessed for this type when removing - // elements from the set if (classifier != RealmObject::class) { val dataSet = typeSafetyManager.dataSetToLoad @@ -812,9 +825,6 @@ internal abstract class ManagedSetTester( set.addAll(dataSet) assertTrue(set.removeAll(dataSet)) - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // If the RealmAny instance contains an object it will NOT be removed until - // the issue above is fixed if (classifier == RealmAny::class) { assertEquals(1, set.size) } else { @@ -826,9 +836,6 @@ internal abstract class ManagedSetTester( assertContainerAndCleanup { container -> val set = typeSafetyManager.getCollection(container) - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // If the RealmAny instance contains an object it will NOT be removed until - // the issue above is fixed if (classifier == RealmAny::class) { assertEquals(1, set.size) } else { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt index 088cf1919e..300740f1a7 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt @@ -31,9 +31,13 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.SerializableEmbeddedObject import io.realm.kotlin.entities.SerializableSample import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.realmAnyDictionaryOf +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnySetOf import io.realm.kotlin.internal.restrictToMillisPrecision import io.realm.kotlin.serializers.MutableRealmIntKSerializer import io.realm.kotlin.serializers.RealmAnyKSerializer +import io.realm.kotlin.serializers.RealmDictionaryKSerializer import io.realm.kotlin.serializers.RealmInstantKSerializer import io.realm.kotlin.serializers.RealmListKSerializer import io.realm.kotlin.serializers.RealmSetKSerializer @@ -46,17 +50,17 @@ import io.realm.kotlin.types.ObjectId import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject -import io.realm.kotlin.types.RealmUUID +import io.realm.kotlin.types.RealmSet import kotlinx.serialization.UseSerializers +import kotlinx.serialization.builtins.nullable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass -import org.mongodb.kbson.BsonObjectId -import org.mongodb.kbson.Decimal128 import kotlin.reflect.KClass import kotlin.reflect.KClassifier import kotlin.reflect.KMutableProperty1 @@ -84,6 +88,16 @@ class SerializationTests { polymorphic(EmbeddedRealmObject::class) { subclass(SerializableEmbeddedObject::class) } + + contextual(RealmSet::class) { _ -> + RealmSetKSerializer(RealmAnyKSerializer.nullable) + } + contextual(RealmList::class) { _ -> + RealmListKSerializer(RealmAnyKSerializer.nullable) + } + contextual(RealmDictionary::class) { _ -> + RealmDictionaryKSerializer(RealmAnyKSerializer.nullable) + } } } @@ -268,62 +282,64 @@ class SerializationTests { @Test fun exhaustiveRealmAnyTester() { - TypeDescriptor - .anyClassifiers - .map { classifier -> - when (classifier.key) { - Byte::class -> SerializableSample().apply { - nullableRealmAnyField = RealmAny.create(byteField) - } - Char::class -> SerializableSample().apply { - nullableRealmAnyField = RealmAny.create(charField) - } - Short::class -> SerializableSample().apply { - nullableRealmAnyField = RealmAny.create(shortField) - } - Int::class -> SerializableSample().apply { - nullableRealmAnyField = RealmAny.create(intField) - } - Long::class -> SerializableSample().apply { + RealmAny.Type.values() + .map { type: RealmAny.Type -> + type to when (type) { + RealmAny.Type.INT -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(longField) } - Float::class -> SerializableSample().apply { + RealmAny.Type.FLOAT -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(floatField) } - Double::class -> SerializableSample().apply { + RealmAny.Type.DOUBLE -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(doubleField) } - ByteArray::class -> SerializableSample().apply { + RealmAny.Type.BINARY -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(binaryField) } - Boolean::class -> SerializableSample().apply { + RealmAny.Type.BOOL -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(booleanField) } - String::class -> SerializableSample().apply { + RealmAny.Type.STRING -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(stringField) } - Decimal128::class -> SerializableSample().apply { + RealmAny.Type.DECIMAL128 -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(decimal128Field) } - RealmInstant::class -> SerializableSample().apply { + RealmAny.Type.TIMESTAMP -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(timestampField) } - BsonObjectId::class -> SerializableSample().apply { + RealmAny.Type.OBJECT -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(bsonObjectIdField) } - RealmUUID::class -> SerializableSample().apply { + RealmAny.Type.UUID -> SerializableSample().apply { nullableRealmAnyField = RealmAny.create(uuidField) } - RealmObject::class -> SerializableSample().apply { + RealmAny.Type.OBJECT_ID -> SerializableSample().apply { SerializableSample().let { nullableObject = it nullableRealmAnyField = RealmAny.create(it) } } - else -> throw IllegalStateException("Untested type $classifier") + RealmAny.Type.OBJECT -> SerializableSample().apply { + SerializableSample().let { + nullableObject = it + nullableRealmAnyField = RealmAny.create(it) + } + } + RealmAny.Type.SET -> SerializableSample().apply { + nullableRealmAnyField = realmAnySetOf(1, 2, 3) + } + RealmAny.Type.LIST -> SerializableSample().apply { + nullableRealmAnyField = realmAnyListOf(RealmAny.create(1), RealmAny.create(2)) + } + RealmAny.Type.DICTIONARY -> SerializableSample().apply { + nullableRealmAnyField = realmAnyDictionaryOf("key1" to RealmAny.create(1), "key2" to RealmAny.create(2)) + } + else -> throw IllegalStateException("Untested type $type") } } - .forEach { expected -> + .forEach { (type, expected) -> val encoded: String = json.encodeToString(expected) val decoded: SerializableSample = json.decodeFromString(encoded) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt index 5e0ddd6172..9f834b9372 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt @@ -72,6 +72,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -311,15 +312,24 @@ class DynamicMutableRealmObjectTests { dynamicSample.set(name, dynamicRealmAny) val expectedValue = dynamicMutableManagedObject.getValue("stringField") - val managedDynamicMutableObject = dynamicSample.getNullableValue(name) - ?.asRealmObject() - val actualValue = managedDynamicMutableObject?.getValue("stringField") + val managedDynamicMutableObject = + dynamicSample.getNullableValue(name) + ?.asRealmObject() + val actualValue = + managedDynamicMutableObject?.getValue("stringField") assertEquals(expectedValue, actualValue) // Check we did indeed get a dynamic mutable object managedDynamicMutableObject?.set("stringField", "NEW") - assertEquals("NEW", managedDynamicMutableObject?.getValue("stringField")) + assertEquals( + "NEW", + managedDynamicMutableObject?.getValue("stringField") + ) } + // Collections in RealmAny are tested in + // testSetsInRealmAny() + // testNestedCollectionsInListInRealmAny() + // testNestedCollectionsInDictionarytInRealmAny() } else -> error("Model contains untested properties: $property") } @@ -582,12 +592,16 @@ class DynamicMutableRealmObjectTests { assertEquals(1, actualList.size) val managedDynamicMutableObject = actualList[0] ?.asRealmObject() - val actualValue = managedDynamicMutableObject?.getValue("stringField") + val actualValue = + managedDynamicMutableObject?.getValue("stringField") assertEquals(expectedValue, actualValue) // Check we did indeed get a dynamic mutable object managedDynamicMutableObject?.set("stringField", "NEW") - assertEquals("NEW", managedDynamicMutableObject?.getValue("stringField")) + assertEquals( + "NEW", + managedDynamicMutableObject?.getValue("stringField") + ) } } else -> error("Model contains untested properties: $property") @@ -869,12 +883,16 @@ class DynamicMutableRealmObjectTests { assertEquals(1, actualSet.size) val managedDynamicMutableObject = actualSet.iterator().next() ?.asRealmObject() - val actualValue = managedDynamicMutableObject?.getValue("stringField") + val actualValue = + managedDynamicMutableObject?.getValue("stringField") assertEquals(expectedValue, actualValue) // Check we did indeed get a dynamic mutable object managedDynamicMutableObject?.set("stringField", "NEW") - assertEquals("NEW", managedDynamicMutableObject?.getValue("stringField")) + assertEquals( + "NEW", + managedDynamicMutableObject?.getValue("stringField") + ) } } else -> error("Model contains untested properties: $property") @@ -1062,9 +1080,13 @@ class DynamicMutableRealmObjectTests { val value = dynamicMutableRealm.copyToRealm( DynamicMutableRealmObject.create("Sample") ).set("stringField", "NEW_OBJECT") - dynamicSample.getNullableValueDictionary(property.name)["A"] = + dynamicSample.getNullableValueDictionary( + property.name + )["A"] = value - dynamicSample.getNullableValueDictionary(property.name)["B"] = + dynamicSample.getNullableValueDictionary( + property.name + )["B"] = null val nullableObjDictionary = @@ -1165,7 +1187,10 @@ class DynamicMutableRealmObjectTests { ).also { dynamicMutableUnmanagedObject -> val dynamicRealmAny = RealmAny.create(dynamicMutableUnmanagedObject) - dynamicSample.set(name, realmDictionaryOf("A" to dynamicRealmAny)) + dynamicSample.set( + name, + realmDictionaryOf("A" to dynamicRealmAny) + ) val expectedValue = dynamicMutableUnmanagedObject.getValue("stringField") val actualDictionary = @@ -1186,7 +1211,10 @@ class DynamicMutableRealmObjectTests { ).also { dynamicMutableManagedObject -> val dynamicRealmAny = RealmAny.create(dynamicMutableManagedObject) - dynamicSample.set(name, realmDictionaryOf("A" to dynamicRealmAny)) + dynamicSample.set( + name, + realmDictionaryOf("A" to dynamicRealmAny) + ) val expectedValue = dynamicMutableManagedObject.getValue("stringField") val actualDictionary = @@ -1194,12 +1222,16 @@ class DynamicMutableRealmObjectTests { assertEquals(1, actualDictionary.size) val managedDynamicMutableObject = actualDictionary["A"] ?.asRealmObject() - val actualValue = managedDynamicMutableObject?.getValue("stringField") + val actualValue = + managedDynamicMutableObject?.getValue("stringField") assertEquals(expectedValue, actualValue) // Check we did indeed get a dynamic mutable object managedDynamicMutableObject?.set("stringField", "NEW") - assertEquals("NEW", managedDynamicMutableObject?.getValue("stringField")) + assertEquals( + "NEW", + managedDynamicMutableObject?.getValue("stringField") + ) } } else -> error("Model contains untested properties: $property") @@ -1354,6 +1386,191 @@ class DynamicMutableRealmObjectTests { } } + @Test + fun testSetsInRealmAny() { + val dynamicSampleInner = dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER" + ) + ) + dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create( + "Sample", + "nullableRealmAnyField" to RealmAny.create( + realmSetOf( + RealmAny.create( + dynamicSampleInner + ) + ) + ) + ) + ).let { + val set = it.getNullableValue("nullableRealmAnyField")!!.asSet() + // Verify that we get mutable instances out of the set + val setObject = set.first()!!.asRealmObject() + assertIs(setObject) + assertEquals("INNER", setObject.getValue("stringField")) + setObject.set("stringField", "UPDATED_INNER") + // Verify that we can add elements to the set + set.add(RealmAny.create(dynamicSampleInner)) + // Verify that we cannot add nested collections + assertFailsWithMessage("Sets cannot contain other collections") { + set.add(RealmAny.create(realmSetOf())) + } + assertFailsWithMessage("Sets cannot contain other collections") { + set.add(RealmAny.create(realmListOf())) + } + assertFailsWithMessage("Sets cannot contain other collections") { + set.add(RealmAny.create(realmDictionaryOf())) + } + } + } + + @Test + fun testNestedCollectionsInListInRealmAny() { + val dynamicSampleInner = dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create("Sample", "stringField" to "INNER") + ) + dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create( + "Sample", + "nullableRealmAnyField" to RealmAny.create( + realmListOf( + RealmAny.create( + realmSetOf( + RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_SET" + ) + ) + ) + ), + RealmAny.create( + realmListOf( + RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_LIST" + ) + ) + ) + ), + RealmAny.create( + realmDictionaryOf( + "key" to RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_DICT" + ) + ) + ) + ), + ) + ) + ) + ).let { + val list = it.getNullableValue("nullableRealmAnyField")!!.asList() + // Verify that we get mutable instances out of the set + list[0]!!.asSet().let { embeddedSet -> + val o = embeddedSet.first()!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_SET", o.getValue("stringField")) + embeddedSet.add(RealmAny.Companion.create(dynamicSampleInner)) + } + list[1]!!.asList().let { embeddedList -> + val o = embeddedList.first()!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_LIST", o.getValue("stringField")) + embeddedList.add(RealmAny.Companion.create(dynamicSampleInner)) + } + list[2]!!.asDictionary().let { embeddedDictionary -> + val o = embeddedDictionary["key"]!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_DICT", o.getValue("stringField")) + embeddedDictionary.put("UPDATE", RealmAny.Companion.create(dynamicSampleInner)) + } + } + } + + @Test + fun testNestedCollectionsInDictionarytInRealmAny() { + val dynamicSampleInner = dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER" + ) + ) + // Collections in dictionary + dynamicMutableRealm.copyToRealm( + DynamicMutableRealmObject.create( + "Sample", + "nullableRealmAnyField" to RealmAny.create( + realmDictionaryOf( + "set" to RealmAny.create( + realmSetOf( + RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_SET" + ) + ) + ) + ), + "list" to RealmAny.create( + realmListOf( + RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_LIST" + ) + ) + ) + ), + "dict" to RealmAny.create( + realmDictionaryOf( + "key" to RealmAny.create( + DynamicMutableRealmObject.create( + "Sample", + "stringField" to "INNER_DICT" + ) + ) + ) + ), + ) + ) + ) + ).let { + val dict = it.getNullableValue("nullableRealmAnyField")!!.asDictionary() + // Verify that we get mutable instances out of the set + dict["set"]!!.asSet().let { embeddedSet -> + val o = embeddedSet.first()!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_SET", o.getValue("stringField")) + embeddedSet.add(RealmAny.Companion.create(dynamicSampleInner)) + } + dict["list"]!!.asList().let { embeddedList -> + val o = embeddedList.first()!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_LIST", o.getValue("stringField")) + embeddedList.add(RealmAny.Companion.create(dynamicSampleInner)) + } + dict["dict"]!!.asDictionary().let { embeddedDictionary -> + val o = embeddedDictionary["key"]!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_DICT", o.getValue("stringField")) + embeddedDictionary.put("UPDATE", RealmAny.Companion.create(dynamicSampleInner)) + } + } + } + @Test fun set_embeddedRealmObject() { val parent = @@ -1735,7 +1952,10 @@ class DynamicMutableRealmObjectTests { "Sample", "stringField" to "intermediate", "nullableObject" to child2, - "nullableObjectDictionaryFieldNotNull" to realmDictionaryOf("A" to child2, "B" to child2) + "nullableObjectDictionaryFieldNotNull" to realmDictionaryOf( + "A" to child2, + "B" to child2 + ) ) val parent = dynamicMutableRealm.copyToRealm(DynamicMutableRealmObject.create("Sample")) parent.getObjectDictionary("nullableObjectDictionaryFieldNotNull").run { @@ -1761,7 +1981,10 @@ class DynamicMutableRealmObjectTests { "Sample", "stringField" to "intermediate", "nullableObject" to child2, - "nullableObjectDictionaryFieldNotNull" to realmDictionaryOf("A" to child2, "B" to child2) + "nullableObjectDictionaryFieldNotNull" to realmDictionaryOf( + "A" to child2, + "B" to child2 + ) ) val parent = dynamicMutableRealm.copyToRealm(DynamicMutableRealmObject.create("Sample")) parent.getObjectDictionary("nullableObjectDictionaryFieldNotNull").run { @@ -1778,4 +2001,15 @@ class DynamicMutableRealmObjectTests { dynamicMutableRealm.copyToRealm(DynamicMutableRealmObject.create("EmbeddedChild")) } } + + @Test + fun throwsOnRealmAnyPrimaryKey() { + val instance = DynamicMutableRealmObject.create( + "PrimaryKeyString", + "primaryKey" to RealmAny.create("PRIMARY_KEY"), + ) + assertFailsWithMessage("Cannot use object 'RealmAny{type=STRING, value=PRIMARY_KEY}' of type 'RealmAnyImpl' as primary key argument") { + dynamicMutableRealm.copyToRealm(instance) + } + } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt index 4c3adbe111..31195ebad0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt @@ -35,6 +35,7 @@ import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmDictionaryOf import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.ext.toRealmSet import io.realm.kotlin.internal.asDynamicRealm import io.realm.kotlin.query.RealmQuery @@ -61,6 +62,7 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -1541,6 +1543,55 @@ class DynamicRealmObjectTests { // This should be tested for DynamicMutableRealm instead. } + @Test + fun get_realmAny_nestedCollectionsInList() { + val unmanagedSample = Sample().apply { + nullableRealmAnyField = RealmAny.create( + realmListOf( + RealmAny.create( + realmListOf(RealmAny.create(Sample().apply { stringField = "INNER_LIST" })) + ), + RealmAny.create( + realmSetOf(RealmAny.create(Sample().apply { stringField = "INNER_SET" })) + ), + RealmAny.create( + realmDictionaryOf("key" to RealmAny.create(Sample().apply { stringField = "INNER_DICT" })) + ), + ) + ) + } + realm.writeBlocking { copyToRealm(unmanagedSample) } + realm.asDynamicRealm() + .also { dynamicRealm -> + val dynamicSample = dynamicRealm.query("Sample") + .find() + .first() + + val actualList = dynamicSample.getNullableValue( + Sample::nullableRealmAnyField.name, + RealmAny::class + )!!.asList() + + actualList[0]!!.let { innerList -> + val actualSample = innerList.asList()[0]!!.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_LIST", actualSample.getValue("stringField")) + } + actualList[1]!!.let { innerSet -> + val actualSample = + innerSet.asSet()!!.first()!!.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_SET", actualSample.getValue("stringField")) + } + actualList[2]!!.let { innerDictionary -> + val actualSample = + innerDictionary.asDictionary()!!["key"]!!.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_DICT", actualSample.getValue("stringField")) + } + } + } + @Test fun get_realmAnySet() { val realmAnyValues = realmListOf( @@ -1550,7 +1601,7 @@ class DynamicRealmObjectTests { ) val unmanagedSample = Sample().apply { - nullableRealmAnySetField = realmAnyValues.toRealmSet() + nullableRealmAnyField = RealmAny.create(realmAnyValues.toRealmSet()) } realm.writeBlocking { copyToRealm(unmanagedSample) } realm.asDynamicRealm() @@ -1574,7 +1625,8 @@ class DynamicRealmObjectTests { if (value?.type == RealmAny.Type.OBJECT) { assertEquals( value.asRealmObject().stringField, - actual.asRealmObject().getValue("stringField") + actual.asRealmObject() + .getValue("stringField") ) assertionSucceeded = true return @@ -1602,6 +1654,61 @@ class DynamicRealmObjectTests { // This should be tested for DynamicMutableRealm instead. } + @Test + fun get_realmAnyNestedSet() { + val realmAnyValues = realmListOf( + null, + RealmAny.create("Hello"), + RealmAny.create(Sample().apply { stringField = "INNER" }), + ) + + val unmanagedSample = Sample().apply { + nullableRealmAnyField = RealmAny.create(realmAnyValues.toRealmSet()) + } + realm.writeBlocking { copyToRealm(unmanagedSample) } + realm.asDynamicRealm() + .also { dynamicRealm -> + val dynamicSample = dynamicRealm.query("Sample") + .find() + .first() + + val actualReifiedSet: RealmSet = + dynamicSample.getNullableValue( + Sample::nullableRealmAnyField.name + )!!.asSet() + + fun assertions(actual: RealmAny?) { + if (actual?.type == RealmAny.Type.OBJECT) { + var assertionSucceeded = false + for (value in realmAnyValues) { + if (value?.type == RealmAny.Type.OBJECT) { + assertEquals( + value.asRealmObject().stringField, + actual.asRealmObject() + .getValue("stringField") + ) + assertionSucceeded = true + return + } + } + assertTrue(assertionSucceeded) + } else { + assertTrue(realmAnyValues.contains(actual)) + } + } + + for (actual in actualReifiedSet) { + assertions(actual) + } + } + + // In case of testing sets we have to skip dynamic managed objects inside a + // RealmSet since the semantics prevent us from writing a DynamicRealmObject + // in this way, rightfully so. The reason for this is that we use the regular realm's + // accessors which go through the non-dynamic path so objects inside the set are expected + // to be non-dynamic - the 'issueDynamicObject' flag is always false following this path. + // This should be tested for DynamicMutableRealm instead. + } @Test fun get_realmAnyDictionary() { val realmAnyValues = realmDictionaryOf( @@ -1671,6 +1778,70 @@ class DynamicRealmObjectTests { // This should be tested for DynamicMutableRealm instead. } + @Test + fun get_realmAny_nestedCollectionsInDictionary() { + val unmanagedSample = Sample().apply { + nullableRealmAnyField = RealmAny.create( + realmDictionaryOf( + "list" to RealmAny.create( + realmListOf(RealmAny.create(Sample().apply { stringField = "INNER_LIST" })) + ), + "set" to RealmAny.create( + realmSetOf(RealmAny.create(Sample().apply { stringField = "INNER_SET" })) + ), + "dict" to RealmAny.create( + realmDictionaryOf("key" to RealmAny.create(Sample().apply { stringField = "INNER_DICT" })) + ), + ) + ) + } + realm.writeBlocking { copyToRealm(unmanagedSample) } + realm.asDynamicRealm() + .also { dynamicRealm -> + val dynamicSample = dynamicRealm.query("Sample") + .find() + .first() + + val actualDictionary = dynamicSample.getNullableValue( + Sample::nullableRealmAnyField.name, + RealmAny::class + )!!.asDictionary() + + actualDictionary["list"]!!.let { innerList -> + val innerSample = innerList.asList()[0]!! + val actualSample = innerSample.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_LIST", actualSample.getValue("stringField")) + + assertFailsWith { + innerSample.asRealmObject() + } + } + actualDictionary["set"]!!.let { innerSet -> + val innerSample = innerSet.asSet()!!.first()!! + val actualSample = + innerSample.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_SET", actualSample.getValue("stringField")) + + assertFailsWith { + innerSample.asRealmObject() + } + } + actualDictionary["dict"]!!.let { innerDictionary -> + val innerSample = innerDictionary.asDictionary()!!["key"]!! + val actualSample = + innerSample.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_DICT", actualSample.getValue("stringField")) + + assertFailsWith { + innerSample.asRealmObject() + } + } + } + } + @Test fun get_throwsOnUnknownName() { realm.writeBlocking { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/BacklinksNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/BacklinksNotificationsTests.kt index e8d8d8a1c6..7bab7cd6c2 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/BacklinksNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/BacklinksNotificationsTests.kt @@ -341,7 +341,7 @@ class BacklinksNotificationsTests : RealmEntityNotificationTests { } @Test - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val sample = realm.write { copyToRealm(Sample()) } val mutex = Mutex(true) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedCollectionNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedCollectionNotificationTest.kt new file mode 100644 index 0000000000..25a6d773b9 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedCollectionNotificationTest.kt @@ -0,0 +1,108 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common.notifications + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.asFlow +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.notifications.DeletedObject +import io.realm.kotlin.notifications.InitialObject +import io.realm.kotlin.notifications.ObjectChange +import io.realm.kotlin.notifications.UpdatedObject +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.types.RealmAny +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class RealmAnyNestedCollectionNotificationTest { + + lateinit var tmpDir: String + lateinit var configuration: RealmConfiguration + lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configuration = RealmConfiguration.Builder( + schema = setOf(JsonStyleRealmObject::class) + ).directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun objectNotificationsOnNestedCollections() = runBlocking { + val channel = Channel>() + + val o: JsonStyleRealmObject = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + id = "SET" + value = realmAnyListOf(realmAnyListOf(1, 2, 3)) + } + ) + } + + val listener = async { + o.asFlow().collect { change -> + channel.send(change) + } + } + + assertIs>(channel.receiveOrFail()) + + realm.write { + findLatest(o)!!.value!!.asList()[0]!!.asList()[1] = RealmAny.create(4) + } + + val objectUpdate = channel.receiveOrFail() + assertIs>(objectUpdate) + objectUpdate.run { + assertEquals(1, changedFields.size) + assertTrue(changedFields.contains("value")) + val nestedList = obj.value!!.asList().first()!!.asList() + assertEquals(listOf(1, 4, 3), nestedList.map { it!!.asInt() }) + } + + realm.write { + delete(findLatest(o)!!) + } + + assertIs>(channel.receiveOrFail()) + + listener.cancel() + channel.close() + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt new file mode 100644 index 0000000000..8ef99181fa --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt @@ -0,0 +1,268 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common.notifications + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.realmAnyDictionaryOf +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnyOf +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.notifications.DeletedMap +import io.realm.kotlin.notifications.InitialMap +import io.realm.kotlin.notifications.MapChange +import io.realm.kotlin.notifications.UpdatedMap +import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.types.RealmAny +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class RealmAnyNestedDictionaryNotificationTest : RealmEntityNotificationTests { + + lateinit var tmpDir: String + lateinit var configuration: RealmConfiguration + lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configuration = RealmConfiguration.Builder( + schema = setOf(JsonStyleRealmObject::class) + ).directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + @Ignore // Initial element events are verified as part of the asFlow tests + override fun initialElement() {} + + @Test + override fun asFlow() = runBlocking { + val channel = Channel>() + + val o: JsonStyleRealmObject = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + id = "DICTIONARY" + value = realmAnyDictionaryOf( + "root" to realmAnyDictionaryOf( + "key1" to 1, + "key2" to 2, + "key3" to 3 + ) + ) + } + ) + } + + val dict = o.value!!.asDictionary()["root"]!!.asDictionary() + assertEquals(3, dict.size) + val listener = async { + dict.asFlow().collect { + channel.send(it) + } + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + assertEquals( + mapOf("key1" to 1, "key2" to 2, "key3" to 3), + this.map.mapValues { it.value!!.asInt() } + ) + } + + realm.write { + val liveList = findLatest(o)!!.value!!.asDictionary()["root"]!!.asDictionary() + liveList.put("key4", RealmAny.create(4)) + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + assertEquals(mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 4), this.map.mapValues { it.value!!.asInt() }) + } + + realm.write { + findLatest(o)!!.value = realmAnyOf(5) + } + + // Fails due to missing deletion events + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + } + listener.cancel() + channel.close() + } + + @Test + override fun cancelAsFlow() { + kotlinx.coroutines.runBlocking { + val container = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + value = realmAnyDictionaryOf("root" to realmAnyDictionaryOf()) + } + ) + } + val channel1 = Channel>(1) + val channel2 = Channel>(1) + val observedDict = container.value!!.asDictionary()["root"]!!.asDictionary() + val observer1 = async { + observedDict.asFlow() + .collect { change -> + channel1.trySend(change) + } + } + val observer2 = async { + observedDict.asFlow() + .collect { change -> + channel2.trySend(change) + } + } + + // Ignore first emission with empty sets + assertTrue { channel1.receiveOrFail(1.seconds).map.isEmpty() } + assertTrue { channel2.receiveOrFail(1.seconds).map.isEmpty() } + + // Trigger an update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asDictionary()["root"]!!.asDictionary().put("key1", RealmAny.create(1)) + } + assertEquals(1, channel1.receiveOrFail().map.size) + assertEquals(1, channel2.receiveOrFail().map.size) + + // Cancel observer 1 + observer1.cancel() + + // Trigger another update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asDictionary()["root"]!!.asDictionary().put("key2", RealmAny.create(2)) + } + + // Check channel 1 didn't receive the update + assertTrue(channel1.isEmpty) + // But channel 2 did + assertEquals(2, channel2.receiveOrFail().map.size) + + observer2.cancel() + channel1.close() + channel2.close() + } + } + + @Test + override fun deleteEntity() = runBlocking { + val container = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + value = realmAnyDictionaryOf("root" to realmAnyDictionaryOf()) + } + ) + } + val mutex = Mutex(true) + val flow = async { + container.value!!.asDictionary()["root"]!!.asDictionary().asFlow().first { + mutex.unlock() + it is DeletedMap + } + } + + // Await that flow is actually running + mutex.lock() + // Update mixed value to overwrite and delete set + realm.write { + findLatest(container)!!.value = realmAnyListOf() + } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + } + + @Test + override fun asFlowOnDeletedEntity() = runBlocking { + val container = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + value = realmAnyDictionaryOf("root" to realmAnyDictionaryOf()) + } + ) + } + val mutex = Mutex(true) + val flow = async { + container.value!!.asDictionary()["root"]!!.asDictionary().asFlow().first { + mutex.unlock() + it is DeletedMap + } + } + + // Await that flow is actually running + mutex.lock() + // And delete containing entity + realm.write { delete(findLatest(container)!!) } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + + // Verify that a flow on the deleted entity will signal a deletion and complete gracefully + withTimeout(10.seconds) { + container.value!!.asDictionary()["root"]!!.asDictionary().asFlow().collect { + assertIs>(it) + } + } + } + + @Test + @Ignore + override fun closingRealmDoesNotCancelFlows() { + TODO("Not yet implemented") + } + + @Ignore + override fun closeRealmInsideFlowThrows() { + TODO("Not yet implemented") + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt new file mode 100644 index 0000000000..6fd2814cfa --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -0,0 +1,254 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common.notifications + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnyOf +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.notifications.DeletedList +import io.realm.kotlin.notifications.InitialList +import io.realm.kotlin.notifications.ListChange +import io.realm.kotlin.notifications.UpdatedList +import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.types.RealmAny +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class RealmAnyNestedListNotificationTest : RealmEntityNotificationTests { + + lateinit var tmpDir: String + lateinit var configuration: RealmConfiguration + lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configuration = RealmConfiguration.Builder( + schema = setOf(JsonStyleRealmObject::class) + ).directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + @Ignore // Initial element events are verified as part of the asFlow tests + override fun initialElement() {} + + @Test + override fun asFlow() = runBlocking { + val channel = Channel>() + + val o: JsonStyleRealmObject = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + id = "LIST" + value = realmAnyListOf(realmAnyListOf(1, 2, 3)) + } + ) + } + + val list = o.value!!.asList()[0]!!.asList() + assertEquals(3, list.size) + val listener = async { + list.asFlow().collect { + channel.send(it) + } + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + assertEquals(listOf(1, 2, 3), this.list.map { it!!.asInt() }) + } + + realm.write { + val liveNestedList = findLatest(o)!!.value!!.asList()[0]!!.asList() + liveNestedList.add(RealmAny.create(4)) + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + assertEquals(listOf(1, 2, 3, 4), this.list.map { it!!.asInt() }) + } + + realm.write { + findLatest(o)!!.value = realmAnyOf(5) + } + + // Fails due to missing deletion events + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + } + listener.cancel() + channel.close() + } + + @Test + override fun cancelAsFlow() { + kotlinx.coroutines.runBlocking { + val container = realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf(realmAnyListOf()) }) + } + val channel1 = Channel>(1) + val channel2 = Channel>(1) + val observedSet = container.value!!.asList()[0]!!.asList() + val observer1 = async { + observedSet.asFlow() + .collect { change -> + channel1.trySend(change) + } + } + val observer2 = async { + observedSet.asFlow() + .collect { change -> + channel2.trySend(change) + } + } + + // Ignore first emission with empty sets + channel1.receiveOrFail() + channel2.receiveOrFail() + + // Trigger an update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asList()[0]!!.asList().add(RealmAny.create(1)) + } + assertEquals(1, channel1.receiveOrFail().list.size) + assertEquals(1, channel2.receiveOrFail().list.size) + + // Cancel observer 1 + observer1.cancel() + + // Trigger another update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asList()[0]!!.asList().add(RealmAny.create(2)) + } + + // Check channel 1 didn't receive the update + assertTrue(channel1.isEmpty) + // But channel 2 did + assertEquals(2, channel2.receiveOrFail().list.size) + + observer2.cancel() + channel1.close() + channel2.close() + } + } + + @Test + override fun deleteEntity() = runBlocking { + val container = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + value = realmAnyListOf( + realmAnyListOf() + ) + } + ) + } + val mutex = Mutex(true) + val flow = async { + container.value!!.asList()[0]!!.asList().asFlow().first { + mutex.unlock() + it is DeletedList<*> + } + } + + // Await that flow is actually running + mutex.lock() + // Update mixed value to overwrite and delete set + realm.write { + findLatest(container)!!.value = realmAnyListOf() + } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + } + + @Test + override fun asFlowOnDeletedEntity() = runBlocking { + val container = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { value = realmAnyListOf(realmAnyListOf()) } + ) + } + val mutex = Mutex(true) + val flow = async { + container.value!!.asList()[0]!!.asList().asFlow().first { + mutex.unlock() + it is DeletedList<*> + } + } + + // Await that flow is actually running + mutex.lock() + // And delete containing entity + realm.write { delete(findLatest(container)!!) } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + + // Verify that a flow on the deleted entity will signal a deletion and complete gracefully + withTimeout(10.seconds) { + container.value!!.asList()[0]!!.asList().asFlow().collect { + assertIs>(it) + } + } + } + + @Test + @Ignore + override fun closingRealmDoesNotCancelFlows() { + TODO("Not yet implemented") + } + + @Ignore + override fun closeRealmInsideFlowThrows() { + TODO("Not yet implemented") + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt new file mode 100644 index 0000000000..e000ad0a06 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt @@ -0,0 +1,249 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common.notifications + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnyOf +import io.realm.kotlin.ext.realmAnySetOf +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.notifications.DeletedSet +import io.realm.kotlin.notifications.InitialSet +import io.realm.kotlin.notifications.SetChange +import io.realm.kotlin.notifications.UpdatedSet +import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.types.RealmAny +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeout +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class RealmAnyNestedSetNotificationTest : RealmEntityNotificationTests { + + lateinit var tmpDir: String + lateinit var configuration: RealmConfiguration + lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configuration = RealmConfiguration.Builder( + schema = setOf(JsonStyleRealmObject::class) + ).directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + @Ignore // Initial element events are verified as part of the asFlow tests + override fun initialElement() {} + + @Test + override fun asFlow() = runBlocking { + val channel = Channel>() + + val o: JsonStyleRealmObject = realm.write { + copyToRealm( + JsonStyleRealmObject().apply { + id = "SET" + value = realmAnySetOf(1, 2, 3) + } + ) + } + + val set = o.value!!.asSet() + assertEquals(3, set.size) + val listener = async { + set.asFlow().collect { + channel.send(it) + } + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + val expectedSet = mutableSetOf(1, 2, 3) + this.set.forEach { expectedSet.remove(it!!.asInt()) } + assertTrue { expectedSet.isEmpty() } + } + + realm.write { + val liveSet = findLatest(o)!!.value!!.asSet() + liveSet.add(RealmAny.create(4)) + } + + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + val expectedSet = mutableSetOf(1, 2, 3, 4) + this.set.forEach { expectedSet.remove(it!!.asInt()) } + assertTrue { expectedSet.isEmpty() } + } + + realm.write { + findLatest(o)!!.value = realmAnyOf(5) + } + + // Fails due to missing deletion events + channel.receiveOrFail(1.seconds).run { + assertIs>(this) + } + listener.cancel() + channel.close() + } + + @Test + override fun cancelAsFlow() { + kotlinx.coroutines.runBlocking { + val container = realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnySetOf() }) + } + val channel1 = Channel>(1) + val channel2 = Channel>(1) + val observedSet = container.value!!.asSet() + val observer1 = async { + observedSet.asFlow() + .collect { change -> + channel1.trySend(change) + } + } + val observer2 = async { + observedSet.asFlow() + .collect { change -> + channel2.trySend(change) + } + } + + // Ignore first emission with empty sets + assertTrue { channel1.receiveOrFail(1.seconds).set.isEmpty() } + assertTrue { channel2.receiveOrFail(1.seconds).set.isEmpty() } + + // Trigger an update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asSet().add(RealmAny.Companion.create(1)) + } + assertEquals(1, channel1.receiveOrFail().set.size) + assertEquals(1, channel2.receiveOrFail().set.size) + + // Cancel observer 1 + observer1.cancel() + + // Trigger another update + realm.write { + val queriedContainer = findLatest(container) + queriedContainer!!.value!!.asSet().add(RealmAny.create(2)) + } + + // Check channel 1 didn't receive the update + assertTrue(channel1.isEmpty) + // But channel 2 did + assertEquals(2, channel2.receiveOrFail().set.size) + + observer2.cancel() + channel1.close() + channel2.close() + } + } + + @Test + override fun deleteEntity() = runBlocking { + val container = + realm.write { copyToRealm(JsonStyleRealmObject().apply { value = realmAnySetOf() }) } + val mutex = Mutex(true) + val flow = async { + container.value!!.asSet().asFlow().first { + mutex.unlock() + it is DeletedSet<*> + } + } + + // Await that flow is actually running + mutex.lock() + // Update mixed value to overwrite and delete set + realm.write { + findLatest(container)!!.value = realmAnyListOf() + } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + } + + @Test + override fun asFlowOnDeletedEntity() = runBlocking { + val container = + realm.write { copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf(realmAnySetOf()) }) } + val mutex = Mutex(true) + val flow = async { + container.value!!.asList()[0]!!.asSet().asFlow().first { + mutex.unlock() + it is DeletedSet<*> + } + } + + // Await that flow is actually running + mutex.lock() + // And delete containing entity + realm.write { delete(findLatest(container)!!) } + + // Await that notifier has signalled the deletion so we are certain that the entity + // has been deleted + withTimeout(10.seconds) { + flow.await() + } + + // Verify that a flow on the deleted entity will signal a deletion and complete gracefully + withTimeout(10.seconds) { + container.value!!.asList()[0]!!.asSet().asFlow().collect { + assertIs>(it) + } + } + } + + @Test + @Ignore + override fun closingRealmDoesNotCancelFlows() { + TODO("Not yet implemented") + } + + @Ignore + override fun closeRealmInsideFlowThrows() { + TODO("Not yet implemented") + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt index 05cee44aad..ffe28e6800 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt @@ -125,7 +125,7 @@ class RealmDictionaryNotificationsTests : RealmEntityNotificationTests { } } - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val container = realm.write { copyToRealm(RealmDictionaryContainer()) } val mutex = Mutex(true) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt index 1121d93221..a25c77df5a 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt @@ -408,7 +408,7 @@ class RealmListNotificationsTests : RealmEntityNotificationTests { } @Test - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val container = realm.write { copyToRealm(RealmListContainer()) } val mutex = Mutex(true) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmObjectNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmObjectNotificationsTests.kt index 6a49bd0806..f9be48a8e4 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmObjectNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmObjectNotificationsTests.kt @@ -241,7 +241,7 @@ class RealmObjectNotificationsTests : RealmEntityNotificationTests { } @Test - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val sample = realm.write { copyToRealm(Sample()) } val mutex = Mutex(true) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmSetNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmSetNotificationsTests.kt index f4dd164bea..f765eb882d 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmSetNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmSetNotificationsTests.kt @@ -282,7 +282,7 @@ class RealmSetNotificationsTests : RealmEntityNotificationTests { } @Test - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val container = realm.write { copyToRealm(RealmSetContainer()) } val mutex = Mutex(true) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt index 76a769d501..5f9b5bfa4c 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt @@ -29,5 +29,5 @@ interface RealmEntityNotificationTests : FlowableTests { // Verify that we emit deletion events and close the flow when registering for notifications on // an outdated entity. - fun asFlowOnDeleteEntity() + fun asFlowOnDeletedEntity() } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index d8c59a2b38..f6bb6c93c9 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -20,6 +20,7 @@ import io.realm.kotlin.LogConfiguration import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.VersionId +import io.realm.kotlin.entities.JsonStyleRealmObject import io.realm.kotlin.entities.sync.BinaryObject import io.realm.kotlin.entities.sync.ChildPk import io.realm.kotlin.entities.sync.ParentPk @@ -28,6 +29,9 @@ import io.realm.kotlin.entities.sync.flx.FlexChildObject import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmAnyDictionaryOf +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnySetOf import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking @@ -1542,6 +1546,7 @@ class SyncedRealmTests { @Test fun flexibleSync_throwsWithLocalInitialRealmFile() { + val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -1556,6 +1561,44 @@ class SyncedRealmTests { } } + @Test + fun cannotSyncCollectionsInMixed() = runBlocking { + TestApp( + "cannotSyncCollectionsInMixed", + logLevel = LogLevel.ALL, + appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, + builder = { + it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) + } + ).use { flexApp -> + val (email, password) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email, password) + val local = createFlexibleSyncConfig( + user = user, + name = "local", + schema = setOf(JsonStyleRealmObject::class) + ) { + initialSubscriptions { + this.add(it.query()) + } + } + Realm.open(local).use { + it.write { + val obj = copyToRealm(JsonStyleRealmObject()) + assertFailsWithMessage("Cannot sync nested set") { + obj.value = realmAnySetOf() + } + assertFailsWithMessage("Cannot sync nested list") { + obj.value = realmAnyListOf() + } + assertFailsWithMessage("Cannot sync nested dictionary") { + obj.value = realmAnyDictionaryOf() + } + } + } + } + } + // @Test // fun initialVersion() { // assertEquals(INITIAL_VERSION, realm.version()) From ec94ae2f54d015a7316cfe4f962e1e372f06e206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Wed, 20 Sep 2023 10:07:01 +0200 Subject: [PATCH 02/12] Add NEXTMAJORCORE suffix to version --- CHANGELOG.md | 2 +- buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a483e7a2a0..566839e47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.12.0-SNAPSHOT (YYYY-MM-DD) +## 1.12.0-NEXTMAJORCORE-SNAPSHOT (YYYY-MM-DD) This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it. Downgrading to a previous file format is not possible. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 3402f0e318..125cfa0e52 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.12.0-SNAPSHOT" + const val version = "1.12.0-NEXTMAJORCORE-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From 53b90afca08affba7b547bbe0ef1980a1180c6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Mon, 11 Dec 2023 12:59:44 +0100 Subject: [PATCH 03/12] Update to latests next-major core and remove set support in RealmAny (#1587) --- CHANGELOG.md | 2 +- .../kotlin/internal/interop/RealmEnums.kt | 1 - .../kotlin/internal/interop/RealmInterop.kt | 10 - .../kotlin/internal/interop/RealmInterop.kt | 24 -- .../kotlin/internal/interop/RealmInterop.kt | 26 -- .../kotlin/io/realm/kotlin/ext/RealmAnyExt.kt | 10 - .../io/realm/kotlin/internal/Converters.kt | 13 - .../io/realm/kotlin/internal/RealmAnyImpl.kt | 3 - .../kotlin/internal/RealmListInternal.kt | 23 -- .../realm/kotlin/internal/RealmMapInternal.kt | 16 -- .../kotlin/internal/RealmObjectHelper.kt | 24 -- .../realm/kotlin/internal/RealmSetInternal.kt | 2 - .../kotlin/serializers/RealmKSerializers.kt | 2 - .../kotlin/io/realm/kotlin/types/RealmAny.kt | 72 +---- .../common/RealmAnyNestedCollectionTests.kt | 195 +------------- .../realm/kotlin/test/common/RealmAnyTests.kt | 3 + .../test/common/RealmDictionaryTests.kt | 4 +- .../kotlin/test/common/RealmListTests.kt | 5 +- .../kotlin/test/common/SerializationTests.kt | 4 - .../dynamic/DynamicMutableRealmObjectTests.kt | 83 +----- .../common/dynamic/DynamicRealmObjectTests.kt | 83 +----- ...ealmAnyNestedDictionaryNotificationTest.kt | 5 +- .../RealmAnyNestedListNotificationTest.kt | 5 +- .../RealmAnyNestedSetNotificationTest.kt | 249 ------------------ .../utils/DeletableEntityNotificationTests.kt | 33 +++ .../utils/RealmEntityNotificationTests.kt | 17 +- .../test/mongodb/common/SyncedRealmTests.kt | 33 +-- 27 files changed, 94 insertions(+), 853 deletions(-) delete mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index eb46097388..8655c7a2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * Minimum R8: 8.0.34. ### Internal -* None +* Updated to Realm Core 14.0.0-beta, commit 4926641c6cf2948fa494624ee802bb8cbc21387f. ## 1.13.1-SNAPSHOT (YYYY-MM-DD) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt index 3970840586..36f25fa852 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -96,6 +96,5 @@ expect enum class ValueType { RLM_TYPE_LINK, RLM_TYPE_UUID, RLM_TYPE_LIST, - RLM_TYPE_SET, RLM_TYPE_DICTIONARY, } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 382f3ae008..55a34904a9 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -291,7 +291,6 @@ expect object RealmInterop { isDefault: Boolean ) fun realm_set_embedded(obj: RealmObjectPointer, key: PropertyKey): RealmObjectPointer - fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) @@ -305,17 +304,14 @@ expect object RealmInterop { fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue - fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) fun realm_list_insert_embedded(list: RealmListPointer, index: Long): RealmObjectPointer // Returns the element previously at the specified position fun realm_list_set(list: RealmListPointer, index: Long, inputTransport: RealmValue) - fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer - fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer fun realm_list_set_dictionary(list: RealmListPointer, index: Long): RealmMapPointer @@ -349,10 +345,6 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue - fun realm_dictionary_find_set( - dictionary: RealmMapPointer, - mapKey: RealmValue - ): RealmSetPointer fun realm_dictionary_find_list( dictionary: RealmMapPointer, mapKey: RealmValue @@ -387,7 +379,6 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue - fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer fun realm_dictionary_insert_dictionary(dictionary: RealmMapPointer, mapKey: RealmValue): RealmMapPointer fun realm_dictionary_get_keys(dictionary: RealmMapPointer): RealmResultsPointer @@ -454,7 +445,6 @@ expect object RealmInterop { // FIXME OPTIMIZE Get many fun MemAllocator.realm_results_get(results: RealmResultsPointer, index: Long): RealmValue - fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer fun realm_results_get_dictionary(results: RealmResultsPointer, index: Long): RealmMapPointer fun realm_results_delete_all(results: RealmResultsPointer) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 27bd106a11..31c13956f8 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -500,10 +500,6 @@ actual object RealmInterop { return LongPointerWrapper(realmc.realm_set_embedded(obj.cptr(), key.key)) } - actual fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer { - realmc.realm_set_set(obj.cptr(), key.key) - return realm_get_set(obj, key) - } actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { realmc.realm_set_list(obj.cptr(), key.key) return realm_get_list(obj, key) @@ -569,8 +565,6 @@ actual object RealmInterop { realmc.realm_list_get(list.cptr(), index, struct) return RealmValue(struct) } - actual fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer = - LongPointerWrapper(realmc.realm_list_get_set(list.cptr(), index)) actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index)) @@ -584,18 +578,12 @@ actual object RealmInterop { actual fun realm_list_insert_embedded(list: RealmListPointer, index: Long): RealmObjectPointer { return LongPointerWrapper(realmc.realm_list_insert_embedded(list.cptr(), index)) } - actual fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer { - return LongPointerWrapper(realmc.realm_list_insert_set(list.cptr(), index)) - } actual fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer { return LongPointerWrapper(realmc.realm_list_insert_list(list.cptr(), index)) } actual fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { return LongPointerWrapper(realmc.realm_list_insert_dictionary(list.cptr(), index)) } - actual fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer { - return LongPointerWrapper(realmc.realm_list_set_set(list.cptr(), index)) - } actual fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer { return LongPointerWrapper(realmc.realm_list_set_list(list.cptr(), index)) } @@ -758,12 +746,6 @@ actual object RealmInterop { realmc.realm_dictionary_find(dictionary.cptr(), mapKey.value, struct, found) return RealmValue(struct) } - actual fun realm_dictionary_find_set( - dictionary: RealmMapPointer, - mapKey: RealmValue - ): RealmSetPointer { - return LongPointerWrapper(realmc.realm_dictionary_get_set(dictionary.cptr(), mapKey.value)) - } actual fun realm_dictionary_find_list( dictionary: RealmMapPointer, @@ -845,9 +827,6 @@ actual object RealmInterop { } ) } - actual fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer { - return LongPointerWrapper(realmc.realm_dictionary_insert_set(dictionary.cptr(), mapKey.value)) - } actual fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer { return LongPointerWrapper(realmc.realm_dictionary_insert_list(dictionary.cptr(), mapKey.value)) @@ -1927,9 +1906,6 @@ actual object RealmInterop { return RealmValue(value) } - actual fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer = - LongPointerWrapper(realmc.realm_results_get_set(results.cptr(), index)) - actual fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer = LongPointerWrapper(realmc.realm_results_get_list(results.cptr(), index)) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index a03e0b2cd8..5c17053581 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -966,10 +966,6 @@ actual object RealmInterop { return CPointerWrapper(realm_wrapper.realm_set_embedded(obj.cptr(), key.key)) } - actual fun realm_set_set(obj: RealmObjectPointer, key: PropertyKey): RealmSetPointer { - checkedBooleanResult(realm_wrapper.realm_set_set(obj.cptr(), key.key)) - return realm_get_set(obj, key) - } actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { checkedBooleanResult(realm_wrapper.realm_set_list(obj.cptr(), key.key)) return realm_get_list(obj, key) @@ -1031,8 +1027,6 @@ actual object RealmInterop { return RealmValue(struct) } - actual fun realm_list_get_set(list: RealmListPointer, index: Long): RealmSetPointer = - CPointerWrapper(realm_wrapper.realm_list_get_set(list.cptr(), index.toULong())) actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong())) @@ -1048,18 +1042,12 @@ actual object RealmInterop { ) ) } - actual fun realm_list_insert_set(list: RealmListPointer, index: Long): RealmSetPointer { - return CPointerWrapper(realm_wrapper.realm_list_insert_set(list.cptr(), index.toULong())) - } actual fun realm_list_insert_list(list: RealmListPointer, index: Long): RealmListPointer { return CPointerWrapper(realm_wrapper.realm_list_insert_list(list.cptr(), index.toULong())) } actual fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer { return CPointerWrapper(realm_wrapper.realm_list_insert_dictionary(list.cptr(), index.toULong())) } - actual fun realm_list_set_set(list: RealmListPointer, index: Long): RealmSetPointer { - return CPointerWrapper(realm_wrapper.realm_list_set_set(list.cptr(), index.toULong())) - } actual fun realm_list_set_list(list: RealmListPointer, index: Long): RealmListPointer { return CPointerWrapper(realm_wrapper.realm_list_set_list(list.cptr(), index.toULong())) } @@ -1281,13 +1269,6 @@ actual object RealmInterop { } } - actual fun realm_dictionary_find_set( - dictionary: RealmMapPointer, - mapKey: RealmValue - ): RealmSetPointer { - return CPointerWrapper(realm_wrapper.realm_dictionary_get_set(dictionary.cptr(), mapKey.value.readValue())) - } - actual fun realm_dictionary_find_list( dictionary: RealmMapPointer, mapKey: RealmValue @@ -1422,10 +1403,6 @@ actual object RealmInterop { return RealmValue(outputStruct) } - actual fun realm_dictionary_insert_set(dictionary: RealmMapPointer, mapKey: RealmValue): RealmSetPointer { - return CPointerWrapper(realm_wrapper.realm_dictionary_insert_set(dictionary.cptr(), mapKey.value.readValue())) - } - actual fun realm_dictionary_insert_list(dictionary: RealmMapPointer, mapKey: RealmValue): RealmListPointer { return CPointerWrapper(realm_wrapper.realm_dictionary_insert_list(dictionary.cptr(), mapKey.value.readValue())) } @@ -1689,9 +1666,6 @@ actual object RealmInterop { return RealmValue(value) } - actual fun realm_results_get_set(results: RealmResultsPointer, index: Long): RealmSetPointer = - CPointerWrapper(realm_wrapper.realm_results_get_set(results.cptr(), index.toULong())) - actual fun realm_results_get_list(results: RealmResultsPointer, index: Long): RealmListPointer = CPointerWrapper(realm_wrapper.realm_results_get_list(results.cptr(), index.toULong())) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt index b40d27ae9b..58b52eaaaf 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt @@ -45,7 +45,6 @@ public fun realmAnyOf(value: Any?): RealmAny? { is RealmUUID -> RealmAny.create(value) is RealmObject -> RealmAny.create(value) is DynamicRealmObject -> RealmAny.create(value) - is Set<*> -> RealmAny.create(value.map { realmAnyOf(it) }.toRealmSet()) is List<*> -> RealmAny.create(value.map { realmAnyOf(it) }.toRealmList()) is Map<*, *> -> RealmAny.create( value.map { (mapKey, mapValue) -> @@ -61,15 +60,6 @@ public fun realmAnyOf(value: Any?): RealmAny? { } } -/** - * Create a [RealmAny] containing a [RealmSet] of all arguments wrapped as [RealmAny]s. - * @param values elements of the set. - * - * See [RealmAny.create] for [RealmSet] constraints and examples of usage. - */ -public fun realmAnySetOf(vararg values: Any?): RealmAny = - RealmAny.create(values.map { realmAnyOf(it) }.toRealmSet()) - /** * Create a [RealmAny] containing a [RealmList] of all arguments wrapped as [RealmAny]s. * @param values elements of the set. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt index e33d020ab9..e13858fc0c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt @@ -29,7 +29,6 @@ import io.realm.kotlin.internal.interop.RealmQueryArgument import io.realm.kotlin.internal.interop.RealmQueryArgumentList import io.realm.kotlin.internal.interop.RealmQueryListArgument import io.realm.kotlin.internal.interop.RealmQuerySingleArgument -import io.realm.kotlin.internal.interop.RealmSetPointer import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.Timestamp import io.realm.kotlin.internal.interop.ValueType @@ -115,7 +114,6 @@ internal inline fun realmValueToRealmAny( owner: RealmReference, issueDynamicObject: Boolean, issueDynamicMutableObject: Boolean, - getSetFunction: () -> RealmSetPointer = { error("Cannot handled embedded sets") }, getListFunction: () -> RealmListPointer = { error("Cannot handled embedded lists") }, getDictionaryFunction: () -> RealmMapPointer = { error("Cannot handled embedded dictionaries") }, ): RealmAny? { @@ -151,11 +149,6 @@ internal inline fun realmValueToRealmAny( RealmAny.create(realmObject!! as RealmObject, clazz as KClass) } } - ValueType.RLM_TYPE_SET -> { - val nativePointer = getSetFunction() - val operator = realmAnySetOperator(mediator, owner, nativePointer, issueDynamicObject, issueDynamicMutableObject) - return RealmAny.create(ManagedRealmSet(parent, nativePointer, operator)) - } ValueType.RLM_TYPE_LIST -> { val nativePointer = getListFunction() val operator = realmAnyListOperator(mediator, owner, nativePointer, issueDynamicObject, issueDynamicMutableObject) @@ -176,7 +169,6 @@ internal fun MemTrackingAllocator.realmAnyHandler( value: RealmAny?, primitiveValueAsRealmValueHandler: (RealmValue) -> T = { throw IllegalArgumentException("Operation not support for primitive values") }, referenceAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for objects") }, - setAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for sets") }, listAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for lists") }, dictionaryAsRealmAnyHandler: (RealmAny) -> T = { throw IllegalArgumentException("Operation not support for dictionaries") }, ): T { @@ -200,10 +192,6 @@ internal fun MemTrackingAllocator.realmAnyHandler( referenceAsRealmAnyHandler(value) } - io.realm.kotlin.types.RealmAny.Type.SET -> { - setAsRealmAnyHandler(value) - } - io.realm.kotlin.types.RealmAny.Type.LIST -> { listAsRealmAnyHandler(value) } @@ -508,7 +496,6 @@ internal inline fun MemTrackingAllocator.realmAnyToRealmValueWithoutImport(value val objRef = realmObjectToRealmReferenceOrError(value.asRealmObject()) realmObjectTransport(objRef) } - RealmAny.Type.SET, RealmAny.Type.LIST, RealmAny.Type.DICTIONARY -> throw IllegalArgumentException("Cannot pass unmanaged collections as input argument") diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt index 4ff5acf465..a1c1b820b0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt @@ -22,7 +22,6 @@ import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject -import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.RealmUUID import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 @@ -93,8 +92,6 @@ internal class RealmAnyImpl constructor( return clazz.cast(getValue) } - override fun asSet(): RealmSet = getValue(RealmAny.Type.SET) as RealmSet - override fun asList(): RealmList = getValue(RealmAny.Type.LIST) as RealmList diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index da323d91da..a1de11c162 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -348,7 +348,6 @@ internal class RealmAnyListOperator( transport, null, mediator, realmReference, issueDynamicObject, issueDynamicMutableObject, - { RealmInterop.realm_list_get_set(nativePointer, index.toLong()) }, { RealmInterop.realm_list_get_list(nativePointer, index.toLong()) }, { RealmInterop.realm_list_get_dictionary(nativePointer, index.toLong()) } ) @@ -376,16 +375,6 @@ internal class RealmAnyListOperator( realmObjectToRealmReferenceWithImport(obj, mediator, realmReference, updatePolicy, cache) RealmInterop.realm_list_add(nativePointer, index.toLong(), realmObjectTransport(objRef)) }, - setAsRealmAnyHandler = { realmValue -> - val nativePointer = RealmInterop.realm_list_insert_set(nativePointer, index.toLong()) - val operator = realmAnySetOperator( - mediator, - realmReference, - nativePointer, - issueDynamicObject, issueDynamicMutableObject - ) - operator.addAll(realmValue.asSet(), updatePolicy, cache) - }, listAsRealmAnyHandler = { realmValue -> val nativePointer = RealmInterop.realm_list_insert_list(nativePointer, index.toLong()) val operator = realmAnyListOperator( @@ -425,18 +414,6 @@ internal class RealmAnyListOperator( realmObjectToRealmReferenceWithImport(realmValue.asRealmObject(), mediator, realmReference, updatePolicy, cache) RealmInterop.realm_list_set(nativePointer, index.toLong(), realmObjectTransport(objRef)) }, - setAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) - val nativePointer = RealmInterop.realm_list_set_set(nativePointer, index.toLong()) - val operator = realmAnySetOperator( - mediator, - realmReference, - nativePointer, - issueDynamicObject, issueDynamicMutableObject - ) - operator.addAll(realmValue.asSet(), updatePolicy, cache) - }, listAsRealmAnyHandler = { realmValue -> // Have to clear existing elements for core to know if we are updating with a new collection RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt index b5e2401ee6..9ee69b254d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt @@ -460,7 +460,6 @@ internal class RealmAnyMapOperator constructor( owner = realmReference, issueDynamicObject = issueDynamicObject, issueDynamicMutableObject = issueDynamicMutableObject, - getSetFunction = { RealmInterop.realm_results_get_set(resultsPointer, index.toLong()) }, getListFunction = { RealmInterop.realm_results_get_list(resultsPointer, index.toLong()) }, getDictionaryFunction = { RealmInterop.realm_results_get_dictionary(resultsPointer, index.toLong()) }, ) @@ -492,7 +491,6 @@ internal class RealmAnyMapOperator constructor( valueTransport, null, mediator, realmReference, issueDynamicObject, issueDynamicMutableObject, - { RealmInterop.realm_dictionary_find_set(nativePointer, keyTransport) }, { RealmInterop.realm_dictionary_find_list(nativePointer, keyTransport) } ) { RealmInterop.realm_dictionary_find_dictionary(nativePointer, keyTransport) } @@ -522,20 +520,6 @@ internal class RealmAnyMapOperator constructor( realmAny(result.first, keyTransport) to result.second } }, - setAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) - val previous = getInternal(key) - val nativePointer = RealmInterop.realm_dictionary_insert_set(nativePointer, keyTransport) - val operator = realmAnySetOperator( - mediator, - realmReference, - nativePointer, - issueDynamicObject, issueDynamicMutableObject - ) - operator.addAll(realmValue.asSet(), updatePolicy, cache) - previous to true - }, listAsRealmAnyHandler = { realmValue -> // Have to clear existing elements for core to know if we are updating with a new collection realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt index 13367aa2ca..d844026595 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt @@ -237,18 +237,6 @@ internal object RealmObjectHelper { referenceAsRealmAnyHandler = { realmValue -> setObjectByKey(obj, key, realmValue.asRealmObject(), updatePolicy, cache) }, - setAsRealmAnyHandler = { realmValue -> - RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) - val nativePointer = RealmInterop.realm_set_set(obj.objectPointer, key) - val operator = realmAnySetOperator( - obj.mediator, - obj.owner, - nativePointer, - false, - false - ) - operator.addAll(value.asSet(), updatePolicy, cache) - }, listAsRealmAnyHandler = { realmValue -> RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) @@ -335,7 +323,6 @@ internal object RealmObjectHelper { it, obj, obj.mediator, obj.owner, false, false, - { RealmInterop.realm_get_set(obj.objectPointer, key) }, { RealmInterop.realm_get_list(obj.objectPointer, key) } ) { RealmInterop.realm_get_dictionary(obj.objectPointer, key) } } @@ -946,7 +933,6 @@ internal object RealmObjectHelper { owner = obj.owner, issueDynamicObject = true, issueDynamicMutableObject = issueDynamicMutableObject, - getSetFunction = { RealmInterop.realm_get_set(obj.objectPointer, propertyInfo.key) }, getListFunction = { RealmInterop.realm_get_list(obj.objectPointer, propertyInfo.key) }, ) { RealmInterop.realm_get_dictionary(obj.objectPointer, propertyInfo.key) } @@ -1150,16 +1136,6 @@ internal object RealmObjectHelper { referenceAsRealmAnyHandler = { realmValue -> setObjectByKey(obj, key, realmValue.asRealmObject(), updatePolicy, cache) }, - setAsRealmAnyHandler = { realmValue -> - val nativePointer = RealmInterop.realm_set_set(obj.objectPointer, key) - val operator = realmAnySetOperator( - obj.mediator, - obj.owner, - nativePointer, - true, - ) - operator.addAll(value.asSet(), updatePolicy, cache) - }, listAsRealmAnyHandler = { realmValue -> val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) val operator = diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt index 6073739455..6f78d00c82 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt @@ -345,7 +345,6 @@ internal class RealmAnySetOperator( transport, null, mediator, realmReference, issueDynamicObject, issueDynamicMutableObject, - { error("Set should never container sets") }, { error("Set should never container lists") } ) { error("Set should never container dictionaries") } } @@ -371,7 +370,6 @@ internal class RealmAnySetOperator( realmObjectToRealmReferenceWithImport(obj, mediator, realmReference, updatePolicy, cache) RealmInterop.realm_set_insert(nativePointer, realmObjectTransport(objRef)) }, - setAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections") }, listAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, dictionaryAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, ) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt index 664a3e2db6..94a796edc4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt @@ -379,7 +379,6 @@ public object RealmAnyKSerializer : KSerializer { Type.OBJECT_ID -> RealmAny.create(it.objectId!!) Type.UUID -> RealmAny.create(it.uuid!!) Type.OBJECT -> RealmAny.create(it.realmObject!!) - Type.SET -> RealmAny.create(it.set!!) Type.LIST -> RealmAny.create(it.list!!) Type.DICTIONARY -> RealmAny.create(it.dictionary!!) } @@ -406,7 +405,6 @@ public object RealmAnyKSerializer : KSerializer { ) Type.UUID -> uuid = value.asRealmUUID() Type.OBJECT -> realmObject = value.asRealmObject() - Type.SET -> set = value.asSet() Type.LIST -> list = value.asList() Type.DICTIONARY -> dictionary = value.asDictionary() } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt index 6eccbd8016..309aa33d7b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt @@ -60,7 +60,6 @@ import kotlin.reflect.KClass * OBJECT_ID -> doSomething(realmAny.asObjectId()) * REALM_UUID -> doSomething(realmAny.asRealmUUID()) * REALM_OBJECT -> doSomething(realmAny.asRealmObject()) - * SET -> doSomething(realmAny.asSet()) * LIST -> doSomething(realmAny.asList()) * DICTIONARY -> doSomething(realmAny.asDictionary()) * } @@ -85,25 +84,20 @@ import kotlin.reflect.KClass * ``` * `RealmAny` cannot store [EmbeddedRealmObject]s. * - * `RealmAny` can contain other collections of [RealmAny]. This means that you can build nested - * collections inside a `RealmAny`-field. The only constraint is that sets cannot contain other - * collections, so must be at the leaf of such nested hierarchies. + * `RealmAny` can contain other [RealmList] and [RealmDictionary] of [RealmAny]. This means that + * you can build nested collections inside a `RealmAny`-field. * ``` * realmObjct.realmAnyField = realmAnyListOf( * // Primitive values can be added in collections * 1, - * // Sets are allowed but cannot contain nested collection types - * "realmSetOf(1), * // Lists and dictionaries can contain other nested collection types * realmListOf( - * realmSetOf(), * realmListOf(), * realmDictionaryOf() * ), * realmDictionaryOf( - * "key1" to realmSetOf(), - * "key2" to realmListOf(), - * "key3" to realmDictionaryOf()) + * "key1" to realmListOf(), + * "key2" to realmDictionaryOf()) * ) * ``` * @@ -137,7 +131,7 @@ public interface RealmAny { * Supported Realm data types that can be stored in a `RealmAny` instance. */ public enum class Type { - INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT, SET, LIST, DICTIONARY; + INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT, LIST, DICTIONARY; } /** @@ -258,12 +252,6 @@ public interface RealmAny { */ public fun asRealmObject(clazz: KClass): T - /** - * Returns the value from this `RealmAny` as a [RealmSet] containing new [RealmAny]s. - * @throws [IllegalStateException] if the stored value is not a set. - */ - public fun asSet(): RealmSet - /** * Returns the value from this `RealmAny` as a [RealmList] containing new [RealmAny]s. * @throws [IllegalStateException] if the stored value is not a list. @@ -387,34 +375,6 @@ public interface RealmAny { public fun create(realmObject: DynamicRealmObject): RealmAny = RealmAnyImpl(Type.OBJECT, DynamicRealmObject::class, realmObject) - /** - * Creates an unmanaged `RealmAny` instance from a [RealmSet] of [RealmAny] values. - * - * To create a [RealmAny] containing a [RealmSet] of arbitrary values wrapped in [RealmAny]s - * use the [io.realm.kotlin.ext.realmAnySetOf]. - * - * **NOTE:** Realm does not support to having other collections ([RealmSet], [RealmList] and - * [RealmDictionary]) in a [RealmSet]. These kind of structures can be built in a [RealmAny] - * but will fail if imported to realm. - * - * Example: - * ``` - * class SampleObject() : RealmObject { - * val realmAnyField: RealmAny? = null - * } - * val realmObject = copyToRealm(SampleObject()) - * - * // Sets with non-collection types can be built and imported into realm. - * realmObject.realmAnyField = realmAnySetOf(1, "Realm", realmObject) - * - * // Sets with collection types can be built but cannot be imported into realm. - * val setsWithCollections = realmAnySetOf(realmSetOf(), realmListOf(), realmDictionaryOf()) - * realmObject.realmAnyField = setsWithCollections // Will throw IllegalArgumentExcception - * ``` - */ - public fun create(value: RealmSet): RealmAny = - RealmAnyImpl(Type.SET, RealmAny::class, value) - /** * Creates an unmanaged `RealmAny` instance from a [RealmList] of [RealmAny] values. * @@ -428,22 +388,19 @@ public interface RealmAny { * } * val realmObject = copyToRealm(SampleObject()) * - * // Lists can contain other collection types, including [RealmSet]s. + * // List can contain other collections, but only `RealmList` and + * // `RealmDictionary`. * realmObject.realmAnyField = realmAnyListOf( * // Primitive values * 1, - * // Sets are allowed but cannot contain nested collection types - * realmSetOf(1), * // Lists and dictionaries can contain other collection types * realmListOf( - * realmSetOf(), * realmListOf(), * realmDictionaryOf() * ), * realmDictionaryOf( - * "key1" to realmSetOf(), - * "key2" to realmListOf(), - * "key3" to realmDictioneryOf()) + * "key1" to realmListOf(), + * "key2" to realmDictioneryOf()) * ) * ``` */ @@ -463,21 +420,18 @@ public interface RealmAny { * } * val realmObject = copyToRealm(SampleObject()) * - * // Dictionaries can contain other collection types, including [RealmSet]s. + * // Dictionaries can contain other collections, but only `RealmList` and + * // `RealmDictionary`. * realmObjct.realmAnyField = realmAnyDictionaryOf( * "int" to 5, - * // Sets are allowed but cannot contain nested collection types - * "set" to "realmSetOf(1), * // Lists and dictionaries can contain other nested collection types * "list" to realmListOf( - * realmSetOf(), * realmListOf(), * realmDictionaryOf() * ), * "dictionary" to realmDictionaryOf( - * "key1" to realmSetOf(), - * "key2" to realmListOf(), - * "key3" to realmDictionaryOf()) + * "key1" to realmListOf(), + * "key2" to realmDictionaryOf()) * ) * ``` */ diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt index 84057002cc..d377684f94 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -24,15 +24,12 @@ import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmAnyDictionaryOf import io.realm.kotlin.ext.realmAnyListOf -import io.realm.kotlin.ext.realmAnySetOf import io.realm.kotlin.ext.realmDictionaryOf import io.realm.kotlin.ext.realmListOf -import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.test.common.utils.assertFailsWithMessage import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.types.RealmAny -import org.mongodb.kbson.ObjectId import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -67,100 +64,6 @@ class RealmAnyNestedCollectionTests { PlatformUtils.deleteTempDir(tmpDir) } - @Test - fun setInRealmAny_copyToRealm() = runBlocking { - val sample = Sample().apply { stringField = "SAMPLE" } - realm.write { - val instance = JsonStyleRealmObject().apply { - value = RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ) - } - copyToRealm(instance) - } - val managedSample: Sample = realm.query().find().single() - val anyValue: RealmAny = realm.query().find().single().value!! - assertEquals(RealmAny.Type.SET, anyValue.type) - anyValue.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } - } - - @Test - fun setInRealmAny_assignment() = runBlocking { - val sample = Sample().apply { stringField = "SAMPLE" } - realm.write { - val managedInstance = copyToRealm(JsonStyleRealmObject()) - managedInstance.value = RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ) - } - val managedSample: Sample = realm.query().find().single() - val anyValue: RealmAny = realm.query().find().single().value!! - anyValue.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } - } - - @Test - fun setInRealmAny_throwsOnNestedCollections_copyToRealm() = runBlocking { - realm.write { - JsonStyleRealmObject(ObjectId().toString()).apply { - value = - RealmAny.create(realmSetOf(RealmAny.create(realmListOf(RealmAny.create(5))))) - }.let { - assertFailsWithMessage("Sets cannot contain other collections") { - copyToRealm(it) - } - } - JsonStyleRealmObject(ObjectId().toString()).apply { - value = RealmAny.create( - realmSetOf( - RealmAny.create(realmDictionaryOf("key" to RealmAny.create(5))) - ) - ) - }.let { - assertFailsWithMessage("Sets cannot contain other collections") { - copyToRealm(it) - } - } - } - } - - @Test - fun setInRealmAny_throwsOnNestedCollections_add() = runBlocking { - realm.write { - val instance = copyToRealm( - JsonStyleRealmObject().apply { value = RealmAny.create(realmSetOf()) } - ) - val set = instance.value!!.asSet() - - val realmAnyList = RealmAny.create(realmListOf()) - assertFailsWithMessage("Sets cannot contain other collections") { - set.add(realmAnyList) - } - - val realmAnyDictionary = RealmAny.create(realmDictionaryOf()) - assertFailsWithMessage("Sets cannot contain other collections") { - set.add(realmAnyDictionary) - } - } - } - @Test fun listInRealmAny_copyToRealm() = runBlocking { val sample = Sample().apply { stringField = "SAMPLE" } @@ -206,14 +109,6 @@ class RealmAnyNestedCollectionTests { RealmAny.create(sample), ) ), - // Embedded set - RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ), // Embedded map RealmAny.create( realmDictionaryOf( @@ -243,13 +138,7 @@ class RealmAnyNestedCollectionTests { assertEquals(RealmAny.create("Realm"), embeddedList[1]) assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) } - it[4]!!.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } - it[5]!!.asDictionary().toMutableMap().let { embeddedDict -> + it[4]!!.asDictionary().toMutableMap().let { embeddedDict -> assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) assertEquals( @@ -280,16 +169,6 @@ class RealmAnyNestedCollectionTests { ) ), ) - // Embedded set - add( - RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ), - ) // Embedded map add( RealmAny.create( @@ -313,13 +192,7 @@ class RealmAnyNestedCollectionTests { assertEquals(RealmAny.create("Realm"), embeddedList[1]) assertEquals("SAMPLE", embeddedList[2]!!.asRealmObject().stringField) } - it[4]!!.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } - it[5]!!.asDictionary().toMutableMap().let { embeddedDict -> + it[4]!!.asDictionary().toMutableMap().let { embeddedDict -> assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) assertEquals( @@ -358,17 +231,6 @@ class RealmAnyNestedCollectionTests { ) ), ) - // Embedded set - set( - 1, - RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ), - ) // Embedded map set( 2, @@ -390,12 +252,6 @@ class RealmAnyNestedCollectionTests { assertEquals(RealmAny.create(5), embeddedList[0]) assertEquals("SAMPLE", embeddedList[1]!!.asRealmObject().stringField) } - it[1]!!.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } it[2]!!.asDictionary().toMutableMap().let { embeddedDict -> assertEquals(RealmAny.create(5), embeddedDict["keyInt"]) assertEquals(RealmAny.create("Realm"), embeddedDict["keyString"]) @@ -452,13 +308,6 @@ class RealmAnyNestedCollectionTests { value = RealmAny.create( realmDictionaryOf( "keyInt" to RealmAny.create(5), - "keySet" to RealmAny.create( - realmSetOf( - RealmAny.create(5), - RealmAny.create("Realm"), - RealmAny.create(sample), - ) - ), "keyList" to RealmAny.create( realmListOf( RealmAny.create(5), @@ -486,14 +335,8 @@ class RealmAnyNestedCollectionTests { assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) val managedSample: Sample = realm.query().find().single() anyValue.asDictionary().run { - assertEquals(4, size) + assertEquals(3, size) assertEquals(5, get("keyInt")!!.asInt()) - get("keySet")!!.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } get("keyList")!!.asList().let { embeddedList -> assertEquals(RealmAny.create(5), embeddedList[0]) assertEquals(RealmAny.create("Realm"), embeddedList[1]) @@ -518,7 +361,6 @@ class RealmAnyNestedCollectionTests { JsonStyleRealmObject().apply { // Assigning dictionary with nested lists and dictionaries value = realmAnyDictionaryOf( - "keySet" to realmAnySetOf(5, "Realm", sample), "keyList" to realmAnyListOf(5, "Realm", sample), "keyDictionary" to realmAnyDictionaryOf( "keyInt" to 5, @@ -536,17 +378,9 @@ class RealmAnyNestedCollectionTests { val anyValue: RealmAny = jsonStyleRealmObject.value!! assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) anyValue.asDictionary().values.run { - assertEquals(3, size) + assertEquals(2, size) forEach { value -> when (value?.type) { - RealmAny.Type.SET -> { - value.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } - } RealmAny.Type.LIST -> { value.asList().let { embeddedList -> assertEquals(RealmAny.create(5), embeddedList[0]) @@ -583,7 +417,6 @@ class RealmAnyNestedCollectionTests { query().find().single().value!!.asDictionary().run { val sample = copyToRealm(Sample().apply { stringField = "SAMPLE" }) put("keyInt", RealmAny.create(5)) - put("keySet", realmAnySetOf(5, "Realm", sample)) put("keyList", realmAnyListOf(5, "Realm", sample)) put( "keyDictionary", @@ -602,14 +435,8 @@ class RealmAnyNestedCollectionTests { val anyValue: RealmAny = jsonStyleRealmObject.value!! assertEquals(RealmAny.Type.DICTIONARY, anyValue.type) anyValue.asDictionary().run { - assertEquals(4, size) + assertEquals(3, size) assertEquals(5, get("keyInt")!!.asInt()) - get("keySet")!!.asSet().let { embeddedSet -> - assertEquals(3, embeddedSet.size) - assertTrue { embeddedSet.contains(RealmAny.create(5)) } - assertTrue { embeddedSet.contains(RealmAny.create("Realm")) } - assertTrue { embeddedSet.contains(RealmAny.create(managedSample)) } - } get("keyList")!!.asList().let { embeddedList -> assertEquals(RealmAny.create(5), embeddedList[0]) assertEquals(RealmAny.create("Realm"), embeddedList[1]) @@ -695,9 +522,6 @@ class RealmAnyNestedCollectionTests { @Test fun query_ThrowsOnNestedCollectionArguments() { - assertFailsWithMessage("Invalid query argument: Cannot pass unmanaged collections as input argument") { - realm.query("value == $0", RealmAny.create(realmSetOf())) - } assertFailsWithMessage("Invalid query argument: Cannot pass unmanaged collections as input argument") { realm.query("value == $0", RealmAny.create(realmListOf())) } @@ -709,12 +533,6 @@ class RealmAnyNestedCollectionTests { @Test fun query() = runBlocking { realm.write { - copyToRealm( - JsonStyleRealmObject().apply { - id = "SET" - value = realmAnySetOf(1, 2, 3) - } - ) copyToRealm( JsonStyleRealmObject().apply { id = "LIST" @@ -735,7 +553,6 @@ class RealmAnyNestedCollectionTests { JsonStyleRealmObject().apply { id = "EMBEDDED" value = realmAnyListOf( - setOf(1, 2, 3), listOf(4, 5, 6), mapOf( "key1" to 7, @@ -747,7 +564,7 @@ class RealmAnyNestedCollectionTests { ) } - assertEquals(4, realm.query().find().size) + assertEquals(3, realm.query().find().size) // Matching lists realm.query("value[0] == 4").find().single().run { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt index 3601d40551..679aeab20e 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt @@ -489,6 +489,9 @@ class RealmAnyTests { // Different objects of same type are not equal assertNotEquals(RealmAny.create(Sample()), RealmAny.create(realmObject)) } + // Collections in RealmAny are tested in RealmAnyNestedCollections.kt + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> {} } } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt index b98e34baba..fbf739539e 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmDictionaryTests.kt @@ -3078,9 +3078,9 @@ internal class RealmAnyDictionaryTester( assertEquals(expectedObj.stringField, assertNotNull(actualObj).stringField) } null -> assertNull(actualValue) - RealmAny.Type.SET, + // Collections in RealmAny are tested separately in RealmAnyNestedCollectionTests RealmAny.Type.LIST, - RealmAny.Type.DICTIONARY -> {} // Tested separately in RealmAnyNestedCollectionTests + RealmAny.Type.DICTIONARY -> {} } } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index eeea0b30b4..7aefea5767 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -1345,9 +1345,8 @@ internal class RealmAnyListTester constructor( expected.asRealmObject().stringField, actual.asRealmObject().stringField ) - // FIXME Should we rather test nested collections somewhere else? - RealmAny.Type.SET -> TODO() - RealmAny.Type.LIST -> TODO() + // Collections in RealmAny are tested separately in RealmAnyNestedCollectionTests + RealmAny.Type.LIST, RealmAny.Type.DICTIONARY -> TODO() } } else if (expected != null || actual != null) { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt index d9c0bf6787..580c8f2a1f 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt @@ -33,7 +33,6 @@ import io.realm.kotlin.entities.SerializableSample import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.realmAnyDictionaryOf import io.realm.kotlin.ext.realmAnyListOf -import io.realm.kotlin.ext.realmAnySetOf import io.realm.kotlin.internal.restrictToMillisPrecision import io.realm.kotlin.serializers.MutableRealmIntKSerializer import io.realm.kotlin.serializers.RealmAnyKSerializer @@ -338,9 +337,6 @@ class SerializationTests { nullableRealmAnyField = RealmAny.create(it) } } - RealmAny.Type.SET -> SerializableSample().apply { - nullableRealmAnyField = realmAnySetOf(1, 2, 3) - } RealmAny.Type.LIST -> SerializableSample().apply { nullableRealmAnyField = realmAnyListOf(RealmAny.create(1), RealmAny.create(2)) } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt index 9f834b9372..32ee015f45 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt @@ -1386,47 +1386,6 @@ class DynamicMutableRealmObjectTests { } } - @Test - fun testSetsInRealmAny() { - val dynamicSampleInner = dynamicMutableRealm.copyToRealm( - DynamicMutableRealmObject.create( - "Sample", - "stringField" to "INNER" - ) - ) - dynamicMutableRealm.copyToRealm( - DynamicMutableRealmObject.create( - "Sample", - "nullableRealmAnyField" to RealmAny.create( - realmSetOf( - RealmAny.create( - dynamicSampleInner - ) - ) - ) - ) - ).let { - val set = it.getNullableValue("nullableRealmAnyField")!!.asSet() - // Verify that we get mutable instances out of the set - val setObject = set.first()!!.asRealmObject() - assertIs(setObject) - assertEquals("INNER", setObject.getValue("stringField")) - setObject.set("stringField", "UPDATED_INNER") - // Verify that we can add elements to the set - set.add(RealmAny.create(dynamicSampleInner)) - // Verify that we cannot add nested collections - assertFailsWithMessage("Sets cannot contain other collections") { - set.add(RealmAny.create(realmSetOf())) - } - assertFailsWithMessage("Sets cannot contain other collections") { - set.add(RealmAny.create(realmListOf())) - } - assertFailsWithMessage("Sets cannot contain other collections") { - set.add(RealmAny.create(realmDictionaryOf())) - } - } - } - @Test fun testNestedCollectionsInListInRealmAny() { val dynamicSampleInner = dynamicMutableRealm.copyToRealm( @@ -1437,16 +1396,6 @@ class DynamicMutableRealmObjectTests { "Sample", "nullableRealmAnyField" to RealmAny.create( realmListOf( - RealmAny.create( - realmSetOf( - RealmAny.create( - DynamicMutableRealmObject.create( - "Sample", - "stringField" to "INNER_SET" - ) - ) - ) - ), RealmAny.create( realmListOf( RealmAny.create( @@ -1472,22 +1421,15 @@ class DynamicMutableRealmObjectTests { ) ).let { val list = it.getNullableValue("nullableRealmAnyField")!!.asList() - // Verify that we get mutable instances out of the set - list[0]!!.asSet().let { embeddedSet -> - val o = embeddedSet.first()!! - .asRealmObject() - assertIs(o) - assertEquals("INNER_SET", o.getValue("stringField")) - embeddedSet.add(RealmAny.Companion.create(dynamicSampleInner)) - } - list[1]!!.asList().let { embeddedList -> + // Verify that we get mutable instances out of the collections + list[0]!!.asList().let { embeddedList -> val o = embeddedList.first()!! .asRealmObject() assertIs(o) assertEquals("INNER_LIST", o.getValue("stringField")) embeddedList.add(RealmAny.Companion.create(dynamicSampleInner)) } - list[2]!!.asDictionary().let { embeddedDictionary -> + list[1]!!.asDictionary().let { embeddedDictionary -> val o = embeddedDictionary["key"]!! .asRealmObject() assertIs(o) @@ -1511,16 +1453,6 @@ class DynamicMutableRealmObjectTests { "Sample", "nullableRealmAnyField" to RealmAny.create( realmDictionaryOf( - "set" to RealmAny.create( - realmSetOf( - RealmAny.create( - DynamicMutableRealmObject.create( - "Sample", - "stringField" to "INNER_SET" - ) - ) - ) - ), "list" to RealmAny.create( realmListOf( RealmAny.create( @@ -1546,14 +1478,7 @@ class DynamicMutableRealmObjectTests { ) ).let { val dict = it.getNullableValue("nullableRealmAnyField")!!.asDictionary() - // Verify that we get mutable instances out of the set - dict["set"]!!.asSet().let { embeddedSet -> - val o = embeddedSet.first()!! - .asRealmObject() - assertIs(o) - assertEquals("INNER_SET", o.getValue("stringField")) - embeddedSet.add(RealmAny.Companion.create(dynamicSampleInner)) - } + // Verify that we get mutable instances out of the collections dict["list"]!!.asList().let { embeddedList -> val o = embeddedList.first()!! .asRealmObject() diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt index 31195ebad0..e798e4a8b0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt @@ -35,7 +35,6 @@ import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmDictionaryOf import io.realm.kotlin.ext.realmListOf -import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.ext.toRealmSet import io.realm.kotlin.internal.asDynamicRealm import io.realm.kotlin.query.RealmQuery @@ -1551,9 +1550,6 @@ class DynamicRealmObjectTests { RealmAny.create( realmListOf(RealmAny.create(Sample().apply { stringField = "INNER_LIST" })) ), - RealmAny.create( - realmSetOf(RealmAny.create(Sample().apply { stringField = "INNER_SET" })) - ), RealmAny.create( realmDictionaryOf("key" to RealmAny.create(Sample().apply { stringField = "INNER_DICT" })) ), @@ -1577,13 +1573,7 @@ class DynamicRealmObjectTests { assertIs(actualSample) assertEquals("INNER_LIST", actualSample.getValue("stringField")) } - actualList[1]!!.let { innerSet -> - val actualSample = - innerSet.asSet()!!.first()!!.asRealmObject() - assertIs(actualSample) - assertEquals("INNER_SET", actualSample.getValue("stringField")) - } - actualList[2]!!.let { innerDictionary -> + actualList[1]!!.let { innerDictionary -> val actualSample = innerDictionary.asDictionary()!!["key"]!!.asRealmObject() assertIs(actualSample) @@ -1601,7 +1591,7 @@ class DynamicRealmObjectTests { ) val unmanagedSample = Sample().apply { - nullableRealmAnyField = RealmAny.create(realmAnyValues.toRealmSet()) + nullableRealmAnySetField = realmAnyValues.toRealmSet() } realm.writeBlocking { copyToRealm(unmanagedSample) } realm.asDynamicRealm() @@ -1654,61 +1644,6 @@ class DynamicRealmObjectTests { // This should be tested for DynamicMutableRealm instead. } - @Test - fun get_realmAnyNestedSet() { - val realmAnyValues = realmListOf( - null, - RealmAny.create("Hello"), - RealmAny.create(Sample().apply { stringField = "INNER" }), - ) - - val unmanagedSample = Sample().apply { - nullableRealmAnyField = RealmAny.create(realmAnyValues.toRealmSet()) - } - realm.writeBlocking { copyToRealm(unmanagedSample) } - realm.asDynamicRealm() - .also { dynamicRealm -> - val dynamicSample = dynamicRealm.query("Sample") - .find() - .first() - - val actualReifiedSet: RealmSet = - dynamicSample.getNullableValue( - Sample::nullableRealmAnyField.name - )!!.asSet() - - fun assertions(actual: RealmAny?) { - if (actual?.type == RealmAny.Type.OBJECT) { - var assertionSucceeded = false - for (value in realmAnyValues) { - if (value?.type == RealmAny.Type.OBJECT) { - assertEquals( - value.asRealmObject().stringField, - actual.asRealmObject() - .getValue("stringField") - ) - assertionSucceeded = true - return - } - } - assertTrue(assertionSucceeded) - } else { - assertTrue(realmAnyValues.contains(actual)) - } - } - - for (actual in actualReifiedSet) { - assertions(actual) - } - } - - // In case of testing sets we have to skip dynamic managed objects inside a - // RealmSet since the semantics prevent us from writing a DynamicRealmObject - // in this way, rightfully so. The reason for this is that we use the regular realm's - // accessors which go through the non-dynamic path so objects inside the set are expected - // to be non-dynamic - the 'issueDynamicObject' flag is always false following this path. - // This should be tested for DynamicMutableRealm instead. - } @Test fun get_realmAnyDictionary() { val realmAnyValues = realmDictionaryOf( @@ -1786,9 +1721,6 @@ class DynamicRealmObjectTests { "list" to RealmAny.create( realmListOf(RealmAny.create(Sample().apply { stringField = "INNER_LIST" })) ), - "set" to RealmAny.create( - realmSetOf(RealmAny.create(Sample().apply { stringField = "INNER_SET" })) - ), "dict" to RealmAny.create( realmDictionaryOf("key" to RealmAny.create(Sample().apply { stringField = "INNER_DICT" })) ), @@ -1817,17 +1749,6 @@ class DynamicRealmObjectTests { innerSample.asRealmObject() } } - actualDictionary["set"]!!.let { innerSet -> - val innerSample = innerSet.asSet()!!.first()!! - val actualSample = - innerSample.asRealmObject() - assertIs(actualSample) - assertEquals("INNER_SET", actualSample.getValue("stringField")) - - assertFailsWith { - innerSample.asRealmObject() - } - } actualDictionary["dict"]!!.let { innerDictionary -> val innerSample = innerDictionary.asDictionary()!!["key"]!! val actualSample = diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt index 8ef99181fa..f5b8fa593d 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt @@ -27,7 +27,8 @@ import io.realm.kotlin.notifications.DeletedMap import io.realm.kotlin.notifications.InitialMap import io.realm.kotlin.notifications.MapChange import io.realm.kotlin.notifications.UpdatedMap -import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests +import io.realm.kotlin.test.common.utils.DeletableEntityNotificationTests +import io.realm.kotlin.test.common.utils.FlowableTests import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.types.RealmAny @@ -45,7 +46,7 @@ import kotlin.test.assertIs import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds -class RealmAnyNestedDictionaryNotificationTest : RealmEntityNotificationTests { +class RealmAnyNestedDictionaryNotificationTest : FlowableTests, DeletableEntityNotificationTests { lateinit var tmpDir: String lateinit var configuration: RealmConfiguration diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt index 6fd2814cfa..e2858f7fbd 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -26,7 +26,8 @@ import io.realm.kotlin.notifications.DeletedList import io.realm.kotlin.notifications.InitialList import io.realm.kotlin.notifications.ListChange import io.realm.kotlin.notifications.UpdatedList -import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests +import io.realm.kotlin.test.common.utils.DeletableEntityNotificationTests +import io.realm.kotlin.test.common.utils.FlowableTests import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.types.RealmAny @@ -44,7 +45,7 @@ import kotlin.test.assertIs import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds -class RealmAnyNestedListNotificationTest : RealmEntityNotificationTests { +class RealmAnyNestedListNotificationTest : FlowableTests, DeletableEntityNotificationTests { lateinit var tmpDir: String lateinit var configuration: RealmConfiguration diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt deleted file mode 100644 index e000ad0a06..0000000000 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedSetNotificationTest.kt +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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. - */ - -package io.realm.kotlin.test.common.notifications - -import io.realm.kotlin.Realm -import io.realm.kotlin.RealmConfiguration -import io.realm.kotlin.entities.JsonStyleRealmObject -import io.realm.kotlin.ext.realmAnyListOf -import io.realm.kotlin.ext.realmAnyOf -import io.realm.kotlin.ext.realmAnySetOf -import io.realm.kotlin.internal.platform.runBlocking -import io.realm.kotlin.notifications.DeletedSet -import io.realm.kotlin.notifications.InitialSet -import io.realm.kotlin.notifications.SetChange -import io.realm.kotlin.notifications.UpdatedSet -import io.realm.kotlin.test.common.utils.RealmEntityNotificationTests -import io.realm.kotlin.test.platform.PlatformUtils -import io.realm.kotlin.test.util.receiveOrFail -import io.realm.kotlin.types.RealmAny -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withTimeout -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -class RealmAnyNestedSetNotificationTest : RealmEntityNotificationTests { - - lateinit var tmpDir: String - lateinit var configuration: RealmConfiguration - lateinit var realm: Realm - - @BeforeTest - fun setup() { - tmpDir = PlatformUtils.createTempDir() - configuration = RealmConfiguration.Builder( - schema = setOf(JsonStyleRealmObject::class) - ).directory(tmpDir) - .build() - realm = Realm.open(configuration) - } - - @AfterTest - fun tearDown() { - if (this::realm.isInitialized && !realm.isClosed()) { - realm.close() - } - PlatformUtils.deleteTempDir(tmpDir) - } - - @Test - @Ignore // Initial element events are verified as part of the asFlow tests - override fun initialElement() {} - - @Test - override fun asFlow() = runBlocking { - val channel = Channel>() - - val o: JsonStyleRealmObject = realm.write { - copyToRealm( - JsonStyleRealmObject().apply { - id = "SET" - value = realmAnySetOf(1, 2, 3) - } - ) - } - - val set = o.value!!.asSet() - assertEquals(3, set.size) - val listener = async { - set.asFlow().collect { - channel.send(it) - } - } - - channel.receiveOrFail(1.seconds).run { - assertIs>(this) - val expectedSet = mutableSetOf(1, 2, 3) - this.set.forEach { expectedSet.remove(it!!.asInt()) } - assertTrue { expectedSet.isEmpty() } - } - - realm.write { - val liveSet = findLatest(o)!!.value!!.asSet() - liveSet.add(RealmAny.create(4)) - } - - channel.receiveOrFail(1.seconds).run { - assertIs>(this) - val expectedSet = mutableSetOf(1, 2, 3, 4) - this.set.forEach { expectedSet.remove(it!!.asInt()) } - assertTrue { expectedSet.isEmpty() } - } - - realm.write { - findLatest(o)!!.value = realmAnyOf(5) - } - - // Fails due to missing deletion events - channel.receiveOrFail(1.seconds).run { - assertIs>(this) - } - listener.cancel() - channel.close() - } - - @Test - override fun cancelAsFlow() { - kotlinx.coroutines.runBlocking { - val container = realm.write { - copyToRealm(JsonStyleRealmObject().apply { value = realmAnySetOf() }) - } - val channel1 = Channel>(1) - val channel2 = Channel>(1) - val observedSet = container.value!!.asSet() - val observer1 = async { - observedSet.asFlow() - .collect { change -> - channel1.trySend(change) - } - } - val observer2 = async { - observedSet.asFlow() - .collect { change -> - channel2.trySend(change) - } - } - - // Ignore first emission with empty sets - assertTrue { channel1.receiveOrFail(1.seconds).set.isEmpty() } - assertTrue { channel2.receiveOrFail(1.seconds).set.isEmpty() } - - // Trigger an update - realm.write { - val queriedContainer = findLatest(container) - queriedContainer!!.value!!.asSet().add(RealmAny.Companion.create(1)) - } - assertEquals(1, channel1.receiveOrFail().set.size) - assertEquals(1, channel2.receiveOrFail().set.size) - - // Cancel observer 1 - observer1.cancel() - - // Trigger another update - realm.write { - val queriedContainer = findLatest(container) - queriedContainer!!.value!!.asSet().add(RealmAny.create(2)) - } - - // Check channel 1 didn't receive the update - assertTrue(channel1.isEmpty) - // But channel 2 did - assertEquals(2, channel2.receiveOrFail().set.size) - - observer2.cancel() - channel1.close() - channel2.close() - } - } - - @Test - override fun deleteEntity() = runBlocking { - val container = - realm.write { copyToRealm(JsonStyleRealmObject().apply { value = realmAnySetOf() }) } - val mutex = Mutex(true) - val flow = async { - container.value!!.asSet().asFlow().first { - mutex.unlock() - it is DeletedSet<*> - } - } - - // Await that flow is actually running - mutex.lock() - // Update mixed value to overwrite and delete set - realm.write { - findLatest(container)!!.value = realmAnyListOf() - } - - // Await that notifier has signalled the deletion so we are certain that the entity - // has been deleted - withTimeout(10.seconds) { - flow.await() - } - } - - @Test - override fun asFlowOnDeletedEntity() = runBlocking { - val container = - realm.write { copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf(realmAnySetOf()) }) } - val mutex = Mutex(true) - val flow = async { - container.value!!.asList()[0]!!.asSet().asFlow().first { - mutex.unlock() - it is DeletedSet<*> - } - } - - // Await that flow is actually running - mutex.lock() - // And delete containing entity - realm.write { delete(findLatest(container)!!) } - - // Await that notifier has signalled the deletion so we are certain that the entity - // has been deleted - withTimeout(10.seconds) { - flow.await() - } - - // Verify that a flow on the deleted entity will signal a deletion and complete gracefully - withTimeout(10.seconds) { - container.value!!.asList()[0]!!.asSet().asFlow().collect { - assertIs>(it) - } - } - } - - @Test - @Ignore - override fun closingRealmDoesNotCancelFlows() { - TODO("Not yet implemented") - } - - @Ignore - override fun closeRealmInsideFlowThrows() { - TODO("Not yet implemented") - } -} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt new file mode 100644 index 0000000000..08839b3f25 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt @@ -0,0 +1,33 @@ +/* + * 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. + */ + +package io.realm.kotlin.test.common.utils + +/** + * All classes that tests classes that exposes notifications on entities that can be removed from + * the realm (i.e. RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm and + * RealmResults) should implement this interface to be sure that we test common behaviour across + * those classes. + */ +interface DeletableEntityNotificationTests { + // Verify that we get deletion events and close the Flow when the object being observed (or + // containing object) is deleted. + fun deleteEntity() + + // Verify that we emit deletion events and close the flow when registering for notifications on + // an outdated entity. + fun asFlowOnDeletedEntity() +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt index 0f7c8bca96..46aea13298 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt @@ -17,17 +17,10 @@ package io.realm.kotlin.test.common.utils /** - * All classes that tests classes that exposes notifications on entities that can be removed from - * the realm (i.e. RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm and - * RealmResults) should implement this interface to be sure that we test common behaviour across + * Test for top level entities that can be deleted and supports key-path-filtering (i.e. + * RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm, RealmResults and + * RealmAny) should implement this interface to be sure that we test common behaviour across * those classes. */ -interface RealmEntityNotificationTests : FlowableTests, KeyPathFlowableTests { - // Verify that we get deletion events and close the Flow when the object being observed (or - // containing object) is deleted. - fun deleteEntity() - - // Verify that we emit deletion events and close the flow when registering for notifications on - // an outdated entity. - fun asFlowOnDeletedEntity() -} +interface RealmEntityNotificationTests : + FlowableTests, DeletableEntityNotificationTests, KeyPathFlowableTests diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 68f79594cf..e4de142f56 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -31,7 +31,6 @@ import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmAnyDictionaryOf import io.realm.kotlin.ext.realmAnyListOf -import io.realm.kotlin.ext.realmAnySetOf import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking @@ -69,6 +68,8 @@ import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.test.util.trySendOrFail import io.realm.kotlin.test.util.use +import io.realm.kotlin.types.BaseRealmObject +import kotlinx.atomicfu.atomic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -83,6 +84,7 @@ import okio.Path.Companion.toPath import org.mongodb.kbson.ObjectId import kotlin.random.Random import kotlin.random.nextULong +import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -1335,6 +1337,8 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } + // This test cannot run multiple times on the same server instance as the primary + // key of the objects from asset-pbs.realm will not be unique on secondary runs. @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" @@ -1364,26 +1368,24 @@ class SyncedRealmTests { } } + val initialDataVerified = atomic(false) val config2 = createPartitionSyncConfig( - user = user, partitionValue = partitionValue, name = "db1", + user = user, partitionValue = partitionValue, name = "db2", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { - fail("Realm 1: $error") + fail("Realm 2: $error") } } ) { waitForInitialRemoteData(30.seconds) initialData { - // Verify that initial data is running before data is synced - assertEquals(0, query().find().size) - } - } - Realm.open(config2).use { - runBlocking { - it.syncSession.downloadAllServerChanges(30.seconds) - assertEquals(4, it.query().find().size) + // Verify that initial data is running after data is synced + assertEquals(4, query().find().size) + initialDataVerified.value = true } } + Realm.open(config2).use { } + assertTrue { initialDataVerified.value } } @Test @@ -1535,9 +1537,6 @@ class SyncedRealmTests { Realm.open(local).use { it.write { val obj = copyToRealm(JsonStyleRealmObject()) - assertFailsWithMessage("Cannot sync nested set") { - obj.value = realmAnySetOf() - } assertFailsWithMessage("Cannot sync nested list") { obj.value = realmAnyListOf() } @@ -1898,9 +1897,10 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, + schema: Set> = PARTITION_BASED_SCHEMA, block: SyncConfiguration.Builder.() -> Unit = {} ): SyncConfiguration = SyncConfiguration.Builder( - schema = PARTITION_BASED_SCHEMA, + schema = schema, user = user, partitionValue = partitionValue ).name(name).also { builder -> @@ -1917,11 +1917,12 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, + schema: Set> = FLEXIBLE_SYNC_SCHEMA, initialSubscriptions: InitialSubscriptionsCallback? = null, block: SyncConfiguration.Builder.() -> Unit = {}, ): SyncConfiguration = SyncConfiguration.Builder( user = user, - schema = FLEXIBLE_SYNC_SCHEMA + schema = schema ).name(name).also { builder -> if (encryptionKey != null) builder.encryptionKey(encryptionKey) if (errorHandler != null) builder.errorHandler(errorHandler) From 40ca9936b1c577798d5d5852bade97bc6b360e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 2 Feb 2024 14:05:54 +0100 Subject: [PATCH 04/12] Bump to latest next-major core --- CHANGELOG.md | 2 +- packages/external/core | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1839d280c..701a4f5333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi ### Internal * Update to Ktor 2.3.4. * Updated to CMake 3.27.7 -* Updated to Realm Core 14.0.0-beta, commit 4926641c6cf2948fa494624ee802bb8cbc21387f. +* Updated to Realm Core 14.0.0-beta, commit 5c367320af95d0bdbf823aa21e056bd342a10091. * Adding Sync tests via Github Action. diff --git a/packages/external/core b/packages/external/core index 5decc76ee0..5c367320af 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 5decc76ee089524e8e387795736e486bfa7797cb +Subproject commit 5c367320af95d0bdbf823aa21e056bd342a10091 From e30c7931aa0df4080afd197c31f96734ae2e64f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 2 Feb 2024 14:21:01 +0100 Subject: [PATCH 05/12] Remove deprecated RLM_TYPE_SET enum entry --- .../kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt index b992b9816c..a6b1a413ff 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -116,7 +116,6 @@ actual enum class ValueType( RLM_TYPE_OBJECT_ID(realm_value_type_e.RLM_TYPE_OBJECT_ID), RLM_TYPE_LINK(realm_value_type_e.RLM_TYPE_LINK), RLM_TYPE_UUID(realm_value_type_e.RLM_TYPE_UUID), - RLM_TYPE_SET(realm_value_type_e.RLM_TYPE_SET), RLM_TYPE_LIST(realm_value_type_e.RLM_TYPE_LIST), RLM_TYPE_DICTIONARY(realm_value_type_e.RLM_TYPE_DICTIONARY), ; From 6a03fb69844f3b7187ff3884ffece7f68f1ba2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 2 Feb 2024 14:47:14 +0100 Subject: [PATCH 06/12] Updates to changed C-API --- .../kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index d2dec6bb34..3ee83ed8b7 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -980,12 +980,10 @@ actual object RealmInterop { } actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { - checkedBooleanResult(realm_wrapper.realm_set_list(obj.cptr(), key.key)) - return realm_get_list(obj, key) + return CPointerWrapper(realm_wrapper.realm_set_list(obj.cptr(), key.key)) } actual fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer { - checkedBooleanResult(realm_wrapper.realm_set_dictionary(obj.cptr(), key.key)) - return realm_get_dictionary(obj, key) + return CPointerWrapper(realm_wrapper.realm_set_dictionary(obj.cptr(), key.key)) } actual fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) { From 8dd0eba3e92eb716bf07b89c15c8f16bae6e482a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Mon, 5 Feb 2024 10:26:46 +0100 Subject: [PATCH 07/12] Fix expected syntax error exception message --- .../commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt index 1292ea5079..0b6eb5dc3c 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt @@ -114,7 +114,7 @@ class QueryTests { @Test fun query_wrongArgumentTypeThrows() { - assertFailsWithMessage(" Unsupported comparison between type 'string' and type 'bool'") { + assertFailsWithMessage("Cannot compare argument \$0 with value 'true' to a string") { realm.query("stringField = $0", true) } } From 68308aa5a1316098aee234e7ddb86fbea4faebb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Mon, 18 Mar 2024 13:58:29 +0100 Subject: [PATCH 08/12] Fix list indexof, remove and contains (#1666) --- CHANGELOG.md | 1 + .../kotlin/internal/interop/RealmInterop.kt | 3 + .../kotlin/internal/interop/RealmInterop.kt | 12 +++ .../kotlin/internal/interop/RealmInterop.kt | 13 +++ .../kotlin/internal/RealmListInternal.kt | 58 +++++++++++ .../common/RealmAnyNestedCollectionTests.kt | 10 ++ .../kotlin/test/common/RealmListTests.kt | 39 -------- .../RealmAnyNestedListNotificationTest.kt | 96 +++++++++++++++++++ .../RealmListNotificationsTests.kt | 77 +++++++++++++++ 9 files changed, 270 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02783bf8d..9793830e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). * `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580)) * The Unpacking of JVM native library will use the current library version instead of a calculated hash for the path. (Issue [#1617](https://github.com/realm/realm-kotlin/issues/1617)). +* Optimized `RealmList.indexOf()` and `RealmList.contains()` using Core implementation of operations instead of iterating elements and comparing them in Kotlin. (Issue [#1625](https://github.com/realm/realm-kotlin/pull/1666) [RKOTLIN-995](https://jira.mongodb.org/browse/RKOTLIN-995)). ### Fixed * Cache notification callback JNI references at startup to ensure that symbols can be resolved in core callbacks. (Issue [#1577](https://github.com/realm/realm-kotlin/issues/1577)) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 8436afd07c..2785898976 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -55,6 +55,8 @@ expect val INVALID_PROPERTY_KEY: PropertyKey const val OBJECT_ID_BYTES_SIZE = 12 const val UUID_BYTES_SIZE = 16 +const val INDEX_NOT_FOUND = -1L + // Pure marker interfaces corresponding to the C-API realm_x_t struct types interface CapiT interface RealmConfigT : CapiT @@ -317,6 +319,7 @@ expect object RealmInterop { fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue + fun realm_list_find(list: RealmListPointer, value: RealmValue): Long fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index c02008f496..1516c3f79f 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -568,6 +568,18 @@ actual object RealmInterop { realmc.realm_list_get(list.cptr(), index, struct) return RealmValue(struct) } + + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + val index = LongArray(1) + val found = BooleanArray(1) + realmc.realm_list_find(list.cptr(), value.value, index, found) + return if (found[0]) { + index[0] + } else { + INDEX_NOT_FOUND + } + } + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index)) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 3ee83ed8b7..f6dd31ba65 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1038,6 +1038,19 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + memScoped { + val index = alloc() + val found = alloc() + checkedBooleanResult(realm_wrapper.realm_list_find(list.cptr(), value.value.readValue(), index.ptr, found.ptr)) + return if (found.value) { + index.value.toLong() + } else { + INDEX_NOT_FOUND + } + } + } + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong())) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index 9446ec4817..badf97fea5 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -20,6 +20,7 @@ import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.Versioned import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.isManaged import io.realm.kotlin.internal.RealmValueArgumentConverter.convertToQueryArgs import io.realm.kotlin.internal.interop.Callback import io.realm.kotlin.internal.interop.ClassKey @@ -50,6 +51,8 @@ import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.flow.Flow import kotlin.reflect.KClass +internal const val INDEX_NOT_FOUND = io.realm.kotlin.internal.interop.INDEX_NOT_FOUND + /** * Implementation for unmanaged lists, backed by a [MutableList]. */ @@ -90,10 +93,22 @@ internal class ManagedRealmList( return operator.get(index) } + override fun contains(element: E): Boolean { + return operator.contains(element) + } + + override fun indexOf(element: E): Int { + return operator.indexOf(element) + } + override fun add(index: Int, element: E) { operator.insert(index, element) } + override fun remove(element: E): Boolean { + return operator.remove(element) + } + // We need explicit overrides of these to ensure that we capture duplicate references to the // same unmanaged object in our internal import caching mechanism override fun addAll(elements: Collection): Boolean = operator.insertAll(size, elements) @@ -225,6 +240,10 @@ internal interface ListOperator : CollectionOperator { fun get(index: Int): E + fun contains(element: E): Boolean = indexOf(element) != -1 + + fun indexOf(element: E): Int + // TODO OPTIMIZE We technically don't need update policy and cache for primitive lists but right now RealmObjectHelper.assign doesn't know how to differentiate the calls to the operator fun insert( index: Int, @@ -233,6 +252,14 @@ internal interface ListOperator : CollectionOperator { cache: UnmanagedToManagedObjectCache = mutableMapOf() ) + fun remove(element: E): Boolean = when (val index = indexOf(element)) { + -1 -> false + else -> { + RealmInterop.realm_list_erase(nativePointer, index.toLong()) + true + } + } + fun insertAll( index: Int, elements: Collection, @@ -277,6 +304,14 @@ internal class PrimitiveListOperator( } } + override fun indexOf(element: E): Int { + inputScope { + with(realmValueConverter) { + return RealmInterop.realm_list_find(nativePointer, publicToRealmValue(element)).toInt() + } + } + } + override fun insert( index: Int, element: E, @@ -354,6 +389,17 @@ internal class RealmAnyListOperator( } } + override fun indexOf(element: RealmAny?): Int { + // Unmanaged objects are never found in a managed collections + if (element?.type == RealmAny.Type.OBJECT) { + if (!element.asRealmObject().isManaged()) return -1 + } + return inputScope { + val transport = realmAnyToRealmValueWithoutImport(element) + RealmInterop.realm_list_find(nativePointer, transport).toInt() + } + } + override fun insert( index: Int, element: RealmAny?, @@ -461,6 +507,18 @@ internal abstract class BaseRealmObjectListOperator ( realmValueToRealmObject(transport, clazz, mediator, realmReference) as E } } + + override fun indexOf(element: E): Int { + // Unmanaged objects are never found in a managed collections + element?.also { + if (!(it as RealmObjectInternal).isManaged()) return -1 + } + return inputScope { + val objRef = realmObjectToRealmReferenceOrError(element as BaseRealmObject?) + val transport = realmObjectTransport(objRef as RealmObjectInterop) + RealmInterop.realm_list_find(nativePointer, transport).toInt() + } + } } internal class RealmObjectListOperator( diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt index d377684f94..2e350642ba 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -573,6 +573,9 @@ class RealmAnyNestedCollectionTests { realm.query("value[*] == 4").find().single().run { assertEquals("LIST", id) } + realm.query("value[*] == {4, 5, 6}").find().single().run { + assertEquals("LIST", id) + } // Matching dictionaries realm.query("value.key1 == 7").find().single().run { @@ -606,5 +609,12 @@ class RealmAnyNestedCollectionTests { realm.query("value[*].key3[0] == 9").find().single().run { assertEquals("EMBEDDED", id) } + realm.query("value[0][*] == {4, 5, 6}").find().single().run { + assertEquals("EMBEDDED", id) + } + // FIXME Core issue https://github.com/realm/realm-core/issues/7393 + // realm.query("value[*][*] == {4, 5, 6}").find().single().run { + // assertEquals("EMBEDDED", id) + // } } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index 9ec49c4185..3103be611c 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -722,13 +722,6 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { ), classifier ) - ByteArray::class -> ByteArrayListTester( - realm = realm, - typeSafetyManager = getTypeSafety( - classifier, - elementType.nullable - ) as ListTypeSafetyManager - ) RealmAny::class -> RealmAnyListTester( realm = realm, typeSafetyManager = ListTypeSafetyManager( @@ -1368,38 +1361,6 @@ internal class RealmObjectListTester( assertEquals(expected.stringField, actual.stringField) } -/** - * Check equality for ByteArrays at a structural level with `assertContentEquals`. - */ -internal class ByteArrayListTester( - realm: Realm, - typeSafetyManager: ListTypeSafetyManager -) : ManagedListTester(realm, typeSafetyManager, ByteArray::class) { - override fun assertElementsAreEqual(expected: ByteArray?, actual: ByteArray?) = - assertContentEquals(expected, actual) - - // Removing elements using equals/hashcode will fail for byte arrays since they are - // are only equal if identical - override fun remove() { - val dataSet = typeSafetyManager.dataSetToLoad - val assertions = { list: RealmList -> - assertFalse(list.isEmpty()) - } - - errorCatcher { - realm.writeBlocking { - val list = typeSafetyManager.createContainerAndGetCollection(this) - assertFalse(list.remove(dataSet[0])) - assertTrue(list.add(dataSet[0])) - assertFalse(list.remove(list.last())) - assertions(list) - } - } - - assertListAndCleanup { list -> assertions(list) } - } -} - // ----------------------------------- // Data used to initialize structures // ----------------------------------- diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt index e2858f7fbd..0176c7f0d0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -19,6 +19,8 @@ package io.realm.kotlin.test.common.notifications import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.realmAnyDictionaryOf import io.realm.kotlin.ext.realmAnyListOf import io.realm.kotlin.ext.realmAnyOf import io.realm.kotlin.internal.platform.runBlocking @@ -30,6 +32,7 @@ import io.realm.kotlin.test.common.utils.DeletableEntityNotificationTests import io.realm.kotlin.test.common.utils.FlowableTests import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail import io.realm.kotlin.types.RealmAny import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -252,4 +255,97 @@ class RealmAnyNestedListNotificationTest : FlowableTests, DeletableEntityNotific override fun closeRealmInsideFlowThrows() { TODO("Not yet implemented") } + + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + RealmAny.create(JsonStyleRealmObject().apply { id = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asRealmObject().value = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals( + "TEST", + it.list[0]!!.asRealmObject().value!!.asString() + ) + } + + listener.cancel() + } + } + + @Test + fun eventsOnDictionaryChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + realmAnyDictionaryOf( + "key1" to "value1" + ) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals(RealmAny.Type.DICTIONARY, it.list[0]!!.type) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asDictionary()["key1"] = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asDictionary()["key1"]!!.asString()) + } + + listener.cancel() + } + } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt index c7f67ab7b4..da7e69c462 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.list.RealmListContainer import io.realm.kotlin.entities.list.listTestSchema +import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.notifications.DeletedList import io.realm.kotlin.notifications.InitialList @@ -35,6 +36,8 @@ import io.realm.kotlin.test.common.utils.assertIsChangeSet import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestChannel import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmList import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -734,6 +737,80 @@ class RealmListNotificationsTests : RealmEntityNotificationTests { } } + @Test + fun eventsOnObjectChangesInList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.objectListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.objectListField.add( + RealmListContainer().apply { stringField = "CHILD" } + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.objectListField[0].stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0].stringField) + } + + listener.cancel() + } + } + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.nullableRealmAnyListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField.add( + RealmAny.create(RealmListContainer().apply { stringField = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField[0]!!.asRealmObject().stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asRealmObject().stringField) + } + + listener.cancel() + } + } + fun RealmList<*>.removeRange(range: IntRange) { range.reversed().forEach { index -> removeAt(index) } } From 31989af75cd512b442c218514318dc4705c4e19a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Thu, 4 Apr 2024 12:36:44 +0200 Subject: [PATCH 09/12] Remove verification of collection in mixed failing on synced realm --- .../test/mongodb/common/SyncedRealmTests.kt | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 627fd0d522..6dc78b00bb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -1518,41 +1518,6 @@ class SyncedRealmTests { } } - @Test - fun cannotSyncCollectionsInMixed() = runBlocking { - TestApp( - "cannotSyncCollectionsInMixed", - logLevel = LogLevel.ALL, - appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, - builder = { - it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) - } - ).use { flexApp -> - val (email, password) = randomEmail() to "password1234" - val user = flexApp.createUserAndLogIn(email, password) - val local = createFlexibleSyncConfig( - user = user, - name = "local", - schema = setOf(JsonStyleRealmObject::class) - ) { - initialSubscriptions { - this.add(it.query()) - } - } - Realm.open(local).use { - it.write { - val obj = copyToRealm(JsonStyleRealmObject()) - assertFailsWithMessage("Cannot sync nested list") { - obj.value = realmAnyListOf() - } - assertFailsWithMessage("Cannot sync nested dictionary") { - obj.value = realmAnyDictionaryOf() - } - } - } - } - } - // @Test // fun initialVersion() { // assertEquals(INITIAL_VERSION, realm.version()) From 54c3711cbd521371aaa99c2cbf6c850d7f4cf804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Thu, 4 Apr 2024 12:52:04 +0200 Subject: [PATCH 10/12] Another round for linting --- .../io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 6dc78b00bb..e7a759c7cb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -20,7 +20,6 @@ import io.realm.kotlin.LogConfiguration import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.VersionId -import io.realm.kotlin.entities.JsonStyleRealmObject import io.realm.kotlin.entities.sync.BinaryObject import io.realm.kotlin.entities.sync.ChildPk import io.realm.kotlin.entities.sync.ParentPk @@ -29,8 +28,6 @@ import io.realm.kotlin.entities.sync.flx.FlexChildObject import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query -import io.realm.kotlin.ext.realmAnyDictionaryOf -import io.realm.kotlin.ext.realmAnyListOf import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking From 75af5bfabc922055fddf65690e6f64d1f4bb7336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 9 Apr 2024 22:04:01 +0200 Subject: [PATCH 11/12] Upgrade to Core 14.5.0 --- CHANGELOG.md | 2 +- .../realm/kotlin/internal/interop/RealmInterop.kt | 3 ++- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 3 ++- .../src/main/jni/realm_api_helpers.h | 2 +- .../io/realm/kotlin/internal/RealmListInternal.kt | 8 ++++---- .../io/realm/kotlin/internal/RealmMapInternal.kt | 6 ++---- .../io/realm/kotlin/internal/RealmObjectHelper.kt | 4 ++-- .../realm/kotlin/internal/SuspendableNotifier.kt | 2 +- .../test/common/RealmAnyNestedCollectionTests.kt | 15 +++++---------- .../RealmAnyNestedListNotificationTest.kt | 2 +- 11 files changed, 22 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ed7ef0cc..9ea733e1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ * Minimum R8: 8.0.34. ### Internal -* Updated to Realm Core 14.4.1 commit 374dd672af357732dccc135fecc905406fec3223. +* Updated to Realm Core 14.5.0 commit f9212cc5db8599278cd3d1d73d95df3188b5c3b9. * Deprecated Jenkins and switching to Github Action ([JIRA]https://jira.mongodb.org/browse/RKOTLIN-825). - Remove CMake required version. diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index c1249ed908..69ba346934 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2768,8 +2768,9 @@ actual object RealmInterop { return CPointerWrapper( realm_wrapper.realm_sync_session_register_progress_notifier( syncSession.cptr(), - staticCFunction { userData, transferred_bytes, total_bytes -> + staticCFunction { userData, transferred_bytes, total_bytes, _ -> safeUserData(userData).run { + // TODO Progress ignored until https://github.com/realm/realm-kotlin/pull/1575 onChange(transferred_bytes.toLong(), total_bytes.toLong()) } }, diff --git a/packages/external/core b/packages/external/core index 374dd672af..f9212cc5db 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 374dd672af357732dccc135fecc905406fec3223 +Subproject commit f9212cc5db8599278cd3d1d73d95df3188b5c3b9 diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 2dfcdfdb39..df18021fc7 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -1244,9 +1244,10 @@ sync_after_client_reset_handler(realm_sync_config_t* config, jobject after_handl } void -realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes) { +realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes, double progress) { auto env = get_env(true); + // TODO Progress ignored until https://github.com/realm/realm-kotlin/pull/1575 static JavaMethod java_callback_method(env, JavaClassGlobalDef::progress_callback(), "onChange", "(JJ)V"); jni_check_exception(env); diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 34f837dbc0..2fae610c7b 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -107,7 +107,7 @@ void sync_after_client_reset_handler(realm_sync_config_t* config, jobject after_handler); void -realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes); +realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes, double progress); void realm_sync_session_connection_state_change_callback(void *userdata, realm_sync_connection_state_e old_state, realm_sync_connection_state_e new_state); diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index badf97fea5..2e2669c955 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -423,6 +423,7 @@ internal class RealmAnyListOperator( }, listAsRealmAnyHandler = { realmValue -> val nativePointer = RealmInterop.realm_list_insert_list(nativePointer, index.toLong()) + RealmInterop.realm_list_clear(nativePointer) val operator = realmAnyListOperator( mediator, realmReference, @@ -433,6 +434,7 @@ internal class RealmAnyListOperator( }, dictionaryAsRealmAnyHandler = { realmValue -> val nativePointer = RealmInterop.realm_list_insert_dictionary(nativePointer, index.toLong()) + RealmInterop.realm_dictionary_clear(nativePointer) val operator = realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) operator.putAll(realmValue.asDictionary(), updatePolicy, cache) @@ -461,9 +463,8 @@ internal class RealmAnyListOperator( RealmInterop.realm_list_set(nativePointer, index.toLong(), realmObjectTransport(objRef)) }, listAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) val nativePointer = RealmInterop.realm_list_set_list(nativePointer, index.toLong()) + RealmInterop.realm_list_clear(nativePointer) val operator = realmAnyListOperator( mediator, realmReference, @@ -473,9 +474,8 @@ internal class RealmAnyListOperator( operator.insertAll(0, realmValue.asList(), updatePolicy, cache) }, dictionaryAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - RealmInterop.realm_list_set(nativePointer, index.toLong(), nullTransport()) val nativePointer = RealmInterop.realm_list_set_dictionary(nativePointer, index.toLong()) + RealmInterop.realm_dictionary_clear(nativePointer) val operator = realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) operator.putAll(realmValue.asDictionary(), updatePolicy, cache) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt index 9ee69b254d..425e31397c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt @@ -521,10 +521,9 @@ internal class RealmAnyMapOperator constructor( } }, listAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) val previous = getInternal(key) val nativePointer = RealmInterop.realm_dictionary_insert_list(nativePointer, keyTransport) + RealmInterop.realm_list_clear(nativePointer) val operator = realmAnyListOperator( mediator, realmReference, @@ -535,10 +534,9 @@ internal class RealmAnyMapOperator constructor( previous to true }, dictionaryAsRealmAnyHandler = { realmValue -> - // Have to clear existing elements for core to know if we are updating with a new collection - realm_dictionary_insert(nativePointer, keyTransport, nullTransport()) val previous = getInternal(key) val nativePointer = RealmInterop.realm_dictionary_insert_dictionary(nativePointer, keyTransport) + RealmInterop.realm_dictionary_clear(nativePointer) val operator = realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) operator.putAll(realmValue.asDictionary(), updatePolicy, cache) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt index d844026595..1b9ce15a8e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt @@ -238,15 +238,15 @@ internal object RealmObjectHelper { setObjectByKey(obj, key, realmValue.asRealmObject(), updatePolicy, cache) }, listAsRealmAnyHandler = { realmValue -> - RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) + RealmInterop.realm_list_clear(nativePointer) val operator = realmAnyListOperator(obj.mediator, obj.owner, nativePointer, false, false) operator.insertAll(0, value.asList(), updatePolicy, cache) }, dictionaryAsRealmAnyHandler = { realmValue -> - RealmInterop.realm_set_value(obj.objectPointer, key, nullTransport(), false) val nativePointer = RealmInterop.realm_set_dictionary(obj.objectPointer, key) + RealmInterop.realm_dictionary_clear(nativePointer) val operator = realmAnyMapOperator(obj.mediator, obj.owner, nativePointer, false, false) operator.putAll(value.asDictionary(), updatePolicy, cache) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt index 20cd3a9d5b..0c173a5cbe 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -124,7 +124,7 @@ internal class SuspendableNotifier( // the fine-grained notification data might be out of sync. // TODO Currently verifying that lifeRef is still valid to indicate // if it was actually deleted. This is only a problem for - // collections as they seemed to be freezable from a delete + // collections as they seemed to be freezable from a deleted // reference (contrary to other objects that returns null from // freeze). An `out_collection_was_deleted` flag was added to the // change object, which would probably be the way to go, but diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt index 2e350642ba..e13edb5c8f 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -276,9 +276,7 @@ class RealmAnyNestedCollectionTests { // Overwriting exact list with new list instance.value!!.asList()[0] = realmAnyListOf(7) - assertFailsWithMessage("List is no longer valid") { - nestedList[0] - } + assertEquals(7, nestedList[0]!!.asInt()) nestedList = instance.value!!.asList()[0]!!.asList() assertEquals(7, nestedList[0]!!.asInt()) @@ -470,9 +468,7 @@ class RealmAnyNestedCollectionTests { // Overwriting exact list with new list instance.value!!.asDictionary()["key"] = realmAnyListOf(7) - assertFailsWithMessage("List is no longer valid") { - nestedList[0] - } + assertEquals(7, nestedList[0]!!.asInt()) // Getting updated reference to embedded list nestedList = instance.value!!.asDictionary()["key"]!!.asList() @@ -499,10 +495,9 @@ class RealmAnyNestedCollectionTests { // Overwriting with new list instance.value = realmAnyListOf(7) - // Accessing original orphaned list return 7 from the new instance, but expected ILLEGAL_STATE_EXCEPTION["List is no longer valid"] - assertFailsWithMessage("List is no longer valid") { - nestedList[0] - } + + // Accessing original orphaned list return 7 from the new instance + assertEquals(7, nestedList[0]!!.asInt()) // Overwriting with null value instance.value = null diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt index 0176c7f0d0..99113c3f08 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -206,7 +206,7 @@ class RealmAnyNestedListNotificationTest : FlowableTests, DeletableEntityNotific // Await that notifier has signalled the deletion so we are certain that the entity // has been deleted - withTimeout(10.seconds) { + withTimeout(3.seconds) { flow.await() } } From 2e893c7a55d0f21b4367029267de4ecf71cb4d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 12 Apr 2024 14:27:20 +0200 Subject: [PATCH 12/12] Upgrade to Core 14.5.1. --- CHANGELOG.md | 2 +- packages/external/core | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a29c1ab3b1..80058fae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ * Minimum R8: 8.0.34. ### Internal -* Updated to Realm Core 14.5.0 commit f9212cc5db8599278cd3d1d73d95df3188b5c3b9. +* Updated to Realm Core 14.5.1 commit 316889b967f845fbc10b4422f96c7eadd47136f2. * Deprecated Jenkins and switching to Github Action ([JIRA]https://jira.mongodb.org/browse/RKOTLIN-825). - Remove CMake required version. * Updated URL to documentation. diff --git a/packages/external/core b/packages/external/core index f9212cc5db..316889b967 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit f9212cc5db8599278cd3d1d73d95df3188b5c3b9 +Subproject commit 316889b967f845fbc10b4422f96c7eadd47136f2