Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8b80703
Add a switchable planner-driven scan v2 path for DataFusion
joseph-isaacs Oct 20, 2026
6b7b44d
Share one V2 segment source per file across DataFusion partitions
joseph-isaacs Oct 20, 2026
3ba184e
List a compiled scan's segments and drop the reach walk
joseph-isaacs Oct 22, 2026
3aa00b8
Plan a V2 scan as a chain of planners from one prepared scan
joseph-isaacs Oct 22, 2026
8b17f7b
Optimize a zoned data plan to its data child
joseph-isaacs Oct 22, 2026
40ce998
Share one V2 scan cache per file across DataFusion partitions
joseph-isaacs Oct 22, 2026
cbde02c
Drive a V2 scan's run from its stream, with compute on spawn_cpu
joseph-isaacs Oct 22, 2026
460725f
Build a prune planner with the planner it continues into
joseph-isaacs Oct 22, 2026
4797a34
Spawn a scan's chunks as independent planners and run them beside eac…
joseph-isaacs Oct 22, 2026
22aa027
Dedup a planner's IO hints with a set
joseph-isaacs Oct 22, 2026
bb10f65
Run a V2 scan's chunks beside each other only as far as the engine do…
joseph-isaacs Oct 22, 2026
d091576
Expose a V2 scan's planners and prepared plans; drive them elsewhere
joseph-isaacs Oct 22, 2026
25e5f42
Compose and drive a V2 scan in DataFusion
joseph-isaacs Oct 22, 2026
d858c4b
Drive a V2 scan of a file for callers without an engine
joseph-isaacs Oct 22, 2026
af81c35
Compose a V2 scan's planners in the DataFusion opener
joseph-isaacs Oct 22, 2026
4cad9a8
Drop is_proved: a proved proof lists no segment
joseph-isaacs Oct 22, 2026
da8a7c8
Call the zone-map plans pruning plans, not proofs
joseph-isaacs Oct 22, 2026
5c130fb
Speculate the next chunk's reads while pruning waits; batch root-run …
joseph-isaacs Oct 22, 2026
cf7117f
Evaluate every pruning plan at once
joseph-isaacs Oct 22, 2026
c6b2ad9
Drive the root run on one task and run the first chunk on it
joseph-isaacs Oct 22, 2026
68c08ec
Snap a partition's planning-time rows to the file's natural splits
joseph-isaacs Oct 22, 2026
3905421
Speculate a chunk's projection as announcements beside its predicate …
joseph-isaacs Oct 22, 2026
e26781b
Register a chunk's speculated reads beside the zone-map fetch they ov…
joseph-isaacs Oct 22, 2026
b9350ec
Let a session's announcement take the one speculated for its segment
joseph-isaacs Oct 22, 2026
b73d248
Share a speculated read with every session that fetches its segment
joseph-isaacs Oct 22, 2026
a8700b4
Cut a partition's chunks from its own start, not a grid over the file
joseph-isaacs Oct 22, 2026
3294ef5
Speculate every chunk of the partition while its zone maps are read
joseph-isaacs Oct 22, 2026
816ce94
Share a speculated announcement with every session once fetched
joseph-isaacs Oct 22, 2026
17f46e6
Hand a run's spawned chunks to the consumer together
joseph-isaacs Oct 22, 2026
a203827
Speculate the projection's reads beside the predicates', not announce…
joseph-isaacs Oct 22, 2026
44e8d1b
Report what a V2 scan's stages did as metrics
joseph-isaacs Oct 22, 2026
ec78689
Read a projection-only partition's segments up front, shared by its c…
joseph-isaacs Oct 22, 2026
19b1f7d
Mark the start and end of every phase of a partition
joseph-isaacs Oct 22, 2026
79d2062
Record what the DataFusion driver runs, beside the scan's stage metrics
joseph-isaacs Oct 22, 2026
629b0e7
Build each projection and filter plan once per file, the other partit…
joseph-isaacs Oct 22, 2026
141570f
Intern a scan's expressions per file, so partitions share its plans a…
joseph-isaacs Oct 22, 2026
4765068
Cut a filtered partition into a few chunks, a quarter of its rows each
joseph-isaacs Oct 23, 2026
6ea72d8
Probe interned expressions under a read lock
joseph-isaacs Oct 23, 2026
4ca88fa
Read a partition's zone maps on their own, ahead of the data they sit…
joseph-isaacs Oct 23, 2026
b817434
Read ahead only a few chunks of a file in an object store
joseph-isaacs Oct 23, 2026
28e7298
Keep a file's scan cache with its cached footer, across queries
joseph-isaacs Oct 23, 2026
48cbf38
Round a filtered partition's quarter up, so it is four chunks and not…
joseph-isaacs Oct 23, 2026
d735965
Give every root admitted to a partition's run an IO session of its own
joseph-isaacs Oct 23, 2026
0c42ba4
Bound the bench's S3 idle connection pool, so every query runs in the…
joseph-isaacs Oct 23, 2026
d389a2b
EXPERIMENT: one segment source per V2 partition, as V1
joseph-isaacs Oct 23, 2026
e922dfc
EXPERIMENT: let a manually dispatched SQL benchmark comment on #10765
joseph-isaacs Oct 24, 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
6 changes: 4 additions & 2 deletions .github/workflows/sql-bench-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@ jobs:
cat comment.md >> "$GITHUB_STEP_SUMMARY"

