diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed745c5c0..80058fae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ ## 1.15.0-SNAPSHOT (YYYY-MM-DD) -[!NOTE] -This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it from file format v10. If you want to upgrade from an earlier file format version you will have to use Realm Kotlin v1.13.1 or earlier. Downgrading to a previous file format is not possible. +> [!NOTE] +> This release will bump the Realm file format from version 23 to 24. Opening a file with an older format will automatically upgrade it from file format v10. If you want to upgrade from an earlier file format version you will have to use Realm Kotlin v1.13.1 or earlier. Downgrading to a previous file format is not possible. ### Breaking changes * If you want to query using `@type` operation, you must use 'objectlink' to match links to objects. 'object' is reserved for dictionary types. * Binary data and String data are now strongly typed for comparisons and queries. This change is especially relevant when querying for a string constant on a RealmAny property, as now only strings will be returned. If searching for Binary data is desired, then that type must be specified by the constant. In RQL the new way to specify a binary constant is to use `mixed = bin('xyz')` or `mixed = binary('xyz')`. (Core issue [realm/realm-core#6407](https://github.com/realm/realm-core/issues/6407)). ### Enhancements +* Support for RealmLists and RealmDictionaries in `RealmAny`. (Issue [#1434](https://github.com/realm/realm-kotlin/issues/1434)) * Add support for using aggregate operations on RealmAny properties in queries (Core issue [realm/realm-core#7398](https://github.com/realm/realm-core/pull/7398)) * Property keypath in RQL can be substituted with value given as argument. Use '$P' in query string. (Core issue [realm/realm-core#7033](https://github.com/realm/realm-core/issues/7033)) * You can now use query substitution for the @type argument (Core issue [realm/realm-core#7289](https://github.com/realm/realm-core/issues/7289)) @@ -16,7 +17,8 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * Index on list of strings property now supported (Core issue [realm/realm-core#7142](https://github.com/realm/realm-core/pull/7142)) * Improved performance of RQL (parsed) queries on a non-linked string property using: >, >=, <, <=, operators and fixed behaviour that a null string should be evaulated as less than everything, previously nulls were not matched. (Core issue [realm/realm-core#3939](https://github.com/realm/realm-core/issues/3939). * Updated bundled OpenSSL version to 3.2.0 (Core issue [realm/realm-core#7303](https://github.com/realm/realm-core/pull/7303)) -* The default base url in `AppConfiguration` has been updated to point to `services.cloud.mongodb.com`. See https://www.mongodb.com/docs/atlas/app-services/domain-migration/ for more information. (Issue [#1685](https://github.com/realm/realm-kotlin/issues/1685)) +* Optimized `RealmList.indexOf()` and `RealmList.contains()` using Core implementation of operations instead of iterating elements and comparing them in Kotlin. (Issue [#1625](https://github.com/realm/realm-kotlin/pull/1666) [RKOTLIN-995](https://jira.mongodb.org/browse/RKOTLIN-995)). +* [Sync] The default base url in `AppConfiguration` has been updated to point to `services.cloud.mongodb.com`. See https://www.mongodb.com/docs/atlas/app-services/domain-migration/ for more information. (Issue [#1685](https://github.com/realm/realm-kotlin/issues/1685)) ### Fixed * Sorting order of strings has changed to use standard unicode codepoint order instead of grouping similar english letters together. A noticeable change will be from "aAbBzZ" to "ABZabz". (Core issue [realm/realm-core#2573](https://github.com/realm/realm-core/issues/2573)) @@ -49,7 +51,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi * Minimum R8: 8.0.34. ### Internal -* Updated to Realm Core 14.4.1 commit 374dd672af357732dccc135fecc905406fec3223. +* Updated to Realm Core 14.5.1 commit 316889b967f845fbc10b4422f96c7eadd47136f2. * Deprecated Jenkins and switching to Github Action ([JIRA]https://jira.mongodb.org/browse/RKOTLIN-825). - Remove CMake required version. * Updated URL to documentation. diff --git a/packages/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 7fe946d6ac..099452f555 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 @@ -181,7 +181,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..36f25fa852 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -95,4 +95,6 @@ expect enum class ValueType { RLM_TYPE_OBJECT_ID, RLM_TYPE_LINK, RLM_TYPE_UUID, + RLM_TYPE_LIST, + 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 1a42b6faac..7dce1dcef0 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -55,6 +55,8 @@ expect val INVALID_PROPERTY_KEY: PropertyKey const val OBJECT_ID_BYTES_SIZE = 12 const val UUID_BYTES_SIZE = 16 +const val INDEX_NOT_FOUND = -1L + // Pure marker interfaces corresponding to the C-API realm_x_t struct types interface CapiT interface RealmConfigT : CapiT @@ -304,6 +306,8 @@ expect object RealmInterop { isDefault: Boolean ) fun realm_set_embedded(obj: RealmObjectPointer, key: PropertyKey): RealmObjectPointer + 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, @@ -315,10 +319,17 @@ expect object RealmInterop { fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue + fun realm_list_find(list: RealmListPointer, value: RealmValue): Long + fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer + fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) 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_list(list: RealmListPointer, index: Long): RealmListPointer + fun realm_list_insert_dictionary(list: RealmListPointer, index: Long): RealmMapPointer + 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 @@ -350,10 +361,19 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue + 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, @@ -375,6 +395,8 @@ expect object RealmInterop { dictionary: RealmMapPointer, mapKey: RealmValue ): RealmValue + 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, @@ -439,6 +461,8 @@ expect object RealmInterop { // FIXME OPTIMIZE Get many fun MemAllocator.realm_results_get(results: RealmResultsPointer, index: Long): RealmValue + 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 cb35340a91..a543934cb0 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 @@ -178,7 +178,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 7797adcf14..613ac82036 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 @@ -503,6 +503,15 @@ actual object RealmInterop { return LongPointerWrapper(realmc.realm_set_embedded(obj.cptr(), key.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) } @@ -560,6 +569,23 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + val index = LongArray(1) + val found = BooleanArray(1) + realmc.realm_list_find(list.cptr(), value.value, index, found) + return if (found[0]) { + index[0] + } else { + INDEX_NOT_FOUND + } + } + + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = + LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index)) + + 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) } @@ -567,6 +593,18 @@ 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_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_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, @@ -724,6 +762,19 @@ actual object RealmInterop { return RealmValue(struct) } + 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, pos: Int @@ -792,6 +843,14 @@ actual object RealmInterop { ) } + 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) val keysPointer = longArrayOf(0) @@ -1053,7 +1112,7 @@ actual object RealmInterop { deletions, insertions, modifications, - collectionWasDeleted + collectionWasDeleted, ) val deletionStructs = realmc.new_valueArray(deletions[0].toInt()) @@ -1861,6 +1920,12 @@ actual object RealmInterop { return RealmValue(value) } + 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..935c87d04c 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,9 @@ 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_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 0f676e6aa3..21993aa4e0 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 @@ -182,7 +182,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..a6b1a413ff 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmEnums.kt @@ -115,7 +115,10 @@ 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_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 af6c2c030e..69ba346934 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -979,6 +979,13 @@ actual object RealmInterop { return CPointerWrapper(realm_wrapper.realm_set_embedded(obj.cptr(), key.key)) } + actual fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer { + return CPointerWrapper(realm_wrapper.realm_set_list(obj.cptr(), key.key)) + } + actual fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer { + return CPointerWrapper(realm_wrapper.realm_set_dictionary(obj.cptr(), key.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)) } @@ -1031,6 +1038,25 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + memScoped { + val index = alloc() + val found = alloc() + checkedBooleanResult(realm_wrapper.realm_list_find(list.cptr(), value.value.readValue(), index.ptr, found.ptr)) + return if (found.value) { + index.value.toLong() + } else { + INDEX_NOT_FOUND + } + } + } + + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = + CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong())) + + 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( @@ -1040,6 +1066,18 @@ actual object RealmInterop { ) ) } + 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_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())) @@ -1255,6 +1293,19 @@ actual object RealmInterop { } } + 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 @@ -1376,6 +1427,14 @@ actual object RealmInterop { return RealmValue(outputStruct) } + 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() @@ -1631,6 +1690,12 @@ actual object RealmInterop { return RealmValue(value) } + 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( @@ -1963,7 +2028,7 @@ actual object RealmInterop { deletions, insertions, modifications, - collectionWasDeleted.ptr + collectionWasDeleted.ptr, ) val deletionStructs = allocArray(deletions[0].toInt()) val insertionStructs = allocArray(insertions[0].toInt()) @@ -1977,7 +2042,7 @@ actual object RealmInterop { insertions, modificationStructs, modifications, - collectionWasCleared.ptr + collectionWasCleared.ptr, ) val deletedKeys = (0 until deletions[0].toInt()).map { @@ -2703,8 +2768,9 @@ actual object RealmInterop { return CPointerWrapper( realm_wrapper.realm_sync_session_register_progress_notifier( syncSession.cptr(), - staticCFunction { userData, transferred_bytes, total_bytes -> + staticCFunction { userData, transferred_bytes, total_bytes, _ -> safeUserData(userData).run { + // TODO Progress ignored until https://github.com/realm/realm-kotlin/pull/1575 onChange(transferred_bytes.toLong(), total_bytes.toLong()) } }, diff --git a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt index b2733215a0..f7e0af80b6 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 @@ -284,6 +284,9 @@ class CinteropTest { } .toIntArray() + val unmappedErrors = coreErrorNativeValues + .filter { ErrorCode.of(it) == null } + val errorCodeValues = coreErrorNativeValues .map { ErrorCode.of(it) @@ -292,7 +295,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 374dd672af..316889b967 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 374dd672af357732dccc135fecc905406fec3223 +Subproject commit 316889b967f845fbc10b4422f96c7eadd47136f2 diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 2dfcdfdb39..df18021fc7 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -1244,9 +1244,10 @@ sync_after_client_reset_handler(realm_sync_config_t* config, jobject after_handl } void -realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes) { +realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes, double progress) { auto env = get_env(true); + // TODO Progress ignored until https://github.com/realm/realm-kotlin/pull/1575 static JavaMethod java_callback_method(env, JavaClassGlobalDef::progress_callback(), "onChange", "(JJ)V"); jni_check_exception(env); diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 34f837dbc0..2fae610c7b 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -107,7 +107,7 @@ void sync_after_client_reset_handler(realm_sync_config_t* config, jobject after_handler); void -realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes); +realm_sync_session_progress_notifier_callback(void *userdata, uint64_t transferred_bytes, uint64_t total_bytes, double progress); void realm_sync_session_connection_state_change_callback(void *userdata, realm_sync_connection_state_e old_state, realm_sync_connection_state_e new_state); diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt index 67ef4a7827..58b52eaaaf 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmAnyExt.kt @@ -1,8 +1,13 @@ package io.realm.kotlin.ext +import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmUUID +import org.mongodb.kbson.Decimal128 +import org.mongodb.kbson.ObjectId /** * Creates an unmanaged `RealmAny` instance from a [BaseRealmObject] value. @@ -11,3 +16,65 @@ 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 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 [RealmList] of all arguments wrapped as [RealmAny]s. + * @param values elements of the set. + * + * See [RealmAny.create] for [RealmList] constraints and examples of usage. + */ +public fun realmAnyListOf(vararg values: Any?): RealmAny = + RealmAny.create(values.map { realmAnyOf(it) }.toRealmList()) + +/** + * Create a [RealmAny] containing a [RealmDictionary] with all argument values wrapped as + * [RealmAnys]s. + * @param values entries of the dictionary. + * + * See [RealmAny.create] for [RealmDictionaries] constraints and examples of usage. + */ +public fun realmAnyDictionaryOf(vararg values: Pair): RealmAny = + RealmAny.create(values.map { (key, value) -> key to realmAnyOf(value) }.toRealmDictionary()) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt index 286847e1af..01a55091f4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/CollectionOperator.kt @@ -22,7 +22,6 @@ import io.realm.kotlin.internal.interop.NativePointer internal interface CollectionOperator { val mediator: Mediator val realmReference: RealmReference - val valueConverter: RealmValueConverter val nativePointer: NativePointer } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt index 7100685660..e13858fc0c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt @@ -22,6 +22,8 @@ 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 @@ -30,7 +32,6 @@ import io.realm.kotlin.internal.interop.RealmQuerySingleArgument 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 +106,102 @@ 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, + 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_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") }, + 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.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 +381,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 +422,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 +433,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 +443,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 +461,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 +483,22 @@ 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.LIST, + RealmAny.Type.DICTIONARY -> + throw IllegalArgumentException("Cannot pass unmanaged collections as input argument") else -> realmAnyPrimitiveToRealmValue(value) } } @@ -576,6 +535,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 +599,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 f8eb5e1c51..687c97f6f5 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 @@ -136,4 +136,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 356eb6397e..a1c1b820b0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt @@ -18,7 +18,9 @@ package io.realm.kotlin.internal import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmUUID import org.mongodb.kbson.BsonObjectId @@ -90,6 +92,12 @@ internal class RealmAnyImpl constructor( return clazz.cast(getValue) } + override fun asList(): RealmList = + getValue(RealmAny.Type.LIST) as RealmList + + override fun asDictionary(): RealmDictionary = + getValue(RealmAny.Type.DICTIONARY) as RealmDictionary + private fun getValue(type: RealmAny.Type): Any { if (this.type != type) { throw IllegalStateException("RealmAny type mismatch, wanted a '${type.name}' but the instance is a '${this.type.name}'.") diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index b0ee54ac8d..2e2669c955 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -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 @@ -29,6 +32,7 @@ import io.realm.kotlin.internal.interop.RealmKeyPathArrayPointer 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 @@ -40,11 +44,15 @@ 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 +internal const val INDEX_NOT_FOUND = io.realm.kotlin.internal.interop.INDEX_NOT_FOUND + /** * Implementation for unmanaged lists, backed by a [MutableList]. */ @@ -69,7 +77,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 { @@ -85,10 +93,22 @@ internal class ManagedRealmList( return operator.get(index) } + override fun contains(element: E): Boolean { + return operator.contains(element) + } + + override fun indexOf(element: E): Int { + return operator.indexOf(element) + } + override fun add(index: Int, element: E) { operator.insert(index, element) } + override fun remove(element: E): Boolean { + return operator.remove(element) + } + // We need explicit overrides of these to ensure that we capture duplicate references to the // same unmanaged object in our internal import caching mechanism override fun addAll(elements: Collection): Boolean = operator.insertAll(size, elements) @@ -147,7 +167,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) @@ -186,6 +206,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( @@ -214,6 +240,10 @@ internal interface ListOperator : CollectionOperator { fun get(index: Int): E + fun contains(element: E): Boolean = indexOf(element) != -1 + + fun indexOf(element: E): Int + // TODO OPTIMIZE We technically don't need update policy and cache for primitive lists but right now RealmObjectHelper.assign doesn't know how to differentiate the calls to the operator fun insert( index: Int, @@ -222,6 +252,14 @@ internal interface ListOperator : CollectionOperator { cache: UnmanagedToManagedObjectCache = mutableMapOf() ) + fun remove(element: E): Boolean = when (val index = indexOf(element)) { + -1 -> false + else -> { + RealmInterop.realm_list_erase(nativePointer, index.toLong()) + true + } + } + fun insertAll( index: Int, elements: Collection, @@ -252,7 +290,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 { @@ -260,12 +298,20 @@ 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 } } } + override fun indexOf(element: E): Int { + inputScope { + with(realmValueConverter) { + return RealmInterop.realm_list_find(nativePointer, publicToRealmValue(element)).toInt() + } + } + } + override fun insert( index: Int, element: E, @@ -273,7 +319,7 @@ internal class PrimitiveListOperator( cache: UnmanagedToManagedObjectCache ) { inputScope { - with(valueConverter) { + with(realmValueConverter) { val transport = publicToRealmValue(element) RealmInterop.realm_list_add(nativePointer, index.toLong(), transport) } @@ -289,7 +335,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) } @@ -301,13 +347,154 @@ internal class PrimitiveListOperator( realmReference: RealmReference, nativePointer: RealmListPointer ): ListOperator = - PrimitiveListOperator(mediator, realmReference, valueConverter, nativePointer) + PrimitiveListOperator(mediator, realmReference, realmValueConverter, nativePointer) +} + +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_list(nativePointer, index.toLong()) }, + { RealmInterop.realm_list_get_dictionary(nativePointer, index.toLong()) } + ) + } + } + + override fun indexOf(element: RealmAny?): Int { + // Unmanaged objects are never found in a managed collections + if (element?.type == RealmAny.Type.OBJECT) { + if (!element.asRealmObject().isManaged()) return -1 + } + return inputScope { + val transport = realmAnyToRealmValueWithoutImport(element) + RealmInterop.realm_list_find(nativePointer, transport).toInt() + } + } + + override fun insert( + index: Int, + element: RealmAny?, + 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)) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_insert_list(nativePointer, index.toLong()) + RealmInterop.realm_list_clear(nativePointer) + 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()) + RealmInterop.realm_dictionary_clear(nativePointer) + 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)) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_set_list(nativePointer, index.toLong()) + RealmInterop.realm_list_clear(nativePointer) + val operator = realmAnyListOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.insertAll(0, realmValue.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_list_set_dictionary(nativePointer, index.toLong()) + RealmInterop.realm_dictionary_clear(nativePointer) + val operator = + realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) + operator.putAll(realmValue.asDictionary(), updatePolicy, cache) + } + ) + } + } + } + + override fun copy( + realmReference: RealmReference, + nativePointer: RealmListPointer + ): ListOperator = + RealmAnyListOperator(mediator, realmReference, nativePointer, issueDynamicObject = issueDynamicObject, issueDynamicMutableObject = issueDynamicMutableObject) } -internal abstract class BaseRealmObjectListOperator( +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, @@ -317,21 +504,30 @@ 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 + } + } + + override fun indexOf(element: E): Int { + // Unmanaged objects are never found in a managed collections + element?.also { + if (!(it as RealmObjectInternal).isManaged()) return -1 + } + return inputScope { + val objRef = realmObjectToRealmReferenceOrError(element as BaseRealmObject?) + val transport = realmObjectTransport(objRef as RealmObjectInterop) + RealmInterop.realm_list_find(nativePointer, transport).toInt() } } } -internal class RealmObjectListOperator( +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, @@ -368,11 +564,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 } } @@ -380,12 +574,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 @@ -396,11 +587,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( @@ -430,11 +620,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 } } @@ -442,12 +630,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 b6c8ff5b47..425e31397c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt @@ -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 @@ -36,6 +37,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 @@ -51,6 +53,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 @@ -60,7 +63,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> { @@ -116,7 +119,7 @@ internal abstract class ManagedRealmMap constructor( ): RealmNotificationTokenPointer = RealmInterop.realm_dictionary_add_notification_callback(nativePointer, keyPaths, 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 @@ -134,6 +137,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( @@ -218,14 +227,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 { @@ -289,7 +291,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 { @@ -304,7 +306,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, @@ -320,7 +322,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) } @@ -334,19 +336,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) } } } @@ -354,7 +365,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) @@ -373,22 +385,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) { @@ -398,21 +432,125 @@ 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, + 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_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 + } + }, + listAsRealmAnyHandler = { realmValue -> + val previous = getInternal(key) + val nativePointer = RealmInterop.realm_dictionary_insert_list(nativePointer, keyTransport) + RealmInterop.realm_list_clear(nativePointer) + val operator = realmAnyListOperator( + mediator, + realmReference, + nativePointer, + issueDynamicObject, issueDynamicMutableObject + ) + operator.insertAll(0, realmValue.asList(), updatePolicy, cache) + previous to true + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val previous = getInternal(key) + val nativePointer = RealmInterop.realm_dictionary_insert_dictionary(nativePointer, keyTransport) + RealmInterop.realm_dictionary_clear(nativePointer) + val operator = + realmAnyMapOperator(mediator, realmReference, nativePointer, issueDynamicObject, issueDynamicMutableObject) + operator.putAll(realmValue.asDictionary(), updatePolicy, cache) + 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, @@ -469,6 +607,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 @@ -478,12 +623,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) + ) } } @@ -503,8 +646,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 @@ -512,10 +654,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, @@ -523,7 +664,6 @@ internal class RealmObjectMapOperator constructor( ) : BaseRealmObjectMapOperator( mediator, realmReference, - valueConverter, keyConverter, nativePointer, clazz, @@ -568,7 +708,6 @@ internal class RealmObjectMapOperator constructor( internal class EmbeddedRealmObjectMapOperator constructor( mediator: Mediator, realmReference: RealmReference, - valueConverter: RealmValueConverter, keyConverter: RealmValueConverter, nativePointer: RealmMapPointer, clazz: KClass, @@ -576,7 +715,6 @@ internal class EmbeddedRealmObjectMapOperator constructo ) : BaseRealmObjectMapOperator( mediator, realmReference, - valueConverter, keyConverter, nativePointer, clazz, @@ -608,11 +746,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 } } @@ -646,30 +782,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}" } @@ -678,7 +822,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, @@ -688,7 +833,8 @@ internal class RealmDictonaryChangeFlow(scope: ProducerScope = DeletedDictionaryImpl(UnmanagedRealmDictionary()) + override fun delete(): MapChange = + DeletedDictionaryImpl(UnmanagedRealmDictionary()) } // ---------------------------------------------------------------------- @@ -701,7 +847,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 @@ -717,9 +863,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}" } @@ -747,7 +897,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 @@ -824,9 +974,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}" } @@ -942,7 +1096,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 @@ -963,7 +1117,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 } } @@ -981,9 +1138,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..1b9ce15a8e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt @@ -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,33 @@ 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) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_list(obj.objectPointer, key) + RealmInterop.realm_list_clear(nativePointer) + val operator = + realmAnyListOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.insertAll(0, value.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_dictionary(obj.objectPointer, key) + RealmInterop.realm_dictionary_clear(nativePointer) + val operator = + realmAnyMapOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.putAll(value.asDictionary(), updatePolicy, cache) } - } else { - realmAnyConverter(obj.mediator, obj.owner) - } - with(converter) { - setValueTransportByKey(obj, key, publicToRealmValue(value)) - } + ) } else -> throw IllegalArgumentException("Unsupported value for transport: $value") } @@ -255,69 +264,82 @@ 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_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 +369,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 +456,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 +546,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 +631,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 +804,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 +927,15 @@ 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, + 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 +1077,90 @@ 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) + }, + 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 ec59b2aea2..aca06a8157 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 @@ -149,7 +149,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 b5586d9ef2..bba98b1701 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 @@ -36,7 +36,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 @@ -58,13 +57,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. @@ -75,10 +67,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 { @@ -150,7 +144,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 57337a8973..6f78d00c82 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt @@ -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 @@ -28,6 +31,7 @@ import io.realm.kotlin.internal.interop.RealmKeyPathArrayPointer 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 @@ -40,6 +44,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 @@ -48,7 +54,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(keyPaths: List?): Flow> { @@ -70,7 +76,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 { @@ -195,7 +202,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) } } @@ -213,6 +220,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( @@ -240,7 +253,6 @@ internal interface SetOperator : CollectionOperator { updatePolicy: UpdatePolicy = UpdatePolicy.ALL, cache: UnmanagedToManagedObjectCache = mutableMapOf() ): Boolean { - realmReference.checkClosed() return addInternal(element, updatePolicy, cache) .also { modCount++ } } @@ -283,13 +295,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 { @@ -303,10 +313,101 @@ 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 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)) + }, + 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 { @@ -315,7 +416,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 @@ -328,16 +429,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) } @@ -348,17 +458,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 @@ -383,27 +506,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) } @@ -413,9 +543,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 ef64cc8da2..0c173a5cbe 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -107,7 +107,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 @@ -122,7 +122,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 deleted + // reference (contrary to other objects that returns null from + // freeze). An `out_collection_was_deleted` flag was added to the + // change object, which would probably be the way to go, but + // 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 bba1c4f58f..89a9eeeb59 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..94a796edc4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/serializers/RealmKSerializers.kt @@ -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,13 @@ public object RealmAnyKSerializer : KSerializer { Type.OBJECT_ID -> RealmAny.create(it.objectId!!) Type.UUID -> RealmAny.create(it.uuid!!) Type.OBJECT -> RealmAny.create(it.realmObject!!) + 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 +405,8 @@ public object RealmAnyKSerializer : KSerializer { ) Type.UUID -> uuid = value.asRealmUUID() Type.OBJECT -> realmObject = value.asRealmObject() + 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..309aa33d7b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/RealmAny.kt @@ -60,6 +60,8 @@ import kotlin.reflect.KClass * OBJECT_ID -> doSomething(realmAny.asObjectId()) * REALM_UUID -> doSomething(realmAny.asRealmUUID()) * REALM_OBJECT -> doSomething(realmAny.asRealmObject()) + * LIST -> doSomething(realmAny.asList()) + * DICTIONARY -> doSomething(realmAny.asDictionary()) * } * ``` * [Short], [Int], [Byte], [Char] and [Long] values are converted internally to `int64_t` values. @@ -82,6 +84,23 @@ import kotlin.reflect.KClass * ``` * `RealmAny` cannot store [EmbeddedRealmObject]s. * + * `RealmAny` can contain other [RealmList] and [RealmDictionary] of [RealmAny]. This means that + * you can build nested collections inside a `RealmAny`-field. + * ``` + * realmObjct.realmAnyField = realmAnyListOf( + * // Primitive values can be added in collections + * 1, + * // Lists and dictionaries can contain other nested collection types + * realmListOf( + * realmListOf(), + * realmDictionaryOf() + * ), + * realmDictionaryOf( + * "key1" to realmListOf(), + * "key2" 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 +131,7 @@ public interface RealmAny { * Supported Realm data types that can be stored in a `RealmAny` instance. */ public enum class Type { - INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT + INT, BOOL, STRING, BINARY, TIMESTAMP, FLOAT, DOUBLE, DECIMAL128, OBJECT_ID, UUID, OBJECT, LIST, DICTIONARY; } /** @@ -233,6 +252,18 @@ public interface RealmAny { */ public fun asRealmObject(clazz: KClass): T + /** + * 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 +374,68 @@ public interface RealmAny { */ public fun create(realmObject: DynamicRealmObject): RealmAny = RealmAnyImpl(Type.OBJECT, DynamicRealmObject::class, realmObject) + + /** + * 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()) + * + * // List can contain other collections, but only `RealmList` and + * // `RealmDictionary`. + * realmObject.realmAnyField = realmAnyListOf( + * // Primitive values + * 1, + * // Lists and dictionaries can contain other collection types + * realmListOf( + * realmListOf(), + * realmDictionaryOf() + * ), + * realmDictionaryOf( + * "key1" to realmListOf(), + * "key2" 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 collections, but only `RealmList` and + * // `RealmDictionary`. + * realmObjct.realmAnyField = realmAnyDictionaryOf( + * "int" to 5, + * // Lists and dictionaries can contain other nested collection types + * "list" to realmListOf( + * realmListOf(), + * realmDictionaryOf() + * ), + * "dictionary" to realmDictionaryOf( + * "key1" to realmListOf(), + * "key2" 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..e13edb5c8f --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -0,0 +1,615 @@ +/* + * 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.realmDictionaryOf +import io.realm.kotlin.ext.realmListOf +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 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 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 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]!!.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 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]!!.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 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[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) + assertEquals(7, nestedList[0]!!.asInt()) + + 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), + "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(3, size) + assertEquals(5, get("keyInt")!!.asInt()) + 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( + "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(2, size) + forEach { value -> + when (value?.type) { + 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("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(3, size) + assertEquals(5, get("keyInt")!!.asInt()) + 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) + + assertEquals(7, nestedList[0]!!.asInt()) + + // 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 + assertEquals(7, nestedList[0]!!.asInt()) + + // 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(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 = "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( + listOf(4, 5, 6), + mapOf( + "key1" to 7, + "key2" to 8, + "key3" to listOf(9), + ) + ) + } + ) + } + + assertEquals(3, 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) + } + realm.query("value[*] == {4, 5, 6}").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) + } + realm.query("value[0][*] == {4, 5, 6}").find().single().run { + assertEquals("EMBEDDED", id) + } + // FIXME Core issue https://github.com/realm/realm-core/issues/7393 + // realm.query("value[*][*] == {4, 5, 6}").find().single().run { + // assertEquals("EMBEDDED", id) + // } + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt index a7b8297bd8..679aeab20e 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt @@ -26,6 +26,9 @@ import io.realm.kotlin.entities.embedded.EmbeddedParent import io.realm.kotlin.entities.embedded.embeddedSchema import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmDictionaryOf +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.notifications.DeletedObject import io.realm.kotlin.notifications.InitialObject @@ -486,6 +489,9 @@ class RealmAnyTests { // Different objects of same type are not equal assertNotEquals(RealmAny.create(Sample()), RealmAny.create(realmObject)) } + // Collections in RealmAny are tested in RealmAnyNestedCollections.kt + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> {} } } } @@ -507,6 +513,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 3ccc1f648e..8507eb24d3 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) + // Collections in RealmAny are tested separately in RealmAnyNestedCollectionTests + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> {} } } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index b46e29bc55..3103be611c 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -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) @@ -693,13 +722,6 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { ), classifier ) - ByteArray::class -> ByteArrayListTester( - realm = realm, - typeSafetyManager = getTypeSafety( - classifier, - elementType.nullable - ) as ListTypeSafetyManager - ) RealmAny::class -> RealmAnyListTester( realm = realm, typeSafetyManager = ListTypeSafetyManager( @@ -1316,6 +1338,9 @@ internal class RealmAnyListTester constructor( expected.asRealmObject().stringField, actual.asRealmObject().stringField ) + // Collections in RealmAny are tested separately in RealmAnyNestedCollectionTests + RealmAny.Type.LIST, + RealmAny.Type.DICTIONARY -> TODO() } } else if (expected != null || actual != null) { fail("One of the RealmAny values is null, expected = $expected, actual = $actual") @@ -1336,38 +1361,6 @@ internal class RealmObjectListTester( assertEquals(expected.stringField, actual.stringField) } -/** - * Check equality for ByteArrays at a structural level with `assertContentEquals`. - */ -internal class ByteArrayListTester( - realm: Realm, - typeSafetyManager: ListTypeSafetyManager -) : ManagedListTester(realm, typeSafetyManager, ByteArray::class) { - override fun assertElementsAreEqual(expected: ByteArray?, actual: ByteArray?) = - assertContentEquals(expected, actual) - - // Removing elements using equals/hashcode will fail for byte arrays since they are - // are only equal if identical - override fun remove() { - val dataSet = typeSafetyManager.dataSetToLoad - val assertions = { list: RealmList -> - assertFalse(list.isEmpty()) - } - - errorCatcher { - realm.writeBlocking { - val list = typeSafetyManager.createContainerAndGetCollection(this) - assertFalse(list.remove(dataSet[0])) - assertTrue(list.add(dataSet[0])) - assertFalse(list.remove(list.last())) - assertions(list) - } - } - - assertListAndCleanup { list -> assertions(list) } - } -} - // ----------------------------------- // Data used to initialize structures // ----------------------------------- diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt index f03eede904..94c34d7a89 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 223f0a9af0..580c8f2a1f 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt @@ -31,9 +31,12 @@ 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.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 +49,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 +87,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) + } } } @@ -279,62 +292,61 @@ 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) + } + } + RealmAny.Type.OBJECT -> SerializableSample().apply { SerializableSample().let { nullableObject = it nullableRealmAnyField = RealmAny.create(it) } } - else -> throw IllegalStateException("Untested type $classifier") + 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..32ee015f45 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicMutableRealmObjectTests.kt @@ -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,116 @@ class DynamicMutableRealmObjectTests { } } + @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( + 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 collections + list[0]!!.asList().let { embeddedList -> + val o = embeddedList.first()!! + .asRealmObject() + assertIs(o) + assertEquals("INNER_LIST", o.getValue("stringField")) + embeddedList.add(RealmAny.Companion.create(dynamicSampleInner)) + } + list[1]!!.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( + "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 collections + 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 +1877,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 +1906,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 +1926,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..e798e4a8b0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/DynamicRealmObjectTests.kt @@ -61,6 +61,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 +1542,46 @@ 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( + 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 { innerDictionary -> + val actualSample = + innerDictionary.asDictionary()!!["key"]!!.asRealmObject() + assertIs(actualSample) + assertEquals("INNER_DICT", actualSample.getValue("stringField")) + } + } + } + @Test fun get_realmAnySet() { val realmAnyValues = realmListOf( @@ -1574,7 +1615,8 @@ class DynamicRealmObjectTests { if (value?.type == RealmAny.Type.OBJECT) { assertEquals( value.asRealmObject().stringField, - actual.asRealmObject().getValue("stringField") + actual.asRealmObject() + .getValue("stringField") ) assertionSucceeded = true return @@ -1671,6 +1713,56 @@ 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" })) + ), + "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["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 52ef1901f4..3cf096dfa2 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 @@ -344,7 +344,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..f5b8fa593d --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedDictionaryNotificationTest.kt @@ -0,0 +1,269 @@ +/* + * 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.DeletableEntityNotificationTests +import io.realm.kotlin.test.common.utils.FlowableTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.types.RealmAny +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 : FlowableTests, DeletableEntityNotificationTests { + + 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..99113c3f08 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -0,0 +1,351 @@ +/* + * 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.asRealmObject +import io.realm.kotlin.ext.realmAnyDictionaryOf +import io.realm.kotlin.ext.realmAnyListOf +import io.realm.kotlin.ext.realmAnyOf +import io.realm.kotlin.internal.platform.runBlocking +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.DeletableEntityNotificationTests +import io.realm.kotlin.test.common.utils.FlowableTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail +import io.realm.kotlin.types.RealmAny +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +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 : FlowableTests, DeletableEntityNotificationTests { + + 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(3.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") + } + + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + RealmAny.create(JsonStyleRealmObject().apply { id = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asRealmObject().value = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals( + "TEST", + it.list[0]!!.asRealmObject().value!!.asString() + ) + } + + listener.cancel() + } + } + + @Test + fun eventsOnDictionaryChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + realmAnyDictionaryOf( + "key1" to "value1" + ) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals(RealmAny.Type.DICTIONARY, it.list[0]!!.type) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asDictionary()["key1"] = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asDictionary()["key1"]!!.asString()) + } + + listener.cancel() + } + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmDictionaryNotificationsTests.kt index 7273e63990..cd9fe302fc 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 @@ -129,7 +129,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 cee0c5b0e3..da7e69c462 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.list.RealmListContainer import io.realm.kotlin.entities.list.listTestSchema +import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.notifications.DeletedList import io.realm.kotlin.notifications.InitialList @@ -35,6 +36,8 @@ import io.realm.kotlin.test.common.utils.assertIsChangeSet import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestChannel import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmList import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -412,7 +415,7 @@ class RealmListNotificationsTests : RealmEntityNotificationTests { } @Test - override fun asFlowOnDeleteEntity() { + override fun asFlowOnDeletedEntity() { runBlocking { val container = realm.write { copyToRealm(RealmListContainer()) } val mutex = Mutex(true) @@ -734,6 +737,80 @@ class RealmListNotificationsTests : RealmEntityNotificationTests { } } + @Test + fun eventsOnObjectChangesInList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.objectListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.objectListField.add( + RealmListContainer().apply { stringField = "CHILD" } + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.objectListField[0].stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0].stringField) + } + + listener.cancel() + } + } + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.nullableRealmAnyListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField.add( + RealmAny.create(RealmListContainer().apply { stringField = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField[0]!!.asRealmObject().stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asRealmObject().stringField) + } + + listener.cancel() + } + } + fun RealmList<*>.removeRange(range: IntRange) { range.reversed().forEach { index -> removeAt(index) } } 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 60dec6fe22..4375f90beb 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 @@ -243,7 +243,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 b1122f64c6..3117be3da2 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 @@ -286,7 +286,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/DeletableEntityNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt new file mode 100644 index 0000000000..08839b3f25 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/DeletableEntityNotificationTests.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.test.common.utils + +/** + * All classes that tests classes that exposes notifications on entities that can be removed from + * the realm (i.e. RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm and + * RealmResults) should implement this interface to be sure that we test common behaviour across + * those classes. + */ +interface DeletableEntityNotificationTests { + // Verify that we get deletion events and close the Flow when the object being observed (or + // containing object) is deleted. + fun deleteEntity() + + // Verify that we emit deletion events and close the flow when registering for notifications on + // an outdated entity. + fun asFlowOnDeletedEntity() +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt index 9f69e83046..46aea13298 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/RealmEntityNotificationTests.kt @@ -17,17 +17,10 @@ package io.realm.kotlin.test.common.utils /** - * All classes that tests classes that exposes notifications on entities that can be removed from - * the realm (i.e. RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm and - * RealmResults) should implement this interface to be sure that we test common behaviour across + * Test for top level entities that can be deleted and supports key-path-filtering (i.e. + * RealmObject, RealmList, RealmSet, Backlinks but specifically not Realm, RealmResults and + * RealmAny) should implement this interface to be sure that we test common behaviour across * those classes. */ -interface RealmEntityNotificationTests : FlowableTests, KeyPathFlowableTests { - // Verify that we get deletion events and close the Flow when the object being observed (or - // containing object) is deleted. - fun deleteEntity() - - // Verify that we emit deletion events and close the flow when registering for notifications on - // an outdated entity. - fun asFlowOnDeleteEntity() -} +interface RealmEntityNotificationTests : + FlowableTests, DeletableEntityNotificationTests, KeyPathFlowableTests diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index a233dfb87a..e7a759c7cb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -66,6 +66,8 @@ import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.test.util.trySendOrFail import io.realm.kotlin.test.util.use +import io.realm.kotlin.types.BaseRealmObject +import kotlinx.atomicfu.atomic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -80,6 +82,7 @@ import okio.Path.Companion.toPath import org.mongodb.kbson.ObjectId import kotlin.random.Random import kotlin.random.nextULong +import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -1336,6 +1339,8 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } + // This test cannot run multiple times on the same server instance as the primary + // key of the objects from asset-pbs.realm will not be unique on secondary runs. @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" @@ -1365,26 +1370,24 @@ class SyncedRealmTests { } } + val initialDataVerified = atomic(false) val config2 = createPartitionSyncConfig( - user = user, partitionValue = partitionValue, name = "db1", + user = user, partitionValue = partitionValue, name = "db2", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { - fail("Realm 1: $error") + fail("Realm 2: $error") } } ) { waitForInitialRemoteData(30.seconds) initialData { - // Verify that initial data is running before data is synced - assertEquals(0, query().find().size) - } - } - Realm.open(config2).use { - runBlocking { - it.syncSession.downloadAllServerChanges(30.seconds) - assertEquals(4, it.query().find().size) + // Verify that initial data is running after data is synced + assertEquals(4, query().find().size) + initialDataVerified.value = true } } + Realm.open(config2).use { } + assertTrue { initialDataVerified.value } } @Test @@ -1497,6 +1500,7 @@ class SyncedRealmTests { @Test fun flexibleSync_throwsWithLocalInitialRealmFile() { + val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -1860,9 +1864,10 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, + schema: Set> = PARTITION_BASED_SCHEMA, block: SyncConfiguration.Builder.() -> Unit = {} ): SyncConfiguration = SyncConfiguration.Builder( - schema = PARTITION_BASED_SCHEMA, + schema = schema, user = user, partitionValue = partitionValue ).name(name).also { builder -> @@ -1879,11 +1884,12 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, + schema: Set> = FLEXIBLE_SYNC_SCHEMA, initialSubscriptions: InitialSubscriptionsCallback? = null, block: SyncConfiguration.Builder.() -> Unit = {}, ): SyncConfiguration = SyncConfiguration.Builder( user = user, - schema = FLEXIBLE_SYNC_SCHEMA + schema = schema ).name(name).also { builder -> if (encryptionKey != null) builder.encryptionKey(encryptionKey) if (errorHandler != null) builder.errorHandler(errorHandler)