using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; using System.Buffers; using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class WalIndexTests : IDisposable { private readonly string _dbPath; private readonly string _walPath; private readonly Shared.TestDbContext _db; private readonly ITestOutputHelper _output; public WalIndexTests(ITestOutputHelper output) { _output = output; _dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_index_{Guid.NewGuid()}.db"); // WAL defaults to .wal next to db _walPath = Path.ChangeExtension(_dbPath, ".wal"); _db = new Shared.TestDbContext(_dbPath); } [Fact] public void IndexWritesAreLoggedToWal() { // 2. Start a transaction using var txn = _db.BeginTransaction(); _output.WriteLine($"Started Transaction: {txn.TransactionId}"); // 3. Insert a user var user = new User { Name = "Alice", Age = 30 }; _db.Users.Insert(user); // 4. Commit txn.Commit(); _output.WriteLine("Committed Transaction"); // 5. Verify WAL // Dispose current storage to release file locks, BUT skip checkpoint/truncate _db.Dispose(); File.Exists(_walPath).ShouldBeTrue("WAL file should exist"); using var walReader = new WriteAheadLog(_walPath); var records = walReader.ReadAll(); _output.WriteLine($"Found {records.Count} WAL records"); // Filter for this transaction var txnRecords = records.Where(r => r.TransactionId == txn.TransactionId).ToList(); txnRecords.ShouldContain(r => r.Type == WalRecordType.Begin); txnRecords.ShouldContain(r => r.Type == WalRecordType.Commit); var writeRecords = txnRecords.Where(r => r.Type == WalRecordType.Write).ToList(); _output.WriteLine($"Found {writeRecords.Count} Write records for Txn {txn.TransactionId}"); // Analyze pages int indexPageCount = 0; int dataPageCount = 0; foreach (var record in writeRecords) { var pageType = ParsePageType(record.AfterImage); _output.WriteLine($"Page {record.PageId}: Type={pageType}, Size={record.AfterImage?.Length}"); if (pageType == PageType.Index) indexPageCount++; else if (pageType == PageType.Data) dataPageCount++; } (indexPageCount > 0).ShouldBeTrue($"Expected at least 1 Index page write, found {indexPageCount}"); (dataPageCount > 0).ShouldBeTrue($"Expected at least 1 Data page write, found {dataPageCount}"); } private PageType ParsePageType(byte[]? pageData) { if (pageData == null || pageData.Length < 32) return (PageType)0; // PageType is at offset 4 (1 byte) return (PageType)pageData[4]; // Casting byte to PageType } [Fact] public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun() { for (var i = 0; i < 100; i++) { _db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 }); } _db.SaveChanges(); _db.Storage.GetWalSize().ShouldBeGreaterThan(0); var stats = _db.Compact(new CompactionOptions { EnableTailTruncation = true, NormalizeFreeList = true, DefragmentSlottedPages = true }); stats.OnlineMode.ShouldBeFalse(); _db.Storage.GetWalSize().ShouldBe(0); new FileInfo(_walPath).Length.ShouldBe(0); } [Fact] public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData() { var dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db"); var walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; var expectedIds = new List(); try { using (var writer = new Shared.TestDbContext(dbPath)) { for (var i = 0; i < 48; i++) { expectedIds.Add(writer.Users.Insert(new User { Name = $"recover-{i:D3}", Age = i % 10 })); } writer.SaveChanges(); writer.Storage.GetWalSize().ShouldBeGreaterThan(0); } new FileInfo(walPath).Length.ShouldBeGreaterThan(0); using (var recovered = new Shared.TestDbContext(dbPath)) { recovered.Users.Count().ShouldBe(expectedIds.Count); recovered.Compact(); recovered.Storage.GetWalSize().ShouldBe(0); foreach (var id in expectedIds) { recovered.Users.FindById(id).ShouldNotBeNull(); } } } finally { if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } } public void Dispose() { try { _db?.Dispose(); // Safe to call multiple times } catch { } try { if (File.Exists(_dbPath)) File.Delete(_dbPath); } catch { } try { if (File.Exists(_walPath)) File.Delete(_walPath); } catch { } } }