Skip to content

fix: error when a path cannot be made relative to the dataset base - #8653

Open
LuciferYang wants to merge 3 commits into
lance-format:mainfrom
LuciferYang:fix/strip-prefix-fails-closed
Open

LuciferYang wants to merge 3 commits into
lance-format:mainfrom
LuciferYang:fix/strip-prefix-fails-closed

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

remove_prefix existed twice, identically, in dataset/cleanup.rs and dataset/files.rs. Both returned the input unchanged when Path::prefix_match failed, which hands back an absolute path where the caller needs one relative to the dataset root. This replaces both with a single strip_prefix in files.rs that returns Result.

The fallback could delete live data. If a misreported location happens to read as base-relative under data, cleanup's classifier passes it through the starts_with("data") check, fails to find it in the keep-set, and deletes a file the manifest still references. tracked_files and all_files also document their path column as relative to base_uri, so an absolute value there breaks a documented contract silently.

Seven of the eight call sites propagate with ?. Four of them build the path by joining onto the base and cannot reach the error at all. The other three pass a location the store reported from a listing, and there an omission is what does the damage: a missing keep-set entry gets a live file deleted, and a missing inventory row gets it deleted by whatever consumes the inventory.

The eighth is CleanupTask::cleanup_file_if_not_referenced, which skips the object instead. It is the only call site that runs while the pass is already deleting, so propagating would strand a half-finished pass and repeat on every later run. A store that misreports one path misreports every path of that kind, so the per-file detail stays at debug! and the pass warns once with a count.

This is pre-work for #8097, which builds a keep-set for external distributed cleanup out of this helper's output. An absolute path in a keep-set means the real path is absent from it, and a driver that diffs the keep-set against a listing deletes a live file.

#8652 was filed out of this work for a related OpenDAL path-encoding mismatch. #8654 fixed it and this branch is rebased onto that, so the error path is no more reachable on those stores than anywhere else.

Not in scope: two byte-wise prefix strips with the same shape remain in mem_wal/memtable/flush.rs and lance-table/src/format/index.rs, both unreachable today, and DataFile.path's single-segment invariant is still neither documented nor validated.

Test plan

Four strip_prefix cases: a nested path, a path equal to the base, a path under a different dataset, and bucket/dataset2 against base bucket/dataset, which pins that matching is per path segment rather than per byte. build_all_files_batch_rejects_a_path_outside_the_base checks that the batch fails and names the offending path instead of emitting an absolute one.

cleanup_skips_listed_files_outside_the_dataset_base wraps the store so listed data files report data/outside-base/{filename}, which is outside the base but still reads as a collectable data path. Both directions were checked by reverting: restore the fallback and the test fails, replace the skip with ? and it fails too.

cargo fmt --all --check, cargo clippy --all --tests --benches -- -D warnings, RUSTDOCFLAGS="-D warnings" cargo doc -p lance --no-deps, and cargo test -p lance --lib dataset:: (2204 tests) are clean.

lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Aug 20, 2026
@LuciferYang

Copy link
Copy Markdown
Contributor Author

cc @wjones127 FYI

@lance-gatekeeper lance-gatekeeper Bot added K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. and removed K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Aug 21, 2026
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

`remove_prefix` existed twice, behaviourally identical, in `cleanup.rs`
and `files.rs`, and both fell back to the input on a non-matching prefix.
That fallback returns an absolute path where every caller expects one
relative to the dataset root.

`tracked_files` and `all_files` both document their `path` column as
"Relative to `base_uri`", so the fallback silently breaks a documented
output contract: a consumer joining `base_uri` with an absolute `path`
gets a path that resolves nowhere. Erroring is the only honest answer,
because the correct root-relative path is not recoverable at that point.

Keep one copy, in `files.rs` because `scan.rs` is its child and shares
the helper, and make it return `Result`. Name it `strip_prefix` to match
`std::path::Path::strip_prefix`, whose fallibility is what a reader
expects.

`Error::internal` rather than `invalid_input`: the helper is private and
every input is derived internally, from `data_dir().join(..)`,
`deletion_file_path(..)`, `indices_dir().join(uuid)`, or a listing under
`base`. No user-supplied path reaches it, so an unmatched prefix is a
broken invariant rather than bad input, and the message should ask for a
bug report.

Seven of the eight production call sites build a keep-set or a reported
path, where dropping an entry is what causes damage, so they propagate
with `?`. The eighth, `cleanup_file_if_not_referenced`, classifies a
listed object as a deletion candidate, and there the safe answer is the
opposite: it logs and returns `Ok(None)`, the value the same function
already returns for every other path shape it cannot classify. Aborting
there would strand a pass that `remove_stream` has already partly
executed, leaving old manifests behind while some of the data files they
reference are gone.

Covered by `cleanup_skips_listed_files_outside_the_dataset_base`, which
installs a store wrapper that reports a rewritten location for the data
files and asserts the pass skips the unclassifiable entry and still
completes. Restoring `?` at that call site makes it fail. `cargo fmt --all
--check`, `cargo clippy -p lance --all-targets -- -D warnings`, and
`RUSTDOCFLAGS="-D warnings" cargo doc -p lance --no-deps` are clean;
`dataset::files` (20) and `dataset::cleanup` (43) pass. The full suite is
left to CI.
Make the skipped-file condition visible at the default log level without
flooding it: the per-file detail stays at `debug!`, and
`delete_unreferenced_files` warns once with a count. A store that
misreports one path misreports every path of that kind, so a per-file
`warn!` would print one line per object on an affected dataset.

Strengthen `cleanup_skips_listed_files_outside_the_dataset_base` so it
discriminates the change it is named for. Rewriting listed data-file
locations to `elsewhere/{filename}` did not: with the removed
absolute-path fallback that value fails the `starts_with("data")` check
and lands in the pre-existing "a .lance file outside the data directory
is left alone" arm, so every assertion held either way. It now rewrites
to `data/outside-base/{filename}`, which is outside the base and still
reads as a collectable data path. The rewritten entry is also stamped a
day before the epoch instead of carrying the fixture's own write time,
which had left it sitting exactly on the `unmodified_since` cutoff and
surviving only because the comparison is `<=`.

Add a test that `build_all_files_batch` refuses a location outside the
base rather than emitting an absolute path, and a near-miss sibling case
pinning that the match is per path segment. Document the new failure mode
on `all_files` and `tracked_files`, whose `path` column is documented as
relative to `base_uri`. Correct `listed_metas`'s doc comment, which
described what the cleanup pass classifies rather than what the helper
returns. Restore the `Vec` reservation that the `extend`-to-`push`
conversion dropped.
@LuciferYang
LuciferYang force-pushed the fix/strip-prefix-fails-closed branch from 5c10f1e to e6f9990 Compare August 24, 2026 14:30
@lance-gatekeeper lance-gatekeeper Bot removed K-approved Latest Gatekeeper recommendation permits acceptance. K-risk Latest Gatekeeper recommendation includes a non-blocking risk. labels Aug 24, 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 Aug 24, 2026
@lance-gatekeeper lance-gatekeeper Bot removed the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 1, 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 merge from main preserves the reviewed patch exactly. The new exact-version cleanup policy still feeds the same manifest-processing and path-classification boundaries, and the merged OpenDAL normalization remains effective, so the fail-closed inventory and cleanup behavior remains sound.

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

Copy link
Copy Markdown
Contributor Author

The red windows-build is not this diff. It fails in LocalSpillStore::default, reached from Session::new, with Access is denied creating a temp directory on the runner, in a conflict_resolver test unrelated to these three files. Filed as #8948.

@LuciferYang

Copy link
Copy Markdown
Contributor Author

friendly ping @Xuanwo

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

Labels

bug Something isn't working K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants