238 lines
9.5 KiB
C#
Executable File
238 lines
9.5 KiB
C#
Executable File
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Xunit;
|
|
using ZB.MOM.WW.CBDD.Bson;
|
|
using ZB.MOM.WW.CBDD.Bson.Schema;
|
|
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.Shared.TestDbContext_TestDbContext_Mappers;
|
|
using ZB.MOM.WW.CBDD.Shared;
|
|
|
|
namespace ZB.MOM.WW.CBDD.Tests;
|
|
|
|
public class SchemaPersistenceTests : IDisposable
|
|
{
|
|
private readonly string _dbPath;
|
|
private readonly Shared.TestDbContext _db;
|
|
|
|
public SchemaPersistenceTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"schema_test_{Guid.NewGuid()}.db");
|
|
_db = new Shared.TestDbContext(_dbPath);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_db.Dispose();
|
|
if (File.Exists(_dbPath)) File.Delete(_dbPath);
|
|
}
|
|
|
|
[Fact]
|
|
public void BsonSchema_Serialization_RoundTrip()
|
|
{
|
|
var schema = new BsonSchema
|
|
{
|
|
Title = "Person",
|
|
Fields =
|
|
{
|
|
new BsonField { Name = "id", Type = BsonType.ObjectId },
|
|
new BsonField { Name = "name", Type = BsonType.String, IsNullable = true },
|
|
new BsonField { Name = "age", Type = BsonType.Int32 },
|
|
new BsonField
|
|
{
|
|
Name = "address",
|
|
Type = BsonType.Document,
|
|
NestedSchema = new BsonSchema
|
|
{
|
|
Fields =
|
|
{
|
|
new BsonField { Name = "city", Type = BsonType.String }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var buffer = new byte[1024];
|
|
var keyMap = new System.Collections.Concurrent.ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
|
|
var keys = new System.Collections.Concurrent.ConcurrentDictionary<ushort, string>();
|
|
|
|
// Manual registration for schema keys
|
|
ushort id = 1;
|
|
foreach (var k in new[] { "person", "id", "name", "age", "address", "city", "fields", "title", "type", "isnullable", "nestedschema", "t", "v", "f", "n", "b", "s", "a", "_v", "0", "1", "2", "3", "4", "5" })
|
|
{
|
|
keyMap[k] = id;
|
|
keys[id] = k;
|
|
id++;
|
|
}
|
|
|
|
var writer = new BsonSpanWriter(buffer, keyMap);
|
|
schema.ToBson(ref writer);
|
|
|
|
var reader = new BsonSpanReader(buffer.AsSpan(0, writer.Position), keys);
|
|
var roundTrip = BsonSchema.FromBson(ref reader);
|
|
|
|
roundTrip.Title.ShouldBe(schema.Title);
|
|
roundTrip.Fields.Count.ShouldBe(schema.Fields.Count);
|
|
roundTrip.Fields[0].Name.ShouldBe(schema.Fields[0].Name);
|
|
roundTrip.Fields[3].NestedSchema!.Fields[0].Name.ShouldBe(schema.Fields[3].NestedSchema!.Fields[0].Name);
|
|
schema.Equals(roundTrip).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void StorageEngine_Collections_Metadata_Persistence()
|
|
{
|
|
var meta = new CollectionMetadata
|
|
{
|
|
Name = "users",
|
|
PrimaryRootPageId = 10,
|
|
SchemaRootPageId = 20
|
|
};
|
|
meta.Indexes.Add(new IndexMetadata { Name = "age", IsUnique = false, Type = IndexType.BTree, PropertyPaths = ["Age"] });
|
|
|
|
_db.Storage.SaveCollectionMetadata(meta);
|
|
|
|
var loaded = _db.Storage.GetCollectionMetadata("users");
|
|
loaded.ShouldNotBeNull();
|
|
loaded.Name.ShouldBe(meta.Name);
|
|
loaded.PrimaryRootPageId.ShouldBe(meta.PrimaryRootPageId);
|
|
loaded.SchemaRootPageId.ShouldBe(meta.SchemaRootPageId);
|
|
loaded.Indexes.Count().ShouldBe(1);
|
|
loaded.Indexes[0].Name.ShouldBe("age");
|
|
}
|
|
|
|
[Fact]
|
|
public void StorageEngine_Schema_Versioning()
|
|
{
|
|
var schema1 = new BsonSchema { Title = "V1", Fields = { new BsonField { Name = "f1", Type = BsonType.String } } };
|
|
var schema2 = new BsonSchema { Title = "V2", Fields = { new BsonField { Name = "f1", Type = BsonType.String }, new BsonField { Name = "f2", Type = BsonType.Int32 } } };
|
|
|
|
var rootId = _db.Storage.AppendSchema(0, schema1);
|
|
rootId.ShouldNotBe(0u);
|
|
|
|
var schemas = _db.Storage.GetSchemas(rootId);
|
|
schemas.Count().ShouldBe(1);
|
|
schemas[0].Title.ShouldBe("V1");
|
|
|
|
var updatedRoot = _db.Storage.AppendSchema(rootId, schema2);
|
|
updatedRoot.ShouldBe(rootId);
|
|
|
|
schemas = _db.Storage.GetSchemas(rootId);
|
|
schemas.Count.ShouldBe(2, $"Expected 2 schemas but found {schemas.Count}. Titles: {(schemas.Count > 0 ? string.Join(", ", schemas.Select(s => s.Title)) : "None")}");
|
|
schemas[0].Title.ShouldBe("V1");
|
|
schemas[1].Title.ShouldBe("V2");
|
|
}
|
|
|
|
[Fact]
|
|
public void DocumentCollection_Integrates_Schema_Versioning_On_Startup()
|
|
{
|
|
// Use a dedicated database for this test to avoid schema pollution from _db
|
|
var testDbPath = Path.Combine(Path.GetTempPath(), $"schema_versioning_test_{Guid.NewGuid()}.db");
|
|
|
|
try
|
|
{
|
|
var mapper1 = new ZB_MOM_WW_CBDD_Shared_PersonMapper();
|
|
var schema1 = mapper1.GetSchema();
|
|
|
|
// 1. First startup - create DB and initialize Person collection
|
|
using (var db1 = new Shared.TestDbContext(testDbPath))
|
|
{
|
|
// Access only People collection to avoid initializing others
|
|
var coll = db1.People;
|
|
var meta = db1.Storage.GetCollectionMetadata("people_collection");
|
|
meta.ShouldNotBeNull();
|
|
var schemas = db1.Storage.GetSchemas(meta.SchemaRootPageId);
|
|
schemas.Count().ShouldBe(1);
|
|
schema1.Equals(schemas[0]).ShouldBeTrue("Persisted schema 1 should equal current schema 1");
|
|
|
|
coll.CurrentSchemaVersion.ShouldNotBeNull();
|
|
coll.CurrentSchemaVersion!.Value.Version.ShouldBe(1);
|
|
coll.CurrentSchemaVersion!.Value.Hash.ShouldBe(schema1.GetHash());
|
|
}
|
|
|
|
// 2. Restart with SAME schema (should NOT append)
|
|
using (var db2 = new Shared.TestDbContext(testDbPath))
|
|
{
|
|
var coll = db2.People;
|
|
var meta = db2.Storage.GetCollectionMetadata("people_collection");
|
|
var schemas = db2.Storage.GetSchemas(meta!.SchemaRootPageId);
|
|
schemas.Count().ShouldBe(1); // Still 1
|
|
|
|
coll.CurrentSchemaVersion!.Value.Version.ShouldBe(1);
|
|
coll.CurrentSchemaVersion!.Value.Hash.ShouldBe(schema1.GetHash());
|
|
}
|
|
|
|
// 3. Simulate schema evolution: Person with an additional field
|
|
// Since we can't change the actual Person class at runtime, this test verifies
|
|
// that the same schema doesn't get re-appended.
|
|
// A real-world scenario would involve deploying a new mapper version.
|
|
using (var db3 = new Shared.TestDbContext(testDbPath))
|
|
{
|
|
var coll = db3.People;
|
|
var meta = db3.Storage.GetCollectionMetadata("people_collection");
|
|
var schemas = db3.Storage.GetSchemas(meta!.SchemaRootPageId);
|
|
|
|
// Schema should still be 1 since we're using the same Person type
|
|
schemas.Count().ShouldBe(1);
|
|
schemas[0].Title.ShouldBe("Person");
|
|
coll.CurrentSchemaVersion!.Value.Version.ShouldBe(1);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(testDbPath)) File.Delete(testDbPath);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Document_Contains_Schema_Version_Field()
|
|
{
|
|
var mapper = new ZB_MOM_WW_CBDD_Shared_PersonMapper();
|
|
using (var coll = _db.People)
|
|
{
|
|
var person = new Person { Name = "John" };
|
|
var id = coll.Insert(person);
|
|
_db.SaveChanges();
|
|
|
|
coll.Count().ShouldBe(1);
|
|
coll.CurrentSchemaVersion.ShouldNotBeNull();
|
|
coll.CurrentSchemaVersion!.Value.Version.ShouldBe(1);
|
|
|
|
// Verify that the document in storage contains _v
|
|
var meta = _db.Storage.GetCollectionMetadata("persons"); // person lowercase
|
|
// meta.ShouldNotBeNull();
|
|
|
|
// Get location from primary index (internal access enabled by InternalsVisibleTo)
|
|
var key = mapper.ToIndexKey(id);
|
|
coll._primaryIndex.TryFind(key, out var location, 0).ShouldBeTrue();
|
|
|
|
// Read raw bytes from page
|
|
var pageBuffer = new byte[_db.Storage.PageSize];
|
|
_db.Storage.ReadPage(location.PageId, 0, pageBuffer);
|
|
var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size);
|
|
var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset));
|
|
var docData = pageBuffer.AsSpan(slot.Offset, slot.Length);
|
|
|
|
// Print some info if it fails (using Assert messages)
|
|
string hex = BitConverter.ToString(docData.ToArray()).Replace("-", "");
|
|
|
|
// Look for _v (BsonType.Int32 + 2-byte ID)
|
|
ushort vId = _db.Storage.GetKeyMap()["_v"];
|
|
string vIdHex = vId.ToString("X4");
|
|
// Reverse endian for hex string check (ushort is LE)
|
|
string vIdHexLE = vIdHex.Substring(2, 2) + vIdHex.Substring(0, 2);
|
|
string pattern = "10" + vIdHexLE;
|
|
|
|
bool found = hex.Contains(pattern);
|
|
found.ShouldBeTrue($"Document should contain _v field ({pattern}). Raw BSON: {hex}");
|
|
|
|
// Verify the value (1) follows the key
|
|
int index = hex.IndexOf(pattern);
|
|
string valueHex = hex.Substring(index + 6, 8); // 4 bytes = 8 hex chars (pattern is 6 hex chars: 10 + ID_LE)
|
|
valueHex.ShouldBe("01000000");
|
|
}
|
|
}
|
|
}
|