Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0ae5213
perf: plan takes through FilteredReadExec's range-read path
LuQQiu Jul 3, 2026
4114b80
bench: use rand 0.9 random_range in take_exec_compare
LuQQiu Jul 3, 2026
d94ef90
style: fix clippy findings in take mode
LuQQiu Jul 3, 2026
058eb8b
perf: parallelize fragment opens in FilteredReadExec take mode
LuQQiu Jul 3, 2026
349ee1a
refactor: model the FilteredReadExec input as a row source
LuQQiu Jul 8, 2026
173d816
chore: keep the take benchmark harness out of the tree
LuQQiu Jul 8, 2026
bfbb97b
feat(filtered-read): coalesce the row-stream input into read rounds
LuQQiu Jul 8, 2026
f3f88cc
feat(filtered-read): accept a _rowaddr key with stable row ids
LuQQiu Jul 8, 2026
0411f5c
perf(filtered-read): tune round I/O ordering and skip no-op alignment
LuQQiu Jul 8, 2026
3911477
refactor(filtered-read): rename the row source to RowSelector, trim docs
LuQQiu Jul 8, 2026
e7d6ec6
refactor(filtered-read): name the round stages and dedupe stream-sour…
LuQQiu Jul 9, 2026
e893cbc
feat(filtered-read): support with_deleted_rows on the row-stream sele…
LuQQiu Jul 9, 2026
5dfe78c
feat(filtered-read): make the identity flags authoritative on row str…
LuQQiu Jul 9, 2026
5b4919b
refactor(filtered-read): share the scan setup glue with the row-strea…
LuQQiu Jul 9, 2026
e982ebd
feat(filtered-read): complete the with_deleted_rows contract on row s…
LuQQiu Jul 9, 2026
529b2ae
revert(filtered-read): reject with_deleted_rows on row-stream reads
LuQQiu Jul 9, 2026
51f8d17
refactor(filtered-read): address review-bot findings and clone cleanup
LuQQiu Jul 10, 2026
4ccf02f
docs(filtered-read): trim review-phase comments to the essentials
LuQQiu Jul 10, 2026
c898edd
test: update python plan assertions for the row-stream display
LuQQiu Jul 10, 2026
2e881db
refactor(filtered-read): run each row-stream round through the scan e…
LuQQiu Jul 10, 2026
b699b54
refactor(filtered-read): batch vocabulary and comment trims from review
LuQQiu Jul 10, 2026
f724d23
fix(scanner): make CoalesceTake collapse work for row-stream takes
LuQQiu Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion python/python/tests/test_mem_wal.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def test_point_lookup_with_memtables(tmp_path):
plan = planner.plan_lookup(pa.array([2], type=pa.int64()))
assert plan.schema.names == ["id", "name"]
assert plan.dataset_schema.names == ["id", "name"]
assert "Take" in plan.explain() or "Scan" in plan.explain()
explained = plan.explain()
assert "Take" in explained or "Scan" in explained or "LanceRead" in explained
result = plan.to_table()
assert len(result) == 1, "Expected exactly one row for id=2"
assert result.column("name")[0].as_py() == "gen1_2", (
Expand Down
5 changes: 2 additions & 3 deletions rust/lance-namespace-impls/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12256,7 +12256,7 @@ mod tests {
&plan_str,
&[
"ProjectionExec: expr=[id@0 as id, name@2 as name",
"Take: columns=\"id, _rowid, (name)\"",
"projection=[name], source=stream(_rowid)",
"LanceRead: uri=",
"projection=[id]",
"row_id=true, row_addr=false",
Expand Down Expand Up @@ -12353,8 +12353,7 @@ mod tests {
"AnalyzeExec verbose=true",
"ProjectionExec: elapsed=",
"expr=[id@0 as id, name@2 as name",
"Take: elapsed=",
"columns=\"id, _rowid, (name)\"",
"projection=[name], source=stream(_rowid)",
"CoalesceBatchesExec: elapsed=",
"LanceRead: elapsed=",
"projection=[id]",
Expand Down
2 changes: 1 addition & 1 deletion rust/lance/src/dataset/mem_wal/memtable/flush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2172,7 +2172,7 @@ mod tests {
crate::utils::test::assert_plan_node_equals(
plan,
"ProjectionExec: expr=[id@2 as id, text@3 as text, _score@1 as _score]
Take: ...
LanceRead: ..., source=stream(_rowid)
CoalesceBatchesExec: ...
MatchQuery: column=text, query=hello",
)
Expand Down
284 changes: 244 additions & 40 deletions rust/lance/src/dataset/scanner.rs

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions rust/lance/src/dataset/write/merge_insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ use crate::{
},
index::DatasetIndexInternalExt,
io::exec::{
AddRowAddrExec, Planner, TakeExec, project,
AddRowAddrExec, Planner, TakeExec,
filtered_read::{FilteredReadExec, FilteredReadOptions},
project,
scalar_index::{IndexLookup, MapIndexExec},
utils::ReplayExec,
},
Expand Down Expand Up @@ -734,14 +736,31 @@ impl MergeInsertJob {
)?);
}

// 4 - Take the mapped row ids
// 4 - Take the mapped row ids. On the v2 storage format the take is
// planned as a FilteredReadExec fed by the index-mapper stream:
// the row ids become a row-id mask read through the planned
// range-read path, which is considerably faster than TakeExec's
// point-lookup path. Both nodes have the same output contract
// (input columns first, then the fetched columns).
//
// INVARIANT: every site that replaces TakeExec with
// FilteredReadExec must check is_legacy_storage() first. The v1
// reader's read_ranges_tasks is a stub that errors ("Attempt to
// perform FilteredRead on v1 files"), so TakeExec remains the
// only take that can read v1 files.

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.

Do we really need this verbose comment?

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.

I will clean up the verbose comments after all the code changes looks good (otherwise agent tend to add back a lot of comments when i delete them up front lollll)

let projection = self
.dataset
.empty_projection()
.union_arrow_schema(schema.as_ref(), OnMissing::Error)?;
let mut target =
let mut target: Arc<dyn ExecutionPlan> = if self.dataset.is_legacy_storage() {
Arc::new(TakeExec::try_new(self.dataset.clone(), index_mapper, projection)?.unwrap())
as Arc<dyn ExecutionPlan>;
} else {
Arc::new(FilteredReadExec::try_new(
self.dataset.clone(),
FilteredReadOptions::new(projection),
Some(index_mapper),
)?)
};

// 5 - Take puts the row id and row addr at the beginning. A full scan (used when there is
// no scalar index) puts the row id and addr at the end. We need to match these up so
Expand Down
13 changes: 13 additions & 0 deletions rust/lance/src/io/exec/count_pushdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ fn try_rewrite(agg: &AggregateExec) -> DFResult<Option<Arc<dyn ExecutionPlan>>>
};

let options = filtered_read.options();
// A read with a row-stream source emits one row per input row; its count
// is driven by the input plan, not by fragment metadata.
//
// COUNT over such a read == the count of input rows whose id still
// exists. A future CountLiveRowsExec could answer that without any
// column I/O: stream the input and, per batch, build the live-row set
// from fragment metadata + deletion vectors and count member rows
// (per row, so duplicates count). Deferred because no plan shape today
// places a row-stream read under a COUNT — count plans request no
// columns, so the scanner never builds one.

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.

Suggested change
// A read with a row-stream source emits one row per input row; its count
// is driven by the input plan, not by fragment metadata.
//
// COUNT over such a read == the count of input rows whose id still
// exists. A future CountLiveRowsExec could answer that without any
// column I/O: stream the input and, per batch, build the live-row set
// from fragment metadata + deletion vectors and count member rows
// (per row, so duplicates count). Deferred because no plan shape today
// places a row-stream read under a COUNT — count plans request no
// columns, so the scanner never builds one.
// We don't currently support count pushdown when the row selector
// is a row stream.

if filtered_read.row_stream_input().is_some() {
return Ok(None);
}
// A refine filter is a residual the index couldn't fully evaluate — it
// needs column data to apply, which we can't.
if options.refine_filter.is_some() {
Expand Down
Loading
Loading