diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5d3adfab..5c9f8212d6 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)) * [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 9529a5124b..f45f3c6289 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() {