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

@@ -1,267 +1,259 @@
using Xunit;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Bson;
using System.Linq;
using System.Collections.Generic;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Storage;
using System;
using System.IO;
using ZB.MOM.WW.CBDD.Shared;
using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers;
namespace ZB.MOM.WW.CBDD.Tests
namespace ZB.MOM.WW.CBDD.Tests;
public class AdvancedQueryTests : IDisposable
{
public class AdvancedQueryTests : IDisposable
private readonly TestDbContext _db;
private readonly string _dbPath;
/// <summary>
/// Initializes test database state used by advanced query tests.
/// </summary>
public AdvancedQueryTests()
{
private readonly string _dbPath;
private readonly Shared.TestDbContext _db;
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_advanced_{Guid.NewGuid()}.db");
_db = new TestDbContext(_dbPath);
/// <summary>
/// Initializes test database state used by advanced query tests.
/// </summary>
public AdvancedQueryTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_advanced_{Guid.NewGuid()}.db");
_db = new Shared.TestDbContext(_dbPath);
// Seed Data
_db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 10, Name = "Item1" });
_db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 20, Name = "Item2" });
_db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 30, Name = "Item3" });
_db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 40, Name = "Item4" });
_db.TestDocuments.Insert(new TestDocument { Category = "C", Amount = 50, Name = "Item5" });
_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()
{
var groups = _db.TestDocuments.AsQueryable()
.GroupBy(x => x.Category)
.ToList();
groups.Count.ShouldBe(3);
var groupA = groups.First(g => g.Key == "A");
groupA.Count().ShouldBe(2);
groupA.ShouldContain(x => x.Amount == 10);
groupA.ShouldContain(x => x.Amount == 20);
var groupB = groups.First(g => g.Key == "B");
groupB.Count().ShouldBe(2);
var groupC = groups.First(g => g.Key == "C");
groupC.Count().ShouldBe(1);
}
/// <summary>
/// Verifies grouped projection with aggregation returns expected totals.
/// </summary>
[Fact]
public void GroupBy_With_Aggregation_Select()
{
var results = _db.TestDocuments.AsQueryable()
.GroupBy(x => x.Category)
.Select(g => new { Category = g.Key, Total = g.Sum(x => x.Amount) })
.OrderBy(x => x.Category)
.ToList();
results.Count.ShouldBe(3);
results[0].Category.ShouldBe("A");
results[0].Total.ShouldBe(30); // 10 + 20
results[1].Category.ShouldBe("B");
results[1].Total.ShouldBe(70); // 30 + 40
results[2].Category.ShouldBe("C");
results[2].Total.ShouldBe(50); // 50
}
/// <summary>
/// Verifies direct aggregate operators return expected values.
/// </summary>
[Fact]
public void Aggregations_Direct_Works()
{
var query = _db.TestDocuments.AsQueryable();
query.Count().ShouldBe(5);
query.Sum(x => x.Amount).ShouldBe(150);
query.Average(x => x.Amount).ShouldBe(30.0);
query.Min(x => x.Amount).ShouldBe(10);
query.Max(x => x.Amount).ShouldBe(50);
}
/// <summary>
/// Verifies aggregate operators with predicates return expected values.
/// </summary>
[Fact]
public void Aggregations_With_Predicate_Works()
{
var query = _db.TestDocuments.AsQueryable().Where(x => x.Category == "A");
query.Count().ShouldBe(2);
query.Sum(x => x.Amount).ShouldBe(30);
}
/// <summary>
/// Verifies in-memory join query execution returns expected rows.
/// </summary>
[Fact]
public void Join_Works_InMemory()
{
// Create a second collection for joining
_db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item1", Quantity = 5 });
_db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item3", Quantity = 2 });
_db.SaveChanges();
var query = _db.TestDocuments.AsQueryable()
.Join(_db.OrderDocuments.AsQueryable(),
doc => doc.Name,
order => order.ItemName,
(doc, order) => new { doc.Name, doc.Category, order.Quantity })
.OrderBy(x => x.Name)
.ToList();
query.Count.ShouldBe(2);
query[0].Name.ShouldBe("Item1");
query[0].Category.ShouldBe("A");
query[0].Quantity.ShouldBe(5);
query[1].Name.ShouldBe("Item3");
query[1].Category.ShouldBe("B");
query[1].Quantity.ShouldBe(2);
}
/// <summary>
/// Verifies projection of nested object properties works.
/// </summary>
[Fact]
public void Select_Project_Nested_Object()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" },
Items = new List<OrderItem>
{
new OrderItem { Name = "Laptop", Price = 1000 },
new OrderItem { Name = "Mouse", Price = 50 }
}
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var query = _db.ComplexDocuments.AsQueryable()
.Select(x => x.ShippingAddress)
.ToList();
query.Count().ShouldBe(1);
query[0].City.Name.ShouldBe("New York");
query[0].Street.ShouldBe("5th Ave");
}
/// <summary>
/// Verifies projection of nested scalar fields works.
/// </summary>
[Fact]
public void Select_Project_Nested_Field()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var cities = _db.ComplexDocuments.AsQueryable()
.Select(x => x.ShippingAddress.City.Name)
.ToList();
cities.Count().ShouldBe(1);
cities[0].ShouldBe("New York");
}
/// <summary>
/// Verifies anonymous projection including nested values works.
/// </summary>
[Fact]
public void Select_Anonymous_Complex()
{
ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers.ZB_MOM_WW_CBDD_Shared_CityMapper cityMapper = new ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers.ZB_MOM_WW_CBDD_Shared_CityMapper();
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var result = _db.ComplexDocuments.AsQueryable()
.Select(x => new { x.Title, x.ShippingAddress.City })
.ToList();
result.Count().ShouldBe(1);
result[0].Title.ShouldBe("Order1");
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()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order with Items",
ShippingAddress = new Address { City = new City { Name = "Los Angeles" }, Street = "Hollywood Blvd" },
Items = new List<OrderItem>
{
new OrderItem { Name = "Laptop", Price = 1500 },
new OrderItem { Name = "Mouse", Price = 25 },
new OrderItem { Name = "Keyboard", Price = 75 }
}
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
// Retrieve the full document and verify Items array
var retrieved = _db.ComplexDocuments.FindAll().First();
retrieved.Title.ShouldBe("Order with Items");
retrieved.ShippingAddress.City.Name.ShouldBe("Los Angeles");
retrieved.ShippingAddress.Street.ShouldBe("Hollywood Blvd");
// Verify array of nested objects
retrieved.Items.Count.ShouldBe(3);
retrieved.Items[0].Name.ShouldBe("Laptop");
retrieved.Items[0].Price.ShouldBe(1500);
retrieved.Items[1].Name.ShouldBe("Mouse");
retrieved.Items[1].Price.ShouldBe(25);
retrieved.Items[2].Name.ShouldBe("Keyboard");
retrieved.Items[2].Price.ShouldBe(75);
}
// Seed Data
_db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 10, Name = "Item1" });
_db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 20, Name = "Item2" });
_db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 30, Name = "Item3" });
_db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 40, Name = "Item4" });
_db.TestDocuments.Insert(new TestDocument { Category = "C", Amount = 50, Name = "Item5" });
_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()
{
var groups = _db.TestDocuments.AsQueryable()
.GroupBy(x => x.Category)
.ToList();
groups.Count.ShouldBe(3);
var groupA = groups.First(g => g.Key == "A");
groupA.Count().ShouldBe(2);
groupA.ShouldContain(x => x.Amount == 10);
groupA.ShouldContain(x => x.Amount == 20);
var groupB = groups.First(g => g.Key == "B");
groupB.Count().ShouldBe(2);
var groupC = groups.First(g => g.Key == "C");
groupC.Count().ShouldBe(1);
}
/// <summary>
/// Verifies grouped projection with aggregation returns expected totals.
/// </summary>
[Fact]
public void GroupBy_With_Aggregation_Select()
{
var results = _db.TestDocuments.AsQueryable()
.GroupBy(x => x.Category)
.Select(g => new { Category = g.Key, Total = g.Sum(x => x.Amount) })
.OrderBy(x => x.Category)
.ToList();
results.Count.ShouldBe(3);
results[0].Category.ShouldBe("A");
results[0].Total.ShouldBe(30); // 10 + 20
results[1].Category.ShouldBe("B");
results[1].Total.ShouldBe(70); // 30 + 40
results[2].Category.ShouldBe("C");
results[2].Total.ShouldBe(50); // 50
}
/// <summary>
/// Verifies direct aggregate operators return expected values.
/// </summary>
[Fact]
public void Aggregations_Direct_Works()
{
var query = _db.TestDocuments.AsQueryable();
query.Count().ShouldBe(5);
query.Sum(x => x.Amount).ShouldBe(150);
query.Average(x => x.Amount).ShouldBe(30.0);
query.Min(x => x.Amount).ShouldBe(10);
query.Max(x => x.Amount).ShouldBe(50);
}
/// <summary>
/// Verifies aggregate operators with predicates return expected values.
/// </summary>
[Fact]
public void Aggregations_With_Predicate_Works()
{
var query = _db.TestDocuments.AsQueryable().Where(x => x.Category == "A");
query.Count().ShouldBe(2);
query.Sum(x => x.Amount).ShouldBe(30);
}
/// <summary>
/// Verifies in-memory join query execution returns expected rows.
/// </summary>
[Fact]
public void Join_Works_InMemory()
{
// Create a second collection for joining
_db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item1", Quantity = 5 });
_db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item3", Quantity = 2 });
_db.SaveChanges();
var query = _db.TestDocuments.AsQueryable()
.Join(_db.OrderDocuments.AsQueryable(),
doc => doc.Name,
order => order.ItemName,
(doc, order) => new { doc.Name, doc.Category, order.Quantity })
.OrderBy(x => x.Name)
.ToList();
query.Count.ShouldBe(2);
query[0].Name.ShouldBe("Item1");
query[0].Category.ShouldBe("A");
query[0].Quantity.ShouldBe(5);
query[1].Name.ShouldBe("Item3");
query[1].Category.ShouldBe("B");
query[1].Quantity.ShouldBe(2);
}
/// <summary>
/// Verifies projection of nested object properties works.
/// </summary>
[Fact]
public void Select_Project_Nested_Object()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" },
Items = new List<OrderItem>
{
new() { Name = "Laptop", Price = 1000 },
new() { Name = "Mouse", Price = 50 }
}
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var query = _db.ComplexDocuments.AsQueryable()
.Select(x => x.ShippingAddress)
.ToList();
query.Count().ShouldBe(1);
query[0].City.Name.ShouldBe("New York");
query[0].Street.ShouldBe("5th Ave");
}
/// <summary>
/// Verifies projection of nested scalar fields works.
/// </summary>
[Fact]
public void Select_Project_Nested_Field()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var cities = _db.ComplexDocuments.AsQueryable()
.Select(x => x.ShippingAddress.City.Name)
.ToList();
cities.Count().ShouldBe(1);
cities[0].ShouldBe("New York");
}
/// <summary>
/// Verifies anonymous projection including nested values works.
/// </summary>
[Fact]
public void Select_Anonymous_Complex()
{
var cityMapper = new ZB_MOM_WW_CBDD_Shared_CityMapper();
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order1",
ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
var result = _db.ComplexDocuments.AsQueryable()
.Select(x => new { x.Title, x.ShippingAddress.City })
.ToList();
result.Count().ShouldBe(1);
result[0].Title.ShouldBe("Order1");
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()
{
var doc = new ComplexDocument
{
Id = ObjectId.NewObjectId(),
Title = "Order with Items",
ShippingAddress = new Address { City = new City { Name = "Los Angeles" }, Street = "Hollywood Blvd" },
Items = new List<OrderItem>
{
new() { Name = "Laptop", Price = 1500 },
new() { Name = "Mouse", Price = 25 },
new() { Name = "Keyboard", Price = 75 }
}
};
_db.ComplexDocuments.Insert(doc);
_db.SaveChanges();
// Retrieve the full document and verify Items array
var retrieved = _db.ComplexDocuments.FindAll().First();
retrieved.Title.ShouldBe("Order with Items");
retrieved.ShippingAddress.City.Name.ShouldBe("Los Angeles");
retrieved.ShippingAddress.Street.ShouldBe("Hollywood Blvd");
// Verify array of nested objects
retrieved.Items.Count.ShouldBe(3);
retrieved.Items[0].Name.ShouldBe("Laptop");
retrieved.Items[0].Price.ShouldBe(1500);
retrieved.Items[1].Name.ShouldBe("Mouse");
retrieved.Items[1].Price.ShouldBe(25);
retrieved.Items[2].Name.ShouldBe("Keyboard");
retrieved.Items[2].Price.ShouldBe(75);
}
}

