[RLC] Case 4 — Capture & persist compaction OPTIMIZE source composition (write side) - #12
Open
sezruby wants to merge 7 commits into
Open
[RLC] Case 4 — Capture & persist compaction OPTIMIZE source composition (write side)#12sezruby wants to merge 7 commits into
sezruby wants to merge 7 commits into
Conversation
This was referenced Aug 4, 2026
sezruby
force-pushed
the
optimize-dv-remap-capture
branch
from
August 4, 2026 19:01
f27d569 to
7cda6c7
Compare
When OPTIMIZE conflict reconciliation is enabled, record on each removed
source file's tombstone where that source's live rows landed in the
compacted output, so a later conflict check can remap a deletion vector
between the source and the output instead of aborting.
- SourceCompositionCaptureExec: a write-stage operator that observes,
per output partition, each source file's contiguous run of rows via
the input-file identity and a per-file row count (row and vectorized
execution paths), accumulating (sourceFile, outputStart, liveCount)
runs without imposing a sort.
- The OptimizeExecutor coalesce/compaction path injects the capture
(compaction only: repartition and a clustering pass permute rows, so
no contiguous offset mapping exists) and writes the composition as the
RemoveFile compactedInto / compactionInfo tombstone tags.
- RemoveFile.Tags.COMPACTED_INTO / COMPACTION_INFO + CompactionInfoEntry
define the on-disk tag format (modeled on what Databricks Runtime
records; cross-engine interop is best-effort, not a verified
guarantee). Persisted, not stripped: an ignored tag only forgoes a
reconcile, never affects correctness.
This is the capture/persist layer only; the conflict-time reconcile that
consumes the tags lands in a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…les overload Address review feedback on the OPTIMIZE source-composition capture/write path: - buildRemoveFilesWithCompactionCompositionTags now returns the tagged Seq[RemoveFile] directly (inner `untagged` fallback), dropping the Map.empty fast/slow-branch plumbing at the call site. - Keep the public 4-arg writeFiles signature undisturbed; add a 5-arg overload (no default arg) carrying sourceCompositionCapture and the SourceCompositionCaptureExec injection. - Drop the vestigial `val writeOutput = output` alias; revert normalizeData/normalizeSchema signatures to their original single-line form. - Add a columnar-path unit test for interleaved source batches (the "mixed" shape the one-run-per-file gate rejects). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e tests Extract the reconcile-capture read (clone session, pin FILES_MAX_PARTITION_BYTES to the compaction target + FILES_MIN_PARTITION_NUM=1, run createDataFrame under the pinned active session) into readCompactionSourceWithWholeFilePins so the vanilla OPTIMIZE route keeps its plain createDataFrame inline. The non-RLC tombstone branch likewise skips the reconcile helper and writes plain untagged RemoveFiles. Strengthen SourceCompositionCaptureExecSuite (now on DeltaSQLCommandTest) with two end-to-end tests over a real compaction OPTIMIZE: - multi-row-group sources under a hostile ambient split size still yield contiguous per-source compactedInto/compactionInfo tags tiling the output from offset 0; - a source with no row-count stats fails the trustworthiness gate and falls back to plain untagged tombstones (a losing DML aborts as today), data intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capture gate excludes the repartition compaction path (useRepartition): repartition(1) shuffles rows into the output, so no source keeps a contiguous row range and an offset composition would be meaningless -- a DV remapped by it would corrupt data. Add a regression test asserting that with reconcile on and optimize.repartition.enabled on, the removed sources carry plain untagged tombstones (no compactedInto / compactionInfo), and the data is intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-by no-tag tests Make the multi-row-group capture test a genuine regression guard for the read pins in readCompactionSourceWithWholeFilePins. It now sets an ambient FILES_MAX_PARTITION_BYTES (8 KiB) that would break each source into a full split plus a row-bearing remainder and interleave the sources under coalesce(1)'s descending-length packing. With the pins present the command overrides this to the compaction target so each source reads whole and stays one contiguous run (tags written); drop the pins and the sources interleave, the one-run-per-file gate declines, and the test fails. (The earlier 512 B value never interleaved -- many equal splits plus a footer-sized remainder -- so it could not catch pin removal.) Also add two isMultiDimClustering-path contract tests asserting that ZORDER and CLUSTER BY OPTIMIZE write no composition tags, since a clustering/z-order pass permutes rows and no contiguous offset composition exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sezruby
force-pushed
the
optimize-dv-remap-capture
branch
from
August 4, 2026 20:21
7cda6c7 to
657bbce
Compare
7 tasks
buildRemoveFilesWithCompactionCompositionTags could throw while building the composition tags (an unmappable source path, a JSON serialization error) after the OPTIMIZE output was already written, turning a best-effort optimization enabler into a hard failure of the OPTIMIZE commit. Wrap the whole tag-building match in try/catch(NonFatal): on any failure fall back to plain untagged tombstones -- exactly what vanilla OPTIMIZE writes -- so the already-written OPTIMIZE still commits and a concurrent loser aborts exactly as it does today. This upholds the method's own documented contract that capture is never required for OPTIMIZE correctness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the row-level-concurrency umbrella (#3) — the shared write side of Case 4 (
OPTIMIZE⟂ DML reconciliation; full design #7). This PR only captures and persists where each compacted source's rows landed. The two reconcile directions that consume it stack on top: forward — OPTIMIZE loses (PR #9) — and reverse — DML loses (PR #11).What
When OPTIMIZE conflict reconciliation is enabled, record on each removed source file's
RemoveFiletombstone where that source's live rows landed in the compacted output. A later conflict check can then remap a concurrent deletion vector between the source and the output instead of aborting the loser. This PR adds only the capture + the persisted tag — no conflict-time logic.Capture:
(file, count)fromInputFileBlockHolder— no_metadataThe run layout can't be derived from the read plan (Spark sorts scan splits size-descending before packing; a non-Spark engine's read order is opaque), so it's observed at write time — but cheaply:
SourceCompositionCaptureExec— a write-stage operator injected likeDeltaOptimizedWriterExec— reads the current source file fromInputFileBlockHolder(the thread-localinput_file_name()uses) and counts rows per file, in write order, on both the row and vectorized execution paths. No helper column, no_metadata.row_index, no_metadata.file_path— rows pass through unchanged (no strip, no copy).coalesce(no-shuffle) compaction path each source file's live rows land in one contiguous output segment, so recording(outputPath, outputStart, liveCount)per source fully describes the layout._metadata.row_indexis the win: requesting it forces the DV-aware position-tracking scan path on every file, and_metadata.file_pathmaterializes a per-row path string — together the bulk of the double-digit end-to-end overhead an earlier_metadata-observation prototype paid.The persisted tags
On each removed source's
RemoveFiletombstone:rowOffsetInTarget— the source's contiguous run-start in the compacted output (running sum in write order, evaluated on the driver at tag time).sourceNumPhysicalRecords— the source's physical row count. A reader derives the live run length assourceNumPhysicalRecords − |read-time DV|, where the read-time DV is carried on the same tombstone (RemoveFile.deletionVector). Storing the physical (not live) count keeps the entryO(#source files)regardless of DV cardinality — a fragmented read-time DV never bloats the tag.AddFile): snapshot reconstruction replays everyAddFileon every read, whereas a removed file's tombstone is never materialized into the live snapshot, so the read hot path stays clean; tombstone retention (default ~7 days) comfortably outlives the conflict window. The Delta protocol ignores unknownRemoveFiletags, so persisting is free and forward-compatible — an ignored tag only forgoes a reconcile, never affects correctness.OPTIMIZEon a shared table is best-effort, not a verified guarantee.CompactionInfoEntrytolerates unknown fields for forward-compatible schema drift.Scope / guards (coalesce-only)
Captured only for order-preserving compaction on the
coalescepath. Otherwise nothing is recorded and a reader falls back to today's abort:InputFileBlockHolderis empty after it.oneRunPerFileviolated, or a sourceliveCountmismatch → not captured.Safe because opt-in: losing the reconcile only restores current behavior, never wrong data.
Dependency
Stacked on the same-file DV-merge PoC (base branch
row-level-concurrency-dv-poc, Case 2 — #5 / PR #2).Test —
SourceCompositionCaptureExecSuiteEnd-to-end capture-and-reconcile behavior is exercised by the stacked reconcile PRs (#9 forward, #11 reverse).
🤖 Generated with Claude Code