diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/MutableRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/MutableRealm.kt index d23f554854..8f69470681 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/MutableRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/MutableRealm.kt @@ -87,6 +87,10 @@ public interface MutableRealm : TypedRealm { */ public fun copyToRealm(instance: T, updatePolicy: UpdatePolicy = UpdatePolicy.ERROR): T + public fun insertToRealm(instance: T, updatePolicy: UpdatePolicy = UpdatePolicy.ERROR) + + public fun insertToRealm(instances: Collection, updatePolicy: UpdatePolicy = UpdatePolicy.ERROR) + /** * Returns a [RealmQuery] matching the predicate represented by [query]. * diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/InternalMutableRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/InternalMutableRealm.kt index 5179aa419e..20f3aea4a6 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/InternalMutableRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/InternalMutableRealm.kt @@ -57,6 +57,24 @@ internal interface InternalMutableRealm : MutableRealm { return copyToRealm(configuration.mediator, realmReference, instance, updatePolicy) } + override fun insertToRealm(instance: T, updatePolicy: UpdatePolicy) { + val cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + + insertToRealm(realmReference, instance, updatePolicy, cache) + + cache.forEach { it.value.release() } + cache.clear() + } + + override fun insertToRealm(instances: Collection, updatePolicy: UpdatePolicy) { + val cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + + insertToRealm(realmReference, instances, updatePolicy, cache) + + cache.forEach { it.value.release() } + cache.clear() + } + override fun delete(deleteable: Deleteable) { deleteable.asInternalDeleteable().delete() } 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 f21e85de1d..353a568138 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 @@ -35,10 +35,13 @@ import io.realm.kotlin.internal.interop.ObjectKey import io.realm.kotlin.internal.interop.PropertyKey import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmInterop.realm_dictionary_insert +import io.realm.kotlin.internal.interop.RealmInterop.realm_dictionary_insert_embedded import io.realm.kotlin.internal.interop.RealmInterop.realm_get_value 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.RealmObjectPointer import io.realm.kotlin.internal.interop.RealmSetPointer import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.Timestamp @@ -49,6 +52,7 @@ import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow import io.realm.kotlin.internal.schema.ClassMetadata import io.realm.kotlin.internal.schema.PropertyMetadata import io.realm.kotlin.internal.schema.RealmStorageTypeImpl +import io.realm.kotlin.internal.schema.SchemaMetadata import io.realm.kotlin.internal.schema.realmStorageType import io.realm.kotlin.internal.util.Validation.sdkError import io.realm.kotlin.query.RealmResults @@ -253,6 +257,306 @@ internal object RealmObjectHelper { } } + @Suppress("ComplexMethod", "LongMethod") + internal inline fun setValueByKeyAndPointer( + obj: RealmObjectPointer, + key: PropertyKey, + value: Any?, + realmReference: LiveRealmReference, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + ) { + inputScope { + when (value) { + null -> RealmInterop.realm_set_value(obj, key, nullTransport(), false) + is String -> RealmInterop.realm_set_value(obj, key, stringTransport(value), false) + is ByteArray -> RealmInterop.realm_set_value(obj, key, byteArrayTransport(value), false) + is Long -> RealmInterop.realm_set_value(obj, key, longTransport(value), false) + is Boolean -> RealmInterop.realm_set_value(obj, key, booleanTransport(value), false) + is Timestamp -> RealmInterop.realm_set_value(obj, key, timestampTransport(value), false) + is Float -> RealmInterop.realm_set_value(obj, key, floatTransport(value), false) + is Double -> RealmInterop.realm_set_value(obj, key, doubleTransport(value), false) + is Decimal128 -> RealmInterop.realm_set_value(obj, key, decimal128Transport(value), false) + is BsonObjectId -> RealmInterop.realm_set_value( + obj, + key, + objectIdTransport(value.toByteArray()), false + ) + is RealmUUID -> RealmInterop.realm_set_value(obj, key, uuidTransport(value.bytes), false) + is RealmObjectInterop -> RealmInterop.realm_set_value( + obj, + key, + realmObjectTransport(value), false + ) + is MutableRealmInt -> RealmInterop.realm_set_value(obj, key, longTransport(value.get()), false) + is RealmAny -> { + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { realmValue -> + RealmInterop.realm_set_value(obj, key, realmValue, false) + }, + referenceAsRealmAnyHandler = { realmValue -> + val realmObject: BaseRealmObject = realmValue.asRealmObject() + val linkPointer: RealmObjectPointer? + + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + + inputScope { + RealmInterop.realm_set_value(obj, key, realmObjectTransport(object: RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }), false) + } + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_list(obj, key) + RealmInterop.realm_list_clear(nativePointer) + realmValue.asList().forEachIndexed { index, innerAnyValue -> + insertRealmAnyInList(nativePointer, innerAnyValue, index.toLong(), realmReference, updatePolicy, cache) + } + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_dictionary(obj, key) + RealmInterop.realm_dictionary_clear(nativePointer) + realmValue.asDictionary().forEach { + insertRealmAnyInDictionary(nativePointer, it.key, it.value, realmReference, updatePolicy, cache) + } + } + ) + } + else -> throw IllegalArgumentException("Unsupported value for transport: $value") + } + } + } + + + private fun insertRealmAnyInDictionary( + nativePointer: RealmMapPointer, + key: String, + value: RealmAny?, + realmReference: LiveRealmReference, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + ) { + inputScope { + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { innerRealmValue -> + realm_dictionary_insert( + nativePointer, stringTransport(key), innerRealmValue + ) + }, + referenceAsRealmAnyHandler = { innerRealmValue -> + val realmObject: BaseRealmObject = innerRealmValue.asRealmObject() + val linkPointer: RealmObjectPointer? + + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + inputScope { + realm_dictionary_insert( + nativePointer, + stringTransport(key), + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } + }, + listAsRealmAnyHandler = { realmValue -> + val innerListNativePointer = RealmInterop.realm_dictionary_insert_list( + nativePointer, + stringTransport(key) + ) + RealmInterop.realm_list_clear(innerListNativePointer) + realmValue.asList().forEachIndexed { index, value -> + insertRealmAnyInList( + innerListNativePointer, + value, + index.toLong(), + realmReference, + updatePolicy, + cache + ) + } + + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val innerMapNativePointer = RealmInterop.realm_dictionary_insert_dictionary( + nativePointer, + stringTransport(key) + ) + RealmInterop.realm_dictionary_clear(innerMapNativePointer) + realmValue.asDictionary().forEach { + insertRealmAnyInDictionary( + innerMapNativePointer, + it.key, + it.value, + realmReference, + updatePolicy, + cache + ) + } + }, + ) + } + } + + private fun insertRealmAnyInSet( + nativePointer: RealmSetPointer, + value: RealmAny?, + realmReference: LiveRealmReference, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + ) { + inputScope { + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { innerRealmValue -> + RealmInterop.realm_set_insert(nativePointer, innerRealmValue) + }, + referenceAsRealmAnyHandler = { innerRealmValue -> + val realmObject: BaseRealmObject = innerRealmValue.asRealmObject() + val linkPointer: RealmObjectPointer? + + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + inputScope { + RealmInterop.realm_set_insert( + nativePointer, + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } + }, + listAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, + dictionaryAsRealmAnyHandler = { realmValue -> throw IllegalArgumentException("Sets cannot contain other collections ") }, + ) + } + } + + + private fun insertRealmAnyInList( + nativePointer: RealmListPointer, + value: RealmAny?, + index: Long, + realmReference: LiveRealmReference, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf() + ) { + + inputScope { + realmAnyHandler(value = value, + primitiveValueAsRealmValueHandler = { innerRealmValue -> + RealmInterop.realm_list_add(nativePointer, index, innerRealmValue) + }, + referenceAsRealmAnyHandler = { innerRealmValue -> + val realmObject: BaseRealmObject = innerRealmValue.asRealmObject() + val linkPointer: RealmObjectPointer? + + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + inputScope { + RealmInterop.realm_list_add( + nativePointer, + index, + // TODO add extension function toRealmObjectTransport + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } + }, + listAsRealmAnyHandler = { innerRealmValue -> + val innerListNativePointer = + RealmInterop.realm_list_insert_list(nativePointer, index) + RealmInterop.realm_list_clear(innerListNativePointer) + innerRealmValue.asList().forEachIndexed { index, realmAny -> + insertRealmAnyInList( + innerListNativePointer, + realmAny, + index.toLong(), + realmReference, + updatePolicy, + cache + ) + } + }) + } + } + // TODO optimize: avoid this many get functions by creating the scope in the accessor via the // compiler plugin. See comment in AccessorModifierIrGeneration.modifyAccessor about this. @@ -749,6 +1053,22 @@ internal object RealmObjectHelper { } } + internal fun assignByPointer( + target: RealmObjectPointer, + source: BaseRealmObject, + realmReference: LiveRealmReference, + schema: SchemaMetadata, + metadata: ClassMetadata, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectPointerCache + ) { + if (target is DynamicRealmObject) { + TODO("DynamicRealmObject not supported yet") + } else { + assignByPointerTyped(target, source, schema, realmReference, metadata, updatePolicy, cache) + } + } + @Suppress("NestedBlockDepth", "LongMethod", "ComplexMethod") internal fun assignTyped( target: BaseRealmObject, @@ -852,6 +1172,570 @@ internal object RealmObjectHelper { } } + @Suppress("NestedBlockDepth", "LongMethod", "ComplexMethod") + internal fun assignByPointerTyped( + target: RealmObjectPointer, + source: BaseRealmObject, + schema: SchemaMetadata, + realmReference: LiveRealmReference, + metadata: ClassMetadata, + updatePolicy: UpdatePolicy, + cache: UnmanagedToManagedObjectPointerCache + ) { + // TODO OPTIMIZE We could set all properties at once with one C-API call + metadata.properties.filter { + // Primary keys are set at construction time + // Computed properties have no assignment + !it.isComputed && !it.isPrimaryKey + }.forEach { property -> + // For synced Realms in ADDITIVE mode, Object Store will return the full on-disk + // schema, including fields not defined in the user schema. This makes it problematic + // to iterate through the Realm schema and assume that all properties will have kotlin + // properties associated with them. To avoid throwing errors we double check that + val accessor: KProperty1 = property.accessor + ?: if (property.isUserDefined()) { + sdkError("Typed object should always have an accessor: ${metadata.className}.${property.name}") + } else { + return@forEach // Property is only visible on disk, ignore. + } + accessor as KMutableProperty1 + when (property.collectionType) { + CollectionType.RLM_COLLECTION_TYPE_NONE -> when (property.type) { + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { + val isTargetEmbedded = + schema.getOrThrow(property.linkTarget).isEmbeddedRealmObject + if (isTargetEmbedded) { + val value = accessor.get(source) as EmbeddedRealmObject? + if (value != null) { + val embedded: RealmObjectPointer = RealmInterop.realm_set_embedded(target, property.key) + + assignByPointer(embedded, value, realmReference, schema, realmReference.schemaMetadata[value::class.simpleName!!]!!, updatePolicy, cache) + } else { + inputScope { + RealmInterop.realm_set_value(target, property.key, nullTransport(), false) + } + } + } else { + val value = accessor.get(source) as RealmObject? + value?.let { + val linkPointer: RealmObjectPointer? + + if (value.isManaged()) { + linkPointer = (value as BaseRealmObject).realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[it] == null) { + // recursive call to persist it + linkPointer = insertToRealm(realmReference, value, updatePolicy, cache) + // update cache + cache[value] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[it] + } + } + + // set link using linkPointer + inputScope { + RealmInterop.realm_set_value(target, property.key, realmObjectTransport(object: RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }), false) + } + } ?: { + // set link to null + inputScope { + RealmInterop.realm_set_value(target, property.key, realmObjectTransport(null), false) + } + } + } + } + else -> { + val getterValue = accessor.get(source) + setValueByKeyAndPointer(target, + property.key, + getterValue, + realmReference + ) + } + } + CollectionType.RLM_COLLECTION_TYPE_LIST -> { + val realmListPointer = + RealmInterop.realm_get_list(target, property.key) + RealmInterop.realm_list_clear(realmListPointer) + + // TODO this could be optimised to get the type of the elements outside the forEachIndexed iteration + (accessor.get(source) as RealmList<*>).forEachIndexed { index, item: Any? -> + when (property.type) { + PropertyType.RLM_PROPERTY_TYPE_INT -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + longTransport(item as Long?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BOOL -> + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + booleanTransport(item as Boolean?) + ) + } + PropertyType.RLM_PROPERTY_TYPE_STRING -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + stringTransport(item as String?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BINARY -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + byteArrayTransport(item as ByteArray?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + floatTransport(item as Float?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + doubleTransport(item as Double?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + decimal128Transport( item as Decimal128?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + timestampTransport(item as Timestamp?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT_ID -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + objectIdTransport( + (item as BsonObjectId?)?.toByteArray() + ) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_UUID -> { + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + uuidTransport( + (item as RealmUUID?)?.bytes + ) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { + insertRealmAnyInList(realmListPointer, + item as RealmAny?, + index.toLong(), + realmReference, + updatePolicy, + cache) + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { + val isTargetEmbedded = + schema.getOrThrow(property.linkTarget).isEmbeddedRealmObject + if (isTargetEmbedded) { + val value = item as EmbeddedRealmObject? + if (value != null) { + val embeddedPointer = + RealmInterop.realm_list_insert_embedded( + realmListPointer, + index.toLong() + ) + assignByPointer(embeddedPointer, value, realmReference, schema, realmReference.schemaMetadata[value::class.simpleName!!]!!, updatePolicy, cache) + } // should not have a nullable RealmObject/RealmEmbedded element + } else { + val realmObject: BaseRealmObject = item as BaseRealmObject + val linkPointer: RealmObjectPointer? + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + + // set link using linkPointer + inputScope { + RealmInterop.realm_list_add( + realmListPointer, + index.toLong(), + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } + } + } + else -> TODO("Collection type ${property.collectionType} is not supported") + } + } + } + CollectionType.RLM_COLLECTION_TYPE_SET -> { + val realmSetPointer = + RealmInterop.realm_get_set(target, property.key) + RealmInterop.realm_set_clear(realmSetPointer) + (accessor.get(source) as RealmSet<*>).forEach { item: Any? -> + when (property.type) { + PropertyType.RLM_PROPERTY_TYPE_INT -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + longTransport((item as Int?)?.toLong()) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BOOL -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + booleanTransport(item as Boolean?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_STRING -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + stringTransport(item as String?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BINARY -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + byteArrayTransport(item as ByteArray?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + floatTransport(item as Float?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + doubleTransport(item as Double?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + decimal128Transport(item as Decimal128?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + timestampTransport(item as Timestamp?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT_ID -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + objectIdTransport((item as BsonObjectId?)?.toByteArray()) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_UUID -> { + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + uuidTransport((item as RealmUUID?)?.bytes) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { + insertRealmAnyInSet(realmSetPointer, + item as RealmAny?, + realmReference, + updatePolicy, + cache) + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { + val realmObject: BaseRealmObject = item as BaseRealmObject + val linkPointer: RealmObjectPointer? + if (realmObject.isManaged()) { + linkPointer = realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + + // set link using linkPointer + inputScope { + RealmInterop.realm_set_insert( + realmSetPointer, + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } + } + + else -> {} + } + } + } + CollectionType.RLM_COLLECTION_TYPE_DICTIONARY -> { + val realmDictionaryPointer: RealmMapPointer = + RealmInterop.realm_get_dictionary(target, property.key) + RealmInterop.realm_dictionary_clear(realmDictionaryPointer) + (accessor.get(source) as RealmDictionary<*>).map { + when (property.type) { + PropertyType.RLM_PROPERTY_TYPE_INT -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + longTransport((it.value as Int?)?.toLong()) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BOOL -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + booleanTransport(it.value as Boolean?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_STRING -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + stringTransport(it.value as String?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_BINARY -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + byteArrayTransport(it.value as ByteArray?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_FLOAT -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + floatTransport(it.value as Float?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DOUBLE -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + doubleTransport(it.value as Double?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_DECIMAL128 -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + decimal128Transport(it.value as Decimal128?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_TIMESTAMP -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + timestampTransport(it.value as Timestamp?) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT_ID -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + objectIdTransport((it.value as BsonObjectId?)?.toByteArray()) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_UUID -> { + inputScope { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + uuidTransport((it.value as RealmUUID?)?.bytes) + ) + } + } + PropertyType.RLM_PROPERTY_TYPE_MIXED -> { + insertRealmAnyInDictionary(realmDictionaryPointer, + it.key, + it.value as RealmAny?, + realmReference, + updatePolicy, + cache) + } + PropertyType.RLM_PROPERTY_TYPE_OBJECT -> { + inputScope { + val isTargetEmbedded = + schema.getOrThrow(property.linkTarget).isEmbeddedRealmObject + if (isTargetEmbedded) { + val value = it.value as EmbeddedRealmObject? + val embeddedPointer = RealmInterop.realm_get_object( + realmReference.dbPointer, + realm_dictionary_insert_embedded( + realmDictionaryPointer, + stringTransport(it.key) + ).getLink() + ) + if (value != null) { + assignByPointer( + embeddedPointer, + value, + realmReference, + schema, + realmReference.schemaMetadata[value::class.simpleName!!]!!, + updatePolicy, + cache + ) + } else { + realm_dictionary_insert(realmDictionaryPointer, stringTransport(it.key), nullTransport()) + } + } else { + val realmObject: BaseRealmObject? = + it.value as BaseRealmObject? + if (realmObject != null) { + val linkPointer: RealmObjectPointer? + if (realmObject.isManaged()) { + linkPointer = + realmObject.realmObjectReference!!.objectPointer + } else { // unmanaged + // first check cache + if (cache[realmObject] == null) { + // recursive call to persist it + linkPointer = insertToRealm( + realmReference, + realmObject, + updatePolicy, + cache + ) + // update cache + cache[realmObject] = linkPointer + } else { + // previously managed set the link only + linkPointer = cache[realmObject] + } + } + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + realmObjectTransport(object : RealmObjectInterop { + override val objectPointer: RealmObjectPointer + get() = linkPointer!! + }) + ) + } else { + realm_dictionary_insert( + realmDictionaryPointer, + stringTransport(it.key), + nullTransport() + ) + } + } + } + } + else -> {} + } + } + + } + else -> TODO("Collection type ${property.collectionType} is not supported") + } + } + } + @Suppress("LongParameterList") internal fun assignDynamic( target: DynamicMutableRealmObject, 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 16f7df2bf9..3de03eef8a 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,12 +23,14 @@ 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.RealmObjectHelper.assignByPointer 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 import io.realm.kotlin.internal.interop.PropertyKey import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmObjectPointer import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.inputScope import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow @@ -47,6 +49,7 @@ import kotlin.reflect.KProperty1 // This cache is only valid for unmanaged realm objects as, for them we only consider the users // `equals` method, which in general just is the memory address of the object. internal typealias UnmanagedToManagedObjectCache = MutableMap // Map +internal typealias UnmanagedToManagedObjectPointerCache = MutableMap // Map // For managed realm objects we use `` as a unique identifier // We are using a hash on the Kotlin side so we can use a HashMap for O(1) lookup rather than @@ -110,6 +113,16 @@ internal fun create( } } +internal fun createAsReference( + realm: LiveRealmReference, + className: String +): RealmObjectPointer { + val key = realm.schemaMetadata.getOrThrow(className).classKey + return key.let { + RealmInterop.realm_object_create(realm.dbPointer, key) + } +} + @Suppress("LongParameterList") internal fun create( mediator: Mediator, @@ -140,6 +153,30 @@ internal fun create( } } +@Suppress("LongParameterList") +internal fun createAsReference( + realm: LiveRealmReference, + className: String, + primaryKey: RealmValue, + updatePolicy: UpdatePolicy +): RealmObjectPointer { + val key = realm.schemaMetadata.getOrThrow(className).classKey + return key.let { + when (updatePolicy) { + UpdatePolicy.ERROR -> RealmInterop.realm_object_create_with_primary_key( + realm.dbPointer, + key, + primaryKey + ) + UpdatePolicy.ALL -> RealmInterop.realm_object_get_or_create_with_primary_key( + realm.dbPointer, + key, + primaryKey + ) + } + } +} + @Suppress("NestedBlockDepth", "LongMethod", "ComplexMethod") internal fun copyToRealm( mediator: Mediator, @@ -223,6 +260,176 @@ internal fun copyToRealm( } } + +internal fun insertToRealm( + realmReference: LiveRealmReference, + element: T, + updatePolicy: UpdatePolicy = UpdatePolicy.ERROR, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf(), +): RealmObjectPointer { + // Throw if object is not valid + if (!element.isValid()) { + throw IllegalArgumentException("Cannot copy an invalid managed object to Realm.") + } + + cache[element]?.let { + return it + } + + // Create a new object if it wasn't managed + val className: String? + var hasPrimaryKey: Boolean = false + var primaryKey: Any? = null + if (element is DynamicUnmanagedRealmObject) { + className = element.type + val primaryKeyName: String? = + realmReference.schemaMetadata[className]?.let { classMetaData -> + if (classMetaData.isEmbeddedRealmObject) { + throw IllegalArgumentException("Cannot create embedded object without a parent") + } + classMetaData.primaryKeyProperty?.key?.let { key: PropertyKey -> + classMetaData[key]?.name + } + } + hasPrimaryKey = primaryKeyName != null + primaryKey = primaryKeyName?.let { + val properties = element.properties + if (properties.containsKey(primaryKeyName)) { + properties[primaryKeyName] + } else { + throw IllegalArgumentException("Cannot create object of type '$className' without primary key property '$primaryKeyName'") + } + } + } else { + val companion = realmObjectCompanionOrThrow(element::class) + className = companion.io_realm_kotlin_className + if (companion.io_realm_kotlin_classKind == RealmClassKind.EMBEDDED) { + throw IllegalArgumentException("Cannot create embedded object without a parent") + } + companion.io_realm_kotlin_primaryKey?.let { + hasPrimaryKey = true + primaryKey = (it as KProperty1).get(element) + } + } + val target: RealmObjectPointer = if (hasPrimaryKey) { + inputScope { + try { + createAsReference( + realmReference, + className, + kAnyToPrimaryKeyRealmValue(primaryKey), + updatePolicy + ) + } catch (e: IllegalStateException) { + // Remap exception to avoid a breaking change. To core this is an IllegalStateException + // as it considers that there is no issue with the argument. + throw IllegalArgumentException(e.message, e.cause) + } + } + } else { + createAsReference(realmReference, className) + } + + cache[element] = target + assignByPointer(target, element, realmReference, realmReference.schemaMetadata, realmReference.schemaMetadata[className]!!, updatePolicy, cache) + return target +} + +internal fun insertToRealm( + realmReference: LiveRealmReference, + elements: Collection, + updatePolicy: UpdatePolicy = UpdatePolicy.ERROR, + cache: UnmanagedToManagedObjectPointerCache = mutableMapOf(), +) { + when (elements.size) { + 0 -> Unit + 1 -> insertToRealm(realmReference, elements.elementAt(0), updatePolicy, cache) + else -> { + // we factor common checks (primary key, companion etc.) + val className: String? + var hasPrimaryKey: Boolean = false + var isDynamic = false + var companion: RealmObjectCompanion? = null + var primaryKey: Any? = null + var primaryKeyName: String? = null + + val element = elements.elementAt(0) + if (element is DynamicUnmanagedRealmObject) { + isDynamic = true + className = element.type + primaryKeyName = + realmReference.schemaMetadata[className]?.let { classMetaData -> + if (classMetaData.isEmbeddedRealmObject) { + throw IllegalArgumentException("Cannot create embedded object without a parent") + } + classMetaData.primaryKeyProperty?.key?.let { key: PropertyKey -> + classMetaData[key]?.name + } + } + hasPrimaryKey = primaryKeyName != null + primaryKeyName?.let { + val properties = element.properties + if (!properties.containsKey(primaryKeyName)) { + throw IllegalArgumentException("Cannot create object of type '$className' without primary key property '$primaryKeyName'") + } + } + + } else { + companion = realmObjectCompanionOrThrow(element::class) + className = companion.io_realm_kotlin_className + if (companion.io_realm_kotlin_classKind == RealmClassKind.EMBEDDED) { + throw IllegalArgumentException("Cannot create embedded object without a parent") + } + companion.io_realm_kotlin_primaryKey?.let { + hasPrimaryKey = true + } + } + + // iterate all the elements + for (e in elements) { + if (!e.isValid()) { + throw IllegalArgumentException("Cannot copy an invalid managed object to Realm.") + } + + if (cache[e] == null) { + if (isDynamic) { + if (hasPrimaryKey) { + primaryKey = (e as DynamicUnmanagedRealmObject).properties[primaryKeyName] + } + + } else { + if (hasPrimaryKey) { + primaryKey = (companion!!.io_realm_kotlin_primaryKey as KProperty1).get(e) + } + } + // Create a new object if it wasn't managed + val target: RealmObjectPointer = if (hasPrimaryKey) { + inputScope { + try { + createAsReference( + realmReference, + className, + kAnyToPrimaryKeyRealmValue(primaryKey), + updatePolicy + ) + } catch (e: IllegalStateException) { + // Remap exception to avoid a breaking change. To core this is an IllegalStateException + // as it considers that there is no issue with the argument. + throw IllegalArgumentException(e.message, e.cause) + } + } + } else { + createAsReference(realmReference, className) + } + + cache[e] = target + assignByPointer(target, e, realmReference, realmReference.schemaMetadata, realmReference.schemaMetadata[className]!!, updatePolicy, cache) + } + } + } + } +} + /** * Work-around for Realms not being available inside RealmObjects until * https://github.com/realm/realm-kotlin/issues/582 is fixed. diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MutableRealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MutableRealmTests.kt index 5893c2c2aa..6d7f39ddb4 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MutableRealmTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MutableRealmTests.kt @@ -364,7 +364,7 @@ class MutableRealmTests { } @Test - fun copytToRealm_existingListIsFlushed_primitiveType() { + fun copyToRealm_existingListIsFlushed_primitiveType() { val child = SampleWithPrimaryKey().apply { primaryKey = 1 stringField = "INITIAL" @@ -383,7 +383,7 @@ class MutableRealmTests { } @Test - fun copytToRealm_existingListIsFlushed_realmObject() { + fun copyToRealm_existingListIsFlushed_realmObject() { val child = SampleWithPrimaryKey().apply { primaryKey = 1 stringField = "INITIAL" diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmInsertTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmInsertTests.kt new file mode 100644 index 0000000000..aab693634d --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmInsertTests.kt @@ -0,0 +1,702 @@ +/* + * Copyright 2024 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.UpdatePolicy +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.test.platform.PlatformUtils +import io.realm.kotlin.types.EmbeddedRealmObject +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmDictionary +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmSet +import io.realm.kotlin.types.annotations.PrimaryKey +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + + +@Suppress("LargeClass") +class RealmInsertTests { + + private lateinit var configuration: RealmConfiguration + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + configuration = RealmConfiguration.Builder( + schema = setOf( + SimplePOJO::class, + SimpleEmbedded::class, + ) + ).directory(tmpDir).build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun insertToRealmLinks() { + val simplePojo0 = realm.writeBlocking { + copyToRealm(SimplePOJO().apply { primaryKey = 0; stringField = "simplePojo0" }) +// insertToRealm(simplePojo2) + } + val simplePojo1 = + SimplePOJO().apply { primaryKey = 1; stringField = "simplePojo1"; parent = simplePojo0 } + val simplePojo2 = + SimplePOJO().apply { primaryKey = 2; stringField = "simplePojo2"; parent = simplePojo1 } + + realm.writeBlocking { + insertToRealm(simplePojo2) + } + assertEquals(3, realm.query().count().find()) + assertEquals( + "simplePojo0", + realm.query("primaryKey = 2").first().find()?.parent?.parent?.stringField + ) + } + + @Test + fun insertToRealmRealmList() { + val simplePojo: SimplePOJO = SimplePOJO().apply { + primaryKey = 0; + stringField = "simplePojo0"; + nullableStringListField.addAll( + arrayOf("One", "Two", null, "Three") + ) + } + realm.writeBlocking { + insertToRealm(simplePojo) + } + assertEquals(1, realm.query().count().find()) + + val first: SimplePOJO = realm.query().first().find()!! + assertEquals("simplePojo0", first.stringField) + assertEquals(4, first.nullableStringListField.size) + assertEquals("One", first.nullableStringListField[0]) + assertEquals("Two", first.nullableStringListField[1]) + assertEquals(null, first.nullableStringListField[2]) + assertEquals("Three", first.nullableStringListField[3]) + + assertTrue(first.objectListField.isEmpty()) + + // making sure clean is called before inserting new elements + realm.writeBlocking { + // do an update with an existing object with the same primary key but different list elements + val simplePojoUpdated: SimplePOJO = SimplePOJO().apply { + primaryKey = 0; + stringField = "simplePojo0 updated"; + nullableStringListField.addAll( + arrayOf("Nabil", "Hachicha") + ) + objectListField + .addAll( + arrayOf(copyToRealm(SimplePOJO().apply { + primaryKey = 1 + stringField = "simplePojo1" + nullableStringListField.addAll( + arrayOf("SP1", null, "SP2") + ) + }),// managed + SimplePOJO().apply { + primaryKey = 2; stringField = "simplePojo2" + } // unmanaged + ) + ) + } + insertToRealm(simplePojoUpdated, updatePolicy = UpdatePolicy.ALL) +// findLatest(first)!!.let { +// it.stringField = "simplePojo0 updated" +// it.stringField = "simplePojo0 updated" // check we can't modify the +// } + } + + assertEquals(3, realm.query().count().find()) + var updated: SimplePOJO = realm.query().first().find()!! + assertEquals("simplePojo0 updated", updated.stringField) + assertEquals(2, updated.nullableStringListField.size) + assertEquals("Nabil", updated.nullableStringListField[0]) + assertEquals("Hachicha", updated.nullableStringListField[1]) + + assertEquals(2, updated.objectListField.size) + assertEquals("simplePojo1", updated.objectListField[0].stringField) + assertEquals(3, updated.objectListField[0].nullableStringListField.size) + assertEquals("SP1", updated.objectListField[0].nullableStringListField[0]) + assertNull(updated.objectListField[0].nullableStringListField[1]) + assertEquals("SP2", updated.objectListField[0].nullableStringListField[2]) + assertEquals("simplePojo2", updated.objectListField[1].stringField) + + // embedded list + realm.writeBlocking { + // do an update with an existing object with the same primary key but different list elements + val simplePojoUpdated: SimplePOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "simplePojo0 updated 2" + embeddedObjectListField + .addAll(arrayOf( + SimpleEmbedded().apply { id = "embedded1"; }, + SimpleEmbedded().apply { id = "embedded2"; } + )) + } + insertToRealm(simplePojoUpdated, updatePolicy = UpdatePolicy.ALL) + } + + updated = realm.query().first().find()!! + assertEquals("simplePojo0 updated 2", updated.stringField) +// assertEquals(2, updated.nullableStringListField.size) + assertEquals(2, updated.embeddedObjectListField.size) + assertEquals("embedded1", updated.embeddedObjectListField[0].id) + assertEquals("embedded2", updated.embeddedObjectListField[1].id) + + } + + @Test + fun insertToRealmEmbedded() { + realm.writeBlocking { + insertToRealm( + SimplePOJO().apply { + primaryKey = 0 + stringField = "SimplePOJO 0" + embedded = SimpleEmbedded().apply { + id = "42"; link = + SimplePOJO().apply { primaryKey = 1; stringField = "SimplePOJO 1" } + } + }) + } + + assertEquals(2, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + } + + @Test + fun insertToRealmAny() { + realm.writeBlocking { + insertToRealm( + SimplePOJO().apply { + primaryKey = 0 + stringField = "SimplePOJO 0" + nullableRealmAnyField = RealmAny.create("string any field") + }) + } + + assertEquals(1, realm.query().count().find()) + var pojo: SimplePOJO = realm.query().first().find()!! + assertEquals(0, pojo.primaryKey) + assertEquals("SimplePOJO 0", pojo.stringField) + assertEquals(RealmAny.Type.STRING, pojo.nullableRealmAnyField?.type) + assertEquals("string any field", pojo.nullableRealmAnyField?.asString()) + + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 1" + nullableRealmAnyField = null + } + + insertToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + pojo = realm.query().first().find()!! + assertEquals("updated 1", pojo.stringField) + assertNull(pojo.nullableRealmAnyField) + + realm.writeBlocking { + val managedPojo = copyToRealm(SimplePOJO().apply { primaryKey = 1 }) + + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 2" + nullableRealmAnyField = RealmAny.create(managedPojo) + } + + insertToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + pojo = realm.query("primaryKey = 0").first().find()!! + assertEquals("updated 2", pojo.stringField) + assertEquals(RealmAny.Type.OBJECT, pojo.nullableRealmAnyField?.type) + assertEquals(1, pojo.nullableRealmAnyField?.asRealmObject()?.primaryKey) + + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 3" + nullableRealmAnyField = RealmAny.create( + realmListOf( + RealmAny.create("One"), + null, + RealmAny.create(17) + ) + ) // try embedded FIXME enabling this crash the test + } + + insertToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + pojo = realm.query("primaryKey = 0").first().find()!! + assertEquals("updated 3", pojo.stringField) + assertEquals(RealmAny.Type.LIST, pojo.nullableRealmAnyField?.type) + var asList: RealmList = pojo.nullableRealmAnyField?.asList()!! + assertEquals(3, asList.size) + assertEquals("One", asList[0]?.asString()) + assertNull(asList[1]) + assertEquals(17, asList[2]?.asInt()) + + // deeply nested list + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 4" + nullableRealmAnyField = + RealmAny.create( + realmListOf( + RealmAny.create("One"), null, RealmAny.create(17), + RealmAny.create( + realmListOf( + RealmAny.create("Level2"), + RealmAny.create( + realmListOf( + RealmAny.create(42), + null, + null, + RealmAny.create("Level2 String") + ) + ) + ) + ) + ) + ) + } + + insertToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + pojo = realm.query("primaryKey = 0").first().find()!! + assertEquals("updated 4", pojo.stringField) + assertEquals(RealmAny.Type.LIST, pojo.nullableRealmAnyField?.type) + asList = pojo.nullableRealmAnyField?.asList()!! // level 1 + assertEquals(4, asList.size) + assertEquals("One", asList[0]?.asString()) + assertNull(asList[1]) + assertEquals(17, asList[2]?.asInt()) + + assertEquals(RealmAny.Type.LIST, asList[3]?.type) + var nestedList1 = asList[3]?.asList()!! // level 1 + assertEquals(2, nestedList1.size) + assertEquals("Level2", nestedList1[0]?.asString()) + + assertEquals(RealmAny.Type.LIST, nestedList1[1]?.type) + var nestedList2 = nestedList1[1]?.asList()!! // level 2 + assertEquals(4, nestedList2.size) + assertEquals(42, nestedList2[0]?.asInt()) + assertNull(nestedList2[1]?.asInt()) + assertNull(nestedList2[2]?.asInt()) + assertEquals("Level2 String", nestedList2[3]?.asString()) + + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 5" + realmAnyListField = realmListOf( + RealmAny.create(10), + null, + RealmAny.create(true), + RealmAny.create( + realmListOf( + RealmAny.create("str1"), + RealmAny.create( + realmListOf( + RealmAny.create("Level2"), + RealmAny.create( + realmListOf( + RealmAny.create(42), + null, + null, + RealmAny.create("Level2 String") + ) + ) + ) + ) + ) + ) + ) + } + insertToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + pojo = realm.query("primaryKey = 0").first().find()!! + assertEquals("updated 5", pojo.stringField) + assertEquals(4, pojo.realmAnyListField.size) + + assertEquals(10, pojo.realmAnyListField[0]?.asInt()) + assertNull(pojo.realmAnyListField[1]) + assertTrue(pojo.realmAnyListField[2]?.asBoolean()!!) + assertEquals(RealmAny.Type.LIST, pojo.realmAnyListField[3]?.type) + nestedList1 = pojo.realmAnyListField[3]?.asList()!! + assertEquals(2, nestedList1.size) + assertEquals("str1", nestedList1[0]?.asString()) + assertEquals(RealmAny.Type.LIST, nestedList1[1]?.type) + nestedList2 = nestedList1[1]?.asList()!! + assertEquals(2, nestedList2.size) + + assertEquals("Level2", nestedList2[0]?.asString()) + assertEquals(RealmAny.Type.LIST, nestedList2[1]?.type) + val nestedList3 = nestedList2[1]?.asList()!! + assertEquals(4, nestedList3.size) + assertEquals(42, nestedList3[0]?.asInt()) + assertNull(nestedList3[1]) + assertNull(nestedList3[2]) + assertEquals("Level2 String", nestedList3[3]?.asString()) + } + + @Test + fun insertToRealmAnyDictionary() { + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 10 + nullableRealmAnyField = RealmAny.create( + realmDictionaryOf( + "d1" to RealmAny.create("One"), + "d2" to null, + "d3" to RealmAny.create(17) + ) + ) // try embedded + }) + } + val pojo = realm.query("primaryKey = 10").first().find()!! + assertEquals(3, pojo.nullableRealmAnyField?.asDictionary()?.size) + assertEquals("One", pojo.nullableRealmAnyField?.asDictionary()?.get("d1")?.asString()) + assertNull(pojo.nullableRealmAnyField?.asDictionary()?.get("d2")) + assertEquals(17, pojo.nullableRealmAnyField?.asDictionary()?.get("d3")?.asInt()) + } + + // FIXME this test will fail even when using copyToRealm + // apparently changing the content of RealmAny from a RealmList to primitive type crash + // E REALM : packages/external/core/src/realm/array.cpp:430: [realm-core-14.7.0] Assertion failed: ndx <= m_size + @Test + fun insertWithAnyBug() { + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 1" + nullableRealmAnyField = null + } + + copyToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + realm.writeBlocking { + val managedPojo = copyToRealm(SimplePOJO().apply { primaryKey = 1 }) + + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 2" + nullableRealmAnyField = RealmAny.create(managedPojo) + } + + copyToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + realm.writeBlocking { + val updatedPOJO = SimplePOJO().apply { + primaryKey = 0 +// nullableRealmAnyField = RealmAny.create(42) + nullableRealmAnyField = RealmAny.create(realmListOf()) +// nullableRealmAnyField = RealmAny.create(realmListOf(RealmAny.create("One"), null, RealmAny.create(17))) // FIXME enabling this crash the test + } + + copyToRealm(updatedPOJO, updatePolicy = UpdatePolicy.ALL) + } + + // The following transaction fails with : packages/external/core/src/realm/array.cpp:430: [realm-core-14.7.0] Assertion failed: ndx <= m_size + realm.writeBlocking { + copyToRealm(SimplePOJO().apply { +// primaryKey = 10 +// nullableRealmAnyField = RealmAny.create(realmDictionaryOf("d1" to RealmAny.create("One"), "d2" to null, "d3" to RealmAny.create(17))) // try embedded + }) + } + } + + @Test + fun insertToRealmSet() { + val simplePojo = SimplePOJO().apply { + primaryKey = 0 + stringField = "simplePojo" + realmSetField.addAll(arrayOf(1, 2, 3)) + + } + + realm.writeBlocking { + insertToRealm(simplePojo) + } + var first: SimplePOJO = realm.query().first().find()!! + assertEquals("simplePojo", first.stringField) + assertEquals(3, first.realmSetField.size) + assertEquals(1, first.realmSetField.elementAt(0)) + assertEquals(2, first.realmSetField.elementAt(1)) + assertEquals(3, first.realmSetField.elementAt(2)) + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0; stringField = "simplePojo update" + realmSetObjectField.addAll(arrayOf( + copyToRealm(SimplePOJO().apply { + primaryKey = 1; stringField = "simplePojo1" + }), + SimplePOJO().apply { primaryKey = 2; stringField = "simplePojo2" } + )) + }, updatePolicy = UpdatePolicy.ALL) + } + + first = realm.query().first().find()!! + assertEquals("simplePojo update", first.stringField) + assertEquals(2, first.realmSetObjectField.size) + assertEquals(1, first.realmSetObjectField.elementAt(0).primaryKey) + assertEquals("simplePojo1", first.realmSetObjectField.elementAt(0).stringField) + assertEquals(2, first.realmSetObjectField.elementAt(1).primaryKey) + assertEquals("simplePojo2", first.realmSetObjectField.elementAt(1).stringField) + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0 + stringField = "simplePojo update 2" + realmSetAnyField.addAll( + arrayOf( + RealmAny.create(42), + null, + RealmAny.create(SimplePOJO().apply { primaryKey = 3 }), +// RealmAny.create(realmListOf(RealmAny.create("One"), RealmAny.create("Two"))) TODO this should be supported + ) + ) + }, UpdatePolicy.ALL) + } + first = realm.query().first().find()!! + assertEquals("simplePojo update 2", first.stringField) + assertEquals(3, first.realmSetAnyField.size) + assertEquals(42, first.realmSetAnyField.elementAt(1)?.asInt()) + assertNull(first.realmSetAnyField.elementAt(0))// TODO it looks like RealmAny of null is stored at first position? set doesn't guarantee position + assertEquals( + 3, + first.realmSetAnyField.elementAt(2)?.asRealmObject()?.primaryKey + ) +// assertEquals(2, first.realmSetAnyField.elementAt(3)?.asList()?.size) +// assertEquals("One", first.realmSetAnyField.elementAt(3)?.asList()?.get(0)?.asString()) +// assertEquals("Two", first.realmSetAnyField.elementAt(3)?.asList()?.get(1)?.asString()) + } + + @Test + fun insertToRealmDictionary() { + val simplePojo = SimplePOJO().apply { + primaryKey = 0 + stringField = "simplePojo" + realmDictionaryField["k1"] = 1 + realmDictionaryField["k2"] = 2 + realmDictionaryField["k3"] = 3 + } + + realm.writeBlocking { + insertToRealm(simplePojo) + } + + var first: SimplePOJO = realm.query().first().find()!! + assertEquals("simplePojo", first.stringField) + assertEquals(3, first.realmDictionaryField.size) + assertEquals(3, first.realmDictionaryField["k3"]) + assertEquals(1, first.realmDictionaryField["k1"]) + assertEquals(2, first.realmDictionaryField["k2"]) + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0 + stringField = "updated" + realmDictionaryObjectField["key1"] = SimplePOJO().apply { primaryKey = 1 } + realmDictionaryObjectField["key2"] = null + realmDictionaryObjectField["key3"] = + copyToRealm(SimplePOJO().apply { primaryKey = 2 }) + }, updatePolicy = UpdatePolicy.ALL) + } + + first = realm.query().first().find()!! + assertEquals("updated", first.stringField) + assertEquals(3, first.realmDictionaryObjectField.size) + assertEquals(1, first.realmDictionaryObjectField["key1"]?.primaryKey) + assertNull(first.realmDictionaryObjectField["key2"]) + assertEquals(2, first.realmDictionaryObjectField["key3"]?.primaryKey) + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0 + stringField = "updated 2" + realmDictionaryEmbeddedField["key1"] = SimpleEmbedded().apply { id = "e1" } + realmDictionaryEmbeddedField["key2"] = null + realmDictionaryEmbeddedField["key3"] = copyToRealm(SimplePOJO().apply { + primaryKey = 3; embedded = SimpleEmbedded().apply { id = "e2" } + }).embedded + }, updatePolicy = UpdatePolicy.ALL) + } + + first = realm.query().first().find()!! + assertEquals("updated 2", first.stringField) + assertEquals(3, first.realmDictionaryEmbeddedField.size) + assertEquals("e1", first.realmDictionaryEmbeddedField["key1"]?.id) + assertNull(first.realmDictionaryEmbeddedField["key2"]) + assertEquals("e2", first.realmDictionaryEmbeddedField["key3"]?.id) + + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0 + stringField = "updated RealmAny" + realmDictionaryAnyField["key_Primitive"] = RealmAny.create(42) + realmDictionaryAnyField["key_Null"] = null + realmDictionaryAnyField["key_RealmList"] = + RealmAny.create(realmListOf(RealmAny.create("One"), null, RealmAny.create(19))) + realmDictionaryAnyField["key_RealmObject"] = + RealmAny.create(copyToRealm(SimplePOJO().apply { primaryKey = 4 })) + realmDictionaryAnyField["key_RealmDictionary"] = RealmAny.create( + realmDictionaryOf( + "d1" to RealmAny.create(7), + "d2" to null, + "d3" to RealmAny.create("foo") + ) + ) + }, updatePolicy = UpdatePolicy.ALL) + } + + first = realm.query().first().find()!! + assertEquals("updated RealmAny", first.stringField) + assertEquals(5, first.realmDictionaryAnyField.size) + + assertEquals(42, first.realmDictionaryAnyField["key_Primitive"]?.asInt()) + + assertNull(first.realmDictionaryAnyField["key_Null"]) + + assertEquals(3, first.realmDictionaryAnyField["key_RealmList"]?.asList()?.size) + assertEquals( + "One", + first.realmDictionaryAnyField["key_RealmList"]?.asList()?.get(0)?.asString() + ) + assertNull(first.realmDictionaryAnyField["key_RealmList"]?.asList()?.get(1)) + assertEquals(19, first.realmDictionaryAnyField["key_RealmList"]?.asList()?.get(2)?.asInt()) + + assertEquals( + 4, + first.realmDictionaryAnyField["key_RealmObject"]?.asRealmObject()?.primaryKey + ) + + assertEquals(3, first.realmDictionaryAnyField["key_RealmDictionary"]?.asDictionary()?.size) + assertEquals( + 7, + first.realmDictionaryAnyField["key_RealmDictionary"]?.asDictionary()?.get("d1")?.asInt() + ) + assertNull(first.realmDictionaryAnyField["key_RealmDictionary"]?.asDictionary()?.get("d2")) + assertEquals( + "foo", + first.realmDictionaryAnyField["key_RealmDictionary"]?.asDictionary()?.get("d3") + ?.asString() + ) + + realm.writeBlocking { + insertToRealm(SimplePOJO().apply { + primaryKey = 0 + nullableRealmAnyField = RealmAny.create( + realmDictionaryOf( + "d1" to RealmAny.create("One"), + "d2" to null, + "d3" to RealmAny.create(17) + ) + ) // try embedded + }, updatePolicy = UpdatePolicy.ALL) + } + val pojo = realm.query("primaryKey = 0").first().find()!! + assertEquals(3, pojo.nullableRealmAnyField?.asDictionary()?.size) + assertEquals("One", pojo.nullableRealmAnyField?.asDictionary()?.get("d1")?.asString()) + assertNull(pojo.nullableRealmAnyField?.asDictionary()?.get("d2")) + assertEquals(17, pojo.nullableRealmAnyField?.asDictionary()?.get("d3")?.asInt()) + } +} + +class SimpleEmbedded : EmbeddedRealmObject { + var id: String? = "" + var link: SimplePOJO? = null +} + +class SimplePOJO : RealmObject { + @PrimaryKey + var primaryKey: Long = 42L + var stringField: String = "Realm" + var parent: SimplePOJO? = null + var embedded: SimpleEmbedded? = null + var nullableRealmAnyField: RealmAny? = null + + + // var stringListField: RealmList = realmListOf() +// var byteListField: RealmList = realmListOf() +// var charListField: RealmList = realmListOf() +// var shortListField: RealmList = realmListOf() +// var intListField: RealmList = realmListOf() +// var longListField: RealmList = realmListOf() +// var booleanListField: RealmList = realmListOf() +// var floatListField: RealmList = realmListOf() +// var doubleListField: RealmList = realmListOf() +// var timestampListField: RealmList = realmListOf() +// var bsonObjectIdListField: RealmList = realmListOf() +// var binaryListField: RealmList = realmListOf() + var objectListField: RealmList = realmListOf() + var embeddedObjectListField: RealmList = realmListOf() + var realmAnyListField: RealmList = realmListOf() + var nullableStringListField: RealmList = realmListOf() + + var realmSetField: RealmSet = realmSetOf() + var realmSetObjectField: RealmSet = realmSetOf() + var realmSetAnyField: RealmSet = realmSetOf() + + var realmDictionaryField: RealmDictionary = realmDictionaryOf() + var realmDictionaryObjectField: RealmDictionary = realmDictionaryOf() + var realmDictionaryEmbeddedField: RealmDictionary = realmDictionaryOf() + var realmDictionaryAnyField: RealmDictionary = realmDictionaryOf() +// + +// var nullableByteListField: RealmList = realmListOf() +// var nullableCharListField: RealmList = realmListOf() +// var nullableShortListField: RealmList = realmListOf() +// var nullableIntListField: RealmList = realmListOf() +// var nullableLongListField: RealmList = realmListOf() +// var nullableBooleanListField: RealmList = realmListOf() +// var nullableFloatListField: RealmList = realmListOf() +// var nullableDoubleListField: RealmList = realmListOf() +// var nullableTimestampListField: RealmList = realmListOf() +// var nullableBsonObjectIdListField: RealmList = realmListOf() +// var nullableBinaryListField: RealmList = realmListOf() +}