feat(localdb): opt-in table registration + capture triggers (oplog + row_version, applying guard)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using ZB.MOM.WW.LocalDb.Internal;
|
||||
using ZB.MOM.WW.LocalDb.Registration;
|
||||
|
||||
namespace ZB.MOM.WW.LocalDb.Tests;
|
||||
|
||||
public sealed class RegistrationTests : 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 });
|
||||
|
||||
private static async Task CreateOrders(SqliteLocalDb db) =>
|
||||
await db.ExecuteAsync("CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT, qty INTEGER)");
|
||||
|
||||
private Task<IReadOnlyList<string>> Triggers(SqliteLocalDb db) =>
|
||||
db.QueryAsync(
|
||||
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='orders' ORDER BY name",
|
||||
r => r.GetString(0));
|
||||
|
||||
[Fact]
|
||||
public async Task Register_TableWithPk_InstallsThreeTriggers()
|
||||
{
|
||||
using var db = Create();
|
||||
await CreateOrders(db);
|
||||
|
||||
db.RegisterReplicated("orders");
|
||||
|
||||
var triggers = await Triggers(db);
|
||||
Assert.Equal(
|
||||
new[] { "__localdb_orders_ad", "__localdb_orders_ai", "__localdb_orders_au" },
|
||||
triggers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_NoPk_Throws()
|
||||
{
|
||||
using var db = Create();
|
||||
await db.ExecuteAsync("CREATE TABLE nopk (a TEXT, b TEXT)");
|
||||
|
||||
var ex = Assert.Throws<LocalDbRegistrationException>(() => db.RegisterReplicated("nopk"));
|
||||
Assert.Contains("nopk", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_MissingTable_Throws()
|
||||
{
|
||||
using var db = Create();
|
||||
|
||||
var ex = Assert.Throws<LocalDbRegistrationException>(() => db.RegisterReplicated("ghost"));
|
||||
Assert.Contains("ghost", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_IsIdempotent_RegeneratesTriggers()
|
||||
{
|
||||
using var db = Create();
|
||||
await CreateOrders(db);
|
||||
|
||||
db.RegisterReplicated("orders");
|
||||
db.RegisterReplicated("orders");
|
||||
|
||||
var triggers = await Triggers(db);
|
||||
Assert.Equal(3, triggers.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_CompositePk_Works()
|
||||
{
|
||||
using var db = Create();
|
||||
await db.ExecuteAsync("CREATE TABLE kv (a INTEGER, b INTEGER, v TEXT, PRIMARY KEY (a, b))");
|
||||
|
||||
db.RegisterReplicated("kv");
|
||||
await db.ExecuteAsync("INSERT INTO kv (a, b, v) VALUES (1, 2, 'x')");
|
||||
|
||||
var pk = await db.QueryAsync(
|
||||
"SELECT pk_json FROM __localdb_oplog WHERE table_name='kv'",
|
||||
r => r.GetString(0));
|
||||
Assert.Single(pk);
|
||||
Assert.Equal("{\"a\":1,\"b\":2}", pk[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Digest_ChangesWhenSchemaChanges()
|
||||
{
|
||||
using var db = Create();
|
||||
await CreateOrders(db);
|
||||
|
||||
db.RegisterReplicated("orders");
|
||||
var before = db.ReplicatedTables["orders"].Digest;
|
||||
|
||||
await db.ExecuteAsync("ALTER TABLE orders ADD COLUMN note TEXT");
|
||||
db.RegisterReplicated("orders");
|
||||
var after = db.ReplicatedTables["orders"].Digest;
|
||||
|
||||
Assert.NotEqual(before, after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplicatedTables_ExposesRegistry()
|
||||
{
|
||||
using var db = Create();
|
||||
await CreateOrders(db);
|
||||
|
||||
db.RegisterReplicated("orders");
|
||||
|
||||
var table = db.ReplicatedTables["orders"];
|
||||
Assert.Equal("orders", table.Name);
|
||||
Assert.Equal(new[] { "id" }, table.PkColumns);
|
||||
Assert.Equal(new[] { "id", "sku", "qty" }, table.Columns.Select(c => c.Name).ToArray());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user