fix(branch): name every branch life by an incarnation-suffixed native ref - #579
Merged
Conversation
… ref Closes #562. A deleted and recreated branch reused the same storage paths, so a warm handle's path-keyed session cache served the dead life's metadata to the new one (`all columns in a record batch must have the same length`, or stale rows), and a late-settling delete of the old fork could land on the new life's bytes. Every branch life now owns a native Lance ref named `{logical}.{ULID}`, minted at `branch_create`. The Lance ref list of `__manifest` stays the only branch registry: resolving a logical name lists the refs and takes the single live incarnation (none is `BranchNotFound`; more than one is a typed conflict). Table forks copy the manifest branch's native name; write paths, first-touch forks, EnsureIndices, merge target naming, sidecar table pins, and the orphan reconciler address forks by native name, while gates, lineage, policy scopes, and sidecar `branch` stay logical. Bare refs remain legacy lives with no migration. The public name stays the only identity: an incarnation-shaped path segment is refused on every entry point, reads included. A warm handle whose native ref vanished re-resolves through the listing; change-feed and Blob live-branch reads map a vanished named fork to their existing incarnation refusal. Branch delete acknowledges the manifest publication; the dead life's forks are reclaimed by `cleanup`. Tests: branching, maintenance, failpoints, and surface-guard owners extended; recovery, merge, change-feed, and cost fixtures converted to address forks by native name; the DST reborn-branch cache-poison repro (seed 9401 standalone, seed 10133 wide face) now runs as regression pins; DST cost golden regenerated for the one ref listing per branch-bound open. Docs: RFC 0042, Lance fence table, write/recovery guides, user branching page, release note.
A recheck that failed for any reason mapped to the incarnation refusal, hiding transient, permission, and dataset-open errors behind a branch lifecycle conclusion. Change-feed and Blob live-branch reads now prove the fork's absence from the table's ref listing and let every other failure keep its own class.
aaltshuler
force-pushed
the
feat/branch-incarnation-paths
branch
from
August 30, 2026 19:50
eedf1dd to
146d919
Compare
The rebase onto main's stricter gateway/chokepoint registries left TableStore::named_fork_is_absent and the Blob recheck's read-only .dataset() access unregistered.
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #562. Deleting a branch and recreating it under the same name reused the same storage paths, so a warm handle's path-keyed session cache served the dead life's metadata to the new one (
all columns in a record batch must have the same length, or stale rows), and a late-settling delete of the old fork could land on the new life's bytes.Every branch life now owns a native Lance ref named
{logical}.{ULID}, minted atbranch_create. A recreated branch therefore never shares atree/path, a table fork, or a cache entry with its predecessor — the same trick already used for table incarnations. The Lance ref list of__manifeststays the only branch registry: no manifest rows, no CAS, no forward recovery. Resolving a logical name lists the refs and takes the single live incarnation; none isBranchNotFound, more than one is a typed conflict that names both.branchfield stay logical..+ 26 Crockford characters is refused on every entry point, reads included, so native refs are never addressable or visible to Cedar. Ordinary dotted names (release.1.2) stay legal; the ancestor/descendant rule on logical names is unchanged.BranchNotFound). Change-feed and Blob live-branch reads map a vanished named fork to their existing incarnation refusal; a surviving fork with a reclaimed version stays a retention gap.branch deleteacknowledges the manifest publication; the dead life's forks are reclaimed bycleanup. A delete that crashed between ref removal and tree removal leaves an inert tree on a dead path (follow-up: sweeper).Alternative considered and rejected on liability: a branch-registry row in the root
__manifest(#578). Comparison in the RFC's Alternatives section.Backing issue / RFC
Checklist
lance_surface_guardsowners extended; recovery, merge, change-feed, and cost fixtures converted to address forks by native name; the DST reborn-branch cache-poison repro (seed 9401 standalone + seed 10133 wide face) now runs as regression pins; DST cost golden regeneratedwrites.md,recovery.md), Lance fence table, RFC registrydocs/dev/invariants.md— no hard invariant weakened, no deny-list item hit; invariant 11 holds (a branch-bound open lists the live refs: cost scales with branch count, not history)Local verification
--features omnigraph-engine/failpoints,omnigraph-cluster/failpoints): green except the pre-existinglegacy_name_is_confined_to_alias_and_compatibility_test, which trips on a stale.worktrees/blob-align-currentcopy on this machineomnigraph-dstcrate: 76/76 (cost golden regenerated: +1…+7l.list/l.getper op class, the one ref listing per branch-bound open)cargo fmt --all --check,check-docs.py,check-agents-md.sh,check-workflow-action-pins.py: cleanNotes for reviewers
_refs/branches/once (LIST + one GET per live branch). Branch delete resolves every surviving branch from that one listing, so its dependency probe stays at one open per branch;branch_control_costandwarm_read_costcontracts hold unchanged.native_dataset_branchin dataset entries (HTTP snapshot responses) now carries the suffixed internal name; it was already a physical detail field.graph_native_ref/native_ref_for/assert_native_branch_ofresolve logical names for fixtures that forge or inspect per-table Lance state;helpers::open_dataset_headresolves transparently.Greptile Summary
This PR assigns each branch lifetime an incarnation-suffixed native Lance ref while retaining logical names at public and policy boundaries.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR L[Logical branch name] --> R[Resolve manifest ref registry] R -->|Exactly one live incarnation| N[Native ref: logical.ULID] R -->|No incarnation| M[BranchNotFound] R -->|Multiple incarnations| C[Manifest conflict] N --> T[Incarnation-scoped table forks and storage paths] T --> B[Reads, writes, merge, cleanup, Blob and change feed]Reviews (3): Last reviewed commit: "test(forbidden): register the vanished-f..." | Re-trigger Greptile