Skip to content

Commit

Permalink
Consistent equals semantics on collection types
Browse files Browse the repository at this point in the history
  • Loading branch information
rorbech committed Feb 29, 2024
1 parent f3e6425 commit 4d63177
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ internal class UnmanagedRealmList<E>(

override fun toString(): String = "UnmanagedRealmList{${joinToString()}}"

Check failure on line 68 in packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt

View workflow job for this annotation

GitHub Actions / Ktlint Results

no-blank-line-before-rbrace

Unexpected blank line(s) before "}"
override fun equals(other: Any?): Boolean = backingList == other

Check failure on line 69 in packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmListInternal.kt

View workflow job for this annotation

GitHub Actions / Ktlint Results

no-consecutive-blank-lines

Needless blank line(s)
override fun hashCode(): Int = backingList.hashCode()
}

/**
Expand Down Expand Up @@ -170,6 +168,19 @@ internal class ManagedRealmList<E>(
!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<E>(producerScope: ProducerScope<ListChange<E>>) :
Expand Down Expand Up @@ -330,7 +341,7 @@ internal class PrimitiveListOperator<E>(
index: Int,
element: E,
updatePolicy: UpdatePolicy,
cache: UnmanagedToManagedObjectCache
cache: UnmanagedToManagedObjectCache,
): E {
return get(index).also {
inputScope {
Expand All @@ -344,7 +355,7 @@ internal class PrimitiveListOperator<E>(

override fun copy(
realmReference: RealmReference,
nativePointer: RealmListPointer
nativePointer: RealmListPointer,
): ListOperator<E> =
PrimitiveListOperator(mediator, realmReference, realmValueConverter, nativePointer)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -769,18 +769,6 @@ internal class UnmanagedRealmDictionary<V>(
override fun toString(): String = entries.joinToString { (key, value) -> "[$key,$value]" }
.let { "UnmanagedRealmDictionary{$it}" }

Check failure on line 771 in packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmMapInternal.kt

View workflow job for this annotation

GitHub Actions / Ktlint Results

no-blank-line-before-rbrace

Unexpected blank line(s) before "}"
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<V> constructor(
Expand Down Expand Up @@ -819,7 +807,18 @@ internal class ManagedRealmDictionary<V> 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<V>(scope: ProducerScope<MapChange<String, V>>) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ public class UnmanagedRealmSet<E>(

override fun toString(): String = "UnmanagedRealmSet{${joinToString()}}"

Check failure on line 69 in packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt

View workflow job for this annotation

GitHub Actions / Ktlint Results

no-blank-line-before-rbrace

Unexpected blank line(s) before "}"
override fun equals(other: Any?): Boolean = backingSet == other

Check failure on line 70 in packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmSetInternal.kt

View workflow job for this annotation

GitHub Actions / Ktlint Results

no-consecutive-blank-lines

Needless blank line(s)
override fun hashCode(): Int = backingSet.hashCode()
}

/**
Expand Down Expand Up @@ -205,6 +203,19 @@ internal class ManagedRealmSet<E> 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 <E : BaseRealmObject> ManagedRealmSet<E>.query(
Expand Down Expand Up @@ -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") }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2836,7 +2861,7 @@ internal abstract class ManagedDictionaryTester<T>(
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)
}
}
Expand All @@ -2848,7 +2873,7 @@ internal abstract class ManagedDictionaryTester<T>(
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())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -632,6 +636,51 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests {
Unit
}

@Test
fun isEquals() = runBlocking<Unit> {
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<Unit> {
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<Unit> {
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<RealmListContainer>().find().size)
assertTrue(liveObject.objectListField.contains(liveObject.objectListField[0]))
assertTrue(liveObject.nullableRealmAnyListField.contains(liveObject.nullableRealmAnyListField[0]))
}
}

@Test
fun contains_unmanagedArgs() = runBlocking<Unit> {
val frozenObject = realm.write {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -653,6 +659,25 @@ class RealmSetTests : CollectionQueryTests {
assertFalse(frozenObject.nullableRealmAnySetField.contains(RealmAny.create(RealmSetContainer())))
}

@Test
fun isEquals() = runBlocking<Unit> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RealmAny?, RealmAny?> ->
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) {
Expand Down
Loading

0 comments on commit 4d63177

Please sign in to comment.