Skip to content
Open
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
ad36407
docs: correct stable row ID migration and storage
lance-gatefixer[bot] Aug 28, 2026
6682e7d
docs: preserve stable row ID contracts
lance-gatefixer[bot] Aug 28, 2026
01197a5
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 28, 2026
5fe6f68
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 29, 2026
c562b59
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 29, 2026
e2a07d0
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 29, 2026
ade6899
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 29, 2026
ba4c908
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 29, 2026
6cd4cd1
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 29, 2026
efdb715
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 29, 2026
ac6ccbe
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 29, 2026
7c43b53
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 30, 2026
ae0fdfe
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 30, 2026
df82305
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 30, 2026
469f4d8
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
fe4f1ad
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
709e1d0
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
babd27d
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 31, 2026
457e5eb
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
b88ba38
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 31, 2026
9ec4373
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 31, 2026
e28be4c
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Aug 31, 2026
5e5d514
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
53c70fd
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
ecfe405
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
9bb5ae7
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Aug 31, 2026
708326a
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
aeef6ad
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
edc31a4
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
5284aaf
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
fe1e986
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
7aa678d
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
82ab036
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
4b49bf6
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
789894e
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
42cda3e
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
807a2b5
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
40cba18
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
8189bdd
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
372e0f7
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 1, 2026
46f75c6
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
f02ea0c
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
07ea9d1
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
95984b2
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
3bde203
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
291ad63
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
4c22e18
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
e3de446
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
d4d9273
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
fe4cb53
Merge branch 'main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 1, 2026
7e146ce
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
bb3d1a6
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
7ad0229
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
528b14e
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
abae758
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
2395088
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
319619a
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
0ef2bb5
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
e7221d0
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
2541d4e
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
525cafb
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
bf0e43f
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
8ba74ff
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
58e731f
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
50455e3
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
cca243f
Merge origin/main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
487e2fe
Merge origin/main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
9e4f376
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
66f20c6
docs: require legacy manifest metadata upgrade
lance-gatefixer[bot] Sep 2, 2026
2370e3a
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 2, 2026
bc35cda
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
f24bfc9
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 2, 2026
0c9b64a
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
87d1f60
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
f86da2c
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
4592b75
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
42f0988
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 3, 2026
6f82716
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 3, 2026
645e1f0
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
b3f7e10
Merge origin/main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
c5cb6ac
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 3, 2026
c5e3538
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
ce2b142
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
f87d191
Merge main into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
a5209fb
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
3d89c80
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
ba4b51b
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
1d81337
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
1fbe766
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 4, 2026
825eba8
style(protos): follow multiline comment lint
lance-gatefixer[bot] Sep 4, 2026
6389cc2
Merge remote-tracking branch 'origin/main' into gatekeeper/fix-8851-1
lance-gatefixer[bot] Sep 4, 2026
620475f
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 4, 2026
7e28185
Merge remote-tracking branch 'refs/remotes/origin/main' into gatekeep…
lance-gatefixer[bot] Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions docs/src/format/table/row_id_lineage.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ This protocol mirrors fragment ID assignment and ensures row IDs are unique acro

Stable row IDs are a dataset-level feature recorded in the table manifest.

- Stable row IDs **must be enabled when the dataset is first created**.
- Currently, they **cannot be turned on later** for an existing dataset. Attempts to write with `enable_stable_row_ids = true` against a dataset that was created without stable row IDs will not change the dataset's configuration.
- Stable row IDs may be enabled when a dataset is created or by migrating an existing dataset.
- An ordinary write with `enable_stable_row_ids = true` does not migrate an existing dataset. Use the stable row ID migration operation instead; the Rust API exposes it as `Dataset::migrate_to_stable_row_ids`.
- Before migrating a dataset whose current manifest has no writer version, use the current Lance writer to commit an ordinary no-op deletion with predicate `false`, then reopen the latest version. This metadata-upgrade commit recomputes the authoritative physical row count for every fragment. Do not invoke stable row ID migration directly on such a legacy manifest: affected releases may have recorded stale counts, which would produce incomplete row ID sequences.
- Before migration, stop all index builds and index commits, drop every secondary index so no index entry remains in the dataset metadata, and keep index creation quiesced until migration completes. An in-flight index commit from a pre-migration snapshot can otherwise attach stale physical row addresses after activation. Recreate indices after migration.
- Quiesce data-modifying writers during migration. The migration uses a single atomic merge commit and does not retry when a concurrent write causes a conflict; the caller must retry the migration.

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.

Index creation must be quiesced too. An index build started on the pre-migration snapshot stores physical row addresses. If migration commits first, CreateIndex explicitly rebases over a non-MemWAL Merge, so that stale index can attach to the stable-ID manifest; searches then treat address values as stable IDs and silently miss rows. The current wording allows this because an index build is not a data-modifying writer. Require no in-flight index builds or commits during migration and state that every index entry must be absent; the implementation follow-up should make migration activation conflict with CreateIndex in either order.

Reproducer run on ad36407

I added this regression to dataset_migrations.rs and ran it against the observed head:

use crate::dataset::transaction::{Operation, Transaction};
use crate::dataset::write::CommitBuilder;

#[tokio::test]
async fn test_migration_rebases_stale_index_after_activation() {
    let mut dataset = make_simple_dataset("memory://migrate_stale_index", 10).await;
    let schema = Arc::new(ArrowSchema::from(dataset.schema()));
    let batch = RecordBatch::try_new(
        schema,
        vec![Arc::new(Int64Array::from_iter_values(10..20))],
    ).unwrap();
    dataset = InsertBuilder::new(Arc::new(dataset))
        .with_params(&WriteParams { mode: WriteMode::Append, ..Default::default() })
        .execute(vec![batch]).await.unwrap();

    dataset.create_index(
        &["id"], IndexType::BTree, Some("stale_btree".to_string()),
        &ScalarIndexParams::default(), true,
    ).await.unwrap();
    let stale_index = dataset.load_indices().await.unwrap()[0].clone();
    dataset.drop_index("stale_btree").await.unwrap();

    let stale_reader = Arc::new(dataset.clone());
    let stale_commit = Transaction::new(
        dataset.manifest.version,
        Operation::CreateIndex {
            new_indices: vec![stale_index],
            removed_indices: vec![],
        },
        None,
    );
    dataset.migrate_to_stable_row_ids().await.unwrap();
    let dataset = CommitBuilder::new(stale_reader)
        .execute(stale_commit).await.unwrap();

    let results = dataset.scan().filter("id = 15").unwrap()
        .try_into_batch().await.unwrap();
    assert_eq!(results.num_rows(), 1);
}

Command:

cargo test -p lance test_migration_rebases_stale_index_after_activation -- --nocapture

Expected one row; observed zero, so the assertion failed with left 0 and right 1.

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 in 6682e7d: migration documentation now requires stopping index builds and commits, removing every index entry, and keeping index creation quiesced until migration completes.

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 6682e7ddc: the migration procedure now stops index builds and commits, requires every secondary-index entry to be removed, and keeps index creation quiesced through activation, excluding the reproduced stale-index rebase.

- Migration assigns an ID to every physical row position, including deleted positions, and atomically enables the feature and advances `next_row_id`. Migrating a dataset that already uses stable row IDs is a no-op.

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 guarantee does not hold for legacy manifests affected by #1531: those manifests can have writer_version == None and an undercounted physical_rows. migrate_to_stable_row_ids constructs sequences from that stale value before migrate_manifest recomputes physical row counts, so the commit succeeds with fewer IDs than physical positions and the resulting dataset is corrupt. Hydrate authoritative counts before assigning IDs (with a released historical-fixture regression), or document and enforce a safe metadata-upgrade prerequisite.

Reproducer run on 9e4f376

Added this test to dataset_migrations.rs:

#[tokio::test]
async fn repro_migrate_to_stable_row_ids_recomputes_legacy_physical_rows() {
    let mut dataset =
        make_simple_dataset("memory://migrate_legacy_physical_rows", 10).await;

    let mut manifest = dataset.manifest.as_ref().clone();
    manifest.writer_version = None;
    let mut fragments = manifest.fragments.as_ref().clone();
    fragments[0].physical_rows = Some(5);
    manifest.fragments = Arc::new(fragments);
    dataset.manifest = Arc::new(manifest);

    dataset.migrate_to_stable_row_ids().await.unwrap();
    dataset.validate().await.unwrap();
}

Command:

cargo test -p lance repro_migrate_to_stable_row_ids_recomputes_legacy_physical_rows --lib -- --nocapture

Expected the migrated dataset to validate. It instead failed with Fragment 0 has 5 row ids, but 10 physical rows.

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 in 66f20c6: migration guidance now requires a current Lance writer to commit a no-op deletion with predicate false and reopen the latest version before migrating a manifest without a writer version, ensuring authoritative physical row counts are recomputed.

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 66f20c655: the documented no-op deletion and reopen now upgrades a legacy manifest before migration; the reproduced stale-count case recomputed 10 physical rows before ID assignment and validated after migration.

- When stable row IDs are disabled, the `_rowid` column (if requested) is not stable and should not be used as a persistent identifier.

Row-level version tracking (`_row_created_at_version`, `_row_last_updated_at_version`) and the row ID index described below are only available when stable row IDs are enabled.
Expand Down Expand Up @@ -181,11 +185,14 @@ The implementation selects the most compact encoding based on the value range, c

</details>

#### Inline vs External Storage
#### Inline and External Storage

Row ID sequences are stored either inline in the fragment metadata or in external files.
Sequences smaller than ~200KB are stored inline to avoid additional I/O, while larger sequences are written to external files referenced by path and offset.
This threshold balances manifest size against the overhead of separate file reads.
`DataFragment` defines inline and external metadata fields as valid wire alternatives for row ID sequences and row version sequences.
These fields do not currently imply a size-based switching threshold.
Current Lance writers store all three sequence types inline in the fragment metadata regardless of their encoded size and do not emit the external alternatives.

Current Lance readers can load externally stored row ID sequences.
The format also permits external created-at and last-updated-at version sequences, but current Lance readers cannot load them; this is an implementation limitation, not an invalid encoding.

<details>
<summary>DataFragment row_id_sequence field</summary>
Expand Down Expand Up @@ -360,4 +367,3 @@ WHERE _row_created_at_version <= {begin_version}
```

This query excludes newly inserted rows by requiring `_row_created_at_version <= {begin_version}`, ensuring only pre-existing rows that were subsequently updated are returned.

14 changes: 8 additions & 6 deletions protos/table.proto
Original file line number Diff line number Diff line change
Expand Up @@ -372,23 +372,25 @@ message DataFragment {
// That is, if a fragment has 3 rows, and the row ids are [1, 42, 3], then the
// first row is row 1, the second row is row 42, and the third row is row 3.
oneof row_id_sequence {
// If small (< 200KB), the row ids are stored inline.
// Current Lance writers store row ids inline regardless of encoded size.
bytes inline_row_ids = 5;
// Otherwise, stored as part of a file.
// Supported by current Lance readers, but not emitted by current Lance writers.
ExternalFile external_row_ids = 6;
} // row_id_sequence

oneof last_updated_at_version_sequence {
// If small (< 200KB), the row latest updated versions are stored inline.
// Current Lance writers store last-updated versions inline regardless of encoded size.
bytes inline_last_updated_at_versions = 7;
// Otherwise, stored as part of a file.
// Valid external alternative. Current Lance writers do not emit this field,
// and current Lance readers cannot load it.
ExternalFile external_last_updated_at_versions = 8;

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.

Fields 8 and 10 cannot be reclassified as reserved. Commit 85d44b6 introduced them as valid external alternatives, and that commit is an ancestor of the released v10.0.0 format; these fields are governed by stable feature flag bit 2, not an unstable flag. This wording makes a previously conforming external-version manifest forbidden, violating the stable persisted-format contract. The current RowDatasetVersionMeta::load_sequence TODO is an implementation gap, not permission to retreat from that contract. Keep both external alternatives valid while documenting that built-in writers choose inline storage and current Lance readers lack support; compatible reader support can follow separately.

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 in 6682e7d: external version references remain valid wire alternatives, while the documentation now distinguishes current inline writer behavior and unsupported reader loading from format validity.

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 6682e7ddc: fields 8 and 10 remain valid external wire alternatives, while the comments now distinguish that contract from current writer and reader limitations.

} // last_updated_at_version_sequence

oneof created_at_version_sequence {
// If small (< 200KB), the row created at versions are stored inline.
// Current Lance writers store created-at versions inline regardless of encoded size.
bytes inline_created_at_versions = 9;
// Otherwise, stored as part of a file.
// Valid external alternative. Current Lance writers do not emit this field,
// and current Lance readers cannot load it.
ExternalFile external_created_at_versions = 10;
} // created_at_version_sequence

Expand Down
Loading