using Microsoft.Data.Sqlite; using ZB.MOM.WW.LocalDb.Internal; namespace ZB.MOM.WW.LocalDb.Tests; public sealed class LocalDbSchemaTests : IDisposable { private readonly string _path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db"); public void Dispose() { SqliteConnection.ClearAllPools(); if (File.Exists(_path)) File.Delete(_path); } private SqliteConnection Open() { var conn = new SqliteConnection($"Data Source={_path}"); conn.Open(); return conn; } private static long ScalarLong(SqliteConnection conn, string sql) { using var cmd = conn.CreateCommand(); cmd.CommandText = sql; return Convert.ToInt64(cmd.ExecuteScalar()); } private static string ScalarString(SqliteConnection conn, string sql) { using var cmd = conn.CreateCommand(); cmd.CommandText = sql; return (string)cmd.ExecuteScalar()!; } [Fact] public void EnsureCreated_CreatesAllObjects() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table'"; var tables = new List(); using (var reader = cmd.ExecuteReader()) while (reader.Read()) tables.Add(reader.GetString(0)); foreach (var expected in new[] { "__localdb_meta", "__localdb_oplog", "__localdb_peer_state", "__localdb_row_version", "__localdb_applying", "__localdb_dead_letter", "__localdb_snapshot_state", }) Assert.Contains(expected, tables); // __localdb_oplog_hlc is deliberately NOT created: schema v2 removed it. It indexed // (table_name, pk_json, hlc), which no query has ever used — every oplog access path is by // seq or a full hlc-predicate delete — while costing a b-tree insert inside every captured // consumer write. Assert.Equal(0, ScalarLong(conn, "SELECT count(*) FROM sqlite_master WHERE type='index' AND name='__localdb_oplog_hlc'")); Assert.Equal(LocalDbSchema.CurrentSchemaVersion, ScalarLong(conn, "SELECT schema_version FROM __localdb_meta WHERE id=1")); } [Fact] public void EnsureCreated_UpgradesLegacyFile_DropsOplogHlcIndex() { using var conn = Open(); // A file exactly as schema v1 left it: the tables, the index, and a v1 stamp. LocalDbSchema.EnsureCreated(conn); Exec(conn, "CREATE INDEX __localdb_oplog_hlc ON __localdb_oplog(table_name, pk_json, hlc);" + "UPDATE __localdb_meta SET schema_version = 1 WHERE id = 1;"); Assert.Equal(1, ScalarLong(conn, "SELECT count(*) FROM sqlite_master WHERE type='index' AND name='__localdb_oplog_hlc'")); LocalDbSchema.EnsureCreated(conn); Assert.Equal(0, ScalarLong(conn, "SELECT count(*) FROM sqlite_master WHERE type='index' AND name='__localdb_oplog_hlc'")); Assert.Equal(LocalDbSchema.CurrentSchemaVersion, ScalarLong(conn, "SELECT schema_version FROM __localdb_meta WHERE id=1")); } [Fact] public void EnsureCreated_UpgradesV2File_AddsPerTableSnapshotState() { using var conn = Open(); // A file exactly as schema v2 left it: everything except __localdb_snapshot_state, stamped 2. LocalDbSchema.EnsureCreated(conn); Exec(conn, "DROP TABLE __localdb_snapshot_state;" + "UPDATE __localdb_meta SET schema_version = 2 WHERE id = 1;"); LocalDbSchema.EnsureCreated(conn); Assert.Equal(1, ScalarLong(conn, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='__localdb_snapshot_state'")); // Empty is the right v3 state for a file that never baselined anything: no table owes a // snapshot, and the database-wide flag it was carrying keeps whatever value it had. Assert.Equal(0, ScalarLong(conn, "SELECT COUNT(*) FROM __localdb_snapshot_state")); Assert.Equal(LocalDbSchema.CurrentSchemaVersion, ScalarLong(conn, "SELECT schema_version FROM __localdb_meta WHERE id=1")); } [Fact] public void EnsureCreated_Upgrade_PreservesTheDatabaseWideSnapshotFlag() { // A v2 file mid-resync (cap prune flagged a full snapshot, the peer was down) must still owe // that snapshot after the upgrade. v3 narrows where NEW debt is recorded; it does not // reinterpret debt already on disk. using var conn = Open(); LocalDbSchema.EnsureCreated(conn); Exec(conn, "DROP TABLE __localdb_snapshot_state;" + "UPDATE __localdb_meta SET schema_version = 2 WHERE id = 1;" + "UPDATE __localdb_peer_state SET needs_snapshot = 1 WHERE id = 1;"); LocalDbSchema.EnsureCreated(conn); Assert.Equal(1, ScalarLong(conn, "SELECT needs_snapshot FROM __localdb_peer_state WHERE id=1")); } [Fact] public void EnsureCreated_Upgrade_PreservesNodeIdentityAndOplogRows() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); var nodeId = ScalarString(conn, "SELECT node_id FROM __localdb_meta WHERE id=1"); Exec(conn, "CREATE INDEX __localdb_oplog_hlc ON __localdb_oplog(table_name, pk_json, hlc);" + "UPDATE __localdb_meta SET schema_version = 1 WHERE id = 1;" + "INSERT INTO __localdb_oplog (table_name, pk_json, row_json, hlc, node_id, is_tombstone) " + "VALUES ('orders', '{\"id\":1}', '{\"id\":1}', 42, 'n', 0);" + "UPDATE __localdb_peer_state SET last_acked_seq = 7 WHERE id = 1;"); LocalDbSchema.EnsureCreated(conn); // The upgrade is DDL only — identity, replication watermarks and captured data survive it. Assert.Equal(nodeId, ScalarString(conn, "SELECT node_id FROM __localdb_meta WHERE id=1")); Assert.Equal(1, ScalarLong(conn, "SELECT COUNT(*) FROM __localdb_oplog")); Assert.Equal(42, ScalarLong(conn, "SELECT hlc FROM __localdb_oplog")); Assert.Equal(7, ScalarLong(conn, "SELECT last_acked_seq FROM __localdb_peer_state WHERE id=1")); } [Fact] public void EnsureCreated_AlreadyCurrent_LeavesVersionAlone() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); LocalDbSchema.EnsureCreated(conn); LocalDbSchema.EnsureCreated(conn); Assert.Equal(LocalDbSchema.CurrentSchemaVersion, ScalarLong(conn, "SELECT schema_version FROM __localdb_meta WHERE id=1")); } private static void Exec(SqliteConnection conn, string sql) { using var cmd = conn.CreateCommand(); cmd.CommandText = sql; cmd.ExecuteNonQuery(); } [Fact] public void EnsureCreated_IsIdempotent() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); LocalDbSchema.EnsureCreated(conn); Assert.Equal(1, ScalarLong(conn, "SELECT COUNT(*) FROM __localdb_meta")); Assert.Equal(1, ScalarLong(conn, "SELECT COUNT(*) FROM __localdb_applying")); Assert.Equal(1, ScalarLong(conn, "SELECT COUNT(*) FROM __localdb_peer_state")); } [Fact] public void Meta_NodeId_MintedOnceAndStable() { string first; using (var conn = Open()) { LocalDbSchema.EnsureCreated(conn); first = ScalarString(conn, "SELECT node_id FROM __localdb_meta WHERE id=1"); } using (var conn = Open()) { LocalDbSchema.EnsureCreated(conn); var second = ScalarString(conn, "SELECT node_id FROM __localdb_meta WHERE id=1"); Assert.Equal(first, second); } Assert.True(Guid.TryParse(first, out _)); } [Fact] public void Applying_SeededWithZeroRow() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); Assert.Equal(0, ScalarLong(conn, "SELECT applying FROM __localdb_applying WHERE id=1")); } [Fact] public void ReadMeta_ReturnsNodeIdAndHighWater() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); var (nodeId, highWater) = LocalDbSchema.ReadMeta(conn); Assert.True(Guid.TryParse(nodeId, out _)); Assert.Equal(0, highWater); } [Fact] public void FlushHlcHighWater_RoundTrips() { using var conn = Open(); LocalDbSchema.EnsureCreated(conn); LocalDbSchema.FlushHlcHighWater(conn, 123_456_789L); var (_, highWater) = LocalDbSchema.ReadMeta(conn); Assert.Equal(123_456_789L, highWater); } }