- name: Comment PR
if: inputs.mode == 'pr' && github.event.pull_request.head.repo.fork == false
if: inputs.mode == 'pr'
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3
with:
pr-number: 10765 # EXPERIMENT: run by workflow_dispatch, which has no PR context
file-path: comment.md
# There is exactly one comment per comment-tag. If a comment with this tag already exists,
# this action will *update* the comment instead of posting a new comment. The preset is
Expand All @@ -264,9 +265,10 @@ jobs:
comment-tag: bench-pr-comment-${{ matrix.id }}-${{ inputs.matrix_preset }}

- name: Comment PR on failure
if: failure() && inputs.mode == 'pr' && github.event.pull_request.head.repo.fork == false
if: failure() && inputs.mode == 'pr'
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3
with:
pr-number: 10765 # EXPERIMENT: run by workflow_dispatch, which has no PR context
message: |
# 🚨🚨🚨❌❌❌ SQL BENCHMARK FAILED ❌❌❌🚨🚨🚨

Expand Down
17 changes: 12 additions & 5 deletions benchmarks/compress-bench/src/vortex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@

use std::io::Cursor;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;

use anyhow::Result;
use anyhow::bail;
use arrow_schema::Field;
use async_trait::async_trait;
use bytes::Bytes;
use futures::StreamExt;
use futures::pin_mut;
use vortex::array::IntoArray;
use vortex::array::VortexSessionExecute;
use vortex::dtype::FieldNames;
use vortex::expr::root;
use vortex::expr::select;
use vortex::file::OpenOptionsSessionExt;
use vortex::file::WriteOptionsSessionExt;
use vortex::file::v2::ScanDriver;
use vortex::io::session::RuntimeSessionExt;
use vortex::layout::scan::v2::ScanBuilder;
use vortex_arrow::ArrowSessionExt;
use vortex_bench::Format;
Expand Down Expand Up @@ -96,13 +99,17 @@ async fn vortex_decompress_read(data: Bytes) -> Result<()> {
.optimize_recursive()?;
scan = scan.with_projection(projection);
}
let schema = Arc::new(SESSION.arrow().to_arrow_schema(scan.dtype())?);
let schema = SESSION.arrow().to_arrow_schema(scan.dtype())?;
let field = Field::new_struct("", schema.fields().clone(), false);

let stream = scan.into_record_batch_stream(schema)?;
let stream = ScanDriver::new(SESSION.handle(), 4, true).run(scan);
pin_mut!(stream);

while let Some(batch) = stream.next().await {
let _batch = batch?;
while let Some(chunk) = stream.next().await {
let mut ctx = SESSION.create_execution_ctx();
let _batch = SESSION
.arrow()
.execute_arrow(chunk?, Some(&field), &mut ctx)?;
}
Ok(())
}
29 changes: 20 additions & 9 deletions benchmarks/datafusion-bench/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ use datafusion::execution::runtime_env::RuntimeEnvBuilder;
use datafusion::prelude::SessionConfig;
use datafusion::prelude::SessionContext;
use datafusion_common::GetExt;
use object_store::ClientConfigKey;
use object_store::ObjectStore;
use object_store::aws::AmazonS3Builder;
use object_store::aws::AmazonS3ConfigKey;
use object_store::gcp::GoogleCloudStorageBuilder;
use object_store::local::LocalFileSystem;
use url::Url;
Expand Down Expand Up @@ -65,11 +67,18 @@ pub fn make_object_store(
match source.scheme() {
"s3" => {
let bucket_name = &source[url::Position::BeforeHost..url::Position::AfterHost];
let s3 = Arc::new(
AmazonS3Builder::from_env()
.with_bucket_name(bucket_name)
.build()?,
);
// A scan's burst of reads opens a few hundred connections, and queries reusing
// them all from an unbounded idle pool alternate between a fast and a slow mode.
// A bounded pool keeps every query in the fast mode; `AWS_POOL_MAX_IDLE_PER_HOST`
// overrides it like the other `AWS_` client options.
let mut builder = AmazonS3Builder::from_env().with_bucket_name(bucket_name);
if std::env::var_os("AWS_POOL_MAX_IDLE_PER_HOST").is_none() {
builder = builder.with_config(
AmazonS3ConfigKey::Client(ClientConfigKey::PoolMaxIdlePerHost),
"32",
);
}
let s3 = Arc::new(builder.build()?);
session.register_object_store(
&Url::parse(&format!("s3://{bucket_name}/"))?,
Arc::<object_store::aws::AmazonS3>::clone(&s3),
Expand Down Expand Up @@ -98,13 +107,15 @@ pub fn make_object_store(
}
}

pub fn format_to_df_format(format: Format) -> anyhow::Result<Arc<dyn FileFormat>> {
pub fn format_to_df_format(format: Format, scan_v2: bool) -> anyhow::Result<Arc<dyn FileFormat>> {
let mut options = vortex_table_options();
options.scan_v2 = scan_v2;
Ok(match format {
Format::Csv => Arc::new(CsvFormat::default()) as _,
Format::Parquet => Arc::new(ParquetFormat::new()),
Format::OnDiskVortex | Format::VortexCompact | Format::VortexSpatialNative => Arc::new(
VortexFormat::new_with_options(SESSION.clone(), vortex_table_options()),
),
Format::OnDiskVortex | Format::VortexCompact | Format::VortexSpatialNative => {
Arc::new(VortexFormat::new_with_options(SESSION.clone(), options))
}
Format::ArrowIpc | Format::OnDiskDuckDB | Format::Lance | Format::LitData => {
anyhow::bail!("Format {format} cannot be turned into a DataFusion `FileFormat`")
}
Expand Down
16 changes: 13 additions & 3 deletions benchmarks/datafusion-bench/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ 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>,

#[arg(long = "opt", value_delimiter = ',', value_parser = value_parser!(Opt))]
options: Vec<Opt>,
}
Expand All @@ -132,6 +136,10 @@ 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")
});

set_join_set_tracer(get_static_tracer())?;
setup_logging_and_tracing(args.verbose, args.tracing)?;
Expand Down Expand Up @@ -191,7 +199,7 @@ async fn main() -> anyhow::Result<()> {
session.sql(&sql).await?.collect().await?;
}
datafusion_bench::make_object_store(&session, benchmark.data_url())?;
register_benchmark_tables(&session, benchmark, format).await?;
register_benchmark_tables(&session, benchmark, format, scan_v2).await?;
Ok((session, format))
}
},
Expand Down Expand Up @@ -255,12 +263,14 @@ async fn register_benchmark_tables<B: Benchmark + ?Sized>(
session: &SessionContext,
benchmark: &B,
format: Format,
scan_v2: bool,
) -> anyhow::Result<()> {
if use_scan_api() && matches!(format, Format::OnDiskVortex | Format::VortexCompact) {
if !scan_v2 && use_scan_api() && matches!(format, Format::OnDiskVortex | Format::VortexCompact)
{
register_v2_tables(session, benchmark, format).await
} else {
let benchmark_base = benchmark.data_url().join(&format!("{}/", format.name()))?;
let file_format = format_to_df_format(format)?;
let file_format = format_to_df_format(format, scan_v2)?;

for table in benchmark.table_specs().iter() {
let pattern = benchmark.pattern(table.name, format);
Expand Down
8 changes: 5 additions & 3 deletions vortex-bench/src/random_access/take.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use vortex::array::stream::ArrayStreamExt;
use vortex::buffer::Buffer;
use vortex::file::OpenOptionsSessionExt;
use vortex::file::VortexFile;
use vortex::file::v2::ScanDriver;
use vortex::io::session::RuntimeSessionExt;
use vortex::layout::scan::v2::ScanBuilder;
use vortex::scan::strict_sorted_buffer::StrictSortedBuffer;

Expand Down Expand Up @@ -190,9 +192,9 @@ impl RandomAccessor for VortexRandomAccessor {

async fn take(&self, indices: &[u64]) -> anyhow::Result<RandomAccessorRet> {
let indices = StrictSortedBuffer::try_new(Buffer::from(indices.to_vec()))?;
let stream = ScanBuilder::new(SESSION.clone(), self.file.scan_file())
.with_row_indices(indices)
.into_stream()?;
let scan =
ScanBuilder::new(SESSION.clone(), self.file.scan_file()).with_row_indices(indices);
let stream = ScanDriver::new(SESSION.handle(), 4, true).run(scan);
let array = ArrayStreamAdapter::new(self.file.dtype().clone(), stream)
.read_all()
.await?;
Expand Down
18 changes: 18 additions & 0 deletions vortex-datafusion/src/persistent/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ use datafusion_common::HashMap;
use datafusion_execution::cache::cache_manager::FileMetadata;
use vortex::file::Footer;
use vortex::file::VortexFile;
use vortex::layout::scan::v2::ScanCache;

/// Cached Vortex file metadata for use with DataFusion's [`FileMetadataCache`].
///
/// [`FileMetadataCache`]: datafusion_execution::cache::cache_manager::FileMetadataCache
pub struct CachedVortexMetadata {
footer: Footer,
/// What V2 scans of the file derived from its layout: the lowered plan, each projection's
/// and filter's optimized plans, the zones a filter has pruned and the dictionary values
/// read. Kept with the footer so later queries of the file plan nothing again. Its size
/// is not counted in [`FileMetadata::memory_size`], which is taken when the entry is put.
scan_cache: ScanCache,
}

impl CachedVortexMetadata {
Expand All @@ -19,9 +25,21 @@ impl CachedVortexMetadata {
let footer = vortex_file.footer();
Self {
footer: footer.clone(),
scan_cache: ScanCache::default(),
}
}

/// Keeps `scan_cache` with the entry, the one the partitions opening the file share.
pub fn with_scan_cache(mut self, scan_cache: ScanCache) -> Self {
self.scan_cache = scan_cache;
self
}

/// The scan cache of V2 scans of the file.
pub fn scan_cache(&self) -> &ScanCache {
&self.scan_cache
}

/// Get the cached footer.
pub fn footer(&self) -> &Footer {
&self.footer
Expand Down
45 changes: 42 additions & 3 deletions vortex-datafusion/src/persistent/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ extensions_options! {
/// [`ParquetFormatFactory::new_with_options`]: https://docs.rs/datafusion/latest/datafusion/datasource/file_format/parquet/struct.ParquetFormatFactory.html#method.new_with_options
/// [`JsonFormatFactory::new_with_options`]: https://docs.rs/datafusion/latest/datafusion/datasource/file_format/json/struct.JsonFormatFactory.html#method.new_with_options
pub struct VortexTableOptions {
/// Use the planner-driven V2 scan executor for supported layouts.
///
/// Disabled by default. Select V2 before creating a table with
/// `SET vortex.scan_v2 = true` (with the `vortex` options extension registered),
/// or for one table with `OPTIONS(scan_v2 'true')`. In Rust, set
/// `VortexTableOptions { scan_v2: true, ..Default::default() }` on the factory.
pub scan_v2: bool, default = false
/// The number of bytes to read when parsing a file footer.
///
/// Values smaller than `MAX_POSTSCRIPT_SIZE + EOF_SIZE` will be clamped to that minimum
Expand Down Expand Up @@ -1033,17 +1040,18 @@ mod tests {
Ok(())
}

#[rstest]
#[tokio::test]
async fn configure_format_source() -> anyhow::Result<()> {
async fn configure_format_source(#[values(false, true)] scan_v2: bool) -> anyhow::Result<()> {
let ctx = TestSessionContext::default();

ctx.session
.sql(
"CREATE EXTERNAL TABLE my_tbl \
&format!("CREATE EXTERNAL TABLE my_tbl \
(c1 VARCHAR NOT NULL, c2 INT NOT NULL) \
STORED AS vortex \
LOCATION 'table/' \
OPTIONS( footer_initial_read_size_bytes '12345', predicate_pushdown 'false', scan_concurrency '3' );",
OPTIONS( footer_initial_read_size_bytes '12345', predicate_pushdown 'false', scan_concurrency '3', scan_v2 '{scan_v2}' );"),
)
.await?
.collect()
Expand All @@ -1061,6 +1069,35 @@ mod tests {
assert_eq!(format.options().footer_initial_read_size_bytes, 12345);
assert!(!format.options().predicate_pushdown);
assert_eq!(format.options().scan_concurrency, Some(3));
assert_eq!(format.options().scan_v2, scan_v2);
Ok(())
}

#[rstest]
#[tokio::test]
async fn session_scan_version(#[values(false, true)] scan_v2: bool) -> anyhow::Result<()> {
let factory = Arc::new(VortexFormatFactory::new());
let ctx = TestSessionContext::new_with_factory(Arc::clone(&factory));
ctx.session
.state_ref()
.write()
.config_mut()
.options_mut()
.extensions
.insert(VortexTableOptions::default());

for enabled in [scan_v2, !scan_v2] {
ctx.session
.sql(&format!("SET vortex.scan_v2 = {enabled}"))
.await?
.collect()
.await?;
let format = factory.create(&ctx.session.state(), &Default::default())?;
let format = format
.downcast_ref::<VortexFormat>()
.ok_or_else(|| anyhow::anyhow!("expected VortexFormat"))?;
assert_eq!(format.options().scan_v2, enabled);
}
Ok(())
}

Expand All @@ -1076,6 +1113,7 @@ mod tests {
#[test]
fn format_plumbs_source_options() -> anyhow::Result<()> {
let opts = VortexTableOptions {
scan_v2: true,
projection_pushdown: true,
predicate_pushdown: false,
scan_concurrency: Some(3),
Expand All @@ -1095,6 +1133,7 @@ mod tests {
);
assert_eq!(source.options().predicate_pushdown, opts.predicate_pushdown);
assert_eq!(source.options().scan_concurrency, opts.scan_concurrency);
assert_eq!(source.options().scan_v2, opts.scan_v2);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions vortex-datafusion/src/persistent/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ fn metric_value_to_datafusion(name: &str, metric: &MetricValue) -> Vec<Datafusio
MetricValue::Timer(timer) => {
let mut res = Vec::new();
res.push(df_counter(format!("{name}_count"), timer.count()));
res.push(df_timer(format!("{name}_total"), timer.total()));

if !timer.is_empty() {
let max = timer.quantile(1.0).vortex_expect("must not be empty");
Expand Down
1 change: 1 addition & 0 deletions vortex-datafusion/src/persistent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod format;
pub mod metrics;
mod opener;
pub mod reader;
mod scan_v2;
mod sink;
mod source;
mod stream;
Expand Down
Loading
Loading