Skip to content

Commit

Permalink
Allow setting primary keys during migration (#2793)
Browse files Browse the repository at this point in the history
Co-authored-by: Yavor Georgiev <fealebenpae@users.noreply.github.com>
  • Loading branch information
nirinchev and fealebenpae authored Feb 8, 2022
1 parent 924d797 commit 4864147
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 5 deletions.
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<Foo>())
{
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))
Expand Down
9 changes: 8 additions & 1 deletion Realm/Realm/DatabaseTypes/RealmObjectBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> GetListValue<T>(string propertyName)
Expand Down
2 changes: 1 addition & 1 deletion Realm/Realm/Handles/SharedRealmHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions Realm/Realm/Realm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ public static void DeleteRealm(RealmConfigurationBase configuration)

internal readonly SharedRealmHandle SharedRealmHandle;
internal readonly RealmMetadata Metadata;
internal readonly bool IsInMigration;

/// <summary>
/// Gets an object encompassing the dynamic API for this Realm instance.
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -1189,7 +1191,12 @@ public T ResolveReference<T>(ThreadSafeReference.Object<T> 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);
}

/// <summary>
Expand Down
182 changes: 182 additions & 0 deletions Tests/Realm.Tests/Database/MigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -556,5 +556,187 @@ public void Migration_NewRealm_Remove()
Assert.That(realm2.All<IntPropertyObject>().ToArray().Select(o => o.Int), Is.EqualTo(expected));
Assert.That(realm2.All<RequiredStringObject>().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<IntPrimaryKeyWithValueObject>(123);
var obj456 = realm.Find<IntPrimaryKeyWithValueObject>(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<IntPrimaryKeyWithValueObject>(123);
value.Id = 456;
}
};

using var realm = GetRealm(newRealmConfig);

var obj123 = realm.Find<IntPrimaryKeyWithValueObject>(123);
var obj456 = realm.Find<IntPrimaryKeyWithValueObject>(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<RealmObject>)migration.OldRealm.DynamicApi.All("Object"))
{
var newObj = (ObjectV2)migration.NewRealm.ResolveReference(ThreadSafeReference.Create(oldObj));
newObj.Id = oldObj.DynamicApi.Get<int>("Id").ToString();
}
}
};

using var realm = GetRealm(newRealmConfig);

Assert.That(realm.All<ObjectV2>().AsEnumerable().Select(o => o.Value), Is.EquivalentTo(new[] { "foo", "bar" }));
Assert.That(realm.All<ObjectV2>().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<IntPrimaryKeyWithValueObject>(1);
value.Id = 2;
}
};

var ex = Assert.Throws<RealmDuplicatePrimaryKeyValueException>(() => 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<IntPrimaryKeyWithValueObject>(1);
var obj2 = oldRealmAgain.Find<IntPrimaryKeyWithValueObject>(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; }
}
}
}

0 comments on commit 4864147

Please sign in to comment.