Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting primary keys during migration #2793

Merged
merged 5 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 realm.All<Foo>())
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
{
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; }
}
}
}