Skip to content

feat(transaction): action-based transaction wire format (Transaction V2) - #7954

Draft
wjones127 wants to merge 15 commits into
mainfrom
will/oss-1530-draft-action-based-transaction
Draft

wjones127 wants to merge 15 commits into
mainfrom
will/oss-1530-draft-action-based-transaction

Conversation

@wjones127

@wjones127 wjones127 commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Defines the wire format for action-based transactions (Transaction V2): a CompositeOperation that replaces the single legacy Operation with an ordered list of granular, composable Action deltas. This is the format change only — read-side fail-closed, no write path. Every protobuf change the implementation stack needs lands here, so the format is a single vote; the PRs stacked on top of it change no .proto file.

Layout

The V2 vocabulary lives in its own files under protos/transaction/:

  • actions.proto — the V2 vocabulary (Ref, CompositeOperation, UserAction, Action + the action messages), with the design rationale summarized in an in-tree file header so it stands on its own.
  • common.proto — shared building blocks (UpdateMap, KeyExistenceFilter, …) referenced by both legacy operations and V2 actions, promoted from nested Transaction messages to top-level to avoid a circular import. Wire-compatible: field numbers unchanged and none are Any-packed.
  • transaction.proto — moved into the directory; keeps the Transaction envelope, legacy operations, and the new composite_operation oneof arm (field 116).

What's in the wire format

CompositeOperation { uuid, read_version, [UserAction] }              
UserAction    { description, [Action] }        // human-readable step; survives squash
Action        { oneof of 19 actions }
Ref           { committed: uint64 | local: uint32 }   // one ref type for field/fragment/base ids

Design principles (fully documented in actions.proto):

  • Deltas, not post-images — actions record the change (AddDataFile, TombstoneFieldData), so compound commits and branch merge fall out uniformly.
  • Minting vs. reference-stable — minted ids (field/fragment/base) carry a Local token via a single Ref and relocate on merge/rebase; reference-stable changes key off stable coordinates and keep a post-image with a derivable delta.
  • Field-level schema — AddField / DropField / identity-preserving AlterField (per-facet, so a concurrent cast and nullability change on the same field commute). No wholesale SetSchema.
  • Index segments — AddIndexSegment / RemoveIndexSegment / AdjustIndexCoverage (a logical index is the segments sharing a name; per-segment config duplication is a pre-existing limitation this draft doesn't fix).
  • Assertions — AssertUniqueKeys carries merge-insert's non-derivable key-existence filter as an explicit precondition.
  • Off the wire — computed conflict footprints and large derivable row-level deltas (deletion affected-rows, update matched-offsets) are recomputed at conflict time, not serialized.
  • Additive-safe — message identities and core mutation fields are pinned; discoveries land as added optional fields.

data_change markers (Delta-style, for CDC skip of compaction) and index-coverage representation carry in-tree TODOs to finalize.

Settled by the implementation

These started out in the PRs stacked above and were pulled down here so the format is voted on once:

  • UserOperation becomes CompositeOperation, naming what distinguishes it from the other operations, and loses its description — the per-step UserAction.description is what keeps history readable. UserAction keeps its name: it is the grouping a user recognizes.
  • The field actions reference fields by Ref, so an action can name a field the same operation is minting.
  • AddIndexSegment gains base, created_at, and dataset_version, and its covered_fragments becomes an optional wrapper rather than a bare repeated. A bare repeated field cannot distinguish "no coverage recorded" — what the MemWAL and fragment-reuse indices carry, and what the query path treats as "serve this segment" — from "covers no fragment", which it treats as "skip it". dataset_version is a correctness gate rather than provenance: an overlay committed at or before it counts as already folded into the index.
  • RemoveIndexSegment and AdjustIndexCoverage carry the name of the logical index the segment belongs to, which is what conflict detection compares.
  • The update_compacted_ss_tables oneof arm is named to match its UpdateCompactedSsTables message, which is what the action vocabulary keys the wire encoding off.

Library changes (minimal, fail-closed)

Read-side rejection only:

  • One arm in TryFrom<pb::Transaction> returns Error::NotSupported for a CompositeOperation, with a comment forbidding lenient parsing (the try_collect contract aborts a concurrent V2 commit rather than silently skipping it).
  • Regression test test_composite_operation_rejected_on_load.

No Operation enum variant, no apply / translation / conflict resolution, no write path, no Cargo feature gate, no Python/Java changes.

Verification

lance-table + lance build; new test passes; cargo fmt --all and cargo clippy -p lance-table -p lance --tests -- -D warnings clean.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Ignore keyword(s) in the title.

⛔ Ignored keywords (2)
  • WIP
  • Draft

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: d6c3e873-ac4a-40f7-89f6-e462d4db9be4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch will/oss-1530-draft-action-based-transaction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Important

This PR touches the Lance format specification.

Substantive changes to the format specification — the .proto definitions
and the spec docs under docs/src/format/ — require a PMC vote before merge.
Minor edits such as typo fixes, wording, or formatting are excluded; use your
judgment.

If this is a meaningful format change:

  • Start a vote following the Lance community voting process.
    Format specification modifications need 3 binding +1 votes (excluding the
    proposer), held on GitHub Discussions, with a minimum voting period of 1 week.
  • Once the vote passes, link the completed vote in this PR. It should not be
    merged until the vote is linked.

@github-actions github-actions Bot added A-format On-disk format: protos and format spec docs enhancement New feature or request labels Jul 23, 2026
@codecov

codecov Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.75510% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance-table/src/format/key_existence.rs 44.44% 5 Missing ⚠️
rust/lance-table/src/transaction/proto.rs 97.50% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@wjones127
wjones127 force-pushed the will/oss-1530-draft-action-based-transaction branch from 19ac8e0 to f53d35a Compare July 28, 2026 22:41
@wjones127
wjones127 changed the base branch from main to refactor/transaction-module-split July 28, 2026 22:41
@wjones127
wjones127 force-pushed the will/oss-1530-draft-action-based-transaction branch from f53d35a to 5422956 Compare July 28, 2026 23:21
Base automatically changed from refactor/transaction-module-split to main August 19, 2026 18:38
@wjones127
wjones127 force-pushed the will/oss-1530-draft-action-based-transaction branch from 5422956 to 132c3b4 Compare August 19, 2026 20:30
@wjones127 wjones127 changed the title feat(transaction): draft action-based transaction wire format (Transaction V2) feat(transaction): action-based transaction wire format (Transaction V2) Aug 19, 2026
wjones127 and others added 6 commits September 8, 2026 11:13
…ction V2)

