diff --git a/CHANGELOG.md b/CHANGELOG.md index a02783bf8d..9793830e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) 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..2785898976 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 @@ -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 @@ -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) 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..1516c3f79f 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 { + INDEX_NOT_FOUND + } + } + 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..f6dd31ba65 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 { + 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())) 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..badf97fea5 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 @@ -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]. */ @@ -90,10 +93,22 @@ 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 +240,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 +252,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 +304,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 +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().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 +507,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/RealmAnyNestedCollectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyNestedCollectionTests.kt index d377684f94..2e350642ba 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,12 @@ 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) + } + // FIXME Core issue https://github.com/realm/realm-core/issues/7393 + // 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/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 // ----------------------------------- 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) } }