[LocalDb] Snapshot streams child rows before parents; a failed row is skipped forever (latent) #37
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the scadaproj#15 shared-library review (
code-reviews/LocalDb/findings.md, LocalDb-002), verified at91fb2b4. Update the finding's Resolution row when fixed.ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/Internal/SnapshotStreamer.cs:79Description
SnapshotStreamer.SendAsyncstreams tables in ordinal name order(
.OrderBy(t => t.Name, StringComparer.Ordinal), line 79–81). Within a table it pagesORDER BY rv.pk_json(line 196), which is text order:{"id":10}sorts before{"id":2}.The receiver applies through
LwwApplieron a connection withPRAGMA foreign_keys=ON. Animmediate FK violation is a
SqliteException, andLwwApplier.cs:116treats it as per-entrypoison: dead-letter, keep going.
SnapshotApplier.OnCompleteAsyncthen moves the watermark toas_of_seq, and the sender clears its snapshot debt on theSnapshotAck. Nothing ever re-sends thedead-lettered rows.
Registration explicitly allows these schemas.
EnsureNoInboundActionOnDeleterejects onlyCASCADE / SET NULL / SET DEFAULT, and README §Limitations recommends
NO ACTION/RESTRICT. So areplicated parent/child pair with a default FK is a supported configuration that the delta path
handles (seq order matches commit order) but the snapshot path does not.
Failure scenario
Reproduced in a scratch test:
z_parent(id PK)anda_child(id PK, parent_id REFERENCES z_parent(id))on both nodes.(1)intoz_parentand(10, 1)intoa_childon A.Result on B:
parents=1 children=0, one dead-lettera_child: SQLite Error 19: 'FOREIGN KEY constraint failed', watermark advanced to 2. A self-referencing table (node(id, parent_id REFERENCES node(id))with rows 2 and 10→2) loses row 10 the same way. The child rows are gone fromB permanently, and the pair reports itself converged.
No current consumer has an FK on a replicated table (per the 0.3.3 notes), so this is latent. The
first consumer to add one loses data on every snapshot resync.
Recommendation
Choose one:
PRAGMA defer_foreign_keys = ONinside the apply transaction, whichchecks FKs at commit. This only helps if parent and child land in the same transaction, which
they do not today across batches. More robustly, apply the whole snapshot with FK enforcement off
(
PRAGMA foreign_keys=OFFon the snapshot apply connection, set outside a transaction) and runPRAGMA foreign_key_checkatSnapshotCompleteto log or dead-letter any true orphans.table still needs deferral.
Also add a snapshot test with a non-cascading FK, and consider not dead-lettering
SQLITE_CONSTRAINT_FOREIGNKEYsilently on the snapshot path.Resolution
Unresolved.
Fixed on main
4af0ea8(LocalDb-002 → Resolved): 2-node test reproduced both child rows dead-lettered with the watermark moving past them. Sender now streams parent tables first; the receiver holds back rows that fail only their FK check, retries them as rows arrive, and dead-letters true orphans at snapshot end. 4 new tests fail without the fix. Mixed-version note: an older receiver still loses self-referencing rows arriving child-first (documented). Evidence: LocalDb 236/236; Auth 344 passed / 78 skipped (live SQL/LDAP gated) after rebase; code-reviews --check consistent.