From 78869a166b1f83f79328d0b0c758d40006a4cc01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Mon, 5 Feb 2024 13:36:44 +0100 Subject: [PATCH 1/3] Additional tests of collections in mixed behavior --- .../common/RealmAnyNestedCollectionTests.kt | 9 ++ .../RealmAnyNestedListNotificationTest.kt | 96 +++++++++++++++++++ .../RealmListNotificationsTests.kt | 77 +++++++++++++++ 3 files changed, 182 insertions(+) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt index d377684f94..9224c1a601 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt @@ -573,6 +573,9 @@ class RealmAnyNestedCollectionTests { realm.query("value[*] == 4").find().single().run { assertEquals("LIST", id) } + realm.query("value[*] == {4, 5, 6}").find().single().run { + assertEquals("LIST", id) + } // Matching dictionaries realm.query("value.key1 == 7").find().single().run { @@ -606,5 +609,11 @@ class RealmAnyNestedCollectionTests { realm.query("value[*].key3[0] == 9").find().single().run { assertEquals("EMBEDDED", id) } + realm.query("value[0][*] == {4, 5, 6}").find().single().run { + assertEquals("EMBEDDED", id) + } + 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/notifications/RealmAnyNestedListNotificationTest.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt index e2858f7fbd..0176c7f0d0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmAnyNestedListNotificationTest.kt @@ -19,6 +19,8 @@ package io.realm.kotlin.test.common.notifications import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.JsonStyleRealmObject +import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.realmAnyDictionaryOf import io.realm.kotlin.ext.realmAnyListOf import io.realm.kotlin.ext.realmAnyOf import io.realm.kotlin.internal.platform.runBlocking @@ -30,6 +32,7 @@ import io.realm.kotlin.test.common.utils.DeletableEntityNotificationTests import io.realm.kotlin.test.common.utils.FlowableTests import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail import io.realm.kotlin.types.RealmAny import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -252,4 +255,97 @@ class RealmAnyNestedListNotificationTest : FlowableTests, DeletableEntityNotific override fun closeRealmInsideFlowThrows() { TODO("Not yet implemented") } + + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + RealmAny.create(JsonStyleRealmObject().apply { id = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asRealmObject().value = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals( + "TEST", + it.list[0]!!.asRealmObject().value!!.asString() + ) + } + + listener.cancel() + } + } + + @Test + fun eventsOnDictionaryChangesInRealmAnyList() { + kotlinx.coroutines.runBlocking { + val channel = Channel>(10) + val parent = + realm.write { + copyToRealm(JsonStyleRealmObject().apply { value = realmAnyListOf() }) + } + + val listener = async { + parent.value!!.asList().asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + val asList = findLatest(parent)!!.value!!.asList() + println(asList.size) + asList.add( + realmAnyDictionaryOf( + "key1" to "value1" + ) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals(RealmAny.Type.DICTIONARY, it.list[0]!!.type) + } + + realm.write { + findLatest(parent)!!.value!!.asList()[0]!!.asDictionary()["key1"] = + RealmAny.create("TEST") + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asDictionary()["key1"]!!.asString()) + } + + listener.cancel() + } + } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt index c7f67ab7b4..da7e69c462 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/RealmListNotificationsTests.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.list.RealmListContainer import io.realm.kotlin.entities.list.listTestSchema +import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.notifications.DeletedList import io.realm.kotlin.notifications.InitialList @@ -35,6 +36,8 @@ import io.realm.kotlin.test.common.utils.assertIsChangeSet import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestChannel import io.realm.kotlin.test.util.receiveOrFail +import io.realm.kotlin.test.util.trySendOrFail +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmList import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -734,6 +737,80 @@ class RealmListNotificationsTests : RealmEntityNotificationTests { } } + @Test + fun eventsOnObjectChangesInList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.objectListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.objectListField.add( + RealmListContainer().apply { stringField = "CHILD" } + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.objectListField[0].stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0].stringField) + } + + listener.cancel() + } + } + @Test + @Ignore // https://github.com/realm/realm-core/issues/7264 + fun eventsOnObjectChangesInRealmAnyList() { + runBlocking { + val channel = Channel>(10) + val parent = realm.write { copyToRealm(RealmListContainer()).apply { stringField = "PARENT" } } + + val listener = async { + parent.nullableRealmAnyListField.asFlow().collect { + channel.trySendOrFail(it) + } + } + + channel.receiveOrFail(message = "Initial event").let { assertIs>(it) } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField.add( + RealmAny.create(RealmListContainer().apply { stringField = "CHILD" }) + ) + } + channel.receiveOrFail(message = "List add").let { + assertIs>(it) + assertEquals(1, it.list.size) + } + + realm.write { + findLatest(parent)!!.nullableRealmAnyListField[0]!!.asRealmObject().stringField = "TEST" + } + channel.receiveOrFail(message = "Object updated").let { + assertIs>(it) + assertEquals(1, it.list.size) + assertEquals("TEST", it.list[0]!!.asRealmObject().stringField) + } + + listener.cancel() + } + } + fun RealmList<*>.removeRange(range: IntRange) { range.reversed().forEach { index -> removeAt(index) } } From f3e6425e86c1f5984090594203b9c77913349962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 27 Feb 2024 14:07:40 +0100 Subject: [PATCH 2/3] Optimize RealmList indexOf, remove and contains --- .../kotlin/internal/interop/RealmInterop.kt | 1 + .../kotlin/internal/interop/RealmInterop.kt | 12 ++++ .../kotlin/internal/interop/RealmInterop.kt | 13 +++++ .../kotlin/internal/RealmListInternal.kt | 57 +++++++++++++++++++ .../kotlin/test/common/RealmListTests.kt | 39 ------------- 5 files changed, 83 insertions(+), 39 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 8436afd07c..bba7bf284f 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 @@ -317,6 +317,7 @@ expect object RealmInterop { fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue + fun realm_list_find(list: RealmListPointer, value: RealmValue): Long fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer fun realm_list_get_dictionary(list: RealmListPointer, index: Long): RealmMapPointer fun realm_list_add(list: RealmListPointer, index: Long, transport: RealmValue) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index c02008f496..914ce5ad1e 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -568,6 +568,18 @@ actual object RealmInterop { realmc.realm_list_get(list.cptr(), index, struct) return RealmValue(struct) } + + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + val index = LongArray(1) + val found = BooleanArray(1) + realmc.realm_list_find(list.cptr(), value.value, index, found) + return if (found[0]) { + index[0] + } else { + -1 + } + } + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index)) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 3ee83ed8b7..22ee5d19d7 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1038,6 +1038,19 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long { + memScoped { + val index = alloc() + val found = alloc() + checkedBooleanResult(realm_wrapper.realm_list_find(list.cptr(), value.value.readValue(), index.ptr, found.ptr)) + return if (found.value) { + index.value.toLong() + } else { + -1 + } + } + } + actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer = CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong())) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt index 9446ec4817..cbcb4dad22 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt @@ -20,6 +20,7 @@ import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.Versioned import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.isManaged import io.realm.kotlin.internal.RealmValueArgumentConverter.convertToQueryArgs import io.realm.kotlin.internal.interop.Callback import io.realm.kotlin.internal.interop.ClassKey @@ -90,10 +91,23 @@ internal class ManagedRealmList( return operator.get(index) } + override fun contains(element: E): Boolean { + return operator.contains(element) + } + + override fun indexOf(element: E): Int { + return operator.indexOf(element) + } + + override fun add(index: Int, element: E) { operator.insert(index, element) } + override fun remove(element: E): Boolean { + return operator.remove(element) + } + // We need explicit overrides of these to ensure that we capture duplicate references to the // same unmanaged object in our internal import caching mechanism override fun addAll(elements: Collection): Boolean = operator.insertAll(size, elements) @@ -225,6 +239,10 @@ internal interface ListOperator : CollectionOperator { fun get(index: Int): E + fun contains(element: E): Boolean = indexOf(element) != -1 + + fun indexOf(element: E): Int + // TODO OPTIMIZE We technically don't need update policy and cache for primitive lists but right now RealmObjectHelper.assign doesn't know how to differentiate the calls to the operator fun insert( index: Int, @@ -233,6 +251,14 @@ internal interface ListOperator : CollectionOperator { cache: UnmanagedToManagedObjectCache = mutableMapOf() ) + fun remove(element: E): Boolean = when (val index = indexOf(element)) { + -1 -> false + else -> { + RealmInterop.realm_list_erase(nativePointer, index.toLong()) + true + } + } + fun insertAll( index: Int, elements: Collection, @@ -277,6 +303,14 @@ internal class PrimitiveListOperator( } } + override fun indexOf(element: E): Int { + inputScope { + with(realmValueConverter) { + return RealmInterop.realm_list_find(nativePointer, publicToRealmValue(element)).toInt() + } + } + } + override fun insert( index: Int, element: E, @@ -354,6 +388,17 @@ internal class RealmAnyListOperator( } } + override fun indexOf(element: RealmAny?): Int { + // Unmanaged objects are never found in a managed collections + if (element?.type == RealmAny.Type.OBJECT) { + if (!element.asRealmObject().isManaged()) return -1 + } + return inputScope { + val transport = realmAnyToRealmValueWithoutImport(element) + RealmInterop.realm_list_find(nativePointer, transport).toInt() + } + } + override fun insert( index: Int, element: RealmAny?, @@ -461,6 +506,18 @@ internal abstract class BaseRealmObjectListOperator ( realmValueToRealmObject(transport, clazz, mediator, realmReference) as E } } + + override fun indexOf(element: E): Int { + // Unmanaged objects are never found in a managed collections + element?.also { + if (!(it as RealmObjectInternal).isManaged()) return -1 + } + return inputScope { + val objRef = realmObjectToRealmReferenceOrError(element as BaseRealmObject?) + val transport = realmObjectTransport(objRef as RealmObjectInterop) + RealmInterop.realm_list_find(nativePointer, transport).toInt() + } + } } internal class RealmObjectListOperator( diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index 9ec49c4185..3103be611c 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -722,13 +722,6 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { ), classifier ) - ByteArray::class -> ByteArrayListTester( - realm = realm, - typeSafetyManager = getTypeSafety( - classifier, - elementType.nullable - ) as ListTypeSafetyManager - ) RealmAny::class -> RealmAnyListTester( realm = realm, typeSafetyManager = ListTypeSafetyManager( @@ -1368,38 +1361,6 @@ internal class RealmObjectListTester( assertEquals(expected.stringField, actual.stringField) } -/** - * Check equality for ByteArrays at a structural level with `assertContentEquals`. - */ -internal class ByteArrayListTester( - realm: Realm, - typeSafetyManager: ListTypeSafetyManager -) : ManagedListTester(realm, typeSafetyManager, ByteArray::class) { - override fun assertElementsAreEqual(expected: ByteArray?, actual: ByteArray?) = - assertContentEquals(expected, actual) - - // Removing elements using equals/hashcode will fail for byte arrays since they are - // are only equal if identical - override fun remove() { - val dataSet = typeSafetyManager.dataSetToLoad - val assertions = { list: RealmList -> - assertFalse(list.isEmpty()) - } - - errorCatcher { - realm.writeBlocking { - val list = typeSafetyManager.createContainerAndGetCollection(this) - assertFalse(list.remove(dataSet[0])) - assertTrue(list.add(dataSet[0])) - assertFalse(list.remove(list.last())) - assertions(list) - } - } - - assertListAndCleanup { list -> assertions(list) } - } -} - // ----------------------------------- // Data used to initialize structures // ----------------------------------- From 4d63177942eab6c46d5740fc62751e17a6151aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Thu, 29 Feb 2024 14:30:19 +0100 Subject: [PATCH 3/3] Consistent equals semantics on collection types --- .../kotlin/internal/RealmListInternal.kt | 19 +++++-- .../realm/kotlin/internal/RealmMapInternal.kt | 25 +++++---- .../kotlin/internal/RealmObjectHelper.kt | 11 ++-- .../realm/kotlin/internal/RealmSetInternal.kt | 19 +++++-- .../realm/kotlin/test/common/RealmAnyTests.kt | 16 ++++-- .../test/common/RealmDictionaryTests.kt | 39 +++++++++++--- .../kotlin/test/common/RealmListTests.kt | 53 ++++++++++++++++++- .../realm/kotlin/test/common/RealmSetTests.kt | 29 +++++++++- .../kotlin/test/common/SerializationTests.kt | 12 +++++ .../test/mongodb/common/FunctionsTests.kt | 5 ++ 10 files changed, 188 insertions(+), 40 deletions(-) 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 cbcb4dad22..616117b1e3 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 @@ -66,9 +66,7 @@ internal class UnmanagedRealmList( override fun toString(): String = "UnmanagedRealmList{${joinToString()}}" - override fun equals(other: Any?): Boolean = backingList == other - override fun hashCode(): Int = backingList.hashCode() } /** @@ -170,6 +168,19 @@ internal class ManagedRealmList( !nativePointer.isReleased() && RealmInterop.realm_list_is_valid(nativePointer) override fun delete() = RealmInterop.realm_list_remove_all(nativePointer) + + override fun equals(other: Any?): Boolean { + val o = other as? ManagedRealmList<*> + return when (o) { + null -> false + else -> RealmInterop.realm_equals(nativePointer, o.nativePointer) + } + } + + override fun hashCode(): Int { + // TODO Improve distribution. Maybe something like parent.table + parent.ref + parent.key + version + return operator.realmReference.version().version.hashCode() + } } internal class RealmListChangeFlow(producerScope: ProducerScope>) : @@ -330,7 +341,7 @@ internal class PrimitiveListOperator( index: Int, element: E, updatePolicy: UpdatePolicy, - cache: UnmanagedToManagedObjectCache + cache: UnmanagedToManagedObjectCache, ): E { return get(index).also { inputScope { @@ -344,7 +355,7 @@ internal class PrimitiveListOperator( override fun copy( realmReference: RealmReference, - nativePointer: RealmListPointer + nativePointer: RealmListPointer, ): ListOperator = PrimitiveListOperator(mediator, realmReference, realmValueConverter, nativePointer) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt index 9ee69b254d..4c785a01dd 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 @@ -769,18 +769,6 @@ internal class UnmanagedRealmDictionary( override fun toString(): String = entries.joinToString { (key, value) -> "[$key,$value]" } .let { "UnmanagedRealmDictionary{$it}" } - override fun equals(other: Any?): Boolean { - if (other !is RealmDictionary<*>) return false - if (this === other) return true - if (this.size == other.size && this.entries.containsAll(other.entries)) return true - return false - } - - override fun hashCode(): Int { - var result = size.hashCode() - result = 31 * result + entries.hashCode() - return result - } } internal class ManagedRealmDictionary constructor( @@ -819,7 +807,18 @@ internal class ManagedRealmDictionary constructor( return "RealmDictionary{size=$size,owner=$owner,objKey=$objKey,version=$version}" } - // TODO add equals and hashCode when https://github.com/realm/realm-kotlin/issues/1097 is fixed + override fun equals(other: Any?): Boolean { + val o = other as? ManagedRealmMap<*, *> + return when (o) { + null -> false + else -> RealmInterop.realm_equals(nativePointer, o.nativePointer) + } + } + + override fun hashCode(): Int { + // TODO Improve distribution. Maybe something like parent.table + parent.ref + parent.key + version + return operator.realmReference.version().version.hashCode() + } } internal class RealmDictonaryChangeFlow(scope: ProducerScope>) : diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt index d844026595..d575bb59fc 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 @@ -1235,14 +1235,15 @@ internal object RealmObjectHelper { if (other == null || obj::class != other::class) return false other as BaseRealmObject - - if (other.isManaged()) { - if (obj.isValid() != other.isValid()) return false - return obj.getIdentifierOrNull() == other.getIdentifierOrNull() + return if (obj.isManaged() && other.isManaged()) { + RealmInterop.realm_equals( + obj.realmObjectReference!!.objectPointer, + other.realmObjectReference!!.objectPointer + ) } else { // If one of the objects are unmanaged, they are only equal if identical, which // should have been caught at the top of this function. - return false + false } } 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 6f78d00c82..31685535c2 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 @@ -67,9 +67,7 @@ public class UnmanagedRealmSet( override fun toString(): String = "UnmanagedRealmSet{${joinToString()}}" - override fun equals(other: Any?): Boolean = backingSet == other - override fun hashCode(): Int = backingSet.hashCode() } /** @@ -205,6 +203,19 @@ internal class ManagedRealmSet constructor( override fun isValid(): Boolean { return !nativePointer.isReleased() && RealmInterop.realm_set_is_valid(nativePointer) } + + override fun equals(other: Any?): Boolean { + val o = other as? ManagedRealmSet<*> + return when (o) { + null -> false + else -> RealmInterop.realm_equals(nativePointer, o.nativePointer) + } + } + + override fun hashCode(): Int { + // TODO Improve distribution. Maybe something like parent.table + parent.ref + parent.key + version + return operator.realmReference.version().version.hashCode() + } } internal fun ManagedRealmSet.query( @@ -345,8 +356,8 @@ internal class RealmAnySetOperator( transport, null, mediator, realmReference, issueDynamicObject, issueDynamicMutableObject, - { error("Set should never container lists") } - ) { error("Set should never container dictionaries") } + { error("Set should never contain lists") } + ) { error("Set should never contain dictionaries") } } } 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 679aeab20e..37bf117cf5 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,7 @@ import io.realm.kotlin.entities.embedded.EmbeddedParent import io.realm.kotlin.entities.embedded.embeddedSchema import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmAnyListOf import io.realm.kotlin.ext.realmDictionaryOf import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.ext.realmSetOf @@ -489,9 +490,18 @@ 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 -> {} + RealmAny.Type.LIST -> { + val value = realmAnyListOf(1, "String", Sample()) + // Lists are only comparing equal on referential equality + assertNotEquals(value, realmAnyListOf(1, "String", Sample())) + assertEquals(value, value) + } + RealmAny.Type.DICTIONARY -> { + val value = realmDictionaryOf("key1" to "String", "key2" to Sample()) + // Lists are only comparing equal on referential equality + assertNotEquals(value, realmDictionaryOf("key1" to "String", "key2" to Sample())) + assertEquals(value, value) + } } } } 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 8507eb24d3..8871a506b5 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 @@ -1108,11 +1108,36 @@ class RealmDictionaryTests : EmbeddedObjectCollectionQueryTests { } @Test - @Ignore - fun managedDictionary_equals() { - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // When we fix this issue we will be able to compare two dictionaries' parent objects based - // on the Realm version, object key and class key making equality logic superfluous + fun managedDictionary_equals() = runBlocking { + val expected = mapOf( + "key1" to 1, + "key2" to 2 + ) + val frozen1 = realm.write { + val liveObject = + copyToRealm(RealmDictionaryContainer().apply { intDictionaryField.putAll(expected) }) + + assertNotEquals(liveObject.intDictionaryField, mapOf("key1" to 1, "key2" to 2)) + liveObject.intDictionaryField.let { + assertEquals(expected.size, it.size) + expected.entries.containsAll(it.entries) + } + + assertTrue(liveObject.intDictionaryField.equals(liveObject.intDictionaryField)) // Same version + + assertEquals(1, liveObject.intDictionaryField["key1"]) + liveObject + } + assertTrue(frozen1.intDictionaryField.equals(frozen1.intDictionaryField)) // Same version + + val frozen2 = realm.write { + findLatest(frozen1)?.also { it.stringField = "UPDATED" } + } + frozen2!!.intDictionaryField.let { + assertEquals(frozen1.intDictionaryField.size, it.size) + frozen1.intDictionaryField.entries.containsAll(it.entries) + } + assertFalse { frozen1.intDictionaryField.equals(frozen2!!.intDictionaryField) } } @Test @@ -2836,7 +2861,7 @@ internal abstract class ManagedDictionaryTester( val unmanaged2 = realmDictionaryOf(dataSet) val unmanaged3 = realmDictionaryOf(dataSet).apply { remove(dataSet[0].first) } assertEquals(unmanaged1, unmanaged1) - assertEquals(unmanaged1, unmanaged2) + assertNotEquals(unmanaged1, unmanaged2) assertNotEquals(unmanaged1, unmanaged3) } } @@ -2848,7 +2873,7 @@ internal abstract class ManagedDictionaryTester( val unmanaged2 = realmDictionaryOf(dataSet) val unmanaged3 = realmDictionaryOf(dataSet).apply { remove(dataSet[0].first) } assertEquals(unmanaged1.hashCode(), unmanaged1.hashCode()) - assertEquals(unmanaged1.hashCode(), unmanaged2.hashCode()) + assertNotEquals(unmanaged1.hashCode(), unmanaged2.hashCode()) assertNotEquals(unmanaged1.hashCode(), unmanaged3.hashCode()) } } 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 3103be611c..75b1bb5763 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 @@ -67,6 +67,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail @@ -114,8 +115,11 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { @Test fun unmanagedRealmList_equalsHash() { - assertEquals(realmListOf("1", "2"), realmListOf("1", "2")) - assertEquals(realmListOf("1", "2").hashCode(), realmListOf("1", "2").hashCode()) + val list = realmListOf("1", "2") + assertEquals(list, list) + assertNotEquals(realmListOf("1", "2"), list) + assertEquals(list.hashCode(), list.hashCode()) + assertNotEquals(realmListOf("1", "2").hashCode(), list.hashCode()) } @Test @@ -632,6 +636,51 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { Unit } + @Test + fun isEquals() = runBlocking { + val frozen1 = realm.write { + val liveObject = copyToRealm(RealmListContainer().apply { intListField.addAll(listOf(1, 2, 3, 4)) }) + + assertContentEquals(liveObject.intListField, listOf(1,2, 3, 4)) + + assertEquals(liveObject.intListField, liveObject.intListField) // Same version + assertEquals(liveObject.intListField.hashCode(), liveObject.intListField.hashCode()) // Same version + + assertEquals(1, liveObject.intListField.indexOf(2)) + liveObject + } + val frozen2 = realm.write { + findLatest(frozen1)?.also { it.stringField = "UPDATED" } + } + assertContentEquals(frozen1.intListField, frozen2!!.intListField) + assertFalse { frozen1.intListField.equals(frozen2!!.intListField) } + } + + @Test + fun contains() = runBlocking { + realm.write { + val liveObject = copyToRealm(RealmListContainer().apply { intListField.addAll(listOf(1, 2, 3, 4)) }) + assertTrue(liveObject.intListField.contains(2)) + assertEquals(1, liveObject.intListField.indexOf(2)) + } + } + + @Test + fun contains_managed() = runBlocking { + realm.write { + val liveObject = copyToRealm( + RealmListContainer().apply { + stringField = "PARENT" + objectListField.add(RealmListContainer().apply { stringField = "CHILD" }) + nullableRealmAnyListField.add(RealmAny.create(RealmListContainer().apply { stringField = "ANYCHILD" })) + } + ) + assertEquals(3, query().find().size) + assertTrue(liveObject.objectListField.contains(liveObject.objectListField[0])) + assertTrue(liveObject.nullableRealmAnyListField.contains(liveObject.nullableRealmAnyListField[0])) + } + } + @Test fun contains_unmanagedArgs() = runBlocking { val frozenObject = realm.write { 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 94c34d7a89..a8d6760380 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 @@ -21,6 +21,7 @@ import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.SampleWithPrimaryKey +import io.realm.kotlin.entities.list.RealmListContainer import io.realm.kotlin.entities.set.RealmSetContainer import io.realm.kotlin.ext.asRealmObject import io.realm.kotlin.ext.query @@ -60,6 +61,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail @@ -154,8 +156,12 @@ class RealmSetTests : CollectionQueryTests { @Test fun unmanagedRealmSet_equalsHash() { - assertEquals(realmSetOf("1", "2"), realmSetOf("1", "2")) - assertEquals(realmSetOf("1", "2").hashCode(), realmSetOf("1", "2").hashCode()) + val set = realmSetOf("1", "2") + assertEquals(set, set) + assertNotEquals(realmSetOf("1", "2"), set) + assertEquals(set.hashCode(), set.hashCode()) + assertNotEquals(realmSetOf("1", "2").hashCode(), set.hashCode()) + assertNotEquals(realmSetOf("1", "2").hashCode(), realmSetOf("1", "2").hashCode()) } @Test @@ -653,6 +659,25 @@ class RealmSetTests : CollectionQueryTests { assertFalse(frozenObject.nullableRealmAnySetField.contains(RealmAny.create(RealmSetContainer()))) } + @Test + fun isEquals() = runBlocking { + val frozen1 = realm.write { + val liveObject = copyToRealm(RealmSetContainer().apply { intSetField.addAll(listOf(1, 2, 3, 4)) }) + + assertContentEquals(liveObject.intSetField, listOf(1,2, 3, 4)) + + assertTrue(liveObject.intSetField.equals(liveObject.intSetField)) // Same version + + assertEquals(1, liveObject.intSetField.indexOf(2)) + liveObject + } + val frozen2 = realm.write { + findLatest(frozen1)?.also { it.stringField = "UPDATED" } + } + assertNotEquals(frozen1.intSetField, frozen2!!.intSetField) // == + assertFalse { frozen1.intSetField.equals(frozen2!!.intSetField) } + } + private fun getCloseableRealm(): Realm = RealmConfiguration.Builder(schema = setOf(RealmSetContainer::class)) .directory(tmpDir) 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 580c8f2a1f..1f311026a9 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 @@ -254,6 +254,18 @@ class SerializationTests { expected.asRealmObject() ) } + // List and Dictionary does not compare equals with RealmAny.equals unless + // it is referential equality, so iterate manually + RealmAny.Type.LIST -> { + expected.asList().zip(actual.asList()).forEach { (a, b): Pair -> + assertEquals(a, b) + } + } + RealmAny.Type.DICTIONARY -> { + expected.asDictionary().entries.zip(actual.asDictionary().entries).forEach { (a, b) -> + assertEquals(a, b) + } + } else -> assertEquals(expected, actual) } } else if (expected != null || actual != null) { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt index cfd87265a1..f8151bcfdf 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt @@ -784,6 +784,11 @@ class FunctionsTests { actual as Iterable<*> assertContentEquals(expected, actual) } + is Map<*, *> -> { + actual is Map<*, *> + assertEquals(expected.size, (actual as Map<*, *>).size) + assertTrue { expected.entries.containsAll(actual.entries) } + } else -> assertEquals(expected, actual) } }