diff --git a/CHANGELOG.md b/CHANGELOG.md index e190dc9c9f..0f74c98e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,16 @@ ## vNext (TBD) ### Enhancements -* Allow `ShouldCompactOnLaunch` to be set on `SyncConfiguration`, not only `RealmConfiguration`. (Issue [#3617](https://github.com/realm/realm-dotnet/issues/3617)) +* None ### Fixed -* A `ForCurrentlyOutstandingWork` progress notifier would not immediately call its callback after registration. Instead you would have to wait for some data to be received to get your first update - if you were already caught up when you registered the notifier you could end up waiting a long time for the server to deliver a download that would call/expire your notifier. (Core 14.8.0) -* After compacting, a file upgrade would be triggered. This could cause loss of data if `ShouldDeleteIfMigrationNeeded` is set to `true`. (Issue [#3583](https://github.com/realm/realm-dotnet/issues/3583), Core 14.9.0) +* None ### Compatibility * Realm Studio: 15.0.0 or later. ### Internal -* Using Core 14.9.0. +* Using Core x.y.z. ## 12.2.0 (2024-05-22) diff --git a/Realm/Realm/Configurations/RealmConfiguration.cs b/Realm/Realm/Configurations/RealmConfiguration.cs index cb88d9f5ed..b9ea5ab0d3 100644 --- a/Realm/Realm/Configurations/RealmConfiguration.cs +++ b/Realm/Realm/Configurations/RealmConfiguration.cs @@ -49,6 +49,17 @@ public class RealmConfiguration : RealmConfigurationBase /// public delegate void MigrationCallbackDelegate(Migration migration, ulong oldSchemaVersion); + /// + /// A callback, invoked when opening a Realm for the first time during the life + /// of a process to determine if it should be compacted before being returned + /// to the user. + /// + /// Total file size (data + free space). + /// Total data size. + /// true to indicate that an attempt to compact the file should be made. + /// The compaction will be skipped if another process is accessing it. + public delegate bool ShouldCompactDelegate(ulong totalBytes, ulong bytesUsed); + /// /// Gets or sets a value indicating whether the database will be deleted if the /// mismatches the one in the code. Use this when debugging and developing your app but never release it with @@ -73,6 +84,15 @@ public class RealmConfiguration : RealmConfigurationBase /// public MigrationCallbackDelegate? MigrationCallback { get; set; } + /// + /// Gets or sets the compact on launch callback. + /// + /// + /// The that will be invoked when opening a Realm for the first time + /// to determine if it should be compacted before being returned to the user. + /// + public ShouldCompactDelegate? ShouldCompactOnLaunch { get; set; } + /// /// Gets or sets the key, used to encrypt the entire Realm. Once set, must be specified each time the file is used. /// @@ -130,6 +150,7 @@ internal override Configuration CreateNativeConfiguration(Arena arena) result.delete_if_migration_needed = ShouldDeleteIfMigrationNeeded; result.read_only = IsReadOnly; result.invoke_migration_callback = MigrationCallback != null; + result.invoke_should_compact_callback = ShouldCompactOnLaunch != null; result.automatically_migrate_embedded = true; return result; diff --git a/Realm/Realm/Configurations/RealmConfigurationBase.cs b/Realm/Realm/Configurations/RealmConfigurationBase.cs index 5fc1de87af..5f3c6c91a5 100644 --- a/Realm/Realm/Configurations/RealmConfigurationBase.cs +++ b/Realm/Realm/Configurations/RealmConfigurationBase.cs @@ -39,17 +39,6 @@ public abstract class RealmConfigurationBase internal delegate void InitialDataDelegate(Realm realm); - /// - /// A callback, invoked when opening a Realm for the first time during the life - /// of a process to determine if it should be compacted before being returned - /// to the user. - /// - /// Total file size (data + free space). - /// Total data size. - /// true to indicate that an attempt to compact the file should be made. - /// The compaction will be skipped if another process is accessing it. - public delegate bool ShouldCompactDelegate(ulong totalBytes, ulong bytesUsed); - /// /// Gets the filename to be combined with the platform-specific document directory. /// @@ -80,15 +69,6 @@ public abstract class RealmConfigurationBase /// true if the Realm will be opened in dynamic mode; false otherwise. public bool IsDynamic { get; set; } - /// - /// Gets or sets the compact on launch callback. - /// - /// - /// The that will be invoked when opening a Realm for the first time - /// to determine if it should be compacted before being returned to the user. - /// - public ShouldCompactDelegate? ShouldCompactOnLaunch { get; set; } - internal bool EnableCache = true; /// @@ -253,7 +233,6 @@ internal virtual Configuration CreateNativeConfiguration(Arena arena) invoke_initial_data_callback = PopulateInitialData != null, managed_config = GCHandle.ToIntPtr(managedConfig), encryption_key = MarshaledVector.AllocateFrom(EncryptionKey, arena), - invoke_should_compact_callback = ShouldCompactOnLaunch != null, }; return config; diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index 9b438f2247..ff223c3e46 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -368,7 +368,7 @@ public virtual void AddChild(RealmHandle childHandle) // if we get !=0 and the real value was in fact 0, then we will just skip and then catch up next time around. // however, doing things this way will save lots and lots of locks when the list is empty, which it should be if people have // been using the dispose pattern correctly, or at least have been eager at disposing as soon as they can - // except of course dot notation users that cannot dispose because they never get a reference in the first place + // except of course dot notation users that cannot dispose cause they never get a reference in the first place lock (_unbindListLock) { UnbindLockedList(); @@ -608,7 +608,8 @@ public void WriteCopy(RealmConfigurationBase config) public RealmSchema GetSchema() { RealmSchema? result = null; - var callbackHandle = GCHandle.Alloc((Action)SchemaCallback); + Action callback = schema => result = RealmSchema.CreateFromObjectStoreSchema(schema); + var callbackHandle = GCHandle.Alloc(callback); try { NativeMethods.get_schema(this, GCHandle.ToIntPtr(callbackHandle), out var nativeException); @@ -620,8 +621,6 @@ public RealmSchema GetSchema() } return result!; - - void SchemaCallback(Native.Schema schema) => result = RealmSchema.CreateFromObjectStoreSchema(schema); } public ObjectHandle CreateObject(TableKey tableKey) @@ -869,7 +868,7 @@ private static IntPtr ShouldCompactOnLaunchCallback(IntPtr managedConfigHandle, try { var configHandle = GCHandle.FromIntPtr(managedConfigHandle); - var config = (RealmConfigurationBase)configHandle.Target!; + var config = (RealmConfiguration)configHandle.Target!; shouldCompact = config.ShouldCompactOnLaunch!.Invoke(totalSize, dataSize); return IntPtr.Zero; diff --git a/Tests/Realm.Tests/Database/InstanceTests.cs b/Tests/Realm.Tests/Database/InstanceTests.cs index e920e1c9e7..91b956c96b 100644 --- a/Tests/Realm.Tests/Database/InstanceTests.cs +++ b/Tests/Realm.Tests/Database/InstanceTests.cs @@ -321,6 +321,50 @@ public void RealmObjectClassesOnlyAllowRealmObjects() Assert.That(ex.Message, Does.Contain("must descend directly from either RealmObject, EmbeddedObject, or AsymmetricObject")); } + [TestCase(true)] + [TestCase(false)] + public void ShouldCompact_IsInvokedAfterOpening(bool shouldCompact) + { + var config = (RealmConfiguration)RealmConfiguration.DefaultConfiguration; + + using (var realm = GetRealm(config)) + { + AddDummyData(realm); + } + + var oldSize = new FileInfo(config.DatabasePath).Length; + long projectedNewSize = 0; + var hasPrompted = false; + config.ShouldCompactOnLaunch = (totalBytes, bytesUsed) => + { + Assert.That(totalBytes, Is.EqualTo(oldSize)); + hasPrompted = true; + projectedNewSize = (long)bytesUsed; + return shouldCompact; + }; + + using (var realm = GetRealm(config)) + { + Assert.That(hasPrompted, Is.True); + var newSize = new FileInfo(config.DatabasePath).Length; + if (shouldCompact) + { + // Less than or equal because of the new online compaction mechanism - it's possible + // that the Realm was already at the optimal size. + Assert.That(newSize, Is.LessThanOrEqualTo(oldSize)); + + // Less than 20% error in projections + Assert.That((newSize - projectedNewSize) / newSize, Is.LessThan(0.2)); + } + else + { + Assert.That(newSize, Is.EqualTo(oldSize)); + } + + Assert.That(realm.All().Count(), Is.EqualTo(DummyDataSize / 2)); + } + } + [TestCase(false, true)] [TestCase(false, false)] [TestCase(true, true)] @@ -410,7 +454,7 @@ public void Compact_WhenResultsAreOpen_ShouldReturnFalse() { using var realm = GetRealm(); - var token = realm.All().SubscribeForNotifications((_, changes) => + var token = realm.All().SubscribeForNotifications((sender, changes) => { Console.WriteLine(changes?.InsertedIndices); }); @@ -419,32 +463,6 @@ public void Compact_WhenResultsAreOpen_ShouldReturnFalse() token.Dispose(); } - [Test] - public void Compact_WhenShouldDeleteIfMigrationNeeded_PreservesObjects() - { - var config = (RealmConfiguration)RealmConfiguration.DefaultConfiguration; - config.ShouldDeleteIfMigrationNeeded = true; - - using (var realm = GetRealm(config)) - { - realm.Write(() => - { - realm.Add(new Person - { - FirstName = "Peter" - }); - }); - } - - Assert.That(Realm.Compact(config), Is.True); - - using (var realm = GetRealm(config)) - { - Assert.That(realm.All().Count(), Is.EqualTo(1)); - Assert.That(realm.All().Single().FirstName, Is.EqualTo("Peter")); - } - } - [Test] public void RealmChangedShouldFireForEveryInstance() { @@ -454,13 +472,13 @@ public void RealmChangedShouldFireForEveryInstance() using var realm2 = GetRealm(); var changed1 = 0; - realm1.RealmChanged += (_, _) => + realm1.RealmChanged += (sender, e) => { changed1++; }; var changed2 = 0; - realm2.RealmChanged += (_, _) => + realm2.RealmChanged += (sender, e) => { changed2++; }; @@ -610,7 +628,7 @@ public void GetInstanceAsync_ExecutesMigrationsInBackground() var threadId = Environment.CurrentManagedThreadId; var hasCompletedMigration = false; config.SchemaVersion = 2; - config.MigrationCallback = (migration, _) => + config.MigrationCallback = (migration, oldSchemaVersion) => { Assert.That(Environment.CurrentManagedThreadId, Is.Not.EqualTo(threadId)); Task.Delay(300).Wait(); @@ -896,7 +914,8 @@ public void FrozenRealm_CannotSubscribeForNotifications() using var realm = GetRealm(); using var frozenRealm = realm.Freeze(); - Assert.Throws(() => frozenRealm.RealmChanged += (_, _) => { }); + Assert.Throws(() => frozenRealm.RealmChanged += (_, __) => { }); + Assert.Throws(() => frozenRealm.RealmChanged -= (_, __) => { }); } [Test] @@ -1027,7 +1046,7 @@ await TestHelpers.EnsureObjectsAreCollected(() => using var realm = Realm.GetInstance(); var state = stateAccessor.GetValue(realm)!; - return new[] { state }; + return new object[] { state }; }); }); } @@ -1074,7 +1093,7 @@ public void GetInstance_WithManualSchema_CanReadAndWrite() { Schema = new RealmSchema.Builder { - new ObjectSchema.Builder("MyType") + new ObjectSchema.Builder("MyType", ObjectSchema.ObjectType.RealmObject) { Property.Primitive("IntValue", RealmValueType.Int), Property.PrimitiveList("ListValue", RealmValueType.Date), @@ -1085,7 +1104,7 @@ public void GetInstance_WithManualSchema_CanReadAndWrite() Property.ObjectSet("ObjectSetValue", "OtherObject"), Property.ObjectDictionary("ObjectDictionaryValue", "OtherObject"), }, - new ObjectSchema.Builder("OtherObject") + new ObjectSchema.Builder("OtherObject", ObjectSchema.ObjectType.RealmObject) { Property.Primitive("Id", RealmValueType.String, isPrimaryKey: true), Property.Backlinks("MyTypes", "MyType", "ObjectValue") @@ -1211,10 +1230,13 @@ public void GetInstance_WithTypedSchemaWithMissingProperties_ThrowsException() using var realm = GetRealm(config); - var person = realm.Write(() => realm.Add(new Person + var person = realm.Write(() => { - LastName = "Smith" - })); + return realm.Add(new Person + { + LastName = "Smith" + }); + }); var exGet = Assert.Throws(() => _ = person.FirstName)!; Assert.That(exGet.Message, Does.Contain(nameof(Person))); @@ -1233,10 +1255,13 @@ public void RealmWithFrozenObjects_WhenDeleted_DoesNotThrow() { var config = new RealmConfiguration(Guid.NewGuid().ToString()); var realm = GetRealm(config); - var frozenObj = realm.Write(() => realm.Add(new IntPropertyObject + var frozenObj = realm.Write(() => { - Int = 1 - }).Freeze()); + return realm.Add(new IntPropertyObject + { + Int = 1 + }).Freeze(); + }); frozenObj.Realm!.Dispose(); realm.Dispose(); @@ -1250,7 +1275,7 @@ public void RealmWithFrozenObjects_WhenDeleted_DoesNotThrow() public void BeginWrite_CalledMultipleTimes_Throws() { using var realm = GetRealm(); - using var ts = realm.BeginWrite(); + var ts = realm.BeginWrite(); Assert.That(() => realm.BeginWrite(), Throws.TypeOf()); } diff --git a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs index 00fededf10..7daf37f9b9 100644 --- a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs +++ b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs @@ -30,6 +30,7 @@ using Realms.Sync; using Realms.Sync.ErrorHandling; using Realms.Sync.Exceptions; +using NUnitExplicit = NUnit.Framework.ExplicitAttribute; namespace Realms.Tests.Sync { @@ -76,49 +77,6 @@ public void Compact_ShouldReduceSize([Values(true, false)] bool encrypt, [Values }); } - [Test] - public void ShouldCompact_IsInvokedAfterOpening([Values(true, false)] bool shouldCompact, [Values(true, false)] bool useSync) - { - RealmConfigurationBase config = useSync ? GetFakeConfig() : new RealmConfiguration(Guid.NewGuid().ToString()); - - using (var realm = GetRealm(config)) - { - AddDummyData(realm, singleTransaction: false); - } - - var oldSize = new FileInfo(config.DatabasePath).Length; - long projectedNewSize = 0; - var hasPrompted = false; - config.ShouldCompactOnLaunch = (totalBytes, bytesUsed) => - { - Assert.That(totalBytes, Is.EqualTo(oldSize)); - hasPrompted = true; - projectedNewSize = (long)bytesUsed; - return shouldCompact; - }; - - using (var realm = GetRealm(config)) - { - Assert.That(hasPrompted, Is.True); - var newSize = new FileInfo(config.DatabasePath).Length; - if (shouldCompact) - { - // Less than or equal because of the new online compaction mechanism - it's possible - // that the Realm was already at the optimal size. - Assert.That(newSize, Is.LessThanOrEqualTo(oldSize)); - - // Less than 20% error in projections - Assert.That((newSize - projectedNewSize) / newSize, Is.LessThan(0.2)); - } - else - { - Assert.That(newSize, Is.EqualTo(oldSize)); - } - - Assert.That(realm.All().Count(), Is.EqualTo(DummyDataSize / 2)); - } - } - [Test] public void GetInstanceAsync_ShouldDownloadRealm([Values(true, false)] bool singleTransaction) { @@ -218,7 +176,10 @@ public void GetInstanceAsync_WithOnProgressThrowing_ReportsErrorToLogs() Logger.Default = logger; config = await GetIntegrationConfigAsync((string?)config.Partition); - config.OnProgress = _ => throw new Exception("Exception in OnProgress"); + config.OnProgress = (progress) => + { + throw new Exception("Exception in OnProgress"); + }; var realmTask = GetRealmAsync(config); config.OnProgress = null; @@ -396,10 +357,13 @@ public void WriteCopy_CanSynchronizeData([Values(true, false)] bool originalEncr Assert.That(copiedRealm.All().Count(), Is.EqualTo(originalRealm.All().Count())); - var fromCopy = copiedRealm.Write(() => copiedRealm.Add(new ObjectIdPrimaryKeyWithValueObject + var fromCopy = copiedRealm.Write(() => { - StringValue = "Added from copy" - })); + return copiedRealm.Add(new ObjectIdPrimaryKeyWithValueObject + { + StringValue = "Added from copy" + }); + }); await WaitForUploadAsync(copiedRealm); await WaitForDownloadAsync(originalRealm); @@ -408,10 +372,13 @@ public void WriteCopy_CanSynchronizeData([Values(true, false)] bool originalEncr Assert.That(itemInOriginal, Is.Not.Null); Assert.That(itemInOriginal!.StringValue, Is.EqualTo(fromCopy.StringValue)); - var fromOriginal = originalRealm.Write(() => originalRealm.Add(new ObjectIdPrimaryKeyWithValueObject + var fromOriginal = originalRealm.Write(() => { - StringValue = "Added from original" - })); + return originalRealm.Add(new ObjectIdPrimaryKeyWithValueObject + { + StringValue = "Added from original" + }); + }); await WaitForUploadAsync(originalRealm); await WaitForDownloadAsync(copiedRealm); @@ -474,10 +441,13 @@ public void WriteCopy_LocalToSync([Values(true, false)] bool originalEncrypted, Assert.That(anotherUserRealm.All().Count(), Is.EqualTo(addedObjects)); - var addedObject = anotherUserRealm.Write(() => anotherUserRealm.Add(new ObjectIdPrimaryKeyWithValueObject + var addedObject = anotherUserRealm.Write(() => { - StringValue = "abc" - })); + return anotherUserRealm.Add(new ObjectIdPrimaryKeyWithValueObject + { + StringValue = "abc" + }); + }); await WaitForUploadAsync(anotherUserRealm); await WaitForDownloadAsync(copiedRealm); @@ -564,7 +534,7 @@ public void RemoveAll_RemovesAllElements([Values(true, false)] bool originalEncr realmConfig.EncryptionKey = TestHelpers.GetEncryptionKey(42); } - var realm = GetRealm(realmConfig); + using var realm = GetRealm(realmConfig); AddDummyData(realm, true); diff --git a/wrappers/realm-core b/wrappers/realm-core index f3d7ae5f9f..14349903d1 160000 --- a/wrappers/realm-core +++ b/wrappers/realm-core @@ -1 +1 @@ -Subproject commit f3d7ae5f9f31d90b327a64536bb7801cc69fd85b +Subproject commit 14349903d1315e13758537a735a649bd1c2d2fec