Conversation
A reordered rewrite (reclustering) distributes live rows of n source fragments across m destination fragments in scan order, so unlike compaction the destination of a row cannot be derived from row order. This adds the standalone format capability that records and replays that mapping, with no transaction or read-path integration yet: - lance-core/utils/stable_partition: pure translation arithmetic. CountsMatrix stores cumulative per-destination row counts at every 64K-row block boundary; a point lookup is counts base + label rank in one block, a sweep is counter[label]++ per row seeded from any block boundary. Encode/decode for the on-disk form plus content validation. - lance-index/frag_reuse/row_map: the row map file. One Lance file with a single nullable u16 label column (one row per physical source row, NULL = deleted at source) and the encoded counts in a global buffer, so open costs one tail read. RowMapWriter interleaves NULLs from the source deletion vectors while the caller streams live-row labels; RowMapReader offers point, coalesced-batch and sweep translation. - benches/stable_partition_row_map: encoded size and translation costs. 2M rows / 1000 destinations on V2_1: 11.15 bits/row uniform-random labels (worst case, nominal 10) and 6.27 bits/row with 16-destination block locality; sweep ~170M rows/s, full-block label rank 3.8us. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups on the stable-partition row map: - The counts header now carries a representation tag. Only the dense grid is written (exact, data-independent size: ~600KB for a 50M-row rewrite across 500 destinations, ~61MB at 1B rows across 1000 — trivial beside the rewrite either way); unknown tags are rejected with a clear error, so sparser encodings can be added later without breaking readers. - RowMapReader::open() now fails loudly on a bad file instead of translating rows to wrong addresses or panicking: it checks the label column's schema, decodes with exact structural checks (magic, version, supported representation, shape, precise payload length), runs the full counts consistency validation, and reconciles label row count against the counts. Batch column casts return errors instead of panicking. - Documented the stable-partition ordering contract the arithmetic rests on (labels in source physical-row order, destinations filled in that same order and never re-sorted, destination list fixed), mirroring the Ordering section of row_addr_remap.rs. - translate_many now subtracts block starts in u64 like translate. - Replaced three copies of a hand-rolled LCG with seeded StdRng; rand is already a workspace dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Deduplicate the ordering-contract paragraph in the module docs and drop a redundant explicit rustdoc link. - open() enforces the full schema contract it claims: exactly one column, named label, u16, nullable. - Document the remaining public writer/reader methods. - Deterministic NULL edge-case test: fully-deleted source, zero-row source, deleted tail drained by finish(), empty translate_many and sweep inputs. - State that sweep's one-block-at-a-time IO is intentional (bounded memory); prefetch belongs to read integration. - Fix the 50M x 500 counts size in docs (1.5 MB, not 600 KB) and allow the size-probe printlns in the bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: make explicit that the row map represents stable partitions only. A rewrite that sorts rows within a destination cannot be expressed by destination labels (equal labels would rank in source order, not output order) and would need a per-row final-offset encoding as a separate format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9f98168 to
4bf7c5f
Compare
The flag table row and the required-columns already state the contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
Restructure around the two use cases, specify version negotiation and shared transition metadata, define both mapping encodings with full translation algorithms and validation rules, and state the reader and writer compatibility contract.
is_tagged must never classify a version-0 history or a user index as tagged, and the version-0 writer must always produce index_version 0, including when carrying an existing entry forward. These pins keep the tagged gates provably inert for version-0 datasets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
The destination backtrack now produces both per-segment coverage and sibling exclusions in one pass (segment_plans), so "direct coverage wins" is owned by one algorithm. Exclusions come from the same direct map the backtrack builds: everything any group member covers directly, minus the segment's own provenance. This deliberately over-approximates per-path contention; fragments not on a segment's path are never checked per-hop, so extra members are inert. segment_coverage remains as a thin wrapper, keeping load_indices behavior unchanged, and load_indices now documents that its rewritten coverage bitmaps are snapshot-derived and must never be persisted back to a manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
# Conflicts: # protos/table.proto
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
Keep table-level system metadata outside fragment-coverage filtering, and reserve coverage remapping for queryable user-index segments. Also move the existing unknown-flag fixtures above the new supported bit. These two changes preserve MemWAL availability on tagged snapshots and keep the existing unsupported-feature guards meaningful.
| } | ||
| if index.name == lance_index::frag_reuse::FRAG_REUSE_INDEX_NAME { | ||
| result[position] = Some(index.clone()); | ||
| } else { |
There was a problem hiding this comment.
This branch also sends the __lance_mem_wal system entry through the fragment-coverage groups. MemWAL metadata intentionally has fragment_bitmap: None, so may_need_translation(None) is true and the entry is silently omitted at lines 60–63. Every MemWAL API built on load_index_by_name then sees an initialized dataset as uninitialized and refuses writers. Preserve the MemWAL system entry unchanged, or otherwise restrict coverage rewriting to queryable user-index segments.
Reproducer
I added this regression beside the existing FRI reader tests, using their fixture, prepare, and install helpers:
let mut dataset = fixture().await;
let mem_wal = lance_table::system_index::mem_wal::new_mem_wal_index_meta(
dataset.manifest.version,
Default::default(),
).unwrap();
dataset.apply_commit(
Transaction::new(
dataset.manifest.version,
Operation::CreateIndex {
new_indices: vec![mem_wal],
removed_indices: vec![],
},
None,
),
&Default::default(),
&Default::default(),
).await.unwrap();
assert!(dataset.load_index_by_name(
lance_table::system_index::mem_wal::MEM_WAL_INDEX_NAME,
).await.unwrap().is_some());
let (transition, destinations) = prepare(&dataset).await;
let content = InlineContent {
legacy_versions: vec![],
transitions: vec![transition],
}.encode_to_vec();
install(&mut dataset, content, destinations, false).await;
assert!(dataset.load_index_by_name(
lance_table::system_index::mem_wal::MEM_WAL_INDEX_NAME,
).await.unwrap().is_some(), "tagged FRI must not hide the MemWAL system index");Command: cargo test -p lance index::frag_reuse_reader::tests::tagged_history_keeps_mem_wal_system_index_visible -- --exact
The first lookup succeeded; the final assertion failed with tagged FRI must not hide the MemWAL system index.
| @@ -68,7 +68,8 @@ const _: () = assert!(FLAG_MIXED_DATA_FILE_VERSIONS == FLAG_UNKNOWN); | |||
| /// preserves them during maintenance. Legacy-only FRI does not set this bit. | |||
| pub const FLAG_FRAGMENT_REUSE_INDEX: u64 = 1 << 9; | |||
There was a problem hiding this comment.
FLAG_UNKNOWN << 1 is no longer an unsupported sentinel: it is exactly this newly supported bit (512). Three existing unsupported-writer fixtures still use that expression, so they now construct a one-sided FRI flag pair instead of an unknown feature. In both clone regressions, manifest setup returns CorruptFile before the tests can exercise the intended NotSupported clone boundary. Move all of those fixtures to a genuinely unsupported bit above the highest supported flag, such as FLAG_FRAGMENT_REUSE_INDEX << 1.
Reproducer
Command: cargo test -p lance clone_rejects_unsupported_writer_before -- --nocapture
Expected: both clone guard tests pass. Observed: both failed while writing the fixture manifest with FRI requires both reader and writer feature flags. The same stale sentinel also appears in rust/lance-namespace-impls/src/dir/manifest.rs.
Bit 9 is taken by the stable-row-id FRI compatibility flag (#9119). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016RynpAxtwGB9Q9CL4JCvR4
|
Adjudicated both points. System metadata in coverage filtering: confirmed, fixed in 433d8c4 on #9139 (this branch is frozen while the format vote runs, so the fix rides the stack tip). Unknown-flag fixtures: not applicable as stated. |
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](#8972 (comment)), 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: ```text 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. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What this adds
This PR adds the dataset-level reader for FRI index version 1. Datasets without FRI and datasets using version 0 continue through the existing legacy read path.
Ordered-compaction transitions use the existing bitmap/rank mapping. Stable-partition transitions bind their immutable Lance row-map file and load label blocks only when translation reaches that transition.
Coverage planning
Stored index bitmaps describe the fragments from which each index segment was built. The reader groups segments by logical index, combines their usable source coverage, and walks lineage to determine which current destination fragments are completely covered.
The planner receives an in-memory copy of each segment’s derived coverage; persisted index metadata is not changed. Empty coverage is removed so the query scans those fragments. Complete coverage for one destination is retained even when another destination cannot be covered.
When a newer segment directly covers an intermediate or destination fragment, older segments stop contributing along that path. The same exclusion is applied during address translation to avoid duplicate results when lineage branches reconverge.
Address translation
For each physical row ID, the reader follows the relevant mappings forward until the address reaches a current live fragment. Deleted rows return
None. Paths containing an unsupported mapping cannot claim derived coverage and fall back to scanning.Cache and compatibility boundaries
Decoded immutable histories are cached by FRI UUID. Mapping readers can be reused across history updates through their content fingerprint and storage binding, while live fragments remain specific to the dataset snapshot.
The first version-1 FRI commit sets and preserves the paired reader/writer feature flag. Manifest publication verifies that version-1 metadata and the flags agree. Writers and maintenance operations that cannot preserve version-1 history fail with an upgrade error. Restore preserves the selected snapshot’s index metadata and external references.
This PR does not modify scalar or vector index loaders. Until consumer integration in #9107, segments requiring version-1 address translation are excluded and their fragments are scanned.
Validation
Tests cover legacy-path isolation, inline and external history, multi-step and branching lineage, partial destination coverage, direct destination coverage, duplicate suppression, deleted rows, unsupported mappings and versions, cache reuse and snapshot isolation, feature-flag publication, restore and clone boundaries, and rejection of unsupported maintenance operations.
Validated with workspace Clippy and
cargo fmt --all.