A merge that deleted anything ran the full projection pass, whose closure
follows foreign keys both ways and so loads every row connected to the
deleted ones. A device deleting a card with its revisions, tags and
reviews cost each device receiving it about 4 s, and the server merging
the push about 5.4 s, in a library of 8,000 cards whose tags join it.
A device deleting a row locally tombstones everything visible below it,
so its batch carries those deletes along. What the pass could still
decide is a row below that the deleting device never saw, inserted here
meanwhile, which it hides by cascade. A batch that only deletes (none
restoring) now leaves the pass out when the deleted tables cascade only,
by the same rules as a local delete (_deletesOnlyCascade, shared with
canSkipProjectionAfterLocalDelete now), no attempted value is pending in
the connected tables, and every visible row below a deleted one, found by
walking down the cascades alone, is deleted by the same batch
(canLeaveProjectionUnchangedByMergedDeletes). A batch both inserting and
deleting, or restoring, takes the pass as before.
Measured on PostgreSQL: such a card delete merges in about 35 ms instead
of 5.4 s; received on a device, 99 ms instead of 4 s. The new tests
cover a leaf deleted through the shortcut and a middle row's delete,
which isn't eligible, hiding its children by the pass; in the app's sync
tests a card deleted while another device rated it declines the shortcut,
and dropping the walk's check fails that test. The core, server,
integration and simulation suites pass, but for two PostgreSQL tests
that can't start their database while another embedded PostgreSQL runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A merge that touched any foreign key ran the planning pass for its new
rows and then the full projection pass, whose closure follows foreign
keys both ways and so loads every row connected to the merged ones. In a
library whose tags join its parts, one merged rating, edit, tag or new
card loaded all of it: about 9.5 s on a device receiving it and 12 s on
the server merging a push, for 8,000 cards.
A batch that only inserts new rows (each once) and updates columns no
projection reads now leaves both passes out when its inserts are proven,
as local inserts are (canLeaveProjectionUnchangedByMergedInserts, on
localProjectionDependenciesAreStable): each row references its parents by
id, each parent visible or inserted earlier in the same batch, nothing
refers to the row yet (no row, no attempted value), and no unique claim
is contested. A parent inserted after its child in the batch would find
the child's reference held back, which only the pass restores, so it
counts only when earlier. Deletes, inserts onto rows there already and
anything unproven take the passes as before. The batch's parents are
still resolved together for the writes.
The new tests cover a child of a visible parent, a parent and child in
one batch (both through the shortcut), and a child of a merged delete
and a parent arriving after its held-back child (both through the
passes); dropping the parent or the reference check fails the matching
test. The core, server, integration and simulation suites pass, but for
two PostgreSQL tests that cannot start their database while another
embedded PostgreSQL runs, with or without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
insteadOfDelete ran the full foreign key projection after every local
delete. Its closure follows edges both ways, so it loads every row
connected to the deleted one, which in a schema whose tables all hang off
one another is nearly the whole database: one deleted row of a person's
8,000 flashcards loaded 56,000 of 56,350 CRDT rows and took about 8 s on
the calling isolate, most of it deserialising rows. Inserts and updates
have had a proof that the pass is unnecessary
(canLeaveLocalProjectionUnchanged); deletes had none.
canSkipProjectionAfterLocalDelete proves it for a delete: every table
the delete can cascade into is reached by cascade edges on id only, none
of them restricts its parent (a deleted child can release a parent
delete held back by it), none has a unique index (a released claim can
bring a contestant back) or lies in a component with a SET DEFAULT edge,
and no attempted value is pending in the connected tables. The delete's
own cascade (applyDeleteActions) then is the whole projection; otherwise
the full pass runs as before. applyDeleteActions also asks for the
visible children of a whole batch of parents at once for cascades by id,
instead of one query per parent.
On that database a single delete now takes 6-63 ms, a card with its
revisions, tags and reviews 46-145 ms instead of 28-39 s. The new tests
cover a restrict child deleted locally releasing a merged parent delete
(on the person tables, and on the chain tables, where the shortcut is
asked and declines: the test fails with the restrict condition removed)
and a cascade child deleted locally through the shortcut, its parent
then deleted by a merged remote delete. The core, server, integration
and simulation suites pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>