Conversation
|
Adjudicated both rounds against the code; the pruning concerns were real and are fixed, the trim concerns were already structural. Pruning validated nothing before removing: fixed in 5db67d4. Removals were derived through the lenient reader path, which only logs on unsupported records, so an old client could prune a segment whose coverage flows through a transition it cannot interpret, and commit that removal before trim's refusal fired. The strict ledger check now sits inside Pruning race on rebase: confirmed and fixed in 867ae45. The removal-only shape fell through every re-derivation branch in Trim from a stale view: already handled. The trim is re-derived against the manifest actually being committed onto during every rebase ( |
867ae45 to
841ccf0
Compare
841ccf0 to
819d2c3
Compare
819d2c3 to
001f1d6
Compare
34edba1 to
36e8791
Compare
36e8791 to
b73c691
Compare
b73c691 to
3ed9d13
Compare
…9107) ## What this adds This PR connects scalar and vector index loading to the common version-1 FRI reader. It applies the coverage and translation plan produced for each index segment while keeping version-0 FRI on its existing synchronous path. ```text stored index segment │ ▼ snapshot FRI plan ├── Identity → use the existing plugin load path ├── Translate → load through asynchronous batch remapping └── No coverage → omit the segment and scan ``` An unaffected segment receives no remapper, so existing and external plugins continue using their normal loader. A segment that requires translation is retained only when its consumer supports batch row-ID remapping; otherwise its destination coverage falls back to scanning. ## Index consumers The shared batch-remapping path is used by: - B-tree, bitmap, zone map, Bloom filter, RTree, label-list, NGram, and inverted/FTS scalar indices. - Vector partition storage, including PQ-backed storage. Consumers translate physical row IDs in batches, preserve result order and deletion handling, and avoid building a partition-sized per-row lookup table. Vector loading preserves its existing partition and quantization layouts. Version-0 FRI continues to use the original synchronous remapper and plugin APIs. A separate version-1 identity state ensures an unaffected segment never receives a synthetic legacy remapper merely because the dataset contains version-1 history. ## Query planning and caching The version-1 plan is built once per dataset snapshot and cached by manifest identity. It records each segment’s derived coverage and the fragments owned by newer selected segments. Warm segment opens reuse this plan instead of recalculating whole-dataset coverage. Direct destination segments take precedence over older translated segments. When a logical index needs multiple source segments to cover a destination, all required contributors are selected; omitting one forces scan fallback for that destination. Fragment-restricted vector queries also scan when their selected segments do not provide complete coverage. Cached objects are keyed by what they depend on. Content decoded without translation (identity segments, version-0 histories, no history, the IVF state entry) lives in the plain per-index namespace and stays warm across commits. Content that embeds translated addresses (pages, postings, partitions loaded through a translating remapper) lives under a namespace named by the segment's translation identity: a hash of its live coverage, its sibling exclusions on its path's destinations, the fingerprints of the transitions on its path, and whether the ledger dropped unsupported records. Such an entry goes cold exactly when that state changes (a transition on the path trimmed or replaced, a fragment on the path dropped, a sibling taking direct ownership of a path destination) and survives appends and rewrites of fragments the segment never covered. The FRI entry's UUID is not part of the key: every rewrite and trim mints a new one. Staged segments. A segment built but not yet committed (the distributed build flow: `execute_uncommitted` per fragment, then merge, then commit) is unknown to the snapshot plan. `open_row_id_remapping_with_plan` / `open_scalar_index_with_plan` open one segment through a caller-supplied plan; the existing entry points are the `None` case. The cache namespace still follows the resolved remapping. The planner that builds such plans for a staged group, and the staged merge itself, land in #9161. Two boundaries. A legacy-layout full-text (inverted) segment, whose per-document lengths cannot be translated, is excluded from coverage on a tagged table and its fragments are scanned; a full-text query must then name its column, since a column-less query discovers columns from the usable segments. Segment-only selection (`with_index_segments` without a fragment list) answers from the selected segments only, by contract, where the pre-FRI path fell back to a flat search; pass the fragment list to get the coverage guard and the flat fallback. ## Validation End-to-end tests cover all supported scalar and vector consumers, multi-segment source coverage, two-source/two-destination rewrites, deleted rows, partial and filtered queries, direct destination precedence, reconverging lineage, unsupported plugins and index versions, scan fallback, cache reuse, prewarm, append/delete/checkout, and eager/deferred compaction. A version-0 lifecycle suite covers scans, filtered queries, writes, compaction, optimization, historical checkout, remapping, and cleanup while asserting that version-0 datasets never enter the new asynchronous path. Validated with workspace Clippy and `cargo fmt --all`. The python and java lockfiles carry the new `blake3` dependency (CI runs `--locked`); the staged-group planner that an earlier revision of this PR carried moved to #9161 with its consumers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
…d table Address the whole-stack review's merge finding, verified as a coverage regression rather than a correctness bug: merge already opens the selected segments through the translating loader, so the merged file holds live addresses, but the maintenance path still reasoned in terms of the stored bitmap intersected with live fragments. - Old-data filters and the "has old data" checks used stored ∩ live. On a tagged table a fully rewritten segment has an empty intersection, so its translated rows were filtered out of the merge and vector segments were classified dormant and skipped. Use the coverage the tagged reader derives for each segment instead: that is where the translated rows live. - The scalar merge committed stored ∩ live as the new bitmap, dropping the translated destination coverage so those fragments fell back to scan. On a tagged table a segment's bitmap is provenance, not a coverage claim: the merged segment now commits the union of the selected segments' stored bitmaps, retired sources included, as the vector merge already did. The reader stops translating at a live fragment, derives destination coverage whenever every contributor is present (so an unselected sibling keeps its share), and trim retains the mapping the provenance still names. A from-scratch rebuild still commits exactly the live fragments it scanned. Tests: the pinned "merge drain not yet supported" case becomes the supported lifecycle (merge, derived coverage of every live fragment, index used, trim, counts equal an index-disabled scan); an unselected sibling keeps its translated coverage; vector merge on a tagged table with newly appended rows; a legacy-format vector file on a tagged table is excluded and scans. Each fails without the change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…im honest
End-to-end leg of admitting mutations on tagged tables: after a stable
partition (i_idx provenance {0, 1} -> destinations {10, 11}) a delta
segment is built over an appended fragment, then a row of one destination
and every even value are deleted, which drops F10 entirely. The
translating segment is kept next to its delta sibling (its stored bitmap
is empty against the live fragments by construction; without the tagged
retention rule it is dropped), every indexed query equals the index-disabled
scan, trim is a no-op while the segment still translates, and draining the
segment afterwards trims the entry as usual.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… translation plan The distributed build flow (`execute_uncommitted` per fragment, then `merge_existing_index_segments`, then `commit_existing_index_segments`) failed on a tagged table, in three places: every staged open went through the snapshot plan, which only lists committed segments; the tagged coverage and filter builders read "not registered" as "covers nothing" and would have filtered every row of a staged segment out of the merge; and the merged coverage went through the v0 handle, which a tagged table does not have, so it shrank to nothing. The merge now plans the staged set as one group with the reader's own algorithm: `plan_staged_segments` runs the steps `fri_query_plan` runs per committed group (`may_need_translation`, `segment_plans`, `translation_fingerprint`), so a staged segment translates exactly as it would once committed, with its stored bitmap as provenance and the other staged segments as its siblings. That plan is threaded through every scalar `merge_segments`, the per-segment filters and the opens (the `*_with_plan` entry points from the consumers PR). `tagged_segment_coverage` takes the plan: a staged segment's coverage comes from it, and a segment that is neither staged nor listed in the manifest is an error, never empty coverage. Coverage follows the destination model. The per-type merges already emit the effective union in the live domain, and a provenance bitmap left on a merged segment is pruned to nothing at commit, so `remap_merged_segment_coverage` writes each staged segment's planned live coverage into its bitmap before the merge and reports the coverage as remapped. A group that covers no rewrite destination completely claims nothing: those rows stay on the scan path and a warning says so. The v0 handle is never consulted on a tagged table. The NGram merge reads spill files and translates only through the v0 remapper, so a staged NGram segment whose plan translates is refused (NotSupported) instead of being merged with stale addresses; the FM merge rebuilds over the derived coverage. Tests (`index/create/tests/staged_tagged.rs`): btree and bitmap staged segments merged and committed across a stable partition claim exactly the destinations the group covers and answer point queries like the scan; a lone contributor over half a partition claims nothing and scans; segments over fragments no rewrite touched merge as identity; the #9421 deferred compaction flow on a tagged table; and the NGram refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mits
`build_index_metadata_from_segments` prunes from a new segment every
fragment the current manifest no longer lists. Under a tagged history that
is exactly a translating segment's provenance: a segment built before a
stable partition names the retired sources, and its rows translate to the
destinations when it is opened. Pruning them left a committed staged vector
merge (which keeps the provenance union; the vector merge never opens the
dataset) with an empty bitmap that the reader then excluded. The prune now
keeps a retired fragment that is on the tagged entry's lineage; a retired
fragment off the lineage (a bare rewrite of uncovered data, a whole-fragment
delete) holds nothing the segment can reach and is pruned as before.
Tests: staged IVF_FLAT shards (shared centroids) merged and committed
across a stable partition keep {0, 1}, translate to both destinations, and
answer k = 1 and k = 8 like the flat scan, before and after trim
(`staged_tagged.rs`); a user index built before a stable partition and
committed after it lands translating, a partial-coverage or NGram segment
retries, an index off the rewritten fragments lands directly
(`index_over_committed_sp.rs`, exercising the resolver rule from the commit
path PR, which admits CreateIndex on tagged tables only from this PR on);
RewriteColumns on a column with a direct post-rewrite index prunes that
index's bitmap as on an untagged table (`tagged_mutations.rs`).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A staged segment was built at its own dataset version. On a tagged table every commit since then may have moved its rows (a rewrite recording a transition: the provenance stays and translates) or rewritten an indexed column in place (the values it holds are stale: that transition's coverage must be withdrawn). The merge used to translate addresses and project coverage without knowing either, and stamped the result with the current version, so a rewrite between the build and the merge left a merged segment claiming full coverage with pre-rewrite values inside. `replay_staged_segments` validates the segments up to the snapshot being merged against with the conflict resolver's own rules: a CreateIndex of the segments at their oldest build version, checked against every transaction up to the snapshot (a transition keeps the provenance; an in-place rewrite withdraws the transition's sources; a bare compaction of a covered fragment or a partly covered group means the segment must be rebuilt, as does a version in the window that can no longer be read). `merge_existing_index_ segments` replays first, then plans and merges only what is still valid, and stamps the result with the validated snapshot. `commit_existing_index_ segments` reads from the oldest validated version (the build for unmerged segments, the snapshot for merged ones), so the commit loop carries the segments across anything newer through the same rules. The cost is proportional to the commits crossed. Tables without a tagged history keep their existing path. The commit-time validator this replaces is gone; the coverage prune keeps a retired fragment that the lineage records, and nothing else. The replay needs the whole history between the build and the snapshot: the transaction listing only knows the manifests that still exist, so a version cleaned up in between (the build kept by a tag, the versions after it removed) would leave no trace and its column rewrite would go unreplayed. The versions listed must be exactly build+1 through the snapshot, else the rebuild error. Segments are replayed per build version, so a segment built after a rewrite is not withdrawn for it again. Rebuilding a withdrawn segment: the tagged reader leaves a segment that derives no coverage out of its listing, so `optimize_indices` could not open it by uuid, skipped the column, and the withdrawn coverage was never rebuilt. Maintenance now opens through `open_*_index_for_maintenance` (`OpenPurpose::Maintenance`): the manifest is consulted for a registered segment the listing omits, and the remapper opens it as contributing nothing (an empty translation), so the optimizer replaces it with a rebuild over the live fragments. The query entries (`open_scalar_index`, `open_vector_index`, `open_generic_index` by uuid) still see the listing only and refuse such a segment: no scan was scheduled for its rows, so an empty answer would drop them. Tests: segments staged over F0 and F1, `i` (or the vector) of a row in F1 rewritten in place, F1 partitioned, merged and committed: the merged segment keeps F0 only and the new value comes from the scan (BTree, Bitmap and IVF_FLAT); a staged segment committed after a destination's column was rewritten claims nothing; the same with two partitions around the rewrite; a bare rewrite of a staged source says rebuild; a cast of the indexed column refuses the stale segment; a user index built before a partition and committed after a concurrent in-place rewrite lands with the transition withdrawn and `optimize_indices` rebuilds it; a merged segment with mixed provenance loses the direct id and the sources alike, is refused by the query entries by uuid and rebuilt by `optimize_indices`; staged segments whose history lost a version to cleanup are refused by merge and commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hrough every optimization Four maintenance paths on a tagged table left indexed rows on the scan path or refused work they should have done. A vector segment whose coverage an in-place rewrite withdrew entirely could not be rebuilt: its empty bitmap made it look like a born-empty definition (mergeable), and the optimizer then opened the logical index through the query listing, which excludes such a segment, so `optimize_indices` failed with "index not found". A segment with index files and no live coverage is dormant now, whatever its bitmap says (a definition without files is still the deferred build it always was), and the optimizer opens the logical index through the maintenance entry (`open_logical_vector_index_for_maintenance`, listed metadata for listed segments, stored metadata for excluded ones), so the dormant segment is replaced by a rebuild over the live fragments, its parameters read from its metadata. The default optimize's steady-state rebalance published `stored ∩ live` for the rebalanced vector segment, empty for retired provenance, and the inverted append/merge kept only `stored ∩ live` of the selected segments as old data and coverage: on a tagged table both turned every indexed row of a partition's destinations into a scan. The rebalanced file and the merged inverted segment are read through the translating loader, so both publish the stored provenance, and the inverted merge keeps as old data the coverage the reader derives for each segment, exactly as the scalar merge already did. Under a history this build cannot interpret the reader lists no user segment, so every fragment looked unindexed and every `optimize_indices` rebuilt the whole table only to have the result excluded again. Optimize now leaves such a table alone with a warning, as trim, superseded pruning and remap already refuse it. The staged-segment replay carried the segments as a CreateIndex through the resolver's full rules, so a same-name CreateIndex in the window (routine `optimize_indices` adding a delta to the logical index a distributed rebuild is replacing) was a conflict and the segments were refused for the whole build window. In replay mode (`TransactionRebase::for_staged_replay`) a same-name CreateIndex is no conflict unless it changed what the index is (its fields or type); the overlap rules settle the rest at commit time. Ordinary concurrent commits are unchanged. Tests: a withdrawn IVF_FLAT index rebuilt by optimize (new segment claims the live fragments, ANN plan, results equal the flat scan) and the scalar counterpart (the new segment holds the rewritten row); the rebalance and the inverted append/merge keeping the translated coverage; optimize as a no-op under an unsupported history (no commit, no segment written, twice); a distributed rebuild merged and committed across a same-name delta in the window, and refused across a same-name replacement of another type. Each fails without its fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… act on it A registered segment the reader lists nothing for used to be one case, "missing coverage", and maintenance could only guess at it. There are four reasons and they want different treatment. The snapshot plan now records which one holds (`SegmentRemappingPlan::MissingCoverage(reason)`): - withdrawn: the stored bitmap is empty (every covered fragment withdrawn by an in-place rewrite, or a deferred definition). Before this an empty bitmap was planned as identity, since it needs no translation, so a merge that selected the segment read its pages raw, stale addresses included; - no derived coverage: the bitmap is not empty but the backtrack derives nothing (newer siblings own its direct coverage, or a destination lacks contributing sources); - unsupported: this build cannot translate it (no batch remapper for its type, no stored bitmap, or a history with transitions it cannot read); - corrupt: its details are missing or cannot be interpreted. A query refuses every kind by uuid, as before: the listing excludes the segment and no scan was scheduled for its rows. Maintenance reads a withdrawn or superseded segment as an empty translation (it contributes nothing and is replaced), skips one this build cannot translate with a `NotSupported` error the optimizer already turns into a skip, and fails on corrupt metadata. The reader's batch-remapping check moved into `segment_supports_batch_remapping` so the plan and the listing agree. Tests: one table with all four kinds on a tagged snapshot (a superseded translating segment, an emptied copy, one without a bitmap, one without details): every query open refuses, maintenance opens the first two as empty translations, skips the third and fails on the fourth. Fails without the classification (an emptied segment opens as identity; unknown coverage would be merged as empty). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
The two remaining optimizer coverage regressions are fixed: vector rebalance retains source provenance, and Inverted merge uses the reader-derived tagged coverage. The focused regressions and original Inverted merge probe now pass.
I also reproduced a separate partial-withdrawal ANN mismatch in the tagged write-path prerequisite: after an indexed-column rewrite, a surviving segment can expose retired rows to search, even after default optimization. Its withdrawal, identity-planning, and query-prefilter paths are unchanged by this maintenance patch, so I am not requesting a change here. This prerequisite risk needs attention before the stacked feature is rolled out.
Please mark this PR with the breaking-change label.
Maintenance for tagged fragment reuse histories: trimming drained records, pruning superseded index segments, garbage-collecting unreferenced row maps, and remapping indices through compaction-only chains. Legacy (version 0) FRI maintenance keeps its existing path untouched.
Trim
cleanup_frag_reuse_indexon a tagged table derives which transitions are still needed and splices the survivors byte-verbatim (no re-encode of retained records). A transition is retained while some index still translates through it: its provenance intersects the index's stored coverage and at least one destination is not directly covered, plus the downstream producer-to-consumer closure so chains never break mid-path. A fully drained entry is deleted outright. The trim intent is re-derived against the current manifest on every commit attempt; if a rebase leaves nothing to trim, the operation aborts instead of committing an empty version.Superseded segment pruning
Before trimming, cleanup removes index segments whose entire valid coverage (stored bitmap plus reader-derived translated coverage) is directly covered by kept newer siblings, newest-first to a fixed point, as a separate removal-only commit. Only segments the reader considers usable count as covering; an unusable segment can neither justify pruning a sibling nor block a trim. The removal set is re-derived against the current manifest on every commit attempt, including commit-path rebases: prune transactions carry an explicit property, the conflict resolver replaces a rebased removal set wholesale, and a rebase that leaves nothing to remove aborts instead of committing (so a concurrent replacement that narrows a justifying sibling's coverage can never make a stale removal land). Pruning also refuses histories it cannot fully interpret, same as trim.
Row-map garbage collection
_fri/<map_id>/directories follow the same three states as_indices: referenced by a retained manifest, unreferenced but young, unreferenced and aged out (deleted). References are collected from every retained manifest version's FRI entry. If any history record cannot be interpreted (unknown envelope records included), row-map GC disables itself for the run rather than guessing.Index remapping
Indices whose provenance runs through ordered-compaction-only chains are remapped and swapped (each hop requires the transition's sources to be within the segment's current coverage); any chain touching a stable partition is skipped with a log line, since translation stays available at query time. Two correctness details: the published coverage is intersected with what the remapped file actually contains (the bitmap-index remap path excludes rows already ceded to direct siblings), and the metadata-only "keep" outcome preserves the original
base_idso indices inherited through a shallow clone keep pointing at their real files.Unknown-record consistency
Envelope-level unknown records flag the decoded ledger as unsupported, giving every consumer the same conservative answer: GC disables, trim, remap, and superseded-segment pruning all refuse, appends onto such histories are rejected, reads are unaffected. The pruning refusal sits inside the per-attempt derivation, so no removal commit can be built from a history the client cannot fully interpret.
Merging segments on a tagged table
optimize_indicesmerge opens the source segments through the translating loader, so the merged files hold live addresses. The merged segment claims the union of the merged segments' stored provenance plus the newly indexed fragments (provenance, not the derived live coverage), so translated coverage is kept, an unselected sibling's coverage is untouched, and the reader keeps translating the merged segment while its provenance runs through retained transitions. Vector merges classify dormant and live segments on the same derived coverage. Draining also works through rebuilds and delta builds.Staged segments (
merge_existing_index_segmentsover segments built withexecute_uncommitted) are not in the manifest, so the merge plans them as one group with the reader's own steps (plan_staged_segments:may_need_translation,segment_plans,translation_fingerprint, the same ones the snapshot plan runs per committed group) and threads that plan through every scalar merge, the per-segment filters and the opens (the*_with_planloaders from #9107). A segment that is neither staged nor listed is an error, never empty coverage. Here coverage follows the destination model: each staged segment's bitmap becomes the live coverage its plan derives before the merge runs, because the per-type merges emit the live-domain union and a provenance bitmap would be pruned to nothing at commit. A group that covers no rewrite destination completely claims nothing and those rows stay on the scan path. The NGram merge translates only through the version-0 remapper, so a staged NGram segment whose plan translates is refused rather than merged with stale addresses; the FM merge rebuilds over the derived coverage. The version-0 handle is never consulted on a tagged table. Staged vector shards never open the dataset: the merge filters each by its stored bitmap in the raw address domain and the merged segment keeps the provenance union, so it translates at query time like a committed vector merge (pinned by an IVF_FLAT case across a stable partition). For that to survive the commit,commit_existing_index_segmentskeeps a retired fragment that the tagged entry's lineage records in a new segment's bitmap; a retired fragment off the lineage is still pruned. Translating is not enough on its own, though: the values a segment holds are the ones it read at its own dataset version, and every commit since then may have moved its rows (a recorded transition: the provenance stays and translates) or rewritten an indexed column in place (the values are stale: that transition's coverage must go). Soreplay_staged_segmentsvalidates the staged segments up to the snapshot being merged against with the conflict resolver's own rules, as a CreateIndex built at the oldest build version and rebased across every transaction up to that snapshot: a transition keeps the provenance, an in-place rewrite withdraws the transition's sources (the withdrawal from #9139), and a bare compaction of a covered fragment, a partly covered rewrite group or a version in the window that can no longer be read is an error pointing at a rebuild. The merge runs after the replay, over what is still valid, and stamps the merged segment with the validated snapshot; the commit reads from the oldest validated version, so the commit loop carries the segments across anything newer through the same rules. The cost is proportional to the commits crossed between the build and the merge, and segments are replayed per build version, so a segment built after a rewrite is not withdrawn for it again. The history has to be complete: the transaction listing only knows the manifests that still exist, so the versions listed must be exactly build+1 through the snapshot, else the segments are refused with the rebuild error (a build version kept by a tag with the versions after it cleaned up would otherwise skip the rewrite in between). An in-place rewrite of an indexed column on a stable-partitioned table costs the whole transition's coverage for every segment still translating through it, so remap (#9187) after a stable partition before rewriting columns in place;optimize_indicesrestores what was withdrawn: maintenance opens throughopen_*_index_for_maintenance, which reaches a registered segment the listing omits and reads it as contributing nothing, so a rebuild over the live fragments replaces it. The query entries (open_scalar_index,open_vector_index,open_generic_indexby uuid) see the listing only and refuse such a segment, since no scan was scheduled for its rows and an empty answer would drop them. The snapshot plan records why the reader derives no coverage for a registered segment (withdrawn, no derived coverage because superseded or missing contributors, unsupported by this build, corrupt metadata): maintenance reads the first two as empty translations and replaces them, skips the third, fails on the fourth; an empty bitmap is never planned as identity, so a merge never reads the withdrawn pages raw. The last segment of a name emptied by a withdrawal stays as the record of what to build: queries scan,optimize_indicesreads its parameters through the maintenance entry and rebuilds it from the live fragments (scalar and vector; a vector segment with files and no live coverage is dormant, a definition without files is still trained from its definition), and it takes part in neither remap nor merge as a normal segment. Under a history this build cannot interpret optimize is a no-op with a warning, like trim, superseded pruning and remap. The default optimize's steady-state vector rebalance and the inverted append/merge publish the stored provenance and read old data through the reader's derived coverage, exactly as the scalar merge does, so neither turns indexed rows of a partition's destinations into scans. In the staged replay a same-name CreateIndex in the window (routineoptimize_indiceson the index a distributed rebuild is replacing) is no conflict unless it changed the index's fields or type; the overlap rules settle it at commit. Staged NGram segments cannot be carried across a stable partition (the commit-time NGram check and the merge both refuse a translating NGram segment): rebuild them.remap_column_indexremaps the first segment of the name it is given, one segment per call. A user index built before a stable partition and committed after it lands the same way, translating, through the resolver rule in #9139, and one committed after a concurrent in-place rewrite lands with that transition withdrawn; the cases live here because this PR is where a user CreateIndex is admitted on a tagged table. The staged plan looks only at the staged group, not at committed same-name siblings: if a committed delta already covers a destination directly, the staged group still claims it and the commit displaces that delta, so the delta's other fragments fall back to scan. That is a coverage regression, not a wrong result.Validation
On the current #9139 head:
lance-table485,lancelib whole suite green (staged-merge tests inindex/create/tests/staged_tagged.rs, each proven to fail without its fix); fmt and clippy clean; python and java bindings compile. Lifecycle tests cover partial and full draining, retained-version file ownership, trim retries, and trim x trim conflicts.Stack: 8 of 11, follows #9139 (synchronized), #9162 builds on this.
🤖 Generated with Claude Code