Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fae2626
Expose a V2 scan's planners and prepared plans; drive them elsewhere
joseph-isaacs Oct 22, 2026
92f3f4d
Compose and drive a V2 scan in DataFusion
joseph-isaacs Oct 22, 2026
c7d3c3f
Drive a V2 scan of a file for callers without an engine
joseph-isaacs Oct 22, 2026
95424b8
Compose a V2 scan's planners in the DataFusion opener
joseph-isaacs Oct 22, 2026
04de767
Drop is_proved: a proved proof lists no segment
joseph-isaacs Oct 22, 2026
429e005
Call the zone-map plans pruning plans, not proofs
joseph-isaacs Oct 22, 2026
1db58aa
Add a planner-driven V2 scan path to the DuckDB extension
claude Oct 26, 2026
fd7a173
End DuckDB V2 splits on layout chunk boundaries
claude Oct 26, 2026
8cae054
Merge #10708 (Compose and drive a V2 scan in DataFusion) into the Duc…
claude Oct 26, 2026
58940c0
Drive DuckDB V2 scans on DuckDB's threads with a custom driver
claude Oct 26, 2026
115575d
Merge develop into the DuckDB V2 branch
claude Oct 26, 2026
c521dd8
Merge develop into the DuckDB V2 branch
claude Oct 26, 2026
9189e89
Merge develop, which now has #10708, into the DuckDB V2 branch
claude Oct 26, 2026
43671cc
Default duckdb-bench to the V2 scan for TPC-DS and ClickBench
claude Oct 26, 2026
2aa98a2
Merge develop after #10809 landed
claude Oct 26, 2026
8a4cefe
Cut DuckDB V2 scan chunks on the file's chunk boundaries
claude Oct 26, 2026
8a464a9
Keep a few V2 chunks per thread when merging file chunks
claude Oct 26, 2026
527684b
Plan each DuckDB V2 file fully at initialize and queue its chunks
claude Oct 26, 2026
1ed88fe
Turn on the DuckDB V2 scan for TPC-DS and ClickBench in vx-bench
claude Oct 26, 2026
431717c
Run a DuckDB V2 part's chunks inline so they share reads
claude Oct 26, 2026
746f52b
Choose the V2 scan per benchmark in vx-bench only
claude Oct 26, 2026
4e9cede
Prune each DuckDB V2 part again under a dynamic filter
claude Oct 26, 2026
e4eb6f2
Prune a DuckDB V2 file only per part under a dynamic filter
claude Oct 26, 2026
9b8a90e
Share a segment's read across a V2 scan's live IO sessions
claude Oct 26, 2026
3b2d95d
Prune a DuckDB V2 file again before its parts under a dynamic filter
claude Oct 26, 2026
c867e73
Plan a V2 projection of fields as one pack of column plans
claude Oct 27, 2026
0ad75b1
Retain a sparse V2 fetch's reads and plan only its selected chunks
claude Oct 27, 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
8 changes: 8 additions & 0 deletions bench-orchestrator/bench_orchestrator/runner/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

console = Console()

# The benchmarks each engine runs on the planner-driven V2 Vortex scan.
SCAN_V2_BENCHMARKS: dict[Engine, frozenset[Benchmark]] = {
Engine.DATAFUSION: frozenset({Benchmark.TPCH}),
Engine.DUCKDB: frozenset({Benchmark.TPCH, Benchmark.TPCDS, Benchmark.CLICKBENCH}),
}


@final
class BenchmarkExecutor:
Expand Down Expand Up @@ -56,6 +62,8 @@ def build_command(
cmd.extend(["--formats", ",".join(fmt.value for fmt in formats)])
if self.backend == Engine.DUCKDB:
cmd.append("--delete-duckdb-database")
if benchmark in SCAN_V2_BENCHMARKS.get(self.backend, frozenset()):
cmd.extend(["--scan-v2", "true"])

if query is not None:
cmd.extend(["--queries", str(query)])
Expand Down
24 changes: 24 additions & 0 deletions bench-orchestrator/tests/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import textwrap
from pathlib import Path

import pytest
from bench_orchestrator.config import Benchmark, Engine, Format
from bench_orchestrator.runner.executor import BenchmarkExecutor

Expand Down Expand Up @@ -33,6 +34,29 @@ def test_build_command_adds_duckdb_cleanup_flag() -> None:
assert cmd[cmd.index("--opt") + 1] == "scale-factor=1.0"


@pytest.mark.parametrize(
("engine", "benchmark", "expected"),
[
(Engine.DUCKDB, Benchmark.TPCDS, True),
(Engine.DUCKDB, Benchmark.CLICKBENCH, True),
(Engine.DUCKDB, Benchmark.TPCH, True),
(Engine.DUCKDB, Benchmark.FINEWEB, False),
(Engine.DATAFUSION, Benchmark.TPCH, True),
(Engine.DATAFUSION, Benchmark.CLICKBENCH, False),
(Engine.LANCE, Benchmark.TPCH, False),
],
)
def test_build_command_turns_on_scan_v2(engine: Engine, benchmark: Benchmark, expected: bool) -> None:
executor = BenchmarkExecutor(Path("/tmp/bench"), engine)

cmd = executor.build_command(benchmark=benchmark, formats=[Format.VORTEX])

if expected:
assert cmd[cmd.index("--scan-v2") + 1] == "true"
else:
assert "--scan-v2" not in cmd


def test_build_command_serializes_vortex_spatial_native_format() -> None:
executor = BenchmarkExecutor(Path("/tmp/duckdb-bench"), Engine.DUCKDB)

Expand Down
11 changes: 4 additions & 7 deletions benchmarks/datafusion-bench/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ struct Args {
#[arg(long, value_delimiter = ',', value_parser = value_parser!(Format))]
formats: Vec<Format>,

/// Use the planner-driven V2 Vortex scan (defaults to true for TPC-H).
#[arg(long, action = clap::ArgAction::Set)]
scan_v2: Option<bool>,
/// Use the planner-driven V2 Vortex scan.
#[arg(long, default_value_t = false, action = clap::ArgAction::Set)]
scan_v2: bool,

#[arg(long = "opt", value_delimiter = ',', value_parser = value_parser!(Opt))]
options: Vec<Opt>,
Expand All @@ -136,10 +136,7 @@ async fn main() -> anyhow::Result<()> {
vortex_bench::configure_allocator();
let args = Args::parse();
let opts = Opts::from(args.options);
let scan_v2 = args.scan_v2.unwrap_or_else(|| {
matches!(args.benchmark, BenchmarkArg::TpcH)
&& !std::env::var("VORTEX_SCAN_V2").is_ok_and(|v| v == "0")
});
let scan_v2 = args.scan_v2;

set_join_set_tracer(get_static_tracer())?;
setup_logging_and_tracing(args.verbose, args.tracing)?;
Expand Down
14 changes: 6 additions & 8 deletions benchmarks/duckdb-bench/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ struct Args {
)]
reuse: bool,

/// Use the planner-driven V2 Vortex scan (defaults to true for TPC-H).
#[arg(long, action = clap::ArgAction::Set)]
scan_v2: Option<bool>,
/// Use the planner-driven V2 Vortex scan.
#[arg(long, default_value_t = false, action = clap::ArgAction::Set)]
scan_v2: bool,
}

#[global_allocator]
Expand All @@ -116,11 +116,9 @@ fn main() -> anyhow::Result<()> {
let args = Args::parse();
let opts = Opts::from(args.options);

let scan_v2 = args.scan_v2.unwrap_or_else(|| {
matches!(args.benchmark, BenchmarkArg::TpcH)
&& !std::env::var("VORTEX_SCAN_V2").is_ok_and(|v| v == "0")
});
vortex_duckdb::set_scan_v2(scan_v2);
if args.scan_v2 {
vortex_duckdb::set_scan_v2(true);
}

setup_logging_and_tracing(args.verbose, args.tracing)?;

Expand Down
4 changes: 4 additions & 0 deletions vortex-duckdb/src/e2e_test/vortex_scan_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,10 @@ fn query_i64_column(path: &str, query: &str) -> Result<Vec<i64>> {
#[case::expression("SELECT a + b FROM ? WHERE a >= 50000 ORDER BY 1")]
#[case::full_scan("SELECT a FROM ? ORDER BY a")]
#[case::empty("SELECT a FROM ? WHERE b > 5000")]
#[case::top_n("SELECT a FROM ? ORDER BY b DESC, a LIMIT 7")]
#[case::filtered_top_n("SELECT a FROM ? WHERE b < 500 ORDER BY a DESC LIMIT 10")]
// DuckDB fetches the columns beyond the sort key by row id, as in ClickBench q23.
#[case::late_materialized_top_n("SELECT * FROM ? WHERE b < 700 ORDER BY a DESC LIMIT 5")]
fn test_vortex_scan_v2_matches_v1(#[case] query: &str) -> Result<()> {
// Many row blocks, so the file is cut into several V2 splits.
let rows = 100_000i64;
Expand Down
32 changes: 28 additions & 4 deletions vortex-duckdb/src/file_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use url::Url;
use vortex::array::ArrayRef;
use vortex::array::VortexSessionExecute as _;
use vortex::array::arrays::struct_::StructArrayExt as _;
use vortex::array::scalar_fn::fns::dynamic::DynamicComparison;
use vortex::dtype::FieldPath;
use vortex::error::VortexExpect;
use vortex::error::VortexResult;
Expand All @@ -28,6 +29,8 @@ use vortex::io::runtime::BlockingRuntime as _;
use vortex::io::std_file::StdFileSystem;
use vortex::layout::LayoutReaderRef;
use vortex::layout::scan::scan_builder::ScanBuilder;
use vortex::layout::scan::scan_builder::referenced_field_masks;
use vortex::layout::scan::split_by::SplitBy;
use vortex::layout::scan::v2::ScanBuilder as V2ScanBuilder;
use vortex::layout::scan::v2::ScanFile;
use vortex::mask::Mask;
Expand Down Expand Up @@ -216,7 +219,18 @@ pub fn reader_initialize(file: &mut OpenFileReader, global: &GlobalState) -> Vor
// global lock.
if global.scan_v2 {
let state = file.scan_state();
let scan = FileScan::try_new(v2_builder(state, global))?;
let dynamic = global
.filter
.filter
.as_ref()
.map(|filter| filter.contains::<DynamicComparison>())
.transpose()?
.unwrap_or(false);
let scan = FileScan::try_new(
v2_builder(state, global),
&v2_boundaries(state, global)?,
dynamic,
)?;
if state.v2.set(scan).is_err() {
vortex_panic!("file initialized twice");
}
Expand Down Expand Up @@ -257,6 +271,16 @@ fn v2_builder(state: &FileScanState, global: &GlobalState) -> V2ScanBuilder {
.with_row_range(rows)
}

/// The rows the file's chunks of the columns the scan reads end at, the boundaries V1 splits on.
fn v2_boundaries(state: &FileScanState, global: &GlobalState) -> VortexResult<Vec<u64>> {
let field_mask = referenced_field_masks(&global.projection, global.filter.filter.as_ref())?;
SplitBy::default().splits(
state.reader.as_ref(),
&(0..state.reader.row_count()),
&field_mask,
)
}

/// What a thread's split yields next.
enum SplitItem {
/// The split has no more rows.
Expand Down Expand Up @@ -301,13 +325,13 @@ fn next_split_item(local: &mut LocalState) -> VortexResult<SplitItem> {
/// the global lock. It only moves references between the file and the local
/// state, and reader_scan drops the retired ones.
pub fn reader_try_initialize_scan(file: &mut OpenFileReader, local: &mut LocalState) -> bool {
// A V2 scan's threads take chunks from the file's scan until it has spawned
// them all, so the file has work for another thread until then.
// A V2 scan's threads take chunks from the file's queue until it is empty,
// so the file has work for one more thread per queued chunk at most.
let v2 = file
.scan_state
.as_ref()
.and_then(|state| state.v2.get())
.map(|scan| (!scan.is_exhausted()).then_some(Split::V2));
.map(|scan| scan.claim().then_some(Split::V2));
let Some(split) = v2.unwrap_or_else(|| file.splits.pop()) else {
// DuckDB releases the file after this call, under the global lock.
// Give the reference of the file to this thread so it drops outside.
Expand Down
Loading
Loading