diff --git a/CHANGELOG.md b/CHANGELOG.md index aa46f86430..8cf5d3f6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +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 -* None. +* 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)) ### Fixed * None. @@ -24,7 +26,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/build.gradle.kts b/packages/library-base/build.gradle.kts index 6c9d007386..a1f76c3e7d 100644 --- a/packages/library-base/build.gradle.kts +++ b/packages/library-base/build.gradle.kts @@ -59,6 +59,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}") implementation("org.jetbrains.kotlinx:atomicfu:${Versions.atomicfu}") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.serialization}") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serialization}") } } 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..e1accffffa 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,20 @@ package io.realm.kotlin.ext +import io.realm.kotlin.dynamic.DynamicRealmObject +import io.realm.kotlin.internal.RealmAnyJsonSerializer +import io.realm.kotlin.internal.defaultJson +import io.realm.kotlin.internal.toRealmAny 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 kotlinx.serialization.json.Json +import org.mongodb.kbson.Decimal128 +import org.mongodb.kbson.ObjectId /** * Creates an unmanaged `RealmAny` instance from a [BaseRealmObject] value. @@ -11,3 +23,80 @@ 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()) + + +public fun String.toRealmAny(json: Json = defaultJson): RealmAny? = json.parseToJsonElement(this).toRealmAny() +public fun RealmAny.toJson(json: Json = defaultJson): String = json.encodeToString( + RealmAnyJsonSerializer, this) 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..54d0dd6424 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 @@ -16,11 +16,31 @@ package io.realm.kotlin.internal +import io.realm.kotlin.ext.toRealmDictionary +import io.realm.kotlin.ext.toRealmList 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 kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.floatOrNull +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.modules.SerializersModule import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 import kotlin.reflect.KClass @@ -90,6 +110,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}'.") @@ -159,3 +187,70 @@ internal class RealmAnyImpl constructor( override fun toString(): String = "RealmAny{type=$type, value=${getValue(type)}}" } + +internal fun realmAnyToJson(realmAny: RealmAny?): JsonElement { + val jsonElement = when(realmAny?.type) { + RealmAny.Type.INT -> JsonPrimitive(realmAny.asLong()) + RealmAny.Type.BOOL -> JsonPrimitive(realmAny.asBoolean()) + RealmAny.Type.STRING -> JsonPrimitive(realmAny.asString()) + RealmAny.Type.BINARY -> TODO() + RealmAny.Type.TIMESTAMP -> TODO() + RealmAny.Type.FLOAT -> JsonPrimitive(realmAny.asFloat()) + RealmAny.Type.DOUBLE -> JsonPrimitive(realmAny.asDouble()) + RealmAny.Type.DECIMAL128 -> TODO() + RealmAny.Type.OBJECT_ID -> TODO() + RealmAny.Type.UUID -> TODO() + RealmAny.Type.OBJECT -> TODO() + RealmAny.Type.SET -> JsonArray(realmAny.asSet().map { realmAnyToJson(it) }) + RealmAny.Type.LIST -> JsonArray(realmAny.asList().map { realmAnyToJson(it) }) + RealmAny.Type.DICTIONARY -> { + JsonObject(realmAny.asDictionary().mapValues { (k, v) -> realmAnyToJson(v) }) + } + null -> JsonNull + } + return jsonElement +} + +internal fun JsonElement.toRealmAny() : RealmAny? = when (this) { + is JsonArray -> { RealmAny.create(this.map { it.toRealmAny() }.toRealmList()) } + is JsonObject -> { RealmAny.create(this.map { (key, value) -> key to value.toRealmAny() }.toRealmDictionary()) } + is JsonPrimitive -> { + if (this.isString) { + RealmAny.create(this.content) + } else + this.longOrNull?.let { + RealmAny.create(it) + } ?: + this.doubleOrNull?.let { + RealmAny.create(it) + } ?: + this.floatOrNull?.let { + RealmAny.create(it) + } ?: + this.booleanOrNull?.let { + RealmAny.create(it) + } ?: + TODO("Cannot parse $this into a RealmAny") + } + JsonNull -> null +} + +internal val defaultJson = Json { + serializersModule = SerializersModule { + contextual(RealmAny::class) { _ -> RealmAnyJsonSerializer } + } +} + +public object RealmAnyJsonSerializer: KSerializer { + private val serializer = JsonElement.serializer() + override val descriptor: SerialDescriptor = serializer.descriptor + override fun deserialize(decoder: Decoder): RealmAny? { + return decoder.decodeSerializableValue(serializer).toRealmAny() + } + override fun serialize(encoder: Encoder, value: RealmAny?) { + encoder.encodeSerializableValue( + serializer = serializer, + value = realmAnyToJson(value) + ) + } +} 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..cfa82c6118 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,12 @@ 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.realmAnyListOf +import io.realm.kotlin.ext.realmDictionaryOf +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf +import io.realm.kotlin.ext.toJson +import io.realm.kotlin.ext.toRealmAny import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.notifications.DeletedObject import io.realm.kotlin.notifications.InitialObject @@ -91,6 +97,20 @@ class RealmAnyTests { PlatformUtils.deleteTempDir(tmpDir) } + @Test + fun json() { + RealmAny.create("Realn").toJson().toRealmAny().let { assertEquals(RealmAny.create("Realm"), it) } + + RealmAny.create(1).toJson().toRealmAny().let { assertEquals(RealmAny.create(1), it) } + + RealmAny.create(1.5).toJson().toRealmAny().let { assertEquals(RealmAny.create(1.5), it) } + +// RealmAny.create(1.5f).toJson().toRealmAny().let { assertEquals(RealmAny.create(1.5f), it) } // Will fail as json parser is parsing it as double + + realmAnyListOf(1, realmDictionaryOf("key" to realmListOf(1, 2))) .toJson().toRealmAny().let { + realmAnyListOf(1, realmDictionaryOf("key" to realmListOf(1, 2))) + } + } @Test fun missingClassFromSchema_unmanagedWorks() { val value = NotInSchema() @@ -438,6 +458,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 038d06c91d..b465c366f4 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.runBlocking import io.realm.kotlin.log.LogLevel @@ -1539,6 +1543,7 @@ class SyncedRealmTests { @Test fun flexibleSync_throwsWithLocalInitialRealmFile() { + val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -1553,6 +1558,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())