feat(format): define a unified tagged fragment reuse history - #9136
Conversation
|
Important Format specification voteThis PR modifies the Lance format specification, so it requires 3 binding +1 votes from PMC members (excluding the proposer) and a minimum 72-hour voting period, weekends excluded, before it can merge. Vote by approving this PR (+1) or requesting changes (−1, a veto). See the voting process. Status: ✅ Vote passed — 3 PMC approvals, voting period elapsed
Updated automatically by the format-spec vote gate, which re-checks every 15 minutes — just voted? Re-check now (press Run workflow; leave the input blank to re-check every open format PR). A PMC member may apply the |
Bit 9 is taken by the stable-row-id FRI compatibility flag (#9119), so the tagged-history capability moves to bit 1024. A rewrite and its transition may land in separate commits: transitions reference only committed fragments, and unmapped destinations are served by scanning, so correctness never depends on the mapping being present; atomic composition is deferred to a future composite transaction mechanism. Tables using stable row IDs do not support tagged histories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
A rewrite replaces the FRI entry with its mapping in the same commit; append_fri_transitions remains the separate-commit path for mappings produced apart from their rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
…nsitions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
| } | ||
|
|
||
| // TODO: compose with Rewrite atomically when Transaction V2 supports composite operations. | ||
| message AppendFragmentReuseTransitions { |
There was a problem hiding this comment.
The standalone AppendFragmentReuseTransitions operation seems premature here, since Rewrite can already publish the FRI entry atomically. Could we defer it until there is a concrete caller for separate mapping publication or composite transactions? That would let us review the durable operation’s semantics alongside its actual use, while keeping this PR focused on the unified history format.
There was a problem hiding this comment.
This is pre-added for composite transaction V2 at the assumption that transaction V2 will be landed soon. In that case, having a separate AppendFragmentReuseTransitions can help make the transaction more clear. I can remove it now and add it back later when transaction V2 is ready
There was a problem hiding this comment.
Agreed, removed in 66232d4: the operation, its oneof entry, and the related doc sections (the Appending FRI Transitions section and the separate-commit wording). Rewrite remains the only publication path, atomic with its own commit. We can bring a durable append back together with a concrete caller or composite transactions.
Remove AppendFragmentReuseTransitions from the transaction proto and its documentation. A rewrite already publishes the FRI entry atomically in its own commit; a durable append operation can be reviewed alongside a concrete caller or composite transactions when one exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
There was a problem hiding this comment.
✅ Gate recommendation: approve.
Deferring the standalone append operation keeps this format change focused on the concrete atomic path: a rewrite publishes its FRI mapping in the same commit as fragment replacement. The unified tagged history, compatibility fence, and fail-closed adapter behavior remain coherent, and the removed field had no released compatibility obligation.
Please mark this PR with the breaking-change label.
|
+1 binding from me |
…landed on main) Resolution: versioning.md keeps both flag rows (256 mixed-data-file-versions from main, 1024 fragment-reuse-index from this PR); the boundary sentence now reserves only bit 512 and keeps main's paired-bits rule. feature_flags.rs and table.proto auto-merged; this build still does not claim the 1024 capability (support lands up-stack), matching the spec-first posture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
|
Synced with main to clear the merge conflict (5410084). The conflict was the flag table in versioning.md: main landed @Xuanwo @BubbleCal apologies for the churn: the vote gate binds approvals to the commit, so this sync dropped your +1s. If the resolution looks right, a re-approve would be appreciated; the FRI text you reviewed is byte-identical. |
…nges) Clean fast-forward-style merge of lance-format/main; no conflicts. Touches only main's new non-spec code (write path, vector index, mem_wal tests). The FRI spec text, table.proto, feature_flags.rs, and versioning.md flag table are unchanged from the voted revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS (#9119) took bit 9, which pushed FLAG_UNKNOWN up to bit 10 -- the same bit FLAG_FRAGMENT_REUSE_INDEX (#9136) already occupies. Move FLAG_UNKNOWN to bit 11 so the unknown boundary sits above every named flag again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS (#9119) took bit 9, which pushed FLAG_UNKNOWN up to bit 10 -- the same bit FLAG_FRAGMENT_REUSE_INDEX (#9136) already occupies. On main the collision is latent because the tagged FRI bit stays below the FLAG_UNKNOWN boundary and is never marked supported. Move FLAG_UNKNOWN to bit 11 so it sits above every named flag again, add a const assert pinning FLAG_FRAGMENT_REUSE_INDEX below the boundary so an insertion cannot recreate the collision silently, and explicitly keep the tagged FRI bit unsupported (as the boundary used to do implicitly) until its reader/writer handling lands. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS (#9119) took bit 9, which pushed FLAG_UNKNOWN up to bit 10 -- the same bit FLAG_FRAGMENT_REUSE_INDEX (#9136) already occupies. On main the collision is latent because the tagged FRI bit stays below the FLAG_UNKNOWN boundary and is never marked supported. Move FLAG_UNKNOWN to bit 11 so it sits above every named flag again, reorder FLAG_FRAGMENT_REUSE_INDEX to its bit-order position (after bit 9, before the unknown boundary), add a const assert pinning it below the boundary so an insertion cannot recreate the collision silently, and explicitly keep the tagged FRI bit unsupported (as the boundary used to do implicitly) until its reader/writer handling lands. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
## Problem Two feature-flag constants collide on bit 10: ``` FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS = 1 << 9 // #9119 FLAG_UNKNOWN = 1 << 10 // collides FLAG_FRAGMENT_REUSE_INDEX = 1 << 10 // #9136 ``` `FLAG_FRAGMENT_REUSE_INDEX` (#9136) was defined at bit 10 while `FLAG_UNKNOWN` was at bit 9. Later #9119 inserted `FLAG_FRAG_REUSE_WITH_STABLE_ROW_IDS` at bit 9 and bumped `FLAG_UNKNOWN` up one -- but only to bit 10, which #9136 had already claimed. So on current main both live on the same bit. The collision is **latent** on main: `supported_flags_when` uses `FLAG_UNKNOWN - 1` as the base mask, so bit 10 is currently *below* the boundary being masked out, and the tagged FRI bit is never marked supported. But the moment any build activates the tagged FRI flag (adds it to the supported set), the `FLAG_UNKNOWN`-based "reject unknown flags" tests break, because `FLAG_UNKNOWN` **is** the FRI bit -- the rejection path would silently accept it. ## Fix - Move `FLAG_UNKNOWN` to `1 << 11` so it sits above every named flag again. - Add `const _: () = assert!(FLAG_FRAGMENT_REUSE_INDEX < FLAG_UNKNOWN)` to pin the invariant so a future insertion can't recreate the collision silently. - Explicitly `mark_supported(FLAG_FRAGMENT_REUSE_INDEX, false)` -- with the boundary moved, bit 10 would otherwise fall inside `FLAG_UNKNOWN - 1` and become supported; keep tagged FRI unsupported (exactly as the boundary did implicitly) until its reader/writer handling lands. **No behavior change**: tagged FRI stays unsupported, unknown-flag rejection still fires on the correct bit. ## Test - `cargo build -p lance-table` (const asserts pass) - `cargo test -p lance-table --lib feature_flags` -- 14 passed, incl. `test_frag_reuse_with_stable_row_ids_flag_is_reserved_not_supported` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
FRI keeps existing indices usable after fragment rewrites by translating old physical row addresses to new ones. Today it supports order-preserving compaction. This proposal extends the same system index to support stable partitioning, with both mapping types sharing one fragment-lineage history.
Following the unified FRI proposal, source/destination lineage stays in FRI details, while large mapping payloads remain in separate immutable files.
One history, multiple mappings
Continue storing FRI information in a single
__lance_frag_reusesystem-index entry. Keep the existingInlineContent/ExternalFileenvelope and the original field number for legacy versions. Add tagged transitions alongside them:Sources and destinations define the common rewrite graph. Each mapping defines how to translate row offsets. Legacy groups can be read as ordered-compaction transitions; mixed histories follow fragment lineage, not the order of records or dataset version numbers.
Lightweight metadata, external row maps
Ordered compaction retains its compact bitmap representation. Stable partition assigns each physical source row a nullable
uint16destination label, preserving source order within each destination. A null label means the row was deleted. A counts matrix lets readers reconstruct destination offsets without reading all preceding labels.Stable-partition metadata records
map_id,map_size_bytes, and optionalbase_id. The labels and counts are stored in_fri/<map_id>/stable_partition.lance. Mapping identity is independent of the FRI index UUID: updating the history rewrites its metadata, but does not rewrite existing row-map files. The history can be opened without loading labels; address translation reads the required blocks.Publication and compatibility
AppendFragmentReuseTransitionsexpresses a transition delta. Combined atomically with a fragment rewrite, it lets the commit apply the delta to the current history and publish destination fragments and their mappings together. The persisted FRI details remain a snapshot of that history.Scope and validation
This PR contains protobuf definitions, the corresponding format documentation, the proposed flag constant, and minimal compile adapters. It does not enable tagged-history reads or writes. Mapping implementations and reader integration follow in #9106 → #9064 → #9067 → #9068 → #9107.
Replaces #9065 as the standalone spec at the bottom of native stack #9137, based on main
31d78d170.cargo fmt --alland whitespace checks pass. Clippy and tests are blocked by dependency resolution: main requiresobject_store_opendal 0.60.1, while the crates.io index currently offers only up to 0.60.0.