Draft the full action vocabulary for action-based transactions (Transaction
V2) directly in canonical `transaction.proto`, so it can drive the OSS-1529
squash/merge spike and the OSS-757 PMC vote.

A `UserOperation` (a new `Transaction.operation` oneof arm, field 116) is an
ordered list of `UserAction` steps, each expanding to granular `Action`
deltas. Actions record the *change* to the manifest (not a post-image), which
is what makes compound commits and branch merge fall out uniformly. Minted
identifiers (field/fragment/base ids) carry a `Local` token via a single
`Ref { committed | local }` so they relocate on merge/rebase; reference-stable
changes key off stable coordinates. Computed conflict footprints and large
derivable row-level deltas stay off the wire.

Library support is intentionally READ-side fail-closed only: a transaction
carrying a `UserOperation` is rejected on load with a clear "not supported"
error, and there is no write path, no `apply`, no translation, and no conflict
resolution yet. This keeps older writers safe (a concurrent V2 commit in the
conflict window aborts an in-flight commit rather than being silently skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tory

Reorganize the action-based transaction (Transaction V2) wire draft into its
own files under protos/transaction/ so the design reads clearly and the shared
building blocks have a proper home:

- protos/transaction/actions.proto: the V2 vocabulary (Ref, UserOperation,
  UserAction, Action + action messages), as top-level messages. The design
  rationale is summarized in an in-tree file header (deltas vs post-images,
  minting vs reference-stable, Ref/Local resolution, field-level schema,
  index segments, what stays off the wire) so it stands on its own.
- protos/transaction/common.proto: UpdateMap/UpdateMapEntry and
  KeyExistenceFilter/ExactKeySetFilter/BloomFilter, promoted from nested
  Transaction messages to top-level so both the legacy operations and the V2
  actions can reference them without a circular import. Wire-compatible: field
  numbers unchanged and none of these types are Any-packed, so the
  fully-qualified name change is invisible on the wire.
- protos/transaction.proto moves to protos/transaction/transaction.proto and
  keeps only the Transaction envelope, legacy operations, and the
  user_operation oneof arm.

Comments use block style for IDE folding. Rust references to the promoted types
are repointed from pb::transaction::X to pb::X (mechanical, compiler-checked);
the hand-written dataset::transaction domain types are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refinements to the Transaction V2 wire draft (protos/transaction/actions.proto),
no behavior change:

- AddFragment / SetDeletionFile: resolve a contradiction. AddFragment's doc
  implied a freshly-minted (Local) fragment could take a deletion file via
  SetDeletionFile, but SetDeletionFile.fragment is a committed-only uint64.
  Clarify that a new fragment has no deletion vector and deletions arrive in a
  later operation once the id is committed, and document why SetDeletionFile
  takes no Ref.
- data_change: document the marker on every carrier (previously only on
  AddFragment), cross-referencing the canonical definition. Spell out its
  non-obvious meaning on AddIndexSegment / RemoveIndexSegment, where it refers
  to the indexed data rather than table rows.
- AssertUniqueKeys: note that key_fields is authoritative and the embedded
  filter.field_ids (an artifact of the shared KeyExistenceFilter type) is
  ignored.
- AdjustIndexCoverage: rename bare add / remove fields to add_fragments /
  remove_fragments for self-documentation and consistency with
  AddIndexSegment.covered_fragments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the drafted protos to the shape the implementation settled on, so the
whole format lands in one PR and one vote:

- `UserOperation` becomes `CompositeOperation`, which names what distinguishes
  it from the other operations -- a composite of granular actions committed
  atomically -- and loses its `description`, since the per-step
  `UserAction.description` is what keeps history readable. `UserAction` keeps
  its name: it is the grouping a user recognizes.
- The field actions reference fields by `Ref`, so an action can name a field
  the same operation is minting.
- `AddIndexSegment`, `RemoveIndexSegment`, `AdjustIndexCoverage` and
  `UpdateCompactedSsTables` join the action set.
- The index actions carry the name of the logical index the segment belongs
  to, which is what conflict detection compares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ReserveFragmentIds` documents that "fragments written against a reserved
range reference those ids as Committed", but `AddFragment` carried only a
`local` token, so nothing could name a reserved id and the reservation was
unreachable from the action vocabulary.

Widen `AddFragment.local` into `Ref id`, which spans both forms. A writer
that has to know a fragment's id before it commits -- because it bakes row
addresses into an index it writes in the same commit -- takes the id from a
reservation and names it as `Committed`.

Field 1 changes type rather than being deprecated in place. The actions
schema is used exclusively by the draft Transaction V2 format, which is
read-side fail-closed with no write path, so there are no encoded messages
to stay compatible with.
The counterpart of `ReserveFragmentIds` for the row id space. Reserving a
fragment id fixes the high half of a row address, which is enough for a
dataset without stable row ids; with them, an index records row ids instead,
and those come off their own counter with no way to reserve from it.

Drafted only here -- the apply side lands with the action implementation.

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Gate recommendation: request changes.

The delta/action direction solves the real atomic-commit and N×N conflict-growth problem and is stronger than composing legacy post-images. The current proposal is not yet a self-contained experimental wire contract: its governing lifecycle and forward-reader prerequisite are not in force, and core payloads cannot yet be interpreted losslessly or unambiguously. Land the governance dependency first, keep unaware readers able to open affected datasets, use the outer transaction envelope as the single source of identity/version, and preserve covering-index semantics.

Comment thread docs/src/format/table/transaction.md
/* Action-based transaction (Transaction V2). See actions.proto.
* EXPERIMENTAL: currently rejected on load; no write path.
*/
CompositeOperation composite_operation = 116;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new oneof arm is unknown to pre-v12 readers. When a composite transaction is inlined, prost drops the unknown arm and those releases propagate the resulting missing-operation error from Dataset::open. #9454 verifies the v1–v11 exposure, and the only matching fix is #7740 on main; no maintained-release backport PR exists. That fails #8304's prerequisite that feature writers not affect unaware readers. Before accepting this field, either make V2 transactions external-only until the compatibility floor moves (and specify/test that writer rule), or land the maintained-release backports first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backports are unfortunately unlikely to be realistic. We have a lot of different packages that depend on Lance and bake it into their binaries, and many of them are size constrained on releases.

Realistically, I think the best solution we can do here is add a strong warning that the datasets written with this won't be backwards compatible for now. This is strictly an opt-in feature to use these. When we actually stabilize it, we'll make it a writer flag to use these. Maybe this bug means it will also be a reader flag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An opt-in warning does not satisfy the forward-compatibility boundary this PR relies on: the proposed experimental policy requires writers using the feature not to affect unaware readers, while an inlined V2 transaction still prevents pre-v12 Dataset::open. The current head therefore still needs either an external-only writer rule or a compatible reader floor before this field can be accepted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, in ea4b6f3f and 0d8addd2. Agreed the backports are not realistic, so the docs now treat this as durable rather than transitional.

Four places a writer will actually hit it:

  • the composite_operation oneof arm here, with the mechanism spelled out
  • the actions.proto file header, short-form
  • a !!! danger admonition in the transaction specification, stating that a table with one anywhere in its history has a minimum reader version of v12.0.0
  • Rust: the action module's Stability section, a # Compatibility section on CommitBuilder::with_experimental_composite_operations, and the NotSupported message a caller gets without opting in -- that error is the first and possibly only place they read about it

All of them say the same thing explicitly: the version cannot be opened, not merely read as history.

On the writer/reader flag at stabilization -- agreed, and #9454 does look like it makes the reader-flag case. Worth settling on the discussion when we get there rather than now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warnings accurately document the break, but this finding is about the acceptance contract rather than discoverability. #8304 requires experimental writers not to affect unaware readers, while pre-v12 Dataset::open still fails. This therefore still needs an external-only writer rule or a compatible reader floor before merge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I think the only solution would be to create a reader feature flag. But that would also cause Dataset::open to fail, just with a nicer error message. Would adding that reader flag make things acceptable? One thing that makes me not like the idea of a reader flag is this is experimental and when we stabilize it we'll actually want the flag to mean "I understand the stable version of the protobuf". So on some level I'm fine with the experimental version just not being compatible. It's entirely opt-in: A user would have to manually construct a transaction with a CompositeOperation. So it's not unlike using an unstable file format version. I'm inclined to let it just fail as is while it's experimental, and add a reader flag upon stabilization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reader flag that still makes Dataset::open fail would not meet #8304's forward-compatibility prerequisite. Opt-in changes writer exposure, but the resulting stable table remains unreadable to an unaware reader. If the governing policy is changed and approved to permit that consequence, the acceptance contract changes; under the current proposed policy, this finding remains.

Comment thread protos/transaction/actions.proto Outdated
Comment on lines +78 to +81
// Unique identifier for this operation (matches Transaction.uuid semantics).
UUID uuid = 1;
// The dataset version this operation was planned against.
uint64 read_version = 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompositeOperation repeats both Transaction.uuid and Transaction.read_version, so the wire format permits two transaction identities and two source versions without saying which copy is authoritative or requiring equality. The outer values already drive transaction-file naming and rebase checkout; an action implementation that reads the inner values can therefore apply against a different source state. This also conflicts with the protobuf rule to store each fact once. Remove the inner copies and make the outer Transaction authoritative—the existing alternative in #7737 already uses that invariant—rather than asking every implementation to reconcile contradictory valid messages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are intentional. The idea is that if multiple transactions were squashed into a single one, you would still be able to derive the original identity and read versions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stated intent is not representable by this schema: squashing flattens each original CompositeOperation into a UserAction, but UserAction carries neither uuid nor read_version, and there is only one inner pair. The inner pair therefore cannot preserve multiple originals, while disagreeing inner and outer values remain valid but uninterpretable. Please encode per-original metadata at the repeated level or remove the duplicate pair.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. I don't know why it's at this level. It should be at UserAction level then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed—that places identity and read version at the repeated level that represents each original operation. The current head still keeps the singleton pair on CompositeOperation while UserAction lacks it, so this finding remains until the fields move or the duplicate pair is removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d1bc81a8: CompositeOperation no longer duplicates transaction identity or read version; the enclosing Transaction is now the sole source for both values.

UUID uuid = 1;
string name = 2;
// Indexed field ids (Committed, or Local for same-op minted fields).
repeated Ref fields = 3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddIndexSegment cannot represent the current IndexMetadata.covering_fields declaration. fields contains keyed columns followed by carried columns, and covering_fields is the only state that splits them; losing it makes selection and maintenance treat payload columns as keys and can omit the covering-index reader feature flag. Add a non-duplicating representation (for example, the validated count of trailing covering fields) and round-trip a covering segment.

Reproducer
printf 'fields { committed: 1 } fields { committed: 2 } covering_fields { committed: 2 }\n' | \
  protoc -I protos --encode=lance.table.AddIndexSegment protos/transaction/actions.proto

The observed head fails with Message type "lance.table.AddIndexSegment" has no field named "covering_fields", while IndexMetadata accepts the equivalent keyed/carried declaration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point. Will address.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. The current head still cannot encode the covering-field split, so I am keeping this finding until the promised representation and round-trip evidence land.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. AddIndexSegment now carries covering_fields (field 12) alongside fields, and it takes #9159's contract rather than main's: fields is the keyed columns only, covering_fields is independent of it, and the segment's dependency set is the union. The action has no historical encoding to stay compatible with, so there was no reason to be born with the legacy suffix convention.

The gap is that no release can store that contract yet — #9159 reserves FLAG_INDEPENDENT_COVERING_FIELDS with mark_supported(..., false), so no build writes or opens a manifest that sets it. Rather than block the action on that, apply lowers:

  • Disjoint declaration (fields = [1], covering_fields = [2]) → legacy IndexMetadata { fields: [1, 2], covering_fields: [2] } under FLAG_COVERED_INDEX_METADATA. Commits today, and the lowering drops out once the flag is implemented.
  • Overlapping declaration (fields = [1], covering_fields = [1, 2]) → rejected with NotSupported naming the flag. Flattening it would publish the weaker claim that the column is merely keyed, silently losing the fact that the segment serves its values.

The action's own shape does not change when the flag lands; only the lowering and the rejection come out.

The wire message and the format doc are in this PR (actions.proto, plus the covering paragraph in docs/src/format/table/transaction.md); the apply behaviour and its tests are in #8645.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in be2dd47e: AddIndexSegment now carries independent covering_fields, the format text defines the keyed/carried union contract, and the protobuf round-trip preserves both lists.

@lance-gatekeeper lance-gatekeeper Bot added K-changes Latest Gatekeeper recommendation requests changes. and removed K-changes Latest Gatekeeper recommendation requests changes. labels Sep 21, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Gate recommendation: request changes.

Author feedback clarified that original transaction identity and read version may be retained for squashed operations; that metadata must be carried per original operation, because the current single inner pair cannot represent multiple originals or resolve disagreement with the outer envelope. The remaining contract is unchanged: land #8304 first, keep experimental writers forward-compatible with unaware readers, and preserve covering-index field roles. A warning alone does not satisfy the explicit forward-compatibility prerequisite, and the promised covering-index representation is not yet in this head.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026
Review raised that the compatibility cost was understated, and the
backports that would remove it are unlikely: several downstream packages
bake Lance into size-constrained binaries, so older release lines will
not pick up #7740.

That makes this durable rather than transitional, so say so where a
writer will see it. Committing a CompositeOperation does not merely make
the transaction unreadable as history -- it makes that table version
unopenable by Lance before v12.0.0, because those releases decode the
manifest's inline transaction while opening the table and propagate the
failure (#9454).

Room in the actions.proto header, which is budgeted at under 300 words,
comes from dropping the point about index actions targeting a segment;
AddIndexSegment's own comment already says it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Gate recommendation: request changes.

The new warning now accurately states the pre-v12 open failure, but it documents rather than removes a compatibility break that the experimental policy forbids. This revision still needs the same self-contained wire contract: land #8304 first, keep unaware readers able to open affected datasets, encode identity and read version per original squashed operation instead of as one conflicting singleton pair, and preserve covering-index field roles. The delta/action design remains worthwhile once those constraints are met.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026
Review found the message could not represent IndexMetadata's
covering_fields at all, so a covering index was not expressible as an
action. Adds the field where the message is defined.

Targets the contract in #9159 rather than the one on main: `fields` is
the keyed columns only, `covering_fields` is an independent declaration
of the carried ones, and the dependency set is the union. A new action
has no historical encoding to stay compatible with, so it can take the
contract the format is moving to and avoid a field-number change later.

#9159 leaves FLAG_INDEPENDENT_COVERING_FIELDS unimplemented, so a
manifest cannot yet express a column that is both keyed and carried.
The overlapping form is therefore rejected at apply, and a disjoint one
lowers to the legacy subset representation; both come out when the flag
is implemented, with no change to this message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wjones127

Copy link
Copy Markdown
Contributor Author

Decision

AddIndexSegment declares keys and carried columns independently, per #9159: fields is the keyed columns only, covering_fields is not a subset of it, and the segment's dependency set is the union. Until a release implements FLAG_INDEPENDENT_COVERING_FIELDS, apply lowers a disjoint declaration to the legacy manifest form (fields = keys ++ carried, covering_fields = carried as the trailing suffix, fenced by FLAG_COVERED_INDEX_METADATA) and rejects an overlapping one with NotSupported.

Status

Settled 2026-09-21. Wire message and format doc on #7954; apply behaviour and tests on #8645 (lower_covering, two tests in add_index_segment.rs).

Options and criteria

Criteria, in order: (a) the action is a new encoding with no history, so it should carry the contract we want rather than the one IndexMetadata inherited; (b) nothing the action commits today may be unrepresentable in a manifest a current build can write; (c) no silent loss of a declared property.

  1. Lower, reject overlap — chosen. Satisfies all three. The lowering and the rejection both delete themselves when the flag ships; the action's shape never changes.
  2. Adopt main's contract (covering_fields a trailing subset of fields) — satisfies (b) and (c), fails (a): the action would be born with a convention feat(format): separate index keys from covering fields #9159 is already replacing, and would need a wire change to catch up.
  3. Adopt feat(format): separate index keys from covering fields #9159's contract and write the flag — fails (b). feat(format): separate index keys from covering fields #9159 registers the bit with mark_supported(..., false), so no build can write or open such a manifest; the action would be able to express commits nothing can read.

Rejected, with the reason and when it was tried

  • Flatten an overlapping declaration (drop the overlapping ids from covering_fields, keep them in fields) — rejected 2026-09-21, not implemented. It commits, but the manifest then claims only that the index is keyed on the column, silently dropping the writer's statement that the segment also serves its values. A query planner reading that manifest would decline a covering scan the index could actually answer. A rejection the caller sees is better than a downgrade it does not.

Open questions

  • When FLAG_INDEPENDENT_COVERING_FIELDS becomes supported, the lowering and the rejection come out together and apply writes the union form directly. Nothing in the action or its wire encoding changes, so this needs no format revision — but it does need the flag's own vote to land first.

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Gate recommendation: request changes.

1 fixed / 3 remain. The covering-field decision is now represented directly in AddIndexSegment and round-trips, so that blocker is closed. The proposal still needs an admissible experimental wire contract: land #8304 first, keep pre-v12 readers able to open V2-authored tables rather than only documenting the break, and encode identity and read version per original squashed operation instead of as a conflicting singleton duplicate. The delta/action design remains worthwhile once those constraints are met.

@lance-gatekeeper lance-gatekeeper Bot added K-changes Latest Gatekeeper recommendation requests changes. and removed K-changes Latest Gatekeeper recommendation requests changes. labels Sep 21, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026
CompositeOperation carried uuid and read_version, both copies of fields
the enclosing Transaction already has. Nothing read them back: the
to-wire path stamped them from the envelope and the read side dropped
them.

The stated purpose -- letting a squashed operation keep its provenance
-- is one the fields cannot serve where they sat. Squashing collapses
each original CompositeOperation into one UserAction, so the message
holding the identity is exactly the one squashing destroys. If per-step
provenance is wanted, it belongs on UserAction, and it can be added as
an optional field when squashing exists to need it.

Renumber actions to 1 rather than reserving 1 and 2: this message has
not shipped, and the file states that its field numbers are not a
stable contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wjones127

Copy link
Copy Markdown
Contributor Author

Decision

CompositeOperation no longer carries uuid or read_version. The message is now just repeated UserAction actions = 1. Transaction identity lives on the enclosing Transaction alone.

Status

Done, in d1bc81a (proto) and 9abec50 on #8644 (the Rust conversions that existed only to populate them).

Options and criteria

The criterion was whether either field can ever carry information the envelope does not already have.

The justification for them was provenance across a future transaction squash. That justification does not survive contact with the squash design in this same file: squashing collapses each original CompositeOperation into one UserAction, so the message holding the identity is exactly the message squashing destroys. Pre-squash the fields duplicate the envelope; post-squash they are overwritten. There is no state in which they carry anything.

That was confirmed against the code rather than assumed. They were write-only — stamped from the envelope on the way out, dropped on the way in; the Rust struct never modelled them. No commit or conflict path consults a per-operation read version: ApplyState takes it from the Transaction (apply.rs:77), and check_action_txn compares only footprints.

Options weighed:

  1. Move both to UserAction, optional, absent = inherit from the envelope. The level that survives squash, next to description, which serves the same readable-history purpose. This is the right shape if the fields are wanted.
  2. Drop them now; add to UserAction when squashing exists. Chosen.
  3. Keep as-is. Rejected outright — nothing reads them.

(2) over (1) because the feature that would justify the fields does not exist, and the current misplacement is itself evidence that they are hard to place correctly before it does. Adding an optional field later is cheap; voting.md commitment 3 permits breaking changes to experimental features without a separate vote, and this PR is not yet merged, so nothing has ever been written with them.

Rejected, with the reason and when it was tried

  • Keeping the fields (option 3), 2026-09-21. Write-only in every path; no reader anywhere in rust/, python/, or java/ — the Java bindings reject CompositeOperation before any field access.
  • Moving them to UserAction now (option 1), 2026-09-21. Correct placement, but speculative: it encodes a squash model that has no implementation, and a format vote should not be asked to ratify a provenance scheme whose consumer does not exist. Revisit when squashing lands — this is the shape to adopt then.
  • reserved 1, 2; instead of renumbering actions to 1, 2026-09-21. Three for three, every removed field elsewhere in protos/ is reserved by number and name, so the convention does point that way. It does not apply here: reserved protects against old data using the number, and this message has never shipped. This file also states that its field numbers are not a stable contract. A gap at 1 and 2 in a message being read for a vote invites a question with no answer.

Open questions

  • If per-step provenance does land on UserAction, read_version there is best-effort audit metadata, not a resolvable reference: the version it names may have been cleaned up, and the {read_version}-{uuid}.txn file with it. That needs saying in the proto comment, or a reader will try to dereference it. If we cannot say what a reader should do with it, that is an argument for not adding it at all.
  • apply.rs:608 suggests squash provenance is expected to ride on per-action stamped fields (created_at_version_meta / last_updated_at_version_meta) rather than on any per-operation or per-step version. If that holds, option 1 may never be needed.

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Gate recommendation: request changes.

3 fixed / 1 remains. The duplicate identity/read-version fields are gone, joining the already-closed governance and covering-field findings. The remaining compatibility contract is unchanged: an inlined V2 transaction makes pre-v12 readers unable to open the table, while the merged experimental-feature policy requires writers using the feature not to affect unaware readers. Keep V2 transactions external-only until the compatibility floor moves (and specify/test that writer rule), or establish an equivalent path that leaves those readers able to open the table. The delta/action design remains worthwhile once that prerequisite is met.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 21, 2026
@wjones127
wjones127 marked this pull request as draft September 22, 2026 22:03
@wjones127

Copy link
Copy Markdown
Contributor Author

I'm going to fix #9454 before marking this ready for review

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 22, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ci CI / build workflows A-format On-disk format: protos and format spec docs enhancement New feature or request format-change A change to the format spec, which requires a vote. Remove if minor (e.g. fixing typo).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant