From 311a6ae52ca1686e6b184725fcc37be2ec33defe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 1 Aug 2023 10:07:20 +0200 Subject: [PATCH 01/27] Add tests for persisted name links in lists (#1462) --- .../kotlin/test/common/PersistedNameTests.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt index d3239242c2..07dd7069e1 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt @@ -386,15 +386,27 @@ class PersistedNameTests { realm.writeBlocking { copyToRealm( Parent().apply { - this.child = Child() + this.child = Child().apply { name = "child1" } + this.children.add( + Child().apply { + name = "first-child" + children.add( + Child().apply { name = "first-grand-child" } + ) + } + ) } ) } assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) + assertEquals(3, realm.query().count().find()) - assertEquals("child", realm.query().first().find()!!.child!!.name) + val parent = realm.query().first().find()!! + assertEquals("child1", parent.child!!.name) + val child2 = parent.children.first() + assertEquals("first-child", child2.name) + assertEquals("first-grand-child", child2.children.first().name) } } @@ -507,9 +519,14 @@ class RealmChild(var id: Int) : RealmObject { class Parent : RealmObject { var name = "parent" var child: Child? = null + + @PersistedName("renamedChildren") + var children: RealmList = realmListOf() } @PersistedName(name = "RenamedChild") class Child : RealmObject { var name = "child" + @PersistedName("renamedChildren") + var children: RealmList = realmListOf() } From 80313b9005717cf96f0ea701e49c10248be7c3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 1 Aug 2023 12:12:08 +0200 Subject: [PATCH 02/27] Update to latest core (#1467) --- CHANGELOG.md | 29 +++++++++++++++++++ .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/ErrorCode.kt | 1 + packages/external/core | 2 +- 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1a3340f9..bcea7d5afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 1.10.3-SNAPSHOT (YYYY-MM-DD) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440)) + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* Updated to Realm Core 13.17.1, commit fb5bdccba1daad0bd6e65757a6a1596dc98cebf4. + + ## 1.10.2 (2023-07-21) ### Breaking Changes diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 13138a5605..55b9dd1305 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -162,6 +162,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_MAINTENANCE_IN_PROGRESS, RLM_ERR_USERPASS_TOKEN_INVALID, RLM_ERR_INVALID_SERVER_RESPONSE, + RLM_ERR_APP_SERVER_ERROR, RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR, RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR, RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR, diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 935354e9cb..008be93984 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -159,6 +159,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno_e.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno_e.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno_e.RLM_ERR_INVALID_SERVER_RESPONSE), + RLM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno_e.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno_e.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 70987acd03..d41a3e1419 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -163,6 +163,7 @@ actual enum class ErrorCode( RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno.RLM_ERR_INVALID_SERVER_RESPONSE), + RLM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), diff --git a/packages/external/core b/packages/external/core index f1e962cd44..fb5bdccba1 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit f1e962cd447f8b69f8f7cf46a188b1c6246923c5 +Subproject commit fb5bdccba1daad0bd6e65757a6a1596dc98cebf4 From e8bff870060dd80a57f96090d7d97f4879814efb Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 2 Aug 2023 08:06:40 +0200 Subject: [PATCH 03/27] Fix compiler issue with Kotlin 1.9 and Backlinks (#1470) --- CHANGELOG.md | 1 + buildSrc/src/main/kotlin/Config.kt | 2 +- examples/kmm-sample/gradle.properties | 1 + .../commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt | 4 ++++ packages/plugin-compiler/build.gradle.kts | 2 +- .../src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt | 4 ++-- .../io/realm/kotlin/compiler/GenerationExtensionTest.kt | 2 +- .../src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt | 2 +- 8 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcea7d5afb..23a6a5adaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed * Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440)) +* Compiler error when using Kotlin 1.9.0 and backlinks. (Issue [#1469](https://github.com/realm/realm-kotlin/issues/1469)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index e65f21a6d7..e89c4c4764 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -127,7 +127,7 @@ object Versions { const val jvmTarget = "1.8" // When updating the Kotlin version, also remember to update /examples/min-android-sample/build.gradle.kts const val kotlin = "1.8.21" // https://github.com/JetBrains/kotlin and https://kotlinlang.org/docs/releases.html#release-details - const val latestKotlin = "1.9.0-Beta" // https://kotlinlang.org/docs/eap.html#build-details + const val latestKotlin = "1.9.0" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "1.5.0" // https://github.com/tschuchortdev/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint const val ktor = "2.1.2" // https://github.com/ktorio/ktor diff --git a/examples/kmm-sample/gradle.properties b/examples/kmm-sample/gradle.properties index eff7ec4f56..abfa6de207 100644 --- a/examples/kmm-sample/gradle.properties +++ b/examples/kmm-sample/gradle.properties @@ -24,3 +24,4 @@ kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false kotlin.mpp.stability.nowarn=true +kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true diff --git a/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt b/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt index 982dc8b5e2..8231e877b9 100644 --- a/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt +++ b/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt @@ -17,6 +17,7 @@ package io.realm.example.kmmsample +import io.realm.kotlin.ext.backlinks import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.ObjectId import io.realm.kotlin.types.RealmInstant @@ -83,4 +84,7 @@ class AllTypes : RealmObject { var objectIdRealmList: RealmList = realmListOf(ObjectId.create()) var objectIdRealmListNullable: RealmList = realmListOf(null) var objectRealmList: RealmList = realmListOf() + + // Special types + val parent by backlinks(AllTypes::objectField) } diff --git a/packages/plugin-compiler/build.gradle.kts b/packages/plugin-compiler/build.gradle.kts index 5dfd688326..1dd9133e7e 100644 --- a/packages/plugin-compiler/build.gradle.kts +++ b/packages/plugin-compiler/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { tasks.withType { kotlinOptions { jvmTarget = "${Versions.jvmTarget}" - freeCompilerArgs = listOf("-Xjvm-default=enable") + freeCompilerArgs = listOf("-Xjvm-default=all-compatibility") } } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt index 49d337ae5d..047ab1ec3f 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt @@ -587,11 +587,11 @@ fun getBacklinksTargetPropertyType(declaration: IrProperty): IrType? { fun getLinkingObjectPropertyName(backingField: IrField): String { (backingField.initializer!!.expression as IrCall).let { irCall -> val propertyReference = irCall.getValueArgument(0) as IrPropertyReference - val targetProperty = propertyReference.symbol.owner + val targetProperty: IrProperty = propertyReference.symbol.owner return if (targetProperty.hasAnnotation(PERSISTED_NAME_ANNOTATION)) { SchemaProperty.getPersistedName(targetProperty) } else { - propertyReference.referencedName.identifier + targetProperty.name.identifier } } } diff --git a/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt b/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt index e43d7ad3ee..b6f0f9bd15 100644 --- a/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt +++ b/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt @@ -402,7 +402,7 @@ class GenerationExtensionTest { componentRegistrars = plugins inheritClassPath = true kotlincArguments = listOf( - "-Xjvm-default=enable", + "-Xjvm-default=all-compatibility", "-Xdump-directory=${inputs.outputDir()}", "-Xphases-to-dump-after=ValidateIrBeforeLowering" ) diff --git a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt index 84d7d9838c..2375417402 100644 --- a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt +++ b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt @@ -34,6 +34,6 @@ object Compiler { messageOutputStream = System.out componentRegistrars = plugins inheritClassPath = true - kotlincArguments = listOf("-Xjvm-default=enable") + kotlincArguments = listOf("-Xjvm-default=all-compatibility") }.compile() } From 6b1fa2fb3f6090f868b222a554d15dbd3d59b2a7 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 3 Aug 2023 10:24:45 +0200 Subject: [PATCH 04/27] Fix corner cases when creating subscriptions (#1471) --- CHANGELOG.md | 1 + .../realm/kotlin/mongodb/ext/RealmQueryExt.kt | 4 ++- .../kotlin/mongodb/ext/RealmResultsExt.kt | 4 ++- .../mongodb/internal/RealmQueryExtImpl.kt | 35 +++++++++++++++--- .../mongodb/internal/SubscriptionImpl.kt | 3 +- .../common/MutableSubscriptionSetTests.kt | 6 ++-- .../common/SubscriptionExtensionsTests.kt | 36 ++++++++++++++----- .../mongodb/common/SubscriptionSetTests.kt | 2 +- .../test/mongodb/common/SubscriptionTests.kt | 6 ++-- 9 files changed, 73 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a6a5adaa..6121bbb98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed * Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440)) * Compiler error when using Kotlin 1.9.0 and backlinks. (Issue [#1469](https://github.com/realm/realm-kotlin/issues/1469)) +* [Sync] Changing a subscriptions query type or query itself will now trigger the `WaitForSync.FIRST_TIME` behaviour, rather than only checking changes to the name. (Issues [#1466](https://github.com/realm/realm-kotlin/issues/1466)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt index e12dafdbfd..425ae1ca7f 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt @@ -60,7 +60,9 @@ import kotlin.time.Duration * depend on which [mode] was used. * @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before * a query result could be returned. - * @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not + * accept the set of queries. The exact reason is found in the exception message. */ @ExperimentalFlexibleSyncApi public suspend fun RealmQuery.subscribe( diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt index 53804d2dd6..d2c3aeb16e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt @@ -62,7 +62,9 @@ import kotlin.time.Duration * depend on which [mode] was used. * @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before * a query result could be returned. - * @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not + * accept the set of queries. The exact reason is found in the exception message. */ @ExperimentalFlexibleSyncApi public suspend fun RealmResults.subscribe( diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt index 867acd213a..3798454fac 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +@file:Suppress("invisible_reference", "invisible_member") package io.realm.kotlin.mongodb.internal import io.realm.kotlin.Realm import io.realm.kotlin.internal.RealmImpl import io.realm.kotlin.internal.getRealm +import io.realm.kotlin.internal.query.ObjectQuery +import io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException import io.realm.kotlin.mongodb.subscriptions import io.realm.kotlin.mongodb.sync.Subscription +import io.realm.kotlin.mongodb.sync.SubscriptionSet import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.mongodb.sync.WaitForSync import io.realm.kotlin.mongodb.syncSession @@ -32,7 +35,6 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlin.time.Duration -@Suppress("invisible_reference", "invisible_member") internal suspend fun createSubscriptionFromQuery( query: RealmQuery, name: String?, @@ -41,7 +43,7 @@ internal suspend fun createSubscriptionFromQuery( timeout: Duration ): RealmResults { - if (query !is io.realm.kotlin.internal.query.ObjectQuery) { + if (query !is ObjectQuery) { throw IllegalStateException("Only queries on objects are supported. This was: ${query::class}") } if (query.realmReference.owner !is RealmImpl) { @@ -53,8 +55,7 @@ internal suspend fun createSubscriptionFromQuery( return withTimeout(timeout) { withContext(appDispatcher) { - val existingSubscription: Subscription? = - if (name != null) subscriptions.findByName(name) else subscriptions.findByQuery(query) + val existingSubscription: Subscription? = findExistingQueryInSubscriptions(name, query, subscriptions) if (existingSubscription == null || updateExisting) { subscriptions.update { add(query, name, updateExisting) @@ -66,9 +67,33 @@ internal suspend fun createSubscriptionFromQuery( // The subscription should already exist, just make sure we downloaded all // server data before continuing. realm.syncSession.downloadAllServerChanges() + subscriptions.refresh() + subscriptions.errorMessage?.let { errorMessage: String -> + throw BadFlexibleSyncQueryException(errorMessage) + } } // Rerun the query on the latest Realm version. realm.query(query.clazz, query.description()).find() } } } + +// A subscription only matches if name, type and query all matches +private fun findExistingQueryInSubscriptions( + name: String?, + query: ObjectQuery, + subscriptions: SubscriptionSet +): Subscription? { + return if (name != null) { + val sub: Subscription? = subscriptions.findByName(name) + val companion = io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow(query.clazz) + val userTypeName = companion.io_realm_kotlin_className + if (sub?.queryDescription == query.description() && sub.objectType == userTypeName) { + sub + } else { + null + } + } else { + subscriptions.findByQuery(query) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt index f9f21fc131..d908305357 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt @@ -39,7 +39,8 @@ internal class SubscriptionImpl( override val updatedAt: RealmInstant = RealmInstantImpl(RealmInterop.realm_sync_subscription_updated_at(nativePointer)) override val name: String? = RealmInterop.realm_sync_subscription_name(nativePointer) override val objectType: String = RealmInterop.realm_sync_subscription_object_class_name(nativePointer) - override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer) + // Trim the query to match the output of RealmQuery.description() + override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer).trim() @Suppress("invisible_member") override fun asQuery(type: KClass): RealmQuery { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt index 2e615de299..6b27ff8ef5 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt @@ -102,7 +102,7 @@ class MutableSubscriptionSetTests { assertEquals(SubscriptionSetState.PENDING, updatedSubs.state) val sub: Subscription = updatedSubs.first() assertEquals("test", sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}") assertEquals(sub.updatedAt, sub.createdAt) @@ -122,7 +122,7 @@ class MutableSubscriptionSetTests { assertEquals(SubscriptionSetState.PENDING, updatedSubs.state) val sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}") assertEquals(sub.updatedAt, sub.createdAt) @@ -185,7 +185,7 @@ class MutableSubscriptionSetTests { val sub = subs.first() assertEquals("sub1", sub.name) assertEquals("FlexParentObject", sub.objectType) - assertEquals("name == \"red\" ", sub.queryDescription) + assertEquals("name == \"red\"", sub.queryDescription) assertTrue(sub.createdAt < sub.updatedAt) assertEquals(createdAt, sub.createdAt) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt index 596633f9a5..f04103ccdb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt @@ -92,7 +92,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -107,7 +107,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -122,7 +122,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -168,7 +168,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) var sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -184,7 +184,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) sub = updatedSubs.last() assertEquals("my-name", sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -269,7 +269,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) var sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -285,7 +285,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) sub = updatedSubs.last() assertEquals("my-name", sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -340,12 +340,12 @@ class SubscriptionExtensionsTests { subQueryResult.subscribe() val subs = realm.subscriptions assertEquals(1, subs.size) - assertEquals("section == 42 and name == \"Jane\" ", subs.first().queryDescription) + assertEquals("section == 42 and name == \"Jane\"", subs.first().queryDescription) subQueryResult.subscribe("my-name") assertEquals(2, subs.size) val lastSub = subs.last() assertEquals("my-name", lastSub.name) - assertEquals("section == 42 and name == \"Jane\" ", lastSub.queryDescription) + assertEquals("section == 42 and name == \"Jane\"", lastSub.queryDescription) } @Test @@ -374,6 +374,24 @@ class SubscriptionExtensionsTests { } } + @Test + fun updatingOnlyQueryWillTriggerFirstTimeBehavior() = runBlocking { + val section = Random.nextInt() + + // 1. Create a named subscription + realm.query("section = $0", section).subscribe("my-name", mode = WaitForSync.FIRST_TIME) + + // 2. Pause the connection in order to go offline + realm.syncSession.pause() + + // 3. Update the query of the named subscription. This should trigger FIRST_TIME behavior again. + // and because we are offline, the subscribe call should throw. + val query = realm.query("section = $0 AND TRUEPREDICATE", section) + assertFailsWith { + query.subscribe("my-name", updateExisting = true, mode = WaitForSync.FIRST_TIME, timeout = 1.seconds) + } + } + private suspend fun uploadServerData(sectionId: Int, noOfObjects: Int) { val user = app.createUserAndLogin() val config = SyncConfiguration.Builder(user, setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class)) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt index bc3c8ecf77..747e1fe7a7 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt @@ -132,7 +132,7 @@ class SubscriptionSetTests { val sub: Subscription = subscriptions.findByQuery(query)!! assertNotNull(sub) assertEquals("FlexParentObject", sub.objectType) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) } @Test diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt index 7996094440..bf7efe7ebf 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt @@ -90,7 +90,7 @@ class SubscriptionTests { assertEquals("mySub", namedSub.name) assertEquals("ParentPk", namedSub.objectType) - assertEquals("TRUEPREDICATE ", namedSub.queryDescription) + assertEquals("TRUEPREDICATE", namedSub.queryDescription) assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}") assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}") @@ -100,7 +100,7 @@ class SubscriptionTests { }.first() assertNull(anonSub.name) assertEquals("ParentPk", anonSub.objectType) - assertEquals("TRUEPREDICATE ", anonSub.queryDescription) + assertEquals("TRUEPREDICATE", anonSub.queryDescription) assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}") assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}") } @@ -119,7 +119,7 @@ class SubscriptionTests { // Check that properties still work even if subscription is deleted elsewhere assertEquals("mySub", snapshotSub.name) assertEquals("ParentPk", snapshotSub.objectType) - assertEquals("TRUEPREDICATE ", snapshotSub.queryDescription) + assertEquals("TRUEPREDICATE", snapshotSub.queryDescription) assertNotNull(snapshotSub.updatedAt) assertNotNull(snapshotSub.createdAt) Unit From 4557761b8b23e0dacd65fa8f087d313e67ce949c Mon Sep 17 00:00:00 2001 From: clementetb Date: Mon, 7 Aug 2023 09:17:01 +0200 Subject: [PATCH 05/27] Fix realm scheduler leak (#1463) --- CHANGELOG.md | 1 + .../kotlin/internal/interop/NativePointer.kt | 11 ++ .../kotlin/internal/interop/RealmInterop.kt | 6 +- .../kotlin/internal/interop/RealmInterop.kt | 30 ++-- .../kotlin/internal/interop/RealmInterop.kt | 139 ++++++++---------- .../io/realm/kotlin/test/CinteropTest.kt | 10 +- packages/jni-swig-stub/realm.i | 2 +- .../src/main/jni/realm_api_helpers.cpp | 12 +- .../src/main/jni/realm_api_helpers.h | 4 +- .../kotlin/io/realm/kotlin/Realm.kt | 18 ++- .../kotlin/internal/ConfigurationImpl.kt | 14 +- .../io/realm/kotlin/internal/LiveRealm.kt | 13 +- .../io/realm/kotlin/internal/RealmImpl.kt | 34 +++-- .../kotlin/internal/SuspendableNotifier.kt | 11 +- .../kotlin/internal/SuspendableWriter.kt | 20 ++- .../util/CoroutineDispatcherFactory.kt | 37 ++++- .../mongodb/internal/SubscriptionSetImpl.kt | 24 ++- .../mongodb/internal/SyncConfigurationImpl.kt | 2 +- .../mongodb/internal/SyncSessionImpl.kt | 2 +- .../test/StandaloneDynamicMutableRealm.kt | 18 ++- .../notifications/SystemNotificationTests.kt | 17 ++- 21 files changed, 260 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6121bbb98d..ff4a868045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed * Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440)) * Compiler error when using Kotlin 1.9.0 and backlinks. (Issue [#1469](https://github.com/realm/realm-kotlin/issues/1469)) +* Leaking `JVMScheduler` instances. In certain circumstances, it could lead to a JNI crash. (Issue [#1463](https://github.com/realm/realm-kotlin/pull/1463)) * [Sync] Changing a subscriptions query type or query itself will now trigger the `WaitForSync.FIRST_TIME` behaviour, rather than only checking changes to the name. (Issues [#1466](https://github.com/realm/realm-kotlin/issues/1466)) ### Compatibility diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt index b73757ac10..4ab0a46515 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt @@ -43,3 +43,14 @@ public interface NativePointer { */ public fun isReleased(): Boolean } + +/** + * Deletes the underlying pointer after executing the lambda block. + */ +fun NativePointer.use( + block: (NativePointer) -> R, +): R = try { + block(this) +} finally { + release() +} 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 f553d15266..9e6728f1fd 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 @@ -70,6 +70,7 @@ interface RealmQueryT : CapiT interface RealmCallbackTokenT : CapiT interface RealmNotificationTokenT : CapiT interface RealmChangesT : CapiT +interface RealmSchedulerT : CapiT // Public type aliases binding to internal verbose type safe type definitions. This should allow us // to easily change implementation details later on. @@ -88,6 +89,7 @@ typealias RealmQueryPointer = NativePointer typealias RealmCallbackTokenPointer = NativePointer typealias RealmNotificationTokenPointer = NativePointer typealias RealmChangesPointer = NativePointer +typealias RealmSchedulerPointer = NativePointer // Sync types // Pure marker interfaces corresponding to the C-API realm_x_t struct types @@ -190,6 +192,8 @@ expect object RealmInterop { fun realm_config_set_in_memory(config: RealmConfigurationPointer, inMemory: Boolean) fun realm_schema_validate(schema: RealmSchemaPointer, mode: SchemaValidationMode): Boolean + fun realm_create_scheduler(): RealmSchedulerPointer + fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer /** * Open a realm on the current thread. * @@ -211,7 +215,7 @@ expect object RealmInterop { */ // The dispatcher argument is only used on Native to build a core scheduler dispatching to the // dispatcher. The realm itself must also be opened on the same thread - fun realm_open(config: RealmConfigurationPointer, dispatcher: CoroutineDispatcher? = null): Pair + fun realm_open(config: RealmConfigurationPointer, scheduler: RealmSchedulerPointer): Pair // Opening a Realm asynchronously. Only supported for synchronized realms. fun realm_open_synchronized(config: RealmConfigurationPointer): RealmAsyncOpenTaskPointer 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 3ef0c015d4..67ba09687f 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 @@ -33,7 +33,6 @@ import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch import org.mongodb.kbson.ObjectId @@ -180,7 +179,16 @@ actual object RealmInterop { realmc.realm_config_set_in_memory(config.cptr(), inMemory) } - actual fun realm_open(config: RealmConfigurationPointer, dispatcher: CoroutineDispatcher?): Pair { + actual fun realm_create_scheduler(): RealmSchedulerPointer = + LongPointerWrapper(realmc.realm_scheduler_make_default()) + + actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer = + LongPointerWrapper(realmc.realm_create_scheduler(JVMScheduler(dispatcher))) + + actual fun realm_open( + config: RealmConfigurationPointer, + scheduler: RealmSchedulerPointer, + ): Pair { // Configure callback to track if the file was created as part of opening var fileCreated = false val callback = DataInitializationCallback { @@ -188,14 +196,9 @@ actual object RealmInterop { } realm_config_set_data_initialization_function(config, callback) - // create a custom Scheduler for JVM if a Coroutine Dispatcher is provided other wise - // pass null to use the generic one - val realmPtr = LongPointerWrapper( - realmc.open_realm_with_scheduler( - (config as LongPointerWrapper).ptr, - if (dispatcher != null) JVMScheduler(dispatcher) else null - ) - ) + realmc.realm_config_set_scheduler(config.cptr(), scheduler.cptr()) + val realmPtr = LongPointerWrapper(realmc.realm_open(config.cptr())) + // Ensure that we can read version information, etc. realm_begin_read(realmPtr) return Pair(realmPtr, fileCreated) @@ -2093,13 +2096,8 @@ private class JVMScheduler(dispatcher: CoroutineDispatcher) { val scope: CoroutineScope = CoroutineScope(dispatcher) fun notifyCore(schedulerPointer: Long) { - val function: suspend CoroutineScope.() -> Unit = { + scope.launch { realmc.invoke_core_notify_callback(schedulerPointer) } - scope.launch( - scope.coroutineContext, - CoroutineStart.DEFAULT, - function - ) } } 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 f569916772..d85de7f650 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 @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH -import io.realm.kotlin.internal.interop.RealmInterop.safeKString import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.AuthProvider @@ -80,7 +79,6 @@ import kotlinx.cinterop.usePinned import kotlinx.cinterop.value import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.ObjectId @@ -494,11 +492,10 @@ actual object RealmInterop { ) } - actual fun realm_open(config: RealmConfigurationPointer, dispatcher: CoroutineDispatcher?): Pair { + actual fun realm_open(config: RealmConfigurationPointer, scheduler: RealmSchedulerPointer): Pair { val fileCreated = atomic(false) val callback = DataInitializationCallback { fileCreated.value = true - true } realm_wrapper.realm_config_set_data_initialization_function( config.cptr(), @@ -516,21 +513,75 @@ actual object RealmInterop { // val dispatcher = runBlocking { coroutineContext[CoroutineDispatcher.Key] } // but requires opting in for @ExperimentalStdlibApi, and have really gotten it to play // for default cases. - if (dispatcher != null) { - val scheduler = checkedPointerResult(createSingleThreadDispatcherScheduler(dispatcher)) - realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler) - } else { - // If there is no notification dispatcher use the default scheduler. - // Re-verify if this is actually needed when notification scheduler is fully in place. - val scheduler = checkedPointerResult(realm_wrapper.realm_scheduler_make_default()) - realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler) - } + realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler.cptr()) + val realmPtr = CPointerWrapper(realm_wrapper.realm_open(config.cptr())) // Ensure that we can read version information, etc. realm_begin_read(realmPtr) return Pair(realmPtr, fileCreated.value) } + actual fun realm_create_scheduler(): RealmSchedulerPointer { + // If there is no notification dispatcher use the default scheduler. + // Re-verify if this is actually needed when notification scheduler is fully in place. + val scheduler = checkedPointerResult(realm_wrapper.realm_scheduler_make_default()) + return CPointerWrapper(scheduler) + } + + actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer { + printlntid("createSingleThreadDispatcherScheduler") + val scheduler = SingleThreadDispatcherScheduler(tid(), dispatcher) + + val capi_scheduler: CPointer = checkedPointerResult( + realm_wrapper.realm_scheduler_new( + // userdata: kotlinx.cinterop.CValuesRef<*>?, + scheduler.ref, + + // free: realm_wrapper.realm_free_userdata_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, + staticCFunction { userdata -> + printlntid("free") + userdata?.asStableRef()?.dispose() + }, + + // notify: realm_wrapper.realm_scheduler_notify_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, + staticCFunction { userdata -> + // Must be thread safe + val scheduler = + userdata!!.asStableRef().get() + printlntid("$scheduler notify") + try { + scheduler.notify() + } catch (e: Exception) { + // Should never happen, but is included for development to get some indicators + // on errors instead of silent crashes. + e.printStackTrace() + } + }, + + // is_on_thread: realm_wrapper.realm_scheduler_is_on_thread_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { userdata -> + // Must be thread safe + val scheduler = + userdata!!.asStableRef().get() + printlntid("is_on_thread[$scheduler] ${scheduler.threadId} " + tid()) + scheduler.threadId == tid() + }, + + // is_same_as: realm_wrapper.realm_scheduler_is_same_as_func_t? /* = kotlinx.cinterop.CPointer? */, kotlinx.cinterop.COpaquePointer? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { userdata, other -> + userdata == other + }, + + // can_deliver_notifications: realm_wrapper.realm_scheduler_can_deliver_notifications_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { _ -> true }, + ) + ) ?: error("Couldn't create scheduler") + + scheduler.set_scheduler(capi_scheduler) + + return CPointerWrapper(capi_scheduler) + } + actual fun realm_open_synchronized(config: RealmConfigurationPointer): RealmAsyncOpenTaskPointer { return CPointerWrapper(realm_wrapper.realm_open_synchronized(config.cptr())) } @@ -3241,61 +3292,6 @@ actual object RealmInterop { ?: throw NullPointerException(identifier?.let { "'$identifier' shouldn't be null." }) } - private fun createSingleThreadDispatcherScheduler( - dispatcher: CoroutineDispatcher - ): CPointer { - printlntid("createSingleThreadDispatcherScheduler") - val scheduler = SingleThreadDispatcherScheduler(tid(), dispatcher) - - val capi_scheduler: CPointer = checkedPointerResult( - realm_wrapper.realm_scheduler_new( - // userdata: kotlinx.cinterop.CValuesRef<*>?, - scheduler.ref, - - // free: realm_wrapper.realm_free_userdata_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, - staticCFunction { userdata -> - printlntid("free") - userdata?.asStableRef()?.dispose() - }, - - // notify: realm_wrapper.realm_scheduler_notify_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, - staticCFunction { userdata -> - // Must be thread safe - val scheduler = - userdata!!.asStableRef().get() - printlntid("$scheduler notify") - try { - scheduler.notify() - } catch (e: Exception) { - // Should never happen, but is included for development to get some indicators - // on errors instead of silent crashes. - e.printStackTrace() - } - }, - - // is_on_thread: realm_wrapper.realm_scheduler_is_on_thread_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata -> - // Must be thread safe - val scheduler = - userdata!!.asStableRef().get() - printlntid("is_on_thread[$scheduler] ${scheduler.threadId} " + tid()) - scheduler.threadId == tid() - }, - - // is_same_as: realm_wrapper.realm_scheduler_is_same_as_func_t? /* = kotlinx.cinterop.CPointer? */, kotlinx.cinterop.COpaquePointer? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata, other -> - userdata == other - }, - - // can_deliver_notifications: realm_wrapper.realm_scheduler_can_deliver_notifications_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata -> true }, - ) - ) ?: error("Couldn't create scheduler") - scheduler.set_scheduler(capi_scheduler) - scheduler - return capi_scheduler - } - private fun handleAppCallback( userData: COpaquePointer?, error: CPointer?, @@ -3401,7 +3397,7 @@ actual object RealmInterop { } override fun notify() { - val function: suspend CoroutineScope.() -> Unit = { + scope.launch { try { printlntid("on dispatcher") realm_wrapper.realm_scheduler_perform_work(scheduler) @@ -3411,11 +3407,6 @@ actual object RealmInterop { e.printStackTrace() } } - scope.launch( - scope.coroutineContext, - CoroutineStart.DEFAULT, - function - ) } } } diff --git a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt index 110d6c47c4..01fd6fec06 100644 --- a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt +++ b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt @@ -28,6 +28,7 @@ import io.realm.kotlin.internal.interop.SchemaMode import io.realm.kotlin.internal.interop.SchemaValidationMode import io.realm.kotlin.internal.interop.set import io.realm.kotlin.internal.interop.toKotlinString +import io.realm.kotlin.internal.interop.use import kotlinx.cinterop.BooleanVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.CPointerVarOf @@ -178,9 +179,12 @@ class CinteropTest { SchemaMode.RLM_SCHEMA_MODE_AUTOMATIC ) RealmInterop.realm_config_set_schema_version(nativeConfig, 1) - - val (realm, fileCreated) = RealmInterop.realm_open(nativeConfig) - assertEquals(1L, RealmInterop.realm_get_num_classes(realm)) + RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (realm, fileCreated) = RealmInterop.realm_open(nativeConfig, scheduler) + assertEquals(1L, RealmInterop.realm_get_num_classes(realm)) + RealmInterop.realm_close(realm) + } } } diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 9bf2bedd97..02880c1df5 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -301,7 +301,7 @@ return $jnicall; realm_flx_sync_mutable_subscription_set_t*, realm_flx_sync_subscription_desc_t*, realm_set_t*, realm_async_open_task_t*, realm_dictionary_t*, realm_sync_session_connection_state_notification_token_t*, - realm_dictionary_changes_t* }; + realm_dictionary_changes_t*, realm_scheduler_t* }; // For all functions returning a pointer or bool, check for null/false and throw an error if // realm_get_last_error returns true. diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9848fc99c4..8f0a4fbb12 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -313,8 +313,8 @@ void invoke_core_notify_callback(int64_t scheduler) { realm_scheduler_perform_work(reinterpret_cast(scheduler)); } -realm_t *open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler) { - auto config = reinterpret_cast(config_ptr); +realm_scheduler_t* +realm_create_scheduler(jobject dispatchScheduler) { if (dispatchScheduler) { auto jvmScheduler = new CustomJVMScheduler(dispatchScheduler); auto scheduler = realm_scheduler_new( @@ -326,13 +326,9 @@ realm_t *open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler [](void *userdata) { return static_cast(userdata)->can_invoke(); } ); jvmScheduler->set_scheduler(scheduler); - realm_config_set_scheduler(config, scheduler); - } else { - // TODO refactor to use public C-API https://github.com/realm/realm-kotlin/issues/496 - auto scheduler = new realm_scheduler_t{realm::util::Scheduler::make_generic()}; - realm_config_set_scheduler(config, scheduler); + return scheduler; } - return realm_open(config); + throw std::runtime_error("Null dispatchScheduler"); } jobject convert_to_jvm_app_error(JNIEnv* env, const realm_app_error_t* error) { diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 0854631d72..9178bf5266 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -47,8 +47,8 @@ realm_network_transport_new(jobject network_transport); void set_log_callback(jint log_level, jobject log_callback); -realm_t* -open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler); +realm_scheduler_t* +realm_create_scheduler(jobject dispatchScheduler); bool realm_should_compact_callback(void* userdata, uint64_t total_bytes, uint64_t used_bytes); diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt index 5ac3626386..ca26a0f362 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt @@ -19,6 +19,7 @@ import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.RealmImpl import io.realm.kotlin.internal.interop.Constants import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.isWindows import io.realm.kotlin.notifications.RealmChange @@ -118,10 +119,19 @@ public interface Realm : TypedRealm { } if (!fileExists(configuration.path)) return false val config = (configuration as InternalConfiguration) - val (dbPointer, _) = RealmInterop.realm_open(config.createNativeConfiguration()) - return RealmInterop.realm_compact(dbPointer).also { - RealmInterop.realm_close(dbPointer) - } + + return RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (dbPointer, _) = RealmInterop.realm_open( + config = config.createNativeConfiguration(), + scheduler = scheduler + ) + try { + RealmInterop.realm_compact(dbPointer) + } finally { + RealmInterop.realm_close(dbPointer) + } + } } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 3826d03dae..559e792985 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.RealmConfigurationPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SchemaMode +import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.prepareRealmFilePath import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow @@ -107,11 +108,14 @@ public open class ConfigurationImpl constructor( override suspend fun openRealm(realm: RealmImpl): Pair { val configPtr = realm.configuration.createNativeConfiguration() - val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr) - val liveRealmReference = LiveRealmReference(realm, dbPointer) - val frozenReference = liveRealmReference.snapshot(realm) - liveRealmReference.close() - return frozenReference to fileCreated + return RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, scheduler) + val liveRealmReference = LiveRealmReference(realm, dbPointer) + val frozenReference = liveRealmReference.snapshot(realm) + liveRealmReference.close() + frozenReference to fileCreated + } } override suspend fun initializeRealmData(realm: RealmImpl, realmFileCreated: Boolean) { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt index 1118589781..f0c7b7fd97 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt @@ -22,9 +22,9 @@ import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SynchronizableObject import io.realm.kotlin.internal.platform.WeakReference import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.internal.util.LiveRealmContext import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext /** @@ -37,13 +37,13 @@ import kotlinx.coroutines.withContext * * @param owner The owner of the snapshot references of this realm. * @param configuration The configuration of the realm. - * @param dispatcher The single thread dispatcher backing the realm scheduler of this realm. The + * @param scheduler The single thread dispatcher backing the realm scheduler of this realm. The * realm itself must only be access on the same thread. */ internal abstract class LiveRealm( val owner: RealmImpl, configuration: InternalConfiguration, - val dispatcher: CoroutineDispatcher + private val scheduler: LiveRealmContext, ) : BaseRealmImpl(configuration) { private val realmChangeRegistration: NotificationToken @@ -52,7 +52,10 @@ internal abstract class LiveRealm( internal val versionTracker = VersionTracker(this, owner.log) override val realmReference: LiveRealmReference by lazy { - val (dbPointer, _) = RealmInterop.realm_open(configuration.createNativeConfiguration(), dispatcher) + val (dbPointer, _) = RealmInterop.realm_open( + configuration.createNativeConfiguration(), + scheduler.scheduler + ) LiveRealmReference(this, dbPointer) } @@ -168,7 +171,7 @@ internal abstract class LiveRealm( * Dump the current snapshot and tracked versions for debugging purpose. */ internal fun versions(): VersionData = runBlocking { - withContext(dispatcher) { + withContext(scheduler.dispatcher) { snapshotLock.withLock { val active = if (!_closeSnapshotWhenAdvancing) { versionTracker.versions() + _snapshot.value.version() diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 2be2d196e5..64d6ed3eaf 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -27,8 +27,9 @@ import io.realm.kotlin.internal.platform.copyAssetFile import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.schema.RealmSchemaImpl -import io.realm.kotlin.internal.util.DispatcherHolder +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.internal.util.Validation.sdkError +import io.realm.kotlin.internal.util.createLiveRealmContext import io.realm.kotlin.internal.util.terminateWhen import io.realm.kotlin.notifications.RealmChange import io.realm.kotlin.notifications.internal.InitialRealmImpl @@ -52,24 +53,30 @@ import kotlin.reflect.KClass // TODO API-PUBLIC Document platform specific internals (RealmInitializer, etc.) // TODO Public due to being accessed from `SyncedRealmContext` public class RealmImpl private constructor( - configuration: InternalConfiguration + configuration: InternalConfiguration, ) : BaseRealmImpl(configuration), Realm, InternalTypedRealm, Flowable> { private val realmPointerMutex = Mutex() - public val notificationDispatcherHolder: DispatcherHolder = - configuration.notificationDispatcherFactory.create() - public val writeDispatcherHolder: DispatcherHolder = - configuration.writeDispatcherFactory.create() + public val notificationScheduler: LiveRealmContext = + configuration.notificationDispatcherFactory.createLiveRealmContext() + + public val writeScheduler: LiveRealmContext = + configuration.writeDispatcherFactory.createLiveRealmContext() internal val realmScope = - CoroutineScope(SupervisorJob() + notificationDispatcherHolder.dispatcher) + CoroutineScope(SupervisorJob() + notificationScheduler.dispatcher) private val notifierFlow: MutableSharedFlow> = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val notifier = - SuspendableNotifier(this, notificationDispatcherHolder.dispatcher) - private val writer = - SuspendableWriter(this, writeDispatcherHolder.dispatcher) + + private val notifier = SuspendableNotifier( + owner = this, + scheduler = notificationScheduler, + ) + private val writer = SuspendableWriter( + owner = this, + scheduler = writeScheduler, + ) // Internal flow to ease monitoring of realm state for closing active flows then the realm is // closed. @@ -272,8 +279,9 @@ public class RealmImpl private constructor( if (!realmStateFlow.tryEmit(State.CLOSED)) { log.warn("Cannot signal internal close") } - notificationDispatcherHolder.close() - writeDispatcherHolder.close() + + notificationScheduler.close() + writeScheduler.close() } internal companion object { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt index dbb91ad499..3d8bfb49e4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -6,6 +6,7 @@ import io.realm.kotlin.internal.interop.RealmChangesPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.schema.RealmSchemaImpl +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.internal.util.Validation.sdkError import io.realm.kotlin.notifications.internal.Cancellable import io.realm.kotlin.notifications.internal.Cancellable.Companion.NO_OP_NOTIFICATION_TOKEN @@ -35,7 +36,7 @@ import kotlinx.coroutines.withContext */ internal class SuspendableNotifier( private val owner: RealmImpl, - private val dispatcher: CoroutineDispatcher + private val scheduler: LiveRealmContext, ) : LiveRealmHolder() { // Flow used to emit events when the version of the live realm is updated // Adding extra buffer capacity as we are otherwise never able to emit anything @@ -45,9 +46,15 @@ internal class SuspendableNotifier( extraBufferCapacity = 1 ) + val dispatcher: CoroutineDispatcher = scheduler.dispatcher + // Could just be anonymous class, but easiest way to get BaseRealmImpl.toString to display the // right type with this - private inner class NotifierRealm : LiveRealm(owner, owner.configuration, dispatcher) { + private inner class NotifierRealm : LiveRealm( + owner = owner, + configuration = owner.configuration, + scheduler = scheduler + ) { // This is guaranteed to be triggered before any other notifications for the same // update as we get all callbacks on the same single thread dispatcher override fun onRealmChanged() { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt index f46b3e3dbe..e18f20b11a 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt @@ -24,6 +24,7 @@ import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.threadId import io.realm.kotlin.internal.schema.RealmClassImpl import io.realm.kotlin.internal.schema.RealmSchemaImpl +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.TypedRealmObject @@ -43,13 +44,26 @@ import kotlin.reflect.KClass * it's thread. * * @param owner The Realm instance needed for emitting updates. - * @param dispatcher The dispatcher on which to execute all the writers operations on. + * @param scheduler The scheduler on which to execute all the writers operations on. */ -internal class SuspendableWriter(private val owner: RealmImpl, val dispatcher: CoroutineDispatcher) : +internal class SuspendableWriter( + private val owner: RealmImpl, + private val scheduler: LiveRealmContext, +) : LiveRealmHolder() { private val tid: ULong - internal inner class WriterRealm : LiveRealm(owner, owner.configuration, dispatcher), InternalMutableRealm, InternalTypedRealm, WriteTransactionManager { + val dispatcher: CoroutineDispatcher = scheduler.dispatcher + + internal inner class WriterRealm : + LiveRealm( + owner = owner, + configuration = owner.configuration, + scheduler = scheduler + ), + InternalMutableRealm, + InternalTypedRealm, + WriteTransactionManager { override val realmReference: LiveRealmReference get() = super.realmReference diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt index a7cddce0c3..370af4d31f 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt @@ -16,12 +16,15 @@ package io.realm.kotlin.internal.util +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmSchedulerPointer import io.realm.kotlin.internal.platform.multiThreadDispatcher +import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlin.jvm.JvmInline +import kotlinx.coroutines.withContext /** * Factory wrapper for passing around dispatchers without needing to create them. This makes it @@ -35,6 +38,7 @@ public fun interface CoroutineDispatcherFactory { * Let Realm create and control the dispatcher. Managed dispatchers will be closed * when their owner Realm/App is closed as well. */ + @OptIn(ExperimentalCoroutinesApi::class) public fun managed(name: String, threads: Int = 1): CoroutineDispatcherFactory { return CoroutineDispatcherFactory { ManagedDispatcherHolder( @@ -88,17 +92,38 @@ public sealed interface DispatcherHolder { public fun close() } -@JvmInline @OptIn(ExperimentalCoroutinesApi::class) -private value class ManagedDispatcherHolder( - override val dispatcher: CloseableCoroutineDispatcher +private class ManagedDispatcherHolder( + override val dispatcher: CloseableCoroutineDispatcher, ) : DispatcherHolder { override fun close(): Unit = dispatcher.close() } -@JvmInline -private value class UnmanagedDispatcherHolder( +private class UnmanagedDispatcherHolder( override val dispatcher: CoroutineDispatcher ) : DispatcherHolder { override fun close(): Unit = Unit } + +/** + * Object that creates a Realm scheduler based and a coroutine dispatcher, and binds their resource + * lifecycle. + */ +public class LiveRealmContext( + private val dispatcherHolder: DispatcherHolder, +) : DispatcherHolder by dispatcherHolder { + + public val scheduler: RealmSchedulerPointer = runBlocking { + withContext(dispatcherHolder.dispatcher) { + RealmInterop.realm_create_scheduler(dispatcher) + } + } + + override fun close() { + scheduler.release() + dispatcherHolder.close() + } +} + +internal fun CoroutineDispatcherFactory.createLiveRealmContext(): LiveRealmContext = + LiveRealmContext(create()) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt index 453e947935..968678cfc9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt @@ -84,19 +84,17 @@ internal class SubscriptionSetImpl( try { val result: Any = withTimeout(timeout) { // TODO Assuming this is always a RealmImpl is probably dangerous. But should be safe until we introduce a public DynamicRealm. - withContext((realm as RealmImpl).notificationDispatcherHolder.dispatcher) { - val callback = object : SubscriptionSetCallback { - override fun onChange(state: CoreSubscriptionSetState) { - when (state) { - CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_COMPLETE -> { - channel.trySend(true) - } - CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_ERROR -> { - channel.trySend(false) - } - else -> { - // Ignore all other states, wait for either complete or error. - } + withContext((realm as RealmImpl).notificationScheduler.dispatcher) { + val callback = SubscriptionSetCallback { state -> + when (state) { + CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_COMPLETE -> { + channel.trySend(true) + } + CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_ERROR -> { + channel.trySend(false) + } + else -> { + // Ignore all other states, wait for either complete or error. } } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt index c23ae4c4d1..3664d5d968 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt @@ -88,7 +88,7 @@ internal class SyncConfigurationImpl( val taskPointer: AtomicRef = atomic(null) try { val result: Any = withTimeout(initialRemoteData.timeout.inWholeMilliseconds) { - withContext(realm.notificationDispatcherHolder.dispatcher) { + withContext(realm.notificationScheduler.dispatcher) { val callback = AsyncOpenCallback { error: Throwable? -> if (error != null) { channel.trySend(error) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 8409b7c556..685bc596b7 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -199,7 +199,7 @@ internal open class SyncSessionImpl( val channel = Channel(1) try { val result: Any = withTimeout(timeout) { - withContext(realm.notificationDispatcherHolder.dispatcher) { + withContext(realm.notificationScheduler.dispatcher) { val callback = object : SyncSessionTransferCompletionCallback { override fun invoke(errorCode: SyncErrorCode?) { if (errorCode != null) { diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt index a860f9c469..5784b90573 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt @@ -20,6 +20,7 @@ package io.realm.kotlin.test import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.dynamic.DynamicMutableRealmImpl import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmSchedulerPointer /** * Special dynamic mutable realm with methods for managing a write transaction. @@ -29,13 +30,26 @@ import io.realm.kotlin.internal.interop.RealmInterop * on it's own shared realm with the ability to manage the write transaction, which allows us to * test the [DynamicMutableRealm] API outside of a migration. */ -internal class StandaloneDynamicMutableRealm(configuration: InternalConfiguration) : +internal class StandaloneDynamicMutableRealm private constructor( + configuration: InternalConfiguration, + private val scheduler: RealmSchedulerPointer, +) : DynamicMutableRealmImpl( configuration, - RealmInterop.realm_open(configuration.createNativeConfiguration(), null) + try { + RealmInterop.realm_open(configuration.createNativeConfiguration(), scheduler) + } catch (exception: Exception) { + scheduler.release() + throw exception + } ) { + constructor(configuration: InternalConfiguration) : this( + configuration, + RealmInterop.realm_create_scheduler() + ) override fun close() { realmReference.close() + scheduler.release() } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt index ac838b5de6..b43ba6c9a6 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt @@ -19,7 +19,10 @@ package io.realm.kotlin.test.common.notifications import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample -import io.realm.kotlin.internal.platform.singleThreadDispatcher +import io.realm.kotlin.internal.RealmImpl +import io.realm.kotlin.internal.SuspendableWriter +import io.realm.kotlin.internal.util.CoroutineDispatcherFactory +import io.realm.kotlin.internal.util.createLiveRealmContext import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.Utils import kotlinx.coroutines.runBlocking @@ -57,10 +60,14 @@ class SystemNotificationTests { @Test fun multipleSchedulersOnSameThread() { Utils.printlntid("main") - val baseRealm = Realm.open(configuration) as io.realm.kotlin.internal.RealmImpl - val dispatcher = singleThreadDispatcher("background") - val writer1 = io.realm.kotlin.internal.SuspendableWriter(baseRealm, dispatcher) - val writer2 = io.realm.kotlin.internal.SuspendableWriter(baseRealm, dispatcher) + val baseRealm = Realm.open(configuration) as RealmImpl + + val liveRealmContext = CoroutineDispatcherFactory + .managed("multipleSchedulersOnSameThread") + .createLiveRealmContext() + + val writer1 = SuspendableWriter(baseRealm, liveRealmContext) + val writer2 = SuspendableWriter(baseRealm, liveRealmContext) runBlocking { baseRealm.write { copyToRealm(Sample()) } writer1.write { copyToRealm(Sample()) } From 64f1096433c1669504268571076f8044ad73b843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Tue, 15 Aug 2023 11:47:44 +0200 Subject: [PATCH 06/27] Add option for automatic resolution of embedded object constraints during migration (#1473) --- CHANGELOG.md | 1 + .../kotlin/internal/interop/RealmInterop.kt | 1 + .../kotlin/internal/interop/RealmInterop.kt | 4 + .../kotlin/internal/interop/RealmInterop.kt | 9 ++ .../io/realm/kotlin/RealmConfiguration.kt | 26 ++++ .../kotlin/internal/ConfigurationImpl.kt | 4 +- .../kotlin/internal/RealmConfigurationImpl.kt | 4 +- .../kotlin/mongodb/sync/SyncConfiguration.kt | 1 + .../embedded/after/EmbeddedMigrationChild.kt | 23 +++ .../embedded/after/EmbeddedMigrationParent.kt | 24 ++++ .../embedded/before/EmbeddedMigrationChild.kt | 23 +++ .../before/EmbeddedMigrationParent.kt | 24 ++++ .../kotlin/test/common/MigrationTests.kt | 132 ++++++++++++++++++ 13 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt create mode 100644 packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt create mode 100644 packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt create mode 100644 packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5d3adfab..489bd20f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements * Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403)) +* Support for automatic resolution of embedded object constraints during migration through `RealmConfiguration.Builder.migration(migration: AutomaticSchemaMigration, resolveEmbeddedObjectConstraints: Boolean)`. (Issue [#1464](https://github.com/realm/realm-kotlin/issues/1464) * [Sync] Add support for customizing authorization headers and adding additional custom headers to all Atlas App service requests with `AppConfiguration.Builder.authorizationHeaderName()` and `AppConfiguration.Builder.addCustomRequestHeader(...)`. (Issue [#1453](https://github.com/realm/realm-kotlin/pull/1453)) ### Fixed 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 f553d15266..33332f2450 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 @@ -186,6 +186,7 @@ expect object RealmInterop { fun realm_config_get_encryption_key(config: RealmConfigurationPointer): ByteArray? fun realm_config_set_should_compact_on_launch_function(config: RealmConfigurationPointer, callback: CompactOnLaunchCallback) fun realm_config_set_migration_function(config: RealmConfigurationPointer, callback: MigrationCallback) + fun realm_config_set_automatic_backlink_handling(config: RealmConfigurationPointer, enabled: Boolean) fun realm_config_set_data_initialization_function(config: RealmConfigurationPointer, callback: DataInitializationCallback) fun realm_config_set_in_memory(config: RealmConfigurationPointer, inMemory: Boolean) fun realm_schema_validate(schema: RealmSchemaPointer, mode: SchemaValidationMode): Boolean 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 3ef0c015d4..88b254ca0b 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 @@ -172,6 +172,10 @@ actual object RealmInterop { realmc.realm_config_set_migration_function(config.cptr(), callback) } + actual fun realm_config_set_automatic_backlink_handling(config: RealmConfigurationPointer, enabled: Boolean) { + realmc.realm_config_set_automatic_backlink_handling(config.cptr(), enabled) + } + actual fun realm_config_set_data_initialization_function(config: RealmConfigurationPointer, callback: DataInitializationCallback) { realmc.realm_config_set_data_initialization_function(config.cptr(), callback) } 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 f569916772..96a5d86e00 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 @@ -433,6 +433,15 @@ actual object RealmInterop { ) } + actual fun realm_config_set_automatic_backlink_handling( + config: RealmConfigurationPointer, + enabled: Boolean + ) { + realm_wrapper.realm_config_set_automatic_backlink_handling( + config.cptr(), + enabled, + ) + } actual fun realm_config_set_migration_function( config: RealmConfigurationPointer, callback: MigrationCallback diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt index 67a854413a..4b46f62364 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.util.CoroutineDispatcherFactory import io.realm.kotlin.log.RealmLog import io.realm.kotlin.log.RealmLogger +import io.realm.kotlin.migration.AutomaticSchemaMigration import io.realm.kotlin.migration.RealmMigration import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.KClass @@ -55,6 +56,7 @@ public interface RealmConfiguration : Configuration { private var directory: String = appFilesDirectory() private var deleteRealmIfMigrationNeeded: Boolean = false private var migration: RealmMigration? = null + private var automaticEmbeddedObjectConstraintsResolution = false /** * Sets the path to the directory that contains the realm file. If the directory does not @@ -109,6 +111,29 @@ public interface RealmConfiguration : Configuration { public fun migration(migration: RealmMigration): Builder = apply { this.migration = migration } + /** + * Sets the migration to handle schema updates with automatic migration of data. + * + * @param migration the [AutomaticSchemaMigration] instance to handle schema and data + * migration in the event of a schema update. + * @param resolveEmbeddedObjectConstraints a flag to indicate whether realm should resolve + * embedded object constraints after migration. If this is `true` then all embedded objects + * without a parent will be deleted and every embedded object with multiple references to it + * will be duplicated so that every referencing object will hold its own copy of the + * embedded object. + * + * @see RealmMigration + * @see AutomaticSchemaMigration + */ + public fun migration( + migration: AutomaticSchemaMigration, + resolveEmbeddedObjectConstraints: Boolean = false + ): Builder = + apply { + this.migration = migration + this.automaticEmbeddedObjectConstraintsResolution = resolveEmbeddedObjectConstraints + } + override fun name(name: String): Builder = apply { checkName(name) this.name = name @@ -163,6 +188,7 @@ public interface RealmConfiguration : Configuration { deleteRealmIfMigrationNeeded, compactOnLaunchCallback, migration, + automaticEmbeddedObjectConstraintsResolution, initialDataCallback, inMemory, initialRealmFileConfiguration, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 3826d03dae..4202311762 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -47,7 +47,7 @@ import kotlin.reflect.KClass // TODO Public due to being accessed from `library-sync` @Suppress("LongParameterList") -public open class ConfigurationImpl constructor( +public open class ConfigurationImpl( directory: String, name: String, schema: Set>, @@ -60,6 +60,7 @@ public open class ConfigurationImpl constructor( private val userEncryptionKey: ByteArray?, compactOnLaunchCallback: CompactOnLaunchCallback?, private val userMigration: RealmMigration?, + automaticBacklinkHandling: Boolean, initialDataCallback: InitialDataCallback?, override val isFlexibleSyncConfiguration: Boolean, inMemory: Boolean, @@ -218,6 +219,7 @@ public open class ConfigurationImpl constructor( migrationCallback?.let { RealmInterop.realm_config_set_migration_function(nativeConfig, it) } + RealmInterop.realm_config_set_automatic_backlink_handling(nativeConfig, automaticBacklinkHandling) userEncryptionKey?.let { key: ByteArray -> RealmInterop.realm_config_set_encryption_key(nativeConfig, key) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt index bbbf8f2304..8b0e620e2d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt @@ -30,7 +30,7 @@ import kotlin.reflect.KClass public const val REALM_FILE_EXTENSION: String = ".realm" @Suppress("LongParameterList") -internal class RealmConfigurationImpl constructor( +internal class RealmConfigurationImpl( directory: String, name: String, schema: Set>, @@ -43,6 +43,7 @@ internal class RealmConfigurationImpl constructor( override val deleteRealmIfMigrationNeeded: Boolean, compactOnLaunchCallback: CompactOnLaunchCallback?, migration: RealmMigration?, + automaticBacklinkHandling: Boolean, initialDataCallback: InitialDataCallback?, inMemory: Boolean, override val initialRealmFileConfiguration: InitialRealmFileConfiguration?, @@ -63,6 +64,7 @@ internal class RealmConfigurationImpl constructor( encryptionKey, compactOnLaunchCallback, migration, + automaticBacklinkHandling, initialDataCallback, false, inMemory, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index db756d4769..6b1e865ae4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -568,6 +568,7 @@ public interface SyncConfiguration : Configuration { encryptionKey, compactOnLaunchCallback, null, // migration is not relevant for sync, + false, // automatic backlink handling is not relevant for sync initialDataCallback, partitionValue == null, inMemory, diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt new file mode 100644 index 0000000000..acbe76a089 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.entities.migration.embedded.after + +import io.realm.kotlin.types.EmbeddedRealmObject + +class EmbeddedMigrationChild : EmbeddedRealmObject { + var id: String = "child" +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt new file mode 100644 index 0000000000..0c24e40fc6 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.entities.migration.embedded.after + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationParent : RealmObject { + var id: String = "parent" + var child: EmbeddedMigrationChild? = null +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt new file mode 100644 index 0000000000..05d2ad15e3 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.entities.migration.embedded.before + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationChild : RealmObject { + var id: String = "child" +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt new file mode 100644 index 0000000000..a75c4af2e7 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.entities.migration.embedded.before + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationParent : RealmObject { + var id: String = "parent" + var child: EmbeddedMigrationChild? = null +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt index 95bc2f555d..3bff6902d1 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt @@ -21,9 +21,15 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.link.Child import io.realm.kotlin.entities.link.Parent +import io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild +import io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent import io.realm.kotlin.ext.query +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.migration.AutomaticSchemaMigration import io.realm.kotlin.query.find +import io.realm.kotlin.test.common.utils.assertFailsWithMessage import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.use import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -178,6 +184,132 @@ class MigrationTests { } } + @Test + fun migrationThrowsOnViolatingEmbeddedObjectConstraints() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open(initialConfiguration).use { + it.write { + copyToRealm(EmbeddedMigrationChild().apply { id = "orphaned-child" }) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }) + .build() + + assertFailsWithMessage("Cannot convert 'EmbeddedMigrationChild' to embedded: at least one object has no incoming links and would be deleted.") { + Realm.open(migratedConfiguration).use { } + } + } + + @Test + fun automaticBacklinkHandling_deleteOrphanedChildren() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open(initialConfiguration).use { + it.write { + copyToRealm( + EmbeddedMigrationParent().apply { + child = EmbeddedMigrationChild().apply { id = "child-with-parent" } + } + ) + copyToRealm(EmbeddedMigrationChild().apply { id = "orphaned-child" }) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }, resolveEmbeddedObjectConstraints = true) + .build() + + Realm.open(migratedConfiguration).use { + val childWithParent = + it.query() + .find().single() + assertEquals("child-with-parent", childWithParent.id) + } + } + + @Test + fun automaticBacklinkHandling_cloneDuplicateReferences() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open( + initialConfiguration + ).use { + it.write { + // Add two parents referencing the same child + val child = copyToRealm(EmbeddedMigrationChild().apply { id = "child-with-parent" }) + copyToRealm( + EmbeddedMigrationParent().apply { + id = "mom1" + this.child = child + } + ) + copyToRealm( + EmbeddedMigrationParent().apply { + id = "mom2" + this.child = child + } + ) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }, resolveEmbeddedObjectConstraints = true) + .build() + + Realm.open(migratedConfiguration).use { + assertEquals( + 2, + it.query() + .find().count() + ) + } + } + // TODO add test for adding/remove columns when we have an API to open with an existing Realm. // https://github.com/realm/realm-kotlin/issues/304 } From 310556e0a184f85772da0c2b116f4660b6c671a5 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Tue, 15 Aug 2023 12:47:46 +0200 Subject: [PATCH 07/27] Remove all deprecated APIs from the compiler plugin. (#1476) --- .../compiler/AccessorModifierIrGeneration.kt | 138 +++++++++--------- .../io/realm/kotlin/compiler/Identifiers.kt | 114 ++++++++------- .../io/realm/kotlin/compiler/IrUtils.kt | 100 ++++++++----- .../compiler/RealmModelLoweringExtension.kt | 6 +- ...RealmModelSyntheticPropertiesGeneration.kt | 77 +++++----- .../kotlin/compiler/SyncLoweringExtension.kt | 6 +- .../kotlin/test/compiler/CollectionTests.kt | 2 +- 7 files changed, 240 insertions(+), 203 deletions(-) diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt index 428929dace..ddf3f025f8 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt @@ -13,28 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.ASYMMETRIC_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.EMBEDDED_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.IGNORE_ANNOTATION -import io.realm.kotlin.compiler.FqNames.KBSON_DECIMAL128 -import io.realm.kotlin.compiler.FqNames.KBSON_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_ANY -import io.realm.kotlin.compiler.FqNames.REALM_BACKLINKS -import io.realm.kotlin.compiler.FqNames.REALM_DICTIONARY -import io.realm.kotlin.compiler.FqNames.REALM_EMBEDDED_BACKLINKS -import io.realm.kotlin.compiler.FqNames.REALM_INSTANT -import io.realm.kotlin.compiler.FqNames.REALM_LIST -import io.realm.kotlin.compiler.FqNames.REALM_MUTABLE_INTEGER -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_HELPER -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_SET -import io.realm.kotlin.compiler.FqNames.REALM_UUID -import io.realm.kotlin.compiler.FqNames.TRANSIENT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.ASYMMETRIC_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.EMBEDDED_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.IGNORE_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.KBSON_DECIMAL128 +import io.realm.kotlin.compiler.ClassIds.KBSON_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_ANY +import io.realm.kotlin.compiler.ClassIds.REALM_BACKLINKS +import io.realm.kotlin.compiler.ClassIds.REALM_DICTIONARY +import io.realm.kotlin.compiler.ClassIds.REALM_EMBEDDED_BACKLINKS +import io.realm.kotlin.compiler.ClassIds.REALM_INSTANT +import io.realm.kotlin.compiler.ClassIds.REALM_LIST +import io.realm.kotlin.compiler.ClassIds.REALM_MUTABLE_INTEGER +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_HELPER +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_SET +import io.realm.kotlin.compiler.ClassIds.REALM_UUID +import io.realm.kotlin.compiler.ClassIds.TRANSIENT_ANNOTATION import io.realm.kotlin.compiler.Names.OBJECT_REFERENCE import io.realm.kotlin.compiler.Names.REALM_ACCESSOR_HELPER_GET_BOOLEAN import io.realm.kotlin.compiler.Names.REALM_ACCESSOR_HELPER_GET_BYTE_ARRAY @@ -59,10 +58,8 @@ import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_LIST import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_OBJECT import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_SET import io.realm.kotlin.compiler.Names.REALM_SYNTHETIC_PROPERTY_PREFIX -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.IrStatement -import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.builders.IrBlockBuilder import org.jetbrains.kotlin.ir.builders.Scope import org.jetbrains.kotlin.ir.builders.irBlockBody @@ -78,6 +75,8 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.descriptors.toIrBasedDescriptor +import org.jetbrains.kotlin.ir.descriptors.toIrBasedKotlinType import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrDeclarationReference import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin @@ -85,7 +84,6 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrSetFieldImpl import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.ir.types.classifierOrFail import org.jetbrains.kotlin.ir.types.impl.IrAbstractSimpleType import org.jetbrains.kotlin.ir.types.isBoolean import org.jetbrains.kotlin.ir.types.isByte @@ -100,16 +98,16 @@ import org.jetbrains.kotlin.ir.types.isShort import org.jetbrains.kotlin.ir.types.isString import org.jetbrains.kotlin.ir.types.isSubtypeOfClass import org.jetbrains.kotlin.ir.types.makeNotNull -import org.jetbrains.kotlin.ir.types.toKotlinType +import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.defaultType -import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.parentAsClass import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.classId -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.StarProjectionImpl import org.jetbrains.kotlin.types.isNullable @@ -120,7 +118,6 @@ import kotlin.collections.set * Modifies the IR tree to transform getter/setter to call the C-Interop layer to retrieve read the managed values from the Realm * It also collect the schema information while processing the class properties. */ -@OptIn(ObsoleteDescriptorBasedAPI::class) class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(REALM_OBJECT_HELPER) @@ -192,25 +189,25 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { // Top level SDK->Core converters private val byteToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.byteToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("byteToLong"))).first().owner private val charToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.charToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("charToLong"))).first().owner private val shortToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.shortToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("shortToLong"))).first().owner private val intToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.intToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("intToLong"))).first().owner // Top level Core->SDK converters private val longToByte: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToByte")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToByte"))).first().owner private val longToChar: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToChar")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToChar"))).first().owner private val longToShort: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToShort")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToShort"))).first().owner private val longToInt: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToInt")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToInt"))).first().owner private val objectIdToRealmObjectId: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.objectIdToRealmObjectId")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("objectIdToRealmObjectId"))).first().owner private lateinit var objectReferenceProperty: IrProperty private lateinit var objectReferenceType: IrType @@ -546,7 +543,7 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { if (!(isValidTargetType || isValidGenericType)) { val targetPropertyName = getLinkingObjectPropertyName(declaration.backingField!!) logError( - "Error in backlinks field '${declaration.name}' - target property '$targetPropertyName' does not reference '${sourceType.toKotlinType()}'.", + "Error in backlinks field '${declaration.name}' - target property '$targetPropertyName' does not reference '${sourceType.toIrBasedKotlinType().getKotlinTypeFqNameCompat(true)}'.", declaration.locationOf() ) } @@ -715,7 +712,7 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { name: String, declaration: IrProperty ) { - val type = declaration.symbol.descriptor.type + val type: KotlinType = declaration.symbol.owner.toIrBasedDescriptor().type if (type.arguments[0] is StarProjectionImpl) { logError( "Error in field ${declaration.name} - ${collectionType.description} cannot use a '*' projection.", @@ -960,80 +957,80 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { } private fun IrType.isRealmList(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmListClassId = realmListClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmListClassId: ClassId? = realmListClass.classId return propertyClassId == realmListClassId } private fun IrType.isRealmSet(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmSetClassId = realmSetClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmSetClassId: ClassId? = realmSetClass.classId return propertyClassId == realmSetClassId } private fun IrType.isRealmDictionary(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmDictionaryClassId = realmDictionaryClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmDictionaryClassId: ClassId? = realmDictionaryClass.classId return propertyClassId == realmDictionaryClassId } private fun IrType.isRealmInstant(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmInstantClassId = realmInstantClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmInstantClassId: ClassId? = realmInstantClass.classId return propertyClassId == realmInstantClassId } private fun IrType.isLinkingObject(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmBacklinksClassId: ClassId? = realmBacklinksClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmBacklinksClassId: ClassId? = realmBacklinksClass.classId return propertyClassId == realmBacklinksClassId } private fun IrType.isEmbeddedLinkingObject(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmEmbeddedBacklinksClassId: ClassId? = realmEmbeddedBacklinksClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmEmbeddedBacklinksClassId: ClassId? = realmEmbeddedBacklinksClass.classId return propertyClassId == realmEmbeddedBacklinksClassId } private fun IrType.isDecimal128(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = decimal128Class.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = decimal128Class.classId return propertyClassId == objectIdClassId } private fun IrType.isObjectId(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = objectIdClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = objectIdClass.classId return propertyClassId == objectIdClassId } private fun IrType.isRealmObjectId(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = realmObjectIdClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = realmObjectIdClass.classId return propertyClassId == objectIdClassId } private fun IrType.hasSameClassId(other: IrType): Boolean { - val classId = this.classifierOrFail.descriptor.classId - val otherClassId = other.classifierOrFail.descriptor.classId - return classId == otherClassId + val propertyClassId: ClassId = this.classIdOrFail() + val otherClassId = other.classIdOrFail() + return propertyClassId == otherClassId } private fun IrType.isRealmUUID(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmUUIDClassId = realmUUIDClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmUUIDClassId: ClassId? = realmUUIDClass.classId return propertyClassId == realmUUIDClassId } fun IrType.isMutableRealmInteger(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val mutableRealmIntegerClassId = mutableRealmIntegerClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val mutableRealmIntegerClassId: ClassId? = mutableRealmIntegerClass.classId return propertyClassId == mutableRealmIntegerClassId } fun IrType.isRealmAny(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val mutableRealmIntegerClassId = realmAnyClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val mutableRealmIntegerClassId: ClassId? = realmAnyClass.classId return propertyClassId == mutableRealmIntegerClassId } @@ -1043,8 +1040,8 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { declaration: IrProperty ): CoreType? { // Check first if the generic is a subclass of RealmObject - val descriptorType = declaration.symbol.descriptor.type - val collectionGenericType = descriptorType.arguments[0].type + val descriptorType: KotlinType = declaration.toIrBasedDescriptor().type + val collectionGenericType: KotlinType = descriptorType.arguments[0].type val supertypes = collectionGenericType.constructor.supertypes val isEmbedded = inheritsFromRealmObject(supertypes, RealmObjectType.EMBEDDED) @@ -1101,10 +1098,11 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { } // Otherwise just return the matching core type present in the declaration - val genericPropertyType = getPropertyTypeFromKotlinType(collectionGenericType) + val genericPropertyType: PropertyType? = getPropertyTypeFromKotlinType(collectionGenericType) return if (genericPropertyType == null) { logError( - "Unsupported type for ${collectionType.description}: '$collectionGenericType'", + "Unsupported type for ${collectionType.description}: '${collectionGenericType.getKotlinTypeFqNameCompat(true) + }'", declaration.locationOf() ) null @@ -1160,12 +1158,12 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { supertypes: Collection, objectType: RealmObjectType = RealmObjectType.EITHER ): Boolean = supertypes.any { - val objectFqNames = when (objectType) { + val objectFqNames: Set = when (objectType) { RealmObjectType.OBJECT -> realmObjectInterfaceFqNames RealmObjectType.EMBEDDED -> realmEmbeddedObjectInterfaceFqNames RealmObjectType.EITHER -> realmObjectInterfaceFqNames + realmEmbeddedObjectInterfaceFqNames } - it.constructor.declarationDescriptor?.fqNameSafe in objectFqNames + it.constructor.declarationDescriptor?.classId in objectFqNames } } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt index 27111250af..c4d1a87176 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt @@ -16,9 +16,9 @@ package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.CLASS_APP_CONFIGURATION import io.realm.kotlin.compiler.FqNames.PACKAGE_MONGODB import io.realm.kotlin.compiler.FqNames.PACKAGE_MONGODB_INTERNAL +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -86,69 +86,79 @@ internal object Names { } internal object FqNames { + val PACKAGE_ANNOTATIONS = FqName("io.realm.kotlin.types.annotations") + val PACKAGE_KBSON = FqName("org.mongodb.kbson") + val PACKAGE_KOTLIN_COLLECTIONS = FqName("kotlin.collections") + val PACKAGE_KOTLIN_REFLECT = FqName("kotlin.reflect") + val PACKAGE_TYPES = FqName("io.realm.kotlin.types") + val PACKAGE_REALM_INTEROP = FqName("io.realm.kotlin.internal.interop") + val PACKAGE_REALM_INTERNAL = FqName("io.realm.kotlin.internal") + val PACKAGE_MONGODB = FqName("io.realm.kotlin.mongodb") + val PACKAGE_MONGODB_INTERNAL = FqName("io.realm.kotlin.mongodb.internal") +} + +object ClassIds { + // TODO we can replace with RealmObject::class.java.canonicalName if we make the runtime_api available as a compile time only dependency for the compiler-plugin val REALM_NATIVE_POINTER = FqName("io.realm.kotlin.internal.interop.NativePointer") - val REALM_OBJECT_INTERNAL_INTERFACE = FqName("io.realm.kotlin.internal.RealmObjectInternal") + val REALM_OBJECT_INTERNAL_INTERFACE = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectInternal")) + + val REALM_MODEL_COMPANION = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectCompanion")) + val REALM_OBJECT_HELPER = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectHelper")) + val REALM_CLASS_IMPL = ClassId(FqName("io.realm.kotlin.internal.schema"), Name.identifier("RealmClassImpl")) + val OBJECT_REFERENCE_CLASS = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectReference")) - val REALM_MODEL_COMPANION = FqName("io.realm.kotlin.internal.RealmObjectCompanion") - val REALM_OBJECT_HELPER = FqName("io.realm.kotlin.internal.RealmObjectHelper") - val REALM_CLASS_IMPL = FqName("io.realm.kotlin.internal.schema.RealmClassImpl") - val OBJECT_REFERENCE_CLASS = FqName("io.realm.kotlin.internal.RealmObjectReference") + val BASE_REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("BaseRealmObject")) + val REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmObject")) + val TYPED_REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("TypedRealmObject")) + val EMBEDDED_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("EmbeddedRealmObject")) + val ASYMMETRIC_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("AsymmetricRealmObject")) - val BASE_REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.BaseRealmObject") - val REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.RealmObject") - val TYPED_REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.TypedRealmObject") - val EMBEDDED_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.EmbeddedRealmObject") - val ASYMMETRIC_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.AsymmetricRealmObject") + val CLASS_APP_CONFIGURATION = ClassId(FqNames.PACKAGE_MONGODB, Name.identifier("AppConfiguration")) // External visible interface of Realm objects - val KOTLIN_COLLECTIONS_SET = FqName("kotlin.collections.Set") - val KOTLIN_COLLECTIONS_LIST = FqName("kotlin.collections.List") - val KOTLIN_COLLECTIONS_LISTOF = FqName("kotlin.collections.listOf") - val KOTLIN_COLLECTIONS_MAP = FqName("kotlin.collections.Map") - val KOTLIN_COLLECTIONS_MAPOF = FqName("kotlin.collections.mapOf") - val KOTLIN_REFLECT_KMUTABLEPROPERTY1 = FqName("kotlin.reflect.KMutableProperty1") - val KOTLIN_REFLECT_KPROPERTY1 = FqName("kotlin.reflect.KProperty1") - val KOTLIN_PAIR = FqName("kotlin.Pair") + val KOTLIN_COLLECTIONS_SET = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("Set")) + val KOTLIN_COLLECTIONS_LIST = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("List")) + val KOTLIN_COLLECTIONS_LISTOF = CallableId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("listOf")) + val KOTLIN_COLLECTIONS_MAP = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("Map")) + val KOTLIN_COLLECTIONS_MAPOF = CallableId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("mapOf")) + val KOTLIN_REFLECT_KMUTABLEPROPERTY1 = ClassId(FqNames.PACKAGE_KOTLIN_REFLECT, Name.identifier("KMutableProperty1")) + val KOTLIN_REFLECT_KPROPERTY1 = ClassId(FqNames.PACKAGE_KOTLIN_REFLECT, Name.identifier("KProperty1")) + val KOTLIN_PAIR = ClassId(FqName("kotlin"), Name.identifier("Pair")) // Schema related types - val CLASS_INFO = FqName("io.realm.kotlin.internal.interop.ClassInfo") - val PROPERTY_INFO = FqName("io.realm.kotlin.internal.interop.PropertyInfo") - val PROPERTY_TYPE = FqName("io.realm.kotlin.internal.interop.PropertyType") - val COLLECTION_TYPE = FqName("io.realm.kotlin.internal.interop.CollectionType") - val PRIMARY_KEY_ANNOTATION = FqName("io.realm.kotlin.types.annotations.PrimaryKey") - val INDEX_ANNOTATION = FqName("io.realm.kotlin.types.annotations.Index") - val FULLTEXT_ANNOTATION = FqName("io.realm.kotlin.types.annotations.FullText") - val IGNORE_ANNOTATION = FqName("io.realm.kotlin.types.annotations.Ignore") - val PERSISTED_NAME_ANNOTATION = FqName("io.realm.kotlin.types.annotations.PersistedName") - val TRANSIENT_ANNOTATION = FqName("kotlin.jvm.Transient") - val MODEL_OBJECT_ANNOTATION = FqName("io.realm.kotlin.internal.platform.ModelObject") - val PROPERTY_INFO_CREATE = FqName("io.realm.kotlin.internal.schema.createPropertyInfo") - val CLASS_KIND_TYPE = FqName("io.realm.kotlin.schema.RealmClassKind") + val CLASS_INFO = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("ClassInfo")) + val PROPERTY_INFO = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("PropertyInfo")) + val PROPERTY_TYPE = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("PropertyType")) + val COLLECTION_TYPE = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("CollectionType")) + val PRIMARY_KEY_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("PrimaryKey")) + val INDEX_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("Index")) + val FULLTEXT_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("FullText")) + val IGNORE_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("Ignore")) + val PERSISTED_NAME_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("PersistedName")) + val TRANSIENT_ANNOTATION = ClassId(FqName("kotlin.jvm"), Name.identifier("Transient")) + val MODEL_OBJECT_ANNOTATION = ClassId(FqName("io.realm.kotlin.internal.platform"), Name.identifier("ModelObject")) + val PROPERTY_INFO_CREATE = CallableId(FqName("io.realm.kotlin.internal.schema"), Name.identifier("createPropertyInfo")) + val CLASS_KIND_TYPE = ClassId(FqName("io.realm.kotlin.schema"), Name.identifier("RealmClassKind")) // Realm data types - val REALM_LIST = FqName("io.realm.kotlin.types.RealmList") - val REALM_SET = FqName("io.realm.kotlin.types.RealmSet") - val REALM_DICTIONARY = FqName("io.realm.kotlin.types.RealmDictionary") - val REALM_INSTANT = FqName("io.realm.kotlin.types.RealmInstant") - val REALM_BACKLINKS = FqName("io.realm.kotlin.types.BacklinksDelegate") - val REALM_EMBEDDED_BACKLINKS = FqName("io.realm.kotlin.types.EmbeddedBacklinksDelegate") - val REALM_OBJECT_ID = FqName("io.realm.kotlin.types.ObjectId") - val KBSON_OBJECT_ID = FqName("org.mongodb.kbson.BsonObjectId") - val KBSON_DECIMAL128 = FqName("org.mongodb.kbson.BsonDecimal128") - val REALM_UUID = FqName("io.realm.kotlin.types.RealmUUID") - val REALM_MUTABLE_INTEGER = FqName("io.realm.kotlin.types.MutableRealmInt") - val REALM_ANY = FqName("io.realm.kotlin.types.RealmAny") - - val PACKAGE_MONGODB = FqName("io.realm.kotlin.mongodb") - val PACKAGE_MONGODB_INTERNAL = FqName("io.realm.kotlin.mongodb.internal") - val CLASS_APP_CONFIGURATION = FqName("io.realm.kotlin.mongodb.AppConfiguration") -} - -object ClassIds { + val REALM_LIST = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmList")) + val REALM_SET = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmSet")) + val REALM_DICTIONARY = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmDictionary")) + val REALM_INSTANT = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmInstant")) + val REALM_BACKLINKS = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("BacklinksDelegate")) + val REALM_EMBEDDED_BACKLINKS = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("EmbeddedBacklinksDelegate")) + val REALM_OBJECT_ID = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("ObjectId")) + val KBSON_OBJECT_ID = ClassId(FqNames.PACKAGE_KBSON, Name.identifier("BsonObjectId")) + val KBSON_DECIMAL128 = ClassId(FqNames.PACKAGE_KBSON, Name.identifier("BsonDecimal128")) + val REALM_UUID = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmUUID")) + val REALM_MUTABLE_INTEGER = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("MutableRealmInt")) + val REALM_ANY = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmAny")) + + // Sync types val APP = ClassId(PACKAGE_MONGODB, Name.identifier("App")) val APP_IMPL = ClassId(PACKAGE_MONGODB_INTERNAL, Name.identifier("AppImpl")) val APP_CONFIGURATION = ClassId(PACKAGE_MONGODB, Name.identifier("AppConfiguration")) val APP_CONFIGURATION_IMPL = ClassId(PACKAGE_MONGODB_INTERNAL, Name.identifier("AppConfigurationImpl")) - val APP_CONFIGURATION_BUILDER = ClassId(CLASS_APP_CONFIGURATION, Name.identifier("Builder")) + val APP_CONFIGURATION_BUILDER = ClassId(FqName("io.realm.kotlin.mongodb.AppConfiguration"), FqName("Builder"), true) } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt index 49d337ae5d..936b0741b6 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt @@ -13,25 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.ASYMMETRIC_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.BASE_REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.EMBEDDED_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_LISTOF -import io.realm.kotlin.compiler.FqNames.PERSISTED_NAME_ANNOTATION -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI +import io.realm.kotlin.compiler.ClassIds.ASYMMETRIC_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.BASE_REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.EMBEDDED_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_LISTOF +import io.realm.kotlin.compiler.ClassIds.PERSISTED_NAME_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocationWithRange import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation +import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder import org.jetbrains.kotlin.ir.builders.IrBlockBuilder @@ -49,6 +50,7 @@ import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.builders.irBlockBody import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin @@ -82,13 +84,12 @@ import org.jetbrains.kotlin.ir.symbols.IrValueSymbol import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.impl.IrAbstractSimpleType -import org.jetbrains.kotlin.ir.types.impl.IrErrorClassImpl.superTypes import org.jetbrains.kotlin.ir.types.impl.IrTypeBase import org.jetbrains.kotlin.ir.types.makeNullable -import org.jetbrains.kotlin.ir.types.superTypes import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.companionObject import org.jetbrains.kotlin.ir.util.copyTo import org.jetbrains.kotlin.ir.util.file @@ -97,12 +98,16 @@ import org.jetbrains.kotlin.ir.util.getPropertyGetter import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.isVararg import org.jetbrains.kotlin.ir.util.properties +import org.jetbrains.kotlin.ir.util.render import org.jetbrains.kotlin.ir.util.superTypes import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.SUPER_TYPE_LIST +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.types.KotlinType import java.lang.reflect.Field import java.util.function.Predicate @@ -129,11 +134,13 @@ fun IrPluginContext.blockBody( val ClassDescriptor.isRealmObjectCompanion get() = isCompanionObject && (containingDeclaration as ClassDescriptor).isBaseRealmObject -val realmObjectInterfaceFqNames = setOf(FqNames.REALM_OBJECT_INTERFACE) -val realmEmbeddedObjectInterfaceFqNames = setOf(FqNames.EMBEDDED_OBJECT_INTERFACE) -val realmAsymmetricObjectInterfaceFqNames = setOf(FqNames.ASYMMETRIC_OBJECT_INTERFACE) +val realmObjectInterfaceFqNames = setOf(REALM_OBJECT_INTERFACE) +val realmEmbeddedObjectInterfaceFqNames = setOf(EMBEDDED_OBJECT_INTERFACE) +val realmAsymmetricObjectInterfaceFqNames = setOf(ASYMMETRIC_OBJECT_INTERFACE) val anyRealmObjectInterfacesFqNames = realmObjectInterfaceFqNames + realmEmbeddedObjectInterfaceFqNames + realmAsymmetricObjectInterfaceFqNames +fun IrType.classIdOrFail(): ClassId = getClass()?.classId ?: error("Can't get classId of ${render()}") + inline fun ClassDescriptor.hasInterfacePsi(interfaces: Set): Boolean { // Using PSI to find super types to avoid cyclic reference (see https://github.com/realm/realm-kotlin/issues/339) var hasRealmObjectAsSuperType = false @@ -179,27 +186,37 @@ val ClassDescriptor.isEmbeddedRealmObject: Boolean val ClassDescriptor.isBaseRealmObject: Boolean get() = this.hasInterfacePsi(realmObjectPsiNames + embeddedRealmObjectPsiNames + asymmetricRealmObjectPsiNames) && !this.hasInterfacePsi(realmJavaObjectPsiNames) +// JetBrains already have a method `fun IrAnnotationContainer.hasAnnotation(symbol: IrClassSymbol)` +// It is unclear exactly what the difference is and how to get a ClassSymbol from a ClassId, +// so for now just work around it. +fun IrAnnotationContainer?.hasAnnotation(annotation: ClassId): Boolean { + return this?.hasAnnotation(annotation.asSingleFqName()) ?: false +} + fun IrMutableAnnotationContainer.hasAnnotation(annotation: FqName): Boolean { return annotations.hasAnnotation(annotation) } val IrClass.isBaseRealmObject - get() = superTypes.any { it.classFqName in anyRealmObjectInterfacesFqNames } + get() = superTypes.any { it.classId in anyRealmObjectInterfacesFqNames } val IrClass.isRealmObject - get() = superTypes.any { it.classFqName == BASE_REALM_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == BASE_REALM_OBJECT_INTERFACE } val IrClass.isEmbeddedRealmObject: Boolean - get() = superTypes.any { it.classFqName == EMBEDDED_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == EMBEDDED_OBJECT_INTERFACE } val IrClass.isAsymmetricRealmObject: Boolean - get() = superTypes.any { it.classFqName == ASYMMETRIC_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == ASYMMETRIC_OBJECT_INTERFACE } + +val IrType.classId: ClassId? + get() = this.getClass()?.classId val IrType.isEmbeddedRealmObject: Boolean - get() = superTypes().any { it.classFqName == EMBEDDED_OBJECT_INTERFACE } + get() = superTypes().any { it.classId == EMBEDDED_OBJECT_INTERFACE } val IrType.isAsymmetricRealmObject: Boolean - get() = superTypes().any { it.classFqName == ASYMMETRIC_OBJECT_INTERFACE } + get() = superTypes().any { it.classId == ASYMMETRIC_OBJECT_INTERFACE } internal fun IrFunctionBuilder.at(startOffset: Int, endOffset: Int) = also { this.startOffset = startOffset @@ -227,10 +244,10 @@ internal fun IrClass.lookupProperty(name: Name): IrProperty { } internal fun IrPluginContext.lookupFunctionInClass( - fqName: FqName, + clazz: ClassId, function: String ): IrSimpleFunction { - return lookupClassOrThrow(fqName).functions.first { + return lookupClassOrThrow(clazz).functions.first { it.name == Name.identifier(function) } } @@ -240,16 +257,11 @@ internal fun IrPluginContext.lookupClassOrThrow(name: ClassId): IrClass { ?: fatalError("Cannot find ${name.asString()} on platform $platform.") } -internal fun IrPluginContext.lookupClassOrThrow(name: FqName): IrClass { - return referenceClass(name)?.owner - ?: fatalError("Cannot find ${name.asString()} on platform $platform.") -} - internal fun IrPluginContext.lookupConstructorInClass( - fqName: FqName, + clazz: ClassId, filter: (ctor: IrConstructorSymbol) -> Boolean = { true } ): IrConstructorSymbol { - return referenceConstructors(fqName).first { + return referenceConstructors(clazz).first { filter(it) } } @@ -263,6 +275,28 @@ internal fun IrClass.lookupCompanionDeclaration( ?: fatalError("Cannot find companion method ${name.asString()} on ${this.name}") } +// Copy of `KotlinType.getKotlinTypeFqName` from Kotlin 1.8.21. This method needs to be backported +// as it is not available in Kotlin 1.8.0. +internal fun KotlinType.getKotlinTypeFqNameCompat(printTypeArguments: Boolean): String { + val declaration = requireNotNull(constructor.declarationDescriptor) { + "declarationDescriptor is null for constructor = $constructor with ${constructor.javaClass}" + } + if (declaration is TypeParameterDescriptor) { + return StringUtil.join(declaration.upperBounds, { type -> type.getKotlinTypeFqNameCompat(printTypeArguments) }, "&") + } + + val typeArguments = arguments + val typeArgumentsAsString = if (printTypeArguments && !typeArguments.isEmpty()) { + val joinedTypeArguments = StringUtil.join(typeArguments, { projection -> projection.type.getKotlinTypeFqNameCompat(false) }, ", ") + + "<$joinedTypeArguments>" + } else { + "" + } + + return DescriptorUtils.getFqName(declaration).asString() + typeArgumentsAsString +} + object SchemaCollector { val properties = mutableMapOf>() } @@ -344,7 +378,7 @@ data class SchemaProperty( companion object { fun getPersistedName(declaration: IrProperty): String { @Suppress("UNCHECKED_CAST") - return (declaration.getAnnotation(PERSISTED_NAME_ANNOTATION).getValueArgument(0)!! as IrConstImpl).value + return (declaration.getAnnotation(PERSISTED_NAME_ANNOTATION.asSingleFqName()).getValueArgument(0)!! as IrConstImpl).value } } } @@ -391,12 +425,12 @@ internal fun buildSetOf( elementType: IrType, args: List ): IrExpression { - val setOf = context.referenceFunctions(FqName("kotlin.collections.setOf")) + val setOf = context.referenceFunctions(CallableId(FqName("kotlin.collections"), Name.identifier("setOf"))) .first { val parameters = it.owner.valueParameters parameters.size == 1 && parameters.first().isVararg } - val setIrClass: IrClass = context.lookupClassOrThrow(FqNames.KOTLIN_COLLECTIONS_SET) + val setIrClass: IrClass = context.lookupClassOrThrow(ClassIds.KOTLIN_COLLECTIONS_SET) return buildOf(context, startOffset, endOffset, setOf, setIrClass, elementType, args) } @@ -412,7 +446,7 @@ internal fun buildListOf( val parameters = it.owner.valueParameters parameters.size == 1 && parameters.first().isVararg } - val listIrClass: IrClass = context.lookupClassOrThrow(FqNames.KOTLIN_COLLECTIONS_LIST) + val listIrClass: IrClass = context.lookupClassOrThrow(ClassIds.KOTLIN_COLLECTIONS_LIST) return buildOf(context, startOffset, endOffset, listOf, listIrClass, elementType, args) } @@ -602,7 +636,7 @@ fun getLinkingObjectPropertyName(backingField: IrField): String { fun getSchemaClassName(clazz: IrClass): String { return if (clazz.hasAnnotation(PERSISTED_NAME_ANNOTATION)) { @Suppress("UNCHECKED_CAST") - return (clazz.getAnnotation(PERSISTED_NAME_ANNOTATION).getValueArgument(0)!! as IrConstImpl).value + return (clazz.getAnnotation(PERSISTED_NAME_ANNOTATION.asSingleFqName()).getValueArgument(0)!! as IrConstImpl).value } else { clazz.name.identifier } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt index 9529a5124b..4e590cf6ab 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt @@ -16,9 +16,9 @@ package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.MODEL_OBJECT_ANNOTATION -import io.realm.kotlin.compiler.FqNames.REALM_MODEL_COMPANION -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERNAL_INTERFACE +import io.realm.kotlin.compiler.ClassIds.MODEL_OBJECT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.REALM_MODEL_COMPANION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERNAL_INTERFACE import org.jetbrains.kotlin.backend.common.ClassLoweringPass import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt index 868fe782a9..db43326fcf 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt @@ -13,32 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.CLASS_INFO -import io.realm.kotlin.compiler.FqNames.CLASS_KIND_TYPE -import io.realm.kotlin.compiler.FqNames.COLLECTION_TYPE -import io.realm.kotlin.compiler.FqNames.FULLTEXT_ANNOTATION -import io.realm.kotlin.compiler.FqNames.INDEX_ANNOTATION -import io.realm.kotlin.compiler.FqNames.KBSON_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_MAP -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_MAPOF -import io.realm.kotlin.compiler.FqNames.KOTLIN_PAIR -import io.realm.kotlin.compiler.FqNames.OBJECT_REFERENCE_CLASS -import io.realm.kotlin.compiler.FqNames.PRIMARY_KEY_ANNOTATION -import io.realm.kotlin.compiler.FqNames.PROPERTY_INFO -import io.realm.kotlin.compiler.FqNames.PROPERTY_INFO_CREATE -import io.realm.kotlin.compiler.FqNames.PROPERTY_TYPE -import io.realm.kotlin.compiler.FqNames.REALM_ANY -import io.realm.kotlin.compiler.FqNames.REALM_INSTANT -import io.realm.kotlin.compiler.FqNames.REALM_MODEL_COMPANION -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERNAL_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_UUID -import io.realm.kotlin.compiler.FqNames.TYPED_REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.CLASS_INFO +import io.realm.kotlin.compiler.ClassIds.CLASS_KIND_TYPE +import io.realm.kotlin.compiler.ClassIds.COLLECTION_TYPE +import io.realm.kotlin.compiler.ClassIds.FULLTEXT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.INDEX_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.KBSON_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_MAP +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_MAPOF +import io.realm.kotlin.compiler.ClassIds.KOTLIN_PAIR +import io.realm.kotlin.compiler.ClassIds.OBJECT_REFERENCE_CLASS +import io.realm.kotlin.compiler.ClassIds.PRIMARY_KEY_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.PROPERTY_INFO +import io.realm.kotlin.compiler.ClassIds.PROPERTY_INFO_CREATE +import io.realm.kotlin.compiler.ClassIds.PROPERTY_TYPE +import io.realm.kotlin.compiler.ClassIds.REALM_ANY +import io.realm.kotlin.compiler.ClassIds.REALM_INSTANT +import io.realm.kotlin.compiler.ClassIds.REALM_MODEL_COMPANION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERNAL_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_UUID +import io.realm.kotlin.compiler.ClassIds.TYPED_REALM_OBJECT_INTERFACE import io.realm.kotlin.compiler.Names.CLASS_INFO_CREATE import io.realm.kotlin.compiler.Names.OBJECT_REFERENCE import io.realm.kotlin.compiler.Names.PROPERTY_COLLECTION_TYPE_DICTIONARY @@ -55,13 +54,11 @@ import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_NEW_INSTANCE_METHOD import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_PRIMARY_KEY_MEMBER import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_SCHEMA_METHOD import io.realm.kotlin.compiler.Names.SET -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.builders.at import org.jetbrains.kotlin.ir.builders.declarations.addGetter @@ -96,7 +93,7 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrPropertyReferenceImpl import org.jetbrains.kotlin.ir.expressions.impl.IrSetFieldImpl import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.classifierOrFail +import org.jetbrains.kotlin.ir.types.classFqName import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.isNullable import org.jetbrains.kotlin.ir.types.makeNullable @@ -156,10 +153,10 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi private val realmAnyType: IrType = pluginContext.lookupClassOrThrow(REALM_ANY).defaultType private val kMutableProperty1Class: IrClass = - pluginContext.lookupClassOrThrow(FqNames.KOTLIN_REFLECT_KMUTABLEPROPERTY1) + pluginContext.lookupClassOrThrow(ClassIds.KOTLIN_REFLECT_KMUTABLEPROPERTY1) private val kProperty1Class: IrClass = - pluginContext.lookupClassOrThrow(FqNames.KOTLIN_REFLECT_KPROPERTY1) + pluginContext.lookupClassOrThrow(ClassIds.KOTLIN_REFLECT_KPROPERTY1) private val mapClass: IrClass = pluginContext.lookupClassOrThrow(KOTLIN_COLLECTIONS_MAP) private val pairClass: IrClass = pluginContext.lookupClassOrThrow(KOTLIN_PAIR) @@ -191,8 +188,8 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi realmObjectMutablePropertyType ) - val realmClassImpl = pluginContext.lookupClassOrThrow(FqNames.REALM_CLASS_IMPL) - private val realmClassCtor = pluginContext.lookupConstructorInClass(FqNames.REALM_CLASS_IMPL) { + val realmClassImpl = pluginContext.lookupClassOrThrow(ClassIds.REALM_CLASS_IMPL) + private val realmClassCtor = pluginContext.lookupConstructorInClass(ClassIds.REALM_CLASS_IMPL) { it.owner.valueParameters.size == 2 } @@ -207,7 +204,7 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi objectIdType, realmObjectIdType, realmUUIDType - ).map { it.classifierOrFail } + ) } private val indexableTypes = with(pluginContext.irBuiltIns) { setOf( @@ -223,12 +220,12 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi realmObjectIdType, realmUUIDType, realmAnyType - ).map { it.classifierOrFail } + ) } private val fullTextIndexableTypes = with(pluginContext.irBuiltIns) { setOf( stringType - ).map { it.classifierOrFail } + ) } /** @@ -424,7 +421,6 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi // Generate body for the synthetic schema method defined inside the Companion instance previously declared via `RealmModelSyntheticCompanionExtension` // TODO OPTIMIZE should be a one time only constructed object - @OptIn(ObsoleteDescriptorBasedAPI::class) @Suppress("LongMethod", "ComplexMethod") fun addSchemaMethodBody(irClass: IrClass) { val companionObject = irClass.companionObject() as? IrClass @@ -545,25 +541,24 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi value.coreGenericTypes?.get(0)?.nullable ?: fatalError("Missing generic type while processing a collection field.") } - val primaryKey = backingField.hasAnnotation(PRIMARY_KEY_ANNOTATION) - if (primaryKey && backingField.type.classifierOrFail !in validPrimaryKeyTypes) { + if (primaryKey && validPrimaryKeyTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Primary key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${validPrimaryKeyTypes.map { it.owner.symbol.descriptor.name }}", + "Primary key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${validPrimaryKeyTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } val isIndexed = backingField.hasAnnotation(INDEX_ANNOTATION) - if (isIndexed && backingField.type.classifierOrFail !in indexableTypes) { + if (isIndexed && indexableTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Indexed key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${indexableTypes.map { it.owner.symbol.descriptor.name }}", + "Indexed key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${indexableTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } val isFullTextIndexed = backingField.hasAnnotation(FULLTEXT_ANNOTATION) - if (isFullTextIndexed && backingField.type.classifierOrFail !in fullTextIndexableTypes) { + if (isFullTextIndexed && fullTextIndexableTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Full-text key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${fullTextIndexableTypes.map { it.owner.symbol.descriptor.name }}", + "Full-text key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${fullTextIndexableTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt index c6ae6d6455..12c088f671 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt @@ -23,7 +23,6 @@ import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower import org.jetbrains.kotlin.backend.common.runOnFilePostfix -import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclarationContainer import org.jetbrains.kotlin.ir.declarations.IrFile @@ -130,7 +129,7 @@ private class SyncLowering(private val pluginContext: IrPluginContext, private v val transformer = object : IrElementTransformerVoid() { override fun visitCall(expression: IrCall): IrExpression { - replacements.get(expression.symbol)?.let { (target, dispatchReceiverFunction) -> + replacements[expression.symbol]?.let { (target, dispatchReceiverFunction) -> return IrCallImpl( startOffset = expression.startOffset, endOffset = expression.endOffset, @@ -142,7 +141,8 @@ private class SyncLowering(private val pluginContext: IrPluginContext, private v superQualifierSymbol = null ).apply { dispatchReceiver = dispatchReceiverFunction(expression) - expression.valueArguments.forEachIndexed { index, irExpression -> + val valueArguments = List(expression.valueArgumentsCount) { expression.getValueArgument(it) } + valueArguments.forEachIndexed { index, irExpression -> putValueArgument(index, irExpression,) } putValueArgument( diff --git a/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt b/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt index 23266602ff..a58fdaf774 100644 --- a/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt +++ b/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt @@ -114,7 +114,7 @@ abstract class CollectionTests( ) ) assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) - assertTrue(result.messages.contains("Unsupported type for ${collectionType.description}: 'A'")) + assertTrue(result.messages.contains("Unsupported type for ${collectionType.description}: 'A'"), result.messages) } // ------------------------------------------------ From 364f546cf432aa7f1c4e2f8058f191349f1396e8 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 16 Aug 2023 08:57:07 +0200 Subject: [PATCH 08/27] Add custom implementations for toString, equals and hashCode (#1480) --- CHANGELOG.md | 22 +- .../kotlin/internal/RealmObjectHelper.kt | 65 ++++++ .../realm/kotlin/internal/RealmObjectUtil.kt | 10 +- .../io/realm/kotlin/internal/RealmUtils.kt | 9 +- .../kotlin/internal/platform/SystemUtils.kt | 5 + .../kotlin/internal/platform/SystemUtils.kt | 2 + .../kotlin/internal/platform/SystemUtils.kt | 3 + .../RealmModelDefaultMethodGeneration.kt | 123 +++++++++++ .../compiler/RealmModelLoweringExtension.kt | 4 + .../RealmModelSyntheticCompanionExtension.kt | 50 ----- .../RealmModelSyntheticMethodsExtension.kt | 138 ++++++++++++ .../io/realm/kotlin/compiler/Registrar.kt | 7 + .../01_AFTER.ValidateIrBeforeLowering.ir | 112 +++++++--- .../01_AFTER.ValidateIrBeforeLowering.ir | 84 ++++++-- .../kotlin/test/common/RealmListTests.kt | 52 ++++- .../kotlin/test/common/RealmObjectTests.kt | 199 +++++++++++++++++- .../realm/kotlin/test/common/RealmSetTests.kt | 30 +-- 17 files changed, 800 insertions(+), 115 deletions(-) create mode 100644 packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt create mode 100644 packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 489bd20f47..e0380bda7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,29 @@ ## 1.11.0-SNAPSHOT (YYYY-MM-DD) ### Breaking Changes -* None. +* `BaseRealmObject.equals()` has changed from being identity-based only (===) to instead return `true` if two objects come from the same Realm version. This e.g means that reading the same object property twice will now be identical. Note, two Realm objects, even with identical values will not be considered equal if they belong to different versions. + +``` +val childA: Child = realm.query().first().find()!! +val childB: Child = realm.query().first().find()!! + +// This behavior is the same both before 1.11.0 and before +childA === childB // false + +// This will return true in 1.11.0 and onwards. Before it will return false +childA == childB + +realm.writeBlocking { /* Do a write */ } +val childC = realm.query().first().find()!! + +// This will return false because childA belong to version 1, while childC belong to version 2. +// Override equals/hashCode if value semantics are wanted. +childA == childC +``` ### Enhancements +* Realm model classes now generate custom `toString`, `equals` and `hashCode` implementations. This makes it possible to compare by object reference across multiple collections. Note that two objects at different versions will not be considered equal, even +if the content is the same. Custom implementations of these methods will be respected if they are present. (Issue [#1097](https://github.com/realm/realm-kotlin/issues/1097)) * Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403)) * Support for automatic resolution of embedded object constraints during migration through `RealmConfiguration.Builder.migration(migration: AutomaticSchemaMigration, resolveEmbeddedObjectConstraints: Boolean)`. (Issue [#1464](https://github.com/realm/realm-kotlin/issues/1464) * [Sync] Add support for customizing authorization headers and adding additional custom headers to all Atlas App service requests with `AppConfiguration.Builder.authorizationHeaderName()` and `AppConfiguration.Builder.addCustomRequestHeader(...)`. (Issue [#1453](https://github.com/realm/realm-kotlin/pull/1453)) 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 c34965306b..8351ee4be2 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 @@ -17,9 +17,12 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.VersionId import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.isManaged +import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.toRealmDictionary import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.ext.toRealmSet @@ -27,6 +30,7 @@ import io.realm.kotlin.internal.dynamic.DynamicUnmanagedRealmObject import io.realm.kotlin.internal.interop.ClassKey import io.realm.kotlin.internal.interop.CollectionType import io.realm.kotlin.internal.interop.MemAllocator +import io.realm.kotlin.internal.interop.ObjectKey import io.realm.kotlin.internal.interop.PropertyKey import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop @@ -39,6 +43,7 @@ import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.Timestamp import io.realm.kotlin.internal.interop.getterScope import io.realm.kotlin.internal.interop.inputScope +import io.realm.kotlin.internal.platform.identityHashCode import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow import io.realm.kotlin.internal.schema.ClassMetadata import io.realm.kotlin.internal.schema.PropertyMetadata @@ -1145,6 +1150,66 @@ internal object RealmObjectHelper { } } + @Suppress("unused") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmToString(obj: BaseRealmObject): String { + // This code assumes no race conditions + val schemaName = obj::class.realmObjectCompanionOrNull()?.io_realm_kotlin_className + val fqName = obj::class.qualifiedName + return obj.realmObjectReference?.let { + if (obj.isValid()) { + val id: RealmObjectIdentifier = obj.getIdentifier() + val objKey = id.objectKey.key + val version = id.versionId.version + "$fqName{state=VALID, schemaName=$schemaName, objKey=$objKey, version=$version, realm=${it.owner.owner.configuration.name}}" + } else { + val state = if (it.owner.isClosed()) { + "CLOSED" + } else { + "INVALID" + } + "$fqName{state=$state, schemaName=$schemaName, realm=${it.owner.owner.configuration.name}, hashCode=${obj.hashCode()}}" + } + } ?: "$fqName{state=UNMANAGED, schemaName=$schemaName, hashCode=${obj.hashCode()}}" + } + + @Suppress("unused", "ReturnCount") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmEquals(obj: BaseRealmObject, other: Any?): Boolean { + if (obj === other) return true + 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() + } 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 + } + } + + @Suppress("unused", "MagicNumber") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmHashCode(obj: BaseRealmObject): Int { + // This code assumes no race conditions + return obj.realmObjectReference?.let { + val isValid: Boolean = obj.isValid() + val identifier: RealmObjectIdentifier = if (it.isClosed()) { + RealmObjectIdentifier(ClassKey(-1), ObjectKey(-1), VersionId(0), "") + } else { + obj.getIdentifier() + } + val realmPath: String = it.owner.owner.configuration.path + var hashCode = isValid.hashCode() + hashCode = 31 * hashCode + identifier.hashCode() + hashCode = 31 * hashCode + realmPath.hashCode() + hashCode + } ?: identityHashCode(obj) + } + private fun checkPropertyType( obj: RealmObjectReference, propertyName: String, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt index d29843ceaf..06c796f828 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt @@ -142,9 +142,15 @@ internal fun BaseRealmObject.getIdentifier(): RealmObjectIdentifier { val classKey: ClassKey = metadata.classKey val objKey: ObjectKey = RealmInterop.realm_object_get_key(objectPointer) val version: VersionId = version() - return Triple(classKey, objKey, version) + val path: String = owner.owner.configuration.path + return RealmObjectIdentifier(classKey, objKey, version, path) } ?: throw IllegalStateException("Identifier can only be calculated for managed objects.") - ULong +} + +public fun BaseRealmObject.getIdentifierOrNull(): RealmObjectIdentifier? { + return runIfManaged { + getIdentifier() + } } /** diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt index 9ba86380a5..702dd178fa 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt @@ -48,10 +48,15 @@ import kotlin.reflect.KProperty1 // `equals` method, which in general just is the memory address of the object. internal typealias UnmanagedToManagedObjectCache = MutableMap // Map -// For managed realm objects we use `` as a unique identifier +// For managed realm objects we use `` as a unique identifier // We are using a hash on the Kotlin side so we can use a HashMap for O(1) lookup rather than // having to do O(n) filter with a JNI call for `realm_equals` for each element. -internal typealias RealmObjectIdentifier = Triple +public data class RealmObjectIdentifier( + val classKey: ClassKey, + val objectKey: ObjectKey, + val versionId: VersionId, + val path: String +) internal typealias ManagedToUnmanagedObjectCache = MutableMap /** diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 7091f986d1..32ce083867 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -145,3 +145,8 @@ public expect fun returnType(field: KMutableProperty1 * Returns whether or not we are running on Windows */ public expect fun isWindows(): Boolean + +/** + * Returns the identity hashcode for a given object. + */ +internal expect fun identityHashCode(obj: Any?): Int diff --git a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 8814ab803e..11ed3e1512 100644 --- a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -124,3 +124,5 @@ private fun preparePath(directoryPath: String) { } public actual fun isWindows(): Boolean = OS_NAME.contains("windows", ignoreCase = true) + +internal actual fun identityHashCode(obj: Any?): Int = System.identityHashCode(obj) diff --git a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index b71e553187..c1fff3dedd 100644 --- a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -29,6 +29,7 @@ import platform.Foundation.dataWithContentsOfFile import platform.Foundation.timeIntervalSince1970 import platform.posix.memcpy import platform.posix.pthread_threadid_np +import kotlin.native.identityHashCode import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KType @@ -192,3 +193,5 @@ private fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length. } public actual fun isWindows(): Boolean = false + +internal actual fun identityHashCode(obj: Any?): Int = obj.identityHashCode() diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt new file mode 100644 index 0000000000..0ffec66ccf --- /dev/null +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt @@ -0,0 +1,123 @@ +package io.realm.kotlin.compiler + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irGetObject +import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.name.Name + +/** + * Class responsible for adding Realm specific logic to the default object methods like: + * - toString() + * - hashCode() + * - equals() + * + * WARNING: The current logic in here does not work well with incremental compilation. The reason + * is that we check if these methods are "empty" before filling them out, and during incremental + * compilation they already have content, and since all of these methods are using inlined + * methods they will not pick up changes in the RealmObjectHelper. + * + * This should only impact us as SDK developers though, but it does mean that changes to + * RealmObjectHelper methods will require a clean build to take effect. + */ +class RealmModelDefaultMethodGeneration(private val pluginContext: IrPluginContext) { + + private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(FqNames.REALM_OBJECT_HELPER) + private val realmToString: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmToString")) + private val realmEquals: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmEquals")) + private val realmHashCode: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmHashCode")) + private lateinit var objectReferenceProperty: IrProperty + private lateinit var objectReferenceType: IrType + + fun addDefaultMethods(irClass: IrClass) { + objectReferenceProperty = irClass.lookupProperty(Names.OBJECT_REFERENCE) + objectReferenceType = objectReferenceProperty.backingField!!.type + + if (syntheticMethodExists(irClass, "toString")) { + addToStringMethodBody(irClass) + } + if (syntheticMethodExists(irClass, "hashCode")) { + addHashCodeMethod(irClass) + } + if (syntheticMethodExists(irClass, "equals")) { + addEqualsMethod(irClass) + } + } + + /** + * Checks if a synthetic method exists in the given class. Methods in super classes + * are ignored, only methods actually declared in the class will return `true`. + * + * These methods are created by an earlier step by the Realm compiler plugin and are + * recognized by not being fake and having an empty body.ß + */ + private fun syntheticMethodExists(irClass: IrClass, methodName: String): Boolean { + return irClass.functions.firstOrNull { + !it.isFakeOverride && it.body == null && it.name == Name.identifier(methodName) + } != null + } + + private fun addEqualsMethod(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "equals" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.booleanType, + symbol = realmEquals.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 2 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + putValueArgument(1, irGet(function.valueParameters[0].type, function.valueParameters[0].symbol)) + } + ) + } + } + + private fun addHashCodeMethod(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "hashCode" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.intType, + symbol = realmHashCode.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 1 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + } + ) + } + } + + private fun addToStringMethodBody(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "toString" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.stringType, + symbol = realmToString.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 1 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + } + ) + } + } +} diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt index 4e590cf6ab..7aac109667 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt @@ -107,6 +107,10 @@ private class RealmModelLowering(private val pluginContext: IrPluginContext) : C // Modify properties accessor to generate custom getter/setter AccessorModifierIrGeneration(pluginContext).modifyPropertiesAndCollectSchema(irClass) + // Add custom toString, equals and hashCode methods + val methodGenerator = RealmModelDefaultMethodGeneration(pluginContext) + methodGenerator.addDefaultMethods(irClass) + // Add body for synthetic companion methods val companion = irClass.companionObject() ?: fatalError("RealmObject without companion: ${irClass.kotlinFqName}") generator.addCompanionFields(irClass, companion, SchemaCollector.properties[irClass]) diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt index 02b8658fb5..f72760985b 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt @@ -19,12 +19,9 @@ package io.realm.kotlin.compiler import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_NEW_INSTANCE_METHOD import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_SCHEMA_METHOD import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor -import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor -import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor import org.jetbrains.kotlin.descriptors.annotations.Annotations import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl @@ -33,11 +30,6 @@ import org.jetbrains.kotlin.name.SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension -import org.jetbrains.kotlin.resolve.lazy.LazyClassContext -import org.jetbrains.kotlin.resolve.lazy.declarations.ClassMemberDeclarationProvider -import org.jetbrains.kotlin.resolve.lazy.declarations.PackageMemberDeclarationProvider -import org.jetbrains.kotlin.types.KotlinType -import java.util.ArrayList /** * Triggers generation of companion objects and ensures that the companion object implement the @@ -58,48 +50,6 @@ class RealmModelSyntheticCompanionExtension : SyntheticResolveExtension { } } - override fun addSyntheticSupertypes( - thisDescriptor: ClassDescriptor, - supertypes: MutableList - ) { - } - - override fun generateSyntheticClasses( - thisDescriptor: ClassDescriptor, - name: Name, - ctx: LazyClassContext, - declarationProvider: ClassMemberDeclarationProvider, - result: MutableSet - ) { - } - - override fun generateSyntheticClasses( - thisDescriptor: PackageFragmentDescriptor, - name: Name, - ctx: LazyClassContext, - declarationProvider: PackageMemberDeclarationProvider, - result: MutableSet - ) { - } - - override fun generateSyntheticProperties( - thisDescriptor: ClassDescriptor, - name: Name, - bindingContext: BindingContext, - fromSupertypes: ArrayList, - result: MutableSet - ) { - } - - override fun generateSyntheticSecondaryConstructors( - thisDescriptor: ClassDescriptor, - bindingContext: BindingContext, - result: MutableCollection - ) { - } - - override fun getSyntheticNestedClassNames(thisDescriptor: ClassDescriptor): List = emptyList() - override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List { return when { thisDescriptor.isRealmObjectCompanion -> { diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt new file mode 100644 index 0000000000..5342091838 --- /dev/null +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.compiler + +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns +import org.jetbrains.kotlin.resolve.descriptorUtil.parents +import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension +import org.jetbrains.kotlin.types.SimpleType + +/** + * Triggers generation of synthetic methods on Realm model classes, in particular + * `toString()`, `equals()` and `hashCode()`. + */ +@Suppress("ComplexCondition") +class RealmModelSyntheticMethodsExtension : SyntheticResolveExtension { + + override fun generateSyntheticMethods( + thisDescriptor: ClassDescriptor, + name: Name, + bindingContext: BindingContext, + fromSupertypes: List, + result: MutableCollection + ) { + if (thisDescriptor.isRealmObject && + !thisDescriptor.isCompanionObject && /* Do not override companion object methods */ + !thisDescriptor.isInner && /* Do not override inner class methods */ + !isNestedInRealmModelClass(thisDescriptor) && /* do not override nested class methods */ + result.isEmpty() /* = no method has been declared in the current class */ + ) { + when (name.identifier) { + "toString" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = emptyList(), + returnType = thisDescriptor.builtIns.stringType + ) + ) + } + "equals" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = listOf(Pair("other", thisDescriptor.builtIns.nullableAnyType)), + returnType = thisDescriptor.builtIns.booleanType + ) + ) + } + "hashCode" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = emptyList(), + returnType = thisDescriptor.builtIns.intType + ) + ) + } + } + } + } + + private fun createMethod( + classDescriptor: ClassDescriptor, + methodName: Name, + arguments: List>, + returnType: SimpleType + ): SimpleFunctionDescriptor { + return SimpleFunctionDescriptorImpl.create( + classDescriptor, + Annotations.EMPTY, + methodName, + CallableMemberDescriptor.Kind.SYNTHESIZED, + classDescriptor.source + ).apply { + initialize( + null, + classDescriptor.thisAsReceiverParameter, + emptyList(), + emptyList(), + arguments.map { (argumentName, argumentType) -> + ValueParameterDescriptorImpl( + containingDeclaration = this, + original = null, + index = 0, + annotations = Annotations.EMPTY, + name = Name.identifier(argumentName), + outType = argumentType, + declaresDefaultValue = false, + isCrossinline = false, + isNoinline = false, + varargElementType = null, + source = this.source + ) + }, + returnType, + Modality.OPEN, + DescriptorVisibilities.PUBLIC + ) + } + } + + private fun isNestedInRealmModelClass(classDescriptor: ClassDescriptor): Boolean { + return classDescriptor.parents.firstOrNull { + return if (it is ClassDescriptor) { + it.isRealmObject + } else { + false + } + } != null + } +} diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt index e5fadb50a4..ed03b01688 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt @@ -69,6 +69,13 @@ class Registrar : ComponentRegistrar { LoadingOrder.LAST, project ) + // Trigger generation of Realm specific methods in model classes: + // toString(), equals() and hashCode() + getExtensionPoint(SyntheticResolveExtension.extensionPointName).registerExtension( + RealmModelSyntheticMethodsExtension(), + LoadingOrder.LAST, + project + ) // Adds RealmObjectInternal properties, rewires accessors and adds static companion // properties and methods getExtensionPoint(IrGenerationExtension.extensionPointName).registerExtension( diff --git a/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir b/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir index 5ef8f93504..3ca90d16e5 100644 --- a/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir +++ b/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir @@ -8508,19 +8508,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.Sample.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.Sample.Companion declared in sample.input.Sample.Companion.' type=sample.input.Sample.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.Sample, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Sample VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.Sample' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.equals' type=sample.input.Sample origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.Sample.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.Sample) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.Sample + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.Sample' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.hashCode' type=sample.input.Sample origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.Sample) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Sample + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.Sample' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.toString' type=sample.input.Sample origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -8795,19 +8811,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.Child.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.Child.Companion declared in sample.input.Child.Companion.' type=sample.input.Child.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.Child, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Child VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.Child' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.equals' type=sample.input.Child origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.Child.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.Child) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.Child + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.Child' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.hashCode' type=sample.input.Child origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.Child) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Child + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.Child' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.toString' type=sample.input.Child origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -9016,19 +9048,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.EmbeddedParent.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.EmbeddedParent.Companion declared in sample.input.EmbeddedParent.Companion.' type=sample.input.EmbeddedParent.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.equals' type=sample.input.EmbeddedParent origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.EmbeddedParent.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.hashCode' type=sample.input.EmbeddedParent origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.toString' type=sample.input.EmbeddedParent origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -9235,19 +9283,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.EmbeddedChild.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.EmbeddedChild.Companion declared in sample.input.EmbeddedChild.Companion.' type=sample.input.EmbeddedChild.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.equals' type=sample.input.EmbeddedChild origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.EmbeddedChild.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.hashCode' type=sample.input.EmbeddedChild origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.toString' type=sample.input.EmbeddedChild origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY diff --git a/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir b/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir index 3a6151e3a8..72f689feb3 100644 --- a/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir +++ b/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir @@ -118,19 +118,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.A.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.A.Companion declared in schema.input.A.Companion.' type=schema.input.A.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.A, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.A VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.A' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.equals' type=schema.input.A origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.A.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.A) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.A + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.A' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.hashCode' type=schema.input.A origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.A) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.A + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.A' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.toString' type=schema.input.A origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -271,19 +287,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.B.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.B.Companion declared in schema.input.B.Companion.' type=schema.input.B.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.B, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.B VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.B' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.equals' type=schema.input.B origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.B.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.B) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.B + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.B' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.hashCode' type=schema.input.B origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.B) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.B + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.B' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.toString' type=schema.input.B origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -424,19 +456,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.C.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.C.Companion declared in schema.input.C.Companion.' type=schema.input.C.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.C, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.C VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.C' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.equals' type=schema.input.C origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.C.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.C) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.C + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.C' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.hashCode' type=schema.input.C origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.C) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.C + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.C' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.toString' type=schema.input.C origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY 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 20f7aa8f2e..4e1bcf0b8f 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 @@ -282,6 +282,13 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { managedTesters[0].clearFailsIfClosed(getCloseableRealm()) } + @Test + fun remove() { + for (tester in managedTesters) { + tester.remove() + } + } + @Test fun removeAt() { for (tester in managedTesters) { @@ -729,6 +736,7 @@ internal interface ListApiTester : ErrorCatcher { fun addAllWithIndexFailsIfClosed(realm: Realm) fun clear() fun clearFailsIfClosed(realm: Realm) + fun remove() fun removeAt() fun removeAtFailsIfClosed(realm: Realm) fun set() @@ -809,7 +817,7 @@ internal class ListTypeSafetyManager( */ internal abstract class ManagedListTester( override val realm: Realm, - private val typeSafetyManager: ListTypeSafetyManager, + protected val typeSafetyManager: ListTypeSafetyManager, override val classifier: KClassifier ) : ListApiTester { @@ -1068,6 +1076,25 @@ internal abstract class ManagedListTester( } } + override fun remove() { + val dataSet = typeSafetyManager.dataSetToLoad + val assertions = { list: RealmList -> + assertTrue(list.isEmpty()) + } + + errorCatcher { + realm.writeBlocking { + val list = typeSafetyManager.createContainerAndGetCollection(this) + assertFalse(list.remove(dataSet[0])) + assertTrue(list.add(dataSet[0])) + assertTrue(list.remove(list.last())) + assertions(list) + } + } + + assertListAndCleanup { list -> assertions(list) } + } + override fun removeAt() { val dataSet = typeSafetyManager.dataSetToLoad val assertions = { list: RealmList -> @@ -1204,7 +1231,7 @@ internal abstract class ManagedListTester( } // Retrieves the list again but this time from Realm to check the getter is called correctly - private fun assertListAndCleanup(assertion: (RealmList) -> Unit) { + protected fun assertListAndCleanup(assertion: (RealmList) -> Unit) { realm.writeBlocking { val container = this.query() .first() @@ -1318,6 +1345,27 @@ internal class ByteArrayListTester( ) : 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) } + } } // ----------------------------------- diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt index db044732a7..09cf50fec8 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.test.common import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.VersionId +import io.realm.kotlin.entities.SampleWithPrimaryKey import io.realm.kotlin.entities.link.Child import io.realm.kotlin.entities.link.Parent import io.realm.kotlin.ext.isFrozen @@ -25,6 +26,7 @@ import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.version import io.realm.kotlin.test.common.utils.RealmStateTest import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmObject import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -32,8 +34,34 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertTrue +// Model class with toString/equals/hashCode +class CustomMethods : RealmObject { + var name: String = "" + var age: Int = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CustomMethods + return this.age == 42 && other.age == 42 + } + + override fun hashCode(): Int { + return if (isValid()) { + 42 + } else { + -1 + } + } + + override fun toString(): String { + return "customToString" + } +} + class RealmObjectTests : RealmStateTest { companion object { @@ -48,7 +76,7 @@ class RealmObjectTests : RealmStateTest { @BeforeTest fun setup() { tmpDir = PlatformUtils.createTempDir() - val configuration = RealmConfiguration.Builder(schema = setOf(Parent::class, Child::class)) + val configuration = RealmConfiguration.Builder(schema = setOf(Parent::class, Child::class, SampleWithPrimaryKey::class, CustomMethods::class)) .directory(tmpDir) .build() realm = Realm.open(configuration) @@ -113,6 +141,175 @@ class RealmObjectTests : RealmStateTest { // FIXME } + @Test + fun toString_managed() { + val managedObj = realm.writeBlocking { + copyToRealm(Parent()) + } + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=VALID, schemaName=Parent, objKey=[0-9]*, version=[0-9]*, realm=${realm.configuration.name}\\}") + assertTrue(regex.matches(managedObj.toString()), managedObj.toString()) + } + + @Test + fun toString_managed_cyclicData() { + val p1 = SampleWithPrimaryKey() + p1.stringField = "Parent" + p1.nullableObject = p1 + val managedObj = realm.writeBlocking { copyToRealm(p1) } + val regex = Regex("io.realm.kotlin.entities.SampleWithPrimaryKey\\{state=VALID, schemaName=SampleWithPrimaryKey, objKey=[0-9]*, version=[0-9]*, realm=${realm.configuration.name}\\}") + assertTrue(regex.matches(managedObj.toString()), managedObj.toString()) + } + + @Test + fun toString_managed_invalid() { + realm.writeBlocking { + val managedObject = copyToRealm(Parent()) + delete(managedObject) + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=INVALID, schemaName=Parent, realm=${realm.configuration.name}, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(managedObject.toString()), managedObject.toString()) + cancelWrite() + } + } + + @Test + fun toString_managed_closedRealm() { + val managedObject = realm.writeBlocking { + copyToRealm(Parent()) + } + realm.close() + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=CLOSED, schemaName=Parent, realm=${realm.configuration.name}, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(managedObject.toString()), managedObject.toString()) + } + + @Test + fun toString_customMethod() { + assertEquals("customToString", CustomMethods().toString()) + val managedObj = realm.writeBlocking { copyToRealm(CustomMethods()) } + assertEquals("customToString", managedObj.toString()) + } + + @Test + fun toString_unmanaged() { + val unmanagedObject = Parent() + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=UNMANAGED, schemaName=Parent, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(unmanagedObject.toString()), unmanagedObject.toString()) + } + + @Test + fun equals_hashCode_managed() { + realm.writeBlocking { + val p1 = copyToRealm(Parent().apply { this.name = "Jane" }) + val p2 = copyToRealm(Parent()) + val p3 = query(Parent::class, "name = 'Jane'").first().find()!! + assertEquals(p1, p1) + assertEquals(p1, p3) + assertEquals(p1.hashCode(), p1.hashCode()) + assertEquals(p1.hashCode(), p3.hashCode()) + + // Not restrictions are given on hashCode if two objects are not equal + assertNotEquals(p2, p1) + assertNotEquals(p2, p3) + } + } + + @Test + fun equals_hashCode_unmanaged() { + val p1 = Parent() + val p2 = Parent() + assertEquals(p1, p1) + assertEquals(p1.hashCode(), p1.hashCode()) + assertNotEquals(p1, p2) + } + + @Test + fun equals_hashCode_mixed() { + val unmanagedObj = Parent() + val managedObj = realm.writeBlocking { copyToRealm(Parent()) } + assertNotEquals(unmanagedObj, managedObj) + assertNotEquals(managedObj, unmanagedObj) + // When objects are not equal, no guarantees are given on the behavior of hashCode() + // thus nothing can be asserted here. + } + + @Test + fun equals_hashCode_managed_cyclicData() { + realm.writeBlocking { + val p1 = copyToRealm( + SampleWithPrimaryKey().apply { + primaryKey = 1 + stringField = "Jane" + } + ) + p1.nullableObject = p1 + val p2 = copyToRealm( + SampleWithPrimaryKey().apply { + primaryKey = 2 + stringField = "John" + } + ) + val p3 = query(SampleWithPrimaryKey::class, "stringField = 'Jane'").first().find()!! + assertEquals(p1, p1) + assertEquals(p1, p3) + assertEquals(p1.hashCode(), p1.hashCode()) + assertEquals(p1.hashCode(), p3.hashCode()) + + // Not restrictions are given on hashCode if two objects are not equal + assertNotEquals(p2, p1) + assertNotEquals(p2, p3) + } + } + + @Test + fun equals_hashCode_customMethod() { + // Only equals if age = 42 or same instance + val obj1 = CustomMethods() + val obj2 = CustomMethods() + assertEquals(obj1, obj1) + assertEquals(obj1.hashCode(), obj1.hashCode()) + assertEquals(42, obj1.hashCode()) + assertNotEquals(obj1, obj2) + + val obj3 = CustomMethods().apply { age = 42 } + val obj4 = CustomMethods().apply { age = 42 } + assertEquals(obj3, obj3) + assertEquals(obj3.hashCode(), obj4.hashCode()) + assertEquals(42, obj3.hashCode()) + assertEquals(obj3, obj4) + assertEquals(obj3.hashCode(), obj4.hashCode()) + + // Managed objects + realm.writeBlocking { + val obj1 = copyToRealm(CustomMethods()) + val obj2 = copyToRealm(CustomMethods()) + assertEquals(obj1, obj1) + assertEquals(obj1.hashCode(), obj1.hashCode()) + assertEquals(42, obj1.hashCode()) + assertNotEquals(obj1, obj2) + + val obj3 = copyToRealm(CustomMethods().apply { age = 42 }) + val obj4 = copyToRealm(CustomMethods().apply { age = 42 }) + assertEquals(obj3, obj3) + assertEquals(obj3.hashCode(), obj3.hashCode()) + assertEquals(42, obj1.hashCode()) + assertEquals(obj3, obj4) + assertEquals(obj3.hashCode(), obj4.hashCode()) + } + } + + @Test + fun equals_hashCode_managed_invalid() { + realm.writeBlocking { + val p1 = copyToRealm(Parent().apply { this.name = "Jane" }) + val p2 = copyToRealm(Parent()) + delete(p1) + delete(p2) + + assertEquals(p1, p1) + assertEquals(p1.hashCode(), p1.hashCode()) + assertNotEquals(p1, p2) + } + } + override fun isFrozen_throwsIfRealmIsClosed() { realm.close() assertFailsWith { 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 e1e60b4b19..437ff45ebc 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 @@ -777,26 +777,26 @@ internal abstract class ManagedSetTester( } override fun remove() { - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // Ignore RealmObject: structural equality cannot be assessed for this type when removing - // elements from the set - if (classifier != RealmObject::class) { - val dataSet = typeSafetyManager.dataSetToLoad + val dataSet = typeSafetyManager.dataSetToLoad - errorCatcher { - realm.writeBlocking { - val set = typeSafetyManager.createContainerAndGetCollection(this) - set.add(dataSet[0]) - assertTrue(set.remove(dataSet[0])) - assertTrue(set.isEmpty()) + errorCatcher { + realm.writeBlocking { + val set = typeSafetyManager.createContainerAndGetCollection(this) + val element = if (classifier == RealmObject::class) { + copyToRealm(dataSet[0] as RealmObject) as T + } else { + dataSet[0] } - } - - assertContainerAndCleanup { container -> - val set = typeSafetyManager.getCollection(container) + set.add(element) + assertTrue(set.remove(element)) assertTrue(set.isEmpty()) } } + + assertContainerAndCleanup { container -> + val set = typeSafetyManager.getCollection(container) + assertTrue(set.isEmpty()) + } } override fun removeAll() { From e7b3c9ee86991b5bb08eed6d3a6ba30779a0b51d Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 16 Aug 2023 10:04:15 +0200 Subject: [PATCH 09/27] Fix bad merge. --- .../realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt index 0ffec66ccf..996c947463 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt @@ -28,7 +28,7 @@ import org.jetbrains.kotlin.name.Name */ class RealmModelDefaultMethodGeneration(private val pluginContext: IrPluginContext) { - private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(FqNames.REALM_OBJECT_HELPER) + private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(ClassIds.REALM_OBJECT_HELPER) private val realmToString: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmToString")) private val realmEquals: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmEquals")) private val realmHashCode: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmHashCode")) From c8f3e0bfd8273cc45e61abc3b51ace3ae44df3e1 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 25 Aug 2023 12:19:42 +0200 Subject: [PATCH 10/27] Rework internal sync error handling (#1490) --- CHANGELOG.md | 3 +- .../realm/kotlin/test/sync/SyncEnumTests.kt | 35 +++--- .../realm/kotlin/internal/interop/Callback.kt | 3 +- .../kotlin/internal/interop/CoreError.kt | 66 ++++++++++++ .../internal/interop/CoreErrorConverter.kt | 11 +- .../kotlin/internal/interop/ErrorCategory.kt | 3 +- .../kotlin/internal/interop/ErrorCode.kt | 20 +++- .../kotlin/internal/interop/RealmInterop.kt | 11 +- .../interop/sync/ProtocolErrorCode.kt | 90 ++++++++-------- .../kotlin/internal/interop/sync/SyncError.kt | 16 ++- .../internal/interop/sync/SyncErrorCode.kt | 55 ---------- .../interop/sync/SyncErrorCodeCategory.kt | 36 ------- .../kotlin/internal/interop/ErrorCategory.kt | 3 +- .../kotlin/internal/interop/ErrorCode.kt | 22 +++- .../kotlin/internal/interop/RealmInterop.kt | 14 +-- ...VMSyncSessionTransferCompletionCallback.kt | 5 +- .../interop/sync/ProtocolErrorCode.kt | 96 ++++++++--------- .../interop/sync/SyncErrorCodeCategory.kt | 37 ------- packages/cinterop/src/native/realm.def | 2 +- .../kotlin/internal/interop/ErrorCategory.kt | 3 +- .../kotlin/internal/interop/ErrorCode.kt | 28 +++-- .../kotlin/internal/interop/RealmInterop.kt | 40 +++---- .../interop/sync/ProtocolErrorCode.kt | 96 ++++++++--------- .../interop/sync/SyncErrorCodeCategory.kt | 40 ------- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 16 ++- .../src/main/jni/realm_api_helpers.h | 2 +- .../mongodb/internal/AppConfigurationImpl.kt | 6 +- .../kotlin/mongodb/internal/RealmSyncUtils.kt | 102 +++++------------- .../mongodb/internal/SyncSessionImpl.kt | 15 ++- .../io/realm/kotlin/test/common/QueryTests.kt | 2 +- .../common/FlexibleSyncIntegrationTests.kt | 5 +- .../common/SyncClientResetIntegrationTests.kt | 63 ++++------- .../test/mongodb/common/SyncedRealmTests.kt | 12 +-- .../common/internal/RealmSyncUtilsTest.kt | 87 ++++++++------- .../internal/SyncConnectionParamsTests.kt | 4 - 36 files changed, 430 insertions(+), 621 deletions(-) create mode 100644 packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt delete mode 100644 packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt delete mode 100644 packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt delete mode 100644 packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt delete mode 100644 packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e0380bda7c..a659cc213e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ childA == childC ``` ### Enhancements +* Fulltext queries now support prefix search by using the * operator, like `description TEXT 'alex*'`. (Core issue [#6860](https://github.com/realm/realm-core/issues/6860)) * Realm model classes now generate custom `toString`, `equals` and `hashCode` implementations. This makes it possible to compare by object reference across multiple collections. Note that two objects at different versions will not be considered equal, even if the content is the same. Custom implementations of these methods will be respected if they are present. (Issue [#1097](https://github.com/realm/realm-kotlin/issues/1097)) * Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403)) @@ -46,7 +47,7 @@ if the content is the same. Custom implementations of these methods will be resp * Minimum Android SDK: 16. ### Internal -* None. +* Updated to Realm Core 13.19.0, commit ea7c5d5e2900b8411a295aea3d1aa56aa55fff1d. ## 1.10.2 (2023-07-21) diff --git a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt index 4f9490dbe9..1effc3cd32 100644 --- a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt +++ b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test.sync +import io.realm.kotlin.internal.interop.CategoryFlags import io.realm.kotlin.internal.interop.ErrorCategory import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.realm_auth_provider_e @@ -23,23 +24,21 @@ import io.realm.kotlin.internal.interop.realm_errno_e import io.realm.kotlin.internal.interop.realm_error_category_e import io.realm.kotlin.internal.interop.realm_sync_client_metadata_mode_e import io.realm.kotlin.internal.interop.realm_sync_connection_state_e -import io.realm.kotlin.internal.interop.realm_sync_errno_client_e import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e -import io.realm.kotlin.internal.interop.realm_sync_error_category_e import io.realm.kotlin.internal.interop.realm_sync_session_resync_mode_e import io.realm.kotlin.internal.interop.realm_sync_session_state_e import io.realm.kotlin.internal.interop.realm_user_state_e +import io.realm.kotlin.internal.interop.realm_web_socket_errno_e import io.realm.kotlin.internal.interop.sync.AuthProvider import io.realm.kotlin.internal.interop.sync.CoreConnectionState import io.realm.kotlin.internal.interop.sync.CoreSyncSessionState import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolConnectionErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolSessionErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory +import io.realm.kotlin.internal.interop.sync.SyncConnectionErrorCode +import io.realm.kotlin.internal.interop.sync.SyncSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import org.junit.Test import kotlin.reflect.KClass import kotlin.test.BeforeTest @@ -59,10 +58,11 @@ class SyncEnumTests { } @Test - fun appErrorCategory() { + fun errorCategory() { checkEnum(realm_error_category_e::class) { nativeValue -> ErrorCategory.of(nativeValue) } + assertEquals(ErrorCategory.values().size, CategoryFlags.CATEGORY_ORDER.size) } @Test @@ -94,30 +94,23 @@ class SyncEnumTests { } @Test - fun protocolClientErrorCode() { - checkEnum(realm_sync_errno_client_e::class) { nativeValue -> - ProtocolClientErrorCode.of(nativeValue) - } - } - - @Test - fun protocolConnectionErrorCode() { + fun syncConnectionErrorCode() { checkEnum(realm_sync_errno_connection_e::class) { nativeValue -> - ProtocolConnectionErrorCode.of(nativeValue) + SyncConnectionErrorCode.of(nativeValue) } } @Test - fun protocolSessionErrorCode() { + fun syncSessionErrorCode() { checkEnum(realm_sync_errno_session_e::class) { nativeValue -> - ProtocolSessionErrorCode.of(nativeValue) + SyncSessionErrorCode.of(nativeValue) } } @Test - fun syncErrorCodeCategory() { - checkEnum(realm_sync_error_category_e::class) { nativeValue -> - SyncErrorCodeCategory.of(nativeValue) + fun websocketErrorCode() { + checkEnum(realm_web_socket_errno_e::class) { nativeValue -> + WebsocketErrorCode.of(nativeValue) } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt index b8534d0b67..4d262f206d 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.CoreSubscriptionSetState import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode // TODO Could be replace by lambda. See realm_app_config_new networkTransportFactory for example. interface Callback { @@ -39,7 +38,7 @@ fun interface SyncErrorCallback { // Interface exposed towards `library-sync` interface SyncSessionTransferCompletionCallback { - fun invoke(error: SyncErrorCode?) + fun invoke(error: CoreError?) } interface LogCallback { diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt new file mode 100644 index 0000000000..3d7334fcd7 --- /dev/null +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt @@ -0,0 +1,66 @@ +package io.realm.kotlin.internal.interop + +/** + * Wrapper for C-API `realm_error_t`. + * See https://github.com/realm/realm-core/blob/master/src/realm.h#L231 + */ +class CoreError( + categoriesNativeValue: Int, + val errorCodeNativeValue: Int, + messageNativeValue: String?, +) { + val categories: CategoryFlags = CategoryFlags((categoriesNativeValue)) + val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue) + val message = messageNativeValue + + operator fun contains(category: ErrorCategory): Boolean = category in categories +} + +data class CategoryFlags(val categoryFlags: Int) { + + companion object { + /** + * See error code mapping to categories here: + * https://github.com/realm/realm-core/blob/master/src/realm/error_codes.cpp#L29 + * + * In most cases, only 1 category is assigned, but some errors have multiple. So instead of + * overwhelming the user with many categories, we only select the most important to show + * in the error message. "important" is of course tricky to define, but generally + * we consider vague categories like [ErrorCategory.RLM_ERR_CAT_RUNTIME] as less important + * than more specific ones like [ErrorCategory.RLM_ERR_CAT_JSON_ERROR]. + * + * In the current implementation, categories between index 0 and 7 are considered equal + * and the order is somewhat arbitrary. No error codes has multiple of these categories + * associated either. + */ + val CATEGORY_ORDER: List = listOf( + ErrorCategory.RLM_ERR_CAT_CUSTOM_ERROR, + ErrorCategory.RLM_ERR_CAT_WEBSOCKET_ERROR, + ErrorCategory.RLM_ERR_CAT_SYNC_ERROR, + ErrorCategory.RLM_ERR_CAT_SERVICE_ERROR, + ErrorCategory.RLM_ERR_CAT_JSON_ERROR, + ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR, + ErrorCategory.RLM_ERR_CAT_SYSTEM_ERROR, + ErrorCategory.RLM_ERR_CAT_FILE_ACCESS, + ErrorCategory.RLM_ERR_CAT_HTTP_ERROR, + ErrorCategory.RLM_ERR_CAT_INVALID_ARG, + ErrorCategory.RLM_ERR_CAT_APP_ERROR, + ErrorCategory.RLM_ERR_CAT_LOGIC, + ErrorCategory.RLM_ERR_CAT_RUNTIME, + ) + } + + /** + * Returns a description of the most important category defined in [categoryFlags]. + * If no known categories are found, the integer values for all the categories is returned + * as debugging information. + */ + val description: String = CATEGORY_ORDER.firstOrNull { category -> + this.contains(category) + }?.description ?: "$categoryFlags" + + /** + * Check whether a given [ErrorCategory] is included in the [categoryFlags]. + */ + operator fun contains(category: ErrorCategory): Boolean = (categoryFlags and category.nativeValue) != 0 +} diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt index fddf363cdf..0a60a8aa7b 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt @@ -31,22 +31,21 @@ object CoreErrorConverter { path: String?, userError: Throwable? ): Throwable { - val categories: CategoryFlag = CategoryFlag(categoriesNativeValue) + val categories: CategoryFlags = CategoryFlags(categoriesNativeValue) val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue) val message: String = "[$errorCode]: $messageNativeValue" return userError ?: when { ErrorCode.RLM_ERR_INDEX_OUT_OF_BOUNDS == errorCode -> IndexOutOfBoundsException(message) - ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories -> + ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories && ErrorCategory.RLM_ERR_CAT_SYNC_ERROR !in categories -> { + // Some sync errors flagged as both logical and illegal. In our case, we consider those + // IllegalState, so discard them them here and let them fall through to the bottom case IllegalArgumentException(message) + } ErrorCategory.RLM_ERR_CAT_LOGIC in categories || ErrorCategory.RLM_ERR_CAT_RUNTIME in categories -> IllegalStateException(message) else -> Error(message) // This can happen when propagating user level exceptions. } } - - data class CategoryFlag(val categoryCode: Int) { - operator fun contains(other: ErrorCategory): Boolean = categoryCode and other.nativeValue != 0 - } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index fb1716377d..53053fc1da 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -32,7 +32,8 @@ expect enum class ErrorCategory : CodeDescription { RLM_ERR_CAT_SERVICE_ERROR, RLM_ERR_CAT_HTTP_ERROR, RLM_ERR_CAT_CUSTOM_ERROR, - RLM_ERR_CAT_WEBSOCKET_ERROR; + RLM_ERR_CAT_WEBSOCKET_ERROR, + RLM_ERR_CAT_SYNC_ERROR; companion object { internal fun of(nativeValue: Int): ErrorCategory? diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 13138a5605..b4d95acbf2 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -50,6 +50,22 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_SCHEMA_VERSION_MISMATCH, RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE, RLM_ERR_OPERATION_ABORTED, + RLM_ERR_AUTO_CLIENT_RESET_FAILED, + RLM_ERR_BAD_SYNC_PARTITION_VALUE, + RLM_ERR_CONNECTION_CLOSED, + RLM_ERR_INVALID_SUBSCRIPTION_QUERY, + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED, + RLM_ERR_SYNC_COMPENSATING_WRITE, + RLM_ERR_SYNC_CONNECT_FAILED, + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE, + RLM_ERR_SYNC_PERMISSION_DENIED, + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED, + RLM_ERR_SYNC_USER_MISMATCH, + RLM_ERR_TLS_HANDSHAKE_FAILED, + RLM_ERR_WRONG_SYNC_TYPE, + RLM_ERR_SYNC_WRITE_NOT_ALLOWED, RLM_ERR_SYSTEM_ERROR, RLM_ERR_LOGIC, RLM_ERR_NOT_SUPPORTED, @@ -162,9 +178,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_MAINTENANCE_IN_PROGRESS, RLM_ERR_USERPASS_TOKEN_INVALID, RLM_ERR_INVALID_SERVER_RESPONSE, - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR, - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR, - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR, + REALM_ERR_APP_SERVER_ERROR, RLM_ERR_CALLBACK, RLM_ERR_UNKNOWN; 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 33332f2450..731a6a1e57 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 @@ -27,8 +27,6 @@ import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.coroutines.CoroutineDispatcher @@ -127,8 +125,6 @@ typealias RealmMutableSubscriptionSetPointer = NativePointer ) : this( - SyncErrorCode.newInstance(category, value, message), - detailedMessage, + CoreError(categoryFlags, value, message), originalFilePath, recoveryFilePath, isFatal, diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt deleted file mode 100644 index 93a9b78052..0000000000 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.realm.kotlin.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.UnknownCodeDescription - -/** - * Wrapper for C-API `realm_sync_error_code`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3306 - */ -data class SyncErrorCode internal constructor( - val category: CodeDescription, - val code: CodeDescription, - val message: String -) { - companion object { - fun newInstance( - categoryCode: Int, - errorCode: Int, - message: String - ): SyncErrorCode { - val category = SyncErrorCodeCategory.of(categoryCode) ?: UnknownCodeDescription(categoryCode) - - val code: CodeDescription = when (category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT -> ProtocolClientErrorCode.of(errorCode) - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION -> ProtocolConnectionErrorCode.of(errorCode) - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SESSION -> ProtocolSessionErrorCode.of(errorCode) - // SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM -> // no mapping available - // SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN -> // no mapping available - else -> null - } ?: UnknownCodeDescription(errorCode) - - return SyncErrorCode( - category, - code, - message - ) - } - } -} diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 049e3fbd1b..0000000000 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.realm.kotlin.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription - -/** - * Wrapper for C-API `realm_sync_error_category`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3198 - */ -expect enum class SyncErrorCodeCategory : CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT, - RLM_SYNC_ERROR_CATEGORY_CONNECTION, - RLM_SYNC_ERROR_CATEGORY_SESSION, - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET, - RLM_SYNC_ERROR_CATEGORY_SYSTEM, - RLM_SYNC_ERROR_CATEGORY_UNKNOWN; - - companion object { - internal fun of(nativeValue: Int): SyncErrorCodeCategory? - } -} diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index 423f657111..5319d0bc90 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -31,7 +31,8 @@ actual enum class ErrorCategory( RLM_ERR_CAT_SERVICE_ERROR("Service", realm_error_category_e.RLM_ERR_CAT_SERVICE_ERROR), RLM_ERR_CAT_HTTP_ERROR("Http", realm_error_category_e.RLM_ERR_CAT_HTTP_ERROR), RLM_ERR_CAT_CUSTOM_ERROR("Custom", realm_error_category_e.RLM_ERR_CAT_CUSTOM_ERROR), - RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category_e.RLM_ERR_CAT_WEBSOCKET_ERROR); + RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category_e.RLM_ERR_CAT_WEBSOCKET_ERROR), + RLM_ERR_CAT_SYNC_ERROR("Sync", realm_error_category_e.RLM_ERR_CAT_SYNC_ERROR); actual companion object { internal actual fun of(nativeValue: Int): ErrorCategory? = diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 935354e9cb..36bcd30327 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -47,6 +47,22 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_SCHEMA_VERSION_MISMATCH("SchemaVersionMismatch", realm_errno_e.RLM_ERR_SCHEMA_VERSION_MISMATCH), RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE("NoSubscriptionForWrite", realm_errno_e.RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE), RLM_ERR_OPERATION_ABORTED("OperationAborted", realm_errno_e.RLM_ERR_OPERATION_ABORTED), + RLM_ERR_AUTO_CLIENT_RESET_FAILED("AutoClientResetFailed", realm_errno_e.RLM_ERR_AUTO_CLIENT_RESET_FAILED), + RLM_ERR_BAD_SYNC_PARTITION_VALUE("BadSyncPartitionValue", realm_errno_e.RLM_ERR_BAD_SYNC_PARTITION_VALUE), + RLM_ERR_CONNECTION_CLOSED("ConnectionClosed", realm_errno_e.RLM_ERR_CONNECTION_CLOSED), + RLM_ERR_INVALID_SUBSCRIPTION_QUERY("InvalidSubscriptionQuery", realm_errno_e.RLM_ERR_INVALID_SUBSCRIPTION_QUERY), + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno_e.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), + RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno_e.RLM_ERR_SYNC_COMPENSATING_WRITE), + RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno_e.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno_e.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), + RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno_e.RLM_ERR_SYNC_PERMISSION_DENIED), + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno_e.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED("SyncProtocolNegotiationFailed", realm_errno_e.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED), + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED("SyncServerPermissionsChanged", realm_errno_e.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED), + RLM_ERR_SYNC_USER_MISMATCH("SyncUserMismatch", realm_errno_e.RLM_ERR_SYNC_USER_MISMATCH), + RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno_e.RLM_ERR_TLS_HANDSHAKE_FAILED), + RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno_e.RLM_ERR_WRONG_SYNC_TYPE), + RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno_e.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno_e.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno_e.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno_e.RLM_ERR_NOT_SUPPORTED), @@ -118,7 +134,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MONGODB_ERROR("MongodbError", realm_errno_e.RLM_ERR_MONGODB_ERROR), RLM_ERR_ARGUMENTS_NOT_ALLOWED("ArgumentsNotAllowed", realm_errno_e.RLM_ERR_ARGUMENTS_NOT_ALLOWED), RLM_ERR_FUNCTION_EXECUTION_ERROR("FunctionExecutionError", realm_errno_e.RLM_ERR_FUNCTION_EXECUTION_ERROR), - RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno_e.RLM_ERR_NO_MATCHING_RULE), + RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno_e.RLM_ERR_NO_MATCHING_RULE_FOUND), RLM_ERR_INTERNAL_SERVER_ERROR("InternalServerError", realm_errno_e.RLM_ERR_INTERNAL_SERVER_ERROR), RLM_ERR_AUTH_PROVIDER_NOT_FOUND("AuthProviderNotFound", realm_errno_e.RLM_ERR_AUTH_PROVIDER_NOT_FOUND), RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS("AuthProviderAlreadyExists", realm_errno_e.RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS), @@ -159,9 +175,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno_e.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno_e.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno_e.RLM_ERR_INVALID_SERVER_RESPONSE), - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno_e.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), + REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno_e.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno_e.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno_e.RLM_ERR_UNKNOWN); 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 88b254ca0b..35a6a7e8b2 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 @@ -27,8 +27,6 @@ import io.realm.kotlin.internal.interop.sync.JVMSyncSessionTransferCompletionCal import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.coroutines.CoroutineDispatcher @@ -1340,15 +1338,13 @@ actual object RealmInterop { actual fun realm_sync_session_handle_error_for_testing( syncSession: RealmSyncSessionPointer, - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + error: ErrorCode, errorMessage: String, isFatal: Boolean ) { realmc.realm_sync_session_handle_error_for_testing( syncSession.cptr(), - errorCode.nativeValue, - category.nativeValue, + error.nativeValue, errorMessage, isFatal ) @@ -1396,12 +1392,6 @@ actual object RealmInterop { baseUrl?.let { realmc.realm_app_config_set_base_url(config, it) } // Sync Connection Parameters - connectionParams.localAppName?.let { appName -> - realmc.realm_app_config_set_local_app_name(config, appName) - } - connectionParams.localAppVersion?.let { appVersion -> - realmc.realm_app_config_set_local_app_name(config, appVersion) - } realmc.realm_app_config_set_sdk(config, connectionParams.sdkName) realmc.realm_app_config_set_sdk_version(config, connectionParams.sdkVersion) realmc.realm_app_config_set_platform_version(config, connectionParams.platformVersion) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt index d06b775d09..8c3cf2c981 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.internal.interop.sync +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.SyncSessionTransferCompletionCallback // Interface used internally as a bridge between Kotlin (JVM) and JNI. @@ -28,7 +29,7 @@ internal class JVMSyncSessionTransferCompletionCallback( fun onSuccess() { callback.invoke(null) } - fun onError(category: Int, value: Int, message: String) { - callback.invoke(SyncErrorCode.newInstance(category, value, message)) + fun onError(categoryFlags: Int, value: Int, message: String) { + callback.invoke(CoreError(categoryFlags, value, message)) } } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index bfc9f20ec5..8ba8028fa8 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -17,55 +17,11 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.realm_sync_errno_client_e import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e +import io.realm.kotlin.internal.interop.realm_web_socket_errno_e -actual enum class ProtocolClientErrorCode( - override val description: String, - override val nativeValue: Int -) : CodeDescription { - RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED("ConnectionClosed", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED), - RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE), - RLM_SYNC_ERR_CLIENT_BAD_SYNTAX("BadSyntax", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SYNTAX), - RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER("BadMessageOrder", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT("BadClientFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_PROGRESS("BadProgress", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_PROGRESS), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX("BadChangesetHeaderSyntax", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE("BadChangesetSize", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE), - RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT("BadOriginFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION("BadServerVersion", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET("BadChangeset", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET), - RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT("BadRequestIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE("BadErrorCode", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE), - RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION("BadCompression", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION("BadClientVersion", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION), - RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED("SslServerCertRejected", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED), - RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT("PongTimeout", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT("BadClientFileIdentSalt", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT), - RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT("BadFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT("ConnectTimeout", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP("BadTimestamp", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP), - RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER("BadProtocolFromServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER("ClientTooOldForServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER("ClientTooNewForServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH("ProtocolMismatch", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH), - RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE("BadStateMessage", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE), - RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE("MissingProtocolFeature", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE), - RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED("HttpTunnelFailed", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED), - RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE("AutoClientResetFailure", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE); - - actual companion object { - internal actual fun of(nativeValue: Int): ProtocolClientErrorCode? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} - -actual enum class ProtocolConnectionErrorCode( +actual enum class SyncConnectionErrorCode( override val description: String, override val nativeValue: Int ) : CodeDescription { @@ -74,7 +30,7 @@ actual enum class ProtocolConnectionErrorCode( RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE), RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX("BadSyntax", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX), RLM_SYNC_ERR_CONNECTION_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION("WrongProtocolVersion", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION), + RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION("WrongBadSyncPartitionValueProtocolVersion", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION), RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT), RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT("ReuseOfSessionIdent", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT), RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION("BoundInOtherSession", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION), @@ -86,14 +42,14 @@ actual enum class ProtocolConnectionErrorCode( RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS("SwitchToPbs", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS); actual companion object { - internal actual fun of(nativeValue: Int): ProtocolConnectionErrorCode? = + internal actual fun of(nativeValue: Int): SyncConnectionErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } } } -actual enum class ProtocolSessionErrorCode( +actual enum class SyncSessionErrorCode( override val description: String, override val nativeValue: Int ) : CodeDescription { @@ -133,7 +89,47 @@ actual enum class ProtocolSessionErrorCode( RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS); actual companion object { - internal actual fun of(nativeValue: Int): ProtocolSessionErrorCode? = + internal actual fun of(nativeValue: Int): SyncSessionErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + +actual enum class WebsocketErrorCode( + override val description: String, + override val nativeValue: Int +) : CodeDescription { + RLM_ERR_WEBSOCKET_OK("Ok", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_OK), + RLM_ERR_WEBSOCKET_GOINGAWAY("GoingAway", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_GOINGAWAY), + RLM_ERR_WEBSOCKET_PROTOCOLERROR("ProtocolError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_PROTOCOLERROR), + RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA("UnsupportedData", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA), + RLM_ERR_WEBSOCKET_RESERVED("Reserved", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RESERVED), + RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED("NoStatusReceived", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED), + RLM_ERR_WEBSOCKET_ABNORMALCLOSURE("AbnormalClosure", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_ABNORMALCLOSURE), + RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA("InvalidPayloadData", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA), + RLM_ERR_WEBSOCKET_POLICYVIOLATION("PolicyViolation", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_POLICYVIOLATION), + RLM_ERR_WEBSOCKET_MESSAGETOOBIG("MessageToBig", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_MESSAGETOOBIG), + RLM_ERR_WEBSOCKET_INAVALIDEXTENSION("InvalidExtension", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INAVALIDEXTENSION), + RLM_ERR_WEBSOCKET_INTERNALSERVERERROR("InternalServerError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INTERNALSERVERERROR), + RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED("TlsHandshakeFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED), + + RLM_ERR_WEBSOCKET_UNAUTHORIZED("Unauthorized", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_UNAUTHORIZED), + RLM_ERR_WEBSOCKET_FORBIDDEN("Forbidden", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_FORBIDDEN), + RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY("MovedPermanently", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY), + RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD("ClientTooOld", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD), + RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW("ClientTooNew", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW), + RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH("ProtocolMismatch", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH), + + RLM_ERR_WEBSOCKET_RESOLVE_FAILED("ResolveFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RESOLVE_FAILED), + RLM_ERR_WEBSOCKET_CONNECTION_FAILED("ConnectionFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_FAILED), + RLM_ERR_WEBSOCKET_READ_ERROR("ReadError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_READ_ERROR), + RLM_ERR_WEBSOCKET_WRITE_ERROR("WriteError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_WRITE_ERROR), + RLM_ERR_WEBSOCKET_RETRY_ERROR("RetryError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RETRY_ERROR), + RLM_ERR_WEBSOCKET_FATAL_ERROR("FatalError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_FATAL_ERROR); + + actual companion object { + internal actual fun of(nativeValue: Int): WebsocketErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 6c66bfbd38..0000000000 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.realm.kotlin.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.realm_sync_error_category_e - -actual enum class SyncErrorCodeCategory(override val description: String, override val nativeValue: Int) : - CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT("Client", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_CLIENT), - RLM_SYNC_ERROR_CATEGORY_CONNECTION("Connection", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_CONNECTION), - RLM_SYNC_ERROR_CATEGORY_SESSION("Session", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_SESSION), - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET("Websocket", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_WEBSOCKET), - RLM_SYNC_ERROR_CATEGORY_SYSTEM("System", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_SYSTEM), - RLM_SYNC_ERROR_CATEGORY_UNKNOWN("Unknown", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_UNKNOWN); - - actual companion object { - internal actual fun of(nativeValue: Int): SyncErrorCodeCategory? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index d5f34c6379..0c92edfbee 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -11,7 +11,7 @@ headerFilter = realm.h realm/error_codes.h // libraryPaths.macos_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ // libraryPaths.ios_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ linkerOpts = -lcompression -lz -framework Foundation -framework CoreFoundation -framework Security -strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session +strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno --- #include #include diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index a792b233d7..ab2a7bfad5 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -35,7 +35,8 @@ actual enum class ErrorCategory( RLM_ERR_CAT_SERVICE_ERROR("Service", realm_error_category.RLM_ERR_CAT_SERVICE_ERROR.value.toInt()), RLM_ERR_CAT_HTTP_ERROR("Http", realm_error_category.RLM_ERR_CAT_HTTP_ERROR.value.toInt()), RLM_ERR_CAT_CUSTOM_ERROR("Custom", realm_error_category.RLM_ERR_CAT_CUSTOM_ERROR.value.toInt()), - RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category.RLM_ERR_CAT_WEBSOCKET_ERROR.value.toInt()); + RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category.RLM_ERR_CAT_WEBSOCKET_ERROR.value.toInt()), + RLM_ERR_CAT_SYNC_ERROR("Sync", realm_error_category.RLM_ERR_CAT_SYNC_ERROR.value.toInt()); actual companion object { diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 70987acd03..3fb35d665c 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -20,7 +20,7 @@ import realm_wrapper.realm_errno actual enum class ErrorCode( override val description: String, - error: realm_errno + private val nativeError: realm_errno ) : CodeDescription { RLM_ERR_NONE("None", realm_errno.RLM_ERR_NONE), RLM_ERR_RUNTIME("Runtime", realm_errno.RLM_ERR_RUNTIME), @@ -51,6 +51,22 @@ actual enum class ErrorCode( RLM_ERR_SCHEMA_VERSION_MISMATCH("SchemaVersionMismatch", realm_errno.RLM_ERR_SCHEMA_VERSION_MISMATCH), RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE("NoSubscriptionForWrite", realm_errno.RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE), RLM_ERR_OPERATION_ABORTED("OperationAborted", realm_errno.RLM_ERR_OPERATION_ABORTED), + RLM_ERR_AUTO_CLIENT_RESET_FAILED("AutoClientResetFailed", realm_errno.RLM_ERR_AUTO_CLIENT_RESET_FAILED), + RLM_ERR_BAD_SYNC_PARTITION_VALUE("BadSyncPartitionValue", realm_errno.RLM_ERR_BAD_SYNC_PARTITION_VALUE), + RLM_ERR_CONNECTION_CLOSED("ConnectionClosed", realm_errno.RLM_ERR_CONNECTION_CLOSED), + RLM_ERR_INVALID_SUBSCRIPTION_QUERY("InvalidSubscriptionQuery", realm_errno.RLM_ERR_INVALID_SUBSCRIPTION_QUERY), + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), + RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno.RLM_ERR_SYNC_COMPENSATING_WRITE), + RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), + RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno.RLM_ERR_SYNC_PERMISSION_DENIED), + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED("SyncProtocolNegotiationFailed", realm_errno.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED), + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED("SyncServerPermissionsChanged", realm_errno.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED), + RLM_ERR_SYNC_USER_MISMATCH("SyncUserMismatch", realm_errno.RLM_ERR_SYNC_USER_MISMATCH), + RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno.RLM_ERR_TLS_HANDSHAKE_FAILED), + RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno.RLM_ERR_WRONG_SYNC_TYPE), + RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno.RLM_ERR_NOT_SUPPORTED), @@ -122,7 +138,7 @@ actual enum class ErrorCode( RLM_ERR_MONGODB_ERROR("MongodbError", realm_errno.RLM_ERR_MONGODB_ERROR), RLM_ERR_ARGUMENTS_NOT_ALLOWED("ArgumentsNotAllowed", realm_errno.RLM_ERR_ARGUMENTS_NOT_ALLOWED), RLM_ERR_FUNCTION_EXECUTION_ERROR("FunctionExecutionError", realm_errno.RLM_ERR_FUNCTION_EXECUTION_ERROR), - RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno.RLM_ERR_NO_MATCHING_RULE), + RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno.RLM_ERR_NO_MATCHING_RULE_FOUND), RLM_ERR_INTERNAL_SERVER_ERROR("InternalServerError", realm_errno.RLM_ERR_INTERNAL_SERVER_ERROR), RLM_ERR_AUTH_PROVIDER_NOT_FOUND("AuthProviderNotFound", realm_errno.RLM_ERR_AUTH_PROVIDER_NOT_FOUND), RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS("AuthProviderAlreadyExists", realm_errno.RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS), @@ -163,13 +179,13 @@ actual enum class ErrorCode( RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno.RLM_ERR_INVALID_SERVER_RESPONSE), - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), + REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno.RLM_ERR_UNKNOWN); - override val nativeValue: Int = error.value.toInt() + override val nativeValue: Int = nativeError.value.toInt() + + val asNativeEnum: realm_errno = nativeError actual companion object { actual fun of(nativeValue: Int): ErrorCode? = 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 96a5d86e00..1f25cf704d 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 @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH -import io.realm.kotlin.internal.interop.RealmInterop.safeKString import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.AuthProvider @@ -31,11 +30,8 @@ import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.Response import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.atomicfu.AtomicBoolean @@ -121,7 +117,6 @@ import realm_wrapper.realm_scheduler_t import realm_wrapper.realm_set_t import realm_wrapper.realm_string_t import realm_wrapper.realm_sync_client_metadata_mode -import realm_wrapper.realm_sync_error_code_t import realm_wrapper.realm_sync_session_resync_mode import realm_wrapper.realm_sync_session_state_e import realm_wrapper.realm_sync_session_stop_policy_e @@ -562,7 +557,7 @@ actual object RealmInterop { ) err.usercode_error?.let { disposeUserData(it) } } else { - realm_wrapper.realm_release(realm) + realm_release(realm) } safeUserData(userData).invoke(exception) } @@ -2391,10 +2386,10 @@ actual object RealmInterop { syncConfig.cptr(), staticCFunction { userData, syncSession, error -> val syncError: SyncError = error.useContents { - val code = SyncErrorCode.newInstance( - error_code.category.value.toInt(), - error_code.value, - error_code.message.safeKString() + val code = CoreError( + this.status.categories.toInt(), + this.status.error.value.toInt(), + this.status.message.safeKString() ) val userInfoMap = (0 until user_info_length.toInt()) @@ -2421,7 +2416,6 @@ actual object RealmInterop { SyncError( errorCode = code, - detailedMessage = detailed_message.safeKString(), originalFilePath = userInfoMap[c_original_file_path_key.safeKString()], recoveryFilePath = userInfoMap[c_recovery_file_path_key.safeKString()], isFatal = is_fatal, @@ -2527,7 +2521,7 @@ actual object RealmInterop { ) { realm_wrapper.realm_sync_session_wait_for_download_completion( syncSession.cptr(), - staticCFunction?, Unit> { userData, error -> + staticCFunction?, Unit> { userData, error -> handleCompletionCallback(userData, error) }, StableRef.create(callback).asCPointer(), @@ -2543,7 +2537,7 @@ actual object RealmInterop { ) { realm_wrapper.realm_sync_session_wait_for_upload_completion( syncSession.cptr(), - staticCFunction?, Unit> { userData, error -> + staticCFunction?, Unit> { userData, error -> handleCompletionCallback(userData, error) }, StableRef.create(callback).asCPointer(), @@ -2574,15 +2568,13 @@ actual object RealmInterop { actual fun realm_sync_session_handle_error_for_testing( syncSession: RealmSyncSessionPointer, - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + error: ErrorCode, errorMessage: String, isFatal: Boolean ) { realm_wrapper.realm_sync_session_handle_error_for_testing( syncSession.cptr(), - errorCode.nativeValue.toInt(), - category.nativeValue, + error.asNativeEnum, errorMessage, isFatal ) @@ -2636,14 +2628,14 @@ actual object RealmInterop { private fun handleCompletionCallback( userData: CPointer?, - error: CPointer? + error: CPointer? ) { val completionCallback = safeUserData(userData) if (error != null) { - val category = error.pointed.category.value.toInt() - val value: Int = error.pointed.value + val category = error.pointed.categories.toInt() + val value: Int = error.pointed.error.value.toInt() val message = error.pointed.message.safeKString() - completionCallback.invoke(SyncErrorCode.newInstance(category, value, message)) + completionCallback.invoke(CoreError(category, value, message)) } else { completionCallback.invoke(null) } @@ -2672,12 +2664,6 @@ actual object RealmInterop { baseUrl?.let { realm_wrapper.realm_app_config_set_base_url(appConfig, it) } // Sync Connection Parameters - connectionParams.localAppName?.let { appName -> - realm_wrapper.realm_app_config_set_local_app_name(appConfig, appName) - } - connectionParams.localAppVersion?.let { appVersion -> - realm_wrapper.realm_app_config_set_local_app_name(appConfig, appVersion) - } realm_wrapper.realm_app_config_set_sdk(appConfig, connectionParams.sdkName) realm_wrapper.realm_app_config_set_sdk_version(appConfig, connectionParams.sdkVersion) realm_wrapper.realm_app_config_set_platform_version(appConfig, connectionParams.platformVersion) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 6504d9b0ea..20817a0ae5 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -16,57 +16,11 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -import realm_wrapper.realm_sync_errno_client import realm_wrapper.realm_sync_errno_connection import realm_wrapper.realm_sync_errno_session +import realm_wrapper.realm_web_socket_errno -actual enum class ProtocolClientErrorCode( - override val description: String, - errorCode: realm_sync_errno_client -) : CodeDescription { - RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED("ConnectionClosed", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED), - RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE), - RLM_SYNC_ERR_CLIENT_BAD_SYNTAX("BadSyntax", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SYNTAX), - RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER("BadMessageOrder", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT("BadClientFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_PROGRESS("BadProgress", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_PROGRESS), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX("BadChangesetHeaderSyntax", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE("BadChangesetSize", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE), - RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT("BadOriginFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION("BadServerVersion", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET("BadChangeset", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET), - RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT("BadRequestIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE("BadErrorCode", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE), - RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION("BadCompression", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION("BadClientVersion", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION), - RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED("SslServerCertRejected", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED), - RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT("PongTimeout", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT("BadClientFileIdentSalt", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT), - RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT("BadFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT("ConnectTimeout", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP("BadTimestamp", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP), - RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER("BadProtocolFromServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER("ClientTooOldForServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER("ClientTooNewForServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH("ProtocolMismatch", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH), - RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE("BadStateMessage", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE), - RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE("MissingProtocolFeature", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE), - RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED("HttpTunnelFailed", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED), - RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE("AutoClientResetFailure", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE); - - override val nativeValue: Int = errorCode.value.toInt() - - actual companion object { - internal actual fun of(nativeValue: Int): ProtocolClientErrorCode? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} - -actual enum class ProtocolConnectionErrorCode( +actual enum class SyncConnectionErrorCode( override val description: String, errorCode: realm_sync_errno_connection ) : CodeDescription { @@ -89,14 +43,14 @@ actual enum class ProtocolConnectionErrorCode( override val nativeValue: Int = errorCode.value.toInt() actual companion object { - internal actual fun of(nativeValue: Int): ProtocolConnectionErrorCode? = + internal actual fun of(nativeValue: Int): SyncConnectionErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } } } -actual enum class ProtocolSessionErrorCode( +actual enum class SyncSessionErrorCode( override val description: String, errorCode: realm_sync_errno_session ) : CodeDescription { @@ -138,7 +92,47 @@ actual enum class ProtocolSessionErrorCode( override val nativeValue: Int = errorCode.value.toInt() actual companion object { - internal actual fun of(nativeValue: Int): ProtocolSessionErrorCode? = + internal actual fun of(nativeValue: Int): SyncSessionErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + +actual enum class WebsocketErrorCode( + override val description: String, + errorCode: realm_web_socket_errno, +) : CodeDescription { + RLM_ERR_WEBSOCKET_OK("Ok", realm_web_socket_errno.RLM_ERR_WEBSOCKET_OK), + RLM_ERR_WEBSOCKET_GOINGAWAY("GoingAway", realm_web_socket_errno.RLM_ERR_WEBSOCKET_GOINGAWAY), + RLM_ERR_WEBSOCKET_PROTOCOLERROR("ProtocolError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_PROTOCOLERROR), + RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA("UnsupportedData", realm_web_socket_errno.RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA), + RLM_ERR_WEBSOCKET_RESERVED("Reserved", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RESERVED), + RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED("NoStatusReceived", realm_web_socket_errno.RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED), + RLM_ERR_WEBSOCKET_ABNORMALCLOSURE("AbnormalClosure", realm_web_socket_errno.RLM_ERR_WEBSOCKET_ABNORMALCLOSURE), + RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA("InvalidPayloadData", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA), + RLM_ERR_WEBSOCKET_POLICYVIOLATION("PolicyViolation", realm_web_socket_errno.RLM_ERR_WEBSOCKET_POLICYVIOLATION), + RLM_ERR_WEBSOCKET_MESSAGETOOBIG("MessageToBig", realm_web_socket_errno.RLM_ERR_WEBSOCKET_MESSAGETOOBIG), + RLM_ERR_WEBSOCKET_INAVALIDEXTENSION("InvalidExtension", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INAVALIDEXTENSION), + RLM_ERR_WEBSOCKET_INTERNALSERVERERROR("InternalServerError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INTERNALSERVERERROR), + RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED("TlsHandshakeFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED), + RLM_ERR_WEBSOCKET_UNAUTHORIZED("Unauthorized", realm_web_socket_errno.RLM_ERR_WEBSOCKET_UNAUTHORIZED), + RLM_ERR_WEBSOCKET_FORBIDDEN("Forbidden", realm_web_socket_errno.RLM_ERR_WEBSOCKET_FORBIDDEN), + RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY("MovedPermanently", realm_web_socket_errno.RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY), + RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD("ClientTooOld", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD), + RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW("ClientTooNew", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW), + RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH("ProtocolMismatch", realm_web_socket_errno.RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH), + RLM_ERR_WEBSOCKET_RESOLVE_FAILED("ResolveFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RESOLVE_FAILED), + RLM_ERR_WEBSOCKET_CONNECTION_FAILED("ConnectionFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CONNECTION_FAILED), + RLM_ERR_WEBSOCKET_READ_ERROR("ReadError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_READ_ERROR), + RLM_ERR_WEBSOCKET_WRITE_ERROR("WriteError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_WRITE_ERROR), + RLM_ERR_WEBSOCKET_RETRY_ERROR("RetryError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RETRY_ERROR), + RLM_ERR_WEBSOCKET_FATAL_ERROR("FatalError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_FATAL_ERROR); + + override val nativeValue: Int = errorCode.value.toInt() + + actual companion object { + internal actual fun of(nativeValue: Int): WebsocketErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 80254655ff..0000000000 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.realm.kotlin.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import realm_wrapper.realm_sync_error_category - -actual enum class SyncErrorCodeCategory( - override val description: String, - override val nativeValue: Int -) : CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT("Client", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_CLIENT.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_CONNECTION("Connection", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_CONNECTION.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_SESSION("Session", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_SESSION.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET("Websocket", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_WEBSOCKET.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_SYSTEM("System", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_SYSTEM.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_UNKNOWN("Unknown", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_UNKNOWN.value.toInt()); - - actual companion object { - - internal actual fun of(nativeValue: Int): SyncErrorCodeCategory? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/external/core b/packages/external/core index f1e962cd44..ea7c5d5e29 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit f1e962cd447f8b69f8f7cf46a188b1c6246923c5 +Subproject commit ea7c5d5e2900b8411a295aea3d1aa56aa55fff1d diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9848fc99c4..bc8a0ed658 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -714,12 +714,11 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) static JavaMethod sync_error_constructor(jenv, JavaClassGlobalDef::sync_error(), "", - "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;)V"); + "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;)V"); - jint category = static_cast(error.error_code.category); - jint value = error.error_code.value; - jstring msg = to_jstring(jenv, error.error_code.message); - jstring detailed_msg = to_jstring(jenv, error.detailed_message); + jint category = static_cast(error.status.categories); + jint value = static_cast(error.status.error); + jstring msg = to_jstring(jenv, error.status.message); jstring joriginal_file_path = nullptr; jstring jrecovery_file_path = nullptr; jboolean is_fatal = error.is_fatal; @@ -793,7 +792,6 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) category, value, msg, - detailed_msg, joriginal_file_path, jrecovery_file_path, is_fatal, @@ -827,7 +825,7 @@ void sync_set_error_handler(realm_sync_config_t* sync_config, jobject error_hand }); } -void transfer_completion_callback(void* userdata, realm_sync_error_code_t* error) { +void transfer_completion_callback(void* userdata, realm_error_t* error) { auto env = get_env(true); static JavaMethod java_success_callback_method(env, JavaClassGlobalDef::sync_session_transfer_completion_callback(), @@ -838,8 +836,8 @@ void transfer_completion_callback(void* userdata, realm_sync_error_code_t* error "onError", "(IILjava/lang/String;)V"); if (error) { - jint category = static_cast(error->category); - jint value = error->value; + jint category = static_cast(error->categories); + jint value = error->error; jstring msg = to_jstring(env, error->message); env->CallVoidMethod(static_cast(userdata), java_error_callback_method, category, value, msg); } else { diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 0854631d72..133b0095ee 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -72,7 +72,7 @@ void complete_http_request(void* request_context, jobject j_response); void -transfer_completion_callback(void* userdata, realm_sync_error_code_t* error); +transfer_completion_callback(void* userdata, realm_error_t* error); void realm_subscriptionset_changed_callback(void* userdata, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 98f9edb155..cb8fa12c7a 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -73,7 +73,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) val appDispatcher = appNetworkDispatcherFactory.create() val networkTransport = networkTransportFactory(appDispatcher) val appConfigPointer: RealmAppConfigurationPointer = - initializeRealmAppConfig(appName, appVersion, bundleId, networkTransport) + initializeRealmAppConfig(bundleId, networkTransport) var applicationInfo: String? = null // Define user agent strings sent when making the WebSocket connection to Device Sync if (appName != null || appVersion == null) { @@ -121,8 +121,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) // Only freeze anything after all properties are setup as this triggers freezing the actual // AppConfigurationImpl instance itself private fun initializeRealmAppConfig( - localAppName: String?, - localAppVersion: String?, bundleId: String, networkTransport: NetworkTransport ): RealmAppConfigurationPointer { @@ -133,8 +131,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) connectionParams = SyncConnectionParams( sdkVersion = SDK_VERSION, bundleId = bundleId, - localAppName = localAppName, - localAppVersion = localAppVersion, platformVersion = OS_VERSION, device = DEVICE_MANUFACTURER, deviceVersion = DEVICE_MODEL, diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt index e5c3d424bb..fb72304c78 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt @@ -1,14 +1,11 @@ package io.realm.kotlin.mongodb.internal import io.realm.kotlin.internal.interop.AppCallback +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.ErrorCategory import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.sync.AppError -import io.realm.kotlin.internal.interop.sync.ProtocolConnectionErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.mongodb.exceptions.AppException import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException @@ -80,64 +77,26 @@ internal fun channelResultCallback( internal fun convertSyncError(syncError: SyncError): SyncException { val errorCode = syncError.errorCode - // FIXME Client Reset errors are just reported as normal Sync Errors for now. - // Will be fixed by https://github.com/realm/realm-kotlin/issues/417 val message = createMessageFromSyncError(errorCode) - return when (errorCode.category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/client_base.hpp#L73 - // For now, it is unclear how to categorize these, so for now, just report as generic - // errors. - SyncException(message) - } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/protocol.hpp#L200 - // Use https://docs.google.com/spreadsheets/d/1SmiRxhFpD1XojqCKC-xAjjV-LKa9azeeWHg-zgr07lE/edit - // as guide for how to categorize Connection type errors. - when (errorCode.code) { - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE, // Unknown type of input message - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX, // Bad syntax in input message head - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION, // Wrong protocol version (CLIENT) (obsolete) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT, // Bad session identifier in input message - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT, // Overlapping reuse of session identifier (BIND) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION, // Client file bound in other session (IDENT) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_MESSAGE_ORDER, // Bad input message order - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_DECOMPRESSION, // Error in decompression (UPLOAD) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_CHANGESET_HEADER_SYNTAX, // Bad syntax in a changeset header (UPLOAD) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_CHANGESET_SIZE -> { // Bad size specified in changeset header (UPLOAD) - UnrecoverableSyncException(message) - } - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_FLX_SYNC, // Connected with wrong wire protocol - should switch to FLX sync - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS -> { // Connected with wrong wire protocol - should switch to PBS - WrongSyncTypeException(message) - } - else -> SyncException(message) - } - } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SESSION -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/protocol.hpp#L217 - // Use https://docs.google.com/spreadsheets/d/1SmiRxhFpD1XojqCKC-xAjjV-LKa9azeeWHg-zgr07lE/edit - // as guide for how to categorize Session type errors. - when (errorCode.code) { - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_BAD_QUERY -> { // Flexible Sync Query was rejected by the server - BadFlexibleSyncQueryException(message) - } - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_PERMISSION_DENIED -> - // Permission denied errors should be unrecoverable according to Core, i.e. the - // client will disconnect sync and transition to the "inactive" state - UnrecoverableSyncException(message) - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE -> - CompensatingWriteException(message, syncError.compensatingWrites) - else -> SyncException(message) - } + return when (errorCode.errorCode) { + ErrorCode.RLM_ERR_WRONG_SYNC_TYPE -> WrongSyncTypeException(message) + + ErrorCode.RLM_ERR_INVALID_SUBSCRIPTION_QUERY -> { + // Flexible Sync Query was rejected by the server + BadFlexibleSyncQueryException(message) } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN -> { - // It is unclear how to handle system level errors, so even though some of them - // are probably benign, report as top-level errors for now. - SyncException(message) + ErrorCode.RLM_ERR_SYNC_COMPENSATING_WRITE -> CompensatingWriteException(message, syncError.compensatingWrites) + + ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, + ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, + ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> { + // Permission denied errors should be unrecoverable according to Core, i.e. the + // client will disconnect sync and transition to the "inactive" state + UnrecoverableSyncException(message) } else -> { + // An error happened we are not sure how to handle. Just report as a generic + // SyncException. SyncException(message) } } @@ -277,30 +236,27 @@ internal fun convertAppError(appError: AppError): Throwable { } } -internal fun createMessageFromSyncError(error: SyncErrorCode): String { - val categoryDesc = error.category.description ?: error.category.nativeValue.toString() - val errorCodeDesc: String? = error.code.description ?: when (error.category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN, - -> { - // We lack information about these kinds of errors, - // so rather than returning a potentially misleading - // name, just return nothing. - null - } - else -> "Unknown" +internal fun createMessageFromSyncError(error: CoreError): String { + val categoryDesc = error.categories.description + val errorCodeDesc: String? = error.errorCode?.description ?: if (ErrorCategory.RLM_ERR_CAT_SYSTEM_ERROR in error.categories) { + // We lack information about these kinds of errors, + // so rather than returning a potentially misleading + // name, just return nothing. + null + } else { + "Unknown" } // Combine all the parts to form an error format that is human-readable. // An example could be this: `[Connection][WrongProtocolVersion(104)] Wrong protocol version was used: 25` val errorDesc: String = - if (errorCodeDesc == null) error.code.nativeValue.toString() else "$errorCodeDesc(${error.code.nativeValue})" + if (errorCodeDesc == null) error.errorCodeNativeValue.toString() else "$errorCodeDesc(${error.errorCodeNativeValue})" // Make sure that messages are uniformly formatted, so it looks nice if we append the // server log. - val msg = error.message.let { message: String -> + val msg = error.message?.let { message: String -> " $message${if (!message.endsWith(".")) "." else ""}" - } + } ?: "" return "[$categoryDesc][$errorDesc]$msg" } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 8409b7c556..720d2e4de2 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -19,16 +19,15 @@ package io.realm.kotlin.mongodb.internal import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.NotificationToken import io.realm.kotlin.internal.RealmImpl +import io.realm.kotlin.internal.interop.CoreError +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSyncSessionPointer import io.realm.kotlin.internal.interop.SyncSessionTransferCompletionCallback import io.realm.kotlin.internal.interop.sync.CoreConnectionState import io.realm.kotlin.internal.interop.sync.CoreSyncSessionState import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.trySendWithBufferOverflowCheck import io.realm.kotlin.mongodb.User @@ -165,15 +164,13 @@ internal open class SyncSessionImpl( /** * Simulates a sync error. Internal visibility only for testing. */ - internal fun simulateError( - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + internal fun simulateSyncError( + error: ErrorCode, message: String = "Simulate Client Reset" ) { RealmInterop.realm_sync_session_handle_error_for_testing( nativePointer, - errorCode, - category, + error, message, true ) @@ -201,7 +198,7 @@ internal open class SyncSessionImpl( val result: Any = withTimeout(timeout) { withContext(realm.notificationDispatcherHolder.dispatcher) { val callback = object : SyncSessionTransferCompletionCallback { - override fun invoke(errorCode: SyncErrorCode?) { + override fun invoke(errorCode: CoreError?) { if (errorCode != null) { // Transform the errorCode into a dummy syncError so we can have a // common path. diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt index 23745dbffc..3f7267c8a0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt @@ -2361,7 +2361,7 @@ class QueryTests { assertEquals(1, realm.query("fulltextField TEXT 'quick dog'").find().size) // words at different locations assertEquals(0, realm.query("fulltextField TEXT 'brown -fox'").find().size) // exclusion - assertEquals(0, realm.query("fulltextField TEXT 'fo*'").find().size) // token prefix search does not work + assertEquals(2, realm.query("fulltextField TEXT 'fo*'").find().size) // token prefix search is supported. assertEquals(1, realm.query("fulltextField TEXT 'cafe big'").find().size) // case- and diacritics-insensitive assertEquals(1, realm.query("fulltextField TEXT 'rødgrød'").find().size) // Latin-1 supplement assertEquals(0, realm.query("fulltextField TEXT '😊'").find().size) // Searching outside supported chars return nothing diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index 661a2538fb..1922c55e7a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.log.LogLevel import io.realm.kotlin.mongodb.exceptions.CompensatingWriteException import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException import io.realm.kotlin.mongodb.exceptions.SyncException @@ -63,7 +64,7 @@ class FlexibleSyncIntegrationTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) val (email, password) = TestHelper.randomEmail() to "password1234" runBlocking { app.createUserAndLogIn(email, password) @@ -332,7 +333,7 @@ class FlexibleSyncIntegrationTests { val exception: CompensatingWriteException = channel.receiveOrFail() - assertEquals("[Session][CompensatingWrite(231)] Client attempted a write that is disallowed by permissions, or modifies an object outside the current query, and the server undid the change.", exception.message) + assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is outside of permissions or query filters; it has been reverted Logs:"), exception.message) assertEquals(1, exception.writes.size) exception.writes[0].run { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 6dbadc3571..d1e362e6f0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -25,8 +25,7 @@ import io.realm.kotlin.entities.sync.flx.FlexChildObject import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel @@ -596,7 +595,7 @@ class SyncClientResetIntegrationTests { // testing the server will send a different message. This just ensures that // we don't accidentally modify or remove the message. assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) @@ -609,10 +608,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) // TODO Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) @@ -682,10 +678,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) // TODO Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) @@ -735,7 +728,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -747,7 +740,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -819,7 +812,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -831,7 +824,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -898,9 +891,8 @@ class SyncClientResetIntegrationTests { ) // assertEquals(ClientResetLogEvents.DISCARD_LOCAL_ON_AFTER_RESET, logChannel.receiveOrFail()) - (realm.syncSession as SyncSessionImpl).simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT + (realm.syncSession as SyncSessionImpl).simulateSyncError( + ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED ) // Validate that we receive logs on the error callback val actual = logChannel.receiveOrFail() @@ -948,10 +940,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with((realm.syncSession as SyncSessionImpl)) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) val exception = channel.receiveOrFail() val originalFilePath = assertNotNull(exception.originalFilePath) @@ -959,7 +948,7 @@ class SyncClientResetIntegrationTests { assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) } @@ -1002,10 +991,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) val exception = channel.receiveOrFail() @@ -1121,17 +1107,14 @@ class SyncClientResetIntegrationTests { runBlocking { realm.syncSession.downloadAllServerChanges(defaultTimeout) - (realm.syncSession as SyncSessionImpl).simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + (realm.syncSession as SyncSessionImpl).simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) val exception = channel.receiveOrFail() assertNotNull(exception.recoveryFilePath) assertNotNull(exception.originalFilePath) assertFalse(fileExists(exception.recoveryFilePath)) assertTrue(fileExists(exception.originalFilePath)) - assertTrue(exception.message!!.contains("Automatic recovery from client reset failed")) + assertTrue(exception.message!!.contains("Simulate Client Reset")) } } } @@ -1174,7 +1157,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -1235,7 +1218,7 @@ class SyncClientResetIntegrationTests { assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) @@ -1248,10 +1231,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) // TODO Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) @@ -1436,7 +1416,7 @@ class SyncClientResetIntegrationTests { assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) @@ -1449,10 +1429,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) // TODO Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 38e0fa85b4..8ddaa64cb7 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -386,15 +386,9 @@ class SyncedRealmTests { exception.message.let { errorMessage -> assertNotNull(errorMessage) // Some race on JVM in particular mean that different errors can be reported. - if (errorMessage.contains("[Client]")) { - assertTrue(errorMessage.contains("[BadChangeset(112)]"), errorMessage) - assertTrue(errorMessage.contains("Bad changeset (DOWNLOAD)"), errorMessage) - } else if (errorMessage.contains("[Session]")) { - assertTrue(errorMessage.contains("InvalidSchemaChange(225)"), errorMessage) - assertTrue( - errorMessage.contains("Invalid schema change (UPLOAD)"), - errorMessage - ) + if (errorMessage.contains("[Sync]")) { + assertTrue(errorMessage.contains("[BadChangeset(1015)]"), errorMessage) + assertTrue(errorMessage.contains("Schema mismatch"), errorMessage) } else { fail("Unexpected error message: $errorMessage") } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt index 9cf0bd49fd..6c6f5a1fc0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt @@ -18,67 +18,74 @@ package io.realm.kotlin.test.mongodb.common.internal +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.ErrorCategory +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.UnknownCodeDescription import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.mongodb.internal.convertAppError import io.realm.kotlin.mongodb.internal.convertSyncError import kotlin.test.Test import kotlin.test.assertEquals -const val UNMAPPED_CODE: Int = 0 +const val UNMAPPED_CATEGORY_CODE: Int = 0 +const val UNMAPPED_ERROR_CODE: Int = -1 class RealmSyncUtilsTest { + @Test - fun convertSyncErrorCode_unmappedErrorCode_categoryTypeUnknown() { + fun convertSyncErrorCode_unmappedErrorCode2() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN, - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR.nativeValue, + errorCodeNativeValue = UNMAPPED_ERROR_CODE, + messageNativeValue = "Placeholder message" ) ) ) - assertEquals("[Unknown][$UNMAPPED_CODE] Placeholder message.", syncException.message) + assertEquals( + "[Client][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", + syncException.message + ) } @Test - fun convertSyncErrorCode_unmappedErrorCode2() { + fun convertSyncErrorCode_unmappedErrorCategory() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION, - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = UNMAPPED_CATEGORY_CODE, + errorCodeNativeValue = UNMAPPED_ERROR_CODE, + messageNativeValue = "Placeholder message" ) ) ) assertEquals( - "[Connection][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", syncException.message ) } + // Core also has a concept of an "unknown" error code. It is being reported the same way + // as a truly unknown code with the description "Unknown()" @Test - fun convertSyncErrorCode_unmappedErrorCategory() { + fun convertSyncErrorCode_unknownNativeErrrorCode() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = UnknownCodeDescription(UNMAPPED_CODE), - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR.nativeValue, + errorCodeNativeValue = ErrorCode.RLM_ERR_UNKNOWN.nativeValue, + messageNativeValue = "Placeholder message" ) ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[Client][Unknown(2000000)] Placeholder message.", syncException.message ) } @@ -88,30 +95,30 @@ class RealmSyncUtilsTest { val appException = convertAppError( AppError( categoryFlags = ErrorCategory.RLM_ERR_CAT_CUSTOM_ERROR.nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) - assertEquals("[Custom][Unknown($UNMAPPED_CODE)] Placeholder message.", appException.message) + assertEquals("[Custom][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", appException.message) } @Test fun convertAppError_unmappedErrorCategory() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", appException.message ) } @@ -120,31 +127,31 @@ class RealmSyncUtilsTest { fun convertAppError_unmappedErrorCategoryAndErrorCode_noMessage() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = null, - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) - assertEquals("[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)]", appException.message) + assertEquals("[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)]", appException.message) } @Test fun convertAppError_unmappedErrorCategoryAndErrorCode_linkServerLog() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = "http://realm.io" ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message. Server log entry: http://realm.io", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message. Server log entry: http://realm.io", appException.message ) } @@ -153,16 +160,16 @@ class RealmSyncUtilsTest { fun convertAppError_unmappedErrorCategoryAndErrorCode_noMessage_linkServerLog() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = null, - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = "http://realm.io" ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Server log entry: http://realm.io", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Server log entry: http://realm.io", appException.message ) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt index 6ef9b2f491..4ece5ae87c 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt @@ -29,8 +29,6 @@ internal class SyncConnectionParamsTests { fun allProperties() { val props = SyncConnectionParams( sdkVersion = "sdkVersion", - localAppName = "appName", - localAppVersion = "appVersion", bundleId = "bundleId", platformVersion = "platformVersion", device = "device", @@ -40,8 +38,6 @@ internal class SyncConnectionParamsTests { ) assertEquals("Kotlin", props.sdkName) assertEquals("sdkVersion", props.sdkVersion) - assertEquals("appName", props.localAppName) - assertEquals("appVersion", props.localAppVersion) assertEquals("bundleId", props.bundleId) assertEquals("platformVersion", props.platformVersion) assertEquals("device", props.device) From 2ce9a278a11a5c6fa977395a5a068a6bda05c6b3 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 1 Sep 2023 11:18:07 +0200 Subject: [PATCH 11/27] Update to Realm Core 13.19.1 (#1493) --- CHANGELOG.md | 2 +- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/ErrorCode.kt | 1 + packages/external/core | 2 +- .../io/realm/kotlin/internal/Converters.kt | 12 ++++ .../io/realm/kotlin/types/geo/GeoBox.kt | 2 +- .../io/realm/kotlin/types/geo/GeoCircle.kt | 3 +- .../io/realm/kotlin/types/geo/GeoPoint.kt | 2 +- .../io/realm/kotlin/types/geo/GeoPolygon.kt | 5 +- .../kotlin/test/common/GeoSpatialTests.kt | 71 +++++++++++++++++++ 11 files changed, 95 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a659cc213e..89a0d45460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,7 @@ if the content is the same. Custom implementations of these methods will be resp * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.19.0, commit ea7c5d5e2900b8411a295aea3d1aa56aa55fff1d. +* Updated to Realm Core 13.19.1, commit c258e2681bca5fb33bbd23c112493817b43bfa86. ## 1.10.2 (2023-07-21) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index b4d95acbf2..31db69c4c8 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -57,6 +57,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_SYNC_CLIENT_RESET_REQUIRED, RLM_ERR_SYNC_COMPENSATING_WRITE, RLM_ERR_SYNC_CONNECT_FAILED, + RLM_ERR_SYNC_CONNECT_TIMEOUT, RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE, RLM_ERR_SYNC_PERMISSION_DENIED, RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 36bcd30327..28bdbb8cb0 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -54,6 +54,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno_e.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno_e.RLM_ERR_SYNC_COMPENSATING_WRITE), RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno_e.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_CONNECT_TIMEOUT("SyncConnectTimeout", realm_errno_e.RLM_ERR_SYNC_CONNECT_TIMEOUT), RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno_e.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno_e.RLM_ERR_SYNC_PERMISSION_DENIED), RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno_e.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 3fb35d665c..aadd8db464 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -58,6 +58,7 @@ actual enum class ErrorCode( RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno.RLM_ERR_SYNC_COMPENSATING_WRITE), RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_CONNECT_TIMEOUT("SyncConnectTimeout", realm_errno.RLM_ERR_SYNC_CONNECT_TIMEOUT), RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno.RLM_ERR_SYNC_PERMISSION_DENIED), RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), diff --git a/packages/external/core b/packages/external/core index ea7c5d5e29..c258e2681b 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit ea7c5d5e2900b8411a295aea3d1aa56aa55fff1d +Subproject commit c258e2681bca5fb33bbd23c112493817b43bfa86 diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt index 4267be41c1..7100685660 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.annotations.ExperimentalGeoSpatialApi import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject @@ -36,6 +37,9 @@ import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmUUID +import io.realm.kotlin.types.geo.GeoBox +import io.realm.kotlin.types.geo.GeoCircle +import io.realm.kotlin.types.geo.GeoPolygon import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 import kotlin.native.concurrent.SharedImmutable @@ -352,6 +356,7 @@ internal object RealmValueArgumentConverter { } ?: nullTransport() } + @OptIn(ExperimentalGeoSpatialApi::class) fun MemTrackingAllocator.convertQueryArg(value: Any?): RealmQueryArgument = when (value) { is Collection<*> -> { @@ -374,6 +379,13 @@ internal object RealmValueArgumentConverter { } ) } + is GeoBox, + is GeoCircle, + is GeoPolygon -> { + // Hack support for geospatial arguments until we have propert C-API support. + // See https://github.com/realm/realm-core/pull/6934 + RealmQuerySingleArgument(kAnyToRealmValue(value.toString())) + } else -> { RealmQuerySingleArgument(kAnyToRealmValue(value)) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt index dde9e57514..5e3d353b45 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt @@ -28,7 +28,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoBox * val bottomLeft = GeoPoint.create(latitude = 5.0, longitude = 5.0) * val topRight = GeoPoint.create(latitude = 10.0, longitude = 10.0) * val searchArea = GeoBox.create(bottomLeft, topRight) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` */ @ExperimentalGeoSpatialApi diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt index ce43fc39fd..df104a6e96 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt @@ -26,7 +26,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoCircle * ``` * val newYork = GeoPoint.create(latitude = 40.730610, longitude = -73.935242) * val searchArea = GeoCircle.create(center = newYork, radius = Distance.fromMiles(2.0)) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` */ @ExperimentalGeoSpatialApi @@ -47,6 +47,7 @@ public interface GeoCircle { * ``` * val circle = GeoCircle.create(center = GeoPoint.create(0.0, 0.0), radius = Distance.fromKilometers(10.0)) * val results = realm.query("location GEOWITHIN $circle").find() + * ``` */ public override fun toString(): String diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt index 774f9c0f8d..275e17be09 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt @@ -88,7 +88,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoPoint * * val newYork = GeoPoint.create(latitude = 40.730610, longitude = -73.935242) * val searchArea = GeoCircle.create(center = newYork, radius = Distance.fromMiles(2.0)) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` * * A proper persistable GeoPoint class will be implemented in an upcoming release. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt index 5409aee9cc..e4047d89ae 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt @@ -60,7 +60,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoPolygon * GeoPoint.create(4.0, 1.0) * )) * ) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * * ``` */ @ExperimentalGeoSpatialApi @@ -88,7 +88,8 @@ public interface GeoPolygon { * GeoPoint.create(0.0, 0.0) * ) * val searchArea = GeoPolygon.create(outerRing) - * val results = realm.query("location GEOWITHIN searchArea").find() + * val results = realm.query("location GEOWITHIN $searchArea").find() + * ``` */ override fun toString(): String diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt index 967ed0a5de..fec02fddd5 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt @@ -520,4 +520,75 @@ class GeoSpatialTests { } } } + + // Verify that geo objects can be passed directly as query arguments + // Kotlin will do implicit conversion to strings until native type support is added + @Test + fun asQueryArguments() { + realm.writeBlocking { + copyToRealm( + Restaurant().apply { + location = Location(latitude = 20.0, longitude = 20.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = 5.0, longitude = 5.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = -5.0, longitude = -5.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = 0.0, longitude = 0.0) + } + ) + } + + var sphere = GeoCircle.create( + center = GeoPoint.create(0.0, 0.0), + radius = Distance.fromKilometers(0.0) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", sphere).count().find()) + + var box = GeoBox.create( + bottomLeft = GeoPoint.create(-1.0, -1.0), + topRight = GeoPoint.create(1.0, 1.0) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", box).count().find()) + + val onlyOuterRing = GeoPolygon.create( + outerRing = listOf( + GeoPoint.create(-5.0, -5.0), + GeoPoint.create(5.0, -5.0), + GeoPoint.create(5.0, 5.0), + GeoPoint.create(-5.0, 5.0), + GeoPoint.create(-5.0, -5.0) + ) + ) + assertEquals(2, realm.query("location GEOWITHIN $0", onlyOuterRing).count().find()) + + val polygonWithHole = GeoPolygon.create( + outerRing = listOf( + GeoPoint.create(-5.0, -5.0), + GeoPoint.create(5.0, -5.0), + GeoPoint.create(5.0, 5.0), + GeoPoint.create(-5.0, 5.0), + GeoPoint.create(-5.0, -5.0) + ), + holes = arrayOf( + listOf( + GeoPoint.create(-4.0, -4.0), + GeoPoint.create(4.0, -4.0), + GeoPoint.create(4.0, 4.0), + GeoPoint.create(-4.0, 4.0), + GeoPoint.create(-4.0, -4.0) + ) + ) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", polygonWithHole).count().find()) + } } From 9138b66846fac53d60c225d1962bd951206d31bd Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 1 Sep 2023 11:21:14 +0200 Subject: [PATCH 12/27] Add support for automatic resetting incremental backoff when network status changes (#1491) --- CHANGELOG.md | 1 + .../kotlin/internal/interop/RealmInterop.kt | 5 + .../kotlin/internal/interop/RealmInterop.kt | 11 + .../kotlin/internal/interop/RealmInterop.kt | 11 + .../realm/kotlin/internal/RealmInstantImpl.kt | 2 +- .../kotlin/internal/platform/SystemUtils.kt | 2 +- .../kotlin/internal/platform/SystemUtils.kt | 2 +- .../kotlin/internal/platform/SystemUtils.kt | 2 +- .../src/androidMain/AndroidManifest.xml | 14 + .../mongodb/internal/NetworkStateObserver.kt | 6 + .../mongodb/internal/RealmSyncInitializer.kt | 129 +++++ .../kotlin/io/realm/kotlin/mongodb/App.kt | 7 + .../realm/kotlin/mongodb/internal/AppImpl.kt | 40 ++ .../mongodb/internal/NetworkStateObserver.kt | 64 +++ .../realm/kotlin/mongodb/internal/SyncImpl.kt | 19 + .../io/realm/kotlin/mongodb/sync/Sync.kt | 45 ++ .../mongodb/internal/NetworkStateObserver.kt | 6 + .../mongodb/internal/NetworkStateObserver.kt | 8 + .../io/realm/kotlin/test/mongodb/TestApp.kt | 78 ++- .../test/mongodb/common/ApiKeyAuthTests.kt | 2 +- .../mongodb/common/AppConfigurationTests.kt | 36 +- .../kotlin/test/mongodb/common/AppTests.kt | 66 +-- .../mongodb/common/AsymmetricSyncTests.kt | 2 +- .../test/mongodb/common/CredentialsTests.kt | 2 +- .../mongodb/common/EmailPasswordAuthTests.kt | 6 +- .../common/FlexibleSyncConfigurationTests.kt | 2 +- .../common/FlexibleSyncIntegrationTests.kt | 2 +- .../test/mongodb/common/FunctionsTests.kt | 1 + .../mongodb/common/HttpLogObfuscatorTests.kt | 2 + .../common/MutableSubscriptionSetTests.kt | 2 +- .../mongodb/common/ProgressListenerTests.kt | 47 +- .../common/SubscriptionExtensionsTests.kt | 2 +- .../mongodb/common/SubscriptionSetTests.kt | 28 +- .../test/mongodb/common/SubscriptionTests.kt | 2 +- .../common/SyncClientResetIntegrationTests.kt | 1 + .../test/mongodb/common/SyncClientTests.kt | 96 ++++ .../test/mongodb/common/SyncConfigTests.kt | 4 +- .../test/mongodb/common/SyncSessionTests.kt | 2 +- .../test/mongodb/common/SyncedRealmTests.kt | 510 +++++++++--------- .../test/mongodb/common/UserProfileTests.kt | 1 + .../kotlin/test/mongodb/common/UserTests.kt | 2 +- .../mongodb/common/nonlatin/NonLatinTests.kt | 2 +- .../kotlin/test/mongodb/jvm/RealmTests.kt | 26 +- 43 files changed, 880 insertions(+), 418 deletions(-) create mode 100644 packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt create mode 100644 packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt create mode 100644 packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt create mode 100644 packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt create mode 100644 packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt create mode 100644 packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a0d45460..9c07ddc507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ if the content is the same. Custom implementations of these methods will be resp * Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403)) * Support for automatic resolution of embedded object constraints during migration through `RealmConfiguration.Builder.migration(migration: AutomaticSchemaMigration, resolveEmbeddedObjectConstraints: Boolean)`. (Issue [#1464](https://github.com/realm/realm-kotlin/issues/1464) * [Sync] Add support for customizing authorization headers and adding additional custom headers to all Atlas App service requests with `AppConfiguration.Builder.authorizationHeaderName()` and `AppConfiguration.Builder.addCustomRequestHeader(...)`. (Issue [#1453](https://github.com/realm/realm-kotlin/pull/1453)) +* [Sync] Added support for manually triggering a reconnect attempt for Device Sync. This is done through a new `App.Sync.reconnect()` method. This method is also now called automatically when a mobile device toggles off airplane mode. (Issue [#1479](https://github.com/realm/realm-kotlin/issues/1479)) ### Fixed * None. 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 731a6a1e57..92e8181adc 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 @@ -707,6 +707,11 @@ expect object RealmInterop { callback: AppCallback ) + // Sync Client + fun realm_app_sync_client_reconnect(app: RealmAppPointer) + fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean + fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) + // Sync config fun realm_config_set_sync_config( realmConfiguration: RealmConfigurationPointer, 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 35a6a7e8b2..b42fe90b2f 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 @@ -1558,6 +1558,17 @@ actual object RealmInterop { ) } + actual fun realm_app_sync_client_reconnect(app: RealmAppPointer) { + realmc.realm_app_sync_client_reconnect(app.cptr()) + } + actual fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean { + return realmc.realm_app_sync_client_has_sessions(app.cptr()) + } + + actual fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) { + realmc.realm_app_sync_client_wait_for_sessions_to_terminate(app.cptr()) + } + actual fun realm_sync_config_new(user: RealmUserPointer, partition: String): RealmSyncConfigurationPointer { return LongPointerWrapper(realmc.realm_sync_config_new(user.cptr(), partition)).also { ptr -> // Stop the session immediately when the Realm is closed, so the lifecycle of the 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 1f25cf704d..268464c636 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 @@ -2948,6 +2948,17 @@ actual object RealmInterop { } } + actual fun realm_app_sync_client_reconnect(app: RealmAppPointer) { + realm_wrapper.realm_app_sync_client_reconnect(app.cptr()) + } + actual fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean { + return realm_wrapper.realm_app_sync_client_has_sessions(app.cptr()) + } + + actual fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) { + realm_wrapper.realm_app_sync_client_wait_for_sessions_to_terminate(app.cptr()) + } + actual fun realm_config_set_sync_config(realmConfiguration: RealmConfigurationPointer, syncConfiguration: RealmSyncConfigurationPointer) { realm_wrapper.realm_config_set_sync_config(realmConfiguration.cptr(), syncConfiguration.cptr()) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt index a6fbd387aa..8f0c714fac 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt @@ -32,7 +32,7 @@ public data class RealmInstantImpl(override val seconds: Long, override val nano } } -internal fun RealmInstant.toDuration(): Duration { +public fun RealmInstant.toDuration(): Duration { return epochSeconds.seconds + nanosecondsOfSecond.nanoseconds } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 32ce083867..bfec4f83b5 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -131,7 +131,7 @@ public expect fun epochInSeconds(): Long /** * Returns a RealmInstant representing the time that has passed since the Unix epoch. */ -internal expect fun currentTime(): RealmInstant +public expect fun currentTime(): RealmInstant /** * Returns the type of a mutable property. diff --git a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 11ed3e1512..5f042e6de1 100644 --- a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -28,7 +28,7 @@ public actual fun epochInSeconds(): Long = * Since internalNow() should only logically return a value after the Unix epoch, it is safe to create a RealmInstant * without considering having to pass negative nanoseconds. */ -internal actual fun currentTime(): RealmInstant { +public actual fun currentTime(): RealmInstant { val jtInstant = systemUTC().instant() return RealmInstantImpl(jtInstant.epochSecond, jtInstant.nano) } diff --git a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index c1fff3dedd..4b71b3708b 100644 --- a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -68,7 +68,7 @@ public actual fun epochInSeconds(): Long = * without considering having to pass negative nanoseconds. */ @Suppress("MagicNumber") -internal actual fun currentTime(): RealmInstant { +public actual fun currentTime(): RealmInstant { val secs: Double = NSDate().timeIntervalSince1970 return when { // We can't convert the MIN value to ms - it is initialized with Long.MIN_VALUE and diff --git a/packages/library-sync/src/androidMain/AndroidManifest.xml b/packages/library-sync/src/androidMain/AndroidManifest.xml index a24af63904..34c98bc70e 100644 --- a/packages/library-sync/src/androidMain/AndroidManifest.xml +++ b/packages/library-sync/src/androidMain/AndroidManifest.xml @@ -16,8 +16,22 @@ --> + + + + + + + diff --git a/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..b89325a7a1 --- /dev/null +++ b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,6 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // Registering network state listeners are done in io.realm.kotlin.mongodb.RealmSyncInitializer + // so we do not have to store the Android Context. +} diff --git a/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt new file mode 100644 index 0000000000..f5bed0c72c --- /dev/null +++ b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.internal + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.net.NetworkRequest +import android.os.Build +import androidx.startup.Initializer +import io.realm.kotlin.internal.RealmInitializer +import io.realm.kotlin.log.RealmLog + +/** + * An **initializer** for Sync specific functionality that does not fit into the `RealmInitializer` + * in cinterop.o allow Realm to access context properties. + */ +class RealmSyncInitializer : Initializer { + + companion object { + @Suppress("DEPRECATION") // Should only be called below API 21 + fun isConnected(cm: ConnectivityManager?): Boolean { + return cm?.let { + val networkInfo: NetworkInfo? = cm.activeNetworkInfo + networkInfo != null && networkInfo.isConnectedOrConnecting || isEmulator() + } ?: true + } + + // Credit: http://stackoverflow.com/questions/2799097/how-can-i-detect-when-an-android-application-is-running-in-the-emulator + fun isEmulator(): Boolean { + return Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.startsWith("unknown") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT + } + } + + @Suppress("invisible_member", "invisible_reference", "NestedBlockDepth") + override fun create(context: Context): Context { + val result: Int = context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + if (result == PackageManager.PERMISSION_GRANTED) { + try { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + // There has been a fair amount of changes and deprecations with regard to how to listen + // to the network status. ConnectivityManager#CONNECTIVITY_ACTION was deprecated in API 28 + // but ConnectivityManager.NetworkCallback became available a lot sooner in API 21, so + // we default to this as soon as possible. + // + // On later versions of Android (need reference), these callbacks will also only trigger + // if the app is in the foreground. + // + // The current implementation is a best-effort in detecting when the network is available + // again. + // + // See https://developer.android.com/training/basics/network-ops/reading-network-state + // See https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION + // See https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 21 */) { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 23 */) { + request.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + RealmLog.info("Register ConnectivityManager network callbacks") + connectivityManager?.registerNetworkCallback( + request.build(), + object : NetworkCallback() { + override fun onAvailable(network: Network) { + NetworkStateObserver.notifyConnectionChange(true) + } + + override fun onUnavailable() { + NetworkStateObserver.notifyConnectionChange(false) + } + } + ) + } else { + RealmLog.info("Register BroadcastReceiver connectivity callbacks") + @Suppress("DEPRECATION") + context.registerReceiver( + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val isConnected: Boolean = isConnected(connectivityManager) + NetworkStateObserver.notifyConnectionChange(isConnected) + } + }, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + ) + } + } catch (ex: Exception) { + RealmLog.warn("Something went wrong trying to register a network state listener: $ex") + } + } else { + RealmLog.warn( + "It was not possible to register a network state listener. " + + "ACCESS_NETWORK_STATE was not granted." + ) + } + return context + } + + override fun dependencies(): MutableList>> { + return mutableListOf(RealmInitializer::class.java) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 8bf334fcf5..21a44d3831 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -23,6 +23,7 @@ import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.AppImpl +import io.realm.kotlin.mongodb.sync.Sync import kotlinx.coroutines.flow.Flow /** @@ -77,6 +78,12 @@ public interface App { */ public val currentUser: User? + /** + * Returns a Device Sync manager that control functionality across all open realms associated + * with this app. + */ + public val sync: Sync + /** * Returns all known users that are either [User.State.LOGGED_IN] or [User.State.LOGGED_OUT]. * Only users that at some point logged into this device will be returned. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index 260a220332..ee1a151d80 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -20,19 +20,25 @@ import io.realm.kotlin.internal.interop.RealmAppPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmUserPointer import io.realm.kotlin.internal.interop.sync.NetworkTransport +import io.realm.kotlin.internal.toDuration import io.realm.kotlin.internal.util.DispatcherHolder import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.use +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.auth.EmailPasswordAuth +import io.realm.kotlin.mongodb.sync.Sync +import io.realm.kotlin.types.RealmInstant import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds internal typealias AppResources = Triple @@ -45,6 +51,37 @@ public class AppImpl( internal val appNetworkDispatcher: DispatcherHolder private val networkTransport: NetworkTransport + private var lastOnlineStateReported: Duration? = null + private var lastConnectedState: Boolean? = null // null = unknown, true = connected, false = disconnected + @Suppress("MagicNumber") + private val reconnectThreshold = 5.seconds + + @Suppress("invisible_member", "invisible_reference", "MagicNumber") + private val connectionListener = NetworkStateObserver.ConnectionListener { connectionAvailable -> + // In an ideal world, we would be able to reliably detect the network coming and + // going. Unfortunately that does not seem to be case (at least on Android). + // + // So instead of assuming that we have always detect the device going offline first, + // we just tell Realm Core to reconnect when we detect the network has come back. + // + // Due to the way network interfaces are re-enabled on Android, we might see multiple + // "isOnline" messages in short order. So in order to prevent resetting the network + // too often we throttle messages, so a reconnect can only happen ever 5 seconds. + RealmLog.debug("Network state change detected. ConnectionAvailable = $connectionAvailable") + val now: Duration = RealmInstant.now().toDuration() + if (connectionAvailable && (lastOnlineStateReported == null || now.minus(lastOnlineStateReported!!) > reconnectThreshold) + ) { + RealmLog.info("Trigger network reconnect.") + try { + sync.reconnect() + } catch (ex: Exception) { + RealmLog.error(ex.toString()) + } + lastOnlineStateReported = now + } + lastConnectedState = connectionAvailable + } + // Allow some delay between events being reported and them being consumed. // When the (somewhat arbitrary) limit is hit, we will throw an exception, since we assume the // consumer is doing something wrong. This is also needed because we don't @@ -61,6 +98,7 @@ public class AppImpl( appNetworkDispatcher = appResources.first networkTransport = appResources.second nativePointer = appResources.third + NetworkStateObserver.addListener(connectionListener) } override val emailPasswordAuth: EmailPasswordAuth by lazy { EmailPasswordAuthImpl(nativePointer) } @@ -68,6 +106,7 @@ public class AppImpl( override val currentUser: User? get() = RealmInterop.realm_app_get_current_user(nativePointer) ?.let { UserImpl(it, this) } + override val sync: Sync by lazy { SyncImpl(nativePointer) } override fun allUsers(): Map { val nativeUsers: List = @@ -130,6 +169,7 @@ public class AppImpl( // be beneficial in order to reason about the lifecycle of the Sync thread and dispatchers. networkTransport.close() nativePointer.release() + NetworkStateObserver.removeListener(connectionListener) } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..3887309801 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,64 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.interop.SynchronizableObject + +// Register a system specific network listener (if supported) +internal expect fun registerSystemNetworkObserver() + +/** + * This class is responsible for keeping track of system events related to the network so it can + * delegate them to interested parties. + */ +internal object NetworkStateObserver { + + /** + * This interface is used in a thread-safe manner, i.e. implementers do not have to think + * about race conditions. + */ + internal fun interface ConnectionListener { + fun onChange(connectionAvailable: Boolean) + } + + private val mutex = SynchronizableObject() + private val listeners = mutableListOf() + + init { + registerSystemNetworkObserver() + } + + /** + * Called by each custom network implementation whenever a network change is detected. + */ + fun notifyConnectionChange(isOnline: Boolean) { + mutex.withLock { + listeners.forEach { + it.onChange(isOnline) + } + } + } + + /** + * Add a listener to be notified about any network changes. + * This method is thread safe. + * IMPORTANT: Not removing it again will result in leaks. + * @param listener the listener to add. + */ + fun addListener(listener: ConnectionListener) { + mutex.withLock { + listeners.add(listener) + } + } + + /** + * Removes a network listener. + * This method is thread safe. + * + * @param listener the listener to remove. + * @return `true` if the listener was removed. + */ + fun removeListener(listener: ConnectionListener): Boolean { + mutex.withLock { + return listeners.remove(listener) + } + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt new file mode 100644 index 0000000000..41391cd869 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt @@ -0,0 +1,19 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.interop.RealmAppPointer +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.mongodb.sync.Sync + +internal class SyncImpl(private val app: RealmAppPointer) : Sync { + + override val hasSyncSessions: Boolean + get() = RealmInterop.realm_app_sync_client_has_sessions(app) + + override fun reconnect() { + RealmInterop.realm_app_sync_client_reconnect(app) + } + + override fun waitForSessionsToTerminate() { + RealmInterop.realm_app_sync_client_wait_for_sessions_to_terminate(app) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt new file mode 100644 index 0000000000..51cbdcd537 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt @@ -0,0 +1,45 @@ +package io.realm.kotlin.mongodb.sync + +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.syncSession + +/** + * A _Device Sync_ manager responsible for controlling all sync sessions across all realms + * associated with a given [App] instance. For session functionality associated with a single + * realm, see [syncSession]. + * + * @see App.sync + * @see io.realm.kotlin.mongodb.syncSession + */ +public interface Sync { + + /** + * Returns whether or not any sync sessions are still active. + */ + public val hasSyncSessions: Boolean + + /** + * Realm will automatically detect when a device gets connectivity after being offline and + * resume syncing. However, as some of these checks are performed using incremental backoff, + * this will in some cases not happen immediately. + * + * In those cases it can be beneficial to call this method manually, which will force all + * sessions to attempt to reconnect immediately and reset any timers they are using for + * incremental backoff. + * + * Note, Realm has an internal default socket read timeout of 2 minutes. Calling this method + * within those two minutes will not trigger a reconnect. + */ + public fun reconnect() + + /** + * Calling this method will block until all sync sessions for a given [App] has terminated. + * + * Closing a Realm will terminate the sync session, but it is not synchronous as Realms + * communicate with their sync session using an asynchronous communication channel. This + * has the effect that trying to delete a Realm right after closing it will sometimes throw + * an [IllegalStateException]. Using this method can be a way to ensure it is safe to delete + * the file. + */ + public fun waitForSessionsToTerminate() +} diff --git a/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..4b8841194f --- /dev/null +++ b/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,6 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // Do nothing on JVM. + // There isn't a great way to detect network connectivity on this platform. +} diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..75cb4b9d68 --- /dev/null +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,8 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // This is handled automatically by Realm Core which will also call `Sync.reconnect()` + // automatically. So on iOS/macOS we do not do anything. + // See https://github.com/realm/realm-core/blob/a678c36a85cf299f745f68f8b5ceff364d714181/src/realm/object-store/sync/impl/sync_client.hpp#L82C3-L82C3 + // for further details. +} diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index f203b8c712..b34dbf1794 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -21,10 +21,12 @@ package io.realm.kotlin.test.mongodb import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.SynchronizableObject import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import io.realm.kotlin.log.LogLevel +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.log.RealmLogger import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration @@ -50,6 +52,15 @@ val TEST_APP_CLUSTER_NAME = SyncServerConfig.clusterName val TEST_SERVER_BASE_URL = SyncServerConfig.url const val DEFAULT_PASSWORD = "password1234" +// Expose a try-with-resource pattern for Test Apps +inline fun App.use(action: (App) -> Unit) { + try { + action(this) + } finally { + this.close() + } +} + /** * This class merges the classes [App] and [AppAdmin] making it easier to create an App that can be * used for testing. @@ -62,6 +73,8 @@ open class TestApp private constructor( pairAdminApp: Pair ) : App by pairAdminApp.first, AppAdmin by pairAdminApp.second { + var mutex = SynchronizableObject() + var isClosed: Boolean = false val app: App = pairAdminApp.first /** @@ -75,10 +88,13 @@ open class TestApp private constructor( **/ @Suppress("LongParameterList") constructor( + testId: String?, appName: String = TEST_APP_PARTITION, - dispatcher: CoroutineDispatcher = singleThreadDispatcher("test-app-dispatcher"), + dispatcher: CoroutineDispatcher = singleThreadDispatcher("$testId-dispatcher"), logLevel: LogLevel? = LogLevel.WARN, - builder: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it }, + builder: (AppConfiguration.Builder) -> AppConfiguration.Builder = { + it.syncRootDirectory(PlatformUtils.createTempDir("$appName-$testId")) + }, debug: Boolean = false, customLogger: RealmLogger? = null, networkTransport: NetworkTransport? = null, @@ -109,39 +125,47 @@ open class TestApp private constructor( } override fun close() { - // This is needed to "properly reset" all sessions across tests since deleting users - // directly using the REST API doesn't do the trick - runBlocking { - while (currentUser != null) { - (currentUser as User).logOut() + mutex.withLock { + if (isClosed) { + return } - deleteAllUsers() - } - if (dispatcher is CloseableCoroutineDispatcher) { - dispatcher.close() - } - app.close() + app.sync.waitForSessionsToTerminate() + + // This is needed to "properly reset" all sessions across tests since deleting users + // directly using the REST API doesn't do the trick + runBlocking { + try { + while (currentUser != null) { + (currentUser as User).logOut() + } + deleteAllUsers() + } catch (ex: Exception) { + // Some tests might render the server inaccessible, preventing us from + // deleting users. Assume those tests know what they are doing and + // ignore errors here. + RealmLog.warn("Server side users could not be deleted: $ex") + } + } + + if (dispatcher is CloseableCoroutineDispatcher) { + dispatcher.close() + } + app.close() - // Close network client resources - closeClient() + // Close network client resources + closeClient() - // Make sure to clear cached apps before deleting files - RealmInterop.realm_clear_cached_apps() + // Make sure to clear cached apps before deleting files + RealmInterop.realm_clear_cached_apps() - // Delete metadata Realm files - PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm") + // Delete metadata Realm files + PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm") + isClosed = true + } } companion object { - // Expose a try-with-resource pattern for Apps - inline fun TestApp.use(action: (TestApp) -> Unit) { - try { - action(this) - } finally { - this.close() - } - } @Suppress("LongParameterList") fun build( diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt index bfb5f3d4e9..626d505c0f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt @@ -49,7 +49,7 @@ class ApiKeyAuthTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) user = app.createUserAndLogin() provider = user.apiKeyAuth } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt index cf95af7372..ee6c4f41b4 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt @@ -29,9 +29,9 @@ import io.realm.kotlin.mongodb.exceptions.ServiceException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp -import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.receiveOrFail @@ -161,12 +161,11 @@ class AppConfigurationTests { @Test fun syncRootDirectory_appendDirectoryToPath() = runBlocking { val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" - val app = TestApp(builder = { + TestApp("syncRootDirectory_appendDirectoryToPath", builder = { it.syncRootDirectory(expectedRoot) - }) - val (email, password) = TestHelper.randomEmail() to "password1234" - val user = app.createUserAndLogIn(email, password) - try { + }).use { app -> + val (email, password) = TestHelper.randomEmail() to "password1234" + val user = app.createUserAndLogIn(email, password) assertEquals(expectedRoot, app.configuration.syncRootDirectory) // When creating the full path for a synced Realm, we will always append `/mongodb-realm` to // the configured `AppConfiguration.syncRootDir` @@ -175,8 +174,6 @@ class AppConfigurationTests { "${PATH_SEPARATOR}myCustomDir${PATH_SEPARATOR}mongodb-realm${PATH_SEPARATOR}${user.app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}s_$partitionValue.realm" val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf()).build() assertTrue(config.path.endsWith(suffix), "Failed: ${config.path} vs. $suffix") - } finally { - app.asTestApp.close() } } @@ -381,21 +378,16 @@ class AppConfigurationTests { // // Check that custom headers and auth header renames are correctly used for HTTP requests. @Test - fun customHeadersTest() { - var app: App? = null - try { - runBlocking { - app = TestApp( - builder = { builder -> - builder.customRequestHeaders { - put(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) - }.authorizationHeaderName(AUTH_HEADER_NAME) - } - ) - doCustomHeaderTest(app!!) + fun customHeadersTest() = runBlocking { + TestApp( + "customHeadersTest", + builder = { builder -> + builder.customRequestHeaders { + put(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) + }.authorizationHeaderName(AUTH_HEADER_NAME) } - } finally { - assertFailsWith { app?.close() } + ).use { app -> + doCustomHeaderTest(app) } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 05a379157a..6079e3edea 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -39,6 +39,7 @@ import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail @@ -65,7 +66,7 @@ class AppTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest @@ -77,9 +78,10 @@ class AppTests { @Test fun defaultApp() { - val defaultApp = App.create("foo") - assertEquals("foo", defaultApp.configuration.appId) - assertEquals(AppConfiguration.DEFAULT_BASE_URL, defaultApp.configuration.baseUrl) + App.create("foo").use { defaultApp -> + assertEquals("foo", defaultApp.configuration.appId) + assertEquals(AppConfiguration.DEFAULT_BASE_URL, defaultApp.configuration.baseUrl) + } } @Test @@ -372,18 +374,17 @@ class AppTests { fun encryptedMetadataRealm() { // Create new test app with a random encryption key val key = TestHelper.getRandomKey() - val app = TestApp( + TestApp( + "encryptedMetadataRealm", appName = TEST_APP_FLEX, builder = { it .encryptionKey(key) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -403,8 +404,6 @@ class AppTests { // Should be possible to open the encrypted metadata realm file with the encryption key Realm.open(config).close() - } finally { - app.close() } } @@ -412,18 +411,17 @@ class AppTests { fun encryptedMetadataRealm_openWithWrongKeyThrows() { // Create new test app with a random encryption key val correctKey = TestHelper.getRandomKey() - val app = TestApp( + TestApp( + "encryptedMetadataRealm_openWithWrongKeyThrows", appName = TEST_APP_FLEX, builder = { it .encryptionKey(correctKey) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -445,26 +443,23 @@ class AppTests { assertFailsWithMessage("Failed to open Realm file at path") { Realm.open(config) } - } finally { - app.close() } } @Test fun encryptedMetadataRealm_openWithoutKeyThrows() { // Create new test app with a random encryption key - val app = TestApp( + TestApp( + "encryptedMetadataRealm_openWithoutKeyThrows", appName = TEST_APP_FLEX, builder = { it .encryptionKey(TestHelper.getRandomKey()) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -483,31 +478,6 @@ class AppTests { assertFailsWithMessage("Failed to open Realm file at path") { Realm.open(config) } - } finally { - app.close() } } - -// -// // Check that it is possible to have two Java instances of an App class, but they will -// // share the underlying App state. -// @Test -// fun multipleInstancesSameApp() { -// // Create a second copy of the test app -// val app2 = TestApp() -// try { -// // User handling are shared between each app -// val user = app.login(Credentials.anonymous()); -// assertEquals(user, app2.currentUser()) -// assertEquals(user, app.allUsers().values.first()) -// assertEquals(user, app2.allUsers().values.first()) -// -// user.logOut(); -// -// assertNull(app.currentUser()) -// assertNull(app2.currentUser()) -// } finally { -// app2.close() -// } -// } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt index 9e7ad99269..7d2f05821f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt @@ -97,7 +97,7 @@ class AsymmetricSyncTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt index c9ed5c60d6..561caf2ae9 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt @@ -60,7 +60,7 @@ class CredentialsTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt index 8ecc85a944..40ee4d0ac1 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt @@ -32,7 +32,7 @@ class EmailPasswordAuthWithAutoConfirmTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) } @AfterTest @@ -243,7 +243,7 @@ class EmailPasswordAuthWithEmailConfirmTests { @BeforeTest fun setup() { - app = TestApp(appName = syncServerAppName("em-cnfrm"), initialSetup = { app: BaasApp, service: Service -> + app = TestApp(this::class.simpleName, appName = syncServerAppName("em-cnfrm"), initialSetup = { app: BaasApp, service: Service -> addEmailProvider(app, autoConfirm = false) }) } @@ -281,7 +281,7 @@ class EmailPasswordAuthWithCustomFunctionTests { @BeforeTest fun setup() { - app = TestApp(appName = syncServerAppName("em-cstm"), initialSetup = { app: BaasApp, service: Service -> + app = TestApp(this::class.simpleName, appName = syncServerAppName("em-cstm"), initialSetup = { app: BaasApp, service: Service -> addEmailProvider(app, autoConfirm = false, runConfirmationFunction = true) }) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt index b6ee0f9a07..1cd8d25aff 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt @@ -46,7 +46,7 @@ class FlexibleSyncConfigurationTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index 1922c55e7a..7ad9ff466a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -64,7 +64,7 @@ class FlexibleSyncIntegrationTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) val (email, password) = TestHelper.randomEmail() to "password1234" runBlocking { app.createUserAndLogIn(email, password) 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 41699ad677..1fd31781cf 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 @@ -196,6 +196,7 @@ class FunctionsTests { @BeforeTest fun setup() { app = TestApp( + FunctionsTests::class.simpleName, syncServerAppName("funcs"), ejson = EJson( serializersModule = SerializersModule { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt index 592058d5b7..b075ce09a7 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt @@ -119,6 +119,7 @@ class HttpLogObfuscatorTests { private fun initApp(): TestApp { return TestApp( + this::class.simpleName, appName = syncServerAppName("obfsctr"), logLevel = LogLevel.DEBUG, customLogger = ObfuscatorLoggerInspector(channel), @@ -144,6 +145,7 @@ class HttpLogObfuscatorTests { fun nullObfuscator() = runBlocking { val logger = CustomLogCollector("NULL-OBFUSCATOR", LogLevel.DEBUG) app = TestApp( + "nullObfuscator", appName = syncServerAppName("null-obf"), logLevel = LogLevel.DEBUG, builder = { it.httpLogObfuscator(null) }, diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt index 2e615de299..cf9bc8ef06 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt @@ -57,7 +57,7 @@ class MutableSubscriptionSetTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt index 6c709badd9..3b0b3dcda8 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt @@ -31,6 +31,7 @@ import io.realm.kotlin.test.mongodb.TEST_APP_PARTITION import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.use import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -69,7 +70,7 @@ class ProgressListenerTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) partitionValue = ObjectId().toString() } @@ -239,35 +240,37 @@ class ProgressListenerTests { @Test fun throwsOnFlexibleSync() = runBlocking { - val app = TestApp(TEST_APP_FLEX) - val user = app.createUserAndLogIn() - val configuration: SyncConfiguration = SyncConfiguration.create(user, schema) - Realm.open(configuration).use { realm -> - assertFailsWithMessage( - "Progress listeners are not supported for Flexible Sync" - ) { - realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.CURRENT_CHANGES) + TestApp("throwsOnFlexibleSync", TEST_APP_FLEX).use { + val user = app.createUserAndLogIn() + val configuration: SyncConfiguration = SyncConfiguration.create(user, schema) + Realm.open(configuration).use { realm -> + assertFailsWithMessage( + "Progress listeners are not supported for Flexible Sync" + ) { + realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.CURRENT_CHANGES) + } } } } @Test fun completesOnClose() = runBlocking { - val app = TestApp(TEST_APP_PARTITION) - val user = app.createUserAndLogIn() - val realm = Realm.open(createSyncConfig(user)) - try { - val flow = realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.INDEFINITELY) - val job = async { - withTimeout(10.seconds) { - flow.collect { } + TestApp("completesOnClose", TEST_APP_PARTITION).use { app -> + val user = app.createUserAndLogIn() + val realm = Realm.open(createSyncConfig(user)) + try { + val flow = realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.INDEFINITELY) + val job = async { + withTimeout(10.seconds) { + flow.collect { } + } } - } - realm.close() - job.await() - } finally { - if (!realm.isClosed()) { realm.close() + job.await() + } finally { + if (!realm.isClosed()) { + realm.close() + } } } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt index 596633f9a5..5dec23d4ef 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt @@ -60,7 +60,7 @@ class SubscriptionExtensionsTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt index bc3c8ecf77..af18bf174e 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt @@ -30,6 +30,7 @@ import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TEST_APP_PARTITION import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.use import kotlin.test.AfterTest @@ -56,7 +57,7 @@ class SubscriptionSetTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -89,18 +90,19 @@ class SubscriptionSetTests { @Test fun subscriptions_failOnNonFlexibleSyncRealms() { - val app = TestApp(appName = TEST_APP_PARTITION) - val (email, password) = TestHelper.randomEmail() to "password1234" - val user = runBlocking { - app.createUserAndLogIn(email, password) - } - val config = SyncConfiguration.create( - user, - TestHelper.randomPartitionValue(), - setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) - ) - Realm.open(config).use { partionBasedRealm -> - assertFailsWith { partionBasedRealm.subscriptions } + TestApp(this::class.simpleName, appName = TEST_APP_PARTITION).use { testApp -> + val (email, password) = TestHelper.randomEmail() to "password1234" + val user = runBlocking { + testApp.createUserAndLogIn(email, password) + } + val config = SyncConfiguration.create( + user, + TestHelper.randomPartitionValue(), + setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + ) + Realm.open(config).use { partionBasedRealm -> + assertFailsWith { partionBasedRealm.subscriptions } + } } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt index 7996094440..1eade76896 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt @@ -55,7 +55,7 @@ class SubscriptionTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index d1e362e6f0..5e9b6bb58d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -113,6 +113,7 @@ class SyncClientResetIntegrationTests { ) -> Unit ) { val app = TestApp( + this::class.simpleName, appName = appName, logLevel = LogLevel.INFO, customLogger = ClientResetLoggerInspector(logChannel), diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt new file mode 100644 index 0000000000..1a79d7349b --- /dev/null +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt @@ -0,0 +1,96 @@ +package io.realm.kotlin.test.mongodb.common + +import io.realm.kotlin.Realm +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.asTestApp +import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.util.TestHelper +import io.realm.kotlin.test.util.use +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for [io.realm.kotlin.mongodb.sync.Sync] that is accessed through + * [io.realm.kotlin.mongodb.App.sync]. + */ +class SyncClientTests { + + private lateinit var user: User + private lateinit var app: App + + @BeforeTest + fun setup() { + app = TestApp(this::class.simpleName) + val (email, password) = TestHelper.randomEmail() to "password1234" + user = runBlocking { + app.createUserAndLogIn(email, password) + } + } + + @AfterTest + fun tearDown() { + if (this::app.isInitialized) { + app.asTestApp.close() + } + } + + @Test + fun sync() { + assertNotNull(app.sync) + } + + // There is no way to test reconnect automatically, so just verify that code path does not crash. + @Test + fun reconnect_noRealms() { + app.sync.reconnect() + } + + // There is no way to test reconnect automatically, so just verify that code path does not crash. + @Test + fun reconnect() { + val config = SyncConfiguration.create(user, schema = setOf()) + Realm.open(config).use { + app.sync.reconnect() + } + } + + @Test + fun hasSyncSessions_noRealms() { + assertFalse(app.sync.hasSyncSessions) + } + + @Test + fun hasSyncSessions() { + val config = SyncConfiguration.create(user, schema = setOf()) + Realm.open(config).use { + assertTrue(app.sync.hasSyncSessions) + } + } + + @Test + fun waitForSessionsToTerminate_noRealms() { + app.sync.waitForSessionsToTerminate() + } + + @Test + fun waitForSessionsToTerminate() { + val config1 = SyncConfiguration.Builder(user, schema = setOf()).build() + val config2 = SyncConfiguration.Builder(user, schema = setOf()).name("other.realm").build() + + Realm.open(config1).use { + assertTrue(app.sync.hasSyncSessions) + Realm.open(config2).use { /* do nothing */ } + assertTrue(app.sync.hasSyncSessions) + } + app.sync.waitForSessionsToTerminate() + assertFalse(app.sync.hasSyncSessions) + } +} diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 7a10d27624..ffbb352aab 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -87,7 +87,7 @@ class SyncConfigTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest @@ -1230,7 +1230,7 @@ class SyncConfigTests { fun logLevelDoesNotGetOverwrittenByConfig() { app.asTestApp.close() // Prevent AppConfiguration to set a log level - app = TestApp(logLevel = null) + app = TestApp("logLevelDoesNotGetOverwrittenByConfig", logLevel = null) val expectedLogLevel = LogLevel.ALL diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt index 24a9d76c9d..aac6fec160 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt @@ -75,7 +75,7 @@ class SyncSessionTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = TestHelper.randomEmail() to "password1234" user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 8ddaa64cb7..038d06c91d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -56,6 +56,7 @@ import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.CustomLogCollector import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail @@ -110,7 +111,7 @@ class SyncedRealmTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = randomEmail() to "password1234" val user = runBlocking { @@ -861,36 +862,37 @@ class SyncedRealmTests { @Test fun writeCopyTo_localToFlexibleSync_throws() = runBlocking { - val flexApp = TestApp( + TestApp( + this::class.simpleName, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val (email1, password1) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val localConfig = createWriteCopyLocalConfig("local.realm") - val flexSyncConfig = createFlexibleSyncConfig( - user = user1, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) - ) - Realm.open(localConfig).use { localRealm -> - localRealm.writeBlocking { - copyToRealm( - SyncObjectWithAllTypes().apply { - stringField = "local object" - } + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val flexSyncConfig = createFlexibleSyncConfig( + user = user1, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) - } - assertFailsWith { - localRealm.writeCopyTo(flexSyncConfig) + ) + Realm.open(localConfig).use { localRealm -> + localRealm.writeBlocking { + copyToRealm( + SyncObjectWithAllTypes().apply { + stringField = "local object" + } + ) + } + assertFailsWith { + localRealm.writeCopyTo(flexSyncConfig) + } } } - flexApp.close() } @Test @@ -945,42 +947,43 @@ class SyncedRealmTests { @Test fun writeCopyTo_flexibleSyncToLocal() = runBlocking { - val flexApp = TestApp( + TestApp( + "writeCopyTo_flexibleSyncToLocal", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val (email1, password1) = randomEmail() to "password1234" - val user = flexApp.createUserAndLogIn(email1, password1) - val localConfig = createWriteCopyLocalConfig("local.realm") - val syncConfig = createSyncConfig( - user = user, - name = "sync.realm", - partitionValue = partitionValue, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) - ) - Realm.open(syncConfig).use { flexSyncRealm: Realm -> - flexSyncRealm.writeBlocking { - copyToRealm( - FlexParentObject().apply { - name = "local object" - } + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val syncConfig = createSyncConfig( + user = user, + name = "sync.realm", + partitionValue = partitionValue, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) + ) + Realm.open(syncConfig).use { flexSyncRealm: Realm -> + flexSyncRealm.writeBlocking { + copyToRealm( + FlexParentObject().apply { + name = "local object" + } + ) + } + // Copy to local Realm + flexSyncRealm.writeCopyTo(localConfig) + } + // Open Local Realm and check that data can read. + Realm.open(localConfig).use { localRealm: Realm -> + assertEquals(1, localRealm.query().count().find()) + assertEquals("local object", localRealm.query().first().find()!!.name) } - // Copy to local Realm - flexSyncRealm.writeCopyTo(localConfig) - } - // Open Local Realm and check that data can read. - Realm.open(localConfig).use { localRealm: Realm -> - assertEquals(1, localRealm.query().count().find()) - assertEquals("local object", localRealm.query().first().find()!!.name) } - flexApp.close() } @Test @@ -1073,87 +1076,88 @@ class SyncedRealmTests { @Test fun writeCopyTo_flexibleSyncToFlexibleSync() = runBlocking { - val flexApp = TestApp( + TestApp( + "writeCopyTo_flexibleSyncToFlexibleSync", logLevel = io.realm.kotlin.log.LogLevel.ALL, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val (email2, password2) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val user2 = flexApp.createUserAndLogIn(email2, password2) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe(name = "parentSubscription") - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user2, - name = "sync2.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val (email2, password2) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val user2 = flexApp.createUserAndLogIn(email2, password2) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class + ), + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe(name = "parentSubscription") + } ) - ) - - Realm.open(syncConfig1).use { flexRealm1: Realm -> - // It is not possible to use `writeCopyTo` if data is written to the Realm before - // the SubscriptionSet is `COMPLETE`. Work around the issue for now. - flexRealm1.subscriptions.waitForSynchronization(30.seconds) - flexRealm1.write { - copyToRealm( - FlexParentObject(section).apply { - name = "User1Object" - } + val syncConfig2 = createFlexibleSyncConfig( + user = user2, + name = "sync2.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) - } - flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) - // Copy to another flex RealmRealm - flexRealm1.writeCopyTo(syncConfig2) - assertTrue(fileExists(syncConfig2.path)) - - // Open the copied Realm and verify we can read and write data - Realm.open(syncConfig2).use { flexRealm2: Realm -> - // Subscriptions are copied - assertEquals(1, flexRealm2.subscriptions.size) - assertEquals("parentSubscription", flexRealm2.subscriptions.first().name) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm2.subscriptions.state) - - // As is data - assertEquals(1, flexRealm2.query().count().find()) - assertEquals("User1Object", flexRealm2.query().first().find()!!.name) - - flexRealm2.subscriptions.waitForSynchronization(30.seconds) - flexRealm2.write { + ) + + Realm.open(syncConfig1).use { flexRealm1: Realm -> + // It is not possible to use `writeCopyTo` if data is written to the Realm before + // the SubscriptionSet is `COMPLETE`. Work around the issue for now. + flexRealm1.subscriptions.waitForSynchronization(30.seconds) + flexRealm1.write { copyToRealm( FlexParentObject(section).apply { - name = "User2Object" + name = "User1Object" } ) } - flexRealm2.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(2, flexRealm2.query().count().find()) + flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) + // Copy to another flex RealmRealm + flexRealm1.writeCopyTo(syncConfig2) + assertTrue(fileExists(syncConfig2.path)) + + // Open the copied Realm and verify we can read and write data + Realm.open(syncConfig2).use { flexRealm2: Realm -> + // Subscriptions are copied + assertEquals(1, flexRealm2.subscriptions.size) + assertEquals("parentSubscription", flexRealm2.subscriptions.first().name) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm2.subscriptions.state) + + // As is data + assertEquals(1, flexRealm2.query().count().find()) + assertEquals("User1Object", flexRealm2.query().first().find()!!.name) + + flexRealm2.subscriptions.waitForSynchronization(30.seconds) + flexRealm2.write { + copyToRealm( + FlexParentObject(section).apply { + name = "User2Object" + } + ) + } + flexRealm2.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(2, flexRealm2.query().count().find()) + } } } - flexApp.close() } @Test @@ -1188,69 +1192,70 @@ class SyncedRealmTests { // works well enough. Also, even if it doesn't surface the bug, it will not the fail the test. @Test fun accessSessionAfterRemoteChange() = runBlocking { - val flexApp = TestApp( + TestApp( + "accessSessionAfterRemoteChange", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val (email2, password2) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val user2 = flexApp.createUserAndLogIn(email2, password2) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user2, - name = "sync2.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - val realm1 = Realm.open(syncConfig1) + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val (email2, password2) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val user2 = flexApp.createUserAndLogIn(email2, password2) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + val syncConfig2 = createFlexibleSyncConfig( + user = user2, + name = "sync2.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + val realm1 = Realm.open(syncConfig1) - Realm.open(syncConfig2).use { realm2 -> - realm2.write { - copyToRealm(FlexParentObject(section)) + Realm.open(syncConfig2).use { realm2 -> + realm2.write { + copyToRealm(FlexParentObject(section)) + } + realm2.syncSession.uploadAllLocalChanges() } - realm2.syncSession.uploadAllLocalChanges() - } - - // Reading the object means we received it from the other Realm - withTimeout(30.seconds) { - val obj: FlexParentObject = realm1.query("section = $0", section).asFlow() - .map { it.list } - .filter { it.isNotEmpty() } - .first().first() - assertEquals(section, obj.section) - // 1. Local write to work around https://github.com/realm/realm-kotlin/issues/1070 - realm1.write { } - - // 2. Trigger GC. This will GC the RealmReference JVM object, making the native reference - // eligible for closing. - PlatformUtils.triggerGC() - - // 3. On the next update of Realm, we run through the weak list of all previous - // RealmReferences and close all native pointers with their JVM object GC'ed. - // This should now include the object created in step 1. - realm1.write { } - } + // Reading the object means we received it from the other Realm + withTimeout(30.seconds) { + val obj: FlexParentObject = realm1.query("section = $0", section).asFlow() + .map { it.list } + .filter { it.isNotEmpty() } + .first().first() + assertEquals(section, obj.section) + + // 1. Local write to work around https://github.com/realm/realm-kotlin/issues/1070 + realm1.write { } + + // 2. Trigger GC. This will GC the RealmReference JVM object, making the native reference + // eligible for closing. + PlatformUtils.triggerGC() + + // 3. On the next update of Realm, we run through the weak list of all previous + // RealmReferences and close all native pointers with their JVM object GC'ed. + // This should now include the object created in step 1. + realm1.write { } + } - // 4. With the original native dbPointer now being closed, accessing the syncSession for - // the first time should still work. - try { - realm1.syncSession.pause() - assertEquals(SyncSession.State.PAUSED, realm1.syncSession.state) - } finally { - realm1.close() - flexApp.close() + // 4. With the original native dbPointer now being closed, accessing the syncSession for + // the first time should still work. + try { + realm1.syncSession.pause() + assertEquals(SyncSession.State.PAUSED, realm1.syncSession.state) + } finally { + realm1.close() + } } } @@ -1258,7 +1263,8 @@ class SyncedRealmTests { fun customLoggersReceiveSyncLogs() = runBlocking { val customLogger = CustomLogCollector("CUSTOM", LogLevel.ALL) val section = Random.nextInt() - val flexApp = TestApp( + TestApp( + "customLoggersReceiveSyncLogs", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) @@ -1266,30 +1272,30 @@ class SyncedRealmTests { it.appName("MyCustomApp") it.appVersion("1.0.0") } - ) - val (email, password) = randomEmail() to "password1234" - val user = flexApp.createUserAndLogIn(email, password) - val syncConfig = createFlexibleSyncConfig( - user = user, - name = "flex.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - Realm.open(syncConfig).use { flexSyncRealm: Realm -> - flexSyncRealm.writeBlocking { - copyToRealm( - FlexParentObject().apply { - name = "local object" - } - ) + ).use { flexApp -> + val (email, password) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email, password) + val syncConfig = createFlexibleSyncConfig( + user = user, + name = "flex.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + Realm.open(syncConfig).use { flexSyncRealm: Realm -> + flexSyncRealm.writeBlocking { + copyToRealm( + FlexParentObject().apply { + name = "local object" + } + ) + } + flexSyncRealm.syncSession.uploadAllLocalChanges() } - flexSyncRealm.syncSession.uploadAllLocalChanges() + assertTrue(customLogger.logs.isNotEmpty()) + assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") + assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") } - assertTrue(customLogger.logs.isNotEmpty()) - assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") - assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") - flexApp.close() } // This test verifies that the user facing Realm instance is actually advanced on an on-needed @@ -1416,76 +1422,78 @@ class SyncedRealmTests { // - test-sync/src/iosTest/resources/asset-fs.realm // - test-sync/src/macosTest/resources/asset-fs.realm fun createInitialRealmFx() = runBlocking { - val flexApp = TestApp( + TestApp( + "createInitialRealmFx", logLevel = LogLevel.ALL, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), - initialSubscriptions = { realm: Realm -> - realm.query() - .subscribe(name = "parentSubscription") - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user1, - name = "asset-fs.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class + ), + initialSubscriptions = { realm: Realm -> + realm.query() + .subscribe(name = "parentSubscription") + } ) - ) - - Realm.open(syncConfig1).use { flexRealm1: Realm -> - // It is not possible to use `writeCopyTo` if data is written to the Realm before - // the SubscriptionSet is `COMPLETE`. Work around the issue for now. - flexRealm1.subscriptions.waitForSynchronization(30.seconds) - flexRealm1.write { - copyToRealm( - FlexParentObject(section).apply { - name = "User1Object" - } + val syncConfig2 = createFlexibleSyncConfig( + user = user1, + name = "asset-fs.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) + ) + + Realm.open(syncConfig1).use { flexRealm1: Realm -> + // It is not possible to use `writeCopyTo` if data is written to the Realm before + // the SubscriptionSet is `COMPLETE`. Work around the issue for now. + flexRealm1.subscriptions.waitForSynchronization(30.seconds) + flexRealm1.write { + copyToRealm( + FlexParentObject(section).apply { + name = "User1Object" + } + ) + } + flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) + // Copy to another flex RealmRealm + flexRealm1.writeCopyTo(syncConfig2) + assertTrue(fileExists(syncConfig2.path)) + // Debug this test, breakpoint here and grab the bundled realm from the location + println("Flexible sync bundled realm is in ${syncConfig2.path}") } - flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) - // Copy to another flex RealmRealm - flexRealm1.writeCopyTo(syncConfig2) - assertTrue(fileExists(syncConfig2.path)) - // Debug this test, breakpoint here and grab the bundled realm from the location - println("Flexible sync bundled realm is in ${syncConfig2.path}") } } // Sanity check that we can in fact open a flexible sync realm file as initial file @Test fun initialRealm_flexibleSync() = runBlocking { - val flexApp = TestApp( + TestApp( + "initialRealm_flexibleSync", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - try { + ).use { flexApp -> val (email1, password1) = randomEmail() to "password1234" val user1 = flexApp.createUserAndLogIn(email1, password1) val syncConfig1 = createFlexibleSyncConfig( @@ -1510,8 +1518,6 @@ class SyncedRealmTests { assertEquals(1, flexRealm1.subscriptions.size) assertNotNull(flexRealm1.subscriptions.findByName("parentSubscription")) } - } finally { - flexApp.close() } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt index 4833848d4a..929fe66582 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt @@ -101,6 +101,7 @@ class UserProfileTests { @BeforeTest fun setUp() { app = TestApp( + this::class.simpleName, networkTransport = object : NetworkTransport { override val authorizationHeaderName: String? get() = "" diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt index 60437fd98f..eb622cf8c0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt @@ -62,7 +62,7 @@ class UserTests { @BeforeTest fun setUp() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt index c06a910d40..5d66dd280a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt @@ -34,7 +34,7 @@ class NonLatinTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = TestHelper.randomEmail() to "password1234" user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt index 10c59b8e2b..8c0d0d7f94 100644 --- a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt @@ -22,12 +22,11 @@ import io.realm.kotlin.entities.sync.ParentPk import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds class RealmTests { @@ -39,18 +38,18 @@ class RealmTests { // effort in detecting the cases we do know about. @Test fun cleanupAllRealmThreadsOnClose() = runBlocking { - val app = TestApp() + val app = TestApp("cleanupAllRealmThreadsOnClose") val user = app.login(Credentials.anonymous()) val configuration = SyncConfiguration.create(user, TestHelper.randomPartitionValue(), setOf(ParentPk::class, ChildPk::class)) Realm.open(configuration).close() app.close() - // Wait max 10 seconds for threads to settle - var activeThreads = 0 + // Wait max 30 seconds for threads to settle + var activeThreads: List = emptyList() var fullyClosed = false - var count = 10 + var count = 5 while (!fullyClosed && count > 0) { - delay(1.seconds) + PlatformUtils.triggerGC() // Ensure we only have daemon threads after closing Realms and Apps activeThreads = Thread.getAllStackTraces().keys .filter { !it.isDaemon } @@ -62,26 +61,25 @@ class RealmTests { // Test thread it.name.startsWith("Test worker") } - .size - if (activeThreads == 0) { + if (activeThreads.isEmpty()) { fullyClosed = true } else { count -= 1 } } - assertEquals(0, activeThreads, "Active threads where found: ${threadTrace()}") + assertEquals(0, activeThreads.size, "Active threads where found ($activeThreads.size): ${threadTrace(activeThreads)}") } - private fun threadTrace(): String { + private fun threadTrace(threads: List? = null): String { val sb = StringBuilder() sb.appendLine("--------------------------------") - val stack = Thread.getAllStackTraces() - stack.keys + val stack: List = threads ?: Thread.getAllStackTraces().keys.toList() + stack .sortedBy { it.name } .forEach { t: Thread -> sb.appendLine("${t.name} - Is Daemon ${t.isDaemon} - Is Alive ${t.isAlive}") } - sb.appendLine("All threads: ${stack.keys.size}") + sb.appendLine("All threads: ${stack.size}") sb.appendLine("Active threads: ${Thread.activeCount()}") return sb.toString() } From c895b1e100e35a612f19064b5526bd9d09fcbece Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 1 Sep 2023 14:02:11 +0200 Subject: [PATCH 13/27] Release 1.11.0 --- CHANGELOG.md | 4 ++-- buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595a53135a..701de24d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.11.0-SNAPSHOT (YYYY-MM-DD) +## 1.11.0 (2023-09-01) ### Breaking Changes * `BaseRealmObject.equals()` has changed from being identity-based only (===) to instead return `true` if two objects come from the same Realm version. This e.g means that reading the same object property twice will now be identical. Note, two Realm objects, even with identical values will not be considered equal if they belong to different versions. @@ -51,7 +51,7 @@ if the content is the same. Custom implementations of these methods will be resp * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.19.1, commit c258e2681bca5fb33bbd23c112493817b43bfa86. +* Updated to Realm Core 13.20.0, commit c258e2681bca5fb33bbd23c112493817b43bfa86. ## 1.10.2 (2023-07-21) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 01e29a20b2..d676d60636 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.0-SNAPSHOT" + const val version = "1.11.0" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From edb07cfd4b17741cbc9ce04ccaea31479b63e512 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Sat, 2 Sep 2023 09:06:56 +0200 Subject: [PATCH 14/27] Prepare next dev iteration --- buildSrc/src/main/kotlin/Config.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index d676d60636..3904ff661d 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.0" + const val version = "1.11.1-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From e3b12ad2be6e09719d4c248ef6a7cec48f145c37 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Sat, 2 Sep 2023 09:08:33 +0200 Subject: [PATCH 15/27] Prepare next dev iteration --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701de24d39..050dffb15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 1.12.0-SNAPSHOT (YYYY-MM-DD) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* None. + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* None. + + ## 1.11.0 (2023-09-01) ### Breaking Changes diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 3904ff661d..e091e4734f 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.1-SNAPSHOT" + const val version = "1.12.0-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From 3747d211026f540e0702c014395b1b73dc5d16a5 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 7 Sep 2023 09:29:05 +0200 Subject: [PATCH 16/27] Fix missing scheduler on Linux and Windows (JVM) (#1503) --- CHANGELOG.md | 26 +++++++++++++++++++ .../kotlin/internal/interop/RealmInterop.kt | 2 +- .../src/main/jni/realm_api_helpers.cpp | 5 ++++ .../src/main/jni/realm_api_helpers.h | 3 +++ .../kotlin/types/annotations/FullText.kt | 5 ++-- .../test/common/VersionTrackingTests.kt | 6 ++++- 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701de24d39..4a760031e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.11.1 (YYYY-MM-DD) + +### Enhancements +* None. + +### Fixed +* Opening a Realm would crash with `No built-in scheduler implementation for this platform` on Linux (JVM) and Windows. (Issue [#1502](https://github.com/realm/realm-kotlin/issues/1502), since 1.11.0) + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* None. + + ## 1.11.0 (2023-09-01) ### Breaking Changes 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 b5965d8106..c2b2a07d8f 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 @@ -182,7 +182,7 @@ actual object RealmInterop { } actual fun realm_create_scheduler(): RealmSchedulerPointer = - LongPointerWrapper(realmc.realm_scheduler_make_default()) + LongPointerWrapper(realmc.realm_create_generic_scheduler()) actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer = LongPointerWrapper(realmc.realm_create_scheduler(JVMScheduler(dispatcher))) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index e22bec122e..392e1a7797 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -1048,3 +1048,8 @@ realm_sync_thread_error(realm_userdata_t userdata, const char* error) { env->CallVoidMethod(static_cast(userdata), java_callback_method, to_jstring(env, msg)); jni_check_exception(env); } + +realm_scheduler_t* +realm_create_generic_scheduler() { + return new realm_scheduler_t { realm::util::Scheduler::make_dummy() }; +} diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index cc845116ed..210202a6c9 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -134,4 +134,7 @@ realm_sync_thread_destroyed(realm_userdata_t userdata); void realm_sync_thread_error(realm_userdata_t userdata, const char* error); +realm_scheduler_t* +realm_create_generic_scheduler(); + #endif //TEST_REALM_API_HELPERS_H diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt index 59f0a1bd39..8352bf3e1d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt @@ -16,10 +16,11 @@ package io.realm.kotlin.types.annotations * * The full-text index currently support this set of features: * - * - Only token or word search, e.g. `bio TEXT 'computer dancing'` will find all objects that + * - Token or word search, e.g. `bio TEXT 'computer dancing'` will find all objects that * contains the words `computer` and `dancing` in their `bio` property. * - Tokens are diacritics- and case-insensitive, e.g.`bio TEXT 'cafe dancing'` and * `bio TEXT 'café DANCING'` will return the same set of matches. + * - Token prefix search can be done using `*`, like `bio TEXT comp*`. * - Ignoring results with certain tokens are done using `-`, e.g. `bio TEXT 'computer -dancing'` * will find all objects that contain `computer` but not `dancing`. * - Tokens are defined by a simple tokenizer that uses the following rules: @@ -29,7 +30,7 @@ package io.realm.kotlin.types.annotations * * Note the following constraints before using full-text search: * - * - Token prefix or suffix search like `bio TEXT 'comp* *cing'` is not supported. + * - Token suffix search like `bio TEXT '*cing'` is not supported. * - Only ASCII and Latin-1 alphanumerical chars are included in the index (most western languages). * - Only boolean match is supported, i.e. "found" or "not found". It is not possible to sort * results by "relevance" . diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt index 37d59c81fb..919c21ba0d 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt @@ -81,7 +81,11 @@ class VersionTrackingTests { realm.activeVersions().run { assertEquals(1, all.size) assertEquals(1, allTracked.size) - assertNull(notifier) + // The notifier might or might not had time to run + notifier?.let { + assertEquals(2, it.current.version) + assertEquals(0, it.active.size) + } assertNull(writer) } } From 4a13372c76f73e580166eb81bad03cd0157a84f0 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 7 Sep 2023 11:37:01 +0200 Subject: [PATCH 17/27] Release v1.11.1 --- CHANGELOG.md | 2 +- buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a760031e7..c3503f7247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.11.1 (YYYY-MM-DD) +## 1.11.1 (2023-09-07) ### Enhancements * None. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 3904ff661d..35da021942 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.1-SNAPSHOT" + const val version = "1.11.1" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From 3918a939e2788aec36f786a252e89d0c46a67458 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 7 Sep 2023 15:56:58 +0200 Subject: [PATCH 18/27] Prepare next dev iteration --- buildSrc/src/main/kotlin/Config.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 35da021942..8f86ea89d5 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.1" + const val version = "1.11.2-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From 7098f0f1acaeadb522da5b0a1bff3b0ec9366cde Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 8 Sep 2023 10:46:28 +0200 Subject: [PATCH 19/27] Update BAAS test server version (#1506) --- dependencies.list | 6 +++--- .../kotlin/io/realm/kotlin/test/util/Utils.kt | 4 ++-- .../common/FlexibleSyncIntegrationTests.kt | 4 ++-- .../kotlin/test/mongodb/common/FunctionsTests.kt | 2 +- .../test/mongodb/common/SubscriptionSetTests.kt | 15 ++++++++++++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/dependencies.list b/dependencies.list index eb618c86ba..19abf07b21 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,11 +1,11 @@ # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions -MONGODB_REALM_SERVER=2023-05-15 +MONGODB_REALM_SERVER=2023-09-07 # `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version # note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits # for that date within the following repositories: # https://github.com/10gen/baas/ # https://github.com/10gen/baas-ui/ -REALM_BAAS_GIT_HASH=92fc646871b507c73e48cc05ebec54fc9a134ae7 -REALM_BAAS_UI_GIT_HASH=67ad3606ec1137640ea84dcfe7a1a1b29aa7508f +REALM_BAAS_GIT_HASH=fc070ea7e874f58eb5b4f7a9d344fa7cdd755817 +REALM_BAAS_UI_GIT_HASH=ca722f5bb7a7485404a68874991ba8e50787ea9a diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt index 3068aea342..5e72a273dd 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.withTimeout import kotlinx.datetime.Instant import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.minutes // Platform independent helper methods object Utils { @@ -92,7 +92,7 @@ fun Instant.toRealmInstant(): RealmInstant { } // Variant of `Channel.receiveOrFail()` that will will throw if a timeout is hit. -suspend fun Channel.receiveOrFail(timeout: Duration = 30.seconds): T { +suspend fun Channel.receiveOrFail(timeout: Duration = 1.minutes): T { return withTimeout(timeout) { receive() } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index 7ad9ff466a..f6597162ec 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -146,7 +146,7 @@ class FlexibleSyncIntegrationTests { .query("(name = 'red' OR name = 'blue')") add(query, "sub") } - assertTrue(realm.subscriptions.waitForSynchronization(60.seconds)) + assertTrue(realm.subscriptions.waitForSynchronization(120.seconds)) realm.write { copyToRealm(FlexParentObject(randomSection).apply { name = "red" }) copyToRealm(FlexParentObject(randomSection).apply { name = "blue" }) @@ -156,7 +156,7 @@ class FlexibleSyncIntegrationTests { val query = realm.query("section = $0 AND name = 'red'", randomSection) add(query, "sub", updateExisting = true) } - assertTrue(realm.subscriptions.waitForSynchronization(60.seconds)) + assertTrue(realm.subscriptions.waitForSynchronization(120.seconds)) assertEquals(1, realm.query().count().find()) } } 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 1fd31781cf..354bc859ac 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 @@ -1015,7 +1015,7 @@ class FunctionsTests { runBlocking { anonUser.logOut() } - assertFailsWithMessage("[Service][Unknown(4351)] expected Authorization header with JWT") { + assertFailsWithMessage("[Service][Unknown(4351)] unauthorized") { runBlocking { functions.call(FIRST_ARG_FUNCTION.name, 1, 2, 3) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt index 68976a40bf..b18352c597 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt @@ -192,7 +192,10 @@ class SubscriptionSetTests { assertFailsWith { subscriptions.waitForSynchronization() } - assertTrue(subscriptions.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + subscriptions.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + subscriptions.errorMessage + ) subscriptions.update { removeAll() } @@ -258,7 +261,10 @@ class SubscriptionSetTests { updatedSubs.waitForSynchronization() } assertEquals(SubscriptionSetState.ERROR, updatedSubs.state) - assertTrue(updatedSubs.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + updatedSubs.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + updatedSubs.errorMessage + ) } // Test case for https://github.com/realm/realm-core/issues/5504 @@ -272,7 +278,10 @@ class SubscriptionSetTests { } assertEquals(SubscriptionSetState.ERROR, updatedSubs.state) assertEquals("TRUEPREDICATE and TRUEPREDICATE LIMIT(1)", updatedSubs.first().queryDescription) - assertTrue(updatedSubs.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + updatedSubs.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + updatedSubs.errorMessage + ) } @Test From bf06ff8f92451d691947320ded5d525703b82425 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Tue, 12 Sep 2023 10:32:45 +0200 Subject: [PATCH 20/27] Test with Kotlin 1.9.20-Beta (#1512) --- buildSrc/src/main/kotlin/Config.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index e091e4734f..78048a52e2 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -127,7 +127,7 @@ object Versions { const val jvmTarget = "1.8" // When updating the Kotlin version, also remember to update /examples/min-android-sample/build.gradle.kts const val kotlin = "1.8.21" // https://github.com/JetBrains/kotlin and https://kotlinlang.org/docs/releases.html#release-details - const val latestKotlin = "1.9.0" // https://kotlinlang.org/docs/eap.html#build-details + const val latestKotlin = "1.9.20-Beta" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "1.5.0" // https://github.com/tschuchortdev/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint const val ktor = "2.1.2" // https://github.com/ktorio/ktor From 619826b8fbabee19bdbdfce3bc9d1ee43d77f1db Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 14 Sep 2023 16:59:20 +0200 Subject: [PATCH 21/27] Fix redirects only working for GET requests. (#1517) --- CHANGELOG.md | 2 +- .../io/realm/kotlin/mongodb/internal/HttpClientCache.kt | 7 ++++++- .../kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa46f86430..f5bc4628fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * None. ### Fixed -* None. +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index d918aa4d6a..208769c6f9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -2,6 +2,7 @@ package io.realm.kotlin.mongodb.internal import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.HttpRedirect import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger @@ -34,7 +35,11 @@ internal fun createClient(timeoutMs: Long, customLogger: Logger?): HttpClient { } } - followRedirects = true + // We should allow redirects for all types, not just GET and HEAD + // See https://github.com/ktorio/ktor/issues/1793 + install(HttpRedirect) { + checkHttpMethod = false + } } } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt index cc8a6ee969..da9e02c6e7 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.test.mongodb.util import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.HttpRedirect import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.Logger @@ -65,7 +66,11 @@ fun defaultClient(name: String, debug: Boolean, block: HttpClientConfig<*>.() -> } } - followRedirects = true + // We should allow redirects for all types, not just GET and HEAD + // See https://github.com/ktorio/ktor/issues/1793 + install(HttpRedirect) { + checkHttpMethod = false + } // TODO connectionPool? this.apply(block) From 4bafc61a564bbf0ab314c3a8e5bd9b2a444c3690 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 14 Sep 2023 16:59:58 +0200 Subject: [PATCH 22/27] Remove Realm Plugin setting the bytecode target to 1.8 (#1514) --- CHANGELOG.md | 2 +- benchmarks/androidApp/build.gradle.kts | 6 ++-- benchmarks/build.gradle.kts | 2 +- buildSrc/build.gradle.kts | 5 ++++ buildSrc/src/main/kotlin/Config.kt | 10 ++++--- .../src/main/kotlin/realm-lint.gradle.kts | 2 ++ .../kmm-sample/androidApp/build.gradle.kts | 4 +-- .../compose-desktop/build.gradle.kts | 2 +- .../realm-java-compatibility/app/build.gradle | 6 ++-- .../single-platform/build.gradle.kts | 6 ++-- packages/build.gradle.kts | 3 +- packages/cinterop/build.gradle.kts | 14 +++------ packages/gradle-plugin/build.gradle.kts | 4 +-- .../io/realm/kotlin/gradle/RealmPlugin.kt | 29 ------------------- packages/jni-swig-stub/build.gradle.kts | 9 ++---- packages/library-base/build.gradle.kts | 7 ++--- packages/library-sync/build.gradle.kts | 7 ++--- .../plugin-compiler-shaded/build.gradle.kts | 4 +-- packages/plugin-compiler/build.gradle.kts | 5 ++-- packages/test-base/build.gradle.kts | 8 ++--- packages/test-sync/build.gradle.kts | 5 ++-- 21 files changed, 49 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bc4628fb..130171b81f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * None. ### Enhancements -* None. +* Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. ([#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) diff --git a/benchmarks/androidApp/build.gradle.kts b/benchmarks/androidApp/build.gradle.kts index 68c7b0d5e6..9cb9b3ae58 100644 --- a/benchmarks/androidApp/build.gradle.kts +++ b/benchmarks/androidApp/build.gradle.kts @@ -8,12 +8,12 @@ android { compileSdk = Versions.Android.compileSdkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = Versions.kotlinJvmTarget } defaultConfig { diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 2bebc22f69..1722d6672d 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -32,7 +32,7 @@ allprojects { } tasks.withType { - kotlinOptions.jvmTarget = Versions.jvmTarget + kotlinOptions.jvmTarget = Versions.kotlinJvmTarget } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e5bd2ee582..ee2e79fee3 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -29,6 +29,11 @@ gradlePlugin { } } +java { + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion +} + repositories { google() gradlePluginPortal() diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 78048a52e2..3402f0e318 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import org.gradle.api.JavaVersion /** * Enum describing operating systems we can build on. @@ -124,20 +124,22 @@ object Versions { const val jmh = "1.34" // https://github.com/openjdk/jmh const val jmhPlugin = "0.6.6" // https://github.com/melix/jmh-gradle-plugin const val junit = "4.13.2" // https://mvnrepository.com/artifact/junit/junit - const val jvmTarget = "1.8" + const val kbson = "0.3.0" // https://github.com/mongodb/kbson // When updating the Kotlin version, also remember to update /examples/min-android-sample/build.gradle.kts const val kotlin = "1.8.21" // https://github.com/JetBrains/kotlin and https://kotlinlang.org/docs/releases.html#release-details + const val kotlinJvmTarget = "1.8" // Which JVM bytecode version is kotlin compiled to. const val latestKotlin = "1.9.20-Beta" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "1.5.0" // https://github.com/tschuchortdev/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint const val ktor = "2.1.2" // https://github.com/ktorio/ktor + const val multidex = "2.0.1" // https://developer.android.com/jetpack/androidx/releases/multidex const val nexusPublishPlugin = "1.1.0" // https://github.com/gradle-nexus/publish-plugin const val okio = "3.2.0" // https://square.github.io/okio/#releases const val relinker = "1.4.5" // https://github.com/KeepSafe/ReLinker const val serialization = "1.4.0" // https://kotlinlang.org/docs/releases.html#release-details const val shadowJar = "6.1.0" // https://mvnrepository.com/artifact/com.github.johnrengelman.shadow/com.github.johnrengelman.shadow.gradle.plugin?repo=gradle-plugins - const val multidex = "2.0.1" // https://developer.android.com/jetpack/androidx/releases/multidex - const val kbson = "0.3.0" // https://github.com/mongodb/kbson + val sourceCompatibilityVersion = JavaVersion.VERSION_1_8 // Language level of any Java source code. + val targetCompatibilityVersion = JavaVersion.VERSION_1_8 // Version of generated JVM bytecode from Java files. } // Could be actual Dependency objects diff --git a/buildSrc/src/main/kotlin/realm-lint.gradle.kts b/buildSrc/src/main/kotlin/realm-lint.gradle.kts index 7875142afd..73714d8f5e 100644 --- a/buildSrc/src/main/kotlin/realm-lint.gradle.kts +++ b/buildSrc/src/main/kotlin/realm-lint.gradle.kts @@ -57,6 +57,7 @@ allprojects { description = "Check Kotlin code style." classpath = ktlint + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED") mainClass.set("com.pinterest.ktlint.Main") args = listOf( "src/**/*.kt", @@ -74,6 +75,7 @@ allprojects { description = "Fix Kotlin code style deviations." classpath = ktlint + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED") mainClass.set("com.pinterest.ktlint.Main") args = listOf( "-F", diff --git a/examples/kmm-sample/androidApp/build.gradle.kts b/examples/kmm-sample/androidApp/build.gradle.kts index dd5415befb..4bffec36c9 100644 --- a/examples/kmm-sample/androidApp/build.gradle.kts +++ b/examples/kmm-sample/androidApp/build.gradle.kts @@ -64,7 +64,7 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } } diff --git a/examples/kmm-sample/compose-desktop/build.gradle.kts b/examples/kmm-sample/compose-desktop/build.gradle.kts index 5735815ed3..9cc8cfd1e5 100644 --- a/examples/kmm-sample/compose-desktop/build.gradle.kts +++ b/examples/kmm-sample/compose-desktop/build.gradle.kts @@ -18,7 +18,7 @@ dependencies { } tasks.withType { - kotlinOptions.jvmTarget = Versions.jvmTarget + kotlinOptions.jvmTarget = Versions.kotlinJvmTarget } application { diff --git a/examples/realm-java-compatibility/app/build.gradle b/examples/realm-java-compatibility/app/build.gradle index ba941ea60f..da72b4e451 100644 --- a/examples/realm-java-compatibility/app/build.gradle +++ b/examples/realm-java-compatibility/app/build.gradle @@ -43,11 +43,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility Versions.sourceCompatibilityVersion + targetCompatibility Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = Versions.kotlinJvmTarget } } diff --git a/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts b/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts index f2336d03cd..a94fea7d14 100644 --- a/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts +++ b/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts @@ -40,11 +40,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = Versions.kotlinJvmTarget } } diff --git a/packages/build.gradle.kts b/packages/build.gradle.kts index c593f82c55..296bf17b24 100644 --- a/packages/build.gradle.kts +++ b/packages/build.gradle.kts @@ -27,8 +27,9 @@ allprojects { version = Realm.version group = Realm.group + // Define JVM bytecode target for all Kotlin targets tasks.withType { - kotlinOptions.jvmTarget = "${Versions.jvmTarget}" + kotlinOptions.jvmTarget = "${Versions.kotlinJvmTarget}" } } diff --git a/packages/cinterop/build.gradle.kts b/packages/cinterop/build.gradle.kts index 4bf66b813f..8cbeb83f8e 100644 --- a/packages/cinterop/build.gradle.kts +++ b/packages/cinterop/build.gradle.kts @@ -113,11 +113,7 @@ val nativeLibraryIncludesIosSimulatorArm64Release = includeBinaries(releaseLibs.map { "$absoluteCorePath/build-simulator-arm64/lib/$it" }) kotlin { - jvm { - compilations.all { - kotlinOptions.jvmTarget = Versions.jvmTarget - } - } + jvm() android("android") { publishLibraryVariants("release") } @@ -344,12 +340,10 @@ android { path = project.file("src/jvm/CMakeLists.txt") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle + compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } } diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index da97942a8e..d7c0c87fbc 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -82,8 +82,8 @@ publishing { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Make version information available at runtime diff --git a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt index 00a2225112..500538896c 100644 --- a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt +++ b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt @@ -27,10 +27,6 @@ import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import org.gradle.build.event.BuildEventsListenerRegistry -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import javax.inject.Inject @Suppress("unused") @@ -79,24 +75,11 @@ open class RealmPlugin : Plugin { // Stand alone Android projects have not initialized kotlin plugin when applying this, so // postpone dependency injection till after evaluation. project.afterEvaluate { - val kotlin: Any? = project.extensions.findByName("kotlin") // TODO AUTO-SETUP To ease configuration we could/should inject dependencies to our // library, but await better insight into when/what to inject and supply appropriate // opt-out options through our own extension? // Dependencies should probably be added by source set and not by target, as // kotlin.sourceSets.getByName("commonMain").dependencies (or "main" for Android), but - when (kotlin) { - is KotlinSingleTargetExtension<*> -> { - updateKotlinOption(kotlin.target) - } - is KotlinMultiplatformExtension -> { - kotlin.targets.all { target -> updateKotlinOption(target) } - } - else -> { - // TODO AUTO-SETUP Should we report errors? Probably an oversighted case - // TODO("Cannot 'realm-kotlin' library dependency to ${if (kotlin != null) kotlin::class.qualifiedName else "null"}") - } - } // Create the analytics during configuration because it needs access to the project // in order to gather project relevant information in afterEvaluate. Currently @@ -112,16 +95,4 @@ open class RealmPlugin : Plugin { } } } - - private fun updateKotlinOption(target: KotlinTarget) { - target.compilations.all { compilation -> - // Setup correct compiler options - // FIXME AUTO-SETUP Are these to dangerous to apply under the hood? - when (val options = compilation.kotlinOptions) { - is KotlinJvmOptions -> { - options.jvmTarget = "1.8" - } - } - } - } } diff --git a/packages/jni-swig-stub/build.gradle.kts b/packages/jni-swig-stub/build.gradle.kts index 2cd951be67..7662f9c2e7 100644 --- a/packages/jni-swig-stub/build.gradle.kts +++ b/packages/jni-swig-stub/build.gradle.kts @@ -32,11 +32,6 @@ java { } } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.create("realmWrapperJvm") { doLast { // If task is actually triggered (not up to date) then we should clean up the old stuff @@ -70,8 +65,8 @@ realmPublish { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } publishing { diff --git a/packages/library-base/build.gradle.kts b/packages/library-base/build.gradle.kts index 6c9d007386..d5ca3b5ffc 100644 --- a/packages/library-base/build.gradle.kts +++ b/packages/library-base/build.gradle.kts @@ -161,12 +161,9 @@ android { consumerProguardFiles("proguard-rules-consumer-common.pro") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Skip BuildConfig generation as it overlaps with io.realm.kotlin.BuildConfig from realm-java buildFeatures { diff --git a/packages/library-sync/build.gradle.kts b/packages/library-sync/build.gradle.kts index 13a714ad9a..d2f3afe90e 100644 --- a/packages/library-sync/build.gradle.kts +++ b/packages/library-sync/build.gradle.kts @@ -157,12 +157,9 @@ android { consumerProguardFiles("proguard-rules-consumer-common.pro") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Skip BuildConfig generation as it overlaps with io.realm.kotlin.BuildConfig from realm-java buildFeatures { diff --git a/packages/plugin-compiler-shaded/build.gradle.kts b/packages/plugin-compiler-shaded/build.gradle.kts index 927f017dba..733801d17b 100644 --- a/packages/plugin-compiler-shaded/build.gradle.kts +++ b/packages/plugin-compiler-shaded/build.gradle.kts @@ -55,8 +55,8 @@ realmPublish { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } publishing { diff --git a/packages/plugin-compiler/build.gradle.kts b/packages/plugin-compiler/build.gradle.kts index 1dd9133e7e..d68062946b 100644 --- a/packages/plugin-compiler/build.gradle.kts +++ b/packages/plugin-compiler/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { tasks.withType { kotlinOptions { - jvmTarget = "${Versions.jvmTarget}" freeCompilerArgs = listOf("-Xjvm-default=all-compatibility") } } @@ -69,6 +68,6 @@ publishing { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } diff --git a/packages/test-base/build.gradle.kts b/packages/test-base/build.gradle.kts index c1caa81308..8976c4f7c8 100644 --- a/packages/test-base/build.gradle.kts +++ b/packages/test-base/build.gradle.kts @@ -103,10 +103,6 @@ kotlin { } } } - - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).all { - kotlinOptions.jvmTarget = Versions.jvmTarget - } } // Android configuration @@ -150,8 +146,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Remove overlapping resources after adding "org.jetbrains.kotlinx:kotlinx-coroutines-test" to diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index c4b9a718a4..72ce21f2c9 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -122,7 +122,6 @@ kotlin { } // JVM specific KotlinCompilation tasks tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).all { - kotlinOptions.jvmTarget = Versions.jvmTarget kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } } @@ -161,8 +160,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Remove overlapping resources after adding "org.jetbrains.kotlinx:kotlinx-coroutines-test" to From 1e6ffa162472cd8d7d9788878796e92e7f2cb9ac Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 14 Sep 2023 18:39:09 +0200 Subject: [PATCH 23/27] Fix SyncConfig tests that would break on Windows (#1518) --- .../kotlin/internal/platform/SystemUtils.kt | 7 ++++++ .../test/common/RealmConfigurationTests.kt | 25 ++++++++++--------- .../io/realm/kotlin/test/common/RealmTests.kt | 4 +-- .../mongodb/common/AppConfigurationTests.kt | 8 +++--- .../common/FlexibleSyncConfigurationTests.kt | 3 ++- .../test/mongodb/common/SyncConfigTests.kt | 5 ++-- .../test/mongodb/common/SyncedRealmTests.kt | 11 +++++--- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index bfec4f83b5..010b1fa9a5 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -59,6 +59,13 @@ public expect val DEVICE_MODEL: String */ public expect val PATH_SEPARATOR: String +/** + * Construct a path from individual components + */ +public fun pathOf(vararg pathParts: String): String { + return pathParts.joinToString(PATH_SEPARATOR) +} + /** * Returns the root directory of the platform's App data. */ diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt index 81d0a96efd..cc9c22f728 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt @@ -23,6 +23,7 @@ import io.realm.kotlin.entities.Sample import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.appFilesDirectory +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.util.CoroutineDispatcherFactory import io.realm.kotlin.log.LogLevel @@ -77,7 +78,7 @@ class RealmConfigurationTests { fun with() { val config = RealmConfiguration.create(schema = setOf(Sample::class)) assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), config.path ) assertEquals(Realm.DEFAULT_FILE_NAME, config.name) @@ -95,7 +96,7 @@ class RealmConfigurationTests { fun defaultPath() { val config = RealmConfiguration.create(schema = setOf(Sample::class)) assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), config.path ) @@ -103,7 +104,7 @@ class RealmConfigurationTests { RealmConfiguration.Builder(schema = setOf(Sample::class)) .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), configFromBuilderWithDefaultName.path ) @@ -112,7 +113,7 @@ class RealmConfigurationTests { .name("custom.realm") .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}custom.realm", + pathOf(appFilesDirectory(), "custom.realm"), configFromBuilderWithCustomName.path ) @@ -122,7 +123,7 @@ class RealmConfigurationTests { .name("foo.realm") .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}my_dir${PATH_SEPARATOR}foo.realm", + pathOf(appFilesDirectory(), "my_dir", "foo.realm"), configFromBuilderWithCurrentDir.path ) } @@ -133,16 +134,16 @@ class RealmConfigurationTests { val config = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) .build() - assertEquals("$tmpDir${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", config.path) + assertEquals(pathOf(tmpDir, Realm.DEFAULT_FILE_NAME), config.path) } @Test fun directory_withSpace() { - val realmDir = tmpDir + "${PATH_SEPARATOR}dir with space" + val realmDir = pathOf(tmpDir, "dir with space") val config = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) .build() - assertEquals("$realmDir${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", config.path) + assertEquals(pathOf(realmDir, Realm.DEFAULT_FILE_NAME), config.path) // Just verifying that we can open the realm Realm.open(config).use { } } @@ -158,7 +159,7 @@ class RealmConfigurationTests { @Test fun directory_createIntermediateDirs() { - val realmDir = tmpDir + listOf("my", "intermediate", "dir").joinToString(separator = PATH_SEPARATOR, prefix = PATH_SEPARATOR) + val realmDir = pathOf(tmpDir, "my", "intermediate", "dir") val configBuilder = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) @@ -168,7 +169,7 @@ class RealmConfigurationTests { @Test fun directory_isFileThrows() { - val tmpFile = "$tmpDir${PATH_SEPARATOR}file" + val tmpFile = pathOf(tmpDir, "file") platformFileSystem.write(tmpFile.toPath(), mustCreate = true) { write(ByteArray(0)) } @@ -186,7 +187,7 @@ class RealmConfigurationTests { fun directoryAndNameCombine() { val realmDir = tmpDir val realmName = "my.realm" - val expectedPath = "$realmDir${PATH_SEPARATOR}$realmName" + val expectedPath = pathOf(realmDir, realmName) val config = RealmConfiguration.Builder(setOf(Sample::class)) @@ -236,7 +237,7 @@ class RealmConfigurationTests { .directory(tmpDir) .name(name) .build() - assertEquals("$tmpDir${PATH_SEPARATOR}$name", config.path) + assertEquals(pathOf(tmpDir, name), config.path) // Just verifying that we can open the realm Realm.open(config).use { } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt index 9ddca51450..624353f975 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt @@ -25,9 +25,9 @@ import io.realm.kotlin.ext.isManaged import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.query import io.realm.kotlin.ext.version -import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.isWindows +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.query.find import io.realm.kotlin.test.common.utils.assertFailsWithMessage import io.realm.kotlin.test.platform.PlatformUtils @@ -524,7 +524,7 @@ class RealmTests { val anotherRealm = Realm.open(configA) // Deleting it without having closed it should fail. - assertFailsWithMessage("Cannot delete files of an open Realm: '$tempDirA${PATH_SEPARATOR}anotherRealm.realm' is still in use") { + assertFailsWithMessage("Cannot delete files of an open Realm: '${pathOf(tempDirA, "anotherRealm.realm")}' is still in use") { Realm.deleteRealm(configA) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt index ee6c4f41b4..3e73576d1b 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt @@ -17,8 +17,8 @@ package io.realm.kotlin.test.mongodb.common -import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.appFilesDirectory +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel import io.realm.kotlin.log.RealmLog @@ -142,7 +142,7 @@ class AppConfigurationTests { @Test fun syncRootDirectory() { val builder: AppConfiguration.Builder = AppConfiguration.Builder(APP_ID) - val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" + val expectedRoot = pathOf(appFilesDirectory(), "myCustomDir") val config = builder .syncRootDirectory(expectedRoot) .build() @@ -160,7 +160,7 @@ class AppConfigurationTests { // the configured `AppConfiguration.syncRootDir` @Test fun syncRootDirectory_appendDirectoryToPath() = runBlocking { - val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" + val expectedRoot = pathOf(appFilesDirectory(), "myCustomDir") TestApp("syncRootDirectory_appendDirectoryToPath", builder = { it.syncRootDirectory(expectedRoot) }).use { app -> @@ -171,7 +171,7 @@ class AppConfigurationTests { // the configured `AppConfiguration.syncRootDir` val partitionValue = TestHelper.randomPartitionValue() val suffix = - "${PATH_SEPARATOR}myCustomDir${PATH_SEPARATOR}mongodb-realm${PATH_SEPARATOR}${user.app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}s_$partitionValue.realm" + pathOf("", "myCustomDir", "mongodb-realm", user.app.configuration.appId, user.id, "s_$partitionValue.realm") val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf()).build() assertTrue(config.path.endsWith(suffix), "Failed: ${config.path} vs. $suffix") } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt index 1cd8d25aff..841def7936 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.test.mongodb.common import io.realm.kotlin.Realm import io.realm.kotlin.internal.platform.PATH_SEPARATOR +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.User @@ -225,6 +226,6 @@ class FlexibleSyncConfigurationTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, setOf()) .name("custom.realm") .build() - assertTrue(config.path.endsWith("${app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}custom.realm"), "Path is: ${config.path}") + assertTrue(config.path.endsWith(pathOf(app.configuration.appId, user.id, "custom.realm")), "Path is: ${config.path}") } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index ffbb352aab..217516f2dd 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -30,6 +30,7 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.createDefaultSystemLogger +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import io.realm.kotlin.log.LogLevel @@ -406,7 +407,7 @@ class SyncConfigTests { private fun verifyName(builder: SyncConfiguration.Builder, expectedFileName: String) { val config = builder.build() - val suffix = "/mongodb-realm/${config.user.app.configuration.appId}/${config.user.identity}/$expectedFileName" + val suffix = pathOf("", "mongodb-realm", config.user.app.configuration.appId, config.user.id, expectedFileName) assertTrue(config.path.contains(suffix), "${config.path} failed.") assertEquals(expectedFileName, config.name) } @@ -1260,7 +1261,7 @@ class SyncConfigTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, partitionValue, setOf()) .name(fileName) .build() - val suffix = "/mongodb-realm/${user.app.configuration.appId}/${user.identity}/$fileName" + val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, fileName) assertTrue(config.path.endsWith(suffix), "${config.path} failed.") assertEquals(fileName, config.name, "${config.name} failed.") } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 038d06c91d..d8c59a2b38 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -29,6 +29,7 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.fileExists +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel import io.realm.kotlin.mongodb.App @@ -548,18 +549,19 @@ class SyncedRealmTests { } } - // Currently no good way to delete synced Realms that has been opened. - // See https://github.com/realm/realm-core/issues/5542 + // Currently there isn't a good good way to delete synced Realms that has been opened, but + // `Sync.waitForSessionsToTerminate` can be used in some cases. + // + // See https://github.com/realm/realm-core/issues/5542 for more details @Test @Suppress("LongMethod") - @Ignore fun deleteRealm() { val fileSystem = FileSystem.SYSTEM val user = app.asTestApp.createUserAndLogin() val configuration: SyncConfiguration = SyncConfiguration.create(user, partitionValue, setOf()) val syncDir: Path = - "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/${user.identity}".toPath() + pathOf(app.configuration.syncRootDirectory, "mongodb-realm", app.configuration.appId, user.id).toPath() val bgThreadReadyChannel = Channel(1) val readyToCloseChannel = Channel(1) @@ -590,6 +592,7 @@ class SyncedRealmTests { closedChannel.receiveOrFail() // Delete realm now that it's fully closed. + app.sync.waitForSessionsToTerminate() Realm.deleteRealm(configuration) // Lock file should never be deleted. From 6f349ef03315ad71443a3fee5e7d40b4bdb26809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Wed, 20 Sep 2023 12:47:49 +0200 Subject: [PATCH 24/27] Fix error in RealmAny.equals when comparing similarly typed object (#1523) --- CHANGELOG.md | 1 + .../io/realm/kotlin/internal/RealmAnyImpl.kt | 21 +----- .../kotlin/entities/SerializableSample.kt | 7 +- .../realm/kotlin/test/common/RealmAnyTests.kt | 69 +++++++++++++++++++ .../kotlin/test/common/SerializationTests.kt | 11 +++ 5 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 130171b81f..07a9db0a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. ([#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed +* Fix error in `RealmAny.equals` that would sometimes return `true` when comparing RealmAnys wrapping same type but different values. (Issue [#1523](https://github.com/realm/realm-kotlin/pull/1523)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) ### Compatibility diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt index 0a2791fab8..356eb6397e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt @@ -127,27 +127,12 @@ internal class RealmAnyImpl constructor( if (other.type != this.type) return false if (clazz == ByteArray::class) { if (other.internalValue !is ByteArray) return false - if (!other.internalValue.contentEquals(this.internalValue as ByteArray)) return false - } else if (internalValue is BsonObjectId) { - if (other.clazz != BsonObjectId::class) return false - if (other.internalValue != this.internalValue) return false + return other.internalValue.contentEquals(this.internalValue as ByteArray) } else if (internalValue is RealmObject) { if (other.clazz != this.clazz) return false - if (other.internalValue !== this.internalValue) return false - } else if (internalValue is Number) { // Numerics are the same as long as their value is the same - when (other.internalValue) { - is Char -> if (other.internalValue.code.toLong() != internalValue.toLong()) return false - is Number -> if (other.internalValue.toLong() != this.internalValue.toLong()) return false - else -> return false - } - } else if (internalValue is Char) { // We are comparing chars - when (other.internalValue) { - is Char -> if (other.internalValue.code.toLong() != internalValue.toLong()) return false - is Number -> if (other.internalValue.toLong() != this.internalValue.toLong()) return false - else -> return false - } + return other.internalValue == this.internalValue } - return true + return internalValue == other.internalValue } override fun hashCode(): Int { diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt index 9f1a4ab718..9e6b939bf7 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt @@ -50,6 +50,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 +import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 @Suppress("MagicNumber") @@ -65,7 +66,9 @@ class SerializableSample : RealmObject { var floatField: Float = 3.14f var doubleField: Double = 1.19840122 var decimal128Field: Decimal128 = Decimal128("1.8446744073709551618E-6157") - var timestampField: RealmInstant = RealmInstant.from(100, 1000) + // We will loose nano second precision when we round trip these, so framework only works for + // timestamps with 0-nanosecond fraction. + var timestampField: RealmInstant = RealmInstant.from(100, 1000000) var bsonObjectIdField: BsonObjectId = BsonObjectId("507f1f77bcf86cd799439011") var uuidField: RealmUUID = RealmUUID.from("46423f1b-ce3e-4a7e-812f-004cf9c42d76") var binaryField: ByteArray = byteArrayOf(42) @@ -212,7 +215,7 @@ class SerializableSample : RealmObject { ) @Suppress("UNCHECKED_CAST") - val listNullableProperties = mapOf( + val listNullableProperties: Map, KMutableProperty1>> = mapOf( String::class to SerializableSample::nullableStringListField as KMutableProperty1>, Byte::class to SerializableSample::nullableByteListField as KMutableProperty1>, Char::class to SerializableSample::nullableCharListField as KMutableProperty1>, 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 843fbd29c2..f99e36d2d0 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 @@ -58,6 +58,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.fail @@ -421,6 +422,74 @@ class RealmAnyTests { } } + @Test + fun equals() { + RealmAny.Type.values().forEach { type -> + when (type) { + RealmAny.Type.INT -> { + assertEquals(RealmAny.create(1), RealmAny.create(Char(1))) + assertEquals(RealmAny.create(1), RealmAny.create(1.toByte())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toShort())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toInt())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toLong())) + assertNotEquals(RealmAny.create(1), RealmAny.create(2)) + } + RealmAny.Type.BOOL -> { + assertEquals(RealmAny.create(true), RealmAny.create(true)) + assertNotEquals(RealmAny.create(true), RealmAny.create(false)) + } + RealmAny.Type.STRING -> { + assertEquals(RealmAny.create("Realm"), RealmAny.create("Realm")) + assertNotEquals(RealmAny.create("Realm"), RealmAny.create("Not Realm")) + } + RealmAny.Type.BINARY -> { + assertEquals( + RealmAny.create(byteArrayOf(1, 2)), RealmAny.create(byteArrayOf(1, 2)) + ) + assertNotEquals( + RealmAny.create(byteArrayOf(1, 2)), RealmAny.create(byteArrayOf(2, 1)) + ) + } + RealmAny.Type.TIMESTAMP -> { + val now = RealmInstant.now() + assertEquals(RealmAny.create(now), RealmAny.create(now)) + assertNotEquals(RealmAny.create(RealmInstant.from(1, 1)), RealmAny.create(now)) + } + RealmAny.Type.FLOAT -> { + assertEquals(RealmAny.create(1.5f), RealmAny.create(1.5f)) + assertNotEquals(RealmAny.create(1.2f), RealmAny.create(1.3f)) + } + RealmAny.Type.DOUBLE -> { + assertEquals(RealmAny.create(1.5), RealmAny.create(1.5)) + assertNotEquals(RealmAny.create(1.2), RealmAny.create(1.3)) + } + RealmAny.Type.DECIMAL128 -> { + assertEquals(RealmAny.create(Decimal128("1E64")), RealmAny.create(Decimal128("1E64"))) + assertNotEquals(RealmAny.create(Decimal128("1E64")), RealmAny.create(Decimal128("-1E64"))) + } + RealmAny.Type.OBJECT_ID -> { + val value = ObjectId() + assertEquals(RealmAny.create(value), RealmAny.create(value)) + assertNotEquals(RealmAny.create(ObjectId()), RealmAny.create(value)) + } + RealmAny.Type.UUID -> { + val value = RealmUUID.random() + assertEquals(RealmAny.create(value), RealmAny.create(value)) + assertNotEquals(RealmAny.create(RealmUUID.random()), RealmAny.create(value)) + } + RealmAny.Type.OBJECT -> { + val realmObject = Sample() + // Same object is equal + assertEquals(RealmAny.create(realmObject), RealmAny.create(realmObject)) + // Different kind of objects are not equal + assertNotEquals(RealmAny.create(RealmAnyContainer()), RealmAny.create(realmObject)) + // Different objects of same type are not equal + assertNotEquals(RealmAny.create(Sample()), RealmAny.create(realmObject)) + } + } + } + } + @Test fun embeddedObject_worksInsideParent() { val embeddedChild = EmbeddedChild("CHILD") 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 088cf1919e..223f0a9af0 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 @@ -139,6 +139,11 @@ class SerializationTests { RealmInstant::class -> dataSet.map { (it as RealmInstant?)?.restrictToMillisPrecision() as T } + RealmAny::class -> dataSet.map { + if ((it as? RealmAny)?.type == RealmAny.Type.TIMESTAMP) { + RealmAny.create((it.asRealmInstant()!!.restrictToMillisPrecision()))as T + } else { it } + } else -> dataSet } @@ -177,6 +182,12 @@ class SerializationTests { RealmInstant::class -> dataSet.map { entry -> entry.first to (entry.second as RealmInstant?)?.restrictToMillisPrecision() as T } + RealmAny::class -> dataSet.map { entry -> + val (key, value) = entry + if ((value as? RealmAny)?.type == RealmAny.Type.TIMESTAMP) { + key to RealmAny.create((value.asRealmInstant()!!.restrictToMillisPrecision()))as T + } else { entry } + } else -> dataSet } From 941d1fb7132b281addb43a257a95a080d59e0b64 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Fri, 22 Sep 2023 09:35:28 +0200 Subject: [PATCH 25/27] Cleanup resources better during Client Reset and closing Realms. (#1515) --- CHANGELOG.md | 6 + dependencies.list | 6 +- .../kotlin/internal/interop/RealmInterop.kt | 2 + .../src/main/jni/realm_api_helpers.cpp | 2 +- .../io/realm/kotlin/internal/RealmImpl.kt | 35 +++-- .../ClientResetRequiredException.kt | 5 +- .../mongodb/internal/SyncConfigurationImpl.kt | 33 +++- .../mongodb/internal/SyncSessionImpl.kt | 4 + .../mongodb/internal/SyncedRealmContext.kt | 17 ++- .../kotlin/test/platform/PlatformUtils.kt | 11 +- .../io/realm/kotlin/test/common/RealmTests.kt | 8 + packages/test-sync/build.gradle.kts | 24 +-- .../mongodb/common/AsymmetricSyncTests.kt | 13 +- .../common/FlexibleSyncIntegrationTests.kt | 24 ++- .../common/MutableSubscriptionSetTests.kt | 4 +- .../mongodb/common/ProgressListenerTests.kt | 6 +- .../kotlin/test/mongodb/common/Schema.kt | 49 ++++++ .../common/SubscriptionExtensionsTests.kt | 9 +- .../mongodb/common/SubscriptionSetTests.kt | 2 +- .../test/mongodb/common/SubscriptionTests.kt | 2 +- .../common/SyncClientResetIntegrationTests.kt | 143 ++++++++++-------- .../test/mongodb/common/SyncClientTests.kt | 8 +- .../test/mongodb/common/SyncConfigTests.kt | 28 ++-- .../test/mongodb/common/SyncSessionTests.kt | 13 +- .../test/mongodb/common/SyncedRealmTests.kt | 142 +++++------------ .../mongodb/common/nonlatin/NonLatinTests.kt | 3 +- 26 files changed, 328 insertions(+), 271 deletions(-) create mode 100644 packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a9db0a72..c990aa834e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,14 @@ * Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. ([#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed +* `Realm.close()` is now idempotent. * Fix error in `RealmAny.equals` that would sometimes return `true` when comparing RealmAnys wrapping same type but different values. (Issue [#1523](https://github.com/realm/realm-kotlin/pull/1523)) * [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) +* [Sync] Manual client reset on Windows would not trigger correctly when run inside `onManualResetFallback`. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) +* [Sync] `ClientResetRequiredException.executeClientReset()` now returns a boolean indicating if the manual reset fully succeded or not. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for +GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/dependencies.list b/dependencies.list index 19abf07b21..ba8ab7b56a 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,11 +1,11 @@ # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions -MONGODB_REALM_SERVER=2023-09-07 +MONGODB_REALM_SERVER=2023-09-22 # `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version # note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits # for that date within the following repositories: # https://github.com/10gen/baas/ # https://github.com/10gen/baas-ui/ -REALM_BAAS_GIT_HASH=fc070ea7e874f58eb5b4f7a9d344fa7cdd755817 -REALM_BAAS_UI_GIT_HASH=ca722f5bb7a7485404a68874991ba8e50787ea9a +REALM_BAAS_GIT_HASH=0b7562d0401d72c909369030dc29332542614ba3 +REALM_BAAS_UI_GIT_HASH=24baee4eb0e9736969a00a7bfac849565bca17f4 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 4418168802..46facf20d2 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 @@ -2544,6 +2544,8 @@ actual object RealmInterop { } catch (e: Throwable) { println(e.message) false + } finally { + realm_wrapper.realm_close(afterRealmPtr) } }, StableRef.create(afterHandler).asCPointer(), diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 392e1a7797..98e52f6890 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -906,7 +906,7 @@ after_client_reset(void* userdata, realm_t* before_realm, realm_t* after_realm_ptr = realm_from_thread_safe_reference(after_realm, &scheduler); auto after_pointer = wrap_pointer(env, reinterpret_cast(after_realm_ptr), false); env->CallVoidMethod(static_cast(userdata), java_after_callback_function, before_pointer, after_pointer, did_recover); - + realm_close(after_realm_ptr); if (env->ExceptionCheck()) { std::string exception_message = get_exception_message(env); std::string message_template = "An error has occurred in the 'onAfter' callback: "; diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 64d6ed3eaf..b96ea5cd0b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -46,7 +46,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.reflect.KClass @@ -56,8 +55,6 @@ public class RealmImpl private constructor( configuration: InternalConfiguration, ) : BaseRealmImpl(configuration), Realm, InternalTypedRealm, Flowable> { - private val realmPointerMutex = Mutex() - public val notificationScheduler: LiveRealmContext = configuration.notificationDispatcherFactory.createLiveRealmContext() @@ -85,6 +82,7 @@ public class RealmImpl private constructor( private var _realmReference: AtomicRef = atomic(null) private val realmReferenceLock = SynchronizableObject() + private val isClosed = atomic(false) /** * The current Realm reference that points to the underlying frozen C++ SharedRealm. @@ -103,7 +101,8 @@ public class RealmImpl private constructor( // Injection point for synchronized Realms. This property should only be used to hold state // required by synchronized realms. See `SyncedRealmContext` for more details. - public var syncContext: AtomicRef = atomic(null) + @OptIn(ExperimentalStdlibApi::class) + public var syncContext: AtomicRef = atomic(null) init { @Suppress("TooGenericExceptionCaught") @@ -261,27 +260,39 @@ public class RealmImpl private constructor( return VersionInfo(mainVersions, notifier.versions(), writer.versions()) } + override fun isClosed(): Boolean { + // We cannot rely on `realmReference()` here. If something happens during open, this might + // not be available and will throw, so we need to track closed state separately. + return isClosed.value + } + override fun close() { // TODO Reconsider this constraint. We have the primitives to check is we are on the // writer thread and just close the realm in writer.close() writer.checkInTransaction("Cannot close the Realm while inside a transaction block") - runBlocking { - realmPointerMutex.withLock { + realmReferenceLock.withLock { + if (isClosed()) { + return + } + isClosed.value = true + runBlocking { writer.close() realmScope.cancel() notifier.close() versionTracker.close() + @OptIn(ExperimentalStdlibApi::class) + syncContext.value?.close() // The local realmReference is pointing to a realm reference managed by either the // version tracker, writer or notifier, so it is already closed super.close() } - } - if (!realmStateFlow.tryEmit(State.CLOSED)) { - log.warn("Cannot signal internal close") - } + if (!realmStateFlow.tryEmit(State.CLOSED)) { + log.warn("Cannot signal internal close") + } - notificationScheduler.close() - writeScheduler.close() + notificationScheduler.close() + writeScheduler.close() + } } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt index 697100dc81..26b2daa2e4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt @@ -56,9 +56,10 @@ public class ClientResetRequiredException constructor( * associated to the session in which this error is generated **must be closed**. Not doing so * might result in unexpected file system errors. * + * @return `true` if the Client Reset succeeded, `false` if not. * @throws IllegalStateException if not all instances have been closed. */ - public fun executeClientReset() { - RealmInterop.realm_sync_immediately_run_file_actions(appPointer, originalFilePath) + public fun executeClientReset(): Boolean { + return RealmInterop.realm_sync_immediately_run_file_actions(appPointer, originalFilePath) } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt index 3664d5d968..23d689a20d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.SyncErrorCallback import io.realm.kotlin.internal.interop.sync.SyncError import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.platform.fileExists +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.mongodb.exceptions.ClientResetRequiredException import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException import io.realm.kotlin.mongodb.subscriptions @@ -52,8 +53,11 @@ import io.realm.kotlin.mongodb.sync.SyncSession import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.mongodb.kbson.BsonValue @@ -194,10 +198,33 @@ internal class SyncConfigurationImpl( SyncErrorCallback { pointer: RealmSyncSessionPointer, error: SyncError -> val session = SyncSessionImpl(pointer) val syncError = convertSyncError(error) - - // Notify before/after callbacks too if error is client reset if (error.isClientResetRequested) { - initializerHelper.onSyncError(session, frozenAppPointer, error) + // If a Client Reset happened, we only get here if `onManualResetFallback` needs + // to be called. This means there is a high likelihood that users will want to + // call ClientResetRequiredException.executeClientReset() inside the callback. + // + // In order to do that, they will need to close the Realm first. + // + // On POSIX this will work fine, but on Windows this will fail as the + // C++ session still holds a DBPointer preventing the release of the file during + // the callback. + // + // So, in order to prevent errors on Windows, we are running the Kotlin callback + // on a separate worker thread. This will allow Core to finish its callback so + // when we close the Realm from the worker thread, the underlying + // session can also be fully freed. + // + // Given that we do not make any promises regarding which thread the callback + // is running on. This should be fine. + @OptIn(DelicateCoroutinesApi::class) + try { + GlobalScope.launch { + initializerHelper.onSyncError(session, frozenAppPointer, error) + } + } catch (ex: Exception) { + @Suppress("invisible_member") + RealmLog.error("Error thrown and ignored in `onManualResetFallback`: $ex") + } } else { userErrorHandler.onError(session, syncError) } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 69acf48137..6ce5e305c6 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -247,6 +247,10 @@ internal open class SyncSessionImpl( } } + fun close() { + nativePointer.release() + } + internal companion object { internal fun stateFrom(coreState: CoreSyncSessionState): SyncSession.State { return when (coreState) { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt index 04f914e697..95c505b1f3 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt @@ -32,7 +32,8 @@ import io.realm.kotlin.mongodb.sync.SyncSession * In order to work around the bootstrap problem, all public API entry points that access this * class must do so through the [executeInSyncContext] closure. */ -internal class SyncedRealmContext(realm: T) { +@OptIn(ExperimentalStdlibApi::class) +internal class SyncedRealmContext(realm: T) : AutoCloseable { // TODO For now this can only be a RealmImpl, which is required by the SyncSessionImpl // When we introduce a public DynamicRealm, this can also be a `DynamicRealmImpl` // And we probably need to modify the SyncSessionImpl to take either of these two. @@ -40,18 +41,27 @@ internal class SyncedRealmContext(realm: T) { internal val config: SyncConfiguration = baseRealm.configuration as SyncConfiguration // Note: Session and Subscriptions only need a valid dbPointer when being created, after that, they // have their own lifecycle and can be cached. - internal val session: SyncSession by lazy { + private val sessionDelegate: Lazy = lazy { SyncSessionImpl( baseRealm, RealmInterop.realm_sync_session_get(baseRealm.realmReference.dbPointer) ) } - internal val subscriptions: SubscriptionSet by lazy { + internal val session: SyncSession by sessionDelegate + + private val subscriptionsDelegate: Lazy> = lazy { SubscriptionSetImpl( realm, RealmInterop.realm_sync_get_latest_subscriptionset(baseRealm.realmReference.dbPointer) ) } + internal val subscriptions: SubscriptionSet by subscriptionsDelegate + + override fun close() { + if (sessionDelegate.isInitialized()) { + (session as SyncSessionImpl).close() + } + } } /** @@ -77,6 +87,7 @@ internal fun executeInSyncContext(realm: R, block: (context: } } +@OptIn(ExperimentalStdlibApi::class) private fun initSyncContextIfNeeded(realm: T): SyncedRealmContext { // INVARIANT: `syncContext` is only ever set once, and never to `null`. // This code works around the fact that `Mutex`'s can only be locked inside suspend functions on diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 2b693a5fa9..cfc7d1f33f 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -21,7 +21,6 @@ import android.os.SystemClock import java.io.File import java.nio.file.Files import java.nio.file.Path -import java.nio.file.attribute.PosixFilePermission import kotlin.io.path.absolutePathString import kotlin.time.Duration @@ -30,14 +29,8 @@ actual object PlatformUtils { actual fun createTempDir(prefix: String, readOnly: Boolean): String { val dir: Path = Files.createTempDirectory("$prefix-android_tests") if (readOnly) { - Files.setPosixFilePermissions( - dir, - setOf( - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OWNER_READ - ) - ) + // Use the File API as it works across Windows and POSIX. + dir.toFile().setReadOnly() } return dir.absolutePathString() } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt index 624353f975..a398bc409e 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt @@ -429,6 +429,14 @@ class RealmTests { } } + @Test + fun close_idempotent() { + realm.close() + assertTrue(realm.isClosed()) + realm.close() + assertTrue(realm.isClosed()) + } + @Test @Suppress("LongMethod") fun deleteRealm() { diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index 72ce21f2c9..d04f38661f 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -286,27 +286,33 @@ kotlin { // - 'syncTestUrl` defines the root URL for the App Services server. Default is `http://localhost:9090` // - 'syncTestAppNamePrefix' is added a differentiator for all apps created by tests. This makes // it possible for builds in parallel to run against the same test server. Default is `test-app`. +fun getPropertyValue(propertyName: String): String? { + if (project.hasProperty(propertyName)) { + return project.property(propertyName) as String + } + return System.getenv(propertyName) +} buildkonfig { packageName = "io.realm.kotlin.test.mongodb" objectName = "SyncServerConfig" defaultConfigs { - buildConfigField(Type.STRING, "url", properties["syncTestUrl"]!! as String) - buildConfigField(Type.STRING, "appPrefix", properties["syncTestAppNamePrefix"]!! as String) - if (properties.containsKey("syncTestLoginEmail") && properties.containsKey("syncTestLoginPassword")) { - buildConfigField(Type.STRING, "email", properties["syncTestLoginEmail"]!! as String) - buildConfigField(Type.STRING, "password", properties["syncTestLoginPassword"]!! as String) + buildConfigField(Type.STRING, "url", getPropertyValue("syncTestUrl")) + buildConfigField(Type.STRING, "appPrefix", getPropertyValue("syncTestAppNamePrefix")) + if (project.hasProperty("syncTestLoginEmail") && project.hasProperty("syncTestLoginPassword")) { + buildConfigField(Type.STRING, "email", getPropertyValue("syncTestLoginEmail")) + buildConfigField(Type.STRING, "password", getPropertyValue("syncTestLoginPassword")) } else { buildConfigField(Type.STRING, "email", "") buildConfigField(Type.STRING, "password", "") } - if (properties.containsKey("syncTestLoginPublicApiKey") && properties.containsKey("syncTestLoginPrivateApiKey")) { - buildConfigField(Type.STRING, "publicApiKey", properties["syncTestLoginPublicApiKey"]!! as String) - buildConfigField(Type.STRING, "privateApiKey", properties["syncTestLoginPrivateApiKey"]!! as String) + if (project.hasProperty("syncTestLoginPublicApiKey") && project.hasProperty("syncTestLoginPrivateApiKey")) { + buildConfigField(Type.STRING, "publicApiKey", getPropertyValue("syncTestLoginPublicApiKey")) + buildConfigField(Type.STRING, "privateApiKey", getPropertyValue("syncTestLoginPrivateApiKey")) } else { buildConfigField(Type.STRING, "publicApiKey", "") buildConfigField(Type.STRING, "privateApiKey", "") } - buildConfigField(Type.STRING, "clusterName", properties["syncTestClusterName"] as String? ?: "") + buildConfigField(Type.STRING, "clusterName", getPropertyValue("syncTestClusterName") ?: "") } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt index 7d2f05821f..642278458c 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt @@ -104,12 +104,7 @@ class AsymmetricSyncTests { } config = SyncConfiguration.Builder( user, - schema = setOf( - Measurement::class, - Device::class, - BackupDevice::class, - DeviceParent::class - ) + schema = FLX_SYNC_SCHEMA ).initialSubscriptions { it.query().subscribe() }.build() @@ -299,11 +294,7 @@ class AsymmetricSyncTests { fun asymmetricSchema() = runBlocking { config = SyncConfiguration.Builder( app.login(Credentials.anonymous()), - schema = setOf( - AsymmetricA::class, - EmbeddedB::class, - StandardC::class, - ) + schema = FLX_SYNC_SCHEMA ).build() Realm.open(config).use { it.write { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index f6597162ec..f3093f2e6a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -22,7 +22,6 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.runBlocking -import io.realm.kotlin.log.LogLevel import io.realm.kotlin.mongodb.exceptions.CompensatingWriteException import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException import io.realm.kotlin.mongodb.exceptions.SyncException @@ -59,12 +58,11 @@ import kotlin.time.Duration.Companion.seconds */ class FlexibleSyncIntegrationTests { - private val defaultSchema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) private lateinit var app: TestApp @BeforeTest fun setup() { - app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" runBlocking { app.createUserAndLogIn(email, password) @@ -84,7 +82,7 @@ class FlexibleSyncIntegrationTests { // Upload data from user 1 val user1 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm1 -> val subs = realm1.subscriptions.update { add(realm1.query("section = $0", randomSection)) @@ -99,7 +97,7 @@ class FlexibleSyncIntegrationTests { // Download data from user 2 val user2 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> add( realm.query( @@ -120,7 +118,7 @@ class FlexibleSyncIntegrationTests { @Test fun writeFailsIfNoSubscription() = runBlocking { val user = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config = SyncConfiguration.Builder(user, defaultSchema) + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA) .build() Realm.open(config).use { realm -> @@ -138,7 +136,7 @@ class FlexibleSyncIntegrationTests { val randomSection = Random.nextInt() // Generate random section to allow replays of unit tests val user = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config = SyncConfiguration.Builder(user, defaultSchema).build() + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA).build() Realm.open(config).use { realm -> realm.subscriptions.update { val query = realm.query() @@ -163,7 +161,7 @@ class FlexibleSyncIntegrationTests { @Test fun initialSubscriptions_timeOut() { - val config = SyncConfiguration.Builder(app.currentUser!!, defaultSchema) + val config = SyncConfiguration.Builder(app.currentUser!!, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> repeat(10) { add(realm.query("section = $0", it)) @@ -186,7 +184,7 @@ class FlexibleSyncIntegrationTests { // Prepare some user data val user1 = app.createUserAndLogin() - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm -> realm.subscriptions.update { add(realm.query("section = $0", randomSection)) @@ -208,7 +206,7 @@ class FlexibleSyncIntegrationTests { // User 2 opens a Realm twice val counter = atomic(0) val user2 = app.createUserAndLogin() - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions(rerunOnOpen = true) { realm -> add( realm.query( @@ -236,7 +234,7 @@ class FlexibleSyncIntegrationTests { // Upload data from user 1 val user1 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm1 -> val subs = realm1.subscriptions.update { add(realm1.query("section = $0", randomSection)) @@ -274,7 +272,7 @@ class FlexibleSyncIntegrationTests { // Download data from user 2 val user2 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> add( realm.query( @@ -305,7 +303,7 @@ class FlexibleSyncIntegrationTests { val channel = Channel(1) - val config1 = SyncConfiguration.Builder(user1, defaultSchema) + val config1 = SyncConfiguration.Builder(user1, FLX_SYNC_SCHEMA) .errorHandler { _: SyncSession, syncException: SyncException -> runBlocking { channel.send(syncException as CompensatingWriteException) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt index 790e3d0141..56ef66dc9a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt @@ -64,7 +64,7 @@ class MutableSubscriptionSetTests { } config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -284,7 +284,7 @@ class MutableSubscriptionSetTests { // Not part of schema realm.subscriptions.update { assertFailsWith { - removeAll(io.realm.kotlin.entities.sync.ParentPk::class) + removeAll(io.realm.kotlin.entities.Sample::class) } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt index 3b0b3dcda8..a3310c5494 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt @@ -61,8 +61,6 @@ import kotlin.time.Duration.Companion.seconds private const val TEST_SIZE = 500 private val TIMEOUT = 30.seconds -private val schema = setOf(SyncObjectWithAllTypes::class) - class ProgressListenerTests { private lateinit var app: TestApp @@ -242,7 +240,7 @@ class ProgressListenerTests { fun throwsOnFlexibleSync() = runBlocking { TestApp("throwsOnFlexibleSync", TEST_APP_FLEX).use { val user = app.createUserAndLogIn() - val configuration: SyncConfiguration = SyncConfiguration.create(user, schema) + val configuration: SyncConfiguration = SyncConfiguration.create(user, FLX_SYNC_SCHEMA) Realm.open(configuration).use { realm -> assertFailsWithMessage( "Progress listeners are not supported for Flexible Sync" @@ -306,7 +304,7 @@ class ProgressListenerTests { user: User, partitionValue: String = getTestPartitionValue() ): SyncConfiguration { - return SyncConfiguration.Builder(user, partitionValue, schema) + return SyncConfiguration.Builder(user, partitionValue, io.realm.kotlin.test.mongodb.common.PARTITION_SYNC_SCHEMA) .build() } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt new file mode 100644 index 0000000000..13591ef636 --- /dev/null +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.test.mongodb.common + +import io.realm.kotlin.entities.sync.ChildPk +import io.realm.kotlin.entities.sync.ObjectIdPk +import io.realm.kotlin.entities.sync.ParentPk +import io.realm.kotlin.entities.sync.SyncObjectWithAllTypes +import io.realm.kotlin.entities.sync.SyncPerson +import io.realm.kotlin.entities.sync.flx.FlexChildObject +import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject +import io.realm.kotlin.entities.sync.flx.FlexParentObject + +private val ASYMMETRIC_CLASSES = setOf( + AsymmetricSyncTests.AsymmetricA::class, + AsymmetricSyncTests.EmbeddedB::class, + AsymmetricSyncTests.StandardC::class, + Measurement::class, +) + +private val DEFAULT_CLASSES = setOf( + BackupDevice::class, + ChildPk::class, + Device::class, + DeviceParent::class, + FlexChildObject::class, + FlexEmbeddedObject::class, + FlexParentObject::class, + ObjectIdPk::class, + ParentPk::class, + SyncObjectWithAllTypes::class, + SyncPerson::class +) + +val FLX_SYNC_SCHEMA = DEFAULT_CLASSES + ASYMMETRIC_CLASSES +val PARTITION_SYNC_SCHEMA = DEFAULT_CLASSES diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt index 8a7649627f..bce3f8697a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt @@ -17,8 +17,6 @@ package io.realm.kotlin.test.mongodb.common import io.realm.kotlin.Realm -import io.realm.kotlin.entities.sync.flx.FlexChildObject -import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.runBlocking @@ -45,7 +43,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlin.text.Typography.section import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds @@ -67,7 +64,7 @@ class SubscriptionExtensionsTests { } val config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -136,7 +133,7 @@ class SubscriptionExtensionsTests { val user1 = app.createUserAndLogIn(email, password) val config = SyncConfiguration.Builder( user1, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ).initialSubscriptions { realm: Realm -> realm.query("section = $0", section).subscribe() }.build() @@ -394,7 +391,7 @@ class SubscriptionExtensionsTests { private suspend fun uploadServerData(sectionId: Int, noOfObjects: Int) { val user = app.createUserAndLogin() - val config = SyncConfiguration.Builder(user, setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class)) + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA) .initialSubscriptions { it.query().subscribe() } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt index b18352c597..73487856bb 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt @@ -64,7 +64,7 @@ class SubscriptionSetTests { } val config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt index 8cc13229a9..1bc7105f75 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt @@ -62,7 +62,7 @@ class SubscriptionTests { } val config = SyncConfiguration.Builder( user, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 5e9b6bb58d..16b68768c6 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -21,8 +21,6 @@ import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm import io.realm.kotlin.TypedRealm import io.realm.kotlin.entities.sync.SyncPerson -import io.realm.kotlin.entities.sync.flx.FlexChildObject -import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.interop.ErrorCode @@ -161,11 +159,7 @@ class SyncClientResetIntegrationTests { configBuilderGenerator = { user -> return@TestEnvironment SyncConfiguration.Builder( user, - setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) + FLX_SYNC_SCHEMA ).initialSubscriptions { realm -> realm.query( "section = $0 AND name = $1", @@ -220,7 +214,7 @@ class SyncClientResetIntegrationTests { return@TestEnvironment SyncConfiguration.Builder( user, TestHelper.randomPartitionValue(), - schema = setOf(SyncPerson::class) + schema = PARTITION_SYNC_SCHEMA ) }, insertElement = { realm: Realm -> @@ -621,21 +615,25 @@ class SyncClientResetIntegrationTests { @Test fun discardUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - discardUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + discardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun discardUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - discardUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + discardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun discardUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { + var testRealm: Realm? = null // Channel size is 2 because both onError and onManualResetFallback are called val channel = Channel(2) val config = builder.syncClientResetStrategy(object : DiscardUnsyncedChangesStrategy { @@ -659,12 +657,14 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) @@ -674,14 +674,13 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) + // Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } @@ -959,19 +958,22 @@ class SyncClientResetIntegrationTests { @Test fun manuallyRecoverUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - manuallyRecoverUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + manuallyRecoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun manuallyRecoverUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - manuallyRecoverUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + manuallyRecoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun manuallyRecoverUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { val channel = Channel(1) @@ -987,21 +989,18 @@ class SyncClientResetIntegrationTests { } ).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) - + runBlocking { + Realm.open(config).use { realm -> + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) val exception = channel.receiveOrFail() - val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) + realm.close() assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - - exception.executeClientReset() + assertTrue(exception.executeClientReset()) assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) } @@ -1180,23 +1179,30 @@ class SyncClientResetIntegrationTests { @Test fun recoverUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - recoverUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + recoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun recoverUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - recoverUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + recoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } - private fun recoverUnsyncedChanges_executeClientReset(builder: SyncConfiguration.Builder) { + private fun recoverUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, + builder: SyncConfiguration.Builder + ) { + var testRealm: Realm? = null val channel = Channel(2) val config = builder.syncClientResetStrategy(object : RecoverUnsyncedChangesStrategy { override fun onBeforeReset(realm: TypedRealm) { - fail("Should not call onBeforeReset") + @Suppress("TooGenericExceptionThrown") + throw RuntimeException("Trigger onManualResetFallback") } override fun onAfterReset(before: TypedRealm, after: MutableRealm) { @@ -1207,19 +1213,21 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) @@ -1227,14 +1235,12 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } } @@ -1372,25 +1378,30 @@ class SyncClientResetIntegrationTests { @Test fun recoverOrDiscardUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - recoverOrDiscardUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + recoverOrDiscardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun recoverOrDiscardUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - recoverOrDiscardUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + recoverOrDiscardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun recoverOrDiscardUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { + var testRealm: Realm? = null val channel = Channel(2) val config = builder.syncClientResetStrategy(object : RecoverOrDiscardUnsyncedChangesStrategy { override fun onBeforeReset(realm: TypedRealm) { - fail("Should not call onBeforeReset") + @Suppress("TooGenericExceptionThrown") + throw RuntimeException("Trigger onManualResetFallback") } override fun onAfterRecovery(before: TypedRealm, after: MutableRealm) { @@ -1405,19 +1416,21 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) @@ -1425,14 +1438,12 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt index 1a79d7349b..5cdd58e678 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt @@ -56,7 +56,7 @@ class SyncClientTests { // There is no way to test reconnect automatically, so just verify that code path does not crash. @Test fun reconnect() { - val config = SyncConfiguration.create(user, schema = setOf()) + val config = SyncConfiguration.create(user, schema = FLX_SYNC_SCHEMA) Realm.open(config).use { app.sync.reconnect() } @@ -69,7 +69,7 @@ class SyncClientTests { @Test fun hasSyncSessions() { - val config = SyncConfiguration.create(user, schema = setOf()) + val config = SyncConfiguration.create(user, schema = FLX_SYNC_SCHEMA) Realm.open(config).use { assertTrue(app.sync.hasSyncSessions) } @@ -82,8 +82,8 @@ class SyncClientTests { @Test fun waitForSessionsToTerminate() { - val config1 = SyncConfiguration.Builder(user, schema = setOf()).build() - val config2 = SyncConfiguration.Builder(user, schema = setOf()).name("other.realm").build() + val config1 = SyncConfiguration.Builder(user, schema = FLX_SYNC_SCHEMA).build() + val config2 = SyncConfiguration.Builder(user, schema = FLX_SYNC_SCHEMA).name("other.realm").build() Realm.open(config1).use { assertTrue(app.sync.hasSyncSessions) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 217516f2dd..77a80463d5 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -105,7 +105,7 @@ class SyncConfigTests { val logger = createDefaultSystemLogger("TEST", LogLevel.DEBUG) val customLoggers = listOf(logger) val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -124,7 +124,7 @@ class SyncConfigTests { } val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -137,7 +137,7 @@ class SyncConfigTests { fun errorHandler_default() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -150,7 +150,7 @@ class SyncConfigTests { fun compactOnLaunch_default() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -164,7 +164,7 @@ class SyncConfigTests { val user = createTestUser() val callback = CompactOnLaunchCallback { _, _ -> false } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ) @@ -186,7 +186,7 @@ class SyncConfigTests { ) } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ) @@ -249,7 +249,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -277,7 +277,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -309,7 +309,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -321,7 +321,7 @@ class SyncConfigTests { fun equals_sameObject() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -374,7 +374,7 @@ class SyncConfigTests { fun equals_syncSpecificFields() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -532,7 +532,7 @@ class SyncConfigTests { fun encryption() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -759,7 +759,7 @@ class SyncConfigTests { fun getPartitionValue() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -1243,7 +1243,7 @@ class SyncConfigTests { } SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt index aac6fec160..827832ed2f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt @@ -258,13 +258,13 @@ class SyncSessionTests { val config1 = SyncConfiguration.Builder( user1, partitionValue, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ).name("user1.realm") .build() val config2 = SyncConfiguration.Builder( user2, partitionValue, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ).name("user2.realm") .build() @@ -320,7 +320,7 @@ class SyncSessionTests { val user = app.createUserAndLogIn(email, password) val channel = Channel(1) val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).errorHandler { session, _ -> @@ -382,7 +382,7 @@ class SyncSessionTests { @Test fun syncingObjectIdFromMongoDB() = runBlocking { val adminApi = app.asTestApp - val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf(ObjectIdPk::class)).build() + val config = SyncConfiguration.Builder(user, partitionValue, schema = PARTITION_SYNC_SCHEMA).build() Realm.open(config).use { realm -> val json: JsonObject = adminApi.insertDocument( ObjectIdPk::class.simpleName!!, @@ -427,7 +427,7 @@ class SyncSessionTests { val config = SyncConfiguration.Builder( user, partitionValue, - schema = setOf(ObjectIdPk::class) + schema = PARTITION_SYNC_SCHEMA ) .build() Realm.open(config).use { realm -> @@ -473,6 +473,7 @@ class SyncSessionTests { } } + @Ignore // TODO Find another way to test with with developer mode v2 @Test fun getConfiguration_inErrorHandlerThrows() = runBlocking { // Open and close a realm with a schema. @@ -480,7 +481,7 @@ class SyncSessionTests { val (email, password) = TestHelper.randomEmail() to "password1234" val user = app.createUserAndLogIn(email, password) val config1 = SyncConfiguration.Builder( - schema = setOf(ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).name("test1.realm").build() diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index d8c59a2b38..e486bd9e4d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -63,7 +63,6 @@ import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.test.util.use -import io.realm.kotlin.types.BaseRealmObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -79,7 +78,6 @@ import okio.Path.Companion.toPath import org.mongodb.kbson.ObjectId import kotlin.random.Random import kotlin.random.nextULong -import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -119,7 +117,7 @@ class SyncedRealmTests { app.createUserAndLogIn(email, password) } - syncConfiguration = createSyncConfig( + syncConfiguration = createPartitionSyncConfig( user = user, partitionValue = partitionValue, ) @@ -149,7 +147,7 @@ class SyncedRealmTests { val user = app.createUserAndLogIn(email, password) val partitionValue = Random.nextULong().toString() - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -158,7 +156,7 @@ class SyncedRealmTests { } ) Realm.open(config1).use { realm1 -> - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db2", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -216,11 +214,10 @@ class SyncedRealmTests { val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user1, name = "db1.realm", - partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) + partitionValue = partitionValue ) val realm1 = Realm.open(config1) val c = Channel>(1) @@ -233,11 +230,10 @@ class SyncedRealmTests { assertTrue(event is InitialRealm) // Write remote change - createSyncConfig( + createPartitionSyncConfig( user = user2, name = "db2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> realm.write { @@ -274,10 +270,10 @@ class SyncedRealmTests { val partitionValue = Random.nextLong().toString() // Setup two realms that synchronizes with the backend - val config1 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config1 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") val realm1 = Realm.open(config1) assertNotNull(realm1) - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db2") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db2") val realm2 = Realm.open(config2) assertNotNull(realm2) @@ -296,7 +292,7 @@ class SyncedRealmTests { // writer and notifier) are opened before the schema is synced from the server, but // empirically it has shown not to be the case and cause trouble if opening the second or // third realm with the wrong sync-intended schema mode. - val config3 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db3") + val config3 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db3") val realm3 = Realm.open(config3) assertNotNull(realm3) @@ -620,10 +616,9 @@ class SyncedRealmTests { val id = "id-${Random.nextLong()}" val masterObject = SyncObjectWithAllTypes.createWithSampleData(id) - createSyncConfig( + createPartitionSyncConfig( user = user1, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> realm.write { @@ -632,10 +627,9 @@ class SyncedRealmTests { realm.syncSession.uploadAllLocalChanges() } } - createSyncConfig( + createPartitionSyncConfig( user = user2, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> val list: RealmResults = @@ -654,16 +648,17 @@ class SyncedRealmTests { // return the full on-disk schema from ObjectStore, but for typed Realms the user visible schema // should still only return classes and properties that was defined by the user. @Test + @Ignore // TODO Need to adopt this to developer mode fun onlyLocalSchemaIsVisible() = runBlocking { val (email1, password1) = randomEmail() to "password1234" val (email2, password2) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - createSyncConfig( + createPartitionSyncConfig( user = user1, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class, ChildPk::class) + // schema = setOf(SyncObjectWithAllTypes::class, ChildPk::class) ).let { config -> Realm.open(config).use { realm -> realm.syncSession.uploadAllLocalChanges() @@ -676,10 +671,10 @@ class SyncedRealmTests { assertNotNull(childPkSchema["linkedFrom"]) } } - createSyncConfig( + createPartitionSyncConfig( user = user2, partitionValue = partitionValue, - schema = setOf(io.realm.kotlin.entities.sync.subset.ChildPk::class) + // schema = setOf(io.realm.kotlin.entities.sync.subset.ChildPk::class) ).let { config -> Realm.open(config).use { realm -> // Make sure that server schema changes are integrated @@ -701,27 +696,24 @@ class SyncedRealmTests { @Test fun mutableRealmInt_convergesAcrossClients() = runBlocking { // Updates and initial data upload are carried out using this config - val config0 = createSyncConfig( + val config0 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db1", - schema = setOf(SyncObjectWithAllTypes::class) ) // Config for update 1 - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db2", - schema = setOf(SyncObjectWithAllTypes::class) ) // Config for update 2 - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db3", - schema = setOf(SyncObjectWithAllTypes::class) ) val counterValue = Channel(1) @@ -816,17 +808,15 @@ class SyncedRealmTests { val user2 = app.createUserAndLogIn(email2, password2) val localConfig = createWriteCopyLocalConfig("local.realm") val partitionValue = TestHelper.randomPartitionValue() - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(localConfig).use { localRealm -> localRealm.writeBlocking { @@ -876,12 +866,7 @@ class SyncedRealmTests { val user1 = flexApp.createUserAndLogIn(email1, password1) val localConfig = createWriteCopyLocalConfig("local.realm") val flexSyncConfig = createFlexibleSyncConfig( - user = user1, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) + user = user1 ) Realm.open(localConfig).use { localRealm -> localRealm.writeBlocking { @@ -907,11 +892,10 @@ class SyncedRealmTests { val migratedLocalConfig = createWriteCopyLocalConfig("local.realm", directory = dir, schemaVersion = 1) val partitionValue = TestHelper.randomPartitionValue() - val syncConfig = createSyncConfig( + val syncConfig = createPartitionSyncConfig( user = user, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig).use { syncRealm -> // Write local data @@ -960,15 +944,10 @@ class SyncedRealmTests { val (email1, password1) = randomEmail() to "password1234" val user = flexApp.createUserAndLogIn(email1, password1) val localConfig = createWriteCopyLocalConfig("local.realm") - val syncConfig = createSyncConfig( + val syncConfig = createPartitionSyncConfig( user = user, name = "sync.realm", partitionValue = partitionValue, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) ) Realm.open(syncConfig).use { flexSyncRealm: Realm -> flexSyncRealm.writeBlocking { @@ -995,17 +974,15 @@ class SyncedRealmTests { val (email2, password2) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig1).use { syncRealm1 -> syncRealm1.writeBlocking { @@ -1039,17 +1016,15 @@ class SyncedRealmTests { val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) val partitionValue = TestHelper.randomPartitionValue() - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig1).use { syncRealm1 -> // Write local data @@ -1098,11 +1073,6 @@ class SyncedRealmTests { errorHandler = { _, error -> fail(error.toString()) }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), initialSubscriptions = { realm: Realm -> realm.query("section = $0", section).subscribe(name = "parentSubscription") } @@ -1112,12 +1082,7 @@ class SyncedRealmTests { name = "sync2.realm", errorHandler = { _, error -> fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) + } ) Realm.open(syncConfig1).use { flexRealm1: Realm -> @@ -1167,17 +1132,15 @@ class SyncedRealmTests { fun writeCopyTo_dataNotUploaded_throws() = runBlocking { val (email1, password1) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) - val syncConfigA = createSyncConfig( + val syncConfigA = createPartitionSyncConfig( user = user1, name = "a.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfigB = createSyncConfig( + val syncConfigB = createPartitionSyncConfig( user = user1, name = "b.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfigA).use { realm -> realm.syncSession.pause() @@ -1312,8 +1275,8 @@ class SyncedRealmTests { partitionValue = TestHelper.randomPartitionValue() - val config1 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db2") + val config1 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db2") Realm.open(config1).use { realm1 -> Realm.open(config2).use { realm2 -> @@ -1356,7 +1319,7 @@ class SyncedRealmTests { copyToRealm(ParentPk().apply { _id = ObjectId().toString() }) } .build() - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") Realm.open(config1).use { assertEquals(2, it.query().find().size) it.writeCopyTo(config2) @@ -1372,7 +1335,7 @@ class SyncedRealmTests { val user = runBlocking { app.createUserAndLogIn(email, password) } - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -1395,7 +1358,7 @@ class SyncedRealmTests { } } - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -1442,11 +1405,6 @@ class SyncedRealmTests { errorHandler = { _, error -> fail(error.toString()) }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), initialSubscriptions = { realm: Realm -> realm.query() .subscribe(name = "parentSubscription") @@ -1457,12 +1415,7 @@ class SyncedRealmTests { name = "asset-fs.realm", errorHandler = { _, error -> fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) + } ) Realm.open(syncConfig1).use { flexRealm1: Realm -> @@ -1504,12 +1457,7 @@ class SyncedRealmTests { name = "sync1.realm", errorHandler = { _, error -> fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), + } ) { initialRealmFile("asset-fs.realm") initialData { @@ -1531,7 +1479,7 @@ class SyncedRealmTests { app.createUserAndLogIn(email, password) } - val local = createSyncConfig(user = user, partitionValue = partitionValue, name = "local") { + val local = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "local") { initialRealmFile("asset-local.realm") } assertFalse(fileExists(local.path)) @@ -1898,17 +1846,16 @@ class SyncedRealmTests { // } @Suppress("LongParameterList") - private fun createSyncConfig( + private fun createPartitionSyncConfig( user: User, partitionValue: String, name: String = DEFAULT_NAME, encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, - schema: Set> = setOf(ParentPk::class, ChildPk::class), block: SyncConfiguration.Builder.() -> Unit = {} ): SyncConfiguration = SyncConfiguration.Builder( - schema = schema, + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).name(name).also { builder -> @@ -1925,16 +1872,11 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, - schema: Set> = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), initialSubscriptions: InitialSubscriptionsCallback? = null, block: SyncConfiguration.Builder.() -> Unit = {}, ): SyncConfiguration = SyncConfiguration.Builder( user = user, - schema = schema + schema = FLX_SYNC_SCHEMA ).name(name).also { builder -> if (encryptionKey != null) builder.encryptionKey(encryptionKey) if (errorHandler != null) builder.errorHandler(errorHandler) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt index 5d66dd280a..effa1d2db0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt @@ -8,6 +8,7 @@ import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.asTestApp +import io.realm.kotlin.test.mongodb.common.PARTITION_SYNC_SCHEMA import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.receiveOrFail @@ -54,7 +55,7 @@ class NonLatinTests { @Test fun readNullCharacterFromMongoDB() = runBlocking { val adminApi = app.asTestApp - val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf(ObjectIdPk::class)).build() + val config = SyncConfiguration.Builder(user, partitionValue, schema = PARTITION_SYNC_SCHEMA).build() Realm.open(config).use { realm -> val json: JsonObject = adminApi.insertDocument( ObjectIdPk::class.simpleName!!, From 52d44c0c9b5a89c34987b8ec814ccad4b5fc0ed8 Mon Sep 17 00:00:00 2001 From: LYK Date: Mon, 2 Oct 2023 18:00:01 +0900 Subject: [PATCH 26/27] Update README.md (#1531) Minor fix syntax highlighting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52d728cbc7..70a5f5dc47 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ Next: head to the full KMM [example](https://github.com/realm/realm-kotlin-sampl If you want to test recent bugfixes or features that have not been packaged in an official release yet, you can use a **-SNAPSHOT** release of the current development version of Realm via Gradle, available on [Maven Central](https://oss.sonatype.org/content/repositories/snapshots/io/realm/kotlin/) ## Groovy -``` +```Gradle // Global build.gradle buildscript { repositories { @@ -280,7 +280,7 @@ apply plugin: "io.realm.kotlin" ``` ## Kotlin -``` +```Kotlin // Global build.gradle buildscript { From 0cc98c0e87bfeec6199a0339e6fe661cbb3131b3 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 4 Oct 2023 11:44:17 +0200 Subject: [PATCH 27/27] Remove dependency on KAPT (#1534) --- CHANGELOG.md | 3 ++- packages/gradle-plugin/build.gradle.kts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c990aa834e..cc76998202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * None. ### Enhancements -* Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. ([#1513](https://github.com/realm/realm-kotlin/issues/1513)) +* Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. (Issue [#1513](https://github.com/realm/realm-kotlin/issues/1513)) +* The Realm Gradle Plugin no longer has a dependency on KAPT. (Issue [#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed * `Realm.close()` is now idempotent. diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index d7c0c87fbc..4c4480148f 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -17,7 +17,6 @@ import kotlin.text.toBoolean plugins { kotlin("jvm") - kotlin("kapt") `java-gradle-plugin` id("com.gradle.plugin-publish") version Versions.gradlePluginPublishPlugin id("realm-publisher")