Skip to content

fix: thread {transaction} into removeEntry for audit:false table deletes#1869

Open
kriszyp wants to merge 2 commits into
mainfrom
fix/audit-false-delete-transaction-rollback
Open

fix: thread {transaction} into removeEntry for audit:false table deletes#1869
kriszyp wants to merge 2 commits into
mainfrom
fix/audit-false-delete-transaction-rollback

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #1854 — F-147: on an audit:false @indexed table, _writeDelete()'s non-audit branch called removeEntry(primaryStore, existingEntry) without threading the enclosing transaction through, unlike its two sibling branches (updateIndices, updateRecord), both of which already pass transaction && { transaction } / { ..., transaction, ... }.

Why

removeEntry() passes its options argument straight to store.remove(key, options). With no {transaction}, the removal committed standalone against the raw store instead of joining the ambient request transaction. Practical effect: an aborted transaction that deletes a record on an audit:false table still durably removed the base row (and unlinked its blob, since removeEntry's blob-unlink gate keys off the removal's own commit, not the outer transaction) — while everything else in the transaction correctly rolled back. This also left a dangling secondary-index entry, since the index removal (updateIndices) did roll back but the base-row removal did not.

Fix

-						removeEntry(primaryStore, existingEntry);
+						removeEntry(primaryStore, existingEntry, transaction && { transaction });

One-line change, matching the pattern already used by the sibling updateIndices call two lines above it.

Test coverage

Added integrationTests/resources/audit-false-delete-rollback.test.ts (+ fixture) with two probes against a fresh @table(audit: false) + @indexed table:

  • P1: delete + forced throw (transaction abort) — row and secondary-index entry must both survive. Verified this fails without the fix (reverted the one-line change locally, confirmed P1 fails with AssertionError: Item must still exist after the aborted delete) and passes with it.
  • P2: delete without a throw (normal commit) — row and index entry must both be gone (regression guard for ordinary deletes).

Ran npm run test:unit:main, npm run test:unit:resources, and the new integration test plus neighboring delete/eviction/relationship-transaction integration tests — all green (pre-existing, unrelated globalIsolation.test.js SES-compartment failures on test:unit:main confirmed present on main too, and a pre-existing unrelated tsc type error in DatabaseTransaction.ts confirmed present on main — neither touches this code path).

Note this was generated by an LLM (Claude Sonnet 5).

@kriszyp
kriszyp requested a review from cb1kenobi July 20, 2026 12:58
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes an issue where aborted deletes on audit: false tables failed to roll back cleanly, leaving dangling secondary-index entries. The fix threads the enclosing transaction into the removeEntry call in resources/Table.ts. Additionally, a comprehensive integration test suite has been added to verify this behavior. The review feedback suggests removing redundant sleep calls in the new integration tests, as the awaited HTTP requests guarantee that the transaction lifecycle has already settled.

Comment thread integrationTests/resources/audit-false-delete-rollback.test.ts Outdated
Comment thread integrationTests/resources/audit-false-delete-rollback.test.ts Outdated
@kriszyp
kriszyp marked this pull request as ready for review July 21, 2026 18:42
kriszyp and others added 2 commits July 22, 2026 13:42
_writeDelete()'s non-audit branch called removeEntry(primaryStore,
existingEntry) without the enclosing transaction, unlike its two sibling
branches (updateIndices, updateRecord) which both thread {transaction}
through. removeEntry() passes its options straight to store.remove(key,
options), so the removal committed standalone against the raw store
instead of joining the ambient transaction — an aborted transaction on
an audit:false @indexed table still durably removed the row (and its
blob), while everything else in the transaction rolled back, leaving a
dangling secondary-index entry behind.

Fixes #1854.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…back test

Adds a file-backed Blob attribute to the P1/P2 fixture so the aborted-delete
test also proves the blob file survives (addresses cb1kenobi's review nit —
the blob-unlink gate on removeEntry was otherwise untested). Also drops the
two sleep(300) calls gemini-code-assist flagged as redundant: postJSON is
fully awaited, so the transaction has already settled by the time the
response returns.

Refs #1854

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kjqUHT6podeU7FeVYStdr
@kriszyp
kriszyp force-pushed the fix/audit-false-delete-transaction-rollback branch from 77ceeeb to 5de34a2 Compare July 22, 2026 19:43
Comment thread resources/Table.ts
if (!audit || isRocksDB) scheduleCleanup();
} else {
removeEntry(primaryStore, existingEntry);
removeEntry(primaryStore, existingEntry, transaction && { transaction });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, it's safe to pass an undefined/falsey transaction into rocksdb-js' remove(). However in lmdb-js, this maps to the 2nd arg which is the ifVersionOrValue and that's probably not ok to pass in an object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aborted transaction on audit:false delete still commits row removal + blob unlink + leaves dangling index entries

2 participants