Skip to content

feat(scanner): add external row-address mask prefilter#7288

Open
JulianYG wants to merge 8 commits into
lance-format:mainfrom
JulianYG:feat/row-addr-mask-prefilter
Open

feat(scanner): add external row-address mask prefilter#7288
JulianYG wants to merge 8 commits into
lance-format:mainfrom
JulianYG:feat/row-addr-mask-prefilter

Conversation

@JulianYG

@JulianYG JulianYG commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Addresses #6852.

What

Adds Scanner::with_row_addr_prefilter(RowAddrMask), letting callers pass a
precomputed row-address allow/block mask as a prefilter into vector and plain
scans, reusing the scanner's existing retrieval plan rather than re-deriving it.

Motivation

Some pipelines precompute a set of eligible rows out-of-band (e.g. a stored
bitmap of rows belonging to a logical subset / dataset) and want to run KNN or a
plain scan restricted to that set -- without expressing it as a SQL filter. A
multi-hundred-thousand-element IN (...) is impractical to build and parse;
passing the row set directly is far cheaper.

How

The mask threads into the existing prefilter machinery at three points:

  • ANN branch: fed through PreFilterSource into new_knn_exec, ANDed with
    any deletion/SQL prefilter via a MaskAndLoader.
  • Flat / unindexed-fragment branch: a new RowAddrMaskFilterExec filters
    scan output by _rowid, so rows appended after the index build are honored.
  • Plain (non-vector) scan: the mask is supplied as the FilteredReadExec
    index input, so only masked rows are read; a SQL filter becomes a refine on top.

Deletions are still applied by DatasetPreFilter; illegal addresses are ignored.

Status

Draft, pending API agreement on #6852. Behavior is exercised by an out-of-tree
PyO3 binding's test suite (based on v7.0.0, this PR is rebased); cargo check -p lance and cargo fmt are clean. I'd
appreciate guidance on the public API shape before finalizing in-tree tests.

@github-actions github-actions Bot added the enhancement New feature or request label Jun 16, 2026
@JulianYG
JulianYG marked this pull request as draft June 16, 2026 07:54
@JulianYG
JulianYG force-pushed the feat/row-addr-mask-prefilter branch from 19a9518 to ed88757 Compare July 10, 2026 04:15
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Changes

The scanner accepts an optional RowAddrMask and applies it to filtered reads, flat scans, appended fragments, ANN/KNN execution, and FTS queries. New execution support intersects prefilters and filters batches by _rowid. Tests cover masking, limits, legacy storage, ANN, and FTS behavior.

Row-address prefilter

Layer / File(s) Summary
Mask execution primitives
rust/lance/src/io/exec/row_addr_mask.rs, rust/lance/src/io/exec.rs, rust/lance/src/io/exec/utils.rs
Adds mask intersection, _rowid batch filtering, validation, tests, and execution-node exports.
Scanner filtered-read integration
rust/lance/src/dataset/scanner.rs, rust/lance/src/io/exec/filtered_read.rs
Adds the scanner builder and applies mask-derived inputs, refine-only filters, projection handling, limit behavior, and legacy-storage validation.
ANN and flat vector-search masking
rust/lance/src/io/exec/knn.rs, rust/lance/src/dataset/scanner.rs
Propagates the mask through ANN execution and filters flat or appended vector-search branches.
FTS mask propagation and validation
rust/lance/src/io/exec/fts.rs, rust/lance/src/dataset/scanner.rs
Passes masks into phrase and match prefilters and filters flat FTS results before top-k selection; adds end-to-end coverage.

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

Possibly related PRs

Suggested labels: A-index

Suggested reviewers: westonpace, xuanwo, bubblecal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the scanner and external row-address mask prefilter, matching the main change.
Description check ✅ Passed The description accurately explains the new row-address mask prefilter across plain, ANN, and FTS scans.
Docstring Coverage ✅ Passed Docstring coverage is 94.12% which is sufficient. The required threshold is 80.00%.
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 unit tests (beta)
  • Create PR with unit tests

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

@JulianYG
JulianYG marked this pull request as ready for review July 11, 2026 08:37
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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/scanner.rs`:
- Around line 737-741: The external_row_mask field and its ANN execution
plumbing should use Arc<RowAddrMask> instead of cloning RowAddrMask values.
Update new_knn_exec, ANNIvfSubIndexExec construction and with_new_children, plus
ann, multivec_ann, knn_combined, and flat-scan paths, to clone the Arc cheaply
while preserving mask behavior. Revise the field documentation to mention both
vector-search prefiltering and use as the plain-scan row source via
use_external_mask.
- Around line 1205-1214: The documentation for with_row_addr_prefilter must
match new_filtered_read: describe the mask as applying to both vector-search and
plain scans as implemented, rather than claiming it only affects nearest
queries. Reword “ANDed” using clear wording such as “combined with” to satisfy
the typos check, while preserving the existing row-address and branch semantics.
- Around line 1211-1214: Reject configurations using with_row_addr_prefilter
when the dataset manifest uses stable row IDs. Add an early invalid-input or
not-supported validation in validate_options or create_plan, before applying
external_row_mask, while preserving the existing behavior for datasets without
stable row IDs.

In `@rust/lance/src/io/exec/knn.rs`:
- Line 1393: Reword the documentation comment for the external row-address
allow/block mask to replace “ANDed” with a spell-check-safe equivalent, while
preserving that the mask is combined with the prefilter using logical AND.

In `@rust/lance/src/io/exec/row_addr_mask.rs`:
- Line 10: Replace the flagged “ANDed” wording in the row address mask
documentation and the corresponding occurrences in knn.rs and scanner.rs with
typos-safe terminology, preserving the description of deletion vectors being
combined by DatasetPreFilter. Alternatively, add an appropriate allow-entry to
the typos configuration if retaining the terminology is required.
🪄 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: 1f78fb0f-93fb-46cb-bcb6-510b6431ea01

📥 Commits

Reviewing files that changed from the base of the PR and between 1946a5a and ed88757.

📒 Files selected for processing (4)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec.rs
  • rust/lance/src/io/exec/knn.rs
  • rust/lance/src/io/exec/row_addr_mask.rs

Comment thread rust/lance/src/dataset/scanner.rs Outdated
Comment thread rust/lance/src/dataset/scanner.rs Outdated
Comment on lines +1211 to +1214
pub fn with_row_addr_prefilter(&mut self, mask: RowAddrMask) -> &mut Self {
self.external_row_mask = Some(mask);
self
}

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm stable-row-id detection API and that no guard exists for external_row_mask
rg -nP 'uses_stable_row_ids' rust/lance/src/dataset/scanner.rs rust/lance/src/dataset/*.rs
rg -nP 'external_row_mask' rust/lance/src/dataset/scanner.rs

Repository: lance-format/lance

Length of output: 2501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the scanner code around the external row mask, validation, and mask application paths.
sed -n '700,780p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
sed -n '1188,1235p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
sed -n '2888,2995p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
sed -n '3908,4110p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
sed -n '4728,4825p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
sed -n '2488,2535p' rust/lance/src/dataset/scanner.rs

Repository: lance-format/lance

Length of output: 23512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether scanner validation already rejects external_row_mask for stable-row-id datasets.
rg -nC 3 'validate_options|create_plan|external_row_mask|uses_stable_row_ids' rust/lance/src/dataset/scanner.rs

Repository: lance-format/lance

Length of output: 13474


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the row-address mask types and the exec path that consumes them.
rg -nC 4 'struct RowAddrMask|enum RowAddrMask|RowAddrMaskFilterExec|mask_as_index_input|RowAddr' rust/lance/src

Repository: lance-format/lance

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrowly inspect the RowAddrMask-related execution path to see what column it filters and
# whether scanner-specific validation already rejects stable-row-id datasets.
rg -nC 3 'RowAddrMaskFilterExec|mask_as_index_input|with_row_addr_prefilter|uses_stable_row_ids\(\)' rust/lance/src/dataset/scanner.rs rust/lance/src/index/prefilter.rs rust/lance/src -g '!**/target/**' | head -n 220

Repository: lance-format/lance

Length of output: 17644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the RowAddrMask-related execution path to see what column it filters and
# whether scanner-specific validation already rejects stable-row-id datasets.
sed -n '1,260p' rust/lance/src/index/prefilter.rs
printf '\n---\n'
sed -n '2388,2538p' rust/lance/src/dataset/scanner.rs
printf '\n---\n'
rg -nC 2 'RowAddrMaskFilterExec|mask_as_index_input|with_row_addr_prefilter' rust/lance/src -g '!**/target/**'

Repository: lance-format/lance

Length of output: 24350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the row-address mask execution semantics to see whether it always interprets
# the input as physical row addresses or can also operate on stable row ids.
sed -n '1,220p' rust/lance/src/io/exec/row_addr_mask.rs

Repository: lance-format/lance

Length of output: 5564


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' rust/lance/src/io/exec/row_addr_mask.rs

Repository: lance-format/lance

Length of output: 5564


Reject with_row_addr_prefilter on stable-row-id datasets. This path reads _rowid, which only matches physical row addresses when stable row ids are disabled. On manifest.uses_stable_row_ids() datasets it can silently filter the wrong rows, so add an early invalid_input/not_supported check in validate_options or create_plan.

🤖 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/scanner.rs` around lines 1211 - 1214, Reject
configurations using with_row_addr_prefilter when the dataset manifest uses
stable row IDs. Add an early invalid-input or not-supported validation in
validate_options or create_plan, before applying external_row_mask, while
preserving the existing behavior for datasets without stable row IDs.

Source: Coding guidelines

Comment thread rust/lance/src/io/exec/knn.rs Outdated
Comment thread rust/lance/src/io/exec/row_addr_mask.rs Outdated
@JulianYG
JulianYG force-pushed the feat/row-addr-mask-prefilter branch from ed88757 to 7c82510 Compare July 19, 2026 12:06
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/scanner.rs (1)

2927-3008: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Critical: external_row_mask is silently ignored on plain scans for legacy-storage datasets.

filtered_read (lines 3013-3050) dispatches to legacy_filtered_read when self.dataset.is_legacy_storage() is true, and legacy_filtered_read has no awareness of external_row_mask at all — it never builds an index_input from the mask and nothing wraps its output afterward. Contrast this with the vector-search paths (vector_search's flat fallback and knn_combined), which explicitly re-wrap their scan output with RowAddrMaskFilterExec regardless of which internal path executed. For the plain-scan path (create_planfiltered_read_sourcefiltered_read), there is no such fallback wrap — masking is enforced only through new_filtered_read's use_external_mask/mask_as_index_input mechanism.

The net effect: on a legacy-storage-format dataset, calling Scanner::with_row_addr_prefilter(mask) followed by a plain (non-nearest) scan will return unmasked results, silently contradicting the updated doc ("On a plain scan the mask is used directly as the row source.").

🐛 Proposed minimal safeguard (reject the unsupported combination)
     fn validate_options(&self) -> Result<()> {
         if self.batch_readahead == 0 {
             return Err(Error::invalid_input_source(
                 "batch_readahead must be greater than 0, got 0".into(),
             ));
         }
+
+        if self.external_row_mask.is_some()
+            && self.nearest.is_none()
+            && self.dataset.is_legacy_storage()
+        {
+            return Err(Error::not_supported(
+                "with_row_addr_prefilter is not supported for plain scans on datasets using the legacy storage format".to_string(),
+            ));
+        }

A more complete fix would be to implement mask support inside legacy_filtered_read (or to wrap filtered_read_source's output with RowAddrMaskFilterExec unconditionally, mirroring the vector-search paths) instead of just rejecting the combination.

🤖 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/scanner.rs` around lines 2927 - 3008, Prevent plain
scans from silently ignoring external_row_mask on legacy-storage datasets. In
filtered_read, detect the combination of self.dataset.is_legacy_storage() and
self.external_row_mask and return an explicit unsupported-operation error before
dispatching to legacy_filtered_read. Preserve existing behavior for legacy scans
without a mask and non-legacy masked scans through new_filtered_read.
🧹 Nitpick comments (2)
rust/lance/src/dataset/scanner.rs (2)

1210-1226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for the new external row-address mask feature.

This PR stack introduces a significant, correctness-sensitive feature (external RowAddrMask prefiltering) spanning three files, but no tests are visible in the diff for any layer: the core filtering primitives, the KNN mask-propagation wiring, or the public Scanner::with_row_addr_prefilter API and its downstream effects (plain scan, ANN, flat/appended-fragment vector search). Per coding guidelines, every feature should have corresponding tests.

  • rust/lance/src/dataset/scanner.rs#L1210-L1226: add tests exercising Scanner::with_row_addr_prefilter end-to-end for (a) a plain scan, (b) an indexed vector search, (c) a flat/unindexed vector search, and (d) appended-fragment vector search (knn_combined), verifying only masked rows are returned in each case.
  • rust/lance/src/io/exec/row_addr_mask.rs#L33-L156: add unit tests for apply_mask (null _rowid handling, allow/block selection) and MaskAndLoader::load (intersection with and without an inner loader).
  • rust/lance/src/io/exec/knn.rs#L1104-L1981: add a test verifying ANNIvfSubIndexExec::execute correctly intersects external_mask with an existing filter-derived prefilter loader (not just the no-prefilter case).
🤖 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/scanner.rs` around lines 1210 - 1226, Add
comprehensive tests for external RowAddrMask filtering: in
rust/lance/src/dataset/scanner.rs lines 1210-1226, cover
Scanner::with_row_addr_prefilter for plain scans, indexed vector searches,
flat/unindexed searches, and appended-fragment knn_combined searches, verifying
only masked rows return; in rust/lance/src/io/exec/row_addr_mask.rs lines
33-156, test apply_mask null _rowid handling and allow/block selection plus
MaskAndLoader::load with and without an inner loader; in
rust/lance/src/io/exec/knn.rs lines 1104-1981, test ANNIvfSubIndexExec::execute
intersects external_mask with an existing filter-derived prefilter loader.

Source: Coding guidelines


3052-3076: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate serialize/stream/OneShotExec boilerplate between mask_as_index_input and u64s_as_take_input.

Both functions build an IndexExprResult, compute fragments_covered/format, serialize into a batch, and wrap it in a OneShotExec via RecordBatchStreamAdapter — identical tail logic duplicated verbatim. Extracting a shared helper (e.g. fn index_result_as_index_input(&self, index_result: IndexExprResult) -> Result<Arc<dyn ExecutionPlan>>) would remove the duplication and reduce the risk of the two implementations diverging over time.

♻️ Proposed refactor
+    fn index_result_as_index_input(
+        &self,
+        index_result: IndexExprResult,
+    ) -> Result<Arc<dyn ExecutionPlan>> {
+        let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
+        let format = self.index_expr_result_format();
+        let batch = index_result.serialize(&fragments_covered, format)?;
+        let schema = batch.schema();
+        let stream = futures::stream::once(async move { Ok(batch) });
+        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
+        Ok(Arc::new(OneShotExec::new(stream)))
+    }
+
     fn mask_as_index_input(&self, mask: &RowAddrMask) -> Result<Arc<dyn ExecutionPlan>> {
-        let index_result = IndexExprResult::exact(mask.clone());
-        let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
-        let format = self.index_expr_result_format();
-        let batch = index_result.serialize(&fragments_covered, format)?;
-        let schema = batch.schema();
-        let stream = futures::stream::once(async move { Ok(batch) });
-        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
-        Ok(Arc::new(OneShotExec::new(stream)))
+        self.index_result_as_index_input(IndexExprResult::exact(mask.clone()))
     }

     fn u64s_as_take_input(&self, u64s: Vec<u64>) -> Result<Arc<dyn ExecutionPlan>> {
         let row_addrs = RowAddrTreeMap::from_iter(u64s);
         let row_addr_mask = RowAddrMask::from_allowed(row_addrs);
-        let index_result = IndexExprResult::exact(row_addr_mask);
-        let fragments_covered = self.dataset.fragment_bitmap.as_ref().clone();
-        let format = self.index_expr_result_format();
-        let batch = index_result.serialize(&fragments_covered, format)?;
-        let schema = batch.schema();
-        let stream = futures::stream::once(async move { Ok(batch) });
-        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
-        Ok(Arc::new(OneShotExec::new(stream)))
+        self.index_result_as_index_input(IndexExprResult::exact(row_addr_mask))
     }
🤖 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/scanner.rs` around lines 3052 - 3076, Extract the
shared serialization and execution-plan construction from mask_as_index_input
and u64s_as_take_input into a helper such as index_result_as_index_input,
accepting an IndexExprResult and returning the OneShotExec plan. Update both
callers to build their respective IndexExprResult and delegate to the helper,
preserving existing behavior.
🤖 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.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 2927-3008: Prevent plain scans from silently ignoring
external_row_mask on legacy-storage datasets. In filtered_read, detect the
combination of self.dataset.is_legacy_storage() and self.external_row_mask and
return an explicit unsupported-operation error before dispatching to
legacy_filtered_read. Preserve existing behavior for legacy scans without a mask
and non-legacy masked scans through new_filtered_read.

---

Nitpick comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 1210-1226: Add comprehensive tests for external RowAddrMask
filtering: in rust/lance/src/dataset/scanner.rs lines 1210-1226, cover
Scanner::with_row_addr_prefilter for plain scans, indexed vector searches,
flat/unindexed searches, and appended-fragment knn_combined searches, verifying
only masked rows return; in rust/lance/src/io/exec/row_addr_mask.rs lines
33-156, test apply_mask null _rowid handling and allow/block selection plus
MaskAndLoader::load with and without an inner loader; in
rust/lance/src/io/exec/knn.rs lines 1104-1981, test ANNIvfSubIndexExec::execute
intersects external_mask with an existing filter-derived prefilter loader.
- Around line 3052-3076: Extract the shared serialization and execution-plan
construction from mask_as_index_input and u64s_as_take_input into a helper such
as index_result_as_index_input, accepting an IndexExprResult and returning the
OneShotExec plan. Update both callers to build their respective IndexExprResult
and delegate to the helper, preserving existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 774a77ab-63af-4315-b074-31a60d5f31e5

📥 Commits

Reviewing files that changed from the base of the PR and between ed88757 and 7c82510.

📒 Files selected for processing (4)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec.rs
  • rust/lance/src/io/exec/knn.rs
  • rust/lance/src/io/exec/row_addr_mask.rs

@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

1210-1222: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a compiling rustdoc example and API links.

with_row_addr_prefilter is a new public API, but its documentation has no example or links to RowAddrMask, RowAddrTreeMap, or the relevant scan methods. Add a synchronized no_run example using the actual signature.

🤖 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/scanner.rs` around lines 1210 - 1222, Update the
rustdoc for with_row_addr_prefilter to include a compiling no_run example using
its actual signature, and add API links for RowAddrMask, RowAddrTreeMap, and the
relevant scanner methods. Keep the example synchronized with the current public
API and existing documentation behavior.

Source: Coding guidelines


3975-3979: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Add Scanner-level regression tests for the new mask contract.

The supplied tests cover primitive filtering and loader composition, but not Scanner::with_row_addr_prefilter. Add coverage for plain allow/block scans with SQL refinement, legacy rejection, flat fallback, and ANN appended/unindexed fragments.

Also applies to: 4129-4133, 4784-4790, 4844-4850

🤖 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/scanner.rs` around lines 3975 - 3979, Add
Scanner-level regression tests covering the row-address mask contract through
Scanner::with_row_addr_prefilter: validate allow/block scans with SQL
refinement, rejection of legacy inputs, flat fallback behavior, and ANN scans
spanning appended and unindexed fragments. Exercise the relevant scanner paths,
including the RowAddrMaskFilterExec application, and assert both returned rows
and expected errors for unsupported cases.

Source: Coding guidelines

rust/lance/src/io/exec/row_addr_mask.rs (1)

109-121: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace panic-based extraction in both library paths.

  • rust/lance/src/io/exec/row_addr_mask.rs#L109-L121: replace children.pop().expect(...) with explicit error handling.
  • rust/lance/src/dataset/scanner.rs#L2991-L2993: replace external_row_mask.as_deref().unwrap() with pattern matching or contextual error propagation.
🤖 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/io/exec/row_addr_mask.rs` around lines 109 - 121, Replace
panic-based extraction in RowAddrMaskFilterExec::with_new_children at
rust/lance/src/io/exec/row_addr_mask.rs:109-121 with explicit error handling
that returns a DataFusionResult error if the child is unavailable; retain the
existing validation for exactly one child. Also update the external_row_mask
access in rust/lance/src/dataset/scanner.rs:2991-2993 to use pattern matching or
contextual error propagation instead of unwrap, handling the missing-mask case
without panicking.

Source: Coding guidelines

🧹 Nitpick comments (2)
rust/lance/src/io/exec/row_addr_mask.rs (2)

147-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the actual _rowid type in the error.

Report row_id_column.data_type() alongside the expected UInt64 type so malformed execution plans are diagnosable.

🤖 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/io/exec/row_addr_mask.rs` around lines 147 - 148, Update the
error construction in the row ID validation flow to include
row_id_column.data_type() alongside the expected UInt64 type. Preserve the
existing DataFusionError::Internal failure path and type-checking behavior.

Source: Coding guidelines


50-56: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid deep-copying the shared mask on every load.

MaskAndLoader receives a shared Arc<RowAddrMask>, so Arc::unwrap_or_clone normally takes the clone branch and copies the potentially large mask even when inner is absent. Arc::unwrap_or_clone only unwraps when the reference is unique. (doc.rust-lang.org) Consider preserving shared ownership in the loader contract, or adding a shared fast path; only materialize a new mask for intersections.

🤖 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/io/exec/row_addr_mask.rs` around lines 50 - 56, Update
MaskAndLoader::load to avoid materializing a cloned RowAddrMask when inner is
absent: preserve or return the shared Arc-backed mask through the loader
contract, and only create an owned mask when applying the intersection with
inner.load().await. Keep the existing intersection behavior unchanged.

Source: MCP tools

🤖 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/io/exec/row_addr_mask.rs`:
- Around line 244-259: Extend mask_and_loader_with_inner_intersects to cover
block-list × allow-list and block-list × block-list combinations in addition to
the existing allow-list × allow-list case. Construct the corresponding
RowAddrMask values, load them through MaskAndLoader, and assert selected and
unselected addresses to verify the expected intersection algebra.
- Around line 215-222: Strengthen apply_mask_missing_rowid_column_errs by
matching the returned DataFusionError::Internal variant and asserting the
expected missing-column message instead of only checking is_err(). Add a
separate test covering a present rowid column with an unsupported data type,
verifying that apply_mask returns the expected Internal error and message for
the wrong-type branch.

---

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 1210-1222: Update the rustdoc for with_row_addr_prefilter to
include a compiling no_run example using its actual signature, and add API links
for RowAddrMask, RowAddrTreeMap, and the relevant scanner methods. Keep the
example synchronized with the current public API and existing documentation
behavior.
- Around line 3975-3979: Add Scanner-level regression tests covering the
row-address mask contract through Scanner::with_row_addr_prefilter: validate
allow/block scans with SQL refinement, rejection of legacy inputs, flat fallback
behavior, and ANN scans spanning appended and unindexed fragments. Exercise the
relevant scanner paths, including the RowAddrMaskFilterExec application, and
assert both returned rows and expected errors for unsupported cases.

In `@rust/lance/src/io/exec/row_addr_mask.rs`:
- Around line 109-121: Replace panic-based extraction in
RowAddrMaskFilterExec::with_new_children at
rust/lance/src/io/exec/row_addr_mask.rs:109-121 with explicit error handling
that returns a DataFusionResult error if the child is unavailable; retain the
existing validation for exactly one child. Also update the external_row_mask
access in rust/lance/src/dataset/scanner.rs:2991-2993 to use pattern matching or
contextual error propagation instead of unwrap, handling the missing-mask case
without panicking.

---

Nitpick comments:
In `@rust/lance/src/io/exec/row_addr_mask.rs`:
- Around line 147-148: Update the error construction in the row ID validation
flow to include row_id_column.data_type() alongside the expected UInt64 type.
Preserve the existing DataFusionError::Internal failure path and type-checking
behavior.
- Around line 50-56: Update MaskAndLoader::load to avoid materializing a cloned
RowAddrMask when inner is absent: preserve or return the shared Arc-backed mask
through the loader contract, and only create an owned mask when applying the
intersection with inner.load().await. Keep the existing intersection behavior
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: QUIET

Plan: Pro Plus

Run ID: 90670792-f928-4db7-8cee-3313ab4151d4

📥 Commits

Reviewing files that changed from the base of the PR and between 7c82510 and 36a3b67.

📒 Files selected for processing (2)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec/row_addr_mask.rs

Comment thread rust/lance/src/io/exec/row_addr_mask.rs
Comment thread rust/lance/src/io/exec/row_addr_mask.rs
@JulianYG
JulianYG force-pushed the feat/row-addr-mask-prefilter branch from 36a3b67 to 4198c4e Compare July 19, 2026 15:45
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
rust/lance/src/dataset/scanner.rs (1)

5623-5717: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add test coverage for stable row ids and block-list masks.

The new integration tests only exercise RowAddrMask::from_allowed(...) on TestVectorDataset::new(LanceFileVersion::Stable, false) (stable row ids disabled). Two gaps worth closing:

  • No test uses enable_stable_row_ids: true, despite with_row_addr_prefilter's doc explicitly claiming both row-address and stable-row-id spaces are "handled" — this is exactly the scenario a past review flagged as a correctness risk.
  • No test uses RowAddrMask::from_block(...) at the scanner level. mask_as_index_inputIndexExprResult::exact(mask.clone()) is a new code path for block masks specifically (previously only allow-masks flowed through u64s_as_take_input); the row_addr_mask.rs unit tests cover block masks only for RowAddrMaskFilterExec/MaskAndLoader, not for the plain-scan FilteredReadExec index-input serialization.
🤖 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/scanner.rs` around lines 5623 - 5717, Extend the
row-address scanner tests around row_addr_mask_plain_scan_allow_with_refine and
row_addr_mask_ann_search_only_allowed to construct TestVectorDataset with
enable_stable_row_ids: true and verify allowed row IDs remain correct. Add
scanner-level coverage using RowAddrMask::from_block(...) through the plain-scan
path, asserting the returned rows match the block mask, including any refine
behavior needed to exercise FilteredReadExec index-input serialization.
🤖 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/scanner.rs`:
- Around line 5623-5717: Extend the row-address scanner tests around
row_addr_mask_plain_scan_allow_with_refine and
row_addr_mask_ann_search_only_allowed to construct TestVectorDataset with
enable_stable_row_ids: true and verify allowed row IDs remain correct. Add
scanner-level coverage using RowAddrMask::from_block(...) through the plain-scan
path, asserting the returned rows match the block mask, including any refine
behavior needed to exercise FilteredReadExec index-input serialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 63da2cce-8392-4b14-8a20-6bc24eb1f5f8

📥 Commits

Reviewing files that changed from the base of the PR and between 36a3b67 and 4198c4e.

📒 Files selected for processing (4)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec.rs
  • rust/lance/src/io/exec/knn.rs
  • rust/lance/src/io/exec/row_addr_mask.rs

row-address scanner tests around row_addr_mask_plain_scan_allow_with_refine and row_addr_mask_ann_search_only_allowed
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/scanner.rs (1)

1222-1224: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new public builder with a compiling example.

Scanner::with_row_addr_prefilter is a public API but has no /// documentation. Describe the mask’s allow/block and _rowid semantics, plain/ANN behavior, and add a compiling example with links to RowAddrMask and related scanner methods.

As per coding guidelines: “Document all public APIs with examples and links to relevant structs and methods; keep examples synchronized with actual signatures.”

🤖 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/scanner.rs` around lines 1222 - 1224, Document the
public Scanner::with_row_addr_prefilter builder with /// comments covering
RowAddrMask allow/block behavior, _rowid semantics, and differences between
plain and ANN scans. Add a compiling example using the actual method signature,
linking to RowAddrMask and related Scanner methods with intra-doc links, and
place the documentation directly above the method.

Source: Coding guidelines

🧹 Nitpick comments (1)
rust/lance/src/dataset/scanner.rs (1)

5632-5636: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Give the rstest cases readable names. Replace #[values(false, true)] with named cases in both tests, e.g. #[case::without_stable_row_ids(false)] and #[case::with_stable_row_ids(true)], so failures identify the row-ID mode.

🤖 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/scanner.rs` around lines 5632 - 5636, Update both
affected rstest functions, including
row_addr_mask_plain_scan_allow_block_refine, by replacing #[values(false, true)]
with named #[case] entries for false and true. Name the cases
without_stable_row_ids and with_stable_row_ids so test failures identify the
row-ID mode.

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.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 1222-1224: Document the public Scanner::with_row_addr_prefilter
builder with /// comments covering RowAddrMask allow/block behavior, _rowid
semantics, and differences between plain and ANN scans. Add a compiling example
using the actual method signature, linking to RowAddrMask and related Scanner
methods with intra-doc links, and place the documentation directly above the
method.

---

Nitpick comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 5632-5636: Update both affected rstest functions, including
row_addr_mask_plain_scan_allow_block_refine, by replacing #[values(false, true)]
with named #[case] entries for false and true. Name the cases
without_stable_row_ids and with_stable_row_ids so test failures identify the
row-ID mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 47923168-701a-4a1a-bc0c-100436bfc520

📥 Commits

Reviewing files that changed from the base of the PR and between 4198c4e and 4ec8186.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
rust/lance/src/dataset/scanner.rs (3)

5683-5687: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Format the new test assertions with rustfmt. cargo fmt --all -- --check currently fails.

  • rust/lance/src/dataset/scanner.rs#L5683-L5687: format the chained column_by_name(...).as_primitive::<Int32Type>() assertion.
  • rust/lance/src/dataset/scanner.rs#L5731-L5735: format the multiline ANN allowlist assertion.
🤖 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/scanner.rs` around lines 5683 - 5687, Run rustfmt on
the new test assertions in rust/lance/src/dataset/scanner.rs at lines 5683-5687
and 5731-5735: format the chained
column_by_name(...).as_primitive::<Int32Type>() assertion and the multiline ANN
allowlist assertion to match cargo fmt output.

Source: Pipeline failures


1210-1225: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new public builder with links and an example.

with_row_addr_prefilter is public but has no usage example or Rustdoc links to relevant APIs such as [RowAddrMask] and the scan execution methods.

As per coding guidelines: “Document all public APIs with examples and links to relevant structs and methods.”

🤖 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/scanner.rs` around lines 1210 - 1225, Expand the
Rustdoc for with_row_addr_prefilter to include links to RowAddrMask and the
relevant scan execution methods, plus a concise example showing how to construct
and apply the mask before running a scan. Keep the existing behavior and
explanation of dataset _rowid semantics unchanged.

Source: Coding guidelines


2935-2947: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat mask-backed plain scans as filtered during source planning.

filtered_read_source still derives projection and scan_range_before_filter from the original filter_plan. Consequently, mask-only scans can apply limit/offset before masking, and exact scalar-index predicates can be rewritten here into a refine filter after their predicate columns were omitted from the projection. This can return too few rows or fail evaluating the refine expression.

Carry use_external_mask/effective_filter into source planning: disable pre-mask range pushdown and include refine columns. Add regressions for a masked limit/offset scan and a masked exact scalar-index predicate projecting an unrelated column.

🤖 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/scanner.rs` around lines 2935 - 2947, The
source-planning path still uses the original filter plan for external-mask plain
scans, allowing limit/offset pushdown before masking and omitting columns needed
by rewritten refine predicates. Update filtered_read_source and its
projection/scan-range planning to use use_external_mask and effective_filter,
disabling pre-mask range pushdown while retaining refine columns; add
regressions covering masked limit/offset scans and masked exact scalar-index
predicates projecting an unrelated column.
🤖 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.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 5683-5687: Run rustfmt on the new test assertions in
rust/lance/src/dataset/scanner.rs at lines 5683-5687 and 5731-5735: format the
chained column_by_name(...).as_primitive::<Int32Type>() assertion and the
multiline ANN allowlist assertion to match cargo fmt output.
- Around line 1210-1225: Expand the Rustdoc for with_row_addr_prefilter to
include links to RowAddrMask and the relevant scan execution methods, plus a
concise example showing how to construct and apply the mask before running a
scan. Keep the existing behavior and explanation of dataset _rowid semantics
unchanged.
- Around line 2935-2947: The source-planning path still uses the original filter
plan for external-mask plain scans, allowing limit/offset pushdown before
masking and omitting columns needed by rewritten refine predicates. Update
filtered_read_source and its projection/scan-range planning to use
use_external_mask and effective_filter, disabling pre-mask range pushdown while
retaining refine columns; add regressions covering masked limit/offset scans and
masked exact scalar-index predicates projecting an unrelated column.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 40a759b0-0578-417d-822f-652ceae85d48

📥 Commits

Reviewing files that changed from the base of the PR and between 4198c4e and 4ec8186.

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

@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/scanner.rs`:
- Around line 2947-2970: Update the scan validation around use_external_mask and
the full-text-search setup to reject any request combining
with_row_addr_prefilter with full_text_search. Return a descriptive error at the
API boundary before FTS planning or prefilter_source can run, while preserving
existing behavior for plain and vector scans.
🪄 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: QUIET

Plan: Pro Plus

Run ID: 2933c7e5-1ee8-4779-a7ef-77acc4a0d9f5

📥 Commits

Reviewing files that changed from the base of the PR and between 4ec8186 and 4ac24a7.

📒 Files selected for processing (3)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec/filtered_read.rs
  • rust/lance/src/io/exec/row_addr_mask.rs

Comment thread rust/lance/src/dataset/scanner.rs
@JulianYG
JulianYG force-pushed the feat/row-addr-mask-prefilter branch from 4ac24a7 to cb2a120 Compare July 20, 2026 03:36
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/io/exec/filtered_read.rs (1)

2300-2348: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing dedicated unit-test case for the new index_input.is_some() guard.

test_with_fetch_limit_pushdown enumerates cases 1-6 for with_fetch, but none construct a plan with index_input set, so the exact branch this diff adds (|| self.index_input.is_some()) isn't directly exercised at this unit level — coverage for it currently only comes indirectly via the scanner-level row_addr_mask_plain_scan_with_limit test. Consider adding a case here that builds a plan with a non-None index_input and asserts with_fetch returns None, to guard this specific branch against regression.

As per coding guidelines: "Every bugfix and feature must have 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/io/exec/filtered_read.rs` around lines 2300 - 2348, The
with_fetch limit-pushdown tests lack direct coverage for the index_input guard.
Extend test_with_fetch_limit_pushdown with a case that constructs a plan having
a non-None index_input while both filters are absent, then assert with_fetch
returns None; leave the existing cases unchanged.

Source: Coding guidelines

rust/lance/src/dataset/scanner.rs (1)

3061-3072: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Legacy-storage mask guard also wrongly blocks FTS queries, not just plain scans.

The guard is self.nearest.is_none() && self.external_row_mask.is_some(), but nearest.is_none() is also true for full-text-search queries (nearest and full_text_query are mutually exclusive). plan_flat_match_query (and prefilter_source when called from fts()) unconditionally calls filtered_read for unindexed fragments, so any FTS query combined with with_row_addr_prefilter() on a legacy-storage dataset will be rejected with "not supported for plain scans on legacy-storage datasets" — even though FTS masking is fully wired via MatchQueryExec/PhraseQueryExec::with_external_mask and the RowAddrMaskFilterExec wrap in plan_flat_match_query, neither of which depends on new_filtered_read's mask-as-index-input mechanism that this guard is meant to protect. The comment itself confirms only vector search is meant to be exempted, but FTS uses the exact same "downstream wrap" mechanism and should be exempted too.

Use self.use_external_mask() (which already excludes both nearest and full_text_query) instead of the ad-hoc nearest.is_none() check.

🐛 Proposed fix
-            if self.nearest.is_none() && self.external_row_mask.is_some() {
+            if self.use_external_mask() {
                 return Err(Error::not_supported(
                     "with_row_addr_prefilter is not supported for plain scans on \
                      legacy-storage datasets"
                         .to_string(),
                 ));
             }
🤖 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/scanner.rs` around lines 3061 - 3072, Update the
legacy-storage mask guard near the scanner planning logic to use
self.use_external_mask() instead of checking self.nearest.is_none(). Preserve
the external-row-mask condition so the guard only rejects masked scans that
require the unsupported plain-scan path, while allowing FTS and vector queries
handled through downstream mask wrapping.
🧹 Nitpick comments (1)
rust/lance/src/dataset/scanner.rs (1)

1213-1244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doc omits the FTS + mask combination.

The doc describes vector-search and plain-scan behavior but never mentions full-text search, even though FTS masking is fully implemented (MatchQueryExec/PhraseQueryExec::with_external_mask, RowAddrMaskFilterExec wrap in plan_flat_match_query) and covered by row_addr_mask_fts_search_only_allowed. As written, a reader of this public API doc would have no indication FTS is supported.

As per coding guidelines: "Document all public APIs with examples and links to relevant structs and methods."

✏️ Proposed doc addition
     /// The mask is keyed in the dataset's `_rowid` space, so build it from the
     /// same dataset you query. That space is the row address when stable row ids
     /// are disabled and the stable row id when they are enabled; both are handled
     /// (index prefilter and filtered read branch on `uses_stable_row_ids`), so no
     /// caller-side translation is needed either way.
+    ///
+    /// On a full-text search, the mask restricts BM25 scoring on indexed
+    /// fragments and is applied to flat (unindexed) matches before top-k
+    /// consumption, so results are limited to masked rows on both branches.
🤖 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/scanner.rs` around lines 1213 - 1244, Update the
public `with_row_addr_prefilter` documentation to explicitly describe full-text
search behavior: explain that the mask is applied to FTS results, including
match and phrase queries, and mention the relevant FTS filtering path
(`plan_flat_match_query`/`RowAddrMaskFilterExec`). Add a concise FTS usage
example or links to the relevant query methods while preserving the existing
vector-search and plain-scan documentation.

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.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 3061-3072: Update the legacy-storage mask guard near the scanner
planning logic to use self.use_external_mask() instead of checking
self.nearest.is_none(). Preserve the external-row-mask condition so the guard
only rejects masked scans that require the unsupported plain-scan path, while
allowing FTS and vector queries handled through downstream mask wrapping.

In `@rust/lance/src/io/exec/filtered_read.rs`:
- Around line 2300-2348: The with_fetch limit-pushdown tests lack direct
coverage for the index_input guard. Extend test_with_fetch_limit_pushdown with a
case that constructs a plan having a non-None index_input while both filters are
absent, then assert with_fetch returns None; leave the existing cases unchanged.

---

Nitpick comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 1213-1244: Update the public `with_row_addr_prefilter`
documentation to explicitly describe full-text search behavior: explain that the
mask is applied to FTS results, including match and phrase queries, and mention
the relevant FTS filtering path
(`plan_flat_match_query`/`RowAddrMaskFilterExec`). Add a concise FTS usage
example or links to the relevant query methods while preserving the existing
vector-search and plain-scan documentation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 812fbd44-9377-45fd-a4ee-613d6d61ded6

📥 Commits

Reviewing files that changed from the base of the PR and between 4ac24a7 and cb2a120.

📒 Files selected for processing (5)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec/filtered_read.rs
  • rust/lance/src/io/exec/fts.rs
  • rust/lance/src/io/exec/row_addr_mask.rs
  • rust/lance/src/io/exec/utils.rs

- with_row_addr_prefilter doc now describes full-text search behavior (mask
  combined into the FTS prefilter; unindexed flat branch masked via
  RowAddrMaskFilterExec) and links full_text_search.
- Legacy-storage mask guard keys on use_external_mask() instead of
  nearest.is_none(), so FTS and vector masked scans (handled via their own
  prefilter wrapping) are no longer wrongly rejected on legacy storage; only
  the unsupported plain-scan mask path is rejected.
- test_with_fetch_limit_pushdown gains a case asserting with_fetch returns None
  when an index_input is present with no filters (the external-mask shape).
@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
rust/lance/src/dataset/scanner.rs (1)

3794-3805: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the mask before flat FTS top-k selection.

FlatMatchQueryExec receives limited FTS parameters before RowAddrMaskFilterExec removes masked rows. Excluded high-scoring rows can consume result slots, hiding lower-scoring allowed matches. Wrap plan before creating FlatMatchQueryExec, and add a flat-only FTS test where the only allowed match ranks below k.

Proposed fix
-        let flat_match_plan: Arc<dyn ExecutionPlan> = Arc::new(FlatMatchQueryExec::new(
+        let plan: Arc<dyn ExecutionPlan> = if let Some(mask) = self.external_row_mask.clone() {
+            Arc::new(RowAddrMaskFilterExec::new(plan, mask))
+        } else {
+            plan
+        };
+        Ok(Arc::new(FlatMatchQueryExec::new(
             self.dataset.clone(),
             query.clone(),
             params.clone(),
             plan,
-        ));
-        // Unindexed fragments never reach the index-side prefilter, so apply the
-        // external row-address mask to the flat FTS results here (mirrors the ANN
-        // flat branch). Applied before the caller's top-k so masked-out rows do
-        // not consume result slots.
-        if let Some(mask) = self.external_row_mask.clone() {
-            return Ok(Arc::new(RowAddrMaskFilterExec::new(flat_match_plan, mask)));
-        }
-        Ok(flat_match_plan)
+        )))
🤖 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/scanner.rs` around lines 3794 - 3805, Apply
external_row_mask to the FTS execution plan before constructing
FlatMatchQueryExec, rather than wrapping its limited results afterward. Update
the flat FTS branch around FlatMatchQueryExec::new to wrap plan with
RowAddrMaskFilterExec when a mask exists, then pass the masked plan into
FlatMatchQueryExec; add a flat-only FTS test where an excluded higher-ranked
match would otherwise consume k and verify the lower-ranked allowed match is
returned.

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.

Outside diff comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 3794-3805: Apply external_row_mask to the FTS execution plan
before constructing FlatMatchQueryExec, rather than wrapping its limited results
afterward. Update the flat FTS branch around FlatMatchQueryExec::new to wrap
plan with RowAddrMaskFilterExec when a mask exists, then pass the masked plan
into FlatMatchQueryExec; add a flat-only FTS test where an excluded
higher-ranked match would otherwise consume k and verify the lower-ranked
allowed match is returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: b61b5bb8-c894-45dd-a9cb-0e93fcb7fdb8

📥 Commits

Reviewing files that changed from the base of the PR and between cb2a120 and b72c16f.

📒 Files selected for processing (2)
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec/filtered_read.rs

@JulianYG

Copy link
Copy Markdown
Contributor Author

@coderabbitai Skipping this one — the premise doesn't hold against the current code.
FlatMatchQueryExec doesn't apply its own top-k. Its execute streams every scored unindexed row via flat_bm25_search_stream_* (supports_limit_pushdown() returns false), so there are no "limited results" to wrap. And RowAddrMaskFilterExec is the outermost node of the flat plan — the only top-k downstream is the SortExec…with_fetch(params.limit) in the mixed union case, or the plan's GlobalLimitExec, both applied strictly after the mask.
So masked rows are dropped before any k is applied and cannot consume a slot. The "excluded higher-ranked match consumes k, lower-ranked allowed match lost" scenario can't occur. Moving the mask below the scan would only avoid BM25-scoring masked rows (a minor cost saving), not change results, so I'm leaving the plan tree as-is.
Flat-branch masking is already tested: row_addr_mask_fts_search_only_allowed re-appends unindexed rows after indexing so the query matches on the flat branch, and asserts both the allowed-subset result and block-all-empty. A "consume k" variant isn't meaningful here because the single-match flat-only path returns before any score-sorted top-k — there's no k at that layer to exercise.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@JulianYG You're right—thanks for the detailed plan-ordering clarification. Since RowAddrMaskFilterExec runs before the downstream SortExec/GlobalLimitExec, excluded flat-branch rows cannot consume a top-k slot. The existing flat-branch test coverage also validates the relevant result semantics.

I’m withdrawing the comment; placing the mask earlier would only be a potential scoring-cost optimization, not a correctness fix.

🐇💭


✏️ Learnings added
Learnt from: JulianYG
URL: https://github.com/lance-format/lance/pull/7288

Timestamp: 2026-07-20T05:33:01.210Z
Learning: In `rust/lance/src/dataset/scanner.rs`, the flat full-text-search plan places `RowAddrMaskFilterExec` outside `FlatMatchQueryExec`. `FlatMatchQueryExec` streams all scored unindexed rows and does not apply a local top-k (`supports_limit_pushdown()` is false); any `SortExec::with_fetch(params.limit)` or `GlobalLimitExec` occurs after row-address masking. Therefore, flat FTS mask placement affects scoring cost but not top-k result correctness.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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