fix(index): keep data overlays masked through OptimizeIndices#7924
fix(index): keep data overlays masked through OptimizeIndices#7924wjones127 wants to merge 1 commit into
Conversation
Overlay index masking gates on `overlay.committed_version > segment.dataset_version`. The merge path of `OptimizeIndices` carries an old segment's stale pre-overlay entries without re-reading overlaid values, but stamped the merged segment with the current `dataset_version`. That flipped the gate off and un-masked the overlay: a stale index entry resurfaced and the overlaid value was dropped. This affected every index type (BTREE, BITMAP, FTS, IVF vector). Treat an overlay-stale fragment (touched by a field-relevant overlay committed after the segment was built) like a compaction-retired one during merge, mirroring `prune_overlay_stale_fields_from_indices`. The new `segment_coverage_split` drops such fragments from the merged segment's coverage; scalar types (BTree/bitmap/label_list/FTS) additionally filter their old entries out via `OldIndexDataFilter`. Vector needs only the coverage exclusion because its ANN prefilter is already restricted to coverage (`DatasetPreFilter::new`), whereas the scalar `MaterializeIndexExec` prefilter is not. Pruned fragments fall to the flat path and read current (overlay-merged) values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughIndex append and merge logic now treats overlay-stale fragments as retired for scalar, FTS, and vector indexes. New regression tests verify stale entries remain masked after index optimization. ChangesOverlay index masking
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Dataset
participant OverlayState
participant IndexMerge
participant Query
Dataset->>OverlayState: identify overlay-stale fragments
Dataset->>IndexMerge: optimize index segments
IndexMerge->>OverlayState: classify stale fragments as retired
IndexMerge->>Query: publish merged index without stale coverage
Query->>Dataset: resolve overlaid values through flat path
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
🧹 Nitpick comments (3)
rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs (2)
1095-1098: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove inline
usestatements to the top of the file.
use crate::index::DatasetIndexExt;,use lance_index::optimize::OptimizeOptions;, anduse lance_index::scalar::BuiltinIndexType;are declared inside the test function body. The same pattern repeats intest_optimize_preserves_fts_overlay_masking(lines 1152-1153) andtest_optimize_preserves_vector_overlay_masking(lines 1220-1221). Hoist these to the file-level imports.As per coding guidelines, "Place
useimports at the top of the file, not inline within function bodies."♻️ Proposed fix
-async fn test_optimize_preserves_scalar_overlay_masking(#[case] index_type: IndexType) { - use crate::index::DatasetIndexExt; - use lance_index::optimize::OptimizeOptions; - use lance_index::scalar::BuiltinIndexType; - +async fn test_optimize_preserves_scalar_overlay_masking(#[case] index_type: IndexType) {(and add the three
usestatements near the top of the file, alongside the corresponding removals in the other two 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/tests/dataset_overlay_index_masking.rs` around lines 1095 - 1098, Hoist DatasetIndexExt, OptimizeOptions, and BuiltinIndexType into the file-level imports, then remove their inline use statements from test_optimize_preserves_scalar_overlay_masking, test_optimize_preserves_fts_overlay_masking, and test_optimize_preserves_vector_overlay_masking.Source: Coding guidelines
1216-1286: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing regression coverage for the stable-row-id path.
All three new regression tests exercise only
uses_stable_row_ids() == false(this test explicitly passescreate_vector_overlay_dataset(false); the scalar/FTS tests don't setenable_stable_row_idseither). Inappend.rs,build_old_data_filtertakes a different code path when stable row IDs are enabled (build_stable_row_id_filter, which derives its allow-list purely fromeffective_old_frags) — this branch is exactly where the overlay-stale exclusion fromsegment_coverage_splitneeds to propagate correctly, and it's untested by this PR.Since
create_vector_overlay_datasetalready accepts astable_row_idsflag, consider parameterizing at least one of these tests (or adding a stable-row-id variant) to cover that branch too.🤖 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/tests/dataset_overlay_index_masking.rs` around lines 1216 - 1286, Extend the regression coverage around test_optimize_preserves_vector_overlay_masking to also run with create_vector_overlay_dataset(true), exercising the stable-row-ID branch in build_old_data_filter and build_stable_row_id_filter. Preserve the existing assertions that the stale id=35 remains excluded and overlaid id=40 remains discoverable after optimization, while keeping the non-stable case covered.rust/lance/src/index/append.rs (1)
439-442: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRedundant coverage-split computation for BTree merges.
split_segment_coverage(line 441-442) already callssegment_coverage_split(and thusoverlaid_fragments(&dataset.manifest.fragments)) once per segment inselected_old_indicesto build the aggregate. For theIndexType::BTreebranch,build_per_segment_filters(line 516-517) then re-runsoverlaid_fragmentsandsegment_coverage_splitfor the same segments to get the per-segment filters —retired_old_fragsfrom line 441 is never even used on this branch. TheInvertedmerge path below (lines 900-912) shows the more efficient pattern: computeoverlaid_fragmentsonce and loopsegment_coverage_splitmanually to get both per-segment and aggregated results in one pass.Consider threading the already-computed
overlaidmap (or the per-segment splits) through tobuild_per_segment_filtersto avoid the duplicateO(fragments)scan and duplicate per-segment bitmap work on every BTree merge.Also applies to: 514-527
🤖 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/index/append.rs` around lines 439 - 442, Avoid recomputing segment coverage for BTree merges: update the flow around split_segment_coverage and build_per_segment_filters to reuse the already computed overlaid fragment map or per-segment coverage splits for selected_old_indices. Preserve the existing aggregate coverage behavior and per-segment filter results while eliminating the second overlaid_fragments and segment_coverage_split pass; keep the Inverted merge path’s single-pass pattern as the model.
🤖 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.
Nitpick comments:
In `@rust/lance/src/dataset/tests/dataset_overlay_index_masking.rs`:
- Around line 1095-1098: Hoist DatasetIndexExt, OptimizeOptions, and
BuiltinIndexType into the file-level imports, then remove their inline use
statements from test_optimize_preserves_scalar_overlay_masking,
test_optimize_preserves_fts_overlay_masking, and
test_optimize_preserves_vector_overlay_masking.
- Around line 1216-1286: Extend the regression coverage around
test_optimize_preserves_vector_overlay_masking to also run with
create_vector_overlay_dataset(true), exercising the stable-row-ID branch in
build_old_data_filter and build_stable_row_id_filter. Preserve the existing
assertions that the stale id=35 remains excluded and overlaid id=40 remains
discoverable after optimization, while keeping the non-stable case covered.
In `@rust/lance/src/index/append.rs`:
- Around line 439-442: Avoid recomputing segment coverage for BTree merges:
update the flow around split_segment_coverage and build_per_segment_filters to
reuse the already computed overlaid fragment map or per-segment coverage splits
for selected_old_indices. Preserve the existing aggregate coverage behavior and
per-segment filter results while eliminating the second overlaid_fragments and
segment_coverage_split pass; keep the Inverted merge path’s single-pass pattern
as the model.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: QUIET
Plan: Pro Plus
Run ID: 6ef5ce11-c3be-49be-b16a-f50d343e336e
📒 Files selected for processing (2)
rust/lance/src/dataset/tests/dataset_overlay_index_masking.rsrust/lance/src/index/append.rs
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Problem
Overlay index masking (#7549) gates on
overlay.committed_version > segment.dataset_version. The merge path ofOptimizeIndicescarries an old segment's stale pre-overlay entries without re-reading overlaid values, yet stamped the merged segment with the currentdataset_version. That flips the gate off and un-masks the overlay: the stale index entry resurfaces and the overlaid value is dropped.Confirmed for every index type — BTREE, BITMAP, FTS, and IVF vector. For example, after overlaying
age 10 → 999on an indexed row and then merging:age = 10returns the row again (stale entry resurfaced)age = 999returns nothing (overlaid value dropped)The delta/append path was already safe (keeps the old segment with its old version), as is rebuild/retrain-from-scan (re-reads and incorporates the overlay). Only merge-without-re-read was affected.
Fix
Treat an overlay-stale fragment — one touched by a field-relevant overlay committed after the segment was built — like a compaction-retired fragment during merge, mirroring the existing
prune_overlay_stale_fields_from_indicesfor compaction.segment_coverage_splitdrops overlay-stale fragments from the merged segment's coverage.OldIndexDataFilter.DatasetPreFilter::new→create_restricted_deletion_mask), whereas the scalarMaterializeIndexExecprefilter is not.Pruned fragments fall to the flat path and read current (overlay-merged) values, so both the stale-drop and the new-match come out correct.
Tests
test_optimize_preserves_*indataset_overlay_index_masking.rsreproduce the un-masking and assert it stays masked across BTREE, BITMAP, FTS, and IVF vector.🤖 Generated with Claude Code