Skip to content

[RLC] Case 3 — exclude relocation (dataChange=false) files from the append-conflict check - #10

Closed
sezruby wants to merge 3 commits into
masterfrom
optimize-append-datachange
Closed

[RLC] Case 3 — exclude relocation (dataChange=false) files from the append-conflict check#10
sezruby wants to merge 3 commits into
masterfrom
optimize-append-datachange

Conversation

@sezruby

@sezruby sezruby commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Part of the row-level-concurrency umbrella (#3) — Case 3. Full design: #6. Opened upstream as delta-io#7331 (this fork PR is superseded by that one).

Problem

A concurrent transaction that only rearranges existing rows — most importantly OPTIMIZE (compaction / Z-ORDER) — commits its outputs as AddFiles with dataChange = false and removes its compacted inputs as RemoveFiles, also with dataChange = false. Because OPTIMIZE is not a blind append (isBlindAppend = false), the WinningCommitSummary classifies those adds as changedDataAddedFiles.

As a result an insert-only / append-only writer running concurrently fails spuriously:

  1. ConflictChecker.checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn treats the OPTIMIZE output as newly-arrived data → ConcurrentAppendException if the writer's read predicate overlaps.
  2. If the writer registered actual readFiles (e.g. a whole-table read), checkForDeletedFilesAgainstCurrentTxnReadFiles sees OPTIMIZE removed a file it read → ConcurrentDeleteReadException.

Both fire even though OPTIMIZE introduced no new logical rows and deleted none — it only relocated existing rows across file boundaries. This is the exact symptom in delta-io#626 ("Prevent insert-only transactions from failing due to concurrent data preserving transactions"), the stale delta-io#1305, and the closed issue delta-io#326. Both prior PRs stalled on the reviewer's request (delta-io#1305) for a general safety argument rather than a targeted patch. This PR supplies that argument — now materially stronger than it could have been in 2022 — and closes both failure modes for an append-only writer.

The safety argument (now backed by a validated invariant)

A file committed with dataChange = false carries, by contract, only rows that already existed in the table at that version. It contributes zero new logical rows and deletes zero logical rows — it rearranges/compacts existing rows into new file boundaries.

Added-files check. Its purpose is to catch rows a loser should have read but didn't because a concurrent writer added them. A dataChange = false add can never hold such a row: every logical row in it was already present (in some other file) in the loser's read snapshot. So it is safe to drop from that check.

Removed-files-vs-read check (new in this PR). Its purpose is to catch a loser that read a file whose rows were concurrently deleted. A dataChange = false remove deleted no rows — the rows it dropped from that file still exist under a new file boundary. So an append-only writer that read the relocated file read nothing that was logically removed, and can safely reconcile. This exclusion is gated on the current transaction being append-only (it adds no RemoveFile and no AddFile carrying a deletion vector): a DML loser (DELETE/UPDATE/MERGE) reads a file precisely to rewrite it, so if that file was concurrently relocated it must still conflict — otherwise its own RemoveFile/DV would target a file that no longer exists and rows could be lost or resurrected.

The 2022 concern was essentially "could a commit mix data-changing and data-preserving files such that filtering by dataChange skips something that matters?" That mixed case can no longer exist: delta-io#6937 / delta-io#6969 ("Validate consistent dataChange across commit FileActions", merged upstream) enforce as a validated invariant that all FileActions in a single commit share one dataChange value (ConflictChecker.trackConsistentDataChange, fatal mode throws on violation). Therefore both .filter(_.dataChange) sites are provably whole-commit gates: either the entire winning commit was data-preserving (safe) or it changed data (checked exactly as before).

The change cannot mask a genuine conflict: a winner that adds real rows commits them dataChange = true (still fully checked), and a winner that genuinely deletes rows commits its RemoveFile dataChange = true (never excluded) — both covered by safety-floor tests.

(Databricks' row-level-concurrency contract documents that non-Z-Order OPTIMIZE "cannot conflict" with concurrent INSERT/UPDATE/DELETE/MERGE. This PR was implemented independently against that observable contract and the OSS ConflictChecker semantics.)

Scope (validated by tests)

Behind a single internal flag (default off), for an append-only current transaction:

  • Added-files checkdataChange = false adds are dropped → no spurious ConcurrentAppendException.
  • Removed-files-vs-read checkdataChange = false removes are dropped → no spurious ConcurrentDeleteReadException, for both a predicate-only reader and a whole-table (readFiles-populated) reader.

Deliberately not touched:

Change

Internal flag spark.databricks.delta.conflictDetection.excludeNoDataChangeFiles.enabled (default off). Added-files site filters addedFilesToCheckForConflicts.filter(_.dataChange); removed-files-vs-read site filters winningCommitSummary.removedFiles.filter(_.dataChange), gated on the current txn being append-only (forall over its actions: no RemoveFile, no AddFile with a deletion vector).

Tests

OptimisticTransactionSuite:

  • dataChange = false append (OPTIMIZE-style) vs concurrent partition read — flag on → reconcile, off → legacy ConcurrentAppendException.
  • dataChange = true append still conflicts … (safety floor) — a genuine new-data append still conflicts with the flag on.
  • full OPTIMIZE (removes + dataChange=false adds) vs insert-only partition reader reconciles — predicate-only reader, Prevent insert-only transactions from failing due to concurrent data preserving transactions delta-io/delta#626 shape.
  • full OPTIMIZE (removes + dataChange=false adds) vs whole-table APPEND-ONLY reader reconciles — whole-table readFiles reader now reconciles too (the removed-files half of the fix).
  • full OPTIMIZE (dataChange=false removes) vs concurrent DELETE loser still raises ConcurrentDeleteReadExceptionboundary: the append-only guard holds; a DML loser is not silenced.
  • genuine delete (dataChange=true remove) vs append-only reader still raises ConcurrentDeleteReadExceptionsafety floor for removes: real deletions are never excluded.

Prior art

sezruby and others added 3 commits July 29, 2026 19:41
… check

OPTIMIZE (compaction / Z-ORDER) commits its compacted outputs as
dataChange=false AddFiles. Because OPTIMIZE is not a blind append
(isBlindAppend=false), those files land in changedDataAddedFiles, so a
concurrent non-blind writer (UPDATE/DELETE/MERGE with a read predicate)
hits a spurious ConcurrentAppendException -- even though OPTIMIZE changed
no logical data. A dataChange=false file only rearranges rows that already
existed, so it can never be a row the losing txn "should have read."

Add conflictDetection.excludeNoDataChangeAddedFiles.enabled (internal,
default off) that filters dataChange=false files out of the added-files
check in checkForAddedFilesThatShouldHaveBeenReadByCurrentTxn. All
FileActions in a commit share one dataChange value (see
trackConsistentDataChange), so this is effectively a commit-level gate.

Tests: OptimisticTransactionSuite +5 cases -- OPTIMIZE-style dataChange=false
winner vs (a) a concurrent partition read and (b) a concurrent whole-table
read (the unpartitioned / Liquid Clustering scenario), each flag on ->
reconcile / off -> legacy abort; plus a safety-floor case proving a genuine
dataChange=true append still raises ConcurrentAppendException with the flag
on. Full OptimisticTransactionSuite 148/148 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validate the exclusion against a *realistic* OPTIMIZE that both removes the
compacted inputs and adds the dataChange=false output (the earlier tests only
modeled the added file):

- full OPTIMIZE vs an insert-only reader that registered only a partition read
  predicate (filterFiles(newFiles) does not populate readFiles) -> the separate
  removed-files check cannot fire, so with the flag on the txn FULLY reconciles
  (not merely swapping ConcurrentAppendException for ConcurrentDeleteRead). This
  is the exact insert-only shape reported in delta-io#326 / delta-io#626 / delta-io#1305.

- full OPTIMIZE vs a whole-table reader (readFiles populated) -> still raises
  ConcurrentDeleteReadException with the flag on, proving the added-files fix
  does not (and must not) silence a genuine read/remove overlap. Fully
  reconciling that case is the harder row-level-concurrency problem, out of
  scope here.

Dropped the earlier unpartitioned 'whole-table read reconciles' test: its
winner added a dataChange=false file without removing anything, which overstated
the fix (a real OPTIMIZE removes, and a whole-table reader then hits the
delete-read check as shown above).

Full OptimisticTransactionSuite 148/148 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nly)

Broaden the exclusion so an append-only writer no longer raises a spurious
ConcurrentDeleteReadException when a concurrent OPTIMIZE (dataChange = false)
merely relocated the files it read. A dataChange = false RemoveFile deletes no
logical rows -- every row survives under a new file boundary -- so it cannot
invalidate a read done by a transaction that itself adds no RemoveFile and no
deletion vector.

The exclusion is gated on the current transaction being append-only: a DML
loser (delete/update) reads files precisely to rewrite them, so a concurrent
relocation of a read file must still conflict, else its RemoveFile/DV would
target a file that no longer exists. Genuine deletes commit dataChange = true
and are always kept, so real delete/read conflicts still fire.

Rename the flag DELTA_CONFLICT_DETECTION_EXCLUDE_NO_DATA_CHANGE_ADDS ->
DELTA_CONFLICT_DETECTION_EXCLUDE_NO_DATA_CHANGE_FILES
(conflictDetection.excludeNoDataChangeFiles.enabled) to reflect that it now
covers both added and removed files; still default-off.

Tests (OptimisticTransactionSuite, 150/150):
- whole-table APPEND-ONLY reader vs full OPTIMIZE -> reconciles (was: raised)
- DML (delete) loser vs OPTIMIZE relocation -> still ConcurrentDeleteRead
- genuine dataChange=true delete vs append-only reader -> still conflicts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sezruby sezruby changed the title [Spark] Exclude dataChange=false added files (OPTIMIZE outputs) from the concurrent-append conflict check Exclude no-data-change (OPTIMIZE) files from append-only conflict checks Jul 30, 2026
@sezruby

sezruby commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Superseded — opened publicly as delta-io#7331 (branch unchanged). Closing this fork-internal review pass.

@sezruby sezruby closed this Jul 30, 2026
@sezruby sezruby changed the title Exclude no-data-change (OPTIMIZE) files from append-only conflict checks [RLC] Case 3 — exclude relocation (dataChange=false) files from the append-conflict check Aug 3, 2026
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