From ef01649a88c5edb46558a20bfc3b32c96cae731e Mon Sep 17 00:00:00 2001 From: Adam Gutglick Date: Tue, 23 Jun 2026 17:11:07 +0100 Subject: [PATCH 1/2] Add clickbench-sorted benchmark Signed-off-by: Adam Gutglick --- .github/workflows/sql-benchmarks.yml | 42 ++++ .../bench_orchestrator/config.py | 1 + benchmarks-website/src/config.js | 9 + benchmarks-website/src/utils.js | 1 + vortex-bench/src/clickbench/benchmark.rs | 91 +++++-- vortex-bench/src/clickbench/data.rs | 228 ++++++++++++++++++ vortex-bench/src/datasets/mod.rs | 7 +- vortex-bench/src/lib.rs | 8 + vortex-bench/src/v3.rs | 12 + 9 files changed, 382 insertions(+), 17 deletions(-) diff --git a/.github/workflows/sql-benchmarks.yml b/.github/workflows/sql-benchmarks.yml index ed86c7b8782..2f737f17c7f 100644 --- a/.github/workflows/sql-benchmarks.yml +++ b/.github/workflows/sql-benchmarks.yml @@ -39,6 +39,29 @@ on: {"engine": "duckdb", "format": "duckdb"} ] }, + { + "id": "clickbench-sorted-nvme", + "subcommand": "clickbench-sorted", + "name": "Clickbench Sorted on NVME", + "data_formats": ["parquet", "vortex", "vortex-compact", "duckdb"], + "pr_targets": [ + {"engine": "datafusion", "format": "parquet"}, + {"engine": "datafusion", "format": "vortex"}, + {"engine": "duckdb", "format": "parquet"}, + {"engine": "duckdb", "format": "vortex"}, + {"engine": "duckdb", "format": "duckdb"} + ], + "develop_targets": [ + {"engine": "datafusion", "format": "parquet"}, + {"engine": "datafusion", "format": "vortex"}, + {"engine": "datafusion", "format": "vortex-compact"}, + {"engine": "datafusion", "format": "lance"}, + {"engine": "duckdb", "format": "parquet"}, + {"engine": "duckdb", "format": "vortex"}, + {"engine": "duckdb", "format": "vortex-compact"}, + {"engine": "duckdb", "format": "duckdb"} + ] + }, { "id": "tpch-nvme", "subcommand": "tpch", @@ -302,6 +325,25 @@ on: {"engine": "duckdb", "format": "vortex"} ] }, + { + "id": "clickbench-sorted-nvme", + "subcommand": "clickbench-sorted", + "name": "Clickbench Sorted on NVME", + "data_formats": ["parquet", "vortex"], + "pr_targets": [ + {"engine": "datafusion", "format": "parquet"}, + {"engine": "datafusion", "format": "vortex"}, + {"engine": "duckdb", "format": "parquet"}, + {"engine": "duckdb", "format": "vortex"} + ], + "develop_targets": [ + {"engine": "datafusion", "format": "parquet"}, + {"engine": "datafusion", "format": "vortex"}, + {"engine": "datafusion", "format": "lance"}, + {"engine": "duckdb", "format": "parquet"}, + {"engine": "duckdb", "format": "vortex"} + ] + }, { "id": "tpch-nvme", "subcommand": "tpch", diff --git a/bench-orchestrator/bench_orchestrator/config.py b/bench-orchestrator/bench_orchestrator/config.py index fb90ac8bd6a..c597e84c6be 100644 --- a/bench-orchestrator/bench_orchestrator/config.py +++ b/bench-orchestrator/bench_orchestrator/config.py @@ -46,6 +46,7 @@ class Benchmark(Enum): TPCH = "tpch" TPCDS = "tpcds" CLICKBENCH = "clickbench" + CLICKBENCH_SORTED = "clickbench-sorted" FINEWEB = "fineweb" GHARCHIVE = "gh-archive" POLARSIGNALS = "polarsignals" diff --git a/benchmarks-website/src/config.js b/benchmarks-website/src/config.js index c6ce9060428..dfe5555019d 100644 --- a/benchmarks-website/src/config.js +++ b/benchmarks-website/src/config.js @@ -14,6 +14,15 @@ export const QUERY_SUITES = [ tags: ["Queries (NVMe)"], hiddenDatasets: ["datafusion:lance"], }, + { + prefix: "clickbench-sorted", + displayName: "Clickbench Sorted", + queryPrefix: "CLICKBENCH SORTED", + description: + "ClickBench queries over data globally sorted by event date and event time", + tags: ["Queries (NVMe)"], + hiddenDatasets: ["datafusion:lance"], + }, { prefix: "statpopgen", displayName: "Statistical and Population Genetics", diff --git a/benchmarks-website/src/utils.js b/benchmarks-website/src/utils.js index 2d6575fe814..140ebebeb6c 100644 --- a/benchmarks-website/src/utils.js +++ b/benchmarks-website/src/utils.js @@ -97,6 +97,7 @@ export function getBenchmarkDescription(categoryName) { 'Compression': 'Measures encoding and decoding throughput (MB/s) for Vortex and Parquet files', 'Compression Size': 'Compares compressed file sizes across different encoding strategies', 'Clickbench': "ClickHouse's analytical benchmark suite on web analytics data", + 'Clickbench Sorted': 'ClickBench queries over data globally sorted by event date and event time', 'Statistical and Population Genetics': 'Statistical and population genetics queries on gnomAD dataset', }; return descriptions[categoryName] || ''; diff --git a/vortex-bench/src/clickbench/benchmark.rs b/vortex-bench/src/clickbench/benchmark.rs index a68ce0b54e8..2885dc29c3b 100644 --- a/vortex-bench/src/clickbench/benchmark.rs +++ b/vortex-bench/src/clickbench/benchmark.rs @@ -41,21 +41,41 @@ impl ClickBenchBenchmark { } } +/// ClickBench sorted by event date and event time. +pub struct ClickBenchSortedBenchmark { + pub queries_file: Option, + pub data_url: Url, +} + +impl ClickBenchSortedBenchmark { + /// Create the sorted ClickBench benchmark, optionally using a remote data directory. + pub fn new(use_remote_data_dir: Option) -> Result { + Ok(Self { + queries_file: None, + data_url: resolve_data_url(use_remote_data_dir.as_deref(), CLICKBENCH_SORTED_NAME)?, + }) + } +} + +fn read_clickbench_queries(queries_file: Option<&str>) -> Result> { + let queries_filepath = match queries_file { + Some(file) => file.into(), + None => Path::new(env!("CARGO_MANIFEST_DIR")).join("clickbench_queries.sql"), + }; + + Ok(fs::read_to_string(queries_filepath)? + .split(';') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .enumerate() + .collect()) +} + #[async_trait::async_trait] impl Benchmark for ClickBenchBenchmark { fn queries(&self) -> Result> { - let queries_filepath = match &self.queries_file { - Some(file) => file.into(), - None => Path::new(env!("CARGO_MANIFEST_DIR")).join("clickbench_queries.sql"), - }; - - Ok(fs::read_to_string(queries_filepath)? - .split(';') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .enumerate() - .collect()) + read_clickbench_queries(self.queries_file.as_deref()) } async fn generate_base_data(&self) -> Result<()> { @@ -70,10 +90,7 @@ impl Benchmark for ClickBenchBenchmark { } fn expected_row_counts(&self) -> Option> { - Some(vec![ - 1, 1, 1, 1, 1, 1, 1, 18, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 4, 1, 10, 10, 10, - 10, 10, 10, 25, 25, 1, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - ]) + Some(clickbench_expected_row_counts()) } fn dataset(&self) -> BenchmarkDataset { @@ -99,6 +116,48 @@ impl Benchmark for ClickBenchBenchmark { } } +#[async_trait::async_trait] +impl Benchmark for ClickBenchSortedBenchmark { + fn queries(&self) -> Result> { + Ok(read_clickbench_queries(self.queries_file.as_deref())? + .into_iter() + .filter(|(idx, _)| CLICKBENCH_SORTED_QUERY_IDS.contains(idx)) + .collect()) + } + + async fn generate_base_data(&self) -> Result<()> { + if self.data_url.scheme() != "file" { + return Ok(()); + } + + generate_sorted_clickbench(CLICKBENCH_SORTED_NAME.to_data_path()).await + } + + fn expected_row_counts(&self) -> Option> { + Some(clickbench_expected_row_counts()) + } + + fn dataset(&self) -> BenchmarkDataset { + BenchmarkDataset::ClickBenchSorted + } + + fn dataset_name(&self) -> &str { + CLICKBENCH_SORTED_NAME + } + + fn dataset_display(&self) -> String { + CLICKBENCH_SORTED_NAME.to_string() + } + + fn data_url(&self) -> &Url { + &self.data_url + } + + fn table_specs(&self) -> Vec { + vec![TableSpec::new("hits", Some(HITS_SCHEMA.clone()))] + } +} + fn clickbench_flavor(flavor: Flavor) -> String { format!("clickbench_{flavor}") } diff --git a/vortex-bench/src/clickbench/data.rs b/vortex-bench/src/clickbench/data.rs index ba2a2104192..f9b85989793 100644 --- a/vortex-bench/src/clickbench/data.rs +++ b/vortex-bench/src/clickbench/data.rs @@ -3,25 +3,43 @@ use std::fmt; use std::fmt::Display; +use std::fs; +use std::fs::File; use std::path::Path; +use std::path::PathBuf; +use std::process::Command; use std::str::FromStr; use std::sync::LazyLock; +use anyhow::Context; use arrow_schema::DataType; use arrow_schema::Field; use arrow_schema::Schema; use arrow_schema::TimeUnit; use clap::ValueEnum; +use parquet::file::reader::FileReader; +use parquet::file::reader::SerializedFileReader; use serde::Deserialize; use serde::Serialize; use tracing::info; use vortex::error::VortexExpect; use crate::Format; +use crate::IdempotentPath; // Re-export for use by clickbench_benchmark pub use crate::conversions::convert_parquet_directory_to_vortex; use crate::datasets::data_downloads::download_data; use crate::datasets::data_downloads::download_many; +use crate::utils::file::temp_download_filepath; + +/// Benchmark and local data directory name for ClickBench sorted by event date/time. +pub const CLICKBENCH_SORTED_NAME: &str = "clickbench-sorted"; +const CLICKBENCH_PARTITIONED_NAME: &str = "clickbench_partitioned"; +const SORTED_SHARD_COUNT: usize = 100; +const SORTED_SHARD_COUNT_U64: u64 = 100; + +/// Zero-based ClickBench query IDs that filter by or order/group on `EventDate`/`EventTime`. +pub const CLICKBENCH_SORTED_QUERY_IDS: &[usize] = &[23, 24, 26, 36, 37, 38, 39, 40, 41, 42]; pub static HITS_SCHEMA: LazyLock = LazyLock::new(|| { use DataType::*; @@ -142,6 +160,14 @@ pub static HITS_SCHEMA: LazyLock = LazyLock::new(|| { ]) }); +/// Expected result row counts for the 43 upstream ClickBench queries. +pub fn clickbench_expected_row_counts() -> Vec { + vec![ + 1, 1, 1, 1, 1, 1, 1, 18, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 4, 1, 10, 10, 10, 10, + 10, 10, 25, 25, 1, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + ] +} + /// Clickbench has two different flavors: /// - Singe - 1 file containing the whole dataset, just under 100 million rows. /// - Partitioned (which we run by default) - 100 files, each containing ~1 million rows, all sharing the same schema. @@ -206,3 +232,205 @@ impl Flavor { Ok(()) } } + +/// Generate globally sorted ClickBench Parquet shards under `basepath`. +pub async fn generate_sorted_clickbench(basepath: impl AsRef) -> anyhow::Result<()> { + let source_base = CLICKBENCH_PARTITIONED_NAME.to_data_path(); + Flavor::Partitioned.download(&source_base).await?; + + let source_parquet_dir = source_base.join(Format::Parquet.name()); + let output_parquet_dir = basepath.as_ref().join(Format::Parquet.name()); + + if output_parquet_dir.exists() { + info!( + "Sorted ClickBench parquet already exists at {}", + output_parquet_dir.display() + ); + return Ok(()); + } + + let temp_root = temp_download_filepath(); + let result = + generate_sorted_clickbench_inner(&source_parquet_dir, &output_parquet_dir, &temp_root); + if result.is_err() { + drop(fs::remove_dir_all(&temp_root)); + } + result +} + +fn generate_sorted_clickbench_inner( + source_parquet_dir: &Path, + output_parquet_dir: &Path, + temp_root: &Path, +) -> anyhow::Result<()> { + let source_rows = parquet_dir_row_count(source_parquet_dir)?; + anyhow::ensure!( + source_rows > 0, + "ClickBench source parquet directory has no rows: {}", + source_parquet_dir.display() + ); + + fs::create_dir_all(temp_root) + .with_context(|| format!("Failed to create temp dir {}", temp_root.display()))?; + + let temp_output_dir = temp_root.join(Format::Parquet.name()); + let duckdb_temp_dir = temp_root.join("duckdb-tmp"); + fs::create_dir_all(&temp_output_dir) + .with_context(|| format!("Failed to create temp dir {}", temp_output_dir.display()))?; + fs::create_dir_all(&duckdb_temp_dir) + .with_context(|| format!("Failed to create temp dir {}", duckdb_temp_dir.display()))?; + + let script = sorted_clickbench_duckdb_script( + source_parquet_dir, + &temp_output_dir, + &duckdb_temp_dir, + source_rows, + ); + let db_path = temp_root.join("sort.duckdb"); + + info!( + "Generating globally sorted ClickBench parquet in {}", + temp_output_dir.display() + ); + + let output = Command::new("duckdb") + .arg(&db_path) + .arg("-c") + .arg(script) + .output() + .context("Failed to run DuckDB CLI while generating sorted ClickBench data")?; + + if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!( + "DuckDB failed generating sorted ClickBench data: stdout=\"{stdout}\", stderr=\"{stderr}\"" + ); + } + + let output_files = parquet_files(&temp_output_dir)?; + anyhow::ensure!( + output_files.len() == SORTED_SHARD_COUNT, + "Expected {SORTED_SHARD_COUNT} sorted ClickBench shards, got {}", + output_files.len() + ); + + let output_rows = parquet_files_row_count(output_files)?; + anyhow::ensure!( + output_rows == source_rows, + "Sorted ClickBench row-count mismatch: source={source_rows}, output={output_rows}" + ); + + if let Some(parent) = output_parquet_dir.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create output dir {}", parent.display()))?; + } + fs::rename(&temp_output_dir, output_parquet_dir).with_context(|| { + format!( + "Failed to move sorted ClickBench parquet from {} to {}", + temp_output_dir.display(), + output_parquet_dir.display() + ) + })?; + + drop(fs::remove_dir_all(temp_root)); + Ok(()) +} + +fn sorted_clickbench_duckdb_script( + source_parquet_dir: &Path, + output_parquet_dir: &Path, + duckdb_temp_dir: &Path, + source_rows: u64, +) -> String { + let source_glob = source_parquet_dir.join("hits_*.parquet"); + let rows_per_shard = source_rows.div_ceil(SORTED_SHARD_COUNT_U64); + let columns = HITS_SCHEMA + .fields() + .iter() + .map(|field| quote_identifier(field.name())) + .collect::>() + .join(", "); + + let mut script = format!( + "\ +PRAGMA temp_directory={temp_dir}; +CREATE TABLE hits_sorted AS + SELECT * + FROM read_parquet({source_glob}) + ORDER BY \"EventDate\", \"EventTime\", \"WatchID\"; +", + temp_dir = sql_string_literal(&duckdb_temp_dir.display().to_string()), + source_glob = sql_string_literal(&source_glob.display().to_string()), + ); + + for shard_idx in 0..SORTED_SHARD_COUNT_U64 { + let start = shard_idx * rows_per_shard; + let end = (start + rows_per_shard).min(source_rows); + let output_path = output_parquet_dir.join(format!("hits_{shard_idx}.parquet")); + script.push_str(&format!( + "\ +COPY ( + SELECT {columns} + FROM hits_sorted + WHERE rowid >= {start} AND rowid < {end} + ORDER BY rowid +) TO {output_path} (FORMAT parquet, COMPRESSION zstd); +", + output_path = sql_string_literal(&output_path.display().to_string()), + )); + } + + script +} + +fn parquet_dir_row_count(parquet_dir: &Path) -> anyhow::Result { + let files = parquet_files(parquet_dir)?; + anyhow::ensure!( + !files.is_empty(), + "No Parquet files found in {}", + parquet_dir.display() + ); + parquet_files_row_count(files) +} + +fn parquet_files(parquet_dir: &Path) -> anyhow::Result> { + let mut files = fs::read_dir(parquet_dir) + .with_context(|| format!("Failed to read parquet dir {}", parquet_dir.display()))? + .map(|entry| entry.map(|entry| entry.path())) + .collect::, _>>() + .with_context(|| format!("Failed to list parquet dir {}", parquet_dir.display()))?; + + files.retain(|path| path.extension().is_some_and(|ext| ext == "parquet")); + files.sort(); + Ok(files) +} + +fn parquet_files_row_count(files: Vec) -> anyhow::Result { + let mut total = 0_u64; + for file_path in files { + let file = File::open(&file_path) + .with_context(|| format!("Failed to open parquet file {}", file_path.display()))?; + let reader = SerializedFileReader::new(file) + .with_context(|| format!("Failed to read parquet metadata {}", file_path.display()))?; + let rows = reader.metadata().file_metadata().num_rows(); + let rows = u64::try_from(rows).with_context(|| { + format!("Parquet row count was negative in {}", file_path.display()) + })?; + total = total.checked_add(rows).with_context(|| { + format!( + "Parquet row count overflow while reading {}", + file_path.display() + ) + })?; + } + Ok(total) +} + +fn sql_string_literal(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + +fn quote_identifier(value: &str) -> String { + format!("\"{}\"", value.replace('"', "\"\"")) +} diff --git a/vortex-bench/src/datasets/mod.rs b/vortex-bench/src/datasets/mod.rs index 3e72ba69e7f..d5c712404d5 100644 --- a/vortex-bench/src/datasets/mod.rs +++ b/vortex-bench/src/datasets/mod.rs @@ -67,6 +67,8 @@ pub enum BenchmarkDataset { TpcDS { scale_factor: String }, #[serde(rename = "clickbench")] ClickBench { flavor: Flavor }, + #[serde(rename = "clickbench-sorted")] + ClickBenchSorted, #[serde(rename = "public-bi")] PublicBi { name: String }, #[serde(rename = "statpopgen")] @@ -86,6 +88,7 @@ impl BenchmarkDataset { BenchmarkDataset::TpcH { .. } => "tpch", BenchmarkDataset::TpcDS { .. } => "tpcds", BenchmarkDataset::ClickBench { .. } => "clickbench", + BenchmarkDataset::ClickBenchSorted => "clickbench-sorted", BenchmarkDataset::PublicBi { .. } => "public-bi", BenchmarkDataset::StatPopGen { .. } => "statpopgen", BenchmarkDataset::PolarSignals { .. } => "polarsignals", @@ -105,6 +108,7 @@ impl Display for BenchmarkDataset { Flavor::Partitioned => write!(f, "clickbench-partitioned"), Flavor::Single => write!(f, "clickbench-single"), }, + BenchmarkDataset::ClickBenchSorted => write!(f, "clickbench-sorted"), BenchmarkDataset::PublicBi { name } => write!(f, "public-bi({name})"), BenchmarkDataset::StatPopGen { n_rows } => write!(f, "statpopgen(n_rows={n_rows})"), BenchmarkDataset::PolarSignals { n_rows } => { @@ -162,7 +166,8 @@ impl BenchmarkDataset { "customer", "lineitem", "nation", "orders", "part", "partsupp", "region", "supplier", ], - BenchmarkDataset::ClickBench { .. } | BenchmarkDataset::PublicBi { .. } => todo!(), + BenchmarkDataset::ClickBench { .. } | BenchmarkDataset::ClickBenchSorted => &["hits"], + BenchmarkDataset::PublicBi { .. } => todo!(), BenchmarkDataset::StatPopGen { .. } => &["statpopgen"], BenchmarkDataset::PolarSignals { .. } => &["stacktraces"], BenchmarkDataset::Fineweb => &["fineweb"], diff --git a/vortex-bench/src/lib.rs b/vortex-bench/src/lib.rs index 30ff45c97a8..8981b4859cf 100644 --- a/vortex-bench/src/lib.rs +++ b/vortex-bench/src/lib.rs @@ -13,6 +13,7 @@ use anyhow::bail; use appian::AppianBenchmark; use clap::ValueEnum; use clickbench::ClickBenchBenchmark; +use clickbench::ClickBenchSortedBenchmark; use clickbench::Flavor; use fineweb::FinewebBenchmark; use itertools::Itertools; @@ -251,6 +252,8 @@ pub enum BenchmarkArg { Appian, #[clap(name = "clickbench")] ClickBench, + #[clap(name = "clickbench-sorted")] + ClickBenchSorted, #[clap(name = "tpch")] TpcH, #[clap(name = "tpcds")] @@ -287,6 +290,11 @@ pub fn create_benchmark(b: BenchmarkArg, opts: &Opts) -> anyhow::Result { + let remote_data_dir = opts.get_as::(REMOTE_DATA_KEY); + let benchmark = ClickBenchSortedBenchmark::new(remote_data_dir)?; + Ok(Box::new(benchmark) as _) + } BenchmarkArg::TpcH => { let scale_factor = opts.get(SCALE_FACTOR_KEY).unwrap_or(DEFAULT_SCALE_FACTOR); let remote_data_dir = opts.get_as::(REMOTE_DATA_KEY); diff --git a/vortex-bench/src/v3.rs b/vortex-bench/src/v3.rs index 48e8a7f1c94..17456efc682 100644 --- a/vortex-bench/src/v3.rs +++ b/vortex-bench/src/v3.rs @@ -288,6 +288,7 @@ fn canonical_tpc_scale_factor(scale_factor: &str) -> String { /// | `TpcH { scale_factor }` | `tpch` | `None` | TPC SF as string (`"1"`, `"10"`, `"100"`, `"1000"`) | Run through `canonical_tpc_scale_factor` so `"1.0"` and `"1"` collapse. | /// | `TpcDS { scale_factor }` | `tpcds` | `None` | TPC SF as string | Same canonicalization as TPC-H. | /// | `ClickBench { flavor: _ }` | `clickbench` | `None` | `None` | Migrate path drops flavor; live emitter matches so historical and live merge. | +/// | `ClickBenchSorted` | `clickbench-sorted` | `None` | `None` | New live-only suite; keep separate from unsorted ClickBench history. | /// | `StatPopGen { n_rows: _ }` | `statpopgen` | `None` | `None` | Migrate path carries no SF for this suite; live drops it for the same reason. | /// | `PolarSignals { n_rows: _ }`| `polarsignals` | `None` | `None` | Same as StatPopGen. | /// | `Fineweb` | `fineweb` | `None` | `None` | | @@ -311,6 +312,7 @@ pub fn benchmark_dataset_dims(d: &BenchmarkDataset) -> (String, Option, // same to keep historical and live records in one `clickbench` group. // Flavor is fixed per CI matrix entry and recoverable from there. BenchmarkDataset::ClickBench { .. } => ("clickbench".to_string(), None, None), + BenchmarkDataset::ClickBenchSorted => ("clickbench-sorted".to_string(), None, None), BenchmarkDataset::PublicBi { name } => ("public-bi".to_string(), Some(name.clone()), None), // StatPopGen / PolarSignals: the migrate path (v2 → v3 backfill) does // not carry a per-record scale factor for these suites, so writing one @@ -726,6 +728,16 @@ mod tests { } } + #[test] + fn clickbench_sorted_dims_are_distinct_from_clickbench() { + let (dataset, variant, scale_factor) = + benchmark_dataset_dims(&BenchmarkDataset::ClickBenchSorted); + + assert_eq!(dataset, "clickbench-sorted"); + assert_eq!(variant, None); + assert_eq!(scale_factor, None); + } + #[test] fn compression_records_lowercase_dataset_for_v2_history_match() { // The v2 → v3 migrate classifier stores `dataset = series.to_lowercase()` From f8cac258efe381943c181d668cfe65d2a6604ee4 Mon Sep 17 00:00:00 2001 From: Adam Gutglick Date: Tue, 23 Jun 2026 17:21:08 +0100 Subject: [PATCH 2/2] better datagen Signed-off-by: Adam Gutglick --- vortex-bench/src/clickbench/data.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/vortex-bench/src/clickbench/data.rs b/vortex-bench/src/clickbench/data.rs index f9b85989793..7347e9fa1ba 100644 --- a/vortex-bench/src/clickbench/data.rs +++ b/vortex-bench/src/clickbench/data.rs @@ -5,9 +5,11 @@ use std::fmt; use std::fmt::Display; use std::fs; use std::fs::File; +use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::process::Stdio; use std::str::FromStr; use std::sync::LazyLock; @@ -293,13 +295,27 @@ fn generate_sorted_clickbench_inner( temp_output_dir.display() ); - let output = Command::new("duckdb") + let mut child = Command::new("duckdb") .arg(&db_path) - .arg("-c") - .arg(script) - .output() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() .context("Failed to run DuckDB CLI while generating sorted ClickBench data")?; + let mut stdin = child + .stdin + .take() + .context("Failed to open DuckDB stdin while generating sorted ClickBench data")?; + stdin + .write_all(script.as_bytes()) + .context("Failed to write sorted ClickBench SQL to DuckDB stdin")?; + drop(stdin); + + let output = child + .wait_with_output() + .context("Failed to wait for DuckDB while generating sorted ClickBench data")?; + if !output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr);