A Delete that rebases over a concurrently committed Project reinstates a data file the projection had pruned. The resulting manifest carries a data file none of whose fields are in the schema, and the field-id watermark goes back up over an id the projection retired.
Reproduction
On main (verified at 8cdffd30e), through the public write paths only:
- Create a dataset with columns
a, b; then add_columns a third column c, so fragment 0 carries two data files — [a, b] and [c]. Call this version v.
- Stage a delete against
v with DeleteBuilder::execute_uncommitted.
- Commit a
Project down to {a, b}. It lands at v+1, correctly pruning the [c] data file: fragment 0 now has one file.
- Commit the staged delete against
v. It rebases over the projection and lands at v+2.
Observed across the three versions:
| version |
schema field ids |
fragment 0 data files |
Manifest::max_field_id |
v |
[0, 1, 2] |
[[0, 1], [2]] |
2 |
v+1 (project) |
[0, 1] |
[[0, 1]] |
1 |
v+2 (delete) |
[0, 1] |
[[0, 1], [2]] |
2 |
The [2] file is back, and the watermark is non-monotonic — so a later write can mint field id 2 while a data file already claims it.
Cause
Same mechanism as #9216: Operation::Delete's apply replaces the fragment entry wholesale from a post-image built at the read version, which still lists the file the projection dropped. check_delete_txn permits a concurrent Project for the same reason it permits a concurrent DataOverlay.
The scan still succeeds — the orphaned file is simply unreachable — so this does not surface as a read error today. It is a correctness hazard for field-id allocation and leaves a file that cleanup will not collect.
Coverage
A failing test asserting the correct behaviour is committed #[ignore]d as io::commit::conflict_matrix::cases::delete_must_not_reinstate_a_pruned_data_file, pointing at this issue. The (Delete, Project) cell is excluded from that module's matrix until this is fixed.
cargo test -p lance --lib conflict_matrix -- --ignored
A
Deletethat rebases over a concurrently committedProjectreinstates a data file the projection had pruned. The resulting manifest carries a data file none of whose fields are in the schema, and the field-id watermark goes back up over an id the projection retired.Reproduction
On
main(verified at8cdffd30e), through the public write paths only:a,b; thenadd_columnsa third columnc, so fragment 0 carries two data files —[a, b]and[c]. Call this versionv.vwithDeleteBuilder::execute_uncommitted.Projectdown to{a, b}. It lands atv+1, correctly pruning the[c]data file: fragment 0 now has one file.v. It rebases over the projection and lands atv+2.Observed across the three versions:
Manifest::max_field_idv[0, 1, 2][[0, 1], [2]]v+1(project)[0, 1][[0, 1]]v+2(delete)[0, 1][[0, 1], [2]]The
[2]file is back, and the watermark is non-monotonic — so a later write can mint field id 2 while a data file already claims it.Cause
Same mechanism as #9216:
Operation::Delete's apply replaces the fragment entry wholesale from a post-image built at the read version, which still lists the file the projection dropped.check_delete_txnpermits a concurrentProjectfor the same reason it permits a concurrentDataOverlay.The scan still succeeds — the orphaned file is simply unreachable — so this does not surface as a read error today. It is a correctness hazard for field-id allocation and leaves a file that cleanup will not collect.
Coverage
A failing test asserting the correct behaviour is committed
#[ignore]d asio::commit::conflict_matrix::cases::delete_must_not_reinstate_a_pruned_data_file, pointing at this issue. The(Delete, Project)cell is excluded from that module's matrix until this is fixed.