Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix list indexof, remove and contains #1666

Merged
merged 5 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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)
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 {
-1
}
}

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 {
-1
rorbech marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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 @@ -90,10 +91,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 +238,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 +250,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 +302,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 +387,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 +505,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realm/realm-core#7264 has been closed

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
Loading