Skip to content

feat(compaction): compact fragments over an overlay-count limit#7772

Merged
wjones127 merged 4 commits into
mainfrom
will/oss-1326-min-overlay-compaction
Jul 15, 2026
Merged

feat(compaction): compact fragments over an overlay-count limit#7772
wjones127 merged 4 commits into
mainfrom
will/oss-1326-min-overlay-compaction

Conversation

@wjones127

@wjones127 wjones127 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

A minimal, first slice of overlay-aware compaction (OSS-1326), stacked on #7536 (OSS-1324, will/oss-1324-take-can-read-overlays) — review against that base.

What it does

Adds a max_overlays_per_fragment: Option<usize> option to CompactionOptions. When a fragment carries more than this many data overlay files, the planner marks it CompactItself, so compact_files fully rewrites it into a fresh fragment with its overlays and deletions materialized into the base data. A fragment at or below the limit is not a candidate on this basis.

The read side needs no new machinery: compaction already scans each fragment through the normal read path, which (per #7536) resolves overlays and applies deletions, so the merged post-image is what gets written to the new base file.

Index correctness

A full rewrite removes the overlays, and with them the staleness signal the query path uses to mask an index older than an overlay. So the Rewrite commit now drops the rewritten fragment from the coverage of any index left stale by those overlays — field-aware (only indices covering a field an overlay actually supplied) and version-gated (overlay.committed_version > index.dataset_version). Those rows fall back to a flat scan until reindex; an index is never left serving stale values. Non-stale indices (built at/after the overlay, or on un-overlaid fields) are remapped normally.

This reuses the existing Operation::Rewrite path — no new persisted operation or conflict-matrix change.

Scope / follow-ups

Only the "fully compact the fragment" strategy is implemented. Overlay→overlay merge, in-place overlay→base folds (column rewrite preserving row addresses), and rebuild-in-place index reconciliation are left as follow-ups.

Tests

  • Planner/e2e (optimize.rs): overlay count over the limit triggers a full compaction (fresh single-file fragment, overlays cleared, values materialized); at-or-below limit is a no-op; deletions are materialized alongside overlays; a stale scalar index is dropped from the compacted fragment's coverage and the indexed query stays correct.
  • Unit (transaction.rs): prune_overlay_stale_fields_from_indices is field-aware and version-gated (stale index dropped; index at/after the overlay kept; index on an un-overlaid field untouched).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a configurable overlay-count trigger for compaction; fragments exceeding the limit are compacted into standalone form.
    • Default limit is 10 overlays per fragment, configurable via lance.compaction.max_overlays_per_fragment (set to none to disable).
  • Bug Fixes

    • Prevented stale index coverage from returning outdated values after overlay/materialization compaction.
    • Improved accuracy by pruning rewritten fragment coverage from older index versions, causing queries to fall back to scans where needed.
  • Tests

    • Added coverage for the overlay trigger and index-staleness pruning behavior.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Compaction options now support overlay-count thresholds that trigger standalone compaction. Manifest rewrites prune stale index coverage for materialized overlay fields, with tests covering threshold behavior, deletion materialization, and index reconciliation.

Changes

Overlay compaction and index reconciliation

Layer / File(s) Summary
Configure and plan overlay compaction
rust/lance/src/dataset/optimize.rs
Adds and parses max_overlays_per_fragment, defaults it to Some(10), and forces CompactItself when a fragment exceeds the threshold. Tests cover configuration, threshold boundaries, deletion materialization, and scalar index queries.
Prune stale index coverage during rewrites
rust/lance/src/dataset/transaction.rs
Manifest rewrite handling removes rewritten fragment IDs from indices whose indexed fields are older than materialized overlays, while retaining fresh and unrelated index coverage.

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

Suggested reviewers: jackye1995, westonpace, xuanyu-z

Sequence Diagram(s)

sequenceDiagram
  participant DatasetConfig
  participant CompactionOptions
  participant DefaultCompactionPlanner
  participant Transaction
  participant IndexMetadata
  DatasetConfig->>CompactionOptions: parse overlay threshold
  CompactionOptions->>DefaultCompactionPlanner: provide max_overlays_per_fragment
  DefaultCompactionPlanner->>Transaction: rewrite fragments with excessive overlays
  Transaction->>IndexMetadata: prune stale rewritten-fragment coverage
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: compaction now triggers when a fragment exceeds an overlay-count limit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch will/oss-1326-min-overlay-compaction

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

@github-actions github-actions Bot added the enhancement New feature or request label Jul 14, 2026
Comment thread rust/lance/src/dataset/optimize.rs Outdated
@wjones127
wjones127 marked this pull request as ready for review July 14, 2026 18:48

@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: 2

🤖 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/optimize.rs`:
- Around line 8074-8106: Update the id_val_map helper to use
scanner.try_into_batch() instead of try_into_stream().try_collect(), then
process the returned single RecordBatch directly while preserving the existing
id-to-value mapping behavior.
- Around line 7946-7969: Update the test helper create_base_dataset to construct
the batch with arrow_array’s record_batch! macro, replacing the manual
ArrowSchema, Arc, and RecordBatch::try_new setup. Preserve the existing id and
val columns and values, then continue using the resulting batch with
RecordBatchIterator and Dataset::write unchanged.
🪄 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: 4b224d4d-3ae2-4788-a9e7-86c9335b97f8

📥 Commits

Reviewing files that changed from the base of the PR and between a1bce9a and e78fadf.

📒 Files selected for processing (2)
  • rust/lance/src/dataset/optimize.rs
  • rust/lance/src/dataset/transaction.rs

Comment thread rust/lance/src/dataset/optimize.rs
Comment thread rust/lance/src/dataset/optimize.rs
@wjones127
wjones127 requested a review from westonpace July 14, 2026 19:50

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

This is actually surprisingly straightforward (except for the fragment bitmap pruning)

That being said, this means overlays will eventually trigger a full-row rewrite. This is true even if the overlays all affect a single field. In those cases it seems it would be more efficient to compact the overlays through some kind of data replacement operation wouldn't it?

Perhaps that is an optimization that can come later.

Comment thread rust/lance/src/dataset/optimize.rs Outdated
Comment on lines +265 to +271
/// Maximum number of data overlay files a fragment may carry before it is
/// fully compacted. When set, any fragment with more than this many overlays
/// is rewritten into a fresh fragment with its overlays (and deletions)
/// materialized into the base data, dropping the fragment from any index
/// left stale by those overlays.
/// Defaults to `Some(10)`. Set to `None` to disable the overlay-count
/// trigger entirely.

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.

Can I set this to 0 to force all overlays to be folded in? If so, we should maybe mention that.

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.

Yes — the check is overlays.len() > max, so Some(0) marks any fragment carrying at least one overlay as CompactItself. I updated the doc comment to call that out (and 828eb7d12 also lets the manifest config set "none" to disable the trigger, since the default is now Some(10)).

Comment on lines +2787 to +2793
/// After a `Rewrite` fully compacts a fragment, its data overlays are baked
/// into the new fragment's base data. An index built *before* one of those
/// overlays (`overlay.committed_version > index.dataset_version`) indexed the
/// stale pre-overlay values -- and unlike a live overlay, the compacted
/// fragment no longer signals that staleness to the query path. Drop each
/// rewritten (new) fragment from the coverage of any index covering a field
/// such an overlay supplied, so those rows fall back to a flat scan.

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.

I don't think you've done anything wrong, I'm just marveling at how complex this can get.

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.

Yeah, part of my thinks it would have been simpler if compaction and index updates were always done together. Then there's less sad intermediate states.

Base automatically changed from will/oss-1324-take-can-read-overlays to main July 15, 2026 17:35
wjones127 and others added 3 commits July 15, 2026 13:04
Add a `max_overlays_per_fragment` option to `CompactionOptions`. When a
fragment carries more than this many data overlay files, it is fully
compacted into a fresh fragment with its overlays (and deletions)
materialized into the base data.

Because the rewrite removes the overlays that mask stale index values at
query time, the `Rewrite` commit drops the compacted fragment from the
coverage of any index left stale by those overlays (field-aware,
version-gated via `committed_version > index.dataset_version`), so an
index is never left serving stale values; the rows fall back to a flat
scan until reindex.

Overlay->overlay merge and in-place folds are left as follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…dule

Address review feedback: move the overlay-compaction tests out of a separate
`overlay_compaction_tests` module into the existing `tests` module, and use the
`record_batch!` macro and `try_into_batch()` test idioms.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Enable the overlay-count compaction trigger by default so fragments that
accumulate more than 10 data overlay files are folded back into base data
without an explicit opt-in. Set to None to disable.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@wjones127
wjones127 force-pushed the will/oss-1326-min-overlay-compaction branch from 0243b3f to c24fee8 Compare July 15, 2026 20:13

@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: 4

🤖 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/optimize.rs`:
- Around line 270-271: Update the manifest configuration parser for the
overlay-count setting near the documented default and its parsing logic around
the referenced lines so it accepts an explicit "none" value as None, while
retaining integer parsing for enabled thresholds and rejecting invalid values.
Ensure removing the key still uses the default Some(10), and add tests covering
enabled, disabled, and invalid manifest values.
- Line 272: Avoid adding max_overlays_per_fragment directly to the public
CompactionOptions struct because downstream exhaustive struct literals would
break. Mark CompactionOptions as non-exhaustive, or expose this setting through
its existing builder/API while preserving compatibility with current struct
literals.

In `@rust/lance/src/dataset/transaction.rs`:
- Around line 2817-2821: Update handle_rewrite_fragments to resolve and retain
the fragment IDs assigned during manifest construction before reconciling
fragments and indexes, rather than rereading potentially stale IDs from
group.new_fragments. Ensure ID-0 cloned fragments use their assigned nonzero IDs
for index pruning, and add coverage for a rewrite using Fragment::new(0).
- Around line 2828-2832: Update both stale-index handling paths around is_stale
and fragment_bitmap so a stale index with unknown fragment coverage (None) is
conservatively assigned an empty bitmap or otherwise invalidated, while
retaining removal of rewritten fragment IDs for known coverage. Add a test
covering a stale index with a None fragment_bitmap and verify it cannot serve
the rewritten fragment.
🪄 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: 4b271a84-92e3-4b06-ab01-1af71f0d8a9b

📥 Commits

Reviewing files that changed from the base of the PR and between e78fadf and c24fee8.

📒 Files selected for processing (2)
  • rust/lance/src/dataset/optimize.rs
  • rust/lance/src/dataset/transaction.rs

Comment thread rust/lance/src/dataset/optimize.rs Outdated
/// left stale by those overlays.
/// Defaults to `Some(10)`. Set to `None` to disable the overlay-count
/// trigger entirely.
pub max_overlays_per_fragment: Option<usize>,

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline rust/lance/src/dataset/optimize.rs \
  --match CompactionOptions --view expanded
sed -n '185,280p' rust/lance/src/dataset/optimize.rs

Repository: lance-format/lance

Length of output: 6366


🏁 Script executed:

#!/bin/bash
sed -n '180,220p' rust/lance/src/dataset/optimize.rs
printf '\n---\n'
rg -n "#\[non_exhaustive\]|pub struct CompactionOptions|impl CompactionOptions|pub use .*CompactionOptions" rust/lance/src -S

Repository: lance-format/lance

Length of output: 2096


Avoid adding this public field directly CompactionOptions is public and not #[non_exhaustive], so this addition breaks downstream exhaustive struct literals. Make the type non-exhaustive or expose the setting through the existing builder/API instead.

🤖 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 `@rust/lance/src/dataset/optimize.rs` at line 272, Avoid adding
max_overlays_per_fragment directly to the public CompactionOptions struct
because downstream exhaustive struct literals would break. Mark
CompactionOptions as non-exhaustive, or expose this setting through its existing
builder/API while preserving compatibility with current struct literals.

Source: Coding guidelines

Comment on lines +2817 to +2821
let new_fragment_ids = group
.new_fragments
.iter()
.map(|f| f.id as u32)
.collect::<Vec<_>>();

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Prune using the fragment IDs assigned during manifest construction.

Line 2817 reads IDs from group.new_fragments, but handle_rewrite_fragments assigns an ID only to cloned fragments when the supplied ID is 0. The helper then removes ID 0, leaving the actual rewritten fragment covered by a stale index. Resolve IDs once before fragment and index reconciliation, and test a rewrite with Fragment::new(0).

Also applies to: 6483-6488

🤖 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 `@rust/lance/src/dataset/transaction.rs` around lines 2817 - 2821, Update
handle_rewrite_fragments to resolve and retain the fragment IDs assigned during
manifest construction before reconciling fragments and indexes, rather than
rereading potentially stale IDs from group.new_fragments. Ensure ID-0 cloned
fragments use their assigned nonzero IDs for index pruning, and add coverage for
a rewrite using Fragment::new(0).

Comment on lines +2828 to +2832
if is_stale && let Some(fragment_bitmap) = &mut index.fragment_bitmap {
for new_id in &new_fragment_ids {
fragment_bitmap.remove(*new_id);
}
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate stale indices whose fragment coverage is unknown.

At Line 2828, a stale index is changed only when fragment_bitmap is Some. None means unknown coverage, so the stale index may still serve the rewritten fragment. Conservatively set an empty bitmap—or derive known coverage—and add a None-bitmap test.

Conservative fix
- if is_stale && let Some(fragment_bitmap) = &mut index.fragment_bitmap {
-     for new_id in &new_fragment_ids {
-         fragment_bitmap.remove(*new_id);
+ if is_stale {
+     match &mut index.fragment_bitmap {
+         Some(fragment_bitmap) => {
+             for new_id in &new_fragment_ids {
+                 fragment_bitmap.remove(*new_id);
+             }
+         }
+         None => {
+             index.fragment_bitmap = Some(RoaringBitmap::new());
+         }
      }
  }

Also applies to: 6490-6500

🤖 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 `@rust/lance/src/dataset/transaction.rs` around lines 2828 - 2832, Update both
stale-index handling paths around is_stale and fragment_bitmap so a stale index
with unknown fragment coverage (None) is conservatively assigned an empty bitmap
or otherwise invalidated, while retaining removal of rewritten fragment IDs for
known coverage. Add a test covering a stale index with a None fragment_bitmap
and verify it cannot serve the rewritten fragment.

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.39210% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/dataset/optimize.rs 99.63% 1 Missing ⚠️
rust/lance/src/dataset/transaction.rs 98.18% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The overlay-count trigger defaults to Some(10), so removing the manifest
key leaves it enabled — there was no way to disable it through config.
Accept an explicit "none" (case-insensitive) to set it to None, and note
that Some(0) folds in every fragment carrying any overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rust/lance/src/dataset/optimize.rs (2)

8318-8327: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use named RecordBatch columns at both test sites.

Both sites depend on projection order instead of the column contract:

  • rust/lance/src/dataset/optimize.rs#L8318-L8327: replace positional access with batch["id"] and batch["val"].
  • rust/lance/src/dataset/optimize.rs#L8495-L8500: replace batch.column(0) with batch["id"].

As per coding guidelines, tests should use batch["column_name"] for RecordBatch access.

🤖 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 `@rust/lance/src/dataset/optimize.rs` around lines 8318 - 8327, Update both
test sites in rust/lance/src/dataset/optimize.rs: at lines 8318-8327, access the
RecordBatch columns by name using batch["id"] and batch["val"] instead of
positional column(0)/column(1); at lines 8495-8500, replace batch.column(0) with
batch["id"].

Source: Coding guidelines


8378-8393: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add execution-level coverage for Some(0) and None.

The implementation documents that Some(0) compacts any overlaid fragment and None disables the trigger, but planner tests only exercise threshold 2. Add tests proving both boundary behaviors end-to-end.

As per coding guidelines, “All Rust bug fixes and features must include corresponding tests.”

🤖 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 `@rust/lance/src/dataset/optimize.rs` around lines 8378 - 8393, Add
execution-level tests alongside test_below_threshold_is_a_noop covering
overlay_only_options(0) and a None threshold. Verify Some(0) compacts an
overlaid fragment, while None leaves the dataset unchanged and performs no
compaction, using the existing metrics and fragment-overlay assertions.

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.

Inline comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 6544-6551: Update the invalid-value test for
CompactionOptions::from_dataset_config to capture the returned error, assert
that it matches Error::InvalidInput, and then verify the existing
max_overlays_per_fragment and not_a_number message contents.

---

Outside diff comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 8318-8327: Update both test sites in
rust/lance/src/dataset/optimize.rs: at lines 8318-8327, access the RecordBatch
columns by name using batch["id"] and batch["val"] instead of positional
column(0)/column(1); at lines 8495-8500, replace batch.column(0) with
batch["id"].
- Around line 8378-8393: Add execution-level tests alongside
test_below_threshold_is_a_noop covering overlay_only_options(0) and a None
threshold. Verify Some(0) compacts an overlaid fragment, while None leaves the
dataset unchanged and performs no compaction, using the existing metrics and
fragment-overlay assertions.
🪄 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: 463e2d63-156a-4bf7-a9ca-01495c8e56a6

📥 Commits

Reviewing files that changed from the base of the PR and between c24fee8 and 828eb7d.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

Comment thread rust/lance/src/dataset/optimize.rs
@wjones127
wjones127 merged commit 6b15a8e into main Jul 15, 2026
36 checks passed
@wjones127
wjones127 deleted the will/oss-1326-min-overlay-compaction branch July 15, 2026 22:49
wjones127 added a commit that referenced this pull request Jul 21, 2026
A minimal, first slice of overlay-aware compaction (OSS-1326), stacked
on #7536 (OSS-1324, `will/oss-1324-take-can-read-overlays`) — review
against that base.

## What it does

Adds a `max_overlays_per_fragment: Option<usize>` option to
`CompactionOptions`. When a fragment carries **more than** this many
data overlay files, the planner marks it `CompactItself`, so
`compact_files` fully rewrites it into a fresh fragment with its
overlays **and** deletions materialized into the base data. A fragment
at or below the limit is not a candidate on this basis.

The read side needs no new machinery: compaction already scans each
fragment through the normal read path, which (per #7536) resolves
overlays and applies deletions, so the merged post-image is what gets
written to the new base file.

### Index correctness

A full rewrite removes the overlays, and with them the staleness signal
the query path uses to mask an index older than an overlay. So the
`Rewrite` commit now drops the rewritten fragment from the coverage of
any index left stale by those overlays — **field-aware** (only indices
covering a field an overlay actually supplied) and **version-gated**
(`overlay.committed_version > index.dataset_version`). Those rows fall
back to a flat scan until reindex; an index is never left serving stale
values. Non-stale indices (built at/after the overlay, or on un-overlaid
fields) are remapped normally.

This reuses the existing `Operation::Rewrite` path — no new persisted
operation or conflict-matrix change.

## Scope / follow-ups

Only the "fully compact the fragment" strategy is implemented.
Overlay→overlay merge, in-place overlay→base folds (column rewrite
preserving row addresses), and rebuild-in-place index reconciliation are
left as follow-ups.

## Tests

- Planner/e2e (`optimize.rs`): overlay count over the limit triggers a
full compaction (fresh single-file fragment, overlays cleared, values
materialized); at-or-below limit is a no-op; deletions are materialized
alongside overlays; a stale scalar index is dropped from the compacted
fragment's coverage and the indexed query stays correct.
- Unit (`transaction.rs`): `prune_overlay_stale_fields_from_indices` is
field-aware and version-gated (stale index dropped; index at/after the
overlay kept; index on an un-overlaid field untouched).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a configurable overlay-count trigger for compaction; fragments
exceeding the limit are compacted into standalone form.
* Default limit is 10 overlays per fragment, configurable via
`lance.compaction.max_overlays_per_fragment` (set to `none` to disable).

* **Bug Fixes**
* Prevented stale index coverage from returning outdated values after
overlay/materialization compaction.
* Improved accuracy by pruning rewritten fragment coverage from older
index versions, causing queries to fall back to scans where needed.

* **Tests**
* Added coverage for the overlay trigger and index-staleness pruning
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
(cherry picked from commit 6b15a8e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants