Skip to content

Commit

Permalink
Fix list indexof, remove and contains (#1666)
Browse files Browse the repository at this point in the history
  • Loading branch information
rorbech authored Mar 18, 2024
1 parent c20f5c7 commit 68308aa
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This release will bump the Realm file format from version 23 to 24. Opening a fi
* [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)).
* `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580))
* The Unpacking of JVM native library will use the current library version instead of a calculated hash for the path. (Issue [#1617](https://github.com/realm/realm-kotlin/issues/1617)).
* Optimized `RealmList.indexOf()` and `RealmList.contains()` using Core implementation of operations instead of iterating elements and comparing them in Kotlin. (Issue [#1625](https://github.com/realm/realm-kotlin/pull/1666) [RKOTLIN-995](https://jira.mongodb.org/browse/RKOTLIN-995)).

### Fixed
* Cache notification callback JNI references at startup to ensure that symbols can be resolved in core callbacks. (Issue [#1577](https://github.com/realm/realm-kotlin/issues/1577))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ expect val INVALID_PROPERTY_KEY: PropertyKey
const val OBJECT_ID_BYTES_SIZE = 12
const val UUID_BYTES_SIZE = 16

const val INDEX_NOT_FOUND = -1L

// Pure marker interfaces corresponding to the C-API realm_x_t struct types
interface CapiT
interface RealmConfigT : CapiT
Expand Down Expand Up @@ -317,6 +319,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
INDEX_NOT_FOUND
}
}

actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer =
LongPointerWrapper(realmc.realm_list_get_list(list.cptr(), index))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,19 @@ actual object RealmInterop {
return RealmValue(struct)
}

actual fun realm_list_find(list: RealmListPointer, value: RealmValue): Long {
memScoped {
val index = alloc<ULongVar>()
val found = alloc<BooleanVar>()
checkedBooleanResult(realm_wrapper.realm_list_find(list.cptr(), value.value.readValue(), index.ptr, found.ptr))
return if (found.value) {
index.value.toLong()
} else {
INDEX_NOT_FOUND
}
}
}

actual fun realm_list_get_list(list: RealmListPointer, index: Long): RealmListPointer =
CPointerWrapper(realm_wrapper.realm_list_get_list(list.cptr(), index.toULong()))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,6 +51,8 @@ import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass

internal const val INDEX_NOT_FOUND = io.realm.kotlin.internal.interop.INDEX_NOT_FOUND

/**
* Implementation for unmanaged lists, backed by a [MutableList].
*/
Expand Down Expand Up @@ -90,10 +93,22 @@ internal class ManagedRealmList<E>(
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<E>): Boolean = operator.insertAll(size, elements)
Expand Down Expand Up @@ -225,6 +240,10 @@ internal interface ListOperator<E> : CollectionOperator<E, RealmListPointer> {

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,
Expand All @@ -233,6 +252,14 @@ internal interface ListOperator<E> : CollectionOperator<E, RealmListPointer> {
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<E>,
Expand Down Expand Up @@ -277,6 +304,14 @@ internal class PrimitiveListOperator<E>(
}
}

override fun indexOf(element: E): Int {
inputScope {
with(realmValueConverter) {
return RealmInterop.realm_list_find(nativePointer, publicToRealmValue(element)).toInt()
}
}
}

override fun insert(
index: Int,
element: E,
Expand Down Expand Up @@ -354,6 +389,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<RealmObjectInternal>().isManaged()) return -1
}
return inputScope {
val transport = realmAnyToRealmValueWithoutImport(element)
RealmInterop.realm_list_find(nativePointer, transport).toInt()
}
}

override fun insert(
index: Int,
element: RealmAny?,
Expand Down Expand Up @@ -461,6 +507,18 @@ internal abstract class BaseRealmObjectListOperator<E : BaseRealmObject?> (
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<E : BaseRealmObject?>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,9 @@ class RealmAnyNestedCollectionTests {
realm.query<JsonStyleRealmObject>("value[*] == 4").find().single().run {
assertEquals("LIST", id)
}
realm.query<JsonStyleRealmObject>("value[*] == {4, 5, 6}").find().single().run {
assertEquals("LIST", id)
}

// Matching dictionaries
realm.query<JsonStyleRealmObject>("value.key1 == 7").find().single().run {
Expand Down Expand Up @@ -606,5 +609,12 @@ class RealmAnyNestedCollectionTests {
realm.query<JsonStyleRealmObject>("value[*].key3[0] == 9").find().single().run {
assertEquals("EMBEDDED", id)
}
realm.query<JsonStyleRealmObject>("value[0][*] == {4, 5, 6}").find().single().run {
assertEquals("EMBEDDED", id)
}
// FIXME Core issue https://github.com/realm/realm-core/issues/7393
// realm.query<JsonStyleRealmObject>("value[*][*] == {4, 5, 6}").find().single().run {
// assertEquals("EMBEDDED", id)
// }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -722,13 +722,6 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests {
),
classifier
)
ByteArray::class -> ByteArrayListTester(
realm = realm,
typeSafetyManager = getTypeSafety(
classifier,
elementType.nullable
) as ListTypeSafetyManager<ByteArray?>
)
RealmAny::class -> RealmAnyListTester(
realm = realm,
typeSafetyManager = ListTypeSafetyManager(
Expand Down Expand Up @@ -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<ByteArray?>
) : ManagedListTester<ByteArray?>(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<ByteArray?> ->
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
// -----------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<ListChange<RealmAny?>>(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<InitialList<*>>(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<UpdatedList<*>>(it)
assertEquals(1, it.list.size)
}

realm.write {
findLatest(parent)!!.value!!.asList()[0]!!.asRealmObject<JsonStyleRealmObject>().value =
RealmAny.create("TEST")
}
channel.receiveOrFail(message = "Object updated").let {
assertIs<UpdatedList<*>>(it)
assertEquals(1, it.list.size)
assertEquals(
"TEST",
it.list[0]!!.asRealmObject<JsonStyleRealmObject>().value!!.asString()
)
}

listener.cancel()
}
}

@Test
fun eventsOnDictionaryChangesInRealmAnyList() {
kotlinx.coroutines.runBlocking {
val channel = Channel<ListChange<RealmAny?>>(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<InitialList<*>>(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<UpdatedList<*>>(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<UpdatedList<*>>(it)
assertEquals(1, it.list.size)
assertEquals("TEST", it.list[0]!!.asDictionary()["key1"]!!.asString())
}

listener.cancel()
}
}
}
Loading

0 comments on commit 68308aa

Please sign in to comment.