Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions src/BLite.Core/DocumentDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,18 @@ public void ConfigureAudit(Audit.BLiteAuditOptions options)
/// </summary>
public Audit.BLiteMetrics? AuditMetrics => _storage?.AuditMetrics;

/// <summary>
/// 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).
/// </summary>
public Task CheckpointAsync(CancellationToken ct = default)
{
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
return _storage.CheckpointAsync(ct);
}

public void Dispose()
{
if (_disposed)
Expand All @@ -545,6 +557,33 @@ public void Dispose()
GC.SuppressFinalize(this);
Comment on lines 546 to 557
}

/// <summary>
/// Checkpoints the WAL (merging all committed records into the main data file) and then
/// releases all resources. Prefer this over <see cref="Dispose"/> when you need the
/// database file to be fully self-contained after the context is closed.
/// </summary>
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);
}
}

/// <summary>
/// Creates a new caller-owned transaction.
/// The caller must pass this transaction to every collection method and
Expand Down
9 changes: 8 additions & 1 deletion src/BLite.Core/IDocumentDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace BLite.Core;
/// <summary>
/// Defines the contract for a document database context.
/// </summary>
public interface IDocumentDbContext : IDisposable, ITransactionHolder
public interface IDocumentDbContext : IDisposable, IAsyncDisposable, ITransactionHolder
{
/// <summary>
/// Provides access to the embedded Key-Value store that shares the same database file.
Expand Down Expand Up @@ -42,6 +42,13 @@ IDocumentCollection<TId, T> CreateSessionCollection<TId, T>(IDocumentMapper<TId,
/// </summary>
IDocumentCollection<TId, T> Set<TId, T>() where T : class;

/// <summary>
/// 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).
/// </summary>
Task CheckpointAsync(CancellationToken ct = default);

/// <summary>
/// Begins a new transaction synchronously.
/// </summary>
Expand Down
52 changes: 51 additions & 1 deletion tests/BLite.Tests/DbContextTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using BLite.Shared;
using BLite.Core;
using BLite.Shared;
using System.Security.Cryptography;

namespace BLite.Tests;
Expand Down Expand Up @@ -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);
}
Comment on lines +127 to +143

[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);
}
Comment on lines +145 to +159

[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<User>().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
Expand Down
Loading