Reformat / cleanup
All checks were successful
NuGet Publish / build-and-pack (push) Successful in 46s
NuGet Publish / publish-to-gitea (push) Successful in 56s

This commit is contained in:
Joseph Doherty
2026-02-21 08:10:36 -05:00
parent 4c6aaa5a3f
commit a70d8befae
176 changed files with 50555 additions and 49587 deletions

View File

@@ -6,44 +6,37 @@ namespace ZB.MOM.WW.CBDD.Tests;
public class BTreeDeleteUnderflowTests
{
/// <summary>
/// Executes Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges.
/// Executes Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges.
/// </summary>
[Fact]
public void Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"btree_underflow_{Guid.NewGuid():N}.db");
string dbPath = Path.Combine(Path.GetTempPath(), $"btree_underflow_{Guid.NewGuid():N}.db");
try
{
using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
var index = new BTreeIndex(storage, IndexOptions.CreateBTree("k"));
var insertTxn = storage.BeginTransaction().TransactionId;
for (int i = 1; i <= 240; i++)
{
ulong insertTxn = storage.BeginTransaction().TransactionId;
for (var i = 1; i <= 240; i++)
index.Insert(IndexKey.Create(i), new DocumentLocation((uint)(1000 + i), 0), insertTxn);
}
storage.CommitTransaction(insertTxn);
var deleteTxn = storage.BeginTransaction().TransactionId;
for (int i = 1; i <= 190; i++)
{
ulong deleteTxn = storage.BeginTransaction().TransactionId;
for (var i = 1; i <= 190; i++)
index.Delete(IndexKey.Create(i), new DocumentLocation((uint)(1000 + i), 0), deleteTxn).ShouldBeTrue();
}
storage.CommitTransaction(deleteTxn);
for (int i = 1; i <= 190; i++)
{
index.TryFind(IndexKey.Create(i), out _, 0).ShouldBeFalse();
}
for (var i = 1; i <= 190; i++) index.TryFind(IndexKey.Create(i), out _, 0).ShouldBeFalse();
for (int i = 191; i <= 240; i++)
for (var i = 191; i <= 240; i++)
{
index.TryFind(IndexKey.Create(i), out var location, 0).ShouldBeTrue();
location.PageId.ShouldBe((uint)(1000 + i));
}
var remaining = index.GreaterThan(IndexKey.Create(190), orEqual: false, 0).ToList();
var remaining = index.GreaterThan(IndexKey.Create(190), false, 0).ToList();
remaining.Count.ShouldBe(50);
remaining.First().Key.ShouldBe(IndexKey.Create(191));
remaining.Last().Key.ShouldBe(IndexKey.Create(240));
@@ -51,8 +44,8 @@ public class BTreeDeleteUnderflowTests
finally
{
if (File.Exists(dbPath)) File.Delete(dbPath);
var walPath = Path.ChangeExtension(dbPath, ".wal");
string walPath = Path.ChangeExtension(dbPath, ".wal");
if (File.Exists(walPath)) File.Delete(walPath);
}
}
}
}

View File

@@ -8,20 +8,20 @@ namespace ZB.MOM.WW.CBDD.Tests;
public class CollectionIndexManagerAndDefinitionTests
{
/// <summary>
/// Tests find best index should prefer unique index.
/// Tests find best index should prefer unique index.
/// </summary>
[Fact]
public void FindBestIndex_Should_Prefer_Unique_Index()
{
var dbPath = NewDbPath();
string dbPath = NewDbPath();
try
{
using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
var mapper = new ZB_MOM_WW_CBDD_Shared_PersonMapper();
using var manager = new CollectionIndexManager<int, Person>(storage, mapper, "people_idx_pref_unique");
manager.CreateIndex(p => p.Age, name: "idx_age", unique: false);
manager.CreateIndex(p => p.Age, name: "idx_age_unique", unique: true);
manager.CreateIndex(p => p.Age, "idx_age");
manager.CreateIndex(p => p.Age, "idx_age_unique", true);
var best = manager.FindBestIndex("Age");
@@ -36,12 +36,12 @@ public class CollectionIndexManagerAndDefinitionTests
}
/// <summary>
/// Tests find best compound index should choose longest prefix.
/// Tests find best compound index should choose longest prefix.
/// </summary>
[Fact]
public void FindBestCompoundIndex_Should_Choose_Longest_Prefix()
{
var dbPath = NewDbPath();
string dbPath = NewDbPath();
try
{
using var storage = new StorageEngine(dbPath, PageFileConfig.Default);
@@ -76,12 +76,12 @@ public class CollectionIndexManagerAndDefinitionTests
}
/// <summary>
/// Tests drop index should remove metadata and be idempotent.
/// Tests drop index should remove metadata and be idempotent.
/// </summary>
[Fact]
public void DropIndex_Should_Remove_Metadata_And_Be_Idempotent()
{
var dbPath = NewDbPath();
string dbPath = NewDbPath();
const string collectionName = "people_idx_drop";
try
@@ -91,7 +91,7 @@ public class CollectionIndexManagerAndDefinitionTests
using (var manager = new CollectionIndexManager<int, Person>(storage, mapper, collectionName))
{
manager.CreateIndex(p => p.Age, name: "idx_age", unique: false);
manager.CreateIndex(p => p.Age, "idx_age");
manager.DropIndex("idx_age").ShouldBeTrue();
manager.DropIndex("idx_age").ShouldBeFalse();
manager.GetIndexInfo().ShouldBeEmpty();
@@ -107,7 +107,7 @@ public class CollectionIndexManagerAndDefinitionTests
}
/// <summary>
/// Tests collection index definition should respect query support rules.
/// Tests collection index definition should respect query support rules.
/// </summary>
[Fact]
public void CollectionIndexDefinition_Should_Respect_Query_Support_Rules()
@@ -129,7 +129,7 @@ public class CollectionIndexManagerAndDefinitionTests
}
/// <summary>
/// Tests collection index info to string should include diagnostics.
/// Tests collection index info to string should include diagnostics.
/// </summary>
[Fact]
public void CollectionIndexInfo_ToString_Should_Include_Diagnostics()
@@ -150,16 +150,18 @@ public class CollectionIndexManagerAndDefinitionTests
}
private static string NewDbPath()
=> Path.Combine(Path.GetTempPath(), $"idx_mgr_{Guid.NewGuid():N}.db");
{
return Path.Combine(Path.GetTempPath(), $"idx_mgr_{Guid.NewGuid():N}.db");
}
private static void CleanupFiles(string dbPath)
{
if (File.Exists(dbPath)) File.Delete(dbPath);
var walPath = Path.ChangeExtension(dbPath, ".wal");
string walPath = Path.ChangeExtension(dbPath, ".wal");
if (File.Exists(walPath)) File.Delete(walPath);
var altWalPath = dbPath + "-wal";
string altWalPath = dbPath + "-wal";
if (File.Exists(altWalPath)) File.Delete(altWalPath);
}
}
}

View File

@@ -1,18 +1,16 @@
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Bson;
using Xunit;
using ZB.MOM.WW.CBDD.Core.Storage;
namespace ZB.MOM.WW.CBDD.Tests;
public class CursorTests : IDisposable
{
private readonly string _testFile;
private readonly StorageEngine _storage;
private readonly BTreeIndex _index;
private readonly StorageEngine _storage;
private readonly string _testFile;
/// <summary>
/// Initializes a new instance of the <see cref="CursorTests"/> class.
/// Initializes a new instance of the <see cref="CursorTests" /> class.
/// </summary>
public CursorTests()
{
@@ -25,9 +23,18 @@ public class CursorTests : IDisposable
SeedData();
}
/// <summary>
/// Disposes the resources used by this instance.
/// </summary>
public void Dispose()
{
_storage.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
}
private void SeedData()
{
var txnId = _storage.BeginTransaction().TransactionId;
ulong txnId = _storage.BeginTransaction().TransactionId;
// Insert 10, 20, 30
_index.Insert(IndexKey.Create(10), new DocumentLocation(1, 0), txnId);
@@ -38,7 +45,7 @@ public class CursorTests : IDisposable
}
/// <summary>
/// Tests move to first should position at first.
/// Tests move to first should position at first.
/// </summary>
[Fact]
public void MoveToFirst_ShouldPositionAtFirst()
@@ -49,7 +56,7 @@ public class CursorTests : IDisposable
}
/// <summary>
/// Tests move to last should position at last.
/// Tests move to last should position at last.
/// </summary>
[Fact]
public void MoveToLast_ShouldPositionAtLast()
@@ -60,7 +67,7 @@ public class CursorTests : IDisposable
}
/// <summary>
/// Tests move next should traverse forward.
/// Tests move next should traverse forward.
/// </summary>
[Fact]
public void MoveNext_ShouldTraverseForward()
@@ -78,7 +85,7 @@ public class CursorTests : IDisposable
}
/// <summary>
/// Tests move prev should traverse backward.
/// Tests move prev should traverse backward.
/// </summary>
[Fact]
public void MovePrev_ShouldTraverseBackward()
@@ -96,7 +103,7 @@ public class CursorTests : IDisposable
}
/// <summary>
/// Tests seek should position exact or next.
/// Tests seek should position exact or next.
/// </summary>
[Fact]
public void Seek_ShouldPositionExact_OrNext()
@@ -116,13 +123,4 @@ public class CursorTests : IDisposable
// Current should throw invalid
Should.Throw<InvalidOperationException>(() => cursor.Current);
}
/// <summary>
/// Disposes the resources used by this instance.
/// </summary>
public void Dispose()
{
_storage.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
}
}
}

