diff --git a/src/BLite.Core/DocumentDbContext.cs b/src/BLite.Core/DocumentDbContext.cs index c2622d8..04fd579 100644 --- a/src/BLite.Core/DocumentDbContext.cs +++ b/src/BLite.Core/DocumentDbContext.cs @@ -531,6 +531,18 @@ public void ConfigureAudit(Audit.BLiteAuditOptions options) /// public Audit.BLiteMetrics? AuditMetrics => _storage?.AuditMetrics; + /// + /// Forces an immediate checkpoint: merges all committed WAL records into the main data + /// file. Call this before disposing the context when you need the database file to be + /// fully self-contained (e.g., before copying or shipping the file). + /// + public Task CheckpointAsync(CancellationToken ct = default) + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + return _storage.CheckpointAsync(ct); + } + public void Dispose() { if (_disposed) @@ -545,6 +557,33 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Checkpoints the WAL (merging all committed records into the main data file) and then + /// releases all resources. Prefer this over when you need the + /// database file to be fully self-contained after the context is closed. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _disposed = true; + + try + { + if (_storage != null) + await _storage.CheckpointAsync(CancellationToken.None); + } + finally + { + _storage?.Dispose(); + _cdc?.Dispose(); + _ownedCoordinator?.Dispose(); + + GC.SuppressFinalize(this); + } + } + /// /// Creates a new caller-owned transaction. /// The caller must pass this transaction to every collection method and diff --git a/src/BLite.Core/IDocumentDbContext.cs b/src/BLite.Core/IDocumentDbContext.cs index c5f7bbb..cc4e74d 100644 --- a/src/BLite.Core/IDocumentDbContext.cs +++ b/src/BLite.Core/IDocumentDbContext.cs @@ -12,7 +12,7 @@ namespace BLite.Core; /// /// Defines the contract for a document database context. /// -public interface IDocumentDbContext : IDisposable, ITransactionHolder +public interface IDocumentDbContext : IDisposable, IAsyncDisposable, ITransactionHolder { /// /// Provides access to the embedded Key-Value store that shares the same database file. @@ -42,6 +42,13 @@ IDocumentCollection CreateSessionCollection(IDocumentMapper IDocumentCollection Set() where T : class; + /// + /// Forces an immediate checkpoint: merges all committed WAL records into the main data + /// file. Call this before disposing the context when you need the database file to be + /// fully self-contained (e.g., before copying or shipping the file). + /// + Task CheckpointAsync(CancellationToken ct = default); + /// /// Begins a new transaction synchronously. /// diff --git a/tests/BLite.Tests/DbContextTests.cs b/tests/BLite.Tests/DbContextTests.cs index e4f48ef..6e28dba 100644 --- a/tests/BLite.Tests/DbContextTests.cs +++ b/tests/BLite.Tests/DbContextTests.cs @@ -1,4 +1,5 @@ -using BLite.Shared; +using BLite.Core; +using BLite.Shared; using System.Security.Cryptography; namespace BLite.Tests; @@ -123,6 +124,55 @@ public async Task DbContext_AutoDerivesWalPath() Assert.True(File.Exists(walPath)); } + [Fact] + public async Task DbContext_CheckpointAsync_PersistsDataToPageFile() + { + // Insert data and call CheckpointAsync explicitly via the public API. + await using (var db = new TestDbContext(_dbPath)) + { + await db.Users.InsertAsync(new User { Name = "Alice", Age = 30 }); + // Checkpoint flushes the WAL into the main page file. + await db.CheckpointAsync(); + } + + // Re-open: data must be visible without replaying the WAL from scratch. + await using var db2 = new TestDbContext(_dbPath); + var all = await db2.Users.FindAllAsync().ToListAsync(); + Assert.Single(all); + Assert.Equal("Alice", all[0].Name); + } + + [Fact] + public async Task DbContext_DisposeAsync_CheckpointsBeforeClose() + { + // Insert data and dispose via DisposeAsync (which checkpoints automatically). + await using (var db = new TestDbContext(_dbPath)) + { + await db.Users.InsertAsync(new User { Name = "Bob", Age = 25 }); + } + + // Re-open: data must survive without any explicit checkpoint call. + await using var db2 = new TestDbContext(_dbPath); + var all = await db2.Users.FindAllAsync().ToListAsync(); + Assert.Single(all); + Assert.Equal("Bob", all[0].Name); + } + + [Fact] + public async Task DbContext_CheckpointAsync_IsExposedOnInterface() + { + // Verify the method is accessible through the interface. + IDocumentDbContext db = new TestDbContext(_dbPath); + await using (db) + { + await db.Set().InsertAsync(new User { Name = "Carol", Age = 40 }); + await db.CheckpointAsync(); + } + + await using var db2 = new TestDbContext(_dbPath); + Assert.Single(await db2.Users.FindAllAsync().ToListAsync()); + } + public void Dispose() { try