perf: Skip the projection pass after a local delete that cannot change it
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>
This commit is contained in:
parent
0345f56787
commit
b5450353b9
1 changed files with 262 additions and 4 deletions
|
|
@ -157,6 +157,182 @@ void main() {
|
|||
expect(tombstone.hlc, remoteParentDelete.hlc);
|
||||
},
|
||||
);
|
||||
|
||||
group('and the restrict child is then deleted locally,', () {
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
await RestrictChild.db.deleteRow(session, child, transaction: tx);
|
||||
});
|
||||
});
|
||||
|
||||
test('then the merged parent delete takes effect.', () async {
|
||||
expect(await Person.db.findById(session, parent.id!), isNull);
|
||||
expect(await RestrictChild.db.findById(session, child.id!), isNull);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'Given a set-null middle with two cascade children,',
|
||||
() {
|
||||
late FkChainSetNullMiddle middle;
|
||||
late FkChainSetNullCascadeChild deleted;
|
||||
late FkChainSetNullCascadeChild sibling;
|
||||
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
middle = await FkChainSetNullMiddle.db.insertRow(
|
||||
session,
|
||||
FkChainSetNullMiddle(id: const Uuid().v7obj(), name: 'middle'),
|
||||
transaction: tx,
|
||||
);
|
||||
deleted = await FkChainSetNullCascadeChild.db.insertRow(
|
||||
session,
|
||||
FkChainSetNullCascadeChild(
|
||||
id: const Uuid().v7obj(),
|
||||
name: 'deleted child',
|
||||
setNullMiddleId: middle.id,
|
||||
),
|
||||
transaction: tx,
|
||||
);
|
||||
sibling = await FkChainSetNullCascadeChild.db.insertRow(
|
||||
session,
|
||||
FkChainSetNullCascadeChild(
|
||||
id: const Uuid().v7obj(),
|
||||
name: 'sibling',
|
||||
setNullMiddleId: middle.id,
|
||||
),
|
||||
transaction: tx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when one cascade child is deleted locally,', () {
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
await FkChainSetNullCascadeChild.db.deleteRow(
|
||||
session,
|
||||
deleted,
|
||||
transaction: tx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('then only that child is hidden, by a user delete.', () async {
|
||||
expect(
|
||||
await FkChainSetNullCascadeChild.db.findById(session, deleted.id!),
|
||||
isNull,
|
||||
);
|
||||
final visibleSibling = await FkChainSetNullCascadeChild.db.findById(
|
||||
session,
|
||||
sibling.id!,
|
||||
);
|
||||
expect(visibleSibling, isNotNull);
|
||||
expect(visibleSibling!.setNullMiddleId, middle.id);
|
||||
expect(
|
||||
await FkChainSetNullMiddle.db.findById(session, middle.id!),
|
||||
isNotNull,
|
||||
);
|
||||
final tombstone = await CrdtDataDeleted.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.row.uuidRowId.equals(deleted.id),
|
||||
);
|
||||
expect(tombstone, isNotNull);
|
||||
expect(tombstone!.isDeleted, isTrue);
|
||||
expect(tombstone.reason, CrdtDataDeletedReason.userDelete);
|
||||
});
|
||||
|
||||
group('and the middle is then deleted by a merged remote delete,', () {
|
||||
setUp(() async {
|
||||
await session.db.mergeChanges(
|
||||
[
|
||||
_deleteChange(
|
||||
tableName: FkChainSetNullMiddle.t.tableName,
|
||||
rowId: middle.id!,
|
||||
after: await rowHlc(middle.id!),
|
||||
),
|
||||
],
|
||||
spaceId: testCrdtUserId,
|
||||
);
|
||||
});
|
||||
|
||||
test('then the remaining child is hidden with it.', () async {
|
||||
expect(
|
||||
await FkChainSetNullMiddle.db.findById(session, middle.id!),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await FkChainSetNullCascadeChild.db.findById(session, sibling.id!),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'Given a set-null middle kept by its restrict child against a merged delete,',
|
||||
() {
|
||||
late FkChainSetNullMiddle middle;
|
||||
late FkChainSetNullRestrictChild blocker;
|
||||
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
middle = await FkChainSetNullMiddle.db.insertRow(
|
||||
session,
|
||||
FkChainSetNullMiddle(id: const Uuid().v7obj(), name: 'kept middle'),
|
||||
transaction: tx,
|
||||
);
|
||||
blocker = await FkChainSetNullRestrictChild.db.insertRow(
|
||||
session,
|
||||
FkChainSetNullRestrictChild(
|
||||
id: const Uuid().v7obj(),
|
||||
name: 'blocker',
|
||||
setNullMiddleId: middle.id,
|
||||
),
|
||||
transaction: tx,
|
||||
);
|
||||
});
|
||||
await session.db.mergeChanges(
|
||||
[
|
||||
_deleteChange(
|
||||
tableName: FkChainSetNullMiddle.t.tableName,
|
||||
rowId: middle.id!,
|
||||
after: await rowHlc(middle.id!),
|
||||
),
|
||||
],
|
||||
spaceId: testCrdtUserId,
|
||||
);
|
||||
expect(
|
||||
await FkChainSetNullMiddle.db.findById(session, middle.id!),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
group('when the restrict child is deleted locally,', () {
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
await FkChainSetNullRestrictChild.db.deleteRow(
|
||||
session,
|
||||
blocker,
|
||||
transaction: tx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('then the merged delete of the middle takes effect.', () async {
|
||||
expect(
|
||||
await FkChainSetNullMiddle.db.findById(session, middle.id!),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await FkChainSetNullRestrictChild.db.findById(session, blocker.id!),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue