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) } }