Skip to content

feat(format): define a unified tagged fragment reuse history - #9136

Merged
LuQQiu merged 12 commits into
mainfrom
lu/fri-spec-first
Sep 15, 2026
Merged

LuQQiu merged 12 commits into
mainfrom
lu/fri-spec-first

Conversation

@LuQQiu

@LuQQiu LuQQiu commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

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_reuse system-index entry. Keep the existing InlineContent / ExternalFile envelope and the original field number for legacy versions. Add tagged transitions alongside them:

FragmentReuseIndexDetails
└── InlineContent, stored inline or in external details.binpb
    ├── legacy_versions[]
    └── transitions[]
        ├── ordered sources[]
        ├── ordered destinations[]
        └── mapping
            ├── OrderedCompaction: surviving-row bitmap
            └── StablePartition: immutable row-map reference

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 uint16 destination 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 optional base_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

AppendFragmentReuseTransitions expresses 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.

  • Index version 0: existing compaction format and read/write behavior remain unchanged.
  • Index version 1: supports legacy groups and tagged transitions in one history.
  • The first commit publishing index version 1 sets reader and writer flag 512. The reader flag prevents old clients from partially interpreting the history; the writer flag prevents them from dropping mappings during metadata maintenance. Subsequent manifests retain both bits.

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 --all and whitespace checks pass. Clippy and tests are blocked by dependency resolution: main requires object_store_opendal 0.60.1, while the crates.io index currently offers only up to 0.60.0.

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Important

Format specification vote

This 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

Approvals (this commit) @Xuanwo, @BubbleCal, @jackye1995 (3/3)
Vetoes none
Voting period elapsed — ended Tue 2026-09-15 18:18 UTC (11:18 PDT)

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 format-waived label to waive the vote for a trivial edit (typo, wording, formatting).

lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
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
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
LuQQiu and others added 3 commits September 10, 2026 15:19
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
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 10, 2026
Comment thread protos/transaction.proto Outdated
}

// TODO: compose with Rewrite atomically when Transaction V2 supports composite operations.
message AppendFragmentReuseTransitions {

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 11, 2026

@lance-gatekeeper lance-gatekeeper 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.

✅ 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.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 11, 2026
@Xuanwo

Xuanwo commented Sep 11, 2026

Copy link
Copy Markdown
Member

+1 binding from me

@BubbleCal BubbleCal 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.

+1

…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
@LuQQiu

LuQQiu commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Synced with main to clear the merge conflict (5410084). The conflict was the flag table in versioning.md: main landed FLAG_MIXED_DATA_FILE_VERSIONS on bit 256, which this PR's text had listed as reserved. Resolution keeps both rows (256 from main, 1024 from this PR), reserves only bit 512 going forward, and keeps main's paired-bits sentence. No FRI spec content changed; feature_flags.rs and table.proto merged mechanically, and this build still does not claim the 1024 capability (support lands in the follow-up PRs, matching the spec-first posture).

@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.

@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 14, 2026
…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

@BubbleCal BubbleCal 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.

+1

@jackye1995 jackye1995 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.

great work!!!

@LuQQiu
LuQQiu merged commit 84810db into main Sep 15, 2026
49 of 50 checks passed
@LuQQiu
LuQQiu deleted the lu/fri-spec-first branch September 15, 2026 18:46
LuQQiu added a commit that referenced this pull request Sep 18, 2026
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
LuQQiu added a commit that referenced this pull request Sep 18, 2026
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
LuQQiu added a commit that referenced this pull request Sep 18, 2026
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
LuQQiu added a commit that referenced this pull request Sep 18, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-format On-disk format: protos and format spec docs enhancement New feature or request format-change A change to the format spec, which requires a vote. Remove if minor (e.g. fixing typo).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants