diff --git a/CHANGELOG.md b/CHANGELOG.md index 768828893d..1d1f88ad79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,21 @@ ## vNext (TBD) ### Enhancements -* None +* Lifted a limitation that would prevent you from changing the primary key of objects during a migration. It is now possible to do it with both the dynamic and the strongly-typed API: + ```csharp + var config = new RealmConfiguration + { + SchemaVersion = 5, + MigrationCallback = (migration, oldVersion) => + { + // Increment the primary key value of all Foos + foreach (var obj in migration.NewRealm.All()) + { + obj.Id = obj.Id + 1000; + } + } + } + ``` ### Fixed * Fixed an issue with xUnit tests that would cause `System.Runtime.InteropServices.SEHException` to be thrown whenever Realm was accessed in a non-async test. (Issue [#1865](https://github.com/realm/realm-dotnet/issues/1865)) diff --git a/Realm/Realm/DatabaseTypes/RealmObjectBase.cs b/Realm/Realm/DatabaseTypes/RealmObjectBase.cs index cec8e72e80..1784318267 100644 --- a/Realm/Realm/DatabaseTypes/RealmObjectBase.cs +++ b/Realm/Realm/DatabaseTypes/RealmObjectBase.cs @@ -224,7 +224,14 @@ protected void SetValueUnique(string propertyName, RealmValue val) { Debug.Assert(IsManaged, "Object is not managed, but managed access was attempted"); - _objectHandle.SetValueUnique(propertyName, _metadata, val); + if (_realm.IsInMigration) + { + _objectHandle.SetValue(propertyName, _metadata, val, _realm); + } + else + { + _objectHandle.SetValueUnique(propertyName, _metadata, val); + } } protected internal IList GetListValue(string propertyName) diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index 9e9bd180f2..04a9da241d 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -713,7 +713,7 @@ private static bool OnMigration(IntPtr oldRealmPtr, IntPtr newRealmPtr, IntPtr m var oldRealm = new Realm(oldRealmHandle, oldConfiguration, RealmSchema.CreateFromObjectStoreSchema(oldSchema)); var newRealmHandle = new UnownedRealmHandle(newRealmPtr); - var newRealm = new Realm(newRealmHandle, migration.Configuration, migration.Schema); + var newRealm = new Realm(newRealmHandle, migration.Configuration, migration.Schema, isInMigration: true); var result = migration.Execute(oldRealm, newRealm, migrationSchema); diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index d66b38b406..e3513cfe79 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -154,6 +154,7 @@ public static void DeleteRealm(RealmConfigurationBase configuration) internal readonly SharedRealmHandle SharedRealmHandle; internal readonly RealmMetadata Metadata; + internal readonly bool IsInMigration; /// /// Gets an object encompassing the dynamic API for this Realm instance. @@ -275,9 +276,10 @@ public SubscriptionSet Subscriptions } } - internal Realm(SharedRealmHandle sharedRealmHandle, RealmConfigurationBase config, RealmSchema schema, RealmMetadata metadata = null) + internal Realm(SharedRealmHandle sharedRealmHandle, RealmConfigurationBase config, RealmSchema schema, RealmMetadata metadata = null, bool isInMigration = false) { Config = config; + IsInMigration = isInMigration; if (config.EnableCache && sharedRealmHandle.OwnsNativeRealm) { @@ -1189,7 +1191,12 @@ public T ResolveReference(ThreadSafeReference.Object reference) return null; } - return (T)MakeObject(reference.Metadata, objectHandle); + if (!Metadata.TryGetValue(reference.Metadata.Schema.Name, out var metadata)) + { + metadata = reference.Metadata; + } + + return (T)MakeObject(metadata, objectHandle); } /// diff --git a/Tests/Realm.Tests/Database/MigrationTests.cs b/Tests/Realm.Tests/Database/MigrationTests.cs index dd3fd1cf22..125e9da136 100644 --- a/Tests/Realm.Tests/Database/MigrationTests.cs +++ b/Tests/Realm.Tests/Database/MigrationTests.cs @@ -556,5 +556,187 @@ public void Migration_NewRealm_Remove() Assert.That(realm2.All().ToArray().Select(o => o.Int), Is.EqualTo(expected)); Assert.That(realm2.All().ToArray().Select(o => int.Parse(o.String)), Is.EqualTo(expected)); } + + [Test] + public void Migration_ChangePrimaryKey_Dynamic() + { + var oldRealmConfig = new RealmConfiguration(Guid.NewGuid().ToString()); + using (var oldRealm = GetRealm(oldRealmConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new IntPrimaryKeyWithValueObject + { + Id = 123, + StringValue = "123" + }); + }); + } + + var newRealmConfig = new RealmConfiguration(oldRealmConfig.DatabasePath) + { + SchemaVersion = 1, + MigrationCallback = (migration, oldSchemaVersion) => + { + var value = (RealmObjectBase)migration.NewRealm.DynamicApi.Find(nameof(IntPrimaryKeyWithValueObject), 123); + value.DynamicApi.Set("_id", 456); + } + }; + + using var realm = GetRealm(newRealmConfig); + + var obj123 = realm.Find(123); + var obj456 = realm.Find(456); + + Assert.That(obj123, Is.Null); + Assert.That(obj456, Is.Not.Null); + Assert.That(obj456.StringValue, Is.EqualTo("123")); + } + + [Test] + public void Migration_ChangePrimaryKey_Static() + { + var oldRealmConfig = new RealmConfiguration(Guid.NewGuid().ToString()); + using (var oldRealm = GetRealm(oldRealmConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new IntPrimaryKeyWithValueObject + { + Id = 123, + StringValue = "123" + }); + }); + } + + var newRealmConfig = new RealmConfiguration(oldRealmConfig.DatabasePath) + { + SchemaVersion = 1, + MigrationCallback = (migration, oldSchemaVersion) => + { + var value = migration.NewRealm.Find(123); + value.Id = 456; + } + }; + + using var realm = GetRealm(newRealmConfig); + + var obj123 = realm.Find(123); + var obj456 = realm.Find(456); + + Assert.That(obj123, Is.Null); + Assert.That(obj456, Is.Not.Null); + Assert.That(obj456.StringValue, Is.EqualTo("123")); + } + + [Test] + public void Migration_ChangePrimaryKeyType() + { + var oldRealmConfig = new RealmConfiguration(Guid.NewGuid().ToString()) + { + Schema = new[] { typeof(ObjectV1) } + }; + + using (var oldRealm = GetRealm(oldRealmConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new ObjectV1 + { + Id = 1, + Value = "foo" + }); + + oldRealm.Add(new ObjectV1 + { + Id = 2, + Value = "bar" + }); + }); + } + + var newRealmConfig = new RealmConfiguration(oldRealmConfig.DatabasePath) + { + SchemaVersion = 1, + Schema = new[] { typeof(ObjectV2) }, + MigrationCallback = (migration, oldSchemaVersion) => + { + foreach (var oldObj in (IQueryable)migration.OldRealm.DynamicApi.All("Object")) + { + var newObj = (ObjectV2)migration.NewRealm.ResolveReference(ThreadSafeReference.Create(oldObj)); + newObj.Id = oldObj.DynamicApi.Get("Id").ToString(); + } + } + }; + + using var realm = GetRealm(newRealmConfig); + + Assert.That(realm.All().AsEnumerable().Select(o => o.Value), Is.EquivalentTo(new[] { "foo", "bar" })); + Assert.That(realm.All().AsEnumerable().Select(o => o.Id), Is.EquivalentTo(new[] { "1", "2" })); + } + + [Test] + public void Migration_ChangePrimaryKey_WithDuplicates_Throws() + { + var oldRealmConfig = new RealmConfiguration(Guid.NewGuid().ToString()); + using (var oldRealm = GetRealm(oldRealmConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new IntPrimaryKeyWithValueObject + { + Id = 1, + StringValue = "1" + }); + oldRealm.Add(new IntPrimaryKeyWithValueObject + { + Id = 2, + StringValue = "2" + }); + }); + } + + var newRealmConfig = new RealmConfiguration(oldRealmConfig.DatabasePath) + { + SchemaVersion = 1, + MigrationCallback = (migration, oldSchemaVersion) => + { + var value = migration.NewRealm.Find(1); + value.Id = 2; + } + }; + + var ex = Assert.Throws(() => GetRealm(newRealmConfig)); + Assert.That(ex.Message, Does.Contain($"{nameof(IntPrimaryKeyWithValueObject)}._id")); + + // Ensure we haven't messed up the data + using var oldRealmAgain = GetRealm(oldRealmConfig); + + var obj1 = oldRealmAgain.Find(1); + var obj2 = oldRealmAgain.Find(2); + + Assert.That(obj1.StringValue, Is.EqualTo("1")); + Assert.That(obj2.StringValue, Is.EqualTo("2")); + } + + [Explicit] + [MapTo("Object")] + private class ObjectV1 : RealmObject + { + [PrimaryKey] + public int Id { get; set; } + + public string Value { get; set; } + } + + [Explicit] + [MapTo("Object")] + private class ObjectV2 : RealmObject + { + [PrimaryKey] + public string Id { get; set; } + + public string Value { get; set; } + } } }