Skip to content

feat(format): separate index keys from covering fields - #9159

Open
Ali2Arslan wants to merge 1 commit into
lance-format:mainfrom
Ali2Arslan:feat/independent-covering-fields
Open

Ali2Arslan wants to merge 1 commit into
lance-format:mainfrom
Ali2Arslan:feat/independent-covering-fields

Conversation

@Ali2Arslan

@Ali2Arslan Ali2Arslan commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • define the new IndexMetadata contract so fields contains key fields only and covering_fields is an independent declaration that may overlap it
  • define the index dependency set as the union of fields and covering_fields
  • reserve FLAG_INDEPENDENT_COVERING_FIELDS (bit 11 / 2048) as a paired reader/writer fence and keep it unsupported until the implementation lands
  • specify activation: setting the flag atomically normalizes every legacy-form entry of the index section in the same manifest (or requires that none declares covering fields), a retry normalizes against the index section it is retrying against, and derived manifests retain the bit
  • update index lifecycle, overlay-staleness, and feature-flag specifications to use the independent declarations

Compatibility

The subset contract for covering fields (added in #8535, refined by the now-merged #8856) shipped in Lance 11.0.0, so the same protobuf bytes cannot be reinterpreted unconditionally. A manifest using the new contract must set both FLAG_COVERED_INDEX_METADATA and FLAG_INDEPENDENT_COVERING_FIELDS. A manifest with only FLAG_COVERED_INDEX_METADATA retains the stable legacy interpretation, where covering_fields is a subset of fields with no id repeated.

The new bit makes older releases reject independent declarations instead of treating covering fields as keys. No protobuf field number or wire type changes. Bit 11 avoids the reserved bit 9 and the bit 10 FLAG_FRAGMENT_REUSE_INDEX from #9136.

This PR intentionally reserves and specifies the contract without implementing readers or writers, following the format-change process. Bit 11 sits above FLAG_UNKNOWN (1 << 9) and supported_flags_when starts from FLAG_UNKNOWN - 1, so no build can open or write a manifest that sets it; FLAG_FRAGMENT_REUSE_INDEX is in the same reserved state on main today. Consequently the existing keyed_fields derivation, which subtracts the carried count from fields, can never observe an independent declaration and is left for the implementation PR. This PR layers on the carried-column storage contract from #8856 so physical storage and query behavior can follow the approved metadata contract.

Testing

  • cargo fmt --all
  • cargo test -p lance-table feature_flags --lib
  • cargo clippy --all --tests --benches -- -D warnings
  • RUSTDOCFLAGS='-D warnings' cargo doc -p lance-table --no-deps
  • uv run --project docs python ci/check_proto_comments.py
  • cd docs && uv run mkdocs build

@github-actions github-actions Bot added A-format On-disk format: protos and format spec docs format-change A change to the format spec, which requires a vote. Remove if minor (e.g. fixing typo). labels Sep 11, 2026
@Ali2Arslan
Ali2Arslan marked this pull request as ready for review September 11, 2026 19:27
@github-actions github-actions Bot added the enhancement New feature or request label Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Important

Format specification vote

This PR modifies the Lance format specification, so it requires 3 binding +1 votes from PMC members (excluding the proposer), at least one of them on the latest commit, and a minimum 72-hour voting period, weekends excluded, before it can merge. Vote by approving this PR (+1) or requesting changes (−1, a veto). See the voting process.

Approvals carry over across pushes, so a rebase or a typo fix does not send everyone back to re-vote. Whoever approves the latest commit is vouching that nothing substantive has changed since the earlier approvals; if something has, ask for fresh votes.

Status: ✅ Vote passed — 3 PMC approvals, voting period elapsed

Approvals @wjones127, @westonpace, @BubbleCal (3/3)
Latest commit approved by @BubbleCal
Vetoes none
Voting period elapsed — ended Wed 2026-09-16 19:27 UTC (12:27 PDT)

Updated automatically by the format-spec vote gate, which re-checks every 15 minutes — just voted? Re-check now (press Run workflow; leave the input blank to re-check every open format PR). A PMC member may apply the format-waived label to waive the vote for a trivial edit (typo, wording, formatting).

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 11, 2026
@Ali2Arslan Ali2Arslan changed the title feat(format): separate index keys from covering fields feat(format)!: separate index keys from covering fields Sep 11, 2026
@lance-gatekeeper lance-gatekeeper Bot added K-changes Latest Gatekeeper recommendation requests changes. and removed K-changes Latest Gatekeeper recommendation requests changes. labels Sep 11, 2026

@wjones127 wjones127 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.

Need to validate my assumption, but if we can I'm fine with making a breaking change to the existing spec so we avoid having these two separate flags.

Comment thread protos/table.proto Outdated
@lance-gatekeeper lance-gatekeeper Bot added K-changes Latest Gatekeeper recommendation requests changes. and removed K-changes Latest Gatekeeper recommendation requests changes. labels Sep 11, 2026
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from 0888867 to fbf5f0f Compare September 11, 2026 22:21
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 11, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added K-changes Latest Gatekeeper recommendation requests changes. and removed K-changes Latest Gatekeeper recommendation requests changes. labels Sep 11, 2026
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from fbf5f0f to 0888867 Compare September 11, 2026 22:55
@Ali2Arslan Ali2Arslan changed the title feat(format)!: separate index keys from covering fields feat(format): separate index keys from covering fields Sep 11, 2026
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 11, 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 11, 2026
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from 0888867 to f3ea036 Compare September 14, 2026 16:42
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 14, 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 14, 2026
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from 64ab4d4 to 8e6c8f6 Compare September 16, 2026 03:26
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 16, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 16, 2026
@Ali2Arslan

Copy link
Copy Markdown
Contributor Author

I've merged #8856 which added (a different) concept of "this is how you specify a key column is also covered" since it already had 3 votes. Let's go ahead and rebase this PR on top of the new wording (since it seems we have alignment on this approach over the other approach). Then we can merge this PR too.

@Ali2Arslan let me know if you need any help. Thanks!

Thanks @westonpace , I updated the wordage if you guys want to take another look.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. and removed K-approved Latest Gatekeeper recommendation permits acceptance. labels Sep 16, 2026
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from 8e6c8f6 to b8adf6c Compare September 21, 2026 17:00
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. 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: approve.

The rebase preserves the accepted independent-declaration contract while fitting it after newly occupied feature-flag bits 9 and 10: bit 11 remains explicitly rejected by current readers and writers, and the unknown boundary moves to bit 12. The atomic activation, current-index retry, and sticky-retention rules remain intact, so released bit-7 manifests and #8856ʼs carried-column storage contract stay unambiguous.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 21, 2026

@wjones127 wjones127 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.

I'm a little sad about the two flags, but I'll accept it.

wjones127 added a commit that referenced this pull request Sep 21, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 21, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 21, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 22, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 22, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 22, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wjones127 added a commit that referenced this pull request Sep 23, 2026
An index can carry a column's values without being keyed on it. The action
vocabulary had no way to say so, so an action set that built such an index
would have presented every field as indexed, and the manifest the action
path assembles would have published it without the fence that stops an
older library from answering a query from a carried column.

`AddIndexSegment` gains a `covering_fields` list of the same references as
`fields`. The two are independent -- `fields` means the keyed columns only
and the segment's dependency set is the union -- which is the contract in
#9159 and the one the action should be born with: unlike `IndexMetadata`,
it has no historical encoding to stay compatible with.

A manifest cannot express that contract yet, because
FLAG_INDEPENDENT_COVERING_FIELDS is reserved but unimplemented. So apply
lowers a disjoint declaration to the legacy form -- keys followed by the
carried columns, `covering_fields` naming that trailing subset -- and
rejects an overlapping one rather than publishing the weaker claim that
the column is merely keyed. Both come out when a release implements the
flag; the action's own shape does not change.

The split also joins the index identity two concurrent writers compare:
disagreeing about which columns are merely carried is disagreeing about
what the index answers for, not just about what it holds.

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

@westonpace westonpace left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the rebase and addressing the feedback, I'm +1 on the new wording.

@BubbleCal BubbleCal 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.

The independent key/covering declarations read much clearer to me, and since nothing writes covering indexes yet this is a good time to make the switch. Left a few small notes, mainly the bit collision after rebase. Approving assuming those get sorted out.

Comment thread rust/lance-table/src/feature_flags.rs Outdated
Comment thread docs/src/format/index/index.md Outdated
Comment thread docs/src/format/index/index.md
Co-authored-by: Cursor <cursoragent@cursor.com>
@Ali2Arslan
Ali2Arslan force-pushed the feat/independent-covering-fields branch from b8adf6c to 7657b56 Compare September 23, 2026 20:21
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 23, 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-format On-disk format: protos and format spec docs breaking-change 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.

5 participants