fix(localdb): capture triggers survive consumer upserts (explicit ON CONFLICT upsert form, outer-statement override)
This commit is contained in:
+36
-20
@@ -141,28 +141,44 @@ public sealed class BidirectionalConvergenceTests
|
||||
Assert.Equal(21, await ConvergenceFixture.CountOrdersAsync(fx.B));
|
||||
}
|
||||
|
||||
// TODO(product bug): `INSERT ... ON CONFLICT(id) DO UPDATE` on a replicated table crashes the
|
||||
// AFTER-UPDATE capture trigger `__localdb_<table>_au` with
|
||||
// SQLite Error 19 (1555 SQLITE_CONSTRAINT_PRIMARYKEY):
|
||||
// 'UNIQUE constraint failed: __localdb_row_version.table_name, __localdb_row_version.pk_json'.
|
||||
// Minimal repro: on a fresh replicated `orders` table, upsert the SAME pk twice via ON CONFLICT
|
||||
// DO UPDATE (first = insert, second = the DO-UPDATE branch) — no replication, no concurrency.
|
||||
// A plain `UPDATE orders SET ...` firing the same au trigger does NOT fail; the defect is
|
||||
// specific to the upsert form perturbing last_insert_rowid() inside the au trigger's
|
||||
// `INSERT OR REPLACE INTO __localdb_row_version ... WHERE seq = last_insert_rowid()` capture.
|
||||
// The convergence tests intentionally avoid the upsert form (see ConvergenceFixture.UpsertAsync).
|
||||
// Un-skip once the capture trigger supports UPSERT.
|
||||
[Fact(Skip = "Product bug: ON CONFLICT DO UPDATE crashes the au capture trigger (UNIQUE on __localdb_row_version). See TODO above.")]
|
||||
public async Task UpsertOnConflictDoUpdate_CrashesCaptureTrigger_ProductBug()
|
||||
// Regression for the former product bug where a consumer `INSERT ... ON CONFLICT(id) DO UPDATE`
|
||||
// crashed the au capture trigger: SQLite replaces a trigger-body statement's OR-conflict
|
||||
// algorithm with the OUTER statement's conflict handling, so the trigger's row_version
|
||||
// `INSERT OR REPLACE` degraded to a plain INSERT -> SQLITE_CONSTRAINT_PRIMARYKEY (1555).
|
||||
// Fixed by an explicit ON CONFLICT DO UPDATE clause in the capture triggers (not overridden).
|
||||
[Fact]
|
||||
public async Task Upsert_OnConflictDoUpdate_CapturesCorrectly()
|
||||
{
|
||||
await using var fx = new ConvergenceFixture();
|
||||
await fx.A.ExecuteAsync(
|
||||
"INSERT INTO orders (id, sku, qty) VALUES (7, 'FIRST', 1) " +
|
||||
"ON CONFLICT(id) DO UPDATE SET sku = excluded.sku, qty = excluded.qty");
|
||||
// Second upsert takes the DO UPDATE branch and throws the UNIQUE-constraint SqliteException.
|
||||
await fx.A.ExecuteAsync(
|
||||
"INSERT INTO orders (id, sku, qty) VALUES (7, 'SECOND', 2) " +
|
||||
"ON CONFLICT(id) DO UPDATE SET sku = excluded.sku, qty = excluded.qty");
|
||||
const string upsert =
|
||||
"INSERT INTO orders (id, sku, qty) VALUES (7, @sku, @qty) " +
|
||||
"ON CONFLICT(id) DO UPDATE SET sku = excluded.sku, qty = excluded.qty";
|
||||
|
||||
await fx.A.ExecuteAsync(upsert, new { sku = "FIRST", qty = 1 }); // insert branch -> ai
|
||||
await fx.A.ExecuteAsync(upsert, new { sku = "SECOND", qty = 2 }); // DO-UPDATE branch -> au
|
||||
|
||||
// Both branches captured, in order, with full-row payloads.
|
||||
var oplog = await fx.A.QueryAsync(
|
||||
"SELECT row_json, hlc, is_tombstone FROM __localdb_oplog WHERE table_name='orders' ORDER BY seq",
|
||||
static r => (RowJson: r.GetString(0), Hlc: r.GetInt64(1), Tomb: r.GetInt64(2)));
|
||||
Assert.Equal(2, oplog.Count);
|
||||
Assert.Equal("{\"id\":7,\"sku\":\"FIRST\",\"qty\":1}", oplog[0].RowJson);
|
||||
Assert.Equal("{\"id\":7,\"sku\":\"SECOND\",\"qty\":2}", oplog[1].RowJson);
|
||||
Assert.All(oplog, o => Assert.Equal(0, o.Tomb));
|
||||
|
||||
// Single row_version entry for the pk, correlated to the LATEST (au-branch) oplog hlc.
|
||||
var rv = await fx.A.QueryAsync(
|
||||
"SELECT pk_json, hlc, is_tombstone FROM __localdb_row_version WHERE table_name='orders'",
|
||||
static r => (PkJson: r.GetString(0), Hlc: r.GetInt64(1), Tomb: r.GetInt64(2)));
|
||||
var v = Assert.Single(rv);
|
||||
Assert.Equal("{\"id\":7}", v.PkJson);
|
||||
Assert.Equal(0, v.Tomb);
|
||||
Assert.Equal(oplog[1].Hlc, v.Hlc);
|
||||
|
||||
// And the captured upsert replicates end-to-end.
|
||||
await fx.StartAsync();
|
||||
await fx.AssertConvergedAsync();
|
||||
Assert.Equal((7L, "SECOND", (long?)2), await ReadRow(fx.B, 7));
|
||||
}
|
||||
|
||||
private static async Task<(long Id, string? Sku, long? Qty)> ReadRow(ILocalDb db, long id)
|
||||
|
||||
Reference in New Issue
Block a user