View File

@@ -1,166 +1,157 @@
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Bson;
using Xunit;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using ZB.MOM.WW.CBDD.Shared;
namespace ZB.MOM.WW.CBDD.Tests
namespace ZB.MOM.WW.CBDD.Tests;
public class LinqTests : IDisposable
{
public class LinqTests : IDisposable
private readonly TestDbContext _db;
private readonly string _testFile;
/// <summary>
/// Initializes a new instance of the <see cref="LinqTests" /> class.
/// </summary>
public LinqTests()
{
private readonly string _testFile;
private readonly Shared.TestDbContext _db;
_testFile = Path.Combine(Path.GetTempPath(), $"linq_tests_{Guid.NewGuid()}.db");
if (File.Exists(_testFile)) File.Delete(_testFile);
string wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
/// <summary>
/// Initializes a new instance of the <see cref="LinqTests"/> class.
/// </summary>
public LinqTests()
{
_testFile = Path.Combine(Path.GetTempPath(), $"linq_tests_{Guid.NewGuid()}.db");
if (File.Exists(_testFile)) File.Delete(_testFile);
var wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
_db = new TestDbContext(_testFile);
_db = new Shared.TestDbContext(_testFile);
// Seed Data
_db.Users.Insert(new User { Name = "Alice", Age = 30 });
_db.Users.Insert(new User { Name = "Bob", Age = 25 });
_db.Users.Insert(new User { Name = "Charlie", Age = 35 });
_db.Users.Insert(new User { Name = "Dave", Age = 20 });
_db.Users.Insert(new User { Name = "Eve", Age = 40 });
_db.SaveChanges();
}
/// <summary>
/// Disposes test resources and removes temporary files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
var wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
/// <summary>
/// Verifies where filters return matching documents.
/// </summary>
[Fact]
public void Where_FiltersDocuments()
{
var query = _db.Users.AsQueryable().Where(x => x.Age > 28);
var results = query.ToList();
results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40)
results.ShouldNotContain(d => d.Name == "Bob");
}
/// <summary>
/// Verifies order by returns sorted documents.
/// </summary>
[Fact]
public void OrderBy_SortsDocuments()
{
var results = _db.Users.AsQueryable().OrderBy(x => x.Age).ToList();
results.Count.ShouldBe(5);
results[0].Name.ShouldBe("Dave"); // 20
results[1].Name.ShouldBe("Bob"); // 25
results.Last().Name.ShouldBe("Eve"); // 40
}
/// <summary>
/// Verifies skip and take support pagination.
/// </summary>
[Fact]
public void SkipTake_Pagination()
{
var results = _db.Users.AsQueryable()
.OrderBy(x => x.Age)
.Skip(1)
.Take(2)
.ToList();
results.Count.ShouldBe(2);
results[0].Name.ShouldBe("Bob"); // 25 (Skipped Dave)
results[1].Name.ShouldBe("Alice"); // 30
}
/// <summary>
/// Verifies select supports projections.
/// </summary>
[Fact]
public void Select_Projections()
{
var names = _db.Users.AsQueryable()
.Where(x => x.Age < 30)
.OrderBy(x => x.Age)
.Select(x => x.Name)
.ToList();
names.Count.ShouldBe(2);
names[0].ShouldBe("Dave");
names[1].ShouldBe("Bob");
}
/// <summary>
/// Verifies indexed where queries use index-backed filtering.
/// </summary>
[Fact]
public void IndexedWhere_UsedIndex()
{
// Create index on Age
_db.Users.EnsureIndex(x => x.Age, "idx_age", false);
var query = _db.Users.AsQueryable().Where(x => x.Age > 25);
var results = query.ToList();
results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40)
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()
{
// Create index on Name
_db.Users.EnsureIndex(x => x.Name!, "idx_name", false);
// StartsWith "Cha" -> Should find "Charlie"
var query = _db.Users.AsQueryable().Where(x => x.Name!.StartsWith("Cha"));
var results = query.ToList();
results.Count().ShouldBe(1);
results[0].Name.ShouldBe("Charlie");
}
/// <summary>
/// Verifies range predicates can use an index.
/// </summary>
[Fact]
public void Between_UsedIndex()
{
// Create index on Age
_db.Users.EnsureIndex(x => x.Age, "idx_age_between", false);
// Age >= 22 && Age <= 32
// Alice(30), Bob(25) -> Should be found.
// Dave(20), Charlie(35), Eve(40) -> excluded.
var query = _db.Users.AsQueryable().Where(x => x.Age >= 22 && x.Age <= 32);
var results = query.ToList();
results.Count.ShouldBe(2);
results.ShouldContain(x => x.Name == "Alice");
results.ShouldContain(x => x.Name == "Bob");
}
// Seed Data
_db.Users.Insert(new User { Name = "Alice", Age = 30 });
_db.Users.Insert(new User { Name = "Bob", Age = 25 });
_db.Users.Insert(new User { Name = "Charlie", Age = 35 });
_db.Users.Insert(new User { Name = "Dave", Age = 20 });
_db.Users.Insert(new User { Name = "Eve", Age = 40 });
_db.SaveChanges();
}
}
/// <summary>
/// Disposes test resources and removes temporary files.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
string wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
/// <summary>
/// Verifies where filters return matching documents.
/// </summary>
[Fact]
public void Where_FiltersDocuments()
{
var query = _db.Users.AsQueryable().Where(x => x.Age > 28);
var results = query.ToList();
results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40)
results.ShouldNotContain(d => d.Name == "Bob");
}
/// <summary>
/// Verifies order by returns sorted documents.
/// </summary>
[Fact]
public void OrderBy_SortsDocuments()
{
var results = _db.Users.AsQueryable().OrderBy(x => x.Age).ToList();
results.Count.ShouldBe(5);
results[0].Name.ShouldBe("Dave"); // 20
results[1].Name.ShouldBe("Bob"); // 25
results.Last().Name.ShouldBe("Eve"); // 40
}
/// <summary>
/// Verifies skip and take support pagination.
/// </summary>
[Fact]
public void SkipTake_Pagination()
{
var results = _db.Users.AsQueryable()
.OrderBy(x => x.Age)
.Skip(1)
.Take(2)
.ToList();
results.Count.ShouldBe(2);
results[0].Name.ShouldBe("Bob"); // 25 (Skipped Dave)
results[1].Name.ShouldBe("Alice"); // 30
}
/// <summary>
/// Verifies select supports projections.
/// </summary>
[Fact]
public void Select_Projections()
{
var names = _db.Users.AsQueryable()
.Where(x => x.Age < 30)
.OrderBy(x => x.Age)
.Select(x => x.Name)
.ToList();
names.Count.ShouldBe(2);
names[0].ShouldBe("Dave");
names[1].ShouldBe("Bob");
}
/// <summary>
/// Verifies indexed where queries use index-backed filtering.
/// </summary>
[Fact]
public void IndexedWhere_UsedIndex()
{
// Create index on Age
_db.Users.EnsureIndex(x => x.Age, "idx_age");
var query = _db.Users.AsQueryable().Where(x => x.Age > 25);
var results = query.ToList();
results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40)
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()
{
// Create index on Name
_db.Users.EnsureIndex(x => x.Name!, "idx_name");
// StartsWith "Cha" -> Should find "Charlie"
var query = _db.Users.AsQueryable().Where(x => x.Name!.StartsWith("Cha"));
var results = query.ToList();
results.Count().ShouldBe(1);
results[0].Name.ShouldBe("Charlie");
}
/// <summary>
/// Verifies range predicates can use an index.
/// </summary>
[Fact]
public void Between_UsedIndex()
{
// Create index on Age
_db.Users.EnsureIndex(x => x.Age, "idx_age_between");
// Age >= 22 && Age <= 32
// Alice(30), Bob(25) -> Should be found.
// Dave(20), Charlie(35), Eve(40) -> excluded.
var query = _db.Users.AsQueryable().Where(x => x.Age >= 22 && x.Age <= 32);
var results = query.ToList();
results.Count.ShouldBe(2);
results.ShouldContain(x => x.Name == "Alice");
results.ShouldContain(x => x.Name == "Bob");
}
}

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 QueryPrimitivesTests : 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="QueryPrimitivesTests"/> class.
/// Initializes a new instance of the <see cref="QueryPrimitivesTests" /> class.
/// </summary>
public QueryPrimitivesTests()
{
@@ -26,12 +24,21 @@ public class QueryPrimitivesTests : IDisposable
SeedData();
}
/// <summary>
/// Executes Dispose.
/// </summary>
public void Dispose()
{
_storage.Dispose();
File.Delete(_testFile);
}
private void SeedData()
{
// Insert keys: 10, 20, 30, 40, 50
// And strings: "A", "AB", "ABC", "B", "C"
var txnId = _storage.BeginTransaction().TransactionId;
ulong txnId = _storage.BeginTransaction().TransactionId;
Insert(10, txnId);
Insert(20, txnId);
@@ -59,7 +66,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes Equal_ShouldFindExactMatch.
/// Executes Equal_ShouldFindExactMatch.
/// </summary>
[Fact]
public void Equal_ShouldFindExactMatch()
@@ -72,7 +79,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes Equal_ShouldReturnEmpty_WhenNotFound.
/// Executes Equal_ShouldReturnEmpty_WhenNotFound.
/// </summary>
[Fact]
public void Equal_ShouldReturnEmpty_WhenNotFound()
@@ -84,13 +91,13 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes GreaterThan_ShouldReturnMatches.
/// Executes GreaterThan_ShouldReturnMatches.
/// </summary>
[Fact]
public void GreaterThan_ShouldReturnMatches()
{
var key = IndexKey.Create(30);
var result = _index.GreaterThan(key, orEqual: false, 0).ToList();
var result = _index.GreaterThan(key, false, 0).ToList();
(result.Count >= 2).ShouldBeTrue();
result[0].Key.ShouldBe(IndexKey.Create(40));
@@ -98,13 +105,13 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes GreaterThanOrEqual_ShouldReturnMatches.
/// Executes GreaterThanOrEqual_ShouldReturnMatches.
/// </summary>
[Fact]
public void GreaterThanOrEqual_ShouldReturnMatches()
{
var key = IndexKey.Create(30);
var result = _index.GreaterThan(key, orEqual: true, 0).ToList();
var result = _index.GreaterThan(key, true, 0).ToList();
(result.Count >= 3).ShouldBeTrue();
result[0].Key.ShouldBe(IndexKey.Create(30));
@@ -113,13 +120,13 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes LessThan_ShouldReturnMatches.
/// Executes LessThan_ShouldReturnMatches.
/// </summary>
[Fact]
public void LessThan_ShouldReturnMatches()
{
var key = IndexKey.Create(30);
var result = _index.LessThan(key, orEqual: false, 0).ToList();
var result = _index.LessThan(key, false, 0).ToList();
result.Count.ShouldBe(2); // 20, 10 (Order is backward?)
// LessThan yields backward?
@@ -129,14 +136,14 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes Between_ShouldReturnRange.
/// Executes Between_ShouldReturnRange.
/// </summary>
[Fact]
public void Between_ShouldReturnRange()
{
var start = IndexKey.Create(20);
var end = IndexKey.Create(40);
var result = _index.Between(start, end, startInclusive: true, endInclusive: true, 0).ToList();
var result = _index.Between(start, end, true, true, 0).ToList();
result.Count.ShouldBe(3); // 20, 30, 40
result[0].Key.ShouldBe(IndexKey.Create(20));
@@ -145,7 +152,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes StartsWith_ShouldReturnPrefixMatches.
/// Executes StartsWith_ShouldReturnPrefixMatches.
/// </summary>
[Fact]
public void StartsWith_ShouldReturnPrefixMatches()
@@ -158,7 +165,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes Like_ShouldSupportWildcards.
/// Executes Like_ShouldSupportWildcards.
/// </summary>
[Fact]
public void Like_ShouldSupportWildcards()
@@ -176,7 +183,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes Like_Underscore_ShouldMatchSingleChar.
/// Executes Like_Underscore_ShouldMatchSingleChar.
/// </summary>
[Fact]
public void Like_Underscore_ShouldMatchSingleChar()
@@ -188,7 +195,7 @@ public class QueryPrimitivesTests : IDisposable
}
/// <summary>
/// Executes In_ShouldReturnSpecificKeys.
/// Executes In_ShouldReturnSpecificKeys.
/// </summary>
[Fact]
public void In_ShouldReturnSpecificKeys()
@@ -201,13 +208,4 @@ public class QueryPrimitivesTests : IDisposable
result[1].Key.ShouldBe(IndexKey.Create(30));
result[2].Key.ShouldBe(IndexKey.Create(50));
}
/// <summary>
/// Executes Dispose.
/// </summary>
public void Dispose()
{
_storage.Dispose();
File.Delete(_testFile);
}
}
}

View File

@@ -1,128 +1,111 @@
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Storage;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Bson;
using Xunit;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using ZB.MOM.WW.CBDD.Shared;
namespace ZB.MOM.WW.CBDD.Tests
namespace ZB.MOM.WW.CBDD.Tests;
public class ScanTests : IDisposable
{
public class ScanTests : IDisposable
private readonly TestDbContext _db;
private readonly string _testFile;
/// <summary>
/// Initializes a new instance of the <see cref="ScanTests" /> class.
/// </summary>
public ScanTests()
{
private readonly string _testFile;
private readonly Shared.TestDbContext _db;
_testFile = Path.Combine(Path.GetTempPath(), $"scan_tests_{Guid.NewGuid()}.db");
if (File.Exists(_testFile)) File.Delete(_testFile);
string wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
/// <summary>
/// Initializes a new instance of the <see cref="ScanTests"/> class.
/// </summary>
public ScanTests()
_db = new TestDbContext(_testFile);
}
/// <summary>
/// Executes Dispose.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
string wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
/// <summary>
/// Executes Scan_FindsMatchingDocuments.
/// </summary>
[Fact]
public void Scan_FindsMatchingDocuments()
{
// Arrange
_db.Users.Insert(new User { Name = "Alice", Age = 30 });
_db.Users.Insert(new User { Name = "Bob", Age = 25 });
_db.Users.Insert(new User { Name = "Charlie", Age = 35 });
_db.SaveChanges();
// Act: Find users older than 28
var results = _db.Users.Scan(reader => ParseAge(reader) > 28).ToList();
// Assert
results.Count.ShouldBe(2);
results.ShouldContain(d => d.Name == "Alice");
results.ShouldContain(d => d.Name == "Charlie");
}
/// <summary>
/// Executes Repro_Insert_Loop_Hang.
/// </summary>
[Fact]
public void Repro_Insert_Loop_Hang()
{
// Reproduce hang reported by user at 501 documents
var count = 600;
for (var i = 0; i < count; i++) _db.Users.Insert(new User { Name = $"User_{i}", Age = i });
_db.SaveChanges();
}
/// <summary>
/// Executes ParallelScan_FindsMatchingDocuments.
/// </summary>
[Fact]
public void ParallelScan_FindsMatchingDocuments()
{
// Arrange
var count = 1000;
for (var i = 0; i < count; i++) _db.Users.Insert(new User { Name = $"User_{i}", Age = i });
_db.SaveChanges();
// Act: Find users with Age >= 500
// Parallelism 2 to force partitioning
var results = _db.Users.ParallelScan(reader => ParseAge(reader) >= 500, 2).ToList();
// Assert
results.Count.ShouldBe(500);
}
private int ParseAge(BsonSpanReader reader)
{
try
{
_testFile = Path.Combine(Path.GetTempPath(), $"scan_tests_{Guid.NewGuid()}.db");
if (File.Exists(_testFile)) File.Delete(_testFile);
var wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
_db = new Shared.TestDbContext(_testFile);
}
/// <summary>
/// Executes Dispose.
/// </summary>
public void Dispose()
{
_db.Dispose();
if (File.Exists(_testFile)) File.Delete(_testFile);
var wal = Path.ChangeExtension(_testFile, ".wal");
if (File.Exists(wal)) File.Delete(wal);
}
/// <summary>
/// Executes Scan_FindsMatchingDocuments.
/// </summary>
[Fact]
public void Scan_FindsMatchingDocuments()
{
// Arrange
_db.Users.Insert(new User { Name = "Alice", Age = 30 });
_db.Users.Insert(new User { Name = "Bob", Age = 25 });
_db.Users.Insert(new User { Name = "Charlie", Age = 35 });
_db.SaveChanges();
// Act: Find users older than 28
var results = _db.Users.Scan(reader => ParseAge(reader) > 28).ToList();
// Assert
results.Count.ShouldBe(2);
results.ShouldContain(d => d.Name == "Alice");
results.ShouldContain(d => d.Name == "Charlie");
}
/// <summary>
/// Executes Repro_Insert_Loop_Hang.
/// </summary>
[Fact]
public void Repro_Insert_Loop_Hang()
{
// Reproduce hang reported by user at 501 documents
int count = 600;
for (int i = 0; i < count; i++)
reader.ReadDocumentSize();
while (reader.Remaining > 0)
{
_db.Users.Insert(new User { Name = $"User_{i}", Age = i });
var type = reader.ReadBsonType();
if (type == 0) break; // End of doc
string name = reader.ReadElementHeader();
if (name == "age") return reader.ReadInt32();
reader.SkipValue(type);
}
_db.SaveChanges();
}
/// <summary>
/// Executes ParallelScan_FindsMatchingDocuments.
/// </summary>
[Fact]
public void ParallelScan_FindsMatchingDocuments()
catch
{
// Arrange
int count = 1000;
for (int i = 0; i < count; i++)
{
_db.Users.Insert(new User { Name = $"User_{i}", Age = i });
}
_db.SaveChanges();
// Act: Find users with Age >= 500
// Parallelism 2 to force partitioning
var results = _db.Users.ParallelScan(reader => ParseAge(reader) >= 500, degreeOfParallelism: 2).ToList();
// Assert
results.Count.ShouldBe(500);
}
private int ParseAge(BsonSpanReader reader)
{
try
{
reader.ReadDocumentSize();
while (reader.Remaining > 0)
{
var type = reader.ReadBsonType();
if (type == 0) break; // End of doc
var name = reader.ReadElementHeader();
if (name == "age")
{
return reader.ReadInt32();
}
else
{
reader.SkipValue(type);
}
}
}
catch { return -1; }
return -1;
}
return -1;
}
}
}