Repository navigation
Conversation
c6a7e75 to
361a1b4
Compare
361a1b4 to
1b1bf31
Compare
1b1bf31 to
c56f4ca
Compare
Self-review pass (polish-pr), 2026-09-20Decision
The covered-index fence hunk that this PR carried in StatusSettled and implemented. fmt clean, Open questionsThe one worth a decision before this merges:
Smaller ones:
🤖 Generated with Claude Code |
Legacy conflict characterization run against this PR (burn-down item 14)Decision#9220's characterization suite passes unchanged on this PR: 42 passed, 0 failed, 6 ignored, ~2s. StatusVerified 2026-09-20. Nothing in the stack changes legacy conflict behaviour. Options and criteriaThe suite's branch ( Method: for each PR, a throwaway branch off the PR head, merge Results, all four layers:
#8644 is the layer that matters most here — it is the one that touches Open questions
🤖 Generated with Claude Code |
c56f4ca to
1589925
Compare
1589925 to
e376707
Compare
6ecaa35 to
b90947c
Compare
1090ca2 to
d8b20e5
Compare
b071969 to
77c2d88
Compare
|
Restacked onto the new #8644 head and added |
…e read version - After applying, segments under each touched index name must agree on resolved fields, covering fields and type, and cover disjoint fragments. - AddIndexSegment rejects an explicit dataset_version other than the read version: nothing checks for rewrites between an older build and the read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transaction layer accepts a segment keyed on a field the same commit mints, but no index builder can build over a column that is not committed yet, so the segment carries coverage only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the index actions AlterField has no type facet any more, so the build-vs-cast test becomes build-vs-drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ils payload to commute Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ootprint Matches the apply-time segment check and the legacy resolver's same_logical_index, so a details payload or index version difference no longer makes two disjoint builds of one index conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rejected Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unrecorded coverage stays reserved for the MemWAL and fragment-reuse system indices. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keep its coverage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t assembly A segment added after a rewrite in the same operation was built over the new data, so the rewrite no longer strips that fragment from it. Segments present when the rewrite runs are pruned as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nown bases rejected Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…claim helper State one reason dataset_version stays on the wire, document the remaining public fields, rename Footprint::build_index to claim_index_segment, and name the stale-segment test for what it asserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers widening onto a sibling's fragment, two segments in one operation, validation of only the indices an operation adds to or adjusts, a reset followed by a rebuilt index, unrecordable fragment ids, unrepresentable created_at values, and data_change for the index actions. A created_at above i64::MAX is expected to be rejected; today it wraps to a negative timestamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of wrapping it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…survives a stale retry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… out of one index Ported from the gatekeeper's reproducer. Both a concurrent and a sequential composite commit of a Dot segment beside an L2 segment must be rejected; today both commit and an L2 query returns the wrong neighbor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Segments under one name are unioned by the query path, so the final segment-set check and the footprint's index identity now compare a per-type query contract. For vector indices that is the metric, the presence of an HNSW layer and the quantization kind, read from VectorIndexDetails; independently trained models still mix. Details that cannot be decoded, or predate the compression field, must match byte for byte. Other index types have no contract yet. lance-table cannot use the generated VectorIndexDetails, which lives in lance-index, so it decodes a mirror of the fields it reads; an e2e test encodes with the generated type to keep the two in step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the FieldRef split: AddIndexSegment's keyed and carried field lists, and the index identity the footprint compares, take FieldRef and resolve to schema fields only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also moves the index-action tests onto committed ids that exist at the read version, which apply now requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A widening's footprint claimed only the segment and the fragments' existence, so a concurrent rewrite of an indexed column in a newly covered fragment went undetected. The action now carries the segment's keyed and carried fields, apply checks them against the segment, and the footprint requires their data in each committed fragment it adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DropIndex removes every segment of an index as they stand at apply, and claims the index with unstated coverage so it conflicts with any concurrent segment under the same name. Dropping an index segment by segment let a concurrently added segment resurrect it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A re-encode with data_change false moves a field's values without changing any, so an index segment built over them still describes them. Claim the moved data as a new Mode::Moves, which collides with writes the way a write does but not with a segment's requirement, matching main's #9705: a concurrent index build no longer conflicts with a DataReplacement whose data_change is false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd builds to survive removal The fragment reuse history is not an index on the action path, so the four index actions must reject `__lance_frag_reuse`. And removal isn't staleness: neither a compaction nor a whole-fragment deletion of a covered fragment conflicts with an index build, while an in-place write still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t builds survive removal The four index actions reject `__lance_frag_reuse`: the fragment reuse history is not an index on the action path, so an index action naming it could only splice in a snapshot. An index segment's requirement on the data it covers is now Mode::RequiresUnlessRemoved. It collides with an in-place write, as Requires does, and commutes with a re-encode, but a removal of the whole fragment no longer breaks it: removal is visible in the metadata, so the segment either translates through the reuse history or loses that coverage. Neither a compaction nor a whole-fragment deletion conflicts with an index build any more. Data written into a committed fragment still collides with its removal, and AdjustIndexCoverage still requires each fragment it adds to exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
The restack leaves the nested-field build-freshness finding unchanged. Keep the composable actions and make those dependencies ancestry-aware, or conservatively conflict data-changing rewrites of covered fragments; final segment-set consistency cannot establish freshness.
The declared-field checks narrow the author-accepted coverage-widening risk, but widening over committed fragments can still race nested-field rewrites and yield incorrect indexed answers. Widening onto fragments minted in the same operation avoids that race.
Please mark this PR with the breaking-change label.
Stacked on the action vocabulary PR. Implements the three index-management actions the Transaction V2 draft left unwritten —
AddIndexSegment,RemoveIndexSegment, andAdjustIndexCoverage— the lowering of the legacyCreateIndexoperation onto them, and the conflict rule that lets two writers extend one index at once. No format change: the wire shapes these need land in #7954.Supersedes #8630 and #8641, which are folded together here and whose heads were on a fork, so they could not be part of a GitHub stack.
The format has no first-class "index" separate from its segments — a logical index is the set of segments sharing a name — so the actions operate on segments. Creating an index and extending one are the same action; dropping an index is one removal per segment. Because a segment's fields, coverage, and base path are all
Refs, one commit can now append data and index what it just appended, which previously took two versions.Two writers can extend one index
Conflict detection previously treated a logical index as a single thing only one writer could touch: any two commits adding a segment to the same index collided. That was a faithful port of the legacy
CreateIndexrule, which conflicts on index name alone.But the query path unions the segments, so two writers indexing different fragments are both right and both should land. The name-level rule is replaced with a claim on the index — the same kind of pairwise footprint entry key assertions use. Two claims on one index conflict when they cover overlapping committed fragments, when either does not state what it covers (the system indices), or when they disagree about what the index is: its fields, its config, or its version. Fragments the operation mints itself are left out of the comparison, since they have no id yet and no concurrent writer can be covering one.
RemoveIndexSegmentandAdjustIndexCoveragecarry aname. The coverage adjustment needs it to say which index it is widening; both use it when applying, to reject an action whose segment turns out to belong to a different index than the one it was planned against.Example
Appending a fragment and covering it with a new index segment, in one atomic commit — the index names the fragment by the token it was minted under, since the fragment has no id until the commit lands:
Two writers each doing this against different fragments now both commit, and the index is the union of their segments.
Not included
AdjustIndexCoveragekeeps the shape the draft gave it, including the note that coverage representation is still an open design area. It rejects a segment that records no coverage rather than treating "unknown" as an empty set to add to.The remaining drafted actions (
AddOverlays,RefreshRowVersionMetadata,UpdateCompactedSsTables,AssertUniqueKeys) are still unimplemented and still rejected on load.Behavior differences from the legacy path
The
CreateIndexlowering is verified by building the same manifest both ways and asserting the resulting index metadata is identical, but two edges the legacy path silently tolerates are rejected here: removing a segment the manifest does not have, and adding one whose uuid an existing segment already uses. Either means the operation was planned against a different set of segments than it is landing on.