Fix audit findings for coverage, architecture checks, and XML docs
This commit is contained in:
@@ -10,11 +10,19 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab
|
||||
private readonly object _sync = new();
|
||||
private ITransaction? _currentTransaction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BenchmarkTransactionHolder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storage">The storage engine used to create transactions.</param>
|
||||
public BenchmarkTransactionHolder(StorageEngine storage)
|
||||
{
|
||||
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current active transaction or starts a new one.
|
||||
/// </summary>
|
||||
/// <returns>The current active transaction.</returns>
|
||||
public ITransaction GetCurrentTransactionOrStart()
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -28,11 +36,18 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current active transaction or starts a new one asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>A task that returns the current active transaction.</returns>
|
||||
public Task<ITransaction> GetCurrentTransactionOrStartAsync()
|
||||
{
|
||||
return Task.FromResult(GetCurrentTransactionOrStart());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits the current transaction when active and clears the holder.
|
||||
/// </summary>
|
||||
public void CommitAndReset()
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -53,6 +68,9 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls back the current transaction when active and clears the holder.
|
||||
/// </summary>
|
||||
public void RollbackAndReset()
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -73,6 +91,9 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this holder and rolls back any outstanding transaction.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
RollbackAndReset();
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
[JsonExporterAttribute.Full]
|
||||
public class CompactionBenchmarks
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the number of documents used per benchmark iteration.
|
||||
/// </summary>
|
||||
[Params(2_000)]
|
||||
public int DocumentCount { get; set; }
|
||||
|
||||
@@ -25,6 +28,9 @@ public class CompactionBenchmarks
|
||||
private DocumentCollection<Person> _collection = null!;
|
||||
private List<ObjectId> _insertedIds = [];
|
||||
|
||||
/// <summary>
|
||||
/// Prepares benchmark state and seed data for each iteration.
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void Setup()
|
||||
{
|
||||
@@ -56,6 +62,9 @@ public class CompactionBenchmarks
|
||||
_storage.Checkpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up benchmark resources and temporary files after each iteration.
|
||||
/// </summary>
|
||||
[IterationCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
@@ -66,6 +75,10 @@ public class CompactionBenchmarks
|
||||
if (File.Exists(_walPath)) File.Delete(_walPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks reclaimed file bytes reported by offline compaction.
|
||||
/// </summary>
|
||||
/// <returns>The reclaimed file byte count.</returns>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("Compaction_Offline")]
|
||||
public long OfflineCompact_ReclaimedBytes()
|
||||
@@ -81,6 +94,10 @@ public class CompactionBenchmarks
|
||||
return stats.ReclaimedFileBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks tail bytes truncated by offline compaction.
|
||||
/// </summary>
|
||||
/// <returns>The truncated tail byte count.</returns>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Compaction_Offline")]
|
||||
public long OfflineCompact_TailBytesTruncated()
|
||||
|
||||
@@ -20,12 +20,21 @@ public class CompressionBenchmarks
|
||||
private const int SeedCount = 300;
|
||||
private const int WorkloadCount = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether compression is enabled for the benchmark run.
|
||||
/// </summary>
|
||||
[Params(false, true)]
|
||||
public bool EnableCompression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression codec for the benchmark run.
|
||||
/// </summary>
|
||||
[Params(CompressionCodec.Brotli, CompressionCodec.Deflate)]
|
||||
public CompressionCodec Codec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression level for the benchmark run.
|
||||
/// </summary>
|
||||
[Params(CompressionLevel.Fastest, CompressionLevel.Optimal)]
|
||||
public CompressionLevel Level { get; set; }
|
||||
|
||||
@@ -38,6 +47,9 @@ public class CompressionBenchmarks
|
||||
private Person[] _insertBatch = Array.Empty<Person>();
|
||||
private ObjectId[] _seedIds = Array.Empty<ObjectId>();
|
||||
|
||||
/// <summary>
|
||||
/// Prepares benchmark storage and seed data for each iteration.
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void Setup()
|
||||
{
|
||||
@@ -72,6 +84,9 @@ public class CompressionBenchmarks
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up benchmark resources for each iteration.
|
||||
/// </summary>
|
||||
[IterationCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
@@ -82,6 +97,9 @@ public class CompressionBenchmarks
|
||||
if (File.Exists(_walPath)) File.Delete(_walPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks insert workload performance.
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("Compression_InsertUpdateRead")]
|
||||
public void Insert_Workload()
|
||||
@@ -90,6 +108,9 @@ public class CompressionBenchmarks
|
||||
_transactionHolder.CommitAndReset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks update workload performance.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Compression_InsertUpdateRead")]
|
||||
public void Update_Workload()
|
||||
@@ -109,6 +130,9 @@ public class CompressionBenchmarks
|
||||
_transactionHolder.CommitAndReset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks read workload performance.
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
[BenchmarkCategory("Compression_InsertUpdateRead")]
|
||||
public int Read_Workload()
|
||||
|
||||
@@ -11,33 +11,55 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
internal static class DatabaseSizeBenchmark
|
||||
{
|
||||
private static readonly int[] TargetCounts = [10_000, 1_000_000, 10_000_000];
|
||||
private static readonly CompressionOptions CompressedBrotliFast = new()
|
||||
{
|
||||
EnableCompression = true,
|
||||
MinSizeBytes = 256,
|
||||
MinSavingsPercent = 0,
|
||||
Codec = CompressionCodec.Brotli,
|
||||
Level = System.IO.Compression.CompressionLevel.Fastest
|
||||
};
|
||||
|
||||
private static readonly Scenario[] Scenarios =
|
||||
[
|
||||
// Separate compression set (no compaction)
|
||||
new(
|
||||
"Uncompressed",
|
||||
CompressionOptions.Default),
|
||||
Set: "compression",
|
||||
Name: "CompressionOnly-Uncompressed",
|
||||
CompressionOptions: CompressionOptions.Default,
|
||||
RunCompaction: false),
|
||||
new(
|
||||
"Compressed-BrotliFast",
|
||||
new CompressionOptions
|
||||
{
|
||||
EnableCompression = true,
|
||||
MinSizeBytes = 256,
|
||||
MinSavingsPercent = 0,
|
||||
Codec = CompressionCodec.Brotli,
|
||||
Level = System.IO.Compression.CompressionLevel.Fastest
|
||||
})
|
||||
Set: "compression",
|
||||
Name: "CompressionOnly-Compressed-BrotliFast",
|
||||
CompressionOptions: CompressedBrotliFast,
|
||||
RunCompaction: false),
|
||||
// Separate compaction set (compaction enabled)
|
||||
new(
|
||||
Set: "compaction",
|
||||
Name: "Compaction-Uncompressed",
|
||||
CompressionOptions: CompressionOptions.Default,
|
||||
RunCompaction: true),
|
||||
new(
|
||||
Set: "compaction",
|
||||
Name: "Compaction-Compressed-BrotliFast",
|
||||
CompressionOptions: CompressedBrotliFast,
|
||||
RunCompaction: true)
|
||||
];
|
||||
|
||||
private const int BatchSize = 50_000;
|
||||
private const int ProgressInterval = 1_000_000;
|
||||
|
||||
/// <summary>
|
||||
/// Tests run.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger for benchmark progress and results.</param>
|
||||
public static void Run(ILogger logger)
|
||||
{
|
||||
var results = new List<SizeResult>(TargetCounts.Length * Scenarios.Length);
|
||||
|
||||
logger.LogInformation("=== CBDD Database Size Benchmark ===");
|
||||
logger.LogInformation("=== CBDD Database Size Benchmark (Separate Compression/Compaction Sets) ===");
|
||||
logger.LogInformation("Targets: {Targets}", string.Join(", ", TargetCounts.Select(x => x.ToString("N0"))));
|
||||
logger.LogInformation("Scenarios: {Scenarios}", string.Join(", ", Scenarios.Select(x => x.Name)));
|
||||
logger.LogInformation("Scenarios: {Scenarios}", string.Join(", ", Scenarios.Select(x => $"{x.Set}:{x.Name}")));
|
||||
logger.LogInformation("Batch size: {BatchSize:N0}", BatchSize);
|
||||
|
||||
foreach (var targetCount in TargetCounts)
|
||||
@@ -48,12 +70,17 @@ internal static class DatabaseSizeBenchmark
|
||||
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
||||
using var _ = LogContext.PushProperty("TargetCount", targetCount);
|
||||
using var __ = LogContext.PushProperty("Scenario", scenario.Name);
|
||||
using var ___ = LogContext.PushProperty("ScenarioSet", scenario.Set);
|
||||
|
||||
logger.LogInformation("Starting scenario {Scenario} for target {TargetCount:N0} docs", scenario.Name, targetCount);
|
||||
logger.LogInformation(
|
||||
"Starting {Set} scenario {Scenario} for target {TargetCount:N0} docs",
|
||||
scenario.Set,
|
||||
scenario.Name,
|
||||
targetCount);
|
||||
|
||||
var insertStopwatch = Stopwatch.StartNew();
|
||||
CompressionStats compressionStats = default;
|
||||
CompactionStats compactionStats;
|
||||
CompactionStats compactionStats = new();
|
||||
long preCompactDbBytes;
|
||||
long preCompactWalBytes;
|
||||
long postCompactDbBytes;
|
||||
@@ -93,12 +120,15 @@ internal static class DatabaseSizeBenchmark
|
||||
preCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0;
|
||||
preCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0;
|
||||
|
||||
compactionStats = storage.Compact(new CompactionOptions
|
||||
if (scenario.RunCompaction)
|
||||
{
|
||||
EnableTailTruncation = true,
|
||||
DefragmentSlottedPages = true,
|
||||
NormalizeFreeList = true
|
||||
});
|
||||
compactionStats = storage.Compact(new CompactionOptions
|
||||
{
|
||||
EnableTailTruncation = true,
|
||||
DefragmentSlottedPages = true,
|
||||
NormalizeFreeList = true
|
||||
});
|
||||
}
|
||||
|
||||
postCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0;
|
||||
postCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0;
|
||||
@@ -106,7 +136,9 @@ internal static class DatabaseSizeBenchmark
|
||||
}
|
||||
|
||||
var result = new SizeResult(
|
||||
scenario.Set,
|
||||
scenario.Name,
|
||||
scenario.RunCompaction,
|
||||
targetCount,
|
||||
insertStopwatch.Elapsed,
|
||||
preCompactDbBytes,
|
||||
@@ -118,13 +150,16 @@ internal static class DatabaseSizeBenchmark
|
||||
results.Add(result);
|
||||
|
||||
logger.LogInformation(
|
||||
"Completed {Scenario} {TargetCount:N0} docs in {Elapsed}. pre={PreTotal}, post={PostTotal}, shrink={Shrink}, compRatio={CompRatio}",
|
||||
"Completed {Set}:{Scenario} {TargetCount:N0} docs in {Elapsed}. pre={PreTotal}, post={PostTotal}, shrink={Shrink}, compactApplied={CompactionApplied}, compactReclaim={CompactReclaim}, compRatio={CompRatio}",
|
||||
scenario.Set,
|
||||
scenario.Name,
|
||||
targetCount,
|
||||
insertStopwatch.Elapsed,
|
||||
FormatBytes(result.PreCompactTotalBytes),
|
||||
FormatBytes(result.PostCompactTotalBytes),
|
||||
FormatBytes(result.ShrinkBytes),
|
||||
scenario.RunCompaction,
|
||||
FormatBytes(result.CompactionStats.ReclaimedFileBytes),
|
||||
result.CompressionRatioText);
|
||||
|
||||
TryDelete(dbPath);
|
||||
@@ -133,10 +168,14 @@ internal static class DatabaseSizeBenchmark
|
||||
}
|
||||
|
||||
logger.LogInformation("=== Size Benchmark Summary ===");
|
||||
foreach (var result in results.OrderBy(x => x.TargetCount).ThenBy(x => x.Scenario))
|
||||
foreach (var result in results
|
||||
.OrderBy(x => x.Set)
|
||||
.ThenBy(x => x.TargetCount)
|
||||
.ThenBy(x => x.Scenario))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"{Scenario,-22} | {Count,12:N0} docs | insert={Elapsed,12} | pre={Pre,12} | post={Post,12} | shrink={Shrink,12} | compact={CompactBytes,12} | ratio={Ratio}",
|
||||
"{Set,-11} | {Scenario,-38} | {Count,12:N0} docs | insert={Elapsed,12} | pre={Pre,12} | post={Post,12} | shrink={Shrink,12} | compact={CompactBytes,12} | ratio={Ratio}",
|
||||
result.Set,
|
||||
result.Scenario,
|
||||
result.TargetCount,
|
||||
result.InsertElapsed,
|
||||
@@ -146,6 +185,8 @@ internal static class DatabaseSizeBenchmark
|
||||
FormatBytes(result.CompactionStats.ReclaimedFileBytes),
|
||||
result.CompressionRatioText);
|
||||
}
|
||||
|
||||
WriteSummaryCsv(results, logger);
|
||||
}
|
||||
|
||||
private static SizeBenchmarkDocument CreateDocument(int value)
|
||||
@@ -181,10 +222,42 @@ internal static class DatabaseSizeBenchmark
|
||||
return $"{size:N2} {units[unitIndex]}";
|
||||
}
|
||||
|
||||
private sealed record Scenario(string Name, CompressionOptions CompressionOptions);
|
||||
private static void WriteSummaryCsv(IEnumerable<SizeResult> results, ILogger logger)
|
||||
{
|
||||
var outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results");
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var outputPath = Path.Combine(outputDirectory, "DatabaseSizeBenchmark-results.csv");
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
"set,scenario,target_count,run_compaction,insert_seconds,pre_total_bytes,post_total_bytes,shrink_bytes,compaction_reclaimed_bytes,compression_ratio_text"
|
||||
};
|
||||
|
||||
foreach (var result in results.OrderBy(x => x.Set).ThenBy(x => x.TargetCount).ThenBy(x => x.Scenario))
|
||||
{
|
||||
lines.Add(string.Join(",",
|
||||
result.Set,
|
||||
result.Scenario,
|
||||
result.TargetCount.ToString(),
|
||||
result.RunCompaction ? "true" : "false",
|
||||
result.InsertElapsed.TotalSeconds.ToString("F3"),
|
||||
result.PreCompactTotalBytes.ToString(),
|
||||
result.PostCompactTotalBytes.ToString(),
|
||||
result.ShrinkBytes.ToString(),
|
||||
result.CompactionStats.ReclaimedFileBytes.ToString(),
|
||||
result.CompressionRatioText));
|
||||
}
|
||||
|
||||
File.WriteAllLines(outputPath, lines);
|
||||
logger.LogInformation("Database size summary CSV written to {OutputPath}", outputPath);
|
||||
}
|
||||
|
||||
private sealed record Scenario(string Set, string Name, CompressionOptions CompressionOptions, bool RunCompaction);
|
||||
|
||||
private sealed record SizeResult(
|
||||
string Set,
|
||||
string Scenario,
|
||||
bool RunCompaction,
|
||||
int TargetCount,
|
||||
TimeSpan InsertElapsed,
|
||||
long PreCompactDbBytes,
|
||||
@@ -194,10 +267,22 @@ internal static class DatabaseSizeBenchmark
|
||||
CompactionStats CompactionStats,
|
||||
CompressionStats CompressionStats)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the pre compact total bytes.
|
||||
/// </summary>
|
||||
public long PreCompactTotalBytes => PreCompactDbBytes + PreCompactWalBytes;
|
||||
/// <summary>
|
||||
/// Gets or sets the post compact total bytes.
|
||||
/// </summary>
|
||||
public long PostCompactTotalBytes => PostCompactDbBytes + PostCompactWalBytes;
|
||||
/// <summary>
|
||||
/// Gets or sets the shrink bytes.
|
||||
/// </summary>
|
||||
public long ShrinkBytes => PreCompactTotalBytes - PostCompactTotalBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compression ratio text.
|
||||
/// </summary>
|
||||
public string CompressionRatioText =>
|
||||
CompressionStats.BytesAfterCompression > 0
|
||||
? $"{(double)CompressionStats.BytesBeforeCompression / CompressionStats.BytesAfterCompression:N2}x"
|
||||
@@ -206,19 +291,32 @@ internal static class DatabaseSizeBenchmark
|
||||
|
||||
private sealed class SizeBenchmarkDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public int Value { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class SizeBenchmarkDocumentMapper : ObjectIdMapperBase<SizeBenchmarkDocument>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string CollectionName => "size_documents";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ObjectId GetId(SizeBenchmarkDocument entity) => entity.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetId(SizeBenchmarkDocument entity, ObjectId id) => entity.Id = id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Serialize(SizeBenchmarkDocument entity, BsonSpanWriter writer)
|
||||
{
|
||||
var sizePos = writer.BeginDocument();
|
||||
@@ -229,6 +327,7 @@ internal static class DatabaseSizeBenchmark
|
||||
return writer.Position;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SizeBenchmarkDocument Deserialize(BsonSpanReader reader)
|
||||
{
|
||||
var document = new SizeBenchmarkDocument();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
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.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
using System.IO;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
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.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
using System.IO;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
|
||||
[InProcess]
|
||||
@@ -18,32 +18,35 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
|
||||
[HtmlExporter]
|
||||
[JsonExporterAttribute.Full]
|
||||
public class InsertBenchmarks
|
||||
{
|
||||
private const int BatchSize = 1000;
|
||||
private static readonly ILogger Logger = Logging.CreateLogger<InsertBenchmarks>();
|
||||
|
||||
private string _docDbPath = "";
|
||||
private string _docDbWalPath = "";
|
||||
|
||||
private StorageEngine? _storage = null;
|
||||
private BenchmarkTransactionHolder? _transactionHolder = null;
|
||||
private DocumentCollection<Person>? _collection = null;
|
||||
|
||||
private Person[] _batchData = Array.Empty<Person>();
|
||||
private Person? _singlePerson = null;
|
||||
public class InsertBenchmarks
|
||||
{
|
||||
private const int BatchSize = 1000;
|
||||
private static readonly ILogger Logger = Logging.CreateLogger<InsertBenchmarks>();
|
||||
|
||||
private string _docDbPath = "";
|
||||
private string _docDbWalPath = "";
|
||||
|
||||
private StorageEngine? _storage = null;
|
||||
private BenchmarkTransactionHolder? _transactionHolder = null;
|
||||
private DocumentCollection<Person>? _collection = null;
|
||||
|
||||
private Person[] _batchData = Array.Empty<Person>();
|
||||
private Person? _singlePerson = null;
|
||||
|
||||
/// <summary>
|
||||
/// Tests setup.
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var temp = AppContext.BaseDirectory;
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
_docDbPath = Path.Combine(temp, $"bench_docdb_{id}.db");
|
||||
_docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal");
|
||||
|
||||
_singlePerson = CreatePerson(0);
|
||||
_batchData = new Person[BatchSize];
|
||||
for (int i = 0; i < BatchSize; i++)
|
||||
var temp = AppContext.BaseDirectory;
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
_docDbPath = Path.Combine(temp, $"bench_docdb_{id}.db");
|
||||
_docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal");
|
||||
|
||||
_singlePerson = CreatePerson(0);
|
||||
_batchData = new Person[BatchSize];
|
||||
for (int i = 0; i < BatchSize; i++)
|
||||
{
|
||||
_batchData[i] = CreatePerson(i);
|
||||
}
|
||||
@@ -60,7 +63,7 @@ public class InsertBenchmarks
|
||||
Bio = null, // Removed large payload to focus on structure
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Balance = 1000.50m * (i + 1),
|
||||
HomeAddress = new Address
|
||||
HomeAddress = new Address
|
||||
{
|
||||
Street = $"{i} Main St",
|
||||
City = "Tech City",
|
||||
@@ -83,51 +86,63 @@ public class InsertBenchmarks
|
||||
return p;
|
||||
}
|
||||
|
||||
[IterationSetup]
|
||||
public void IterationSetup()
|
||||
{
|
||||
_storage = new StorageEngine(_docDbPath, PageFileConfig.Default);
|
||||
_transactionHolder = new BenchmarkTransactionHolder(_storage);
|
||||
_collection = new DocumentCollection<Person>(_storage, _transactionHolder, new PersonMapper());
|
||||
}
|
||||
|
||||
[IterationCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Benchmark", nameof(InsertBenchmarks));
|
||||
_transactionHolder?.Dispose();
|
||||
_transactionHolder = null;
|
||||
_storage?.Dispose();
|
||||
_storage = null;
|
||||
|
||||
System.Threading.Thread.Sleep(100);
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Cleanup warning");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Benchmarks ---
|
||||
|
||||
[Benchmark(Baseline = true, Description = "CBDD Single Insert")]
|
||||
[BenchmarkCategory("Insert_Single")]
|
||||
public void DocumentDb_Insert_Single()
|
||||
{
|
||||
_collection?.Insert(_singlePerson!);
|
||||
_transactionHolder?.CommitAndReset();
|
||||
}
|
||||
|
||||
[Benchmark(Description = "CBDD Batch Insert (1000 items, 1 Txn)")]
|
||||
[BenchmarkCategory("Insert_Batch")]
|
||||
public void DocumentDb_Insert_Batch()
|
||||
{
|
||||
_collection?.InsertBulk(_batchData);
|
||||
_transactionHolder?.CommitAndReset();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Tests iteration setup.
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void IterationSetup()
|
||||
{
|
||||
_storage = new StorageEngine(_docDbPath, PageFileConfig.Default);
|
||||
_transactionHolder = new BenchmarkTransactionHolder(_storage);
|
||||
_collection = new DocumentCollection<Person>(_storage, _transactionHolder, new PersonMapper());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests cleanup.
|
||||
/// </summary>
|
||||
[IterationCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Benchmark", nameof(InsertBenchmarks));
|
||||
_transactionHolder?.Dispose();
|
||||
_transactionHolder = null;
|
||||
_storage?.Dispose();
|
||||
_storage = null;
|
||||
|
||||
System.Threading.Thread.Sleep(100);
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Cleanup warning");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Benchmarks ---
|
||||
|
||||
/// <summary>
|
||||
/// Tests document db insert single.
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true, Description = "CBDD Single Insert")]
|
||||
[BenchmarkCategory("Insert_Single")]
|
||||
public void DocumentDb_Insert_Single()
|
||||
{
|
||||
_collection?.Insert(_singlePerson!);
|
||||
_transactionHolder?.CommitAndReset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests document db insert batch.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "CBDD Batch Insert (1000 items, 1 Txn)")]
|
||||
[BenchmarkCategory("Insert_Batch")]
|
||||
public void DocumentDb_Insert_Batch()
|
||||
{
|
||||
_collection?.InsertBulk(_batchData);
|
||||
_transactionHolder?.CommitAndReset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,16 @@ internal static class Logging
|
||||
{
|
||||
private static readonly Lazy<ILoggerFactory> LoggerFactoryInstance = new(CreateFactory);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared logger factory for benchmarks.
|
||||
/// </summary>
|
||||
public static ILoggerFactory LoggerFactory => LoggerFactoryInstance.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a logger for the specified category type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The logger category type.</typeparam>
|
||||
/// <returns>A logger for <typeparamref name="T"/>.</returns>
|
||||
public static Microsoft.Extensions.Logging.ILogger CreateLogger<T>()
|
||||
{
|
||||
return LoggerFactory.CreateLogger<T>();
|
||||
|
||||
@@ -1,110 +1,114 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
|
||||
public class ManualBenchmark
|
||||
{
|
||||
private static StringBuilder _log = new();
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
private static void Log(ILogger logger, string message = "")
|
||||
{
|
||||
logger.LogInformation("{Message}", message);
|
||||
_log.AppendLine(message);
|
||||
}
|
||||
|
||||
public class ManualBenchmark
|
||||
{
|
||||
private static StringBuilder _log = new();
|
||||
|
||||
private static void Log(ILogger logger, string message = "")
|
||||
{
|
||||
logger.LogInformation("{Message}", message);
|
||||
_log.AppendLine(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests run.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger for benchmark progress and results.</param>
|
||||
public static void Run(ILogger logger)
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Benchmark", nameof(ManualBenchmark));
|
||||
_log.Clear();
|
||||
Log(logger, "=== MANUAL BENCHMARK: CBDD ===");
|
||||
Log(logger, $"Date: {DateTime.Now}");
|
||||
Log(logger, "Testing: Complex Objects (Nested Documents + Collections)\n");
|
||||
|
||||
long batchInsertMs;
|
||||
long singleInsertMs;
|
||||
long readByIdMs;
|
||||
|
||||
using (LogContext.PushProperty("Phase", "BatchInsert"))
|
||||
{
|
||||
Log(logger, "1. Batch Insert (1000 items)");
|
||||
var insertBench = new InsertBenchmarks();
|
||||
insertBench.Setup();
|
||||
insertBench.IterationSetup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
insertBench.DocumentDb_Insert_Batch();
|
||||
sw.Stop();
|
||||
batchInsertMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD InsertBulk (1000): {batchInsertMs} ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
insertBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
using (LogContext.PushProperty("Phase", "FindById"))
|
||||
{
|
||||
Log(logger, "\n2. FindById Performance (1000 operations)");
|
||||
var readBench = new ReadBenchmarks();
|
||||
readBench.Setup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
readBench.DocumentDb_FindById();
|
||||
}
|
||||
sw.Stop();
|
||||
readByIdMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD FindById x1000: {readByIdMs} ms ({(double)readByIdMs / 1000:F3} ms/op)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
readBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
using (LogContext.PushProperty("Phase", "SingleInsert"))
|
||||
{
|
||||
Log(logger, "\n3. Single Insert");
|
||||
var insertBench = new InsertBenchmarks();
|
||||
insertBench.Setup();
|
||||
insertBench.IterationSetup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
insertBench.DocumentDb_Insert_Single();
|
||||
sw.Stop();
|
||||
singleInsertMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD Single Insert: {singleInsertMs} ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
insertBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
Log(logger, "\n============================================================================");
|
||||
Log(logger, "BENCHMARK RESULTS (CBDD ONLY):");
|
||||
Log(logger, "============================================================================");
|
||||
Log(logger, $"Batch Insert (1000): {batchInsertMs} ms");
|
||||
Log(logger, $"FindById x1000: {readByIdMs} ms");
|
||||
Log(logger, $"Single Insert: {singleInsertMs} ms");
|
||||
|
||||
var artifactsDir = Path.Combine(AppContext.BaseDirectory, "BenchmarkDotNet.Artifacts", "results");
|
||||
if (!Directory.Exists(artifactsDir))
|
||||
{
|
||||
Directory.CreateDirectory(artifactsDir);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(artifactsDir, "manual_report.txt");
|
||||
File.WriteAllText(filePath, _log.ToString());
|
||||
logger.LogInformation("Report saved to: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Benchmark", nameof(ManualBenchmark));
|
||||
_log.Clear();
|
||||
Log(logger, "=== MANUAL BENCHMARK: CBDD ===");
|
||||
Log(logger, $"Date: {DateTime.Now}");
|
||||
Log(logger, "Testing: Complex Objects (Nested Documents + Collections)\n");
|
||||
|
||||
long batchInsertMs;
|
||||
long singleInsertMs;
|
||||
long readByIdMs;
|
||||
|
||||
using (LogContext.PushProperty("Phase", "BatchInsert"))
|
||||
{
|
||||
Log(logger, "1. Batch Insert (1000 items)");
|
||||
var insertBench = new InsertBenchmarks();
|
||||
insertBench.Setup();
|
||||
insertBench.IterationSetup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
insertBench.DocumentDb_Insert_Batch();
|
||||
sw.Stop();
|
||||
batchInsertMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD InsertBulk (1000): {batchInsertMs} ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
insertBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
using (LogContext.PushProperty("Phase", "FindById"))
|
||||
{
|
||||
Log(logger, "\n2. FindById Performance (1000 operations)");
|
||||
var readBench = new ReadBenchmarks();
|
||||
readBench.Setup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
readBench.DocumentDb_FindById();
|
||||
}
|
||||
sw.Stop();
|
||||
readByIdMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD FindById x1000: {readByIdMs} ms ({(double)readByIdMs / 1000:F3} ms/op)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
readBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
using (LogContext.PushProperty("Phase", "SingleInsert"))
|
||||
{
|
||||
Log(logger, "\n3. Single Insert");
|
||||
var insertBench = new InsertBenchmarks();
|
||||
insertBench.Setup();
|
||||
insertBench.IterationSetup();
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
insertBench.DocumentDb_Insert_Single();
|
||||
sw.Stop();
|
||||
singleInsertMs = sw.ElapsedMilliseconds;
|
||||
Log(logger, $" CBDD Single Insert: {singleInsertMs} ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
insertBench.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
Log(logger, "\n============================================================================");
|
||||
Log(logger, "BENCHMARK RESULTS (CBDD ONLY):");
|
||||
Log(logger, "============================================================================");
|
||||
Log(logger, $"Batch Insert (1000): {batchInsertMs} ms");
|
||||
Log(logger, $"FindById x1000: {readByIdMs} ms");
|
||||
Log(logger, $"Single Insert: {singleInsertMs} ms");
|
||||
|
||||
var artifactsDir = Path.Combine(AppContext.BaseDirectory, "BenchmarkDotNet.Artifacts", "results");
|
||||
if (!Directory.Exists(artifactsDir))
|
||||
{
|
||||
Directory.CreateDirectory(artifactsDir);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(artifactsDir, "manual_report.txt");
|
||||
File.WriteAllText(filePath, _log.ToString());
|
||||
logger.LogInformation("Report saved to: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,15 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
[JsonExporterAttribute.Full]
|
||||
public class MixedWorkloadBenchmarks
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether periodic online compaction is enabled.
|
||||
/// </summary>
|
||||
[Params(false, true)]
|
||||
public bool PeriodicCompaction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of operations per benchmark iteration.
|
||||
/// </summary>
|
||||
[Params(800)]
|
||||
public int Operations { get; set; }
|
||||
|
||||
@@ -30,6 +36,9 @@ public class MixedWorkloadBenchmarks
|
||||
private readonly List<ObjectId> _activeIds = [];
|
||||
private int _nextValueSeed;
|
||||
|
||||
/// <summary>
|
||||
/// Prepares benchmark storage and seed data for each iteration.
|
||||
/// </summary>
|
||||
[IterationSetup]
|
||||
public void Setup()
|
||||
{
|
||||
@@ -61,6 +70,9 @@ public class MixedWorkloadBenchmarks
|
||||
_transactionHolder.CommitAndReset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up benchmark resources for each iteration.
|
||||
/// </summary>
|
||||
[IterationCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
@@ -71,6 +83,9 @@ public class MixedWorkloadBenchmarks
|
||||
if (File.Exists(_walPath)) File.Delete(_walPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks a mixed insert/update/delete workload.
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
[BenchmarkCategory("MixedWorkload")]
|
||||
public int InsertUpdateDeleteMix()
|
||||
|
||||
287
tests/CBDD.Tests.Benchmark/PerformanceGateSmoke.cs
Normal file
287
tests/CBDD.Tests.Benchmark/PerformanceGateSmoke.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
internal static class PerformanceGateSmoke
|
||||
{
|
||||
private const int CompactionDocumentCount = 2_000;
|
||||
private const int CompressionDocumentCount = 1_500;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the performance gate smoke probes and writes a report.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public static void Run(ILogger logger)
|
||||
{
|
||||
var compaction = RunCompactionProbe();
|
||||
var compressionOff = RunCompressionGcProbe(enableCompression: false);
|
||||
var compressionOn = RunCompressionGcProbe(enableCompression: true);
|
||||
|
||||
var report = new PerformanceGateReport(
|
||||
DateTimeOffset.UtcNow,
|
||||
compaction,
|
||||
compressionOff,
|
||||
compressionOn);
|
||||
var reportPath = WriteReport(report);
|
||||
|
||||
logger.LogInformation("Performance gate smoke report written to {ReportPath}", reportPath);
|
||||
|
||||
Console.WriteLine("[performance_gate]");
|
||||
Console.WriteLine($"report_path={reportPath}");
|
||||
Console.WriteLine($"compaction.pre_pages={compaction.PrePages}");
|
||||
Console.WriteLine($"compaction.post_pages={compaction.PostPages}");
|
||||
Console.WriteLine($"compaction.reclaimed_file_bytes={compaction.ReclaimedFileBytes}");
|
||||
Console.WriteLine($"compaction.throughput_bytes_per_sec={compaction.ThroughputBytesPerSecond:F2}");
|
||||
Console.WriteLine($"compaction.throughput_pages_per_sec={compaction.ThroughputPagesPerSecond:F2}");
|
||||
Console.WriteLine($"compaction.throughput_docs_per_sec={compaction.ThroughputDocumentsPerSecond:F2}");
|
||||
Console.WriteLine($"compression_off.gen0_delta={compressionOff.Gen0Delta}");
|
||||
Console.WriteLine($"compression_off.gen1_delta={compressionOff.Gen1Delta}");
|
||||
Console.WriteLine($"compression_off.gen2_delta={compressionOff.Gen2Delta}");
|
||||
Console.WriteLine($"compression_off.alloc_bytes_delta={compressionOff.AllocatedBytesDelta}");
|
||||
Console.WriteLine($"compression_on.gen0_delta={compressionOn.Gen0Delta}");
|
||||
Console.WriteLine($"compression_on.gen1_delta={compressionOn.Gen1Delta}");
|
||||
Console.WriteLine($"compression_on.gen2_delta={compressionOn.Gen2Delta}");
|
||||
Console.WriteLine($"compression_on.alloc_bytes_delta={compressionOn.AllocatedBytesDelta}");
|
||||
}
|
||||
|
||||
private static CompactionProbeResult RunCompactionProbe()
|
||||
{
|
||||
var dbPath = NewDbPath("gate_compaction");
|
||||
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
||||
|
||||
try
|
||||
{
|
||||
using var storage = new StorageEngine(dbPath, PageFileConfig.Small);
|
||||
using var transactionHolder = new BenchmarkTransactionHolder(storage);
|
||||
var collection = new DocumentCollection<Person>(storage, transactionHolder, new PersonMapper());
|
||||
|
||||
var ids = new List<ObjectId>(CompactionDocumentCount);
|
||||
for (var i = 0; i < CompactionDocumentCount; i++)
|
||||
{
|
||||
ids.Add(collection.Insert(CreatePerson(i, includeLargeBio: true)));
|
||||
}
|
||||
|
||||
transactionHolder.CommitAndReset();
|
||||
storage.Checkpoint();
|
||||
|
||||
for (var i = 0; i < ids.Count; i += 3)
|
||||
{
|
||||
collection.Delete(ids[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < ids.Count; i += 5)
|
||||
{
|
||||
var current = collection.FindById(ids[i]);
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
current.Bio = BuildBio(i + 10_000);
|
||||
current.Age += 1;
|
||||
collection.Update(current);
|
||||
}
|
||||
|
||||
transactionHolder.CommitAndReset();
|
||||
storage.Checkpoint();
|
||||
|
||||
var stats = storage.Compact(new CompactionOptions
|
||||
{
|
||||
OnlineMode = false,
|
||||
DefragmentSlottedPages = true,
|
||||
NormalizeFreeList = true,
|
||||
EnableTailTruncation = true
|
||||
});
|
||||
|
||||
return new CompactionProbeResult(
|
||||
stats.PrePageCount,
|
||||
stats.PostPageCount,
|
||||
stats.ReclaimedFileBytes,
|
||||
stats.ThroughputBytesPerSecond,
|
||||
stats.ThroughputPagesPerSecond,
|
||||
stats.ThroughputDocumentsPerSecond);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dbPath);
|
||||
TryDelete(walPath);
|
||||
TryDelete($"{dbPath}.compact.state");
|
||||
TryDelete($"{dbPath}.compact.tmp");
|
||||
TryDelete($"{dbPath}.compact.bak");
|
||||
}
|
||||
}
|
||||
|
||||
private static CompressionGcProbeResult RunCompressionGcProbe(bool enableCompression)
|
||||
{
|
||||
var dbPath = NewDbPath(enableCompression ? "gate_gc_on" : "gate_gc_off");
|
||||
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
||||
var compressionOptions = enableCompression
|
||||
? new CompressionOptions
|
||||
{
|
||||
EnableCompression = true,
|
||||
MinSizeBytes = 256,
|
||||
MinSavingsPercent = 0,
|
||||
Codec = CompressionCodec.Brotli,
|
||||
Level = CompressionLevel.Fastest
|
||||
}
|
||||
: CompressionOptions.Default;
|
||||
|
||||
try
|
||||
{
|
||||
using var storage = new StorageEngine(dbPath, PageFileConfig.Default, compressionOptions);
|
||||
using var transactionHolder = new BenchmarkTransactionHolder(storage);
|
||||
var collection = new DocumentCollection<Person>(storage, transactionHolder, new PersonMapper());
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var g0Before = GC.CollectionCount(0);
|
||||
var g1Before = GC.CollectionCount(1);
|
||||
var g2Before = GC.CollectionCount(2);
|
||||
var allocBefore = GC.GetTotalAllocatedBytes(true);
|
||||
|
||||
var ids = new ObjectId[CompressionDocumentCount];
|
||||
for (var i = 0; i < CompressionDocumentCount; i++)
|
||||
{
|
||||
ids[i] = collection.Insert(CreatePerson(i, includeLargeBio: true));
|
||||
}
|
||||
|
||||
transactionHolder.CommitAndReset();
|
||||
|
||||
for (var i = 0; i < ids.Length; i += 4)
|
||||
{
|
||||
var current = collection.FindById(ids[i]);
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
current.Bio = BuildBio(i + 20_000);
|
||||
current.Age += 1;
|
||||
collection.Update(current);
|
||||
}
|
||||
|
||||
transactionHolder.CommitAndReset();
|
||||
|
||||
var readCount = collection.FindAll().Count();
|
||||
transactionHolder.CommitAndReset();
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var g0After = GC.CollectionCount(0);
|
||||
var g1After = GC.CollectionCount(1);
|
||||
var g2After = GC.CollectionCount(2);
|
||||
var allocAfter = GC.GetTotalAllocatedBytes(true);
|
||||
|
||||
return new CompressionGcProbeResult(
|
||||
enableCompression,
|
||||
readCount,
|
||||
g0After - g0Before,
|
||||
g1After - g1Before,
|
||||
g2After - g2Before,
|
||||
allocAfter - allocBefore);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dbPath);
|
||||
TryDelete(walPath);
|
||||
TryDelete($"{dbPath}.compact.state");
|
||||
TryDelete($"{dbPath}.compact.tmp");
|
||||
TryDelete($"{dbPath}.compact.bak");
|
||||
}
|
||||
}
|
||||
|
||||
private static string WriteReport(PerformanceGateReport report)
|
||||
{
|
||||
var outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results");
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
|
||||
var reportPath = Path.Combine(outputDirectory, "PerformanceGateSmoke-report.json");
|
||||
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(reportPath, json);
|
||||
return reportPath;
|
||||
}
|
||||
|
||||
private static Person CreatePerson(int i, bool includeLargeBio)
|
||||
{
|
||||
return new Person
|
||||
{
|
||||
Id = ObjectId.NewObjectId(),
|
||||
FirstName = $"First_{i}",
|
||||
LastName = $"Last_{i}",
|
||||
Age = 20 + (i % 50),
|
||||
Bio = includeLargeBio ? BuildBio(i) : $"bio-{i}",
|
||||
CreatedAt = DateTime.UnixEpoch.AddMinutes(i),
|
||||
Balance = 100 + i,
|
||||
HomeAddress = new Address
|
||||
{
|
||||
Street = $"{i} Main St",
|
||||
City = "Gate City",
|
||||
ZipCode = "12345"
|
||||
},
|
||||
EmploymentHistory =
|
||||
[
|
||||
new WorkHistory
|
||||
{
|
||||
CompanyName = $"Company_{i}",
|
||||
Title = "Engineer",
|
||||
DurationYears = i % 10,
|
||||
Tags = ["csharp", "db", "compression"]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildBio(int seed)
|
||||
{
|
||||
var builder = new System.Text.StringBuilder(4500);
|
||||
for (var i = 0; i < 150; i++)
|
||||
{
|
||||
builder.Append("bio-");
|
||||
builder.Append(seed.ToString("D6"));
|
||||
builder.Append('-');
|
||||
builder.Append(i.ToString("D3"));
|
||||
builder.Append('|');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string NewDbPath(string prefix)
|
||||
=> Path.Combine(Path.GetTempPath(), $"{prefix}_{Guid.NewGuid():N}.db");
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PerformanceGateReport(
|
||||
DateTimeOffset CapturedAtUtc,
|
||||
CompactionProbeResult Compaction,
|
||||
CompressionGcProbeResult CompressionOff,
|
||||
CompressionGcProbeResult CompressionOn);
|
||||
|
||||
private sealed record CompactionProbeResult(
|
||||
uint PrePages,
|
||||
uint PostPages,
|
||||
long ReclaimedFileBytes,
|
||||
double ThroughputBytesPerSecond,
|
||||
double ThroughputPagesPerSecond,
|
||||
double ThroughputDocumentsPerSecond);
|
||||
|
||||
private sealed record CompressionGcProbeResult(
|
||||
bool CompressionEnabled,
|
||||
int ReadCount,
|
||||
int Gen0Delta,
|
||||
int Gen1Delta,
|
||||
int Gen2Delta,
|
||||
long AllocatedBytesDelta);
|
||||
}
|
||||
@@ -6,30 +6,78 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
public class Address
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Street.
|
||||
/// </summary>
|
||||
public string Street { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the City.
|
||||
/// </summary>
|
||||
public string City { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the ZipCode.
|
||||
/// </summary>
|
||||
public string ZipCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class WorkHistory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the CompanyName.
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the Title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the DurationYears.
|
||||
/// </summary>
|
||||
public int DurationYears { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Tags.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class Person
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the FirstName.
|
||||
/// </summary>
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the LastName.
|
||||
/// </summary>
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the Age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Bio.
|
||||
/// </summary>
|
||||
public string? Bio { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
// Complex fields
|
||||
/// <summary>
|
||||
/// Gets or sets the CreatedAt.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
// Complex fields
|
||||
/// <summary>
|
||||
/// Gets or sets the Balance.
|
||||
/// </summary>
|
||||
public decimal Balance { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the HomeAddress.
|
||||
/// </summary>
|
||||
public Address HomeAddress { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the EmploymentHistory.
|
||||
/// </summary>
|
||||
public List<WorkHistory> EmploymentHistory { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -5,16 +5,20 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
public class PersonMapper : ObjectIdMapperBase<Person>
|
||||
{
|
||||
public override string CollectionName => "people";
|
||||
|
||||
public override ObjectId GetId(Person entity) => entity.Id;
|
||||
|
||||
public override void SetId(Person entity, ObjectId id) => entity.Id = id;
|
||||
|
||||
public override int Serialize(Person entity, BsonSpanWriter writer)
|
||||
{
|
||||
public class PersonMapper : ObjectIdMapperBase<Person>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string CollectionName => "people";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ObjectId GetId(Person entity) => entity.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetId(Person entity, ObjectId id) => entity.Id = id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Serialize(Person entity, BsonSpanWriter writer)
|
||||
{
|
||||
var sizePos = writer.BeginDocument();
|
||||
|
||||
writer.WriteObjectId("_id", entity.Id);
|
||||
@@ -67,8 +71,9 @@ public class PersonMapper : ObjectIdMapperBase<Person>
|
||||
return writer.Position;
|
||||
}
|
||||
|
||||
public override Person Deserialize(BsonSpanReader reader)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Person Deserialize(BsonSpanReader reader)
|
||||
{
|
||||
var person = new Person();
|
||||
|
||||
reader.ReadDocumentSize();
|
||||
|
||||
@@ -50,6 +50,13 @@ class Program
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == "gate")
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Mode", "PerformanceGateSmoke");
|
||||
PerformanceGateSmoke.Run(logger);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == "all")
|
||||
{
|
||||
using var _ = LogContext.PushProperty("Mode", "AllBenchmarks");
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
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.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
using System.IO;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
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.Storage;
|
||||
using ZB.MOM.WW.CBDD.Core.Transactions;
|
||||
using System.IO;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
|
||||
[SimpleJob]
|
||||
[InProcess]
|
||||
@@ -18,53 +18,59 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark;
|
||||
[JsonExporterAttribute.Full]
|
||||
public class ReadBenchmarks
|
||||
{
|
||||
private const int DocCount = 1000;
|
||||
|
||||
private string _docDbPath = null!;
|
||||
private string _docDbWalPath = null!;
|
||||
|
||||
private StorageEngine _storage = null!;
|
||||
private BenchmarkTransactionHolder _transactionHolder = null!;
|
||||
private DocumentCollection<Person> _collection = null!;
|
||||
|
||||
private ObjectId[] _ids = null!;
|
||||
private ObjectId _targetId;
|
||||
private const int DocCount = 1000;
|
||||
|
||||
private string _docDbPath = null!;
|
||||
private string _docDbWalPath = null!;
|
||||
|
||||
private StorageEngine _storage = null!;
|
||||
private BenchmarkTransactionHolder _transactionHolder = null!;
|
||||
private DocumentCollection<Person> _collection = null!;
|
||||
|
||||
private ObjectId[] _ids = null!;
|
||||
private ObjectId _targetId;
|
||||
|
||||
/// <summary>
|
||||
/// Tests setup.
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var temp = AppContext.BaseDirectory;
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
_docDbPath = Path.Combine(temp, $"bench_read_docdb_{id}.db");
|
||||
_docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal");
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
|
||||
_storage = new StorageEngine(_docDbPath, PageFileConfig.Default);
|
||||
_transactionHolder = new BenchmarkTransactionHolder(_storage);
|
||||
_collection = new DocumentCollection<Person>(_storage, _transactionHolder, new PersonMapper());
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
_docDbPath = Path.Combine(temp, $"bench_read_docdb_{id}.db");
|
||||
_docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal");
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
|
||||
_storage = new StorageEngine(_docDbPath, PageFileConfig.Default);
|
||||
_transactionHolder = new BenchmarkTransactionHolder(_storage);
|
||||
_collection = new DocumentCollection<Person>(_storage, _transactionHolder, new PersonMapper());
|
||||
|
||||
_ids = new ObjectId[DocCount];
|
||||
for (int i = 0; i < DocCount; i++)
|
||||
{
|
||||
var p = CreatePerson(i);
|
||||
_ids[i] = _collection.Insert(p);
|
||||
}
|
||||
_transactionHolder.CommitAndReset();
|
||||
|
||||
_targetId = _ids[DocCount / 2];
|
||||
}
|
||||
|
||||
for (int i = 0; i < DocCount; i++)
|
||||
{
|
||||
var p = CreatePerson(i);
|
||||
_ids[i] = _collection.Insert(p);
|
||||
}
|
||||
_transactionHolder.CommitAndReset();
|
||||
|
||||
_targetId = _ids[DocCount / 2];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests cleanup.
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_transactionHolder?.Dispose();
|
||||
_storage?.Dispose();
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
}
|
||||
public void Cleanup()
|
||||
{
|
||||
_transactionHolder?.Dispose();
|
||||
_storage?.Dispose();
|
||||
|
||||
if (File.Exists(_docDbPath)) File.Delete(_docDbPath);
|
||||
if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath);
|
||||
}
|
||||
|
||||
private Person CreatePerson(int i)
|
||||
{
|
||||
@@ -77,7 +83,7 @@ public class ReadBenchmarks
|
||||
Bio = null,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Balance = 1000.50m * (i + 1),
|
||||
HomeAddress = new Address
|
||||
HomeAddress = new Address
|
||||
{
|
||||
Street = $"{i} Main St",
|
||||
City = "Tech City",
|
||||
@@ -100,10 +106,13 @@ public class ReadBenchmarks
|
||||
return p;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true, Description = "CBDD FindById")]
|
||||
[BenchmarkCategory("Read_Single")]
|
||||
public Person? DocumentDb_FindById()
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests document db find by id.
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true, Description = "CBDD FindById")]
|
||||
[BenchmarkCategory("Read_Single")]
|
||||
public Person? DocumentDb_FindById()
|
||||
{
|
||||
return _collection.FindById(_targetId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SerializationBenchmarks
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ushort, string> _keys = new();
|
||||
|
||||
static SerializationBenchmarks()
|
||||
static SerializationBenchmarks()
|
||||
{
|
||||
ushort id = 1;
|
||||
string[] initialKeys = { "_id", "firstname", "lastname", "age", "bio", "createdat", "balance", "homeaddress", "street", "city", "zipcode", "employmenthistory", "companyname", "title", "durationyears", "tags" };
|
||||
@@ -47,8 +47,11 @@ public class SerializationBenchmarks
|
||||
}
|
||||
}
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
/// <summary>
|
||||
/// Prepares benchmark data for serialization and deserialization scenarios.
|
||||
/// </summary>
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_person = CreatePerson(0);
|
||||
_people = new List<Person>(BatchSize);
|
||||
@@ -108,39 +111,54 @@ public class SerializationBenchmarks
|
||||
return p;
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Serialize Single (BSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public void Serialize_Bson()
|
||||
/// <summary>
|
||||
/// Benchmarks BSON serialization for a single document.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Serialize Single (BSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public void Serialize_Bson()
|
||||
{
|
||||
var writer = new BsonSpanWriter(_serializeBuffer, _keyMap);
|
||||
_mapper.Serialize(_person, writer);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Serialize Single (JSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public void Serialize_Json()
|
||||
/// <summary>
|
||||
/// Benchmarks JSON serialization for a single document.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Serialize Single (JSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public void Serialize_Json()
|
||||
{
|
||||
JsonSerializer.SerializeToUtf8Bytes(_person);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Deserialize Single (BSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public Person Deserialize_Bson()
|
||||
/// <summary>
|
||||
/// Benchmarks BSON deserialization for a single document.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Deserialize Single (BSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public Person Deserialize_Bson()
|
||||
{
|
||||
var reader = new BsonSpanReader(_bsonData, _keys);
|
||||
return _mapper.Deserialize(reader);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Deserialize Single (JSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public Person? Deserialize_Json()
|
||||
/// <summary>
|
||||
/// Benchmarks JSON deserialization for a single document.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Deserialize Single (JSON)")]
|
||||
[BenchmarkCategory("Single")]
|
||||
public Person? Deserialize_Json()
|
||||
{
|
||||
return JsonSerializer.Deserialize<Person>(_jsonData);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Serialize List 10k (BSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Serialize_List_Bson()
|
||||
/// <summary>
|
||||
/// Benchmarks BSON serialization for a list of documents.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Serialize List 10k (BSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Serialize_List_Bson()
|
||||
{
|
||||
foreach (var p in _people)
|
||||
{
|
||||
@@ -149,9 +167,12 @@ public class SerializationBenchmarks
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Serialize List 10k (JSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Serialize_List_Json()
|
||||
/// <summary>
|
||||
/// Benchmarks JSON serialization for a list of documents.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Serialize List 10k (JSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Serialize_List_Json()
|
||||
{
|
||||
foreach (var p in _people)
|
||||
{
|
||||
@@ -159,9 +180,12 @@ public class SerializationBenchmarks
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Deserialize List 10k (BSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Deserialize_List_Bson()
|
||||
/// <summary>
|
||||
/// Benchmarks BSON deserialization for a list of documents.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Deserialize List 10k (BSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Deserialize_List_Bson()
|
||||
{
|
||||
foreach (var data in _bsonDataList)
|
||||
{
|
||||
@@ -170,9 +194,12 @@ public class SerializationBenchmarks
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Deserialize List 10k (JSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Deserialize_List_Json()
|
||||
/// <summary>
|
||||
/// Benchmarks JSON deserialization for a list of documents.
|
||||
/// </summary>
|
||||
[Benchmark(Description = "Deserialize List 10k (JSON loop)")]
|
||||
[BenchmarkCategory("Batch")]
|
||||
public void Deserialize_List_Json()
|
||||
{
|
||||
foreach (var data in _jsonDataList)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes test database state used by advanced query tests.
|
||||
/// </summary>
|
||||
public AdvancedQueryTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_advanced_{Guid.NewGuid()}.db");
|
||||
@@ -30,12 +33,18 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies grouping by a simple key returns expected groups and counts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GroupBy_Simple_Key_Works()
|
||||
{
|
||||
@@ -57,6 +66,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
groupC.Count().ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies grouped projection with aggregation returns expected totals.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GroupBy_With_Aggregation_Select()
|
||||
{
|
||||
@@ -77,6 +89,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results[2].Total.ShouldBe(50); // 50
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies direct aggregate operators return expected values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Aggregations_Direct_Works()
|
||||
{
|
||||
@@ -89,6 +104,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
query.Max(x => x.Amount).ShouldBe(50);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies aggregate operators with predicates return expected values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Aggregations_With_Predicate_Works()
|
||||
{
|
||||
@@ -98,6 +116,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
query.Sum(x => x.Amount).ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies in-memory join query execution returns expected rows.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Join_Works_InMemory()
|
||||
{
|
||||
@@ -126,6 +147,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Verifies projection of nested object properties works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Select_Project_Nested_Object()
|
||||
{
|
||||
@@ -152,6 +176,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
query[0].Street.ShouldBe("5th Ave");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies projection of nested scalar fields works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Select_Project_Nested_Field()
|
||||
{
|
||||
@@ -172,6 +199,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
cities[0].ShouldBe("New York");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies anonymous projection including nested values works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Select_Anonymous_Complex()
|
||||
{
|
||||
@@ -196,6 +226,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result[0].City.Name.ShouldBe("New York");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies projection and retrieval of nested arrays of objects works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Select_Project_Nested_Array_Of_Objects()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ public class ArchitectureFitnessTests
|
||||
private const string SourceGeneratorsProject = "src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj";
|
||||
private const string FacadeProject = "src/CBDD/ZB.MOM.WW.CBDD.csproj";
|
||||
|
||||
/// <summary>
|
||||
/// Executes Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection()
|
||||
{
|
||||
@@ -40,6 +43,9 @@ public class ArchitectureFitnessTests
|
||||
.ShouldBeFalse("Project references must remain acyclic.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface()
|
||||
{
|
||||
@@ -65,6 +71,9 @@ public class ArchitectureFitnessTests
|
||||
dbContextOffenders.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes CollectionAndIndexOrchestration_ShouldUseStoragePortInternally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CollectionAndIndexOrchestration_ShouldUseStoragePortInternally()
|
||||
{
|
||||
|
||||
@@ -11,17 +11,26 @@ public class AsyncTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncTests"/> class.
|
||||
/// </summary>
|
||||
public AsyncTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_async_{Guid.NewGuid()}.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
if (File.Exists(Path.ChangeExtension(_dbPath, ".wal"))) File.Delete(Path.ChangeExtension(_dbPath, ".wal"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Async_Transaction_Commit_Should_Persist_Data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Async_Transaction_Commit_Should_Persist_Data()
|
||||
{
|
||||
@@ -48,6 +57,9 @@ public class AsyncTests : IDisposable
|
||||
doc2.Name.ShouldBe("Async2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Async_Transaction_Rollback_Should_Discard_Data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Async_Transaction_Rollback_Should_Discard_Data()
|
||||
{
|
||||
@@ -63,6 +75,9 @@ public class AsyncTests : IDisposable
|
||||
doc.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Bulk_Async_Insert_Should_Persist_Data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Bulk_Async_Insert_Should_Persist_Data()
|
||||
{
|
||||
@@ -78,6 +93,9 @@ public class AsyncTests : IDisposable
|
||||
doc50.Name.ShouldBe("Bulk50");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Bulk_Async_Update_Should_Persist_Changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Bulk_Async_Update_Should_Persist_Changes()
|
||||
{
|
||||
@@ -102,6 +120,9 @@ public class AsyncTests : IDisposable
|
||||
doc50.Name.ShouldBe("Updated50");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes High_Concurrency_Async_Commits.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task High_Concurrency_Async_Commits()
|
||||
{
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<ushort, string> _keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes lookup maps used by attribute mapper tests.
|
||||
/// </summary>
|
||||
public AttributeTests()
|
||||
{
|
||||
ushort id = 1;
|
||||
@@ -25,6 +28,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies table attribute mapping resolves the expected collection name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Table_Attribute_Mapping()
|
||||
{
|
||||
@@ -33,6 +39,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
mapper.CollectionName.ShouldBe("test.custom_users");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies required attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Required_Validation()
|
||||
{
|
||||
@@ -52,6 +61,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for empty Name.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies string length attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_StringLength_Validation()
|
||||
{
|
||||
@@ -69,6 +81,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Name too long.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies range attribute validation is enforced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Range_Validation()
|
||||
{
|
||||
@@ -81,6 +96,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
thrown.ShouldBeTrue("Should throw ValidationException for Age out of range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies column attribute maps to the expected BSON field name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Column_Name_Mapping()
|
||||
{
|
||||
@@ -108,6 +126,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
foundDisplayName.ShouldBeTrue("BSON field name should be 'display_name' from [Column] attribute.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies not-mapped attribute excludes properties from BSON serialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_NotMapped_Attribute()
|
||||
{
|
||||
|
||||
@@ -6,16 +6,25 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
{
|
||||
private const string DbPath = "autoinit.db";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoInitTests"/> class.
|
||||
/// </summary>
|
||||
public AutoInitTests()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies generated collection initializers set up collections automatically.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Collections_Are_Initialized_By_Generator()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class BTreeDeleteUnderflowTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class BsonDocumentAndBufferWriterTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies BSON document creation and typed retrieval roundtrip.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonDocument_Create_And_TryGet_RoundTrip()
|
||||
{
|
||||
@@ -42,6 +45,9 @@ public class BsonDocumentAndBufferWriterTests
|
||||
reader.ReadDocumentSize().ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies typed getters return false for missing fields and type mismatches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonDocument_TryGet_Should_Return_False_For_Missing_Or_Wrong_Type()
|
||||
{
|
||||
@@ -64,6 +70,9 @@ public class BsonDocumentAndBufferWriterTests
|
||||
wrapped.TryGetObjectId("age", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the BSON document builder grows its internal buffer for large documents.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonDocumentBuilder_Should_Grow_Buffer_When_Document_Is_Large()
|
||||
{
|
||||
@@ -90,6 +99,9 @@ public class BsonDocumentAndBufferWriterTests
|
||||
value.ShouldBe(180);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies BSON buffer writer emits expected nested document and array layout.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonBufferWriter_Should_Write_Nested_Document_And_Array()
|
||||
{
|
||||
@@ -151,6 +163,9 @@ public class BsonDocumentAndBufferWriterTests
|
||||
reader.ReadBsonType().ShouldBe(BsonType.EndOfDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies single-byte and C-string span reads operate correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonSpanReader_ReadByte_And_ReadCStringSpan_Should_Work()
|
||||
{
|
||||
|
||||
@@ -11,12 +11,30 @@ public class BsonSchemaTests
|
||||
{
|
||||
public class SimpleEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the entity is active.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema generation for a simple entity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_SimpleEntity()
|
||||
{
|
||||
@@ -37,10 +55,20 @@ public class BsonSchemaTests
|
||||
|
||||
public class CollectionEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets tags.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets scores.
|
||||
/// </summary>
|
||||
public int[] Scores { get; set; } = Array.Empty<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema generation for collection fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_Collections()
|
||||
{
|
||||
@@ -57,9 +85,15 @@ public class BsonSchemaTests
|
||||
|
||||
public class NestedEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the parent entity.
|
||||
/// </summary>
|
||||
public SimpleEntity Parent { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema generation for nested document fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_Nested()
|
||||
{
|
||||
@@ -73,9 +107,15 @@ public class BsonSchemaTests
|
||||
|
||||
public class ComplexCollectionEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets items.
|
||||
/// </summary>
|
||||
public List<SimpleEntity> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema generation for collections of complex types.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_ComplexCollection()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ public class BsonSpanReaderWriterTests
|
||||
private readonly ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<ushort, string> _keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BsonSpanReaderWriterTests"/> class.
|
||||
/// </summary>
|
||||
public BsonSpanReaderWriterTests()
|
||||
{
|
||||
ushort id = 1;
|
||||
@@ -21,6 +24,9 @@ public class BsonSpanReaderWriterTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read simple document.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_SimpleDocument()
|
||||
{
|
||||
@@ -65,6 +71,9 @@ public class BsonSpanReaderWriterTests
|
||||
value3.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read object id.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_ObjectId()
|
||||
{
|
||||
@@ -90,6 +99,9 @@ public class BsonSpanReaderWriterTests
|
||||
readOid.ShouldBe(oid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests read write double.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ReadWrite_Double()
|
||||
{
|
||||
@@ -108,6 +120,9 @@ public class BsonSpanReaderWriterTests
|
||||
val.ShouldBe(123.456);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests read write decimal128 round trip.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ReadWrite_Decimal128_RoundTrip()
|
||||
{
|
||||
@@ -127,6 +142,9 @@ public class BsonSpanReaderWriterTests
|
||||
val.ShouldBe(original);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read date time.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_DateTime()
|
||||
{
|
||||
@@ -155,6 +173,9 @@ public class BsonSpanReaderWriterTests
|
||||
readTime.ShouldBe(expectedTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read numeric types.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_NumericTypes()
|
||||
{
|
||||
@@ -185,6 +206,9 @@ public class BsonSpanReaderWriterTests
|
||||
Math.Round(reader.ReadDouble(), 5).ShouldBe(Math.Round(3.14159, 5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read binary.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_Binary()
|
||||
{
|
||||
@@ -211,6 +235,9 @@ public class BsonSpanReaderWriterTests
|
||||
testData.AsSpan().SequenceEqual(readData).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests write and read nested document.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WriteAndRead_NestedDocument()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ public class BulkOperationsTests : IDisposable
|
||||
private readonly string _walPath;
|
||||
private readonly Shared.TestDbContext _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BulkOperationsTests"/> class.
|
||||
/// </summary>
|
||||
public BulkOperationsTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_bulk_{Guid.NewGuid()}.db");
|
||||
@@ -23,11 +26,17 @@ public class BulkOperationsTests : IDisposable
|
||||
_dbContext = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_dbContext.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes UpdateBulk_UpdatesMultipleDocuments.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UpdateBulk_UpdatesMultipleDocuments()
|
||||
{
|
||||
@@ -64,6 +73,9 @@ public class BulkOperationsTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes DeleteBulk_RemovesMultipleDocuments.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DeleteBulk_RemovesMultipleDocuments()
|
||||
{
|
||||
@@ -103,6 +115,9 @@ public class BulkOperationsTests : IDisposable
|
||||
_dbContext.Users.FindAll().Count().ShouldBe(50);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes DeleteBulk_WithTransaction_Rollworks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DeleteBulk_WithTransaction_Rollworks()
|
||||
{
|
||||
|
||||
@@ -13,12 +13,18 @@ public class CdcScalabilityTests : IDisposable
|
||||
private readonly Shared.TestDbContext _db;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CdcScalabilityTests"/> class.
|
||||
/// </summary>
|
||||
public CdcScalabilityTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cdc_scaling_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies CDC dispatch reaches all registered subscribers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Test_Cdc_1000_Subscribers_Receive_Events()
|
||||
{
|
||||
@@ -55,6 +61,9 @@ public class CdcScalabilityTests : IDisposable
|
||||
foreach (var sub in subscriptions) sub.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a slow subscriber does not block other subscribers.
|
||||
/// </summary>
|
||||
[Fact(Skip = "Performance test - run manually when needed")]
|
||||
public async Task Test_Cdc_Slow_Subscriber_Does_Not_Block_Others()
|
||||
{
|
||||
@@ -99,6 +108,9 @@ public class CdcScalabilityTests : IDisposable
|
||||
slowEventCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -21,11 +21,17 @@ public class CdcTests : IDisposable
|
||||
private readonly string _dbPath = $"cdc_test_{Guid.NewGuid()}.db";
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CdcTests"/> class.
|
||||
/// </summary>
|
||||
public CdcTests()
|
||||
{
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that an insert operation publishes a CDC event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Test_Cdc_Basic_Insert_Fires_Event()
|
||||
{
|
||||
@@ -47,6 +53,9 @@ public class CdcTests : IDisposable
|
||||
snapshot[0].Entity!.Name.ShouldBe("John");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies payload is omitted when CDC capture payload is disabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Test_Cdc_No_Payload_When_Not_Requested()
|
||||
{
|
||||
@@ -65,6 +74,9 @@ public class CdcTests : IDisposable
|
||||
snapshot[0].Entity.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies CDC events are published only for committed changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Test_Cdc_Commit_Only()
|
||||
{
|
||||
@@ -95,6 +107,9 @@ public class CdcTests : IDisposable
|
||||
snapshot[0].DocumentId.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update and delete operations publish CDC events.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Test_Cdc_Update_And_Delete()
|
||||
{
|
||||
@@ -125,6 +140,9 @@ public class CdcTests : IDisposable
|
||||
snapshot[2].DocumentId.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
@@ -155,6 +173,13 @@ public class CdcTests : IDisposable
|
||||
// Simple helper to avoid System.Reactive dependency in tests
|
||||
public static class ObservableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribes to an observable sequence using an action callback.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The event type.</typeparam>
|
||||
/// <param name="observable">The observable sequence.</param>
|
||||
/// <param name="onNext">The callback for next events.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> subscription.</returns>
|
||||
public static IDisposable Subscribe<T>(this IObservable<T> observable, Action<T> onNext)
|
||||
{
|
||||
return observable.Subscribe(new AnonymousObserver<T>(onNext));
|
||||
@@ -163,9 +188,28 @@ public static class ObservableExtensions
|
||||
private class AnonymousObserver<T> : IObserver<T>
|
||||
{
|
||||
private readonly Action<T> _onNext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AnonymousObserver{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="onNext">The callback for next events.</param>
|
||||
public AnonymousObserver(Action<T> onNext) => _onNext = onNext;
|
||||
|
||||
/// <summary>
|
||||
/// Handles completion.
|
||||
/// </summary>
|
||||
public void OnCompleted() { }
|
||||
|
||||
/// <summary>
|
||||
/// Handles an observable error.
|
||||
/// </summary>
|
||||
/// <param name="error">The observed error.</param>
|
||||
public void OnError(Exception error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Handles the next value.
|
||||
/// </summary>
|
||||
/// <param name="value">The observed value.</param>
|
||||
public void OnNext(T value) => _onNext(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,18 @@ public class CircularReferenceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CircularReferenceTests"/> class.
|
||||
/// </summary>
|
||||
public CircularReferenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}");
|
||||
_context = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_context?.Dispose();
|
||||
@@ -39,6 +45,9 @@ public class CircularReferenceTests : IDisposable
|
||||
// Self-Reference Tests (Employee hierarchy with ObjectId references)
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_InsertAndQuery_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_InsertAndQuery_ShouldWork()
|
||||
{
|
||||
@@ -115,6 +124,9 @@ public class CircularReferenceTests : IDisposable
|
||||
(queriedDeveloper.DirectReportIds ?? new List<ObjectId>()).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_UpdateDirectReports_ShouldPersist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_UpdateDirectReports_ShouldPersist()
|
||||
{
|
||||
@@ -164,6 +176,9 @@ public class CircularReferenceTests : IDisposable
|
||||
queried.DirectReportIds.ShouldContain(employee2Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes SelfReference_QueryByManagerId_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SelfReference_QueryByManagerId_ShouldWork()
|
||||
{
|
||||
@@ -214,6 +229,9 @@ public class CircularReferenceTests : IDisposable
|
||||
// BEST PRACTICE for document databases
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_InsertAndQuery_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_InsertAndQuery_ShouldWork()
|
||||
{
|
||||
@@ -279,6 +297,9 @@ public class CircularReferenceTests : IDisposable
|
||||
queriedProduct.CategoryIds.ShouldContain(categoryId2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_UpdateRelationships_ShouldPersist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_UpdateRelationships_ShouldPersist()
|
||||
{
|
||||
@@ -336,6 +357,9 @@ public class CircularReferenceTests : IDisposable
|
||||
queriedProduct2.CategoryIds.ShouldContain(categoryId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_DocumentSize_RemainSmall.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_DocumentSize_RemainSmall()
|
||||
{
|
||||
@@ -365,6 +389,9 @@ public class CircularReferenceTests : IDisposable
|
||||
// This demonstrates why referencing is preferred for large N-N relationships
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes NtoNReferencing_QueryByProductId_ShouldWork.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NtoNReferencing_QueryByProductId_ShouldWork()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CollectionIndexManagerAndDefinitionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests find best index should prefer unique index.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindBestIndex_Should_Prefer_Unique_Index()
|
||||
{
|
||||
@@ -32,6 +35,9 @@ public class CollectionIndexManagerAndDefinitionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests find best compound index should choose longest prefix.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindBestCompoundIndex_Should_Choose_Longest_Prefix()
|
||||
{
|
||||
@@ -69,6 +75,9 @@ public class CollectionIndexManagerAndDefinitionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests drop index should remove metadata and be idempotent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DropIndex_Should_Remove_Metadata_And_Be_Idempotent()
|
||||
{
|
||||
@@ -97,6 +106,9 @@ public class CollectionIndexManagerAndDefinitionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests collection index definition should respect query support rules.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CollectionIndexDefinition_Should_Respect_Query_Support_Rules()
|
||||
{
|
||||
@@ -116,6 +128,9 @@ public class CollectionIndexManagerAndDefinitionTests
|
||||
definition.ToString().ShouldContain("Name");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests collection index info to string should include diagnostics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CollectionIndexInfo_ToString_Should_Include_Diagnostics()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,10 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompactionCrashRecoveryTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies compaction resumes from marker phases and preserves data.
|
||||
/// </summary>
|
||||
/// <param name="phase">The crash marker phase to resume from.</param>
|
||||
[Theory]
|
||||
[InlineData("Started")]
|
||||
[InlineData("Copied")]
|
||||
@@ -49,6 +53,9 @@ public class CompactionCrashRecoveryTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies corrupted compaction markers are recovered deterministically.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResumeCompaction_WithCorruptedMarker_ShouldRecoverDeterministically()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompactionOfflineTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests offline compact should preserve logical data equivalence.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_ShouldPreserveLogicalDataEquivalence()
|
||||
{
|
||||
@@ -72,6 +75,9 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact should keep index results consistent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_ShouldKeepIndexResultsConsistent()
|
||||
{
|
||||
@@ -127,6 +133,83 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact should rebuild hash index metadata and preserve results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_ShouldRebuildHashIndexMetadataAndPreserveResults()
|
||||
{
|
||||
var dbPath = NewDbPath();
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new TestDbContext(dbPath);
|
||||
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
db.People.Insert(new Person
|
||||
{
|
||||
Name = $"hash-person-{i:D4}",
|
||||
Age = i % 12
|
||||
});
|
||||
}
|
||||
|
||||
db.SaveChanges();
|
||||
db.ForceCheckpoint();
|
||||
|
||||
var expectedByAge = db.People.FindAll()
|
||||
.GroupBy(p => p.Age)
|
||||
.ToDictionary(g => g.Key, g => g.Select(x => x.Name).OrderBy(x => x).ToArray());
|
||||
|
||||
var metadata = db.Storage.GetCollectionMetadata("people_collection");
|
||||
metadata.ShouldNotBeNull();
|
||||
|
||||
var targetIndex = metadata!.Indexes
|
||||
.FirstOrDefault(index => index.PropertyPaths.Any(path => path.Equals("Age", StringComparison.OrdinalIgnoreCase)));
|
||||
targetIndex.ShouldNotBeNull();
|
||||
|
||||
targetIndex!.Type = IndexType.Hash;
|
||||
db.Storage.SaveCollectionMetadata(metadata);
|
||||
db.SaveChanges();
|
||||
|
||||
var stats = db.Compact(new CompactionOptions
|
||||
{
|
||||
DefragmentSlottedPages = true,
|
||||
NormalizeFreeList = true,
|
||||
EnableTailTruncation = true
|
||||
});
|
||||
stats.PrePageCount.ShouldBeGreaterThanOrEqualTo(stats.PostPageCount);
|
||||
|
||||
var reloadedMetadata = db.Storage.GetCollectionMetadata("people_collection");
|
||||
reloadedMetadata.ShouldNotBeNull();
|
||||
var rebuiltIndex = reloadedMetadata!.Indexes.FirstOrDefault(index => index.Name == targetIndex.Name);
|
||||
rebuiltIndex.ShouldNotBeNull();
|
||||
rebuiltIndex!.Type.ShouldBe(IndexType.Hash);
|
||||
rebuiltIndex.RootPageId.ShouldBeGreaterThan(0u);
|
||||
|
||||
var runtimeIndex = db.People.GetIndexes().FirstOrDefault(index => index.Name == targetIndex.Name);
|
||||
runtimeIndex.ShouldNotBeNull();
|
||||
runtimeIndex!.Type.ShouldBe(IndexType.Hash);
|
||||
|
||||
foreach (var age in expectedByAge.Keys.OrderBy(x => x))
|
||||
{
|
||||
var actual = db.People.FindAll(p => p.Age == age)
|
||||
.Select(x => x.Name)
|
||||
.OrderBy(x => x)
|
||||
.ToArray();
|
||||
|
||||
actual.ShouldBe(expectedByAge[age]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupFiles(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact when tail is reclaimable should reduce file size.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_WhenTailIsReclaimable_ShouldReduceFileSize()
|
||||
{
|
||||
@@ -178,6 +261,9 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact with invalid primary root metadata should fail validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_WithInvalidPrimaryRootMetadata_ShouldFailValidation()
|
||||
{
|
||||
@@ -208,6 +294,9 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact with invalid secondary root metadata should fail validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_WithInvalidSecondaryRootMetadata_ShouldFailValidation()
|
||||
{
|
||||
@@ -239,6 +328,9 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact should report live bytes relocation and throughput telemetry.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_ShouldReportLiveBytesRelocationAndThroughputTelemetry()
|
||||
{
|
||||
@@ -290,6 +382,9 @@ public class CompactionOfflineTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests offline compact when primary index points to deleted slot should fail validation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_WhenPrimaryIndexPointsToDeletedSlot_ShouldFailValidation()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompactionOnlineConcurrencyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies online compaction completes without deadlock under concurrent workload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OnlineCompaction_WithConcurrentishWorkload_ShouldCompleteWithoutDeadlock()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompactionWalCoordinationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies offline compaction checkpoints and leaves the WAL empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OfflineCompact_ShouldCheckpointAndLeaveWalEmpty()
|
||||
{
|
||||
@@ -42,6 +45,9 @@ public class CompactionWalCoordinationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies compaction after WAL recovery preserves durable data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Compact_AfterWalRecovery_ShouldKeepDataDurable()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompressionCompatibilityTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies opening legacy uncompressed files with compression enabled does not mutate database bytes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpeningLegacyUncompressedFile_WithCompressionEnabled_ShouldNotMutateDbFile()
|
||||
{
|
||||
@@ -56,6 +59,9 @@ public class CompressionCompatibilityTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies mixed compressed and uncompressed documents remain readable after partial migration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MixedFormatDocuments_ShouldRemainReadableAfterPartialMigration()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompressionCorruptionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies corrupted compressed payload checksum triggers invalid data errors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Read_WithBadChecksum_ShouldThrowInvalidData()
|
||||
{
|
||||
@@ -34,6 +37,9 @@ public class CompressionCorruptionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies invalid original length metadata triggers invalid data errors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Read_WithBadOriginalLength_ShouldThrowInvalidData()
|
||||
{
|
||||
@@ -57,6 +63,9 @@ public class CompressionCorruptionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies oversized declared decompressed length enforces safety guardrails.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Read_WithOversizedDeclaredLength_ShouldEnforceGuardrail()
|
||||
{
|
||||
@@ -81,6 +90,9 @@ public class CompressionCorruptionTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies invalid codec identifiers in compressed headers trigger invalid data errors.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Read_WithInvalidCodecId_ShouldThrowInvalidData()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompressionInsertReadTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests insert with threshold should store mixed compressed and uncompressed slots.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_WithThreshold_ShouldStoreMixedCompressedAndUncompressedSlots()
|
||||
{
|
||||
@@ -46,6 +49,9 @@ public class CompressionInsertReadTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests find by id should read mixed compressed and uncompressed documents.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindById_ShouldReadMixedCompressedAndUncompressedDocuments()
|
||||
{
|
||||
@@ -91,6 +97,9 @@ public class CompressionInsertReadTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests insert when codec throws should fallback to uncompressed storage.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_WhenCodecThrows_ShouldFallbackToUncompressedStorage()
|
||||
{
|
||||
@@ -186,11 +195,25 @@ public class CompressionInsertReadTests
|
||||
|
||||
private sealed class FailingBrotliCodec : ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the codec.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec => CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Tests compress.
|
||||
/// </summary>
|
||||
/// <param name="input">Payload bytes to compress.</param>
|
||||
/// <param name="level">Compression level.</param>
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
=> throw new InvalidOperationException("Forced codec failure for test coverage.");
|
||||
|
||||
/// <summary>
|
||||
/// Tests decompress.
|
||||
/// </summary>
|
||||
/// <param name="input">Compressed payload bytes.</param>
|
||||
/// <param name="expectedLength">Expected decompressed payload length.</param>
|
||||
/// <param name="maxDecompressedSizeBytes">Maximum allowed decompressed size.</param>
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
=> throw new InvalidOperationException("This codec should not be used for reads in this scenario.");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class CompressionOverflowTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests insert compressed document spanning overflow pages should round trip.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_CompressedDocumentSpanningOverflowPages_ShouldRoundTrip()
|
||||
{
|
||||
@@ -43,6 +46,9 @@ public class CompressionOverflowTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests update should transition across compression thresholds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Update_ShouldTransitionAcrossCompressionThresholds()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ public class CursorTests : IDisposable
|
||||
private readonly StorageEngine _storage;
|
||||
private readonly BTreeIndex _index;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CursorTests"/> class.
|
||||
/// </summary>
|
||||
public CursorTests()
|
||||
{
|
||||
_testFile = Path.Combine(Path.GetTempPath(), $"docdb_cursor_test_{Guid.NewGuid()}.db");
|
||||
@@ -34,6 +37,9 @@ public class CursorTests : IDisposable
|
||||
_storage.CommitTransaction(txnId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests move to first should position at first.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MoveToFirst_ShouldPositionAtFirst()
|
||||
{
|
||||
@@ -42,6 +48,9 @@ public class CursorTests : IDisposable
|
||||
cursor.Current.Key.ShouldBe(IndexKey.Create(10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests move to last should position at last.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MoveToLast_ShouldPositionAtLast()
|
||||
{
|
||||
@@ -50,6 +59,9 @@ public class CursorTests : IDisposable
|
||||
cursor.Current.Key.ShouldBe(IndexKey.Create(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests move next should traverse forward.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MoveNext_ShouldTraverseForward()
|
||||
{
|
||||
@@ -65,6 +77,9 @@ public class CursorTests : IDisposable
|
||||
cursor.MoveNext().ShouldBeFalse(); // End
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests move prev should traverse backward.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MovePrev_ShouldTraverseBackward()
|
||||
{
|
||||
@@ -80,6 +95,9 @@ public class CursorTests : IDisposable
|
||||
cursor.MovePrev().ShouldBeFalse(); // Start
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests seek should position exact or next.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Seek_ShouldPositionExact_OrNext()
|
||||
{
|
||||
@@ -99,6 +117,9 @@ public class CursorTests : IDisposable
|
||||
Should.Throw<InvalidOperationException>(() => cursor.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resources used by this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_storage.Dispose();
|
||||
|
||||
@@ -11,18 +11,27 @@ public class DbContextInheritanceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestExtendedDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DbContextInheritanceTests"/> class.
|
||||
/// </summary>
|
||||
public DbContextInheritanceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_inheritance_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestExtendedDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies parent collections are initialized in the extended context.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedContext_Should_Initialize_Parent_Collections()
|
||||
{
|
||||
@@ -35,6 +44,9 @@ public class DbContextInheritanceTests : IDisposable
|
||||
_db.TestDocuments.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies extended context collections are initialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedContext_Should_Initialize_Own_Collections()
|
||||
{
|
||||
@@ -42,6 +54,9 @@ public class DbContextInheritanceTests : IDisposable
|
||||
_db.ExtendedEntities.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies parent collections are usable from the extended context.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedContext_Can_Use_Parent_Collections()
|
||||
{
|
||||
@@ -57,6 +72,9 @@ public class DbContextInheritanceTests : IDisposable
|
||||
retrieved.Age.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies extended collections are usable from the extended context.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedContext_Can_Use_Own_Collections()
|
||||
{
|
||||
@@ -76,6 +94,9 @@ public class DbContextInheritanceTests : IDisposable
|
||||
retrieved.Description.ShouldBe("Test Extended Entity");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies parent and extended collections can be used together.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExtendedContext_Can_Use_Both_Parent_And_Own_Collections()
|
||||
{
|
||||
|
||||
@@ -12,11 +12,17 @@ public class DbContextTests : IDisposable
|
||||
{
|
||||
private string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes test file paths for database context tests.
|
||||
/// </summary>
|
||||
public DbContextTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_{Guid.NewGuid()}.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the basic database context lifecycle works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_BasicLifecycle_Works()
|
||||
{
|
||||
@@ -31,6 +37,9 @@ public class DbContextTests : IDisposable
|
||||
found.Age.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies multiple CRUD operations execute correctly in one context.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_MultipleOperations_Work()
|
||||
{
|
||||
@@ -59,6 +68,9 @@ public class DbContextTests : IDisposable
|
||||
db.Users.Count().ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies disposing and reopening context preserves persisted data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_Dispose_ReleasesResources()
|
||||
{
|
||||
@@ -90,6 +102,9 @@ public class DbContextTests : IDisposable
|
||||
return Convert.ToHexString(sha256.ComputeHash(stream));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies database file size and content change after insert and checkpoint.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DatabaseFile_SizeAndContent_ChangeAfterInsert()
|
||||
{
|
||||
@@ -117,6 +132,9 @@ public class DbContextTests : IDisposable
|
||||
afterInsertHash.ShouldNotBe(initialHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the WAL file path is auto-derived from database path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_AutoDerivesWalPath()
|
||||
{
|
||||
@@ -127,6 +145,9 @@ public class DbContextTests : IDisposable
|
||||
File.Exists(walPath).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies custom page file and compression options support roundtrip data access.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_WithCustomPageFileAndCompressionOptions_ShouldSupportRoundTrip()
|
||||
{
|
||||
@@ -165,6 +186,9 @@ public class DbContextTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies compact API returns stats and preserves data consistency.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DbContext_CompactApi_ShouldReturnStatsAndPreserveData()
|
||||
{
|
||||
@@ -197,6 +221,9 @@ public class DbContextTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and cleans up generated files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -9,6 +9,9 @@ public class DictionaryPageTests
|
||||
{
|
||||
private const int PageSize = 16384;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dictionary page initialization sets expected defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Initialize_ShouldSetupEmptyPage()
|
||||
{
|
||||
@@ -26,6 +29,9 @@ public class DictionaryPageTests
|
||||
freeSpaceEnd.ShouldBe((ushort)PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert adds entries and keeps them ordered.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_ShouldAddEntryAndSort()
|
||||
{
|
||||
@@ -58,6 +64,9 @@ public class DictionaryPageTests
|
||||
entries[2].Value.ShouldBe((ushort)30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies key lookup returns the expected value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryFind_ShouldReturnCorrectValue()
|
||||
{
|
||||
@@ -76,6 +85,9 @@ public class DictionaryPageTests
|
||||
found.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inserts fail when the page is full.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Overflow_ShouldReturnFalse_WhenFull()
|
||||
{
|
||||
@@ -105,6 +117,9 @@ public class DictionaryPageTests
|
||||
inserted.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies global lookup finds keys across chained dictionary pages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Chaining_ShouldFindKeysInLinkedPages()
|
||||
{
|
||||
@@ -158,6 +173,9 @@ public class DictionaryPageTests
|
||||
if (File.Exists(Path.ChangeExtension(dbPath, ".wal"))) File.Delete(Path.ChangeExtension(dbPath, ".wal"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies global enumeration returns keys across chained dictionary pages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindAllGlobal_ShouldRetrieveAllKeys()
|
||||
{
|
||||
|
||||
@@ -13,12 +13,18 @@ public class DictionaryPersistenceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly StorageEngine _storage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DictionaryPersistenceTests"/> class.
|
||||
/// </summary>
|
||||
public DictionaryPersistenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_dict_{Guid.NewGuid():N}.db");
|
||||
_storage = new StorageEngine(_dbPath, PageFileConfig.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_storage.Dispose();
|
||||
@@ -32,22 +38,41 @@ public class DictionaryPersistenceTests : IDisposable
|
||||
private readonly string _collectionName;
|
||||
private readonly List<string> _keys;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MockMapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The collection name.</param>
|
||||
/// <param name="keys">The mapper keys.</param>
|
||||
public MockMapper(string name, params string[] keys)
|
||||
{
|
||||
_collectionName = name;
|
||||
_keys = keys.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CollectionName => _collectionName;
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<string> UsedKeys => _keys;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BsonSchema GetSchema() => new BsonSchema { Title = _collectionName };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ObjectId GetId(Dictionary<string, object> entity) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetId(Dictionary<string, object> entity, ObjectId id) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Serialize(Dictionary<string, object> entity, BsonSpanWriter writer) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Dictionary<string, object> Deserialize(BsonSpanReader reader) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies mapper registration adds all unique dictionary keys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RegisterMappers_Registers_All_Unique_Keys()
|
||||
{
|
||||
@@ -73,6 +98,9 @@ public class DictionaryPersistenceTests : IDisposable
|
||||
ids.Count.ShouldBe(4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dictionary keys persist across storage restarts.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dictionary_Keys_Persist_Across_Restarts()
|
||||
{
|
||||
@@ -93,7 +121,10 @@ public class DictionaryPersistenceTests : IDisposable
|
||||
|
||||
private class NestedMockMapper : DocumentMapperBase<ObjectId, object>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string CollectionName => "Nested";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BsonSchema GetSchema()
|
||||
{
|
||||
var schema = new BsonSchema { Title = "Nested" };
|
||||
@@ -109,12 +140,22 @@ public class DictionaryPersistenceTests : IDisposable
|
||||
return schema;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ObjectId GetId(object entity) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetId(object entity, ObjectId id) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Serialize(object entity, BsonSpanWriter writer) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object Deserialize(BsonSpanReader reader) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies nested schema fields are registered as dictionary keys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RegisterMappers_Handles_Nested_Keys()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ public class DocumentCollectionDeleteTests : IDisposable
|
||||
private readonly string _walPath;
|
||||
private readonly Shared.TestDbContext _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentCollectionDeleteTests"/> class.
|
||||
/// </summary>
|
||||
public DocumentCollectionDeleteTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_delete_{Guid.NewGuid()}.db");
|
||||
@@ -22,11 +25,17 @@ public class DocumentCollectionDeleteTests : IDisposable
|
||||
_dbContext = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_dbContext.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete removes both the document and its index entry.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_RemovesDocumentAndIndexEntry()
|
||||
{
|
||||
@@ -52,6 +61,9 @@ public class DocumentCollectionDeleteTests : IDisposable
|
||||
all.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete returns false for a non-existent document.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_NonExistent_ReturnsFalse()
|
||||
{
|
||||
@@ -61,6 +73,9 @@ public class DocumentCollectionDeleteTests : IDisposable
|
||||
deleted.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies deletes inside a transaction commit successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_WithTransaction_CommitsSuccessfully()
|
||||
{
|
||||
|
||||
@@ -8,12 +8,18 @@ public class DocumentCollectionIndexApiTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentCollectionIndexApiTests"/> class.
|
||||
/// </summary>
|
||||
public DocumentCollectionIndexApiTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"collection_index_api_{Guid.NewGuid():N}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies vector index creation and deletion behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CreateVectorIndex_And_DropIndex_Should_Work()
|
||||
{
|
||||
@@ -32,6 +38,9 @@ public class DocumentCollectionIndexApiTests : IDisposable
|
||||
_db.VectorItems.GetIndexes().Select(x => x.Name).ShouldNotContain("idx_vector_extra");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies ensure-index returns existing indexes when already present.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EnsureIndex_Should_Return_Existing_Index_When_Already_Present()
|
||||
{
|
||||
@@ -41,12 +50,18 @@ public class DocumentCollectionIndexApiTests : IDisposable
|
||||
ReferenceEquals(first, second).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dropping the primary index name is rejected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DropIndex_Should_Reject_Primary_Index_Name()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => _db.People.DropIndex("_id"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -13,6 +13,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
private readonly string _walPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentCollectionTests"/> class.
|
||||
/// </summary>
|
||||
public DocumentCollectionTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_collection_{Guid.NewGuid()}.db");
|
||||
@@ -21,6 +24,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert and find-by-id operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_And_FindById_Works()
|
||||
{
|
||||
@@ -39,6 +45,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
found.Age.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies find-by-id returns null when no document is found.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindById_Returns_Null_When_Not_Found()
|
||||
{
|
||||
@@ -49,6 +58,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
found.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies find-all returns all entities.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindAll_Returns_All_Entities()
|
||||
{
|
||||
@@ -68,6 +80,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
all.ShouldContain(u => u.Name == "Charlie");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update modifies an existing entity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Update_Modifies_Entity()
|
||||
{
|
||||
@@ -89,6 +104,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
found.Age.ShouldBe(31);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update returns false when the entity does not exist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Update_Returns_False_When_Not_Found()
|
||||
{
|
||||
@@ -103,6 +121,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
updated.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete removes an entity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_Removes_Entity()
|
||||
{
|
||||
@@ -120,6 +141,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
_db.Users.FindById(id).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete returns false when the entity does not exist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Delete_Returns_False_When_Not_Found()
|
||||
{
|
||||
@@ -131,6 +155,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
deleted.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies count returns the correct entity count.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Count_Returns_Correct_Count()
|
||||
{
|
||||
@@ -146,6 +173,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
count.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies predicate queries filter entities correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Find_With_Predicate_Filters_Correctly()
|
||||
{
|
||||
@@ -163,6 +193,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
over30[0].Name.ShouldBe("Charlie");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies bulk insert stores multiple entities.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InsertBulk_Inserts_Multiple_Entities()
|
||||
{
|
||||
@@ -183,6 +216,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
_db.Users.Count().ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inserts preserve an explicitly assigned identifier.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_With_SpecifiedId_RetainsId()
|
||||
{
|
||||
@@ -203,6 +239,9 @@ public class DocumentCollectionTests : IDisposable
|
||||
found.Name.ShouldBe("SpecifiedID");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Dispose();
|
||||
|
||||
@@ -16,6 +16,9 @@ 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");
|
||||
@@ -23,12 +26,18 @@ public class DocumentOverflowTests : IDisposable
|
||||
_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()
|
||||
{
|
||||
@@ -50,6 +59,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
retrieved.Name.ShouldBe(largeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inserting a large document succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_LargeDoc_100KB_ShouldSucceed()
|
||||
{
|
||||
@@ -70,6 +82,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
retrieved.Name.ShouldBe(largeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inserting a very large document succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_HugeDoc_3MB_ShouldSucceed()
|
||||
{
|
||||
@@ -93,6 +108,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
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()
|
||||
{
|
||||
@@ -114,6 +132,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
retrieved.Name.Length.ShouldBe(hugeString.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies bulk inserts with mixed payload sizes succeed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InsertBulk_MixedSizes_ShouldSucceed()
|
||||
{
|
||||
@@ -136,6 +157,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies huge inserts succeed with compression enabled and small page configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_HugeDoc_WithCompressionEnabledAndSmallPages_ShouldSucceed()
|
||||
{
|
||||
@@ -172,6 +196,9 @@ public class DocumentOverflowTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies updates from huge to small payloads succeed with compression enabled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Update_HugeToSmall_WithCompressionEnabled_ShouldSucceed()
|
||||
{
|
||||
|
||||
@@ -7,12 +7,18 @@ public class GeospatialStressTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes database state for geospatial stress tests.
|
||||
/// </summary>
|
||||
public GeospatialStressTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"geo_stress_{Guid.NewGuid():N}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies spatial index handles node splits and query operations under load.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SpatialIndex_Should_Handle_Node_Splits_And_Queries()
|
||||
{
|
||||
@@ -40,6 +46,9 @@ public class GeospatialStressTests : IDisposable
|
||||
near.Count.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes generated files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -12,12 +12,18 @@ public class GeospatialTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GeospatialTests"/> class.
|
||||
/// </summary>
|
||||
public GeospatialTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_geo_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies spatial within queries return expected results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Can_Insert_And_Search_Within()
|
||||
{
|
||||
@@ -38,6 +44,9 @@ public class GeospatialTests : IDisposable
|
||||
results.ShouldContain(r => r.Name == "Point 2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies near queries return expected proximity results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Can_Search_Near_Proximity()
|
||||
{
|
||||
@@ -59,6 +68,9 @@ public class GeospatialTests : IDisposable
|
||||
results.ShouldNotContain(r => r.Name == "New York Office");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies LINQ near integration returns expected results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LINQ_Integration_Near_Works()
|
||||
{
|
||||
@@ -76,6 +88,9 @@ public class GeospatialTests : IDisposable
|
||||
results[0].Name.ShouldBe("Milan Office");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies LINQ within integration returns expected results.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LINQ_Integration_Within_Works()
|
||||
{
|
||||
@@ -94,6 +109,9 @@ public class GeospatialTests : IDisposable
|
||||
results[0].Name.ShouldBe("Milan Office");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class HashIndexTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes Insert_And_TryFind_Should_Return_Location.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Insert_And_TryFind_Should_Return_Location()
|
||||
{
|
||||
@@ -19,6 +22,9 @@ public class HashIndexTests
|
||||
found.SlotIndex.ShouldBe(location.SlotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Unique_HashIndex_Should_Throw_On_Duplicate_Key.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Unique_HashIndex_Should_Throw_On_Duplicate_Key()
|
||||
{
|
||||
@@ -38,6 +44,9 @@ public class HashIndexTests
|
||||
index.Insert(key, new DocumentLocation(2, 2)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Remove_Should_Remove_Only_Matching_Entry.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Remove_Should_Remove_Only_Matching_Entry()
|
||||
{
|
||||
@@ -61,6 +70,9 @@ public class HashIndexTests
|
||||
index.FindAll(key).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes FindAll_Should_Return_All_Matching_Entries.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FindAll_Should_Return_All_Matching_Entries()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ public class IndexDirectionTests : IDisposable
|
||||
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes database state for index direction tests.
|
||||
/// </summary>
|
||||
public IndexDirectionTests()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
@@ -21,12 +24,18 @@ public class IndexDirectionTests : IDisposable
|
||||
// _db.Database.EnsureCreated(); // Not needed/doesn't exist? StorageEngine handles creation.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and deletes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies forward range scans return values in ascending order.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Range_Forward_ReturnsOrderedResults()
|
||||
{
|
||||
@@ -45,6 +54,9 @@ public class IndexDirectionTests : IDisposable
|
||||
collection.FindByLocation(results.Last())!.Age.ShouldBe(20); // Last is 20
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies backward range scans return values in descending order.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Range_Backward_ReturnsReverseOrderedResults()
|
||||
{
|
||||
@@ -63,6 +75,9 @@ public class IndexDirectionTests : IDisposable
|
||||
collection.FindByLocation(results.Last())!.Age.ShouldBe(10); // Last is 10
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies backward scans across split index pages return complete result sets.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Range_Backward_WithMultiplePages_ReturnsReverseOrderedResults()
|
||||
{
|
||||
|
||||
@@ -11,11 +11,23 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
{
|
||||
public class TestEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer identifies equality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Identifies_Equality()
|
||||
{
|
||||
@@ -36,6 +48,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result.IsRange.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer identifies range greater than.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Identifies_Range_GreaterThan()
|
||||
{
|
||||
@@ -56,6 +71,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result.IsRange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer identifies range less than.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Identifies_Range_LessThan()
|
||||
{
|
||||
@@ -76,6 +94,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result.IsRange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer identifies range between simulated.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Identifies_Range_Between_Simulated()
|
||||
{
|
||||
@@ -96,6 +117,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result.IsRange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer identifies starts with.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Identifies_StartsWith()
|
||||
{
|
||||
@@ -117,6 +141,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
result.IsRange.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests optimizer ignores non indexed fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Optimizer_Ignores_NonIndexed_Fields()
|
||||
{
|
||||
|
||||
@@ -13,17 +13,26 @@ public class InsertBulkTests : IDisposable
|
||||
private readonly string _testFile;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InsertBulkTests"/> class.
|
||||
/// </summary>
|
||||
public InsertBulkTests()
|
||||
{
|
||||
_testFile = Path.GetTempFileName();
|
||||
_db = new Shared.TestDbContext(_testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies bulk inserts are immediately persisted and visible.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InsertBulk_PersistsData_ImmediatelyVisible()
|
||||
{
|
||||
@@ -41,6 +50,9 @@ public class InsertBulkTests : IDisposable
|
||||
insertedUsers.Count.ShouldBe(50);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies bulk inserts spanning multiple pages persist correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InsertBulk_SpanningMultiplePages_PersistsCorrectly()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
private readonly string _testFile;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinqTests"/> class.
|
||||
/// </summary>
|
||||
public LinqTests()
|
||||
{
|
||||
_testFile = Path.Combine(Path.GetTempPath(), $"linq_tests_{Guid.NewGuid()}.db");
|
||||
@@ -35,6 +38,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
@@ -43,6 +49,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
if (File.Exists(wal)) File.Delete(wal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies where filters return matching documents.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Where_FiltersDocuments()
|
||||
{
|
||||
@@ -53,6 +62,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results.ShouldNotContain(d => d.Name == "Bob");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies order by returns sorted documents.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OrderBy_SortsDocuments()
|
||||
{
|
||||
@@ -64,6 +76,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results.Last().Name.ShouldBe("Eve"); // 40
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies skip and take support pagination.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SkipTake_Pagination()
|
||||
{
|
||||
@@ -78,6 +93,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results[1].Name.ShouldBe("Alice"); // 30
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies select supports projections.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Select_Projections()
|
||||
{
|
||||
@@ -91,6 +109,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
names[0].ShouldBe("Dave");
|
||||
names[1].ShouldBe("Bob");
|
||||
}
|
||||
/// <summary>
|
||||
/// Verifies indexed where queries use index-backed filtering.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndexedWhere_UsedIndex()
|
||||
{
|
||||
@@ -104,6 +125,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results.ShouldNotContain(d => d.Name == "Bob"); // Age 25 (filtered out by strict >)
|
||||
results.ShouldNotContain(d => d.Name == "Dave"); // Age 20
|
||||
}
|
||||
/// <summary>
|
||||
/// Verifies starts-with predicates can use an index.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StartsWith_UsedIndex()
|
||||
{
|
||||
@@ -118,6 +142,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results[0].Name.ShouldBe("Charlie");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies range predicates can use an index.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Between_UsedIndex()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class MaintenanceDiagnosticsAndMigrationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies diagnostics APIs return page usage, compression, and fragmentation data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DiagnosticsApis_ShouldReturnPageUsageCompressionAndFragmentationData()
|
||||
{
|
||||
@@ -61,6 +64,9 @@ public class MaintenanceDiagnosticsAndMigrationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies compression migration dry-run and apply modes return deterministic stats and preserve data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MigrateCompression_DryRunAndApply_ShouldReturnDeterministicStatsAndPreserveData()
|
||||
{
|
||||
|
||||
@@ -14,12 +14,18 @@ public class MetadataPersistenceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly string _walPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MetadataPersistenceTests"/> class.
|
||||
/// </summary>
|
||||
public MetadataPersistenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"docdb_meta_{Guid.NewGuid()}.db");
|
||||
_walPath = Path.ChangeExtension(_dbPath, ".wal");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests index definitions are persisted and reloaded.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndexDefinitions_ArePersisted_AndReloaded()
|
||||
{
|
||||
@@ -59,6 +65,9 @@ public class MetadataPersistenceTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests ensure index does not recreate if index exists.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EnsureIndex_DoesNotRecreate_IfIndexExists()
|
||||
{
|
||||
@@ -91,6 +100,9 @@ public class MetadataPersistenceTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resources used by this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
|
||||
@@ -12,8 +12,17 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
@@ -21,32 +30,62 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
|
||||
public class ComplexUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
// Direct nested object
|
||||
/// <summary>
|
||||
/// Gets or sets the main address.
|
||||
/// </summary>
|
||||
public Address MainAddress { get; set; } = new();
|
||||
|
||||
// Collection of nested objects
|
||||
/// <summary>
|
||||
/// Gets or sets the other addresses.
|
||||
/// </summary>
|
||||
public List<Address> OtherAddresses { get; set; } = new();
|
||||
|
||||
// Primitive collection
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret.
|
||||
/// </summary>
|
||||
[BsonIgnore]
|
||||
public string Secret { get; set; } = "";
|
||||
}
|
||||
|
||||
public class Address
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the street.
|
||||
/// </summary>
|
||||
public string Street { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
public City City { get; set; } = new(); // Depth 2
|
||||
}
|
||||
|
||||
public class City
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the zip code.
|
||||
/// </summary>
|
||||
public string ZipCode { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -54,19 +93,37 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
|
||||
public class IntEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class StringEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public required string Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
public class GuidEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
@@ -75,8 +132,14 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class CustomKeyEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
public required string Code { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
@@ -84,121 +147,252 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
|
||||
public class AutoInitEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class Person
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
public class Product
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the price.
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
|
||||
public class AsyncDoc
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
public class SchemaUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the address.
|
||||
/// </summary>
|
||||
public Address Address { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VectorEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the embedding.
|
||||
/// </summary>
|
||||
public float[] Embedding { get; set; } = Array.Empty<float>();
|
||||
}
|
||||
|
||||
public class GeoEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
public (double Latitude, double Longitude) Location { get; set; }
|
||||
}
|
||||
|
||||
public record OrderId(string Value)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
public OrderId() : this(string.Empty) { }
|
||||
}
|
||||
|
||||
public class OrderIdConverter : ValueConverter<OrderId, string>
|
||||
{
|
||||
public override string ConvertToProvider(OrderId model) => model?.Value ?? string.Empty;
|
||||
public override OrderId ConvertFromProvider(string provider) => new OrderId(provider);
|
||||
/// <inheritdoc />
|
||||
public override string ConvertToProvider(OrderId model) => model?.Value ?? string.Empty;
|
||||
/// <inheritdoc />
|
||||
public override OrderId ConvertFromProvider(string provider) => new OrderId(provider);
|
||||
}
|
||||
|
||||
public class Order
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public OrderId Id { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the customer name.
|
||||
/// </summary>
|
||||
public string CustomerName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TestDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the category.
|
||||
/// </summary>
|
||||
public string Category { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the amount.
|
||||
/// </summary>
|
||||
public int Amount { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class OrderDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the item name.
|
||||
/// </summary>
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the quantity.
|
||||
/// </summary>
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
public class OrderItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the price.
|
||||
/// </summary>
|
||||
public int Price { get; set; }
|
||||
}
|
||||
|
||||
public class ComplexDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the shipping address.
|
||||
/// </summary>
|
||||
public Address ShippingAddress { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the items.
|
||||
/// </summary>
|
||||
public List<OrderItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
[Table("custom_users", Schema = "test")]
|
||||
public class AnnotatedUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Column("display_name")]
|
||||
[StringLength(50, MinimumLength = 3)]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
[Range(0, 150)]
|
||||
public int Age { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the computed info.
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string ComputedInfo => $"{Name} ({Age})";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[Column(TypeName = "geopoint")]
|
||||
public (double Lat, double Lon) Location { get; set; }
|
||||
}
|
||||
public class PersonV2
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; set; }
|
||||
}
|
||||
|
||||
@@ -207,8 +401,17 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class ExtendedEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the created at.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -219,7 +422,13 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class BaseEntityWithId
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the created at.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -228,7 +437,13 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class DerivedEntity : BaseEntityWithId
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -237,14 +452,35 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class EntityWithComputedProperties
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the first name.
|
||||
/// </summary>
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the last name.
|
||||
/// </summary>
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the birth year.
|
||||
/// </summary>
|
||||
public int BirthYear { get; set; }
|
||||
|
||||
// Computed properties - should NOT be serialized
|
||||
/// <summary>
|
||||
/// Gets the full name.
|
||||
/// </summary>
|
||||
public string FullName => $"{FirstName} {LastName}";
|
||||
/// <summary>
|
||||
/// Gets the age.
|
||||
/// </summary>
|
||||
public int Age => DateTime.Now.Year - BirthYear;
|
||||
/// <summary>
|
||||
/// Gets the display info.
|
||||
/// </summary>
|
||||
public string DisplayInfo => $"{FullName} (Age: {Age})";
|
||||
}
|
||||
|
||||
@@ -253,18 +489,45 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class EntityWithAdvancedCollections
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
// Various collection types that should all be recognized
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
public HashSet<string> Tags { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the numbers.
|
||||
/// </summary>
|
||||
public ISet<int> Numbers { get; set; } = new HashSet<int>();
|
||||
/// <summary>
|
||||
/// Gets or sets the history.
|
||||
/// </summary>
|
||||
public LinkedList<string> History { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the pending items.
|
||||
/// </summary>
|
||||
public Queue<string> PendingItems { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the undo stack.
|
||||
/// </summary>
|
||||
public Stack<string> UndoStack { get; set; } = new();
|
||||
|
||||
// Nested objects in collections
|
||||
/// <summary>
|
||||
/// Gets or sets the addresses.
|
||||
/// </summary>
|
||||
public HashSet<Address> Addresses { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the favorite cities.
|
||||
/// </summary>
|
||||
public ISet<City> FavoriteCities { get; set; } = new HashSet<City>();
|
||||
}
|
||||
|
||||
@@ -273,13 +536,30 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class EntityWithPrivateSetters
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the created at.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
// Factory method for creation
|
||||
public static EntityWithPrivateSetters Create(string name, int age)
|
||||
/// <summary>
|
||||
/// Executes the create operation.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="age">The age.</param>
|
||||
public static EntityWithPrivateSetters Create(string name, int age)
|
||||
{
|
||||
return new EntityWithPrivateSetters
|
||||
{
|
||||
@@ -296,9 +576,21 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class EntityWithInitSetters
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the age.
|
||||
/// </summary>
|
||||
public int Age { get; init; }
|
||||
/// <summary>
|
||||
/// Gets or sets the created at.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -313,10 +605,25 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class Employee
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the department.
|
||||
/// </summary>
|
||||
public string Department { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the manager id.
|
||||
/// </summary>
|
||||
public ObjectId? ManagerId { get; set; } // Reference to manager
|
||||
/// <summary>
|
||||
/// Gets or sets the direct report ids.
|
||||
/// </summary>
|
||||
public List<ObjectId>? DirectReportIds { get; set; } // References to direct reports (best practice)
|
||||
}
|
||||
|
||||
@@ -327,9 +634,21 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class CategoryRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the product ids.
|
||||
/// </summary>
|
||||
public List<ObjectId>? ProductIds { get; set; } // Only IDs - no embedding
|
||||
}
|
||||
|
||||
@@ -340,9 +659,21 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class ProductRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public ObjectId Id { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the price.
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the category ids.
|
||||
/// </summary>
|
||||
public List<ObjectId>? CategoryIds { get; set; } // Only IDs - no embedding
|
||||
}
|
||||
|
||||
@@ -358,12 +689,22 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
where TId : IEquatable<TId>
|
||||
where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
public virtual TId? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
protected MockBaseEntity() { }
|
||||
|
||||
protected MockBaseEntity(TId? id)
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
protected MockBaseEntity(TId? id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
@@ -377,9 +718,16 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
public abstract class MockUuidEntity<TEntity> : MockBaseEntity<string, TEntity>
|
||||
where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
protected MockUuidEntity() : base() { }
|
||||
|
||||
protected MockUuidEntity(string? id) : base(id) { }
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
protected MockUuidEntity(string? id) : base(id) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -388,11 +736,24 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class MockCounter : MockUuidEntity<MockCounter>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
public MockCounter() : base() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
public MockCounter(string? id) : base(id) { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
@@ -401,25 +762,58 @@ namespace ZB.MOM.WW.CBDD.Shared
|
||||
/// </summary>
|
||||
public class TemporalEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
// DateTime types
|
||||
/// <summary>
|
||||
/// Gets or sets the created at.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the updated at.
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the last accessed at.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastAccessedAt { get; set; }
|
||||
|
||||
// TimeSpan
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the optional duration.
|
||||
/// </summary>
|
||||
public TimeSpan? OptionalDuration { get; set; }
|
||||
|
||||
// DateOnly and TimeOnly (.NET 6+)
|
||||
/// <summary>
|
||||
/// Gets or sets the birth date.
|
||||
/// </summary>
|
||||
public DateOnly BirthDate { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the anniversary.
|
||||
/// </summary>
|
||||
public DateOnly? Anniversary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the opening time.
|
||||
/// </summary>
|
||||
public TimeOnly OpeningTime { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the closing time.
|
||||
/// </summary>
|
||||
public TimeOnly? ClosingTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,25 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
{
|
||||
private const string DbPath = "nullable_string_id.db";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NullableStringIdTests"/> class.
|
||||
/// </summary>
|
||||
public NullableStringIdTests()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(DbPath)) File.Delete(DbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the mock counter collection is initialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Collection_IsInitialized()
|
||||
{
|
||||
@@ -30,6 +39,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
db.MockCounters.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert and find-by-id operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Insert_And_FindById_Works()
|
||||
{
|
||||
@@ -52,6 +64,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
stored.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies update operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Update_Works()
|
||||
{
|
||||
@@ -77,6 +92,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
updated.Value.ShouldBe(20);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies delete operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Delete_Works()
|
||||
{
|
||||
@@ -99,6 +117,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
deleted.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies query operations work for string identifiers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_Query_Works()
|
||||
{
|
||||
@@ -121,6 +142,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
highValues[0].Name.ShouldBe("Second");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies inherited string identifiers are stored and retrieved correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MockCounter_InheritedId_IsStoredCorrectly()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class ObjectIdTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies new object identifiers are 12 bytes long.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NewObjectId_ShouldCreate12ByteId()
|
||||
{
|
||||
@@ -16,6 +19,9 @@ public class ObjectIdTests
|
||||
bytes.Length.ShouldBe(12);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies object identifiers round-trip from their binary form.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ObjectId_ShouldRoundTrip()
|
||||
{
|
||||
@@ -29,6 +35,9 @@ public class ObjectIdTests
|
||||
restored.ShouldBe(original);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies object identifier equality behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ObjectId_Equals_ShouldWork()
|
||||
{
|
||||
@@ -40,6 +49,9 @@ public class ObjectIdTests
|
||||
oid3.ShouldNotBe(oid1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies object identifier timestamps are recent UTC values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ObjectId_Timestamp_ShouldBeRecentUtc()
|
||||
{
|
||||
|
||||
@@ -12,16 +12,25 @@ public class PrimaryKeyTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath = "primary_key_tests.db";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PrimaryKeyTests"/> class.
|
||||
/// </summary>
|
||||
public PrimaryKeyTests()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Test_Int_PrimaryKey.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Int_PrimaryKey()
|
||||
{
|
||||
@@ -46,6 +55,9 @@ public class PrimaryKeyTests : IDisposable
|
||||
db.IntEntities.FindById(1).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Test_String_PrimaryKey.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_String_PrimaryKey()
|
||||
{
|
||||
@@ -65,6 +77,9 @@ public class PrimaryKeyTests : IDisposable
|
||||
db.StringEntities.FindById("key1").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Test_Guid_PrimaryKey.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_Guid_PrimaryKey()
|
||||
{
|
||||
@@ -84,6 +99,9 @@ public class PrimaryKeyTests : IDisposable
|
||||
db.GuidEntities.FindById(id).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Test_String_PrimaryKey_With_Custom_Name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_String_PrimaryKey_With_Custom_Name()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
private readonly StorageEngine _storage;
|
||||
private readonly BTreeIndex _index;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QueryPrimitivesTests"/> class.
|
||||
/// </summary>
|
||||
public QueryPrimitivesTests()
|
||||
{
|
||||
_testFile = Path.Combine(Path.GetTempPath(), $"docdb_test_{Guid.NewGuid()}.db");
|
||||
@@ -55,6 +58,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
_index.Insert(key, new DocumentLocation(1, 1), txnId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Equal_ShouldFindExactMatch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equal_ShouldFindExactMatch()
|
||||
{
|
||||
@@ -65,6 +71,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[0].Key.ShouldBe(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Equal_ShouldReturnEmpty_WhenNotFound.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equal_ShouldReturnEmpty_WhenNotFound()
|
||||
{
|
||||
@@ -74,6 +83,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes GreaterThan_ShouldReturnMatches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GreaterThan_ShouldReturnMatches()
|
||||
{
|
||||
@@ -85,6 +97,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[1].Key.ShouldBe(IndexKey.Create(50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes GreaterThanOrEqual_ShouldReturnMatches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GreaterThanOrEqual_ShouldReturnMatches()
|
||||
{
|
||||
@@ -97,6 +112,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[2].Key.ShouldBe(IndexKey.Create(50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes LessThan_ShouldReturnMatches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LessThan_ShouldReturnMatches()
|
||||
{
|
||||
@@ -110,6 +128,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[1].Key.ShouldBe(IndexKey.Create(10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Between_ShouldReturnRange.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Between_ShouldReturnRange()
|
||||
{
|
||||
@@ -123,6 +144,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[2].Key.ShouldBe(IndexKey.Create(40));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes StartsWith_ShouldReturnPrefixMatches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StartsWith_ShouldReturnPrefixMatches()
|
||||
{
|
||||
@@ -133,6 +157,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[1].Key.ShouldBe(IndexKey.Create("ABC"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Like_ShouldSupportWildcards.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Like_ShouldSupportWildcards()
|
||||
{
|
||||
@@ -148,6 +175,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
// AB ok. ABC ok. B ok.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Like_Underscore_ShouldMatchSingleChar.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Like_Underscore_ShouldMatchSingleChar()
|
||||
{
|
||||
@@ -157,6 +187,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[0].Key.ShouldBe(IndexKey.Create("AB"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes In_ShouldReturnSpecificKeys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void In_ShouldReturnSpecificKeys()
|
||||
{
|
||||
@@ -169,6 +202,9 @@ public class QueryPrimitivesTests : IDisposable
|
||||
result[2].Key.ShouldBe(IndexKey.Create(50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_storage.Dispose();
|
||||
|
||||
@@ -11,19 +11,43 @@ public class RobustnessTests
|
||||
{
|
||||
public struct Point
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the X.
|
||||
/// </summary>
|
||||
public int X { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Y.
|
||||
/// </summary>
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
public class RobustEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the NullableInts.
|
||||
/// </summary>
|
||||
public List<int?> NullableInts { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the Map.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> Map { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Gets or sets the EnumerableStrings.
|
||||
/// </summary>
|
||||
public IEnumerable<string> EnumerableStrings { get; set; } = Array.Empty<string>();
|
||||
/// <summary>
|
||||
/// Gets or sets the Location.
|
||||
/// </summary>
|
||||
public Point Location { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the NullableLocation.
|
||||
/// </summary>
|
||||
public Point? NullableLocation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes GenerateSchema_RobustnessChecks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_RobustnessChecks()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
private readonly string _testFile;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScanTests"/> class.
|
||||
/// </summary>
|
||||
public ScanTests()
|
||||
{
|
||||
_testFile = Path.Combine(Path.GetTempPath(), $"scan_tests_{Guid.NewGuid()}.db");
|
||||
@@ -27,6 +30,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
_db = new Shared.TestDbContext(_testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
@@ -35,6 +41,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
if (File.Exists(wal)) File.Delete(wal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Scan_FindsMatchingDocuments.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Scan_FindsMatchingDocuments()
|
||||
{
|
||||
@@ -53,6 +62,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results.ShouldContain(d => d.Name == "Charlie");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Repro_Insert_Loop_Hang.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Repro_Insert_Loop_Hang()
|
||||
{
|
||||
@@ -65,6 +77,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes ParallelScan_FindsMatchingDocuments.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParallelScan_FindsMatchingDocuments()
|
||||
{
|
||||
|
||||
@@ -17,18 +17,27 @@ public class SchemaPersistenceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SchemaPersistenceTests"/> class.
|
||||
/// </summary>
|
||||
public SchemaPersistenceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"schema_test_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes test resources and removes temporary files.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies BSON schema serialization and deserialization round-trips correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BsonSchema_Serialization_RoundTrip()
|
||||
{
|
||||
@@ -81,6 +90,9 @@ public class SchemaPersistenceTests : IDisposable
|
||||
schema.Equals(roundTrip).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies collection metadata is persisted and reloaded correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_Collections_Metadata_Persistence()
|
||||
{
|
||||
@@ -103,6 +115,9 @@ public class SchemaPersistenceTests : IDisposable
|
||||
loaded.Indexes[0].Name.ShouldBe("age");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies schema versioning appends new schema versions correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_Schema_Versioning()
|
||||
{
|
||||
@@ -125,6 +140,9 @@ public class SchemaPersistenceTests : IDisposable
|
||||
schemas[1].Title.ShouldBe("V2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies collection startup integrates schema versioning behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DocumentCollection_Integrates_Schema_Versioning_On_Startup()
|
||||
{
|
||||
@@ -186,6 +204,9 @@ public class SchemaPersistenceTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies persisted documents include the schema version field.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Document_Contains_Schema_Version_Field()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ public class SchemaTests
|
||||
foreach (var k in new[] { "_id", "name", "mainaddress", "otheraddresses", "tags", "secret", "street", "city" }) _testKeyMap[k] = id++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes UsedKeys_ShouldReturnAllKeys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UsedKeys_ShouldReturnAllKeys()
|
||||
{
|
||||
@@ -33,6 +36,9 @@ public class SchemaTests
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes GetSchema_ShouldReturnBsonSchema.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetSchema_ShouldReturnBsonSchema()
|
||||
{
|
||||
|
||||
@@ -9,18 +9,27 @@ public class SetMethodTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetMethodTests"/> class.
|
||||
/// </summary>
|
||||
public SetMethodTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_set_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resources used by this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set object id returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ObjectId_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -29,6 +38,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Users);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set shorthand returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_Shorthand_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -37,6 +49,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Users);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set int returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_Int_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -45,6 +60,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.People);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set string returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_String_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -53,6 +71,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.StringEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set guid returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_Guid_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -61,6 +82,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.GuidEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set custom key returns correct collection.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_CustomKey_ReturnsCorrectCollection()
|
||||
{
|
||||
@@ -69,6 +93,9 @@ public class SetMethodTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Orders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set all object id collections return correct instances.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_AllObjectIdCollections_ReturnCorrectInstances()
|
||||
{
|
||||
@@ -82,6 +109,9 @@ public class SetMethodTests : IDisposable
|
||||
_db.Set<ObjectId, GeoEntity>().ShouldBeSameAs(_db.GeoItems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set all int collections return correct instances.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_AllIntCollections_ReturnCorrectInstances()
|
||||
{
|
||||
@@ -92,24 +122,36 @@ public class SetMethodTests : IDisposable
|
||||
_db.Set<int, SchemaUser>().ShouldBeSameAs(_db.SchemaUsers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set string key collections return correct instances.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_StringKeyCollections_ReturnCorrectInstances()
|
||||
{
|
||||
_db.Set<string, CustomKeyEntity>().ShouldBeSameAs(_db.CustomKeyEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set unregistered entity throws invalid operation exception.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_UnregisteredEntity_ThrowsInvalidOperationException()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => _db.Set<ObjectId, Address>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set wrong key type throws invalid operation exception.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_WrongKeyType_ThrowsInvalidOperationException()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => _db.Set<string, User>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set can perform operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_CanPerformOperations()
|
||||
{
|
||||
@@ -124,6 +166,9 @@ public class SetMethodTests : IDisposable
|
||||
found.Age.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set with int key can perform operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_WithIntKey_CanPerformOperations()
|
||||
{
|
||||
@@ -144,18 +189,27 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly Shared.TestExtendedDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetMethodInheritanceTests"/> class.
|
||||
/// </summary>
|
||||
public SetMethodInheritanceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_set_inherit_{Guid.NewGuid()}.db");
|
||||
_db = new Shared.TestExtendedDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resources used by this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set own collection returns correct instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_OwnCollection_ReturnsCorrectInstance()
|
||||
{
|
||||
@@ -164,6 +218,9 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.ExtendedEntities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set parent collection returns correct instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ParentCollection_ReturnsCorrectInstance()
|
||||
{
|
||||
@@ -172,6 +229,9 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Users);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set parent shorthand returns correct instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ParentShorthand_ReturnsCorrectInstance()
|
||||
{
|
||||
@@ -180,6 +240,9 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Users);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set parent int collection returns correct instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ParentIntCollection_ReturnsCorrectInstance()
|
||||
{
|
||||
@@ -187,6 +250,9 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
_db.Set<int, Product>().ShouldBeSameAs(_db.Products);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set parent custom key returns correct instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ParentCustomKey_ReturnsCorrectInstance()
|
||||
{
|
||||
@@ -195,12 +261,18 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
collection.ShouldBeSameAs(_db.Orders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set unregistered entity throws invalid operation exception.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_UnregisteredEntity_ThrowsInvalidOperationException()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => _db.Set<ObjectId, Address>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set own collection can perform operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_OwnCollection_CanPerformOperations()
|
||||
{
|
||||
@@ -214,6 +286,9 @@ public class SetMethodInheritanceTests : IDisposable
|
||||
found.Description.ShouldBe("Test");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests set parent collection can perform operations.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Set_ParentCollection_CanPerformOperations()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
private readonly string _walPath;
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SourceGeneratorFeaturesTests"/> class.
|
||||
/// </summary>
|
||||
public SourceGeneratorFeaturesTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_sg_features_{Guid.NewGuid()}.db");
|
||||
@@ -26,6 +29,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#region Inheritance Tests
|
||||
|
||||
/// <summary>
|
||||
/// Tests derived entity inherits id from base class.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DerivedEntity_InheritsId_FromBaseClass()
|
||||
{
|
||||
@@ -50,6 +56,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.CreatedAt.Date.ShouldBe(entity.CreatedAt.Date); // Compare just date part
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests derived entity update works with inherited id.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DerivedEntity_Update_WorksWithInheritedId()
|
||||
{
|
||||
@@ -80,6 +89,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
updated.Description.ShouldBe("Updated Description");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests derived entity query works with inherited properties.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DerivedEntity_Query_WorksWithInheritedProperties()
|
||||
{
|
||||
@@ -107,6 +119,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#region Computed Properties Tests
|
||||
|
||||
/// <summary>
|
||||
/// Tests computed properties are not serialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputedProperties_AreNotSerialized()
|
||||
{
|
||||
@@ -135,6 +150,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.DisplayInfo.ShouldContain("John Doe");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests computed properties update does not break.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComputedProperties_UpdateDoesNotBreak()
|
||||
{
|
||||
@@ -170,6 +188,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#region Advanced Collections Tests
|
||||
|
||||
/// <summary>
|
||||
/// Tests hash set serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HashSet_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -197,6 +218,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.Tags.ShouldContain("tag3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests iset serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ISet_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -225,6 +249,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.Numbers.ShouldContain(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests linked list serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LinkedList_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -253,6 +280,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
historyList[2].ShouldBe("third");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests queue serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Queue_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -280,6 +310,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
items.ShouldContain("item3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests stack serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Stack_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -307,6 +340,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
items.ShouldContain("action3");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests hash set with nested objects serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HashSet_WithNestedObjects_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -334,6 +370,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
addressList.ShouldContain(a => a.Street == "456 Oak Ave" && a.City.Name == "LA");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests iset with nested objects serializes and deserializes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ISet_WithNestedObjects_SerializesAndDeserializes()
|
||||
{
|
||||
@@ -363,6 +402,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
cityNames.ShouldContain("London");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests advanced collections all types in single entity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AdvancedCollections_AllTypesInSingleEntity()
|
||||
{
|
||||
@@ -411,6 +453,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#region Private Setters Tests
|
||||
|
||||
/// <summary>
|
||||
/// Tests entity with private setters can be deserialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityWithPrivateSetters_CanBeDeserialized()
|
||||
{
|
||||
@@ -429,6 +474,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.Age.ShouldBe(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests entity with private setters update works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityWithPrivateSetters_Update_Works()
|
||||
{
|
||||
@@ -452,6 +500,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.Age.ShouldBe(35);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests entity with private setters query works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityWithPrivateSetters_Query_Works()
|
||||
{
|
||||
@@ -478,6 +529,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#region Init-Only Setters Tests
|
||||
|
||||
/// <summary>
|
||||
/// Tests entity with init setters can be deserialized.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityWithInitSetters_CanBeDeserialized()
|
||||
{
|
||||
@@ -502,6 +556,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
retrieved.Age.ShouldBe(28);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests entity with init setters query works.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EntityWithInitSetters_Query_Works()
|
||||
{
|
||||
@@ -526,6 +583,9 @@ public class SourceGeneratorFeaturesTests : IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resources used by this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Dispose();
|
||||
|
||||
@@ -13,6 +13,9 @@ public class StorageEngineDictionaryTests
|
||||
if (File.Exists(Path.ChangeExtension(path, ".wal"))) File.Delete(Path.ChangeExtension(path, ".wal"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dictionary pages are initialized and return normalized keys.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_ShouldInitializeDictionary()
|
||||
{
|
||||
@@ -32,6 +35,9 @@ public class StorageEngineDictionaryTests
|
||||
finally { Cleanup(path); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dictionary entries persist across reopen.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_ShouldPersistDictionary()
|
||||
{
|
||||
@@ -61,6 +67,9 @@ public class StorageEngineDictionaryTests
|
||||
finally { Cleanup(path); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dictionary handling scales to many keys and remains durable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StorageEngine_ShouldHandleManyKeys()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class StorageEngineTransactionProtocolTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies preparing an unknown transaction returns false.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PrepareTransaction_Should_ReturnFalse_For_Unknown_Transaction()
|
||||
{
|
||||
@@ -20,6 +23,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies committing a detached transaction object throws.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CommitTransaction_With_TransactionObject_Should_Throw_When_Not_Active()
|
||||
{
|
||||
@@ -37,6 +43,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies committing a transaction object persists writes and clears active state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CommitTransaction_With_TransactionObject_Should_Commit_Writes()
|
||||
{
|
||||
@@ -65,6 +74,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies committing by identifier with no writes does not throw.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CommitTransaction_ById_With_NoWrites_Should_Not_Throw()
|
||||
{
|
||||
@@ -80,6 +92,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies committed transaction cache moves into readable state and active count is cleared.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MarkTransactionCommitted_Should_Move_Cache_And_Clear_ActiveCount()
|
||||
{
|
||||
@@ -108,6 +123,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies rollback discards uncommitted page writes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RollbackTransaction_Should_Discard_Uncommitted_Write()
|
||||
{
|
||||
@@ -140,6 +158,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies marking a transaction committed transitions state correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Transaction_MarkCommitted_Should_Transition_State()
|
||||
{
|
||||
@@ -169,6 +190,9 @@ public class StorageEngineTransactionProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies preparing then committing writes WAL data and updates transaction state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Transaction_Prepare_Should_Write_Wal_And_Transition_State()
|
||||
{
|
||||
|
||||
@@ -11,12 +11,18 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
private readonly Shared.TestDbContext _db;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TemporalTypesTests"/> class.
|
||||
/// </summary>
|
||||
public TemporalTypesTests()
|
||||
{
|
||||
_dbPath = $"temporal_test_{Guid.NewGuid()}.db";
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Dispose();
|
||||
@@ -24,12 +30,18 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity collection initialization.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Collection_IsInitialized()
|
||||
{
|
||||
_db.TemporalEntities.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal fields round-trip through insert and lookup.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_And_FindById_Works()
|
||||
{
|
||||
@@ -87,6 +99,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
retrieved.ClosingTime!.Value.ShouldBe(entity.ClosingTime!.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies insert behavior when optional temporal fields are null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Insert_WithNullOptionalFields_Works()
|
||||
{
|
||||
@@ -120,6 +135,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
retrieved.ClosingTime.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies temporal entity updates persist correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Update_Works()
|
||||
{
|
||||
@@ -155,6 +173,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
retrieved.OpeningTime.ShouldBe(entity.OpeningTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies querying temporal entities by temporal fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TemporalEntity_Query_Works()
|
||||
{
|
||||
@@ -197,6 +218,9 @@ namespace ZB.MOM.WW.CBDD.Tests
|
||||
results[0].Name.ShouldBe("Person 1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies edge-case TimeSpan values are persisted correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TimeSpan_EdgeCases_Work()
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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.Compression;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Core.Metadata;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
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.Compression;
|
||||
using ZB.MOM.WW.CBDD.Core.Indexing;
|
||||
using ZB.MOM.WW.CBDD.Core.Metadata;
|
||||
using ZB.MOM.WW.CBDD.Core.Storage;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Shared;
|
||||
|
||||
@@ -14,69 +14,178 @@ namespace ZB.MOM.WW.CBDD.Shared;
|
||||
/// </summary>
|
||||
public partial class TestDbContext : DocumentDbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the AnnotatedUsers.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, AnnotatedUser> AnnotatedUsers { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the Orders.
|
||||
/// </summary>
|
||||
public DocumentCollection<OrderId, Order> Orders { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the TestDocuments.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, TestDocument> TestDocuments { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the OrderDocuments.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, OrderDocument> OrderDocuments { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the ComplexDocuments.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, ComplexDocument> ComplexDocuments { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the Users.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, User> Users { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the ComplexUsers.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, ComplexUser> ComplexUsers { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the AutoInitEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, AutoInitEntity> AutoInitEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the People.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, Person> People { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the PeopleV2.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, PersonV2> PeopleV2 { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the Products.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, Product> Products { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the IntEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, IntEntity> IntEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the StringEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<string, StringEntity> StringEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the GuidEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<Guid, GuidEntity> GuidEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the CustomKeyEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<string, CustomKeyEntity> CustomKeyEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the AsyncDocs.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, AsyncDoc> AsyncDocs { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the SchemaUsers.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, SchemaUser> SchemaUsers { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the VectorItems.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, VectorEntity> VectorItems { get; set; } = null!;
|
||||
public DocumentCollection<ObjectId, GeoEntity> GeoItems { get; set; } = null!;
|
||||
|
||||
// Source Generator Feature Tests
|
||||
/// <summary>
|
||||
/// Gets or sets the GeoItems.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, GeoEntity> GeoItems { get; set; } = null!;
|
||||
|
||||
// Source Generator Feature Tests
|
||||
/// <summary>
|
||||
/// Gets or sets the DerivedEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, DerivedEntity> DerivedEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the ComputedPropertyEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, EntityWithComputedProperties> ComputedPropertyEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the AdvancedCollectionEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, EntityWithAdvancedCollections> AdvancedCollectionEntities { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the PrivateSetterEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, EntityWithPrivateSetters> PrivateSetterEntities { get; set; } = null!;
|
||||
public DocumentCollection<ObjectId, EntityWithInitSetters> InitSetterEntities { get; set; } = null!;
|
||||
|
||||
// Circular Reference Tests
|
||||
/// <summary>
|
||||
/// Gets or sets the InitSetterEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, EntityWithInitSetters> InitSetterEntities { get; set; } = null!;
|
||||
|
||||
// Circular Reference Tests
|
||||
/// <summary>
|
||||
/// Gets or sets the Employees.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, Employee> Employees { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// Gets or sets the CategoryRefs.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, CategoryRef> CategoryRefs { get; set; } = null!;
|
||||
public DocumentCollection<ObjectId, ProductRef> ProductRefs { get; set; } = null!;
|
||||
|
||||
// Nullable String Id Test (UuidEntity scenario with inheritance)
|
||||
public DocumentCollection<string, MockCounter> MockCounters { get; set; } = null!;
|
||||
|
||||
// Temporal Types Test (DateTimeOffset, TimeSpan, DateOnly, TimeOnly)
|
||||
/// <summary>
|
||||
/// Gets or sets the ProductRefs.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, ProductRef> ProductRefs { get; set; } = null!;
|
||||
|
||||
// Nullable String Id Test (UuidEntity scenario with inheritance)
|
||||
/// <summary>
|
||||
/// Gets or sets the MockCounters.
|
||||
/// </summary>
|
||||
public DocumentCollection<string, MockCounter> MockCounters { get; set; } = null!;
|
||||
|
||||
// Temporal Types Test (DateTimeOffset, TimeSpan, DateOnly, TimeOnly)
|
||||
/// <summary>
|
||||
/// Gets or sets the TemporalEntities.
|
||||
/// </summary>
|
||||
public DocumentCollection<ObjectId, TemporalEntity> TemporalEntities { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The database path.</param>
|
||||
public TestDbContext(string databasePath)
|
||||
: this(databasePath, PageFileConfig.Default, CompressionOptions.Default)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The database path.</param>
|
||||
/// <param name="compressionOptions">The compression options.</param>
|
||||
public TestDbContext(string databasePath, CompressionOptions compressionOptions)
|
||||
: this(databasePath, PageFileConfig.Default, compressionOptions)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The database path.</param>
|
||||
/// <param name="pageFileConfig">The page file configuration.</param>
|
||||
public TestDbContext(string databasePath, PageFileConfig pageFileConfig)
|
||||
: this(databasePath, pageFileConfig, CompressionOptions.Default)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The database path.</param>
|
||||
/// <param name="pageFileConfig">The page file configuration.</param>
|
||||
/// <param name="compressionOptions">The compression options.</param>
|
||||
/// <param name="maintenanceOptions">The maintenance options.</param>
|
||||
public TestDbContext(
|
||||
string databasePath,
|
||||
PageFileConfig pageFileConfig,
|
||||
CompressionOptions? compressionOptions,
|
||||
MaintenanceOptions? maintenanceOptions = null)
|
||||
: base(databasePath, pageFileConfig, compressionOptions, maintenanceOptions)
|
||||
{
|
||||
}
|
||||
: base(databasePath, pageFileConfig, compressionOptions, maintenanceOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<AnnotatedUser>();
|
||||
modelBuilder.Entity<User>().ToCollection("users");
|
||||
@@ -93,8 +202,8 @@ public partial class TestDbContext : DocumentDbContext
|
||||
modelBuilder.Entity<SchemaUser>().ToCollection("schema_users").HasKey(e => e.Id);
|
||||
modelBuilder.Entity<TestDocument>();
|
||||
modelBuilder.Entity<OrderDocument>();
|
||||
modelBuilder.Entity<ComplexDocument>();
|
||||
|
||||
modelBuilder.Entity<ComplexDocument>();
|
||||
|
||||
modelBuilder.Entity<VectorEntity>()
|
||||
.ToCollection("vector_items")
|
||||
.HasVectorIndex(x => x.Embedding, dimensions: 3, metric: VectorMetric.L2, name: "idx_vector");
|
||||
@@ -112,24 +221,30 @@ public partial class TestDbContext : DocumentDbContext
|
||||
modelBuilder.Entity<EntityWithComputedProperties>().ToCollection("computed_property_entities");
|
||||
modelBuilder.Entity<EntityWithAdvancedCollections>().ToCollection("advanced_collection_entities");
|
||||
modelBuilder.Entity<EntityWithPrivateSetters>().ToCollection("private_setter_entities");
|
||||
modelBuilder.Entity<EntityWithInitSetters>().ToCollection("init_setter_entities");
|
||||
|
||||
// Circular Reference Tests
|
||||
modelBuilder.Entity<EntityWithInitSetters>().ToCollection("init_setter_entities");
|
||||
|
||||
// Circular Reference Tests
|
||||
modelBuilder.Entity<Employee>().ToCollection("employees");
|
||||
modelBuilder.Entity<CategoryRef>().ToCollection("category_refs");
|
||||
modelBuilder.Entity<ProductRef>().ToCollection("product_refs");
|
||||
|
||||
// Nullable String Id Test (UuidEntity scenario)
|
||||
modelBuilder.Entity<MockCounter>().ToCollection("mock_counters").HasKey(e => e.Id);
|
||||
|
||||
// Temporal Types Test
|
||||
modelBuilder.Entity<ProductRef>().ToCollection("product_refs");
|
||||
|
||||
// Nullable String Id Test (UuidEntity scenario)
|
||||
modelBuilder.Entity<MockCounter>().ToCollection("mock_counters").HasKey(e => e.Id);
|
||||
|
||||
// Temporal Types Test
|
||||
modelBuilder.Entity<TemporalEntity>().ToCollection("temporal_entities").HasKey(e => e.Id);
|
||||
}
|
||||
|
||||
public void ForceCheckpoint()
|
||||
{
|
||||
Engine.Checkpoint();
|
||||
}
|
||||
|
||||
public StorageEngine Storage => Engine;
|
||||
}
|
||||
/// <summary>
|
||||
/// Executes ForceCheckpoint.
|
||||
/// </summary>
|
||||
public void ForceCheckpoint()
|
||||
{
|
||||
Engine.Checkpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Storage.
|
||||
/// </summary>
|
||||
public StorageEngine Storage => Engine;
|
||||
}
|
||||
|
||||
@@ -9,17 +9,25 @@ namespace ZB.MOM.WW.CBDD.Shared;
|
||||
/// </summary>
|
||||
public partial class TestExtendedDbContext : TestDbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the extended entities.
|
||||
/// </summary>
|
||||
public DocumentCollection<int, ExtendedEntity> ExtendedEntities { get; set; } = null!;
|
||||
|
||||
public TestExtendedDbContext(string databasePath) : base(databasePath)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TestExtendedDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">Database file path.</param>
|
||||
public TestExtendedDbContext(string databasePath) : base(databasePath)
|
||||
{
|
||||
InitializeCollections();
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<ExtendedEntity>()
|
||||
.ToCollection("extended_entities")
|
||||
.HasKey(e => e.Id);
|
||||
|
||||
@@ -12,12 +12,18 @@ public class ValueObjectIdTests : IDisposable
|
||||
private readonly string _dbPath = "value_object_ids.db";
|
||||
private readonly Shared.TestDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValueObjectIdTests"/> class.
|
||||
/// </summary>
|
||||
public ValueObjectIdTests()
|
||||
{
|
||||
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Should_Support_ValueObject_Id_Conversion.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Should_Support_ValueObject_Id_Conversion()
|
||||
{
|
||||
@@ -36,6 +42,9 @@ public class ValueObjectIdTests : IDisposable
|
||||
retrieved.CustomerName.ShouldBe("John Doe");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes Dispose.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class VectorMathTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies distance calculations across all supported vector metrics.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Distance_Should_Cover_All_Metrics()
|
||||
{
|
||||
@@ -21,6 +24,9 @@ public class VectorMathTests
|
||||
MathF.Abs(cosineDistance - expectedCosine).ShouldBeLessThan(0.0001f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies cosine similarity returns zero when one vector has zero magnitude.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CosineSimilarity_Should_Return_Zero_For_ZeroMagnitude_Vector()
|
||||
{
|
||||
@@ -30,6 +36,9 @@ public class VectorMathTests
|
||||
VectorMath.CosineSimilarity(v1, v2).ShouldBe(0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies dot product throws for mismatched vector lengths.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DotProduct_Should_Throw_For_Length_Mismatch()
|
||||
{
|
||||
@@ -39,6 +48,9 @@ public class VectorMathTests
|
||||
Should.Throw<ArgumentException>(() => VectorMath.DotProduct(v1, v2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies squared Euclidean distance throws for mismatched vector lengths.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EuclideanDistanceSquared_Should_Throw_For_Length_Mismatch()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.CBDD.Tests;
|
||||
|
||||
public class VectorSearchTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies basic vector-search query behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_VectorSearch_Basic()
|
||||
{
|
||||
|
||||
@@ -10,12 +10,21 @@ public class VisibilityTests
|
||||
public class VisibilityEntity
|
||||
{
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the normal prop.
|
||||
/// </summary>
|
||||
public int NormalProp { get; set; }
|
||||
|
||||
// Should be included (serialization usually writes it)
|
||||
/// <summary>
|
||||
/// Gets or sets the private set prop.
|
||||
/// </summary>
|
||||
public int PrivateSetProp { get; private set; }
|
||||
|
||||
// Should be included
|
||||
/// <summary>
|
||||
/// Gets or sets the init prop.
|
||||
/// </summary>
|
||||
public int InitProp { get; init; }
|
||||
|
||||
// Fields - typically included in BSON if public, but reflection need GetFields
|
||||
@@ -25,9 +34,16 @@ public class VisibilityTests
|
||||
private int _privateField;
|
||||
|
||||
// Helper to set private
|
||||
/// <summary>
|
||||
/// Tests set private.
|
||||
/// </summary>
|
||||
/// <param name="val">Value assigned to the private field.</param>
|
||||
public void SetPrivate(int val) => _privateField = val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests generate schema visibility checks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GenerateSchema_VisibilityChecks()
|
||||
{
|
||||
|
||||
@@ -18,6 +18,10 @@ public class WalIndexTests : IDisposable
|
||||
private readonly Shared.TestDbContext _db;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WalIndexTests"/> class.
|
||||
/// </summary>
|
||||
/// <param name="output">Test output sink.</param>
|
||||
public WalIndexTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
@@ -28,6 +32,9 @@ public class WalIndexTests : IDisposable
|
||||
_db = new Shared.TestDbContext(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies index writes are recorded in the WAL.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndexWritesAreLoggedToWal()
|
||||
{
|
||||
@@ -87,6 +94,9 @@ public class WalIndexTests : IDisposable
|
||||
return (PageType)pageData[4]; // Casting byte to PageType
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies offline compaction leaves the WAL empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun()
|
||||
{
|
||||
@@ -110,6 +120,9 @@ public class WalIndexTests : IDisposable
|
||||
new FileInfo(_walPath).Length.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies WAL recovery followed by compaction preserves data.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData()
|
||||
{
|
||||
@@ -153,6 +166,9 @@ public class WalIndexTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases test resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user