using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Compression; 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.IO.Compression; using System.IO.MemoryMappedFiles; using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class DocumentOverflowTests : IDisposable { private readonly string _dbPath; private readonly Shared.TestDbContext _db; /// /// Initializes a new instance of the class. /// public DocumentOverflowTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_{Guid.NewGuid()}.db"); // Use default PageSize (16KB) _db = new Shared.TestDbContext(_dbPath); } /// /// Releases test resources. /// public void Dispose() { _db.Dispose(); if (File.Exists(_dbPath)) File.Delete(_dbPath); } /// /// Verifies inserting a medium-sized document succeeds. /// [Fact] public void Insert_MediumDoc_64KB_ShouldSucceed() { // 20KB - Fits in 64KB buffer (First attempt) // But triggers overflow pages in storage (20KB > 16KB PageSize) var largeString = new string('A', 20 * 1024); var user = new User { Id = ObjectId.NewObjectId(), Name = largeString, Age = 10 }; var id = _db.Users.Insert(user); _db.SaveChanges(); var retrieved = _db.Users.FindById(id); retrieved.ShouldNotBeNull(); retrieved.Name.ShouldBe(largeString); } /// /// Verifies inserting a large document succeeds. /// [Fact] public void Insert_LargeDoc_100KB_ShouldSucceed() { // 100KB - Fails 64KB buffer, Retries with 2MB var largeString = new string('B', 100 * 1024); var user = new User { Id = ObjectId.NewObjectId(), Name = largeString, Age = 20 }; var id = _db.Users.Insert(user); _db.SaveChanges(); var retrieved = _db.Users.FindById(id); retrieved.ShouldNotBeNull(); retrieved.Name.ShouldBe(largeString); } /// /// Verifies inserting a very large document succeeds. /// [Fact] public void Insert_HugeDoc_3MB_ShouldSucceed() { // 3MB - Fails 64KB, Fails 2MB, Retries with 16MB var largeString = new string('C', 3 * 1024 * 1024); var user = new User { Id = ObjectId.NewObjectId(), Name = largeString, Age = 30 }; var id = _db.Users.Insert(user); _db.SaveChanges(); var retrieved = _db.Users.FindById(id); retrieved.ShouldNotBeNull(); retrieved.Name.Length.ShouldBe(largeString.Length); // Checking full string might be slow, length check + substring check is faster retrieved.Name.Substring(0, 100).ShouldBe(largeString.Substring(0, 100)); retrieved.Name.Substring(retrieved.Name.Length - 100).ShouldBe(largeString.Substring(largeString.Length - 100)); } /// /// Verifies updating from a small payload to a huge payload succeeds. /// [Fact] public void Update_SmallToHuge_ShouldSucceed() { // Insert Small var user = new User { Id = ObjectId.NewObjectId(), Name = "Small", Age = 1 }; var id = _db.Users.Insert(user); _db.SaveChanges(); // Update to Huge (3MB) var hugeString = new string('U', 3 * 1024 * 1024); user.Name = hugeString; var updated = _db.Users.Update(user); _db.SaveChanges(); updated.ShouldBeTrue(); var retrieved = _db.Users.FindById(id); retrieved.ShouldNotBeNull(); retrieved.Name.Length.ShouldBe(hugeString.Length); } /// /// Verifies bulk inserts with mixed payload sizes succeed. /// [Fact] public void InsertBulk_MixedSizes_ShouldSucceed() { var users = new List { new User { Id = ObjectId.NewObjectId(), Name = "Small 1", Age = 1 }, new User { Id = ObjectId.NewObjectId(), Name = new string('M', 100 * 1024), Age = 2 }, // 100KB new User { Id = ObjectId.NewObjectId(), Name = "Small 2", Age = 3 }, new User { Id = ObjectId.NewObjectId(), Name = new string('H', 3 * 1024 * 1024), Age = 4 } // 3MB }; var ids = _db.Users.InsertBulk(users); ids.Count.ShouldBe(4); foreach (var u in users) { var r = _db.Users.FindById(u.Id); r.ShouldNotBeNull(); r.Name.Length.ShouldBe(u.Name.Length); } } /// /// Verifies huge inserts succeed with compression enabled and small page configuration. /// [Fact] public void Insert_HugeDoc_WithCompressionEnabledAndSmallPages_ShouldSucceed() { var localDbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_compression_{Guid.NewGuid():N}.db"); var options = new CompressionOptions { EnableCompression = true, MinSizeBytes = 0, MinSavingsPercent = 0, Codec = CompressionCodec.Brotli, Level = CompressionLevel.Fastest }; try { using var db = new Shared.TestDbContext(localDbPath, TinyPageConfig(), options); var huge = new string('Z', 2 * 1024 * 1024); var id = db.Users.Insert(new User { Id = ObjectId.NewObjectId(), Name = huge, Age = 50 }); db.SaveChanges(); var loaded = db.Users.FindById(id); loaded.ShouldNotBeNull(); loaded.Name.ShouldBe(huge); db.GetCompressionStats().CompressedDocumentCount.ShouldBeGreaterThanOrEqualTo(1); } finally { CleanupLocalFiles(localDbPath); } } /// /// Verifies updates from huge to small payloads succeed with compression enabled. /// [Fact] public void Update_HugeToSmall_WithCompressionEnabled_ShouldSucceed() { var localDbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_compression_update_{Guid.NewGuid():N}.db"); var options = new CompressionOptions { EnableCompression = true, MinSizeBytes = 1024, MinSavingsPercent = 0, Codec = CompressionCodec.Deflate, Level = CompressionLevel.Fastest }; try { using var db = new Shared.TestDbContext(localDbPath, TinyPageConfig(), options); var user = new User { Id = ObjectId.NewObjectId(), Name = new string('Q', 256 * 1024), Age = 44 }; var id = db.Users.Insert(user); db.SaveChanges(); user.Name = "small-after-overflow"; db.Users.Update(user).ShouldBeTrue(); db.SaveChanges(); var loaded = db.Users.FindById(id); loaded.ShouldNotBeNull(); loaded.Name.ShouldBe("small-after-overflow"); } finally { CleanupLocalFiles(localDbPath); } } private static PageFileConfig TinyPageConfig() { return new PageFileConfig { PageSize = 16 * 1024, InitialFileSize = 1024 * 1024, Access = MemoryMappedFileAccess.ReadWrite }; } private static void CleanupLocalFiles(string dbPath) { var walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } }