using Microsoft.Data.Sqlite; using ZB.MOM.WW.LocalDb.Internal; namespace ZB.MOM.WW.LocalDb.Tests; public sealed class SqliteLocalDbTests : 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 SqliteLocalDb Create() => new(new LocalDbOptions { Path = _path }); [Fact] public void Open_CreatesDbFile_WalMode() { using var db = Create(); Assert.True(File.Exists(_path)); using var conn = db.CreateConnection(); using var cmd = conn.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode"; var mode = (string)cmd.ExecuteScalar()!; Assert.Equal("wal", mode, ignoreCase: true); } [Fact] public async Task ExecuteAsync_ParameterizedInsert_Works() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); await db.ExecuteAsync("INSERT INTO t (id, name) VALUES (@id, @name)", new { id = 1, name = "x" }); var rows = await db.QueryAsync("SELECT COUNT(*) FROM t", r => r.GetInt64(0)); Assert.Equal(1L, rows[0]); } [Fact] public async Task QueryAsync_MapsRows() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); await db.ExecuteAsync("INSERT INTO t (id, name) VALUES (@id, @name)", new { id = 1, name = "a" }); await db.ExecuteAsync("INSERT INTO t (id, name) VALUES (@id, @name)", new { id = 2, name = "b" }); var rows = await db.QueryAsync( "SELECT id, name FROM t ORDER BY id", r => (Id: r.GetInt64(0), Name: r.GetString(1))); Assert.Equal(2, rows.Count); Assert.Equal((1L, "a"), rows[0]); Assert.Equal((2L, "b"), rows[1]); } [Fact] public async Task Transaction_CommitAndRollback() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY)"); await using (var tx = await db.BeginTransactionAsync()) { await tx.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 1 }); await tx.CommitAsync(default); } await using (var tx = await db.BeginTransactionAsync()) { await tx.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 2 }); await tx.RollbackAsync(default); } var ids = await db.QueryAsync("SELECT id FROM t ORDER BY id", r => r.GetInt64(0)); Assert.Equal(new[] { 1L }, ids); } [Fact] public async Task Transaction_DisposeWithoutCommit_RollsBack() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY)"); await using (var tx = await db.BeginTransactionAsync()) { await tx.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 1 }); } var count = await db.QueryAsync("SELECT COUNT(*) FROM t", r => r.GetInt64(0)); Assert.Equal(0L, count[0]); } [Fact] public async Task Bind_GuidByteArrayAndNull_RoundTrip() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, g TEXT, blob BLOB, missing TEXT)"); var guid = Guid.NewGuid(); var bytes = new byte[] { 1, 2, 3 }; await db.ExecuteAsync( "INSERT INTO t (id, g, blob, missing) VALUES (@id, @g, @blob, @missing)", new { id = 1, g = guid, blob = bytes, missing = (string?)null }); var rows = await db.QueryAsync( "SELECT g, blob, missing FROM t WHERE id = 1", r => (Guid: r.GetGuid(0), Blob: (byte[])r.GetValue(1), MissingIsNull: r.IsDBNull(2))); Assert.Equal(guid, rows[0].Guid); Assert.Equal(bytes, rows[0].Blob); Assert.True(rows[0].MissingIsNull); } [Fact] public async Task Bind_DictionaryParameters_BindByKey() { using var db = Create(); await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"); var parameters = new Dictionary { ["id"] = 7, ["name"] = null }; await db.ExecuteAsync("INSERT INTO t (id, name) VALUES (@id, @name)", parameters); var rows = await db.QueryAsync( "SELECT id, name FROM t", r => (Id: r.GetInt64(0), NameIsNull: r.IsDBNull(1))); Assert.Equal(7L, rows[0].Id); Assert.True(rows[0].NameIsNull); } [Fact] public void Ctor_EmptyPath_Throws() { Assert.Throws(() => new SqliteLocalDb(new LocalDbOptions { Path = " " })); } [Fact] public void Ctor_InvalidSynchronous_Throws() { Assert.Throws(() => new SqliteLocalDb(new LocalDbOptions { Path = _path, Synchronous = "NORMAL; PRAGMA foreign_keys=OFF" })); } [Fact] public void Dispose_IsIdempotent() { var db = Create(); db.Dispose(); db.Dispose(); } [Fact] public async Task HlcUdf_RegisteredOnConnection() { using var db = Create(); var values = await db.QueryAsync( "SELECT zb_hlc_next() UNION ALL SELECT zb_hlc_next()", r => r.GetInt64(0)); Assert.Equal(2, values.Count); Assert.True(values[1] > values[0], $"{values[1]} should be greater than {values[0]}"); } [Fact] public async Task Hlc_ResumesFromDurableMax_OnReopen() { const long huge = 9_000_000_000_000_000L; using (var db = Create()) { await db.ExecuteAsync( "INSERT INTO __localdb_oplog (table_name, pk_json, hlc, node_id) VALUES ('t', '{}', @hlc, 'n')", new { hlc = huge }); } SqliteConnection.ClearAllPools(); using (var db = Create()) { var next = await db.QueryAsync("SELECT zb_hlc_next()", r => r.GetInt64(0)); Assert.True(next[0] > huge, $"{next[0]} should be greater than {huge}"); } } }