From 58b168b573ad39ea1b6e8cb007ea17e6d3c068e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Wed, 10 Jul 2024 15:34:55 +0200 Subject: [PATCH] Experiments --- .../kotlin/internal/interop/RealmInterop.kt | 14 + .../interop/sync/ProtocolErrorCode.kt | 3 - .../kotlin/internal/interop/RealmInterop.kt | 60 ++++ .../interop/sync/ProtocolErrorCode.kt | 3 - packages/external/core | 2 +- packages/jni-swig-stub/realm.i | 2 +- .../src/main/jni/realm_api_helpers.cpp | 22 ++ .../src/main/jni/realm_api_helpers.h | 1 + .../kotlin/io/realm/kotlin/BaseRealm.kt | 1 + .../kotlin/io/realm/kotlin/Configuration.kt | 10 + .../io/realm/kotlin/RealmConfiguration.kt | 1 + .../io/realm/kotlin/annotations/DynamicAPI.kt | 22 ++ .../dynamic/getgeneric/BaseRealmObjectExt.kt | 50 +++ .../getinterface/BaseRealmObjectExt.kt | 56 ++++ .../dynamic/getrealmany/BaseRealmObjectExt.kt | 46 +++ .../kotlin/internal/ConfigurationImpl.kt | 5 + .../kotlin/internal/RealmConfigurationImpl.kt | 2 + .../io/realm/kotlin/internal/RealmImpl.kt | 2 +- .../kotlin/internal/RealmObjectHelper.kt | 317 +++++++++++++++++- .../kotlin/internal/RealmObjectInternal.kt | 7 +- .../realm/kotlin/internal/RealmObjectUtil.kt | 8 + .../dynamic/DynamicMutableRealmObjectImpl.kt | 1 + .../dynamic/DynamicRealmObjectImpl.kt | 5 + .../kotlin/internal/schema/RealmClassImpl.kt | 11 +- .../internal/schema/RealmPropertyImpl.kt | 10 +- .../kotlin/internal/schema/RealmSchemaImpl.kt | 2 +- .../io/realm/kotlin/schema/RealmClass.kt | 9 + .../io/realm/kotlin/schema/RealmProperty.kt | 8 + .../io/realm/kotlin/types/BaseRealmObject.kt | 50 ++- .../kotlin/mongodb/sync/SyncConfiguration.kt | 3 +- ...RealmModelSyntheticPropertiesGeneration.kt | 2 +- .../kotlin/test/common/RealmSchemaTests.kt | 3 +- .../kotlin/test/common/RelaxedSchemaTests.kt | 309 +++++++++++++++++ .../dynamic/GenericDynamicRealmTests.kt | 96 ++++++ .../dynamic/RealmAnyDynamicRealmTests.kt | 101 ++++++ .../dynamic/RelaxedDynamicRealmTests.kt | 94 ++++++ .../test/mongodb/common/SyncedRealmTests.kt | 1 + 37 files changed, 1303 insertions(+), 36 deletions(-) create mode 100644 packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/DynamicAPI.kt create mode 100644 packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getgeneric/BaseRealmObjectExt.kt create mode 100644 packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getinterface/BaseRealmObjectExt.kt create mode 100644 packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getrealmany/BaseRealmObjectExt.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RelaxedSchemaTests.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/GenericDynamicRealmTests.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RealmAnyDynamicRealmTests.kt create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RelaxedDynamicRealmTests.kt 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 2595d08e98..5541d0c663 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 @@ -200,6 +200,7 @@ expect object RealmInterop { 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_config_set_relaxed_schema(config: RealmConfigurationPointer, relaxedSchema: Boolean) fun realm_schema_validate(schema: RealmSchemaPointer, mode: SchemaValidationMode): Boolean fun realm_create_scheduler(): RealmSchedulerPointer @@ -299,15 +300,26 @@ expect object RealmInterop { fun realm_get_col_key(realm: RealmPointer, classKey: ClassKey, col: String): PropertyKey fun MemAllocator.realm_get_value(obj: RealmObjectPointer, key: PropertyKey): RealmValue + fun MemAllocator.realm_get_value_by_name(obj: RealmObjectPointer, name: String): RealmValue fun realm_set_value( obj: RealmObjectPointer, key: PropertyKey, value: RealmValue, isDefault: Boolean ) + fun realm_set_value_by_name( + obj: RealmObjectPointer, + name: String, + value: RealmValue, + ) + fun realm_has_property(obj: RealmObjectPointer, name: String): Boolean + fun realm_get_additional_properties(obj: RealmObjectPointer): List + fun realm_erase_property(obj: RealmObjectPointer, key: String): Boolean fun realm_set_embedded(obj: RealmObjectPointer, key: PropertyKey): RealmObjectPointer fun realm_set_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer + fun realm_set_list_by_name(obj: RealmObjectPointer, propertyName: String): RealmListPointer fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer + fun realm_set_dictionary_by_name(obj: RealmObjectPointer, propertyName: String): RealmMapPointer fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) fun realm_object_get_parent( obj: RealmObjectPointer, @@ -316,6 +328,7 @@ expect object RealmInterop { // list fun realm_get_list(obj: RealmObjectPointer, key: PropertyKey): RealmListPointer + fun realm_get_list_by_name(obj: RealmObjectPointer, propertyName: String): RealmListPointer fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer fun realm_list_size(list: RealmListPointer): Long fun MemAllocator.realm_list_get(list: RealmListPointer, index: Long): RealmValue @@ -354,6 +367,7 @@ expect object RealmInterop { // dictionary fun realm_get_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer + fun realm_get_dictionary_by_name(obj: RealmObjectPointer, propertyName: String): RealmMapPointer fun realm_dictionary_clear(dictionary: RealmMapPointer) fun realm_dictionary_size(dictionary: RealmMapPointer): Long fun realm_dictionary_to_results(dictionary: RealmMapPointer): RealmResultsPointer diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 1eb7aa437e..5a2483d04d 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -110,9 +110,6 @@ expect enum class WebsocketErrorCode : CodeDescription { RLM_ERR_WEBSOCKET_UNAUTHORIZED, RLM_ERR_WEBSOCKET_FORBIDDEN, RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY, - RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD, - RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW, - RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH, RLM_ERR_WEBSOCKET_RESOLVE_FAILED, RLM_ERR_WEBSOCKET_CONNECTION_FAILED, RLM_ERR_WEBSOCKET_READ_ERROR, 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 849e75f94d..aea1215355 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 @@ -216,6 +216,10 @@ actual object RealmInterop { realmc.realm_config_set_in_memory(config.cptr(), inMemory) } + actual fun realm_config_set_relaxed_schema(config: RealmConfigurationPointer, relaxedSchema: Boolean) { + realmc.realm_config_set_flexible_schema(config.cptr(), relaxedSchema) + } + actual fun realm_create_scheduler(): RealmSchedulerPointer = LongPointerWrapper(realmc.realm_create_generic_scheduler()) @@ -489,6 +493,15 @@ actual object RealmInterop { return RealmValue(struct) } + actual fun MemAllocator.realm_get_value_by_name( + obj: RealmObjectPointer, + name: String, + ): RealmValue { + val struct = allocRealmValueT() + realmc.realm_get_value_by_name((obj as LongPointerWrapper).ptr, name, struct) + return RealmValue(struct) + } + actual fun realm_set_value( obj: RealmObjectPointer, key: PropertyKey, @@ -498,6 +511,29 @@ actual object RealmInterop { realmc.realm_set_value(obj.cptr(), key.key, value.value, isDefault) } + actual fun realm_set_value_by_name( + obj: RealmObjectPointer, + name: String, + value: RealmValue, + ) { + realmc.realm_set_value_by_name(obj.cptr(), name, value.value) + } + + actual fun realm_has_property(obj: RealmObjectPointer, name: String): Boolean { + val found = BooleanArray(1) + realmc.realm_has_property(obj.cptr(), name, found) + return found[0] + } + + actual fun realm_get_additional_properties(obj: RealmObjectPointer): List { + @Suppress("UNCHECKED_CAST") + val properties = realmc.realm_get_additional_properties_helper(obj.cptr()) as Array + return properties.asList() + } + actual fun realm_erase_property(obj: RealmObjectPointer, key: String): Boolean { + return realmc.realm_erase_additional_property(obj.cptr(), key) + } + actual fun realm_set_embedded(obj: RealmObjectPointer, key: PropertyKey): RealmObjectPointer { return LongPointerWrapper(realmc.realm_set_embedded(obj.cptr(), key.key)) } @@ -506,10 +542,18 @@ actual object RealmInterop { realmc.realm_set_list(obj.cptr(), key.key) return realm_get_list(obj, key) } + actual fun realm_set_list_by_name(obj: RealmObjectPointer, propertyName: String): RealmListPointer { + realmc.realm_set_list_by_name(obj.cptr(), propertyName) + return realm_get_list_by_name(obj, propertyName) + } actual fun realm_set_dictionary(obj: RealmObjectPointer, key: PropertyKey): RealmMapPointer { realmc.realm_set_dictionary(obj.cptr(), key.key) return realm_get_dictionary(obj, key) } + actual fun realm_set_dictionary_by_name(obj: RealmObjectPointer, propertyName: String): RealmMapPointer { + realmc.realm_set_dictionary_by_name(obj.cptr(), propertyName) + return realm_get_dictionary_by_name(obj, propertyName) + } actual fun realm_object_add_int(obj: RealmObjectPointer, key: PropertyKey, value: Long) { realmc.realm_object_add_int(obj.cptr(), key.key, value) @@ -542,6 +586,14 @@ actual object RealmInterop { ) ) } + actual fun realm_get_list_by_name(obj: RealmObjectPointer, propertyName: String): RealmListPointer { + return LongPointerWrapper( + realmc.realm_get_list_by_name( + (obj as LongPointerWrapper).ptr, + propertyName + ) + ) + } actual fun realm_get_backlinks(obj: RealmObjectPointer, sourceClassKey: ClassKey, sourcePropertyKey: PropertyKey): RealmResultsPointer { return LongPointerWrapper( @@ -731,6 +783,14 @@ actual object RealmInterop { return LongPointerWrapper(ptr) } + actual fun realm_get_dictionary_by_name( + obj: RealmObjectPointer, + propertyName: String + ): RealmMapPointer { + val ptr = realmc.realm_get_dictionary_by_name(obj.cptr(), propertyName) + return LongPointerWrapper(ptr) + } + actual fun realm_dictionary_clear(dictionary: RealmMapPointer) { realmc.realm_dictionary_clear(dictionary.cptr()) } 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 2b4f74131e..452136341e 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 @@ -120,9 +120,6 @@ actual enum class WebsocketErrorCode( 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), diff --git a/packages/external/core b/packages/external/core index 1f0378ae53..1d4f514f54 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 1f0378ae53f73d67a309c9499aec512f4cde53f1 +Subproject commit 1d4f514f5448d30b3caaf3a5a619a962e6ed7ad2 diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 3e8d2af436..aaeff788b5 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -377,7 +377,7 @@ import static io.realm.kotlin.internal.interop.realm_errno_e.*; bool* erased, bool* out_erased, bool* did_refresh, bool* did_run, bool* found, bool* out_collection_was_cleared, bool* did_compact, bool* collection_was_cleared, bool* out_collection_was_deleted, - bool* out_was_deleted}; + bool* out_was_deleted, bool* out_has_property }; // uint64_t output parameter for realm_get_num_versions %apply int64_t* OUTPUT { uint64_t* out_versions_count }; 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 bd4ad3a654..e6537404f0 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 @@ -1411,3 +1411,25 @@ jobjectArray realm_get_log_category_names() { return array; } + +jobjectArray realm_get_additional_properties_helper(realm_object_t* obj) { + JNIEnv* env = get_env(true); + + size_t count = 0; + realm_get_additional_properties(obj, nullptr, 0xffffff, &count); + + const char** properties = new const char*[count]; + realm_get_additional_properties(obj, properties, count, &count); + // FIXME Guard count != count + + auto array = env->NewObjectArray(count, JavaClassGlobalDef::java_lang_string(), nullptr); + + for(size_t i = 0; i < count; i++) { + jstring string = env->NewStringUTF(properties[i]); + env->SetObjectArrayElement(array, i, string); + } + + delete[] properties; + + return array; +} 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 a6bf096d4e..bc70b2fedf 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 @@ -162,5 +162,6 @@ bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason); jobjectArray realm_get_log_category_names(); +jobjectArray realm_get_additional_properties_helper(realm_object_t* obj); #endif //TEST_REALM_API_HELPERS_H diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/BaseRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/BaseRealm.kt index 76d5d45fcf..366aee0572 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/BaseRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/BaseRealm.kt @@ -31,6 +31,7 @@ public interface BaseRealm : Versioned { * @return the schema of the realm. */ public fun schema(): RealmSchema +// public fun schema(fullSchema: Boolean = false): RealmSchema /** * Returns the schema version of the realm. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index cd261b23bf..94e3a20c49 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -170,6 +170,9 @@ public interface Configuration { */ public val initialRealmFileConfiguration: InitialRealmFileConfiguration? + // FIXME DOCS + public val relaxedSchema: Boolean + /** * Base class for configuration builders that holds properties available to both * [RealmConfiguration] and [SyncConfiguration]. @@ -209,6 +212,7 @@ public interface Configuration { protected var initialDataCallback: InitialDataCallback? = null protected var inMemory: Boolean = false protected var initialRealmFileConfiguration: InitialRealmFileConfiguration? = null + protected var relaxedSchema: Boolean = false /** * Sets the filename of the realm file. @@ -399,6 +403,12 @@ public interface Configuration { return this as S } + // FIXME Docs + public fun relaxedSchema(relaxedSchema: Boolean): S { + this.relaxedSchema = relaxedSchema + return this as S + } + protected fun validateEncryptionKey(encryptionKey: ByteArray): ByteArray { if (encryptionKey.size != Realm.ENCRYPTION_KEY_LENGTH) { throw IllegalArgumentException("The provided key must be ${Realm.ENCRYPTION_KEY_LENGTH} bytes. The provided key was ${encryptionKey.size} bytes.") 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 7ef592841c..a23300d755 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 @@ -181,6 +181,7 @@ public interface RealmConfiguration : Configuration { initialDataCallback, inMemory, initialRealmFileConfiguration, + relaxedSchema, realmLogger ) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/DynamicAPI.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/DynamicAPI.kt new file mode 100644 index 0000000000..eb978af689 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/DynamicAPI.kt @@ -0,0 +1,22 @@ +/* + * 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.annotations + +@MustBeDocumented +@Target( + AnnotationTarget.FUNCTION, +) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +public annotation class DynamicAPI diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getgeneric/BaseRealmObjectExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getgeneric/BaseRealmObjectExt.kt new file mode 100644 index 0000000000..9aaa6dcbb8 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getgeneric/BaseRealmObjectExt.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.dynamic.getgeneric + +import io.realm.kotlin.annotations.DynamicAPI +import io.realm.kotlin.internal.RealmObjectHelper +import io.realm.kotlin.internal.runIfManagedOrThrow +import io.realm.kotlin.types.BaseRealmObject +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +// Proposal 1 of generic/dynamic API +// This seems like the most promising +@DynamicAPI +public inline operator fun BaseRealmObject.get(propertyName: String): T { + return get(propertyName, typeOf()) +} + +@DynamicAPI +public operator fun BaseRealmObject.set(name: String, value: T) { + return this.runIfManagedOrThrow { + RealmObjectHelper.setValueByName(this, name, value) + } +} + +@PublishedApi +internal fun BaseRealmObject.get(propertyName: String, type: KType): T { + return this.runIfManagedOrThrow { + RealmObjectHelper.dynamicGetFromKType( + obj = this, + propertyName = propertyName, + type = type + ) + } +} + diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getinterface/BaseRealmObjectExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getinterface/BaseRealmObjectExt.kt new file mode 100644 index 0000000000..6753d6bbca --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getinterface/BaseRealmObjectExt.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024 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.dynamic.getinterface + +import io.realm.kotlin.internal.RealmObjectHelper +import io.realm.kotlin.internal.runIfManagedOrThrow +import io.realm.kotlin.types.BaseRealmObject +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +// Proposal 2 of generic/dynamic API +// Hiding things a bit by only exposing it on a specific type. This is a bit cumbersome as you +// cannot use a single entry point to get into the dynamic domain (requires to use .relaxed +// everywhere) +public interface RelaxedRealmObject + +// FIXME Naming +// - Extras? +public val BaseRealmObject.relaxed: RelaxedRealmObject + get() = this as RelaxedRealmObject + + +public inline operator fun RelaxedRealmObject.get(propertyName: String): T { + return get(propertyName, typeOf()) +} + +public operator fun RelaxedRealmObject.set(name: String, value: T) { + return (this as BaseRealmObject).runIfManagedOrThrow { + RealmObjectHelper.setValueByName(this, name, value) + } +} + +@PublishedApi +internal fun RelaxedRealmObject.get(propertyName: String, type: KType): T { + return (this as BaseRealmObject).runIfManagedOrThrow { + RealmObjectHelper.dynamicGetFromKType( + obj = this, + propertyName = propertyName, + type = type + ) + } +} diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getrealmany/BaseRealmObjectExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getrealmany/BaseRealmObjectExt.kt new file mode 100644 index 0000000000..51ca47bf79 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/dynamic/getrealmany/BaseRealmObjectExt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 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.dynamic.getrealmany + +import io.realm.kotlin.internal.RealmObjectHelper +import io.realm.kotlin.internal.runIfManagedOrThrow +import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmAny +import kotlin.reflect.typeOf + +// Proposal 3 of generic/dynamic API +// Wrapping anything in RealmAny/Mixed to avoid the need of generics over. This would maybe be +// preferable for the non-schema properties of the "relaxed schema"-concept, but is really annoying +// to work with if you know the types as you would if using this as a general dynamic API for +// fixed schema properties. If you really want to work with RealmAny, you could just the +// `get` variant from proposal 1. +public operator fun BaseRealmObject.get(propertyName: String): RealmAny { + return this.runIfManagedOrThrow { + RealmObjectHelper.dynamicGetFromKType( + obj = this, + propertyName = propertyName, + type = typeOf() + ) + } +} + +// FIXME Does this need to be typed +public operator fun BaseRealmObject.set(name: String, value: T) { + return this.runIfManagedOrThrow { + RealmObjectHelper.setValueByName(this, name, value) + } +} 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 6b2d3a234a..6e72749fd7 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 @@ -65,6 +65,7 @@ public open class ConfigurationImpl( override val isFlexibleSyncConfiguration: Boolean, inMemory: Boolean, initialRealmFileConfiguration: InitialRealmFileConfiguration?, + relaxedSchema: Boolean, override val logger: ContextLogger ) : InternalConfiguration { @@ -96,6 +97,7 @@ public open class ConfigurationImpl( final override val initialDataCallback: InitialDataCallback? final override val inMemory: Boolean final override val initialRealmFileConfiguration: InitialRealmFileConfiguration? + final override val relaxedSchema: Boolean override fun createNativeConfiguration(): RealmConfigurationPointer { val nativeConfig: RealmConfigurationPointer = RealmInterop.realm_config_new() @@ -141,6 +143,7 @@ public open class ConfigurationImpl( this.initialDataCallback = initialDataCallback this.inMemory = inMemory this.initialRealmFileConfiguration = initialRealmFileConfiguration + this.relaxedSchema = relaxedSchema // We need to freeze `compactOnLaunchCallback` reference on initial thread for Kotlin Native val compactCallback = compactOnLaunchCallback?.let { callback -> @@ -225,6 +228,8 @@ public open class ConfigurationImpl( RealmInterop.realm_config_set_in_memory(nativeConfig, inMemory) + RealmInterop.realm_config_set_relaxed_schema(nativeConfig, relaxedSchema) + nativeConfig } 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 4ca59cb4e7..5b252139a8 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 @@ -45,6 +45,7 @@ internal class RealmConfigurationImpl( initialDataCallback: InitialDataCallback?, inMemory: Boolean, initialRealmFileConfiguration: InitialRealmFileConfiguration?, + relaxedSchema: Boolean, logger: ContextLogger ) : ConfigurationImpl( directory, @@ -66,6 +67,7 @@ internal class RealmConfigurationImpl( false, inMemory, initialRealmFileConfiguration, + relaxedSchema, logger ), RealmConfiguration 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 463d604eea..1887d9313a 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 @@ -335,7 +335,7 @@ public class RealmImpl private constructor( // Returns a DynamicRealm of the current version of the Realm. Only used to be able to test the // DynamicRealm API outside of a migration. -internal fun Realm.asDynamicRealm(): DynamicRealm { +public fun Realm.asDynamicRealm(): DynamicRealm { val dbPointer = (this as RealmImpl).realmReference.dbPointer return DynamicRealmImpl(this@asDynamicRealm.configuration, dbPointer) } 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 f21e85de1d..dcf57e3b7b 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 @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.PropertyKey import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmInterop.realm_get_value +import io.realm.kotlin.internal.interop.RealmInterop.realm_get_value_by_name import io.realm.kotlin.internal.interop.RealmListPointer import io.realm.kotlin.internal.interop.RealmMapPointer import io.realm.kotlin.internal.interop.RealmObjectInterop @@ -50,6 +51,7 @@ import io.realm.kotlin.internal.schema.ClassMetadata import io.realm.kotlin.internal.schema.PropertyMetadata import io.realm.kotlin.internal.schema.RealmStorageTypeImpl import io.realm.kotlin.internal.schema.realmStorageType +import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.Validation.sdkError import io.realm.kotlin.query.RealmResults import io.realm.kotlin.schema.RealmClassKind @@ -70,6 +72,8 @@ import org.mongodb.kbson.Decimal128 import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * This object holds helper methods for the compiler plugin generated methods, providing the @@ -110,19 +114,49 @@ internal object RealmObjectHelper { inputScope { setValueTransportByKey(obj, key, realmObjectTransport(objRef)) } } + internal inline fun setObjectByName( + obj: RealmObjectReference, + propertyName: String, + value: BaseRealmObject?, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectCache = mutableMapOf() + ) { + obj.checkValid() + val objRef = + realmObjectToRealmReferenceWithImport(value, obj.mediator, obj.owner, updatePolicy, cache) + inputScope { setValueTransportByName(obj, propertyName, realmObjectTransport(objRef)) } + } + // Return type should be R? but causes compilation errors for native @Suppress("unused") internal inline fun getObject( obj: RealmObjectReference, propertyName: String, - ): Any? { + ): R? { obj.checkValid() val key: PropertyKey = obj.propertyInfoOrThrow(propertyName).key return getterScope { val transport = realm_get_value(obj.objectPointer, key) when { transport.isNull() -> null - else -> realm_get_value(obj.objectPointer, key) + else -> transport + .getLink() + .toRealmObject(R::class, obj.mediator, obj.owner) + } + } + } + + @Suppress("unused") + internal inline fun getObjectByName( + obj: RealmObjectReference, + propertyName: String, + ): R? { + obj.checkValid() + return getterScope { + val transport = realm_get_value_by_name(obj.objectPointer, propertyName) + when { + transport.isNull() -> null + else -> transport .getLink() .toRealmObject(R::class, obj.mediator, obj.owner) } @@ -186,6 +220,74 @@ internal object RealmObjectHelper { return setValueByKey(obj, key, value) } + @Suppress("ComplexMethod", "LongMethod") + internal inline fun setValueByName( + obj: RealmObjectReference, + name: String, + value: Any?, + updatePolicy: UpdatePolicy = UpdatePolicy.ALL, + cache: UnmanagedToManagedObjectCache = mutableMapOf() + ) { + // TODO optimize: avoid this by creating the scope in the accessor via the compiler plugin + // See comment in AccessorModifierIrGeneration.modifyAccessor about this. + inputScope { + when (value) { + null -> setValueTransportByName(obj, name, nullTransport()) + is String -> setValueTransportByName(obj, name, stringTransport(value)) + is ByteArray -> setValueTransportByName(obj, name, byteArrayTransport(value)) + is Int -> setValueTransportByName(obj, name, longTransport(value.toLong())) + is Long -> setValueTransportByName(obj, name, longTransport(value)) + is Boolean -> setValueTransportByName(obj, name, booleanTransport(value)) + is Timestamp -> setValueTransportByName(obj, name, timestampTransport(value)) + is Float -> setValueTransportByName(obj, name, floatTransport(value)) + is Double -> setValueTransportByName(obj, name, doubleTransport(value)) + is Decimal128 -> setValueTransportByName(obj, name, decimal128Transport(value)) + is BsonObjectId -> setValueTransportByName( + obj, + name, + objectIdTransport(value.toByteArray()) + ) + is RealmUUID -> setValueTransportByName(obj, name, uuidTransport(value.bytes)) + is RealmObjectInterop -> setValueTransportByName( + obj, + name, + realmObjectTransport(value) + ) + is MutableRealmInt -> setValueTransportByName(obj, name, longTransport(value.get())) + is RealmAny -> { + realmAnyHandler( + value = value, + primitiveValueAsRealmValueHandler = { realmValue -> + setValueTransportByName( + obj, + name, + realmValue + ) + }, + referenceAsRealmAnyHandler = { realmValue -> + setObjectByName(obj, name, realmValue.asRealmObject(), updatePolicy, cache) + }, + listAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_list_by_name(obj.objectPointer, name) + RealmInterop.realm_list_clear(nativePointer) + val operator = + realmAnyListOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.insertAll(0, value.asList(), updatePolicy, cache) + }, + dictionaryAsRealmAnyHandler = { realmValue -> + val nativePointer = RealmInterop.realm_set_dictionary_by_name(obj.objectPointer, name) + RealmInterop.realm_dictionary_clear(nativePointer) + val operator = + realmAnyMapOperator(obj.mediator, obj.owner, nativePointer, false, false) + operator.putAll(value.asDictionary(), updatePolicy, cache) + } + ) + } + else -> throw IllegalArgumentException("Unsupported value for transport: $value") + } + } + } + @Suppress("ComplexMethod", "LongMethod") internal inline fun setValueByKey( obj: RealmObjectReference, @@ -321,8 +423,134 @@ internal object RealmObjectHelper { { RealmInterop.realm_get_list(obj.objectPointer, key) } ) { RealmInterop.realm_get_dictionary(obj.objectPointer, key) } } + + } + + internal fun dynamicGetFromKType( + obj: RealmObjectReference, + propertyName: String, + type: KType, + issueDynamicMutableObject: Boolean = false + ): R { + obj.checkValid() + val collectionType = when { + type.classifier == RealmList::class -> CollectionType.RLM_COLLECTION_TYPE_LIST + type.classifier == RealmSet::class -> CollectionType.RLM_COLLECTION_TYPE_SET + type.classifier == RealmDictionary::class -> CollectionType.RLM_COLLECTION_TYPE_DICTIONARY + else -> CollectionType.RLM_COLLECTION_TYPE_NONE + } + val elementType: KType = if (collectionType != CollectionType.RLM_COLLECTION_TYPE_NONE) { + type.arguments[0].type!! + } else type + + val propertyMetadata = checkPropertyType( + obj, + propertyName, + collectionType, + elementType.classifier as KClass<*>, + elementType.isMarkedNullable + ) + val operatorType = when { + // FIXME Do we want extra properties to require + propertyMetadata == null || + propertyMetadata.type == PropertyType.RLM_PROPERTY_TYPE_MIXED -> + CollectionOperatorType.REALM_ANY + + propertyMetadata.type != PropertyType.RLM_PROPERTY_TYPE_OBJECT -> + CollectionOperatorType.PRIMITIVE + + !obj.owner.schemaMetadata[propertyMetadata.linkTarget]!!.isEmbeddedRealmObject -> + CollectionOperatorType.REALM_OBJECT + + // FIXME Embedded objects are not supported for sets + else -> CollectionOperatorType.EMBEDDED_OBJECT + } + // FIXME get to return collections in mixed + return when (collectionType) { + CollectionType.RLM_COLLECTION_TYPE_NONE -> { + return getterScope { +// val transport = when(propertyMetadata) { +// null -> realm_get_value_by_name(obj.objectPointer, propertyName) +// else -> realm_get_value(obj.objectPointer, propertyMetadata.key) +// } + val transport = realm_get_value_by_name(obj.objectPointer, propertyName) + + // Consider moving this dynamic conversion to Converters.kt + val value = when (type.classifier) { + DynamicRealmObject::class, + DynamicMutableRealmObject::class -> realmValueToRealmObject( + transport, + type.classifier as KClass, + obj.mediator, + obj.owner + ) + RealmAny::class -> realmValueToRealmAny( + realmValue = transport, + parent = obj, + mediator = obj.mediator, + owner = obj.owner, + issueDynamicObject = true, + issueDynamicMutableObject = issueDynamicMutableObject, + getListFunction = { + RealmInterop.realm_get_list_by_name(obj.objectPointer, propertyName) + }, + getDictionaryFunction = { + RealmInterop.realm_get_dictionary_by_name(obj.objectPointer, propertyName) + } + ) + + else -> when (type.classifier) { + Unit::class -> Unit as R // Special case to prevent get("name") to return errors when return value is not used but inferred to Unit + else -> with(primitiveTypeConverters.getValue(type.classifier as KClass)) { + realmValueToPublic(transport) + } + } + } + value?.let { + @Suppress("UNCHECKED_CAST") + if ((type.classifier as KClass).isInstance(value)) { + value as R + } else { + throw ClassCastException("Retrieving value of type '${(type.classifier as KClass).simpleName}' but was of type '${value::class.simpleName}'") + } + } as R + } + } + CollectionType.RLM_COLLECTION_TYPE_LIST -> { + getListByKey( + obj, + propertyMetadata, + elementType.classifier as KClass, + operatorType, + true, + issueDynamicMutableObject + ) as R + } + CollectionType.RLM_COLLECTION_TYPE_SET -> { + getSetByKey( + obj, + propertyMetadata, + elementType.classifier as KClass, + operatorType, + true, + issueDynamicMutableObject + ) as R + } + CollectionType.RLM_COLLECTION_TYPE_DICTIONARY -> { + getDictionaryByKey( + obj, + propertyMetadata, + elementType.classifier as KClass, + operatorType, + true, + issueDynamicMutableObject + ) as R + } + else -> sdkError("Unknown collection type $collectionType") + } } + internal inline fun MemAllocator.getRealmValue( obj: RealmObjectReference, propertyName: String, @@ -342,6 +570,20 @@ internal object RealmObjectHelper { } } + internal inline fun MemAllocator.getRealmValueFromName( + obj: RealmObjectReference, + name: String + ): RealmValue? { + val realmValue = realm_get_value_by_name( + obj.objectPointer, + name + ) + return when (realmValue.isNull()) { + true -> null + false -> realmValue + } + } + // --------------------------------------------------------------------- // End new implementation // --------------------------------------------------------------------- @@ -413,20 +655,44 @@ internal object RealmObjectHelper { return RealmResultsImpl(obj.owner, objects, sourceClassKey, sourceClass, obj.mediator) } + @Suppress("LongParameterList") + internal fun getListByName( + obj: RealmObjectReference, + propertyMetadata: PropertyMetadata?, + elementType: KClass, + operatorType: CollectionOperatorType, + issueDynamicObject: Boolean = false, + issueDynamicMutableObject: Boolean = false + ): ManagedRealmList { + Validation.isType(propertyMetadata) + val listPtr = RealmInterop.realm_get_list(obj.objectPointer, propertyMetadata.key) + val operator = createListOperator( + listPtr, + elementType, + propertyMetadata.linkTarget, + obj.mediator, + obj.owner, + operatorType, + issueDynamicObject, + issueDynamicMutableObject + ) + return ManagedRealmList(obj, listPtr, operator) + } @Suppress("LongParameterList") internal fun getListByKey( obj: RealmObjectReference, - propertyMetadata: PropertyMetadata, + propertyMetadata: PropertyMetadata?, elementType: KClass, operatorType: CollectionOperatorType, issueDynamicObject: Boolean = false, issueDynamicMutableObject: Boolean = false ): ManagedRealmList { + Validation.isType(propertyMetadata) val listPtr = RealmInterop.realm_get_list(obj.objectPointer, propertyMetadata.key) val operator = createListOperator( listPtr, elementType, - propertyMetadata, + propertyMetadata.linkTarget, obj.mediator, obj.owner, operatorType, @@ -440,7 +706,7 @@ internal object RealmObjectHelper { private fun createListOperator( listPtr: RealmListPointer, clazz: KClass, - propertyMetadata: PropertyMetadata, + targetClassName: String, mediator: Mediator, realm: RealmReference, operatorType: CollectionOperatorType, @@ -462,7 +728,7 @@ internal object RealmObjectHelper { issueDynamicMutableObject = issueDynamicMutableObject ) as ListOperator CollectionOperatorType.REALM_OBJECT -> { - val classKey: ClassKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey + val classKey: ClassKey = realm.schemaMetadata.getOrThrow(targetClassName).classKey RealmObjectListOperator( mediator, realm, @@ -472,7 +738,7 @@ internal object RealmObjectHelper { ) as ListOperator } CollectionOperatorType.EMBEDDED_OBJECT -> { - val classKey: ClassKey = realm.schemaMetadata.getOrThrow(propertyMetadata.linkTarget).classKey + val classKey: ClassKey = realm.schemaMetadata.getOrThrow(targetClassName).classKey EmbeddedRealmObjectListOperator( mediator, realm, @@ -506,12 +772,13 @@ internal object RealmObjectHelper { @Suppress("LongParameterList") internal fun getSetByKey( obj: RealmObjectReference, - propertyMetadata: PropertyMetadata, + propertyMetadata: PropertyMetadata?, elementType: KClass, operatorType: CollectionOperatorType, issueDynamicObject: Boolean = false, issueDynamicMutableObject: Boolean = false ): ManagedRealmSet { + Validation.isType(propertyMetadata) val setPtr = RealmInterop.realm_get_set(obj.objectPointer, propertyMetadata.key) val operator = createSetOperator( setPtr, @@ -590,12 +857,13 @@ internal object RealmObjectHelper { @Suppress("LongParameterList") internal fun getDictionaryByKey( obj: RealmObjectReference, - propertyMetadata: PropertyMetadata, + propertyMetadata: PropertyMetadata?, elementType: KClass, operatorType: CollectionOperatorType, issueDynamicObject: Boolean = false, issueDynamicMutableObject: Boolean = false ): ManagedRealmDictionary { + Validation.isType(propertyMetadata) val dictionaryPtr = RealmInterop.realm_get_dictionary(obj.objectPointer, propertyMetadata.key) val operator = createDictionaryOperator( @@ -675,6 +943,19 @@ internal object RealmObjectHelper { RealmInterop.realm_set_value(obj.objectPointer, key, transport, false) } + internal fun setValueTransportByName( + obj: RealmObjectReference, + name: String, + transport: RealmValue, + ) { + // TODO Consider making a RealmValue cinterop type and move the various to_realm_value + // implementations in the various platform RealmInterops here to eliminate + // RealmObjectInterop and make cinterop operate on primitive values and native pointers + // only. This relates to the overall concern of having a generic path for getter/setter + // instead of generating a typed path for each type. + RealmInterop.realm_set_value_by_name(obj.objectPointer, name, transport) + } + @Suppress("unused") // Called from generated code internal inline fun setList( obj: RealmObjectReference, @@ -908,7 +1189,7 @@ internal object RealmObjectHelper { CollectionType.RLM_COLLECTION_TYPE_NONE, clazz, nullable - ) + ) ?: throw IllegalStateException("Cannot access non-data model properties through the dynamic API") return getterScope { val transport = realm_get_value(obj.objectPointer, propertyInfo.key) @@ -960,7 +1241,7 @@ internal object RealmObjectHelper { CollectionType.RLM_COLLECTION_TYPE_LIST, clazz, nullable - ) + ) ?: throw IllegalStateException("Cannot access extra properties through this API") val operatorType = when { propertyMetadata.type == PropertyType.RLM_PROPERTY_TYPE_MIXED -> CollectionOperatorType.REALM_ANY @@ -995,7 +1276,7 @@ internal object RealmObjectHelper { CollectionType.RLM_COLLECTION_TYPE_SET, clazz, nullable - ) + ) ?: throw IllegalStateException("Cannot access non-data model properties through the dynamic API") val operatorType = when { propertyMetadata.type == PropertyType.RLM_PROPERTY_TYPE_MIXED -> CollectionOperatorType.REALM_ANY @@ -1030,7 +1311,7 @@ internal object RealmObjectHelper { CollectionType.RLM_COLLECTION_TYPE_DICTIONARY, clazz, nullable - ) + ) ?: throw IllegalStateException("Cannot access non-data model properties through the dynamic API") val operatorType = when { propertyMetadata.type == PropertyType.RLM_PROPERTY_TYPE_MIXED -> CollectionOperatorType.REALM_ANY @@ -1266,13 +1547,15 @@ internal object RealmObjectHelper { collectionType: CollectionType, elementType: KClass<*>, nullable: Boolean - ): PropertyMetadata { + ): PropertyMetadata? { val realElementType = elementType.realmStorageType() - return obj.metadata.getOrThrow(propertyName).also { propertyInfo -> + // FIXME Should this hold data model or realm schema? + return obj.metadata[propertyName]?.also { propertyInfo -> val kClass = RealmStorageTypeImpl.fromCorePropertyType(propertyInfo.type).kClass - if (collectionType != propertyInfo.collectionType || + // FIXME We can fix anything into RealmAny ... except EmbeddedObject + if (elementType != RealmAny::class && ( collectionType != propertyInfo.collectionType || realElementType != kClass || - nullable != propertyInfo.isNullable + nullable != propertyInfo.isNullable) ) { val expected = formatType(collectionType, realElementType, nullable) val actual = diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectInternal.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectInternal.kt index 676d29f97e..c7122b642f 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectInternal.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectInternal.kt @@ -16,7 +16,9 @@ package io.realm.kotlin.internal +import io.realm.kotlin.dynamic.getinterface.RelaxedRealmObject import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmObject /** @@ -28,6 +30,9 @@ import io.realm.kotlin.types.RealmObject */ // TODO Public due to being a transitive dependency of Mediator @Suppress("VariableNaming") -public interface RealmObjectInternal : BaseRealmObject { +// FIXME RelaxedRealmObject is only needed for proposal 2 of the relaxed schema API and this is +// maybe not the way to inject it in the hierarchy +public interface RealmObjectInternal : BaseRealmObject, RelaxedRealmObject { public var `io_realm_kotlin_objectReference`: RealmObjectReference? + public var `io_realm_kotlin_extras`: Map? } 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 4e86a39423..f46b721ac6 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 @@ -136,6 +136,14 @@ internal val T.realmObjectReference: RealmObjectReference< internal inline fun T.runIfManaged(block: RealmObjectReference.() -> R): R? = realmObjectReference?.run(block) +internal inline fun T.runIfManagedOrThrow(block: RealmObjectReference.() -> R): R { + val realmObjectReference = realmObjectReference + return when (realmObjectReference) { + null -> throw IllegalStateException("Cannot perform operation on unmanaged object") + else -> realmObjectReference.run (block) + } +} + /** * Returns an identifier that uniquely identifies a RealmObject. This includes the version of the * object, so the same RealmObject at two different versions must have different identifiers, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicMutableRealmObjectImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicMutableRealmObjectImpl.kt index ad8d6088eb..83c439e2e2 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicMutableRealmObjectImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicMutableRealmObjectImpl.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmSet import kotlin.reflect.KClass +import kotlin.reflect.typeOf internal class DynamicMutableRealmObjectImpl : DynamicMutableRealmObject, DynamicRealmObjectImpl() { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicRealmObjectImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicRealmObjectImpl.kt index 5179fb4298..aea430258d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicRealmObjectImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/dynamic/DynamicRealmObjectImpl.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.internal.RealmObjectInternal import io.realm.kotlin.internal.RealmObjectReference import io.realm.kotlin.query.RealmResults import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmSet @@ -31,6 +32,10 @@ public open class DynamicRealmObjectImpl : DynamicRealmObject, RealmObjectIntern override val type: String get() = this.`io_realm_kotlin_objectReference`!!.className + override var io_realm_kotlin_extras: Map? = + get() = TODO("Not yet implemented") + set(value) {} + // This should never be null after initialization of a dynamic object, but we currently cannot // represent that in the type system as we one some code paths construct the Kotlin object // before having the realm object reference diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmClassImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmClassImpl.kt index f6c91c7770..c6ec4acce5 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmClassImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmClassImpl.kt @@ -22,18 +22,20 @@ import io.realm.kotlin.schema.RealmClass import io.realm.kotlin.schema.RealmClassKind import io.realm.kotlin.schema.RealmProperty import io.realm.kotlin.schema.ValuePropertyType +import kotlin.reflect.KClass // TODO Public due to being a transitive dependency to RealmObjectCompanion public data class RealmClassImpl( // Optimization: Store the schema in the C-API alike structure directly from compiler plugin to // avoid unnecessary repeated initializations for realm_schema_new val cinteropClass: ClassInfo, - val cinteropProperties: List + val cinteropProperties: List, + override val inDataModel: Boolean = true, ) : RealmClass { override val name: String = cinteropClass.name override val properties: Collection = cinteropProperties.map { - RealmPropertyImpl.fromCoreProperty(it) + RealmPropertyImpl.fromCoreProperty(it, inDataModel) } override val primaryKey: RealmProperty? = properties.firstOrNull { it.type.run { this is ValuePropertyType && isPrimaryKey } @@ -47,4 +49,9 @@ public data class RealmClassImpl( } override fun get(key: String): RealmProperty? = properties.firstOrNull { it.name == key } + override val kClass: KClass<*>? + get() = TODO("Not yet implemented") + + // override val userDefined: Boolean +// get() = TODO("Not yet implemented") } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmPropertyImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmPropertyImpl.kt index c1b7fb6847..b18ae5d07d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmPropertyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmPropertyImpl.kt @@ -24,10 +24,13 @@ import io.realm.kotlin.schema.RealmProperty import io.realm.kotlin.schema.RealmPropertyType import io.realm.kotlin.schema.SetPropertyType import io.realm.kotlin.schema.ValuePropertyType +import io.realm.kotlin.types.BaseRealmObject +import kotlin.reflect.KMutableProperty1 internal data class RealmPropertyImpl( override var name: String, override var type: RealmPropertyType, + override val inDataModel: Boolean, ) : RealmProperty { override val isNullable: Boolean = when (type) { @@ -37,8 +40,11 @@ internal data class RealmPropertyImpl( is MapPropertyType -> false } + override val accessor: KMutableProperty1? + get() = null + companion object { - fun fromCoreProperty(corePropertyImpl: PropertyInfo): RealmPropertyImpl { + fun fromCoreProperty(corePropertyImpl: PropertyInfo, inModel: Boolean): RealmPropertyImpl { return with(corePropertyImpl) { val storageType = RealmStorageTypeImpl.fromCorePropertyType(type) val type = when (collectionType) { @@ -64,7 +70,7 @@ internal data class RealmPropertyImpl( ) else -> error("Unsupported type $collectionType") } - RealmPropertyImpl(name, type) + RealmPropertyImpl(name, type, inModel) } } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmSchemaImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmSchemaImpl.kt index 272b7a657c..d754d3ed4f 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmSchemaImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/schema/RealmSchemaImpl.kt @@ -42,7 +42,7 @@ internal data class RealmSchemaImpl( ).filter { property: PropertyInfo -> schemaMetadata == null || classMetadata?.get(property.name)?.isUserDefined() == true } - RealmClassImpl(table, properties) + RealmClassImpl(table, properties, true) } else { null } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmClass.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmClass.kt index 8b2e53f4ca..b3d0638d9c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmClass.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmClass.kt @@ -16,6 +16,8 @@ package io.realm.kotlin.schema +import kotlin.reflect.KClass + /** * A [RealmClass] describing the object model of a specific class. */ @@ -41,10 +43,17 @@ public interface RealmClass { */ public val kind: RealmClassKind + + public val inDataModel: Boolean // Flexible property would be false + /** * Index operator to lookup a specific [RealmProperty] from its persisted property name. * * @return the [RealmProperty] with the given `propertyName` or `null` if no such property exists. */ public operator fun get(key: String): RealmProperty? + + // Nullable not reflected in model + // TODO Consider deprecating inDataModel + public val kClass: KClass<*>? } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmProperty.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmProperty.kt index edff005fb0..d15a40daa3 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmProperty.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/schema/RealmProperty.kt @@ -16,6 +16,9 @@ package io.realm.kotlin.schema +import io.realm.kotlin.types.BaseRealmObject +import kotlin.reflect.KMutableProperty1 + /** * A [RealmProperty] describes the properties of a class property in the object model. */ @@ -40,4 +43,9 @@ public interface RealmProperty { * other property types it will always be false. */ public val isNullable: Boolean + + public val inDataModel: Boolean + + // Nullable not reflected in model (replacement for isDataModel) + public val accessor: KMutableProperty1? } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/BaseRealmObject.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/BaseRealmObject.kt index d39ae21360..60631d9650 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/BaseRealmObject.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/BaseRealmObject.kt @@ -17,8 +17,56 @@ package io.realm.kotlin.types import io.realm.kotlin.Deleteable +import io.realm.kotlin.internal.RealmObjectHelper +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow +import io.realm.kotlin.internal.runIfManaged +import io.realm.kotlin.internal.runIfManagedOrThrow +import io.realm.kotlin.schema.RealmClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +private const val s = "Cannot access or add additional properties to unmanaged object" /** * Base interface for all realm classes. */ -public interface BaseRealmObject : Deleteable +public interface BaseRealmObject : Deleteable { + // Dynamic API? +// public fun hasProperty(name: String): Boolean + + // Need to be isolated from data model properties + public val dataModelProperties: Set + get() = realmObjectCompanionOrThrow(this::class).io_realm_kotlin_fields.keys + public val extraProperties: Set + get() { + return this.runIfManaged { + RealmInterop.realm_get_additional_properties(this.objectPointer).toSet() + } ?: throw IllegalStateException(s) + } + + public val allProperties: Set + get() = dataModelProperties + extraProperties +} + +@PublishedApi +internal fun BaseRealmObject.get(propertyName: String, type: KType): T { + return this.runIfManagedOrThrow { + RealmObjectHelper.dynamicGetFromKType( + obj = this, + propertyName = propertyName, + type = type + ) + } +} +public inline operator fun BaseRealmObject.get(propertyName: String): T { + return get(propertyName, typeOf()) +} +public fun BaseRealmObject.hasProperty(name: String): Boolean { + return this.runIfManagedOrThrow { + RealmInterop.realm_has_property(this.objectPointer, name) + } +} + +public val BaseRealmObject.schema: RealmClass + get() = TODO() 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 b2c53632a1..0dc998b084 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 @@ -526,7 +526,8 @@ public interface SyncConfiguration : Configuration { partitionValue == null, inMemory, initialRealmFileConfiguration, - realmLogger + relaxedSchema, + realmLogger, ) return SyncConfigurationImpl( 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 0ce7399497..5c6df7feaa 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 @@ -193,7 +193,7 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi val realmClassImpl = pluginContext.lookupClassOrThrow(ClassIds.REALM_CLASS_IMPL) private val realmClassCtor = pluginContext.lookupConstructorInClass(ClassIds.REALM_CLASS_IMPL) { - it.owner.valueParameters.size == 2 + it.owner.valueParameters.size == 3 } private val validPrimaryKeyTypes = with(pluginContext.irBuiltIns) { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSchemaTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSchemaTests.kt index 1592abf877..7ff72e32bd 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSchemaTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSchemaTests.kt @@ -303,7 +303,8 @@ class RealmSchemaTests { sampleDescriptor, RealmClassImpl( io.realm.kotlin.internal.interop.ClassInfo("NEW_CLASS", numProperties = 1), - listOf(io.realm.kotlin.internal.interop.PropertyInfo("NEW_PROPERTY", type = PropertyType.RLM_PROPERTY_TYPE_STRING)) + listOf(io.realm.kotlin.internal.interop.PropertyInfo("NEW_PROPERTY", type = PropertyType.RLM_PROPERTY_TYPE_STRING)), + true, ) ) ) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RelaxedSchemaTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RelaxedSchemaTests.kt new file mode 100644 index 0000000000..6b3c833f0b --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RelaxedSchemaTests.kt @@ -0,0 +1,309 @@ +import io.realm.kotlin.types.RealmObject + +///* +// * Copyright 2024 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.common +// +//import io.realm.kotlin.Realm +//import io.realm.kotlin.RealmConfiguration +//import io.realm.kotlin.dynamic.DynamicRealmObject +//import io.realm.kotlin.entities.Sample +//import io.realm.kotlin.ext.query +//import io.realm.kotlin.internal.asDynamicRealm +//import io.realm.kotlin.internal.platform.runBlocking +//import io.realm.kotlin.query.RealmQuery +//import io.realm.kotlin.query.RealmResults +//import io.realm.kotlin.schema.RealmClass +//import io.realm.kotlin.schema.RealmProperty +//import io.realm.kotlin.schema.RealmSchema +//import io.realm.kotlin.schema.ValuePropertyType +//import io.realm.kotlin.test.common.utils.assertFailsWithMessage +//import io.realm.kotlin.test.platform.PlatformUtils +//import io.realm.kotlin.types.RealmAny +//import io.realm.kotlin.types.RealmList +//import io.realm.kotlin.types.RealmObject +////import io.realm.kotlin.types.extras +//import io.realm.kotlin.types.get +//import io.realm.kotlin.types.removeProperty +//import io.realm.kotlin.types.set +//import io.realm.kotlin.types.setIfNotPresent +//import io.realm.kotlin.types.setIfPresent +//import kotlin.test.AfterTest +//import kotlin.test.BeforeTest +//import kotlin.test.Test +// +//class RelaxedSchemaTests { +// +// lateinit var realm: Realm +// private lateinit var tmpDir: String +// +// @BeforeTest +// fun setup() { +// val configuration = RealmConfiguration.Builder(setOf(Sample::class, A::class)) +// .relaxedSchema(true) +// .directory(tmpDir) +// .build() +// realm = Realm.open(configuration) +// } +// +// @AfterTest +// fun tearDown() { +// if (this::realm.isInitialized && !realm.isClosed()) { +// realm.close() +// } +// PlatformUtils.deleteTempDir(tmpDir) +// } +// +// +// @Test +// fun basic() = runBlocking { +// +//// val configuration = RealmConfiguration.Builder(setOf(Sample::class, A::class)) +//// .relaxedSchema(true) +//// .build() +//// Realm.deleteRealm(configuration) +//// val realm = Realm.open(configuration) +// val schema: RealmSchema = realm.schema() +// val table: RealmClass = schema["Sample"]!! +// val schemaProperties: Collection = table.properties +// val property: RealmProperty = schemaProperties.first() +//// property.run { +//// this.name +//// this.isNullable +//// this.type.run { +//// this.isNullable +//// this.storageType +//// this.collectionType +//// when (this) { +//// is ListPropertyType -> TODO() +//// is MapPropertyType -> TODO() +//// is SetPropertyType -> TODO() +//// is ValuePropertyType -> TODO() +//// } +//// } +//// } +// var instance = realm.write { +// copyToRealm(Sample()) +// } +// +// // Iterate all strict properties dynamically (as RealmAny) +//// val x1: RealmList = instance["strinaListField"] +// val x2: RealmAny = instance.get("stringListField") +// val x3: RealmList = instance["stringListField"] +// val x4: RealmAny = instance["stringListField"] +// val x = schemaProperties.forEach { +//// if (it.type is ValuePropertyType) { +// val y: RealmAny = instance[it.name] +// println("instance[${it.name}] = $y") +//// } +// } +// +// instance = realm.write { +// val instance = findLatest(instance)!! +// instance["prop3"] = RealmAny.create("Realm") +// instance["prop4"] = RealmAny.create(5) +// instance +// } +// // Iterate all strict+relaxed properties dynamically (as RealmAny) +// // Must be from instance as non-strict properties are differing across instances +// instance.extraProperties.forEach { it -> +// val y = instance.get(it) +// println("instance.extras[${it}] = $y") +// } +// +// // Iterate relaxed properties dynamically (as RealmAny) +// // Must be from instance as non-strict properties are differing across instances +// instance.extraProperties.forEach { it -> +// if (table["it"] == null) { +// instance.get(it) +// } +// } +// +// instance.extraProperties.forEach { it -> +// instance.get(it) +// } +// // Can only remove extra properties, hence must not contain realm-schema-props +// realm.write { +// val instance1 = findLatest(instance)!! +// instance1.extraProperties.forEach { +// println("Removed: $it ${instance1.removeProperty(it)}") +// } +// } +// +// // What about these shortcuts for common patterns +//// if (instance.hasProperty("age")) { +//// instance.set("age", 6) +//// } +//// val present1 = instance.setIfPresent("age", 6) +//// if (!instance.hasProperty("age")) { +//// instance.set("age", 6) +//// } +//// val present1 = instance.setIfNotPresent("age", 6) +// +// instance = realm.write { +// val obj = instance +// val present2 = findLatest(obj)!!.setIfNotPresent("age", RealmAny.create("asdfasdf")) +// obj +// } +// +// // +// // Strict properties +// realm.schema()["Sample"]!!.properties.forEach { realmProperty: RealmProperty -> +// if (realmProperty.type is ValuePropertyType) { +// instance.get(realmProperty.name) +// } +// } +// // If this is too verbose, we could make explicit accessors for the individual selections +// // all-, dataModel-, realmSchema-, extra-Properties +// instance.extraProperties.forEach { it -> +// val y = instance.get(it) +// println("instance.extras[${it}] = $y") +// } +// +// // EXTRAS +// // Pretty neat, but how to keep things in Extras-namespace +//// realm.write { +//// findLatest(instance)!!.extras["asdf"] = RealmAny.create("ASDf") +//// } +// +// // Non-strict properties +//// instance.extraProperties().forEach { it -> instance.get(it) } +// +//// val x: DynamicRealm +//// val y = x.query("Sample") +//// val dynamicSample: DynamicRealmObject = y.find().get(0) +//// dynamicSample.getNullableValue("prop") +// +// // ?? Do we need some RealmProperty? from RealmObject instances +// // - We would newer have different type than RealmAny +// // Are there any RealmProperty information that is interesting +// // - Can we set/get/erase based on the type of property (model, schema, extra) +// // - Think it would be extremely heavy to pull RealmProperty details +// // - And if there is no concept of a server schema then I can't see why we need the info +// } +// +// @Test +// fun createRealm() = runBlocking { +// val configuration = RealmConfiguration.Builder(setOf(A::class)) +// .relaxedSchema(true) +// .build() +// val realm = Realm.open(configuration) +// println("schema: ${realm.schema()}") +// println("As: ${realm.query().find().size}") +// val x = realm.write { +// val x = copyToRealm(A()) +// println("PROPS: ${x.extraProperties}") +// x["PROP"] = RealmAny.create(34) +// x +// } +// println("PROPS: ${x.extraProperties}") +// for (property in x.extraProperties) { +// println("PROP[$property] = ${x.get(property)}") +// } +// } +// +// @Test +// fun iterateProperties() = runBlocking { +// val configuration = RealmConfiguration.Builder(setOf(A::class)) +// .relaxedSchema(true) +// .build() +// val realm = Realm.open(configuration) +// realm.write { +// delete(query()) +// } +// println("schema: ${realm.schema()}") +// println("As: ${realm.query().find().size}") +// val x = realm.write { +// val x = copyToRealm(A()) +// println("PROPS: ${x.extraProperties}") +// x["PROP"] = RealmAny.create(34) +// x +// } +// println("PROPS: ${x.extraProperties}") +// // +// val properties = realm.schema()["A"]!!.properties +// properties.forEach { +//// println("PROP[${it.name}] = ${(it.accessor as KMutableProperty1?)!!.get(x)}") +//// println("PROP[${it.name}] = ${(it.accessor as KMutableProperty1?)!!.set(x, null)}") +// } +// for (property in x.extraProperties) { +// println("PROP[$property] = ${x.get(property)}") +// } +// +// // TODO Maybe have a speedier way to get to the dynamic object +// val result: RealmQuery = realm.asDynamicRealm().query("A") +// val find: RealmResults = result.find() +// +// find[0].run { +// +// // Can be iterated across instances +// val allProps = properties.map { it.name } + x.extraProperties +// println("allProps: $allProps") +// for (allProp in allProps) { +// // Needs to be generalised to DynamicObject.get(name: String) :thinking: RealmAny vs. T vs. Any +//// this.get<>() +// println("property: $allProp") +// val y: String = x[allProp] +//// x.asdf(allProp) +// println("x[$allProp]: String = ${x.get(allProp)}") +// println("this[$allProp]: RealmAny = ${this.get(allProp)}") +// } +// } +// +// } +// +// // Tests +// // - Unmanaged objects +// // - Non-existing keys!? +// // - Open non-relaxed file with relaxed flag +// // - open relaxed file without relaxed flag +// // - Sync? +// +// // - Embedded objects import and assignment - Maybe more of an dynamic API test +// +// // TEST +// // - Add property outside write +// // - Remove property outside write +// @Test +// fun removeModelPropertyThrows() { +// // Can we remove a property from the schema - Needs to be in a write transaction +// } +// +// @Test +// fun removeSchemaPropertyThrows() { +// // Can we remove a property from the schema - Needs to be in a write transaction +// } +// +// @Test +// fun unmanagedObject_throws() { +// val sample = Sample() +// assertFailsWithMessage("Cannot access or add additional properties to unmanaged object") { +// sample["unknown"] +// } +// assertFailsWithMessage("Cannot access or add additional properties to unmanaged object") { +// sample["unknown"] = "TEST" +// } +// } +//} +// +class A : RealmObject { + var id: String = "DEFAULT" +} + +class B : RealmObject { + var id: String = "DEFAULT" +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/GenericDynamicRealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/GenericDynamicRealmTests.kt new file mode 100644 index 0000000000..bf9d415cea --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/GenericDynamicRealmTests.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024 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.common.dynamic + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.annotations.DynamicAPI +import io.realm.kotlin.annotations.ExperimentalGeoSpatialApi +import io.realm.kotlin.dynamic.getgeneric.get +import io.realm.kotlin.dynamic.getgeneric.set +import io.realm.kotlin.entities.Sample +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmList +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(DynamicAPI::class) +class GenericDynamicRealmTests { + + lateinit var realm: Realm + private lateinit var tmpDir: String + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + val configuration = RealmConfiguration.Builder(setOf(Sample::class, A::class)) + .relaxedSchema(true) + .directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun getRealmAny() = runBlocking { + + var sample = realm.write { copyToRealm(Sample()) + } + sample = realm.write { + findLatest(sample)!!.also { + it["test"] = 5 +// it.get>("stringListField").add("Realm") +// it.get("stringListField") + // This will still allow you to do generic runtime paths by asking for an RealmAny + val x : RealmAny = it.get("stringListField") + when(x.type) { + + } + } + } + @OptIn(ExperimentalGeoSpatialApi::class) + val actual2: Int = sample["test"] + val string = sample.stringField +// val string = sample.get("stringField") +// val string = sample.get("stringField") +// val string = sample.get("stringField") +// val string = sample.get("stringField") + assertEquals(5, actual2) + + val actual = sample.get>("stringListField")[0] + assertEquals("Realm", actual) + + // Gives some flexibility as you can also ask for a RealmAny + val actual1 = sample.get("stringListField") + assertEquals("Realm", actual1.asList()[0]!!.asString()) + + // Gives also the option of using a DynamicRealmObject + +// realm.copyFromRealm(DynamicMutableRealmObject.create("Person", mapOf())) + } +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RealmAnyDynamicRealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RealmAnyDynamicRealmTests.kt new file mode 100644 index 0000000000..6c832dc70d --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RealmAnyDynamicRealmTests.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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.common.dynamic + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.dynamic.DynamicRealmObject +import io.realm.kotlin.dynamic.getrealmany.get +import io.realm.kotlin.dynamic.getrealmany.set +import io.realm.kotlin.entities.Sample +import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.schema.RealmSchema +import io.realm.kotlin.test.common.A +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class RealmAnyDynamicRealmTests { + + lateinit var realm: Realm + private lateinit var tmpDir: String + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + val configuration = RealmConfiguration.Builder(setOf(Sample::class, A::class)) + .relaxedSchema(true) + .directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun getRealmAny() = runBlocking { + + var sample = realm.write { copyToRealm(Sample()) + } + sample = realm.write { + findLatest(sample)!!.also { + it["test"] = 5 + it["nullableObject"] = RealmAny.create(it) + val asList: RealmList = it["stringListField"].asList() + asList.add(RealmAny.create("String")) + } + } + val actual: Int = sample["test"].asInt() + assertEquals(5, actual) + + val actualObjectRealmAny: RealmAny = sample["nullableObject"] + val actualObject = actualObjectRealmAny.asRealmObject() // Typed and/or dynamic/related + + + val asList: RealmList = sample["stringListField"].asList() + val actual1: String = asList[0]!!.asString() + assertEquals("String", asList[0]!!.asString()) + assertEquals("String", actual1) + + + } +} + + +class A : RealmObject { + var properties: Map + + var name: String + + val realm: Realm + + + val extraProperties: List // They will always be RealmAny + val schema: RealmSchema = realm.schema() // properties will be typed potentially RealmAny if the schema says so +} diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RelaxedDynamicRealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RelaxedDynamicRealmTests.kt new file mode 100644 index 0000000000..d7de07f993 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/dynamic/RelaxedDynamicRealmTests.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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.common.dynamic + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.dynamic.getinterface.RelaxedRealmObject +import io.realm.kotlin.dynamic.getinterface.get +import io.realm.kotlin.dynamic.getinterface.relaxed +import io.realm.kotlin.dynamic.getinterface.set +import io.realm.kotlin.entities.Sample +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.test.common.A +import io.realm.kotlin.test.common.RealmListTests +import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmAny +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.get +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class RelaxedDynamicRealmTests { + + lateinit var realm: Realm + private lateinit var tmpDir: String + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + val configuration = RealmConfiguration.Builder(setOf(Sample::class, A::class)) + .relaxedSchema(true) + .directory(tmpDir) + .build() + realm = Realm.open(configuration) + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun getRealmAny() = runBlocking { + + var sample = realm.write { copyToRealm(Sample()) + } + sample = realm.write { + findLatest(sample)!!.also { + val relaxed = it.relaxed +// sample["test"] + relaxed["test"] = 5 + relaxed["nullableObject"] = RealmAny.create(Sample()) + relaxed.get>("stringListField").add("String") + } + } + val relaxed: RelaxedRealmObject = sample.relaxed + assertEquals(5, relaxed["test"]) + // + val actualSample: Sample = relaxed.get("nullableObject") + +// actualSample["test"] + val actualRelaxed: RelaxedRealmObject = relaxed.get("nullableObject") + // Typed fields are available but cannot use index operator unless unwrapped to RelaxedRealmObject again + assertEquals(5, actualSample.intField) + assertEquals(5, actualSample.get("sadf")) + // Typed fields are not available + //actualRelaxed.intField + // If type cannot be derived then we need to use get to +// actualRelaxed["intField"] + assertEquals("String", relaxed.get>("stringListField")[0]) + + // With this going from relaxed mode to static mode, will not onlly require that user is chaning the get("name") to static field accessors + // but will also have to address the indirection through the relaxed-type. + } +} 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 4d30150a34..41f56a4ec2 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 @@ -38,6 +38,7 @@ import io.realm.kotlin.internal.interop.RealmInterop 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.log.RealmLog import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.Credentials