261 lines
8.0 KiB
C#
Executable File
261 lines
8.0 KiB
C#
Executable File
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;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DocumentOverflowTests"/> class.
|
|
/// </summary>
|
|
public DocumentOverflowTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_{Guid.NewGuid()}.db");
|
|
// Use default PageSize (16KB)
|
|
_db = new Shared.TestDbContext(_dbPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases test resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_db.Dispose();
|
|
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies inserting a medium-sized document succeeds.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies inserting a large document succeeds.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies inserting a very large document succeeds.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies updating from a small payload to a huge payload succeeds.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies bulk inserts with mixed payload sizes succeed.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InsertBulk_MixedSizes_ShouldSucceed()
|
|
{
|
|
var users = new List<User>
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies huge inserts succeed with compression enabled and small page configuration.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies updates from huge to small payloads succeed with compression enabled.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|