lance-format / lance-format/lance
Stable-row-id Update/Merge: action for per-row version metadata refresh
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 7.1k
- Forks
- 852
- Avg merge
- 3d 18h
- Merged PRs (30d)
- 272
Description
Parent PRD
Milestone: Action-based Transactions (UserOperation) — see milestone #11. Discussion: #5960. Design spike: #6448.
Background
#6454 (the final cutover) has a stated precondition: every operation has been migrated onto the action-level conflict resolver. That precondition is not met. update_to_actions and merge_to_add_fields (rust/lance/src/dataset/transaction/action.rs) return None — declining action translation — whenever manifest.uses_stable_row_ids() is true. actions_from_operation_with_manifest propagates that None, and ActionRebase::try_new surfaces it as Error::NotSupported.
Because commit_transaction routes every commit except a strict overwrite through the conflict resolver, cutting the production path over to ActionRebase and deleting TransactionRebase (as #6454 specifies) would make every Update/Merge commit on a stable-row-id dataset fail hard. Stable row IDs is a supported, increasingly-default feature — that is a severe regression and violates #6454's "no regression" acceptance criterion.
The root cause is a planning gap, not a design dead-end. #6843 ("representation of Update auxiliary fields") mapped the explicit fields of Operation::Update onto the action vocabulary. Per-row version metadata is not a field of Operation::Update — it is derived inside build_manifest from the stable-row-id counter and the current manifest version — so the field-by-field exercise never surfaced it, and #6448's §4 catalog was frozen at 19 actions without an action for it.
The gap
A stable-row-id Update/Merge refreshes per-row version metadata on fragments — created_at_version_meta and last_updated_at_version_meta — that no current Action reproduces. Three legacy code paths in build_manifest:
resolve_update_version_metadata(transaction.rs) — setscreated_at/last_updatedon anUpdate's new fragments;created_atis traced back from the rows' source fragments.refresh_row_latest_update_meta_for_partial_frag_rewrite_cols—UpdateRewriteColumns, partially rewritten fragments.refresh_row_latest_update_meta_for_full_frag_rewrite_cols—Merge, physically rewritten / brand-new fragments.
The index-coverage re-pointing the update_to_actions comment also names is not part of this gap: RebindIndexCoverage (action A14) already exists and is already used for stable-row-id Rewrite. Stable-row-id Update RewriteRows can reuse it; the only nuance is reconciling A14's paired remove+insert with the legacy register_pure_rewrite_rows_update_frags_in_indices (insert-only, gated on the index covering all original fragments).
Why it is expressible
Action::apply takes &Manifest, so it is manifest-aware by design, and everything the refresh needs is manifest-derivable at apply time:
- The new version number is
manifest.version + 1— the trickUpdateMergedGenerations::applyalready uses. created_attracing reads the source fragments' version metadata; those fragments are still in the manifest as long as the refresh action is ordered beforeRemoveFragments(the action list is ordered — spike §3.3).- The payload is effectively stateless ("refresh these fragments"), so
rebaseis a no-op and the action adds no conflict surface beyond fragments already claimed byAddFragments/RemoveFragments/UpdateDeletionVector.
What to build
- Add a new action —
RefreshRowVersionMetadata— refreshingcreated_at_version_meta/last_updated_at_version_metaon a set of target fragments. Payload carries the target fragments and, for the partial-RewriteColumns case, the touched row offsets. Decide one parameterized action vs. separate full/partial actions. - Implement its
apply(porting all three legacy refresh paths),validate,reads,writes, andrebase(a no-op). Guarantee apply ordering beforeRemoveFragmentsso thecreated_attrace can read source fragments. - The new action is in-memory only — no proto message.
Actions here are transient (conflict resolution during a commit); the action-transaction wire format is not yet decided — #6455 is a pending PMC vote on it and #6456 implements serialization behind a feature flag, both after this issue and the #6454 cutover. The new action joins serialization in #6456. - Make
update_to_actionsandmerge_to_add_fieldsstop bailing onuses_stable_row_ids(): emit the new action, and forUpdateRewriteRows additionally emitRebindIndexCoverage. Reconcile the A14 / legacy index-rebind semantics. - Extend the differential matrix with stable-row-id
Update/Mergegenerators (currently excluded — the matrix is "N×N" only over translatable shapes). Confirm the new resolver agrees with the legacy oracle. - Update the design doc: unfreeze §4 to 20 actions with the new catalog row (payload, read-set, write-set); rewrite the §5 "stable-row-id bail" paragraphs for
UpdateandMerge; resolve the §5/§11 contradiction (§5 documents a permanent legacy fallback, §11 says legacy is deleted at cutover); record therebase-correctness argument.
conflict_resolver.rs must not be modified — it remains the production path and the differential oracle until the #6454 cutover.
Out of scope: the file-level deletion-vector auto-merge divergence (design doc §5 divergence (1)) — orthogonal, non-fatal (retryable, eventually correct), and remains deferred.
Acceptance criteria
- New
RefreshRowVersionMetadataaction defined;applyreproduces all three legacy refresh paths -
apply/validate/reads/writes/rebaseimplemented;rebase-correctness argument recorded in the design doc -
update_to_actions/merge_to_add_fieldsno longer bail onuses_stable_row_ids() -
RebindIndexCoverage/ legacy index-rebind semantics reconciled for stable-row-idUpdateRewriteRows -
ActionRebase::try_newno longer returnsError::NotSupportedfor any stable-row-idUpdate/Merge - Differential matrix extended with stable-row-id
Update/Mergegenerators; green - Design doc §4/§5/§11 updated; §5/§11 contradiction resolved
- Existing stable-row-id update / merge tests still pass
-
conflict_resolver.rsunmodified -
cargo fmt --all;cargo clippy --all --tests --benches -- -D warningsclean
Blocked by
- #6898 (Update — action translation + conflict resolution)
- #6842 (DataReplacement —
ReplaceFragmentColumns+ conflict resolution)
Blocks
- #6454 (final cutover) — its "every operation migrated" precondition holds only once this lands.
User stories addressed
- User story 6: single code path for conflict resolution
- User story 9: old operations translated to actions
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in rust/lance/src/dataset/transaction/action.rs and transaction.rs, tracing the three legacy version-metadata refresh paths and the update_to_actions and merge_to_add_fields entry points. Then inspect the differential matrix and the design document sections 4, 5, and 11. Done means the new action and stable-row-id translations agree with the legacy oracle, tests and checks pass, and conflict_resolver.rs remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100