Skip to content

fix(python): added cleanup_frag_reuse_index#7221

Open
gstamatakis95 wants to merge 14 commits into
lance-format:mainfrom
gstamatakis95:feat/python-cleanup-frag-reuse-index
Open

fix(python): added cleanup_frag_reuse_index#7221
gstamatakis95 wants to merge 14 commits into
lance-format:mainfrom
gstamatakis95:feat/python-cleanup-frag-reuse-index

Conversation

@gstamatakis95

@gstamatakis95 gstamatakis95 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Closes #7220

Expose the existing Rust frag-reuse pruner to Python as LanceDataset.cleanup_frag_reuse_index().

Deferred-remap compaction appends a generation to the __lance_frag_reuse index on every commit, and without pruning these accumulate forever and are loaded on every vector index open. The race-safe pruner from #3836 already exists in Rust but had no Python binding and is never invoked automatically.

The binding follows the migrate_manifest_paths_v2 wrapper pattern, the Python method is documented beside cleanup_old_versions as a periodic maintenance call, and a new test verifies that __lance_frag_reuse reports zero versions once all indexes have caught up and the cleanup runs.

@github-actions github-actions Bot added A-python Python bindings bug Something isn't working labels Jun 10, 2026
@gstamatakis95
gstamatakis95 force-pushed the feat/python-cleanup-frag-reuse-index branch from f2eaabb to d90747b Compare June 10, 2026 22:44

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. The new Python cleanup entry point can panic when fragment-reuse metadata cannot be loaded. Corrupt or stale metadata can surface as a panic instead of a normal Python exception.
  2. The current test only exercises the already-caught-up index case. A cleanup implementation that drops all generations would still pass, so the regression can reappear.

@gstamatakis95

gstamatakis95 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author
  1. The new Python cleanup entry point can panic when fragment-reuse metadata cannot be loaded. Corrupt or stale metadata can surface as a panic instead of a normal Python exception.
  2. The current test only exercises the already-caught-up index case. A cleanup implementation that drops all generations would still pass, so the regression can reappear.

Thanks for the feedback @Xuanwo, addressed both issues.

@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.17647% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/index/frag_reuse.rs 72.50% 6 Missing and 5 partials ⚠️
rust/lance/src/dataset/index/frag_reuse.rs 96.92% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@gstamatakis95

Copy link
Copy Markdown
Contributor Author

Also moved to dataset.optimize which IMO is a better fit for this.

Ok(true) => {}
Ok(false) => all_caught_up = false,
// An InvalidInput error means the reuse version is likely corrupted; drop it.
Err(Error::InvalidInput { .. }) => continue 'versions,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The cleanup path treats a partially indexed rewrite group as corrupt and drops the reuse generation, while the load path treats the same straddling state as recoverable. Calling the new Python cleanup API on an older dataset can remove the generation that keeps affected indexes safe.

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.

Good catch, fixed.

.await
.unwrap();
// Surface corrupt/stale metadata as a normal error instead of panicking on unwrap.
let frag_reuse_details = load_frag_reuse_index_details(dataset, frag_reuse_index_meta).await?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The cleanup path still reads external fragment-reuse details with unchecked offset/size arithmetic. Malformed metadata can still panic or produce an invalid object-store range instead of returning a Python exception.

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.

Fixed in load_frag_reuse_index_details. The read now converts offset and size with usize::try_from so overflow errors instead of truncating, computes the end with checked_add so it errors instead of wrapping, and stats the file via reader.size() to reject any range past EOF before calling get_range.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Python API for pruning obsolete fragment-reuse generations, strengthens Rust cleanup retention and error handling, validates external fragment-reuse detail ranges, and rejects invalid scanner batch readahead values.

Changes

Fragment-Reuse Cleanup and Validation

Layer / File(s) Summary
Python cleanup API
python/python/lance/dataset.py, python/python/lance/lance/__init__.pyi, python/src/dataset.rs, python/python/tests/test_optimize.py
Exposes cleanup_frag_reuse_index() through the optimizer, Python binding, and type declarations; tests retention before index rebuild and removal afterward.
Cleanup retention and error semantics
rust/lance/src/dataset/index/frag_reuse.rs
Retains versions for behind or straddling indexes, propagates metadata errors, and tests caught-up evaluation and cleanup behavior.
External details range validation
rust/lance/src/index/frag_reuse.rs
Checks conversions, arithmetic, and external file bounds, with tests for malformed and overflowing ranges.
Scanner input validation
python/python/lance/dataset.py, python/python/tests/test_dataset.py
Requires batch_readahead to be a positive non-boolean integer and tests rejected input types.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DatasetOptimizer
  participant PythonDataset
  participant FragReuseCleanup
  participant FragReuseIndex
  DatasetOptimizer->>PythonDataset: cleanup_frag_reuse_index()
  PythonDataset->>FragReuseCleanup: clone dataset and invoke cleanup
  FragReuseCleanup->>FragReuseIndex: evaluate reuse generations against all indexes
  FragReuseIndex-->>FragReuseCleanup: retain or remove generations
  FragReuseCleanup-->>PythonDataset: cleaned dataset or error
  PythonDataset-->>DatasetOptimizer: None or propagated error
Loading

Possibly related PRs

  • lance-format/lance#7774: Changes when deferred compaction creates fragment-reuse index versions, which affects cleanup behavior.

Suggested reviewers: bubblecal, wjones127, xuanwo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The batch_readahead validation and test are unrelated to frag-reuse cleanup and appear outside the stated objective. Split the batch_readahead validation into a separate PR unless it belongs to a linked issue or is required by this change.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding cleanup_frag_reuse_index for Python.
Description check ✅ Passed The description is directly related to exposing and documenting cleanup_frag_reuse_index in Python.
Linked Issues check ✅ Passed The PR exposes the frag-reuse cleanup API, documents it as maintenance, and adds tests for the intended pruning behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/index/frag_reuse.rs`:
- Around line 336-339: Update the regression test around
cleanup_frag_reuse_index to capture its returned error and assert both the
expected error variant and identifying message content, rather than only calling
is_err(). Use the existing error type and message produced for corrupt
frag-reuse metadata so unrelated failures cannot satisfy the test.

In `@rust/lance/src/index/frag_reuse.rs`:
- Around line 236-285: Refactor
test_load_external_details_malformed_offset_size_errors into an rstest
parameterized test, with named cases #[case::out_of_bounds(...)] for
payload-length-plus-one and #[case::offset_past_eof(...)] for the past-EOF
offset, plus #[case::overflow(...)] for u64::MAX values. Pass each case’s offset
and size to external_details_meta, retain the shared setup and error assertions,
and adjust the expected-message assertion so it applies only to the
out-of-bounds case if needed.
- Around line 56-86: Update the final two malformed-input test cases around the
fragment reuse validation tests to assert the specific error variant and
message, rather than only checking is_err(). Cover the expected errors for the
external file range overflow and out-of-bounds conditions, using the relevant
fragment reuse test function and error construction paths as references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e26bc62-db67-4690-b162-5fc9673f2066

📥 Commits

Reviewing files that changed from the base of the PR and between 1ce1764 and d7fd04a.

📒 Files selected for processing (6)
  • python/python/lance/dataset.py
  • python/python/lance/lance/__init__.pyi
  • python/python/tests/test_optimize.py
  • python/src/dataset.rs
  • rust/lance/src/dataset/index/frag_reuse.rs
  • rust/lance/src/index/frag_reuse.rs

Comment thread rust/lance/src/dataset/index/frag_reuse.rs
Comment thread rust/lance/src/index/frag_reuse.rs
Comment thread rust/lance/src/index/frag_reuse.rs Outdated
@gstamatakis95
gstamatakis95 force-pushed the feat/python-cleanup-frag-reuse-index branch from d7fd04a to b26e9ac Compare July 10, 2026 19:18

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/index/frag_reuse.rs`:
- Line 44: The retained_versions buffer is initialized without pre-allocating
known capacity. In the relevant fragment-reuse logic, change its initialization
to Vec::with_capacity(frag_reuse_details.versions.len()) before the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 432d9243-56c5-4cd5-b7f6-00773076a5a9

📥 Commits

Reviewing files that changed from the base of the PR and between d7fd04a and b26e9ac.

📒 Files selected for processing (6)
  • python/python/lance/dataset.py
  • python/python/lance/lance/__init__.pyi
  • python/python/tests/test_optimize.py
  • python/src/dataset.rs
  • rust/lance/src/dataset/index/frag_reuse.rs
  • rust/lance/src/index/frag_reuse.rs

Comment thread rust/lance/src/dataset/index/frag_reuse.rs Outdated
@gstamatakis95

Copy link
Copy Markdown
Contributor Author

@Xuanwo I think this is ready.

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
python/python/lance/dataset.py-6284-6290 (1)

6284-6290: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-integral batch_readahead values at this boundary.

int(nbatches) is used only for validation, but the original value is stored. Therefore values such as 1.5 or "1" can pass this check and fail later when the native scanner expects an integer. Validate integer semantics and store the normalized value.

As per coding guidelines, API boundaries must reject invalid values with descriptive errors and must not silently adjust them.

Proposed fix
-        if nbatches is not None and int(nbatches) <= 0:
-            raise ValueError("batch_readahead must be greater than 0")
+        if nbatches is not None:
+            if not isinstance(nbatches, int):
+                raise TypeError("batch_readahead must be an integer")
+            if nbatches <= 0:
+                raise ValueError("batch_readahead must be greater than 0")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/python/lance/dataset.py` around lines 6284 - 6290, Update the
batch_readahead validation in the shown setter to reject non-integral values,
including numeric strings and fractional numbers, with a descriptive ValueError.
Preserve None as allowed, validate positive integer semantics without silently
coercing or rounding, and store the normalized integer value after validation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Other comments:
In `@python/python/lance/dataset.py`:
- Around line 6284-6290: Update the batch_readahead validation in the shown
setter to reject non-integral values, including numeric strings and fractional
numbers, with a descriptive ValueError. Preserve None as allowed, validate
positive integer semantics without silently coercing or rounding, and store the
normalized integer value after validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 10a2fa99-8691-4ae8-8045-6bcff990d06a

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec8936 and 64e8031.

📒 Files selected for processing (4)
  • python/python/lance/dataset.py
  • python/python/lance/lance/__init__.pyi
  • python/python/tests/test_optimize.py
  • python/src/dataset.rs

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

Labels

A-python Python bindings bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No Python API to prune frag-reuse generations, so deferred-remap users accumulate read-path debt forever

2 participants