View File

@@ -4,34 +4,43 @@ namespace ZB.MOM.WW.CBDD.Tests;
public class GeospatialStressTests : IDisposable
{
private readonly TestDbContext _db;
private readonly string _dbPath;
private readonly Shared.TestDbContext _db;
/// <summary>
/// Initializes database state for geospatial stress tests.
/// 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);
_db = new TestDbContext(_dbPath);
}
/// <summary>
/// Verifies spatial index handles node splits and query operations under load.
/// Disposes test resources and removes generated files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_dbPath)) File.Delete(_dbPath);
string wal = Path.ChangeExtension(_dbPath, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
/// <summary>
/// Verifies spatial index handles node splits and query operations under load.
/// </summary>
[Fact]
public void SpatialIndex_Should_Handle_Node_Splits_And_Queries()
{
const int count = 350;
for (int i = 0; i < count; i++)
{
for (var i = 0; i < count; i++)
_db.GeoItems.Insert(new GeoEntity
{
Name = $"pt-{i}",
Location = (40.0 + (i * 0.001), -73.0 - (i * 0.001))
Location = (40.0 + i * 0.001, -73.0 - i * 0.001)
});
}
_db.SaveChanges();
@@ -45,15 +54,4 @@ public class GeospatialStressTests : IDisposable
var near = _db.GeoItems.Near("idx_spatial", (40.10, -73.10), 30.0).ToList();
near.Count.ShouldBeGreaterThan(0);
}
/// <summary>
/// Disposes test resources and removes generated files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_dbPath)) File.Delete(_dbPath);
var wal = Path.ChangeExtension(_dbPath, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
}
}

View File

@@ -1,28 +1,33 @@
using Xunit;
using ZB.MOM.WW.CBDD.Core.Indexing;
using System.IO;
using System.Linq;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Shared;
namespace ZB.MOM.WW.CBDD.Tests;
public class GeospatialTests : IDisposable
{
private readonly TestDbContext _db;
private readonly string _dbPath;
private readonly Shared.TestDbContext _db;
/// <summary>
/// Initializes a new instance of the <see cref="GeospatialTests"/> class.
/// 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);
_db = new TestDbContext(_dbPath);
}
/// <summary>
/// Verifies spatial within queries return expected results.
/// Disposes test resources and removes temporary files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_dbPath)) File.Delete(_dbPath);
}
/// <summary>
/// Verifies spatial within queries return expected results.
/// </summary>
[Fact]
public void Can_Insert_And_Search_Within()
@@ -45,7 +50,7 @@ public class GeospatialTests : IDisposable
}
/// <summary>
/// Verifies near queries return expected proximity results.
/// Verifies near queries return expected proximity results.
/// </summary>
[Fact]
public void Can_Search_Near_Proximity()
@@ -69,7 +74,7 @@ public class GeospatialTests : IDisposable
}
/// <summary>
/// Verifies LINQ near integration returns expected results.
/// Verifies LINQ near integration returns expected results.
/// </summary>
[Fact]
public void LINQ_Integration_Near_Works()
@@ -79,8 +84,8 @@ public class GeospatialTests : IDisposable
// LINQ query using .Near() extension
var query = from p in _db.GeoItems.AsQueryable()
where p.Location.Near(milan, 10.0)
select p;
where p.Location.Near(milan, 10.0)
select p;
var results = query.ToList();
@@ -89,7 +94,7 @@ public class GeospatialTests : IDisposable
}
/// <summary>
/// Verifies LINQ within integration returns expected results.
/// Verifies LINQ within integration returns expected results.
/// </summary>
[Fact]
public void LINQ_Integration_Within_Works()
@@ -102,19 +107,10 @@ public class GeospatialTests : IDisposable
// LINQ query using .Within() extension
var results = _db.GeoItems.AsQueryable()
.Where(p => p.Location.Within(min, max))
.ToList();
.Where(p => p.Location.Within(min, max))
.ToList();
results.Count().ShouldBe(1);
results[0].Name.ShouldBe("Milan Office");
}
/// <summary>
/// Disposes test resources and removes temporary files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_dbPath)) File.Delete(_dbPath);
}
}
}

View File

@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.CBDD.Tests;
public class HashIndexTests
{
/// <summary>
/// Executes Insert_And_TryFind_Should_Return_Location.
/// Executes Insert_And_TryFind_Should_Return_Location.
/// </summary>
[Fact]
public void Insert_And_TryFind_Should_Return_Location()
@@ -23,7 +23,7 @@ public class HashIndexTests
}
/// <summary>
/// Executes Unique_HashIndex_Should_Throw_On_Duplicate_Key.
/// Executes Unique_HashIndex_Should_Throw_On_Duplicate_Key.
/// </summary>
[Fact]
public void Unique_HashIndex_Should_Throw_On_Duplicate_Key()
@@ -45,7 +45,7 @@ public class HashIndexTests
}
/// <summary>
/// Executes Remove_Should_Remove_Only_Matching_Entry.
/// Executes Remove_Should_Remove_Only_Matching_Entry.
/// </summary>
[Fact]
public void Remove_Should_Remove_Only_Matching_Entry()
@@ -71,7 +71,7 @@ public class HashIndexTests
}
/// <summary>
/// Executes FindAll_Should_Return_All_Matching_Entries.
/// Executes FindAll_Should_Return_All_Matching_Entries.
/// </summary>
[Fact]
public void FindAll_Should_Return_All_Matching_Entries()
@@ -88,4 +88,4 @@ public class HashIndexTests
matches.Count.ShouldBe(2);
matches.All(e => e.Key == key).ShouldBeTrue();
}
}
}

View File

@@ -1,31 +1,25 @@
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Shared;
using System;
using Xunit;
namespace ZB.MOM.WW.CBDD.Tests;
public class IndexDirectionTests : IDisposable
{
private readonly TestDbContext _db;
private readonly string _dbPath = "index_direction_tests.db";
private readonly Shared.TestDbContext _db;
/// <summary>
/// Initializes database state for index direction tests.
/// Initializes database state for index direction tests.
/// </summary>
public IndexDirectionTests()
{
if (File.Exists(_dbPath)) File.Delete(_dbPath);
_db = new Shared.TestDbContext(_dbPath);
_db = new TestDbContext(_dbPath);
// _db.Database.EnsureCreated(); // Not needed/doesn't exist? StorageEngine handles creation.
}
/// <summary>
/// Disposes test resources and deletes temporary files.
/// Disposes test resources and deletes temporary files.
/// </summary>
public void Dispose()
{
@@ -34,7 +28,7 @@ public class IndexDirectionTests : IDisposable
}
/// <summary>
/// Verifies forward range scans return values in ascending order.
/// Verifies forward range scans return values in ascending order.
/// </summary>
[Fact]
public void Range_Forward_ReturnsOrderedResults()
@@ -42,20 +36,21 @@ public class IndexDirectionTests : IDisposable
var collection = _db.People;
var index = collection.EnsureIndex(p => p.Age, "idx_age");
var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList();
var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i })
.ToList();
collection.InsertBulk(people);
_db.SaveChanges();
// Scan Forward
var results = index.Range(10, 20, IndexDirection.Forward).ToList();
var results = index.Range(10, 20).ToList();
results.Count.ShouldBe(11); // 10 to 20 inclusive
collection.FindByLocation(results.First())!.Age.ShouldBe(10); // First is 10
collection.FindByLocation(results.Last())!.Age.ShouldBe(20); // Last is 20
collection.FindByLocation(results.Last())!.Age.ShouldBe(20); // Last is 20
}
/// <summary>
/// Verifies backward range scans return values in descending order.
/// Verifies backward range scans return values in descending order.
/// </summary>
[Fact]
public void Range_Backward_ReturnsReverseOrderedResults()
@@ -63,7 +58,8 @@ public class IndexDirectionTests : IDisposable
var collection = _db.People;
var index = collection.EnsureIndex(p => p.Age, "idx_age");
var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList();
var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i })
.ToList();
collection.InsertBulk(people);
_db.SaveChanges();
@@ -72,11 +68,11 @@ public class IndexDirectionTests : IDisposable
results.Count.ShouldBe(11); // 10 to 20 inclusive
collection.FindByLocation(results.First())!.Age.ShouldBe(20); // First is 20 (Reverse)
collection.FindByLocation(results.Last())!.Age.ShouldBe(10); // Last is 10
collection.FindByLocation(results.Last())!.Age.ShouldBe(10); // Last is 10
}
/// <summary>
/// Verifies backward scans across split index pages return complete result sets.
/// Verifies backward scans across split index pages return complete result sets.
/// </summary>
[Fact]
public void Range_Backward_WithMultiplePages_ReturnsReverseOrderedResults()
@@ -88,7 +84,8 @@ public class IndexDirectionTests : IDisposable
// Entry size approx 10 bytes key + 6 bytes loc + overhead
// 1000 items * 20 bytes = 20KB > 4KB.
var count = 1000;
var people = Enumerable.Range(1, count).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList();
var people = Enumerable.Range(1, count).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i })
.ToList();
collection.InsertBulk(people);
_db.SaveChanges();
@@ -105,4 +102,4 @@ public class IndexDirectionTests : IDisposable
// collection.FindByLocation(results.First(), null)!.Age.ShouldBe(count); // Max Age (Fails: Max is likely 255)
// collection.FindByLocation(results.Last(), null)!.Age.ShouldBe(1); // Min Age (Fails: Min is likely 256)
}
}
}

View File

@@ -1,163 +1,161 @@
using Xunit;
using ZB.MOM.WW.CBDD.Core.Query;
using ZB.MOM.WW.CBDD.Core.Indexing;
using System.Linq.Expressions;
using System.Collections.Generic;
using System;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Query;
namespace ZB.MOM.WW.CBDD.Tests
namespace ZB.MOM.WW.CBDD.Tests;
public class IndexOptimizationTests
{
public class IndexOptimizationTests
/// <summary>
/// Tests optimizer identifies equality.
/// </summary>
[Fact]
public void Optimizer_Identifies_Equality()
{
public class TestEntity
var indexes = new List<CollectionIndexInfo>
{
/// <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; }
}
new() { Name = "idx_age", PropertyPaths = ["Age"] }
};
/// <summary>
/// Tests optimizer identifies equality.
/// </summary>
[Fact]
public void Optimizer_Identifies_Equality()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age == 30;
var model = new QueryModel { WhereClause = predicate };
Expression<Func<TestEntity, bool>> predicate = x => x.Age == 30;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(30);
result.MaxValue.ShouldBe(30);
result.IsRange.ShouldBeFalse();
}
/// <summary>
/// Tests optimizer identifies range greater than.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_GreaterThan()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age > 25;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(25);
result.MaxValue.ShouldBeNull();
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies range less than.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_LessThan()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age < 50;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBeNull();
result.MaxValue.ShouldBe(50);
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies range between simulated.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_Between_Simulated()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age > 20 && x.Age < 40;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(20);
result.MaxValue.ShouldBe(40);
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies starts with.
/// </summary>
[Fact]
public void Optimizer_Identifies_StartsWith()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_name", PropertyPaths = ["Name"], Type = IndexType.BTree }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Name.StartsWith("Ali");
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_name");
result.MinValue.ShouldBe("Ali");
// "Ali" + next char -> "Alj"
result.MaxValue.ShouldBe("Alj");
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer ignores non indexed fields.
/// </summary>
[Fact]
public void Optimizer_Ignores_NonIndexed_Fields()
{
var indexes = new List<CollectionIndexInfo>
{
new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Name == "Alice"; // Name is not indexed
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldBeNull();
}
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(30);
result.MaxValue.ShouldBe(30);
result.IsRange.ShouldBeFalse();
}
}
/// <summary>
/// Tests optimizer identifies range greater than.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_GreaterThan()
{
var indexes = new List<CollectionIndexInfo>
{
new() { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age > 25;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(25);
result.MaxValue.ShouldBeNull();
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies range less than.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_LessThan()
{
var indexes = new List<CollectionIndexInfo>
{
new() { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age < 50;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBeNull();
result.MaxValue.ShouldBe(50);
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies range between simulated.
/// </summary>
[Fact]
public void Optimizer_Identifies_Range_Between_Simulated()
{
var indexes = new List<CollectionIndexInfo>
{
new() { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Age > 20 && x.Age < 40;
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_age");
result.MinValue.ShouldBe(20);
result.MaxValue.ShouldBe(40);
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer identifies starts with.
/// </summary>
[Fact]
public void Optimizer_Identifies_StartsWith()
{
var indexes = new List<CollectionIndexInfo>
{
new() { Name = "idx_name", PropertyPaths = ["Name"], Type = IndexType.BTree }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Name.StartsWith("Ali");
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldNotBeNull();
result.IndexName.ShouldBe("idx_name");
result.MinValue.ShouldBe("Ali");
// "Ali" + next char -> "Alj"
result.MaxValue.ShouldBe("Alj");
result.IsRange.ShouldBeTrue();
}
/// <summary>
/// Tests optimizer ignores non indexed fields.
/// </summary>
[Fact]
public void Optimizer_Ignores_NonIndexed_Fields()
{
var indexes = new List<CollectionIndexInfo>
{
new() { Name = "idx_age", PropertyPaths = ["Age"] }
};
Expression<Func<TestEntity, bool>> predicate = x => x.Name == "Alice"; // Name is not indexed
var model = new QueryModel { WhereClause = predicate };
var result = IndexOptimizer.TryOptimize<TestEntity>(model, indexes);
result.ShouldBeNull();
}
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; }
}
}

View File

@@ -1,10 +1,4 @@
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.Metadata;
using ZB.MOM.WW.CBDD.Shared;
using System;
using System.Buffers;
namespace ZB.MOM.WW.CBDD.Tests;
@@ -13,7 +7,7 @@ public class PrimaryKeyTests : IDisposable
private readonly string _dbPath = "primary_key_tests.db";
/// <summary>
/// Initializes a new instance of the <see cref="PrimaryKeyTests"/> class.
/// Initializes a new instance of the <see cref="PrimaryKeyTests" /> class.
/// </summary>
public PrimaryKeyTests()
{
@@ -21,7 +15,7 @@ public class PrimaryKeyTests : IDisposable
}
/// <summary>
/// Executes Dispose.
/// Executes Dispose.
/// </summary>
public void Dispose()
{
@@ -29,12 +23,12 @@ public class PrimaryKeyTests : IDisposable
}
/// <summary>
/// Executes Test_Int_PrimaryKey.
/// Executes Test_Int_PrimaryKey.
/// </summary>
[Fact]
public void Test_Int_PrimaryKey()
{
using var db = new Shared.TestDbContext(_dbPath);
using var db = new TestDbContext(_dbPath);
var entity = new IntEntity { Id = 1, Name = "Test 1" };
db.IntEntities.Insert(entity);
@@ -56,12 +50,12 @@ public class PrimaryKeyTests : IDisposable
}
/// <summary>
/// Executes Test_String_PrimaryKey.
/// Executes Test_String_PrimaryKey.
/// </summary>
[Fact]
public void Test_String_PrimaryKey()
{
using var db = new Shared.TestDbContext(_dbPath);
using var db = new TestDbContext(_dbPath);
var entity = new StringEntity { Id = "key1", Value = "Value 1" };
db.StringEntities.Insert(entity);
@@ -78,12 +72,12 @@ public class PrimaryKeyTests : IDisposable
}
/// <summary>
/// Executes Test_Guid_PrimaryKey.
/// Executes Test_Guid_PrimaryKey.
/// </summary>
[Fact]
public void Test_Guid_PrimaryKey()
{
using var db = new Shared.TestDbContext(_dbPath);
using var db = new TestDbContext(_dbPath);
var id = Guid.NewGuid();
var entity = new GuidEntity { Id = id, Name = "Guid Test" };
@@ -100,13 +94,13 @@ public class PrimaryKeyTests : IDisposable
}
/// <summary>
/// Executes Test_String_PrimaryKey_With_Custom_Name.
/// Executes Test_String_PrimaryKey_With_Custom_Name.
/// </summary>
[Fact]
public void Test_String_PrimaryKey_With_Custom_Name()
{
// Test entity with string key NOT named "Id" (named "Code" instead)
using var db = new Shared.TestDbContext(_dbPath);
using var db = new TestDbContext(_dbPath);
var entity = new CustomKeyEntity { Code = "ABC123", Description = "Test Description" };
db.CustomKeyEntities.Insert(entity);
@@ -131,4 +125,4 @@ public class PrimaryKeyTests : IDisposable
db.SaveChanges();
db.CustomKeyEntities.FindById("ABC123").ShouldBeNull();
}
}
}

View File

@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.CBDD.Tests;
public class VectorMathTests
{
/// <summary>
/// Verifies distance calculations across all supported vector metrics.
/// Verifies distance calculations across all supported vector metrics.
/// </summary>
[Fact]
public void Distance_Should_Cover_All_Metrics()
@@ -13,19 +13,19 @@ public class VectorMathTests
float[] v1 = [1f, 2f];
float[] v2 = [3f, 4f];
var cosineDistance = VectorMath.Distance(v1, v2, VectorMetric.Cosine);
var l2Distance = VectorMath.Distance(v1, v2, VectorMetric.L2);
var dotDistance = VectorMath.Distance(v1, v2, VectorMetric.DotProduct);
float cosineDistance = VectorMath.Distance(v1, v2, VectorMetric.Cosine);
float l2Distance = VectorMath.Distance(v1, v2, VectorMetric.L2);
float dotDistance = VectorMath.Distance(v1, v2, VectorMetric.DotProduct);
l2Distance.ShouldBe(8f);
dotDistance.ShouldBe(-11f);
var expectedCosine = 1f - (11f / (MathF.Sqrt(5f) * 5f));
float expectedCosine = 1f - 11f / (MathF.Sqrt(5f) * 5f);
MathF.Abs(cosineDistance - expectedCosine).ShouldBeLessThan(0.0001f);
}
/// <summary>
/// Verifies cosine similarity returns zero when one vector has zero magnitude.
/// Verifies cosine similarity returns zero when one vector has zero magnitude.
/// </summary>
[Fact]
public void CosineSimilarity_Should_Return_Zero_For_ZeroMagnitude_Vector()
@@ -37,7 +37,7 @@ public class VectorMathTests
}
/// <summary>
/// Verifies dot product throws for mismatched vector lengths.
/// Verifies dot product throws for mismatched vector lengths.
/// </summary>
[Fact]
public void DotProduct_Should_Throw_For_Length_Mismatch()
@@ -49,7 +49,7 @@ public class VectorMathTests
}
/// <summary>
/// Verifies squared Euclidean distance throws for mismatched vector lengths.
/// Verifies squared Euclidean distance throws for mismatched vector lengths.
/// </summary>
[Fact]
public void EuclideanDistanceSquared_Should_Throw_For_Length_Mismatch()
@@ -59,4 +59,4 @@ public class VectorMathTests
Should.Throw<ArgumentException>(() => VectorMath.EuclideanDistanceSquared(v1, v2));
}
}
}

View File

@@ -1,23 +1,20 @@
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Shared;
using Xunit;
namespace ZB.MOM.WW.CBDD.Tests;
public class VectorSearchTests
{
/// <summary>
/// Verifies basic vector-search query behavior.
/// Verifies basic vector-search query behavior.
/// </summary>
[Fact]
public void Test_VectorSearch_Basic()
{
string dbPath = "vector_test.db";
var dbPath = "vector_test.db";
if (File.Exists(dbPath)) File.Delete(dbPath);
using (var db = new Shared.TestDbContext(dbPath))
using (var db = new TestDbContext(dbPath))
{
db.VectorItems.Insert(new VectorEntity { Title = "Near", Embedding = [1.0f, 1.0f, 1.0f] });
db.VectorItems.Insert(new VectorEntity { Title = "Far", Embedding = [10.0f, 10.0f, 10.0f] });
@@ -31,4 +28,4 @@ public class VectorSearchTests
File.Delete(dbPath);
}
}
}

View File

@@ -1,25 +1,19 @@
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Transactions;
using ZB.MOM.WW.CBDD.Shared;
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
using System.Buffers;
using Xunit;
namespace ZB.MOM.WW.CBDD.Tests;
public class WalIndexTests : IDisposable
{
private readonly TestDbContext _db;
private readonly string _dbPath;
private readonly string _walPath;
private readonly Shared.TestDbContext _db;
private readonly ITestOutputHelper _output;
private readonly string _walPath;
/// <summary>
/// Initializes a new instance of the <see cref="WalIndexTests"/> class.
/// Initializes a new instance of the <see cref="WalIndexTests" /> class.
/// </summary>
/// <param name="output">Test output sink.</param>
public WalIndexTests(ITestOutputHelper output)
@@ -29,11 +23,41 @@ public class WalIndexTests : IDisposable
// WAL defaults to .wal next to db
_walPath = Path.ChangeExtension(_dbPath, ".wal");
_db = new Shared.TestDbContext(_dbPath);
_db = new TestDbContext(_dbPath);
}
/// <summary>
/// Verifies index writes are recorded in the WAL.
/// Releases test resources.
/// </summary>
public void Dispose()
{
try
{
_db?.Dispose(); // Safe to call multiple times
}
catch
{
}
try
{
if (File.Exists(_dbPath)) File.Delete(_dbPath);
}
catch
{
}
try
{
if (File.Exists(_walPath)) File.Delete(_walPath);
}
catch
{
}
}
/// <summary>
/// Verifies index writes are recorded in the WAL.
/// </summary>
[Fact]
public void IndexWritesAreLoggedToWal()
@@ -71,8 +95,8 @@ public class WalIndexTests : IDisposable
_output.WriteLine($"Found {writeRecords.Count} Write records for Txn {txn.TransactionId}");
// Analyze pages
int indexPageCount = 0;
int dataPageCount = 0;
var indexPageCount = 0;
var dataPageCount = 0;
foreach (var record in writeRecords)
{
@@ -89,21 +113,18 @@ public class WalIndexTests : IDisposable
private PageType ParsePageType(byte[]? pageData)
{
if (pageData == null || pageData.Length < 32) return (PageType)0;
if (pageData == null || pageData.Length < 32) return 0;
// PageType is at offset 4 (1 byte)
return (PageType)pageData[4]; // Casting byte to PageType
}
/// <summary>
/// Verifies offline compaction leaves the WAL empty.
/// Verifies offline compaction leaves the WAL empty.
/// </summary>
[Fact]
public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun()
{
for (var i = 0; i < 100; i++)
{
_db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 });
}
for (var i = 0; i < 100; i++) _db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 });
_db.SaveChanges();
_db.Storage.GetWalSize().ShouldBeGreaterThan(0);
@@ -121,24 +142,22 @@ public class WalIndexTests : IDisposable
}
/// <summary>
/// Verifies WAL recovery followed by compaction preserves data.
/// Verifies WAL recovery followed by compaction preserves data.
/// </summary>
[Fact]
public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db");
var walPath = Path.ChangeExtension(dbPath, ".wal");
string dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db");
string walPath = Path.ChangeExtension(dbPath, ".wal");
var markerPath = $"{dbPath}.compact.state";
var expectedIds = new List<ObjectId>();
try
{
using (var writer = new Shared.TestDbContext(dbPath))
using (var writer = new TestDbContext(dbPath))
{
for (var i = 0; i < 48; i++)
{
expectedIds.Add(writer.Users.Insert(new User { Name = $"recover-{i:D3}", Age = i % 10 }));
}
writer.SaveChanges();
writer.Storage.GetWalSize().ShouldBeGreaterThan(0);
@@ -146,16 +165,13 @@ public class WalIndexTests : IDisposable
new FileInfo(walPath).Length.ShouldBeGreaterThan(0);
using (var recovered = new Shared.TestDbContext(dbPath))
using (var recovered = new TestDbContext(dbPath))
{
recovered.Users.Count().ShouldBe(expectedIds.Count);
recovered.Compact();
recovered.Storage.GetWalSize().ShouldBe(0);
foreach (var id in expectedIds)
{
recovered.Users.FindById(id).ShouldNotBeNull();
}
foreach (var id in expectedIds) recovered.Users.FindById(id).ShouldNotBeNull();
}
}
finally
@@ -165,19 +181,4 @@ public class WalIndexTests : IDisposable
if (File.Exists(markerPath)) File.Delete(markerPath);
}
}
/// <summary>
/// Releases test resources.
/// </summary>
public void Dispose()
{
try
{
_db?.Dispose(); // Safe to call multiple times
}
catch { }
try { if (File.Exists(_dbPath)) File.Delete(_dbPath); } catch { }
try { if (File.Exists(_walPath)) File.Delete(_walPath); } catch { }
}
}
}