Skip to content

[Spark] Conflict-time data skipping (delete/read tier) - #15

Open
sezruby wants to merge 1 commit into
conflict-data-skipping-value-exactfrom
conflict-data-skipping-delete-read
Open

[Spark] Conflict-time data skipping (delete/read tier)#15
sezruby wants to merge 1 commit into
conflict-data-skipping-value-exactfrom
conflict-data-skipping-delete-read

Conversation

@sezruby

@sezruby sezruby commented Aug 19, 2026

Copy link
Copy Markdown
Owner

What

Refines the delete/read conflict check (checkForDeletedFilesAgainstCurrentTxnReadFiles) from path-keyed to row-level — the delete/read analogue of the value-exact added-files skipping. Stacked on the value-exact PR (base branch conflict-data-skipping-value-exact).

Today the check aborts the current transaction whenever a file the winner removed is in the transaction's read set, regardless of whether any removed row matches what the transaction actually read. This refines it so a delete/read conflict is raised only when a removed row matches a read predicate.

How

Remove arm. Of the removed files the transaction read, keep the abort only if the winner removed a row matching a read predicate. The removed rows are recovered without an inverse deletion-vector read: because a DML only ever adds deletions, the winner's new DV is a superset of the pre-image's, so

matches(removed) = matches(pre-image live rows) - matches(post-image live rows)

where the pre-image is the removed file under its old DV and the post-image is the winner's paired re-added file under its new DV (absent for a full-file removal). The two images are unioned with signed weights (+1 pre, -1 post) and summed in a single Spark job; the difference is >= 1 iff some removed row matches. New reader ConflictDataSkippingReader.anyRemovedRowMatchesReadPredicate, wired in via a small private helper; the existing path-keyed lookup is unchanged when the refinement is off.

Add arm (merge-on-read companion). A merge-on-read DELETE removes rows by widening a file's DV: it emits RemoveFile(P, oldDV) and re-adds AddFile(P, newDV). The re-add's live rows are survivors that already existed in the read snapshot — it carries no new data, so it is excluded from the added-files check (checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn) by an in-memory path filter. Without this, a reader disjoint from the removed rows would still abort on the added-files arm with ConcurrentAppendException before the remove-arm refinement is consulted. Genuinely new rows from an UPDATE/MERGE land at a fresh path (not in removedFiles) and remain checked. The value-exact scan itself is otherwise untouched.

Config

Both arms read data during conflict detection, so they ride on the existing value-exact flags — conflictDetection.dataSkipping.enabled + conflictDetection.dataSkipping.valueExact.enabled — rather than a separate config. The refinement is one-way safe (abort unless a scan proves no removed row matches) and fail-safe (any error or missing information keeps today's path-keyed abort).

Tests

New DeleteReadConflictDataSkippingSuite (11): direct-reader count tests with real deletion vectors, whole-file COW delete e2e, and merge-on-read e2e (disjoint reader commits, overlapping reader aborts with delete/read).

🤖 Generated with Claude Code

@sezruby
sezruby force-pushed the conflict-data-skipping-delete-read branch from cfc2f36 to f9969af Compare August 19, 2026 20:41
@sezruby
sezruby force-pushed the conflict-data-skipping-delete-read branch 4 times, most recently from 1d396de to 2559729 Compare August 25, 2026 20:13
@sezruby sezruby changed the title [Spark] Row-level delete/read refinement for conflict detection (RLC Case 1b) [Spark] Row-level delete/read refinement for conflict detection Aug 25, 2026
@sezruby
sezruby force-pushed the conflict-data-skipping-delete-read branch from 2559729 to 7e322e9 Compare August 25, 2026 21:41
@sezruby sezruby changed the title [Spark] Row-level delete/read refinement for conflict detection [Spark] Conflict-time data skipping (delete/read tier) Aug 25, 2026
@sezruby
sezruby force-pushed the conflict-data-skipping-delete-read branch from 7e322e9 to 26623f0 Compare August 25, 2026 21:42
The delete/read check (checkForDeletedFilesAgainstCurrentTxnReadFiles) is
path-keyed: it aborts the current transaction whenever a file the winner
removed is in the transaction's read set, regardless of whether any removed
ROW matches what the transaction read. This refines it to a row-level check,
the delete/read analogue of the value-exact added-files skipping.

Remove arm. Of the removed files the transaction read, keep the abort only if
the winner removed a row matching a read predicate. The removed rows are
obtained without an inverse deletion-vector read: because a DML only ever ADDS
deletions, the winner's new DV is a superset of the pre-image one, so
  matches(removed) = matches(pre-image live) - matches(post-image live)
where the pre-image is the removed file under its old DV and the post-image is
the winner's paired re-added file under its new DV (absent for a full-file
removal). The two images are unioned with signed weights (+1 pre, -1 post) and
summed in a single Spark job; the difference is >= 1 iff some removed row
matches. New reader ConflictDataSkippingReader.anyRemovedRowMatchesReadPredicate,
wired in via the small private helper removedRowMatchesReadPredicate; the
existing path-keyed lookup is unchanged when the refinement is off.

Add arm. A merge-on-read DELETE removes rows by widening a file's DV: it emits
RemoveFile(P, oldDV) AND re-adds AddFile(P, newDV). The re-add's live rows are
survivors that already existed in the read snapshot -- it carries no new data,
so it is excluded from the added-files check
(checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn) by an in-memory path
filter. Without this, a reader disjoint from the removed rows would still abort
on the added-files arm with ConcurrentAppendException before the remove-arm
refinement is consulted. Genuinely new rows from an UPDATE/MERGE land at a
fresh path (not in removedFiles) and remain checked. The value-exact scan of
getFirstFileMatchingPartitionPredicates is otherwise untouched.

Both arms read data during conflict detection, so they ride on the existing
value-exact flags (conflictDetection.dataSkipping.enabled +
conflictDetection.dataSkipping.valueExact.enabled) rather than a separate
config (deleteReadRowLevelRefinementEnabled). One-way safe (abort unless a scan
proves no removed row matches) and fail-safe (any error or missing information
keeps the path-keyed abort).

The remove-arm scan runs as its own Spark job, launched on a shared
DeltaThreadPool just before the added-files scan so the two independent jobs
overlap; the delete/read check harvests the future (ThreadUtils.awaitResult),
falling back to inline execution when no scan was started. The checker-side
glue lives in a self-typed trait ConflictCheckerDataSkipping mixed into
ConflictChecker, mirroring the reader-side ConflictDataSkippingReader, so the
feature stays a single additive unit.

Tests: new DeleteReadConflictDataSkippingSuite (11) -- direct-reader count
tests with real DVs, whole-file COW delete e2e, and merge-on-read e2e
(disjoint reader commits, overlapping reader aborts with delete/read).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant