blob: b90f655dc7e9f60df4e96828cde7e01482f68805 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use datafusion::sql::sqlparser::dialect::GenericDialect;
use datafusion::sql::sqlparser::parser::Parser;
mod cli;
mod command;
mod context;
mod load;
mod report;
mod run;
pub use cli::{Cli, Command, ExistingPolicyArg, LoadArgs, RunArgs, RuntimeArgs};
pub use command::execute_command;
pub use context::{
build_sql_context, open_catalog_session, BenchmarkRuntimeConfig, CatalogSession,
};
pub use load::{load_parquet_table, ExistingTablePolicy, LoadStatus, TableLoadResult};
pub use report::{write_report, BenchmarkReport, SourceKind, NON_TPC_DISCLOSURE};
pub use run::{
register_parquet_tables, run_query_file, IterationResult, PhysicalMetrics, QueryRunConfig,
QueryRunResult,
};
pub const TPCDS_TABLES: [&str; 24] = [
"call_center",
"catalog_page",
"catalog_returns",
"catalog_sales",
"customer",
"customer_address",
"customer_demographics",
"date_dim",
"time_dim",
"household_demographics",
"income_band",
"inventory",
"item",
"promotion",
"reason",
"ship_mode",
"store",
"store_returns",
"store_sales",
"warehouse",
"web_page",
"web_returns",
"web_sales",
"web_site",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryFile {
pub number: u32,
pub path: PathBuf,
pub statements: Vec<String>,
}
pub fn load_query_files(query_dir: &Path, queries: &[u32]) -> Result<Vec<QueryFile>, String> {
queries
.iter()
.map(|number| {
let path = query_dir.join(format!("q{number}.sql"));
let sql = fs::read_to_string(&path)
.map_err(|error| format!("failed to read {}: {error}", path.display()))?;
let statements = Parser::parse_sql(&GenericDialect, &sql)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))?
.into_iter()
.map(|statement| statement.to_string())
.collect::<Vec<_>>();
if statements.is_empty() {
return Err(format!("query file {} is empty", path.display()));
}
Ok(QueryFile {
number: *number,
path,
statements,
})
})
.collect()
}
pub fn parse_number_selection(
selection: Option<&str>,
min: u32,
max: u32,
) -> Result<Vec<u32>, String> {
let mut values = BTreeSet::new();
let selection = selection.unwrap_or("");
for part in selection
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
{
if let Some((start, end)) = part.split_once('-') {
let start = start
.trim()
.parse::<u32>()
.map_err(|_| format!("invalid selection value '{start}'"))?;
let end = end
.trim()
.parse::<u32>()
.map_err(|_| format!("invalid selection value '{end}'"))?;
if start > end {
return Err(format!("invalid descending range '{part}'"));
}
values.extend(start..=end);
} else {
values.insert(
part.parse::<u32>()
.map_err(|_| format!("invalid selection value '{part}'"))?,
);
}
}
if values.is_empty() {
values.extend(min..=max);
}
if let Some(value) = values.iter().find(|value| **value < min || **value > max) {
return Err(format!("selection value {value} is outside {min}..={max}"));
}
Ok(values.into_iter().collect())
}
#[cfg(test)]
mod tests {
use std::fs;
use clap::Parser;
use datafusion::execution::memory_pool::MemoryLimit;
use super::{
build_sql_context, load_query_files, parse_number_selection, BenchmarkReport,
BenchmarkRuntimeConfig, Cli, Command, QueryRunConfig, SourceKind, TPCDS_TABLES,
};
use tempfile::TempDir;
#[test]
fn query_selection_supports_lists_and_ranges() {
assert_eq!(
parse_number_selection(Some("1,3-5"), 1, 99).unwrap(),
vec![1, 3, 4, 5]
);
}
#[test]
fn query_files_are_external_and_can_contain_multiple_statements() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("q1.sql"), "SELECT 1; SELECT 2;").unwrap();
fs::write(dir.path().join("q3.sql"), "SELECT 3;").unwrap();
let files = load_query_files(dir.path(), &[1, 3]).unwrap();
assert_eq!(
files.iter().map(|file| file.number).collect::<Vec<_>>(),
vec![1, 3]
);
assert_eq!(files[0].statements, vec!["SELECT 1", "SELECT 2"]);
assert_eq!(files[1].statements, vec!["SELECT 3"]);
}
#[test]
fn runtime_config_controls_parallelism_memory_and_spill_path() {
let spill_dir = TempDir::new().unwrap();
let ctx = build_sql_context(&BenchmarkRuntimeConfig {
target_partitions: 3,
batch_size: 4096,
parquet_pushdown_filters: true,
memory_limit_bytes: Some(32 * 1024 * 1024),
spill_dir: Some(spill_dir.path().to_path_buf()),
max_spill_bytes: Some(64 * 1024 * 1024),
})
.unwrap();
assert_eq!(
ctx.ctx()
.state()
.config_options()
.execution
.target_partitions,
3
);
assert_eq!(
ctx.ctx().state().config_options().execution.batch_size,
4096
);
assert!(
ctx.ctx()
.state()
.config_options()
.execution
.parquet
.pushdown_filters
);
assert!(matches!(
ctx.ctx().runtime_env().memory_pool.memory_limit(),
MemoryLimit::Finite(size) if size == 32 * 1024 * 1024
));
assert!(
ctx.ctx().runtime_env().disk_manager.temp_dir_paths()[0].starts_with(spill_dir.path())
);
assert_eq!(
ctx.ctx()
.runtime_env()
.disk_manager
.max_temp_directory_size(),
64 * 1024 * 1024
);
}
#[test]
fn canonical_table_list_contains_all_24_tpcds_tables() {
assert_eq!(TPCDS_TABLES.len(), 24);
assert_eq!(TPCDS_TABLES.first(), Some(&"call_center"));
assert_eq!(TPCDS_TABLES.last(), Some(&"web_site"));
assert!(TPCDS_TABLES.contains(&"store_sales"));
}
#[test]
fn report_json_contains_non_tpc_disclosure_and_round_trips() {
let report = BenchmarkReport::new(
SourceKind::Paimon,
BenchmarkRuntimeConfig::default(),
QueryRunConfig::default(),
"/warehouse".to_string(),
"/data".to_string(),
"/queries".to_string(),
"tpcds".to_string(),
vec![],
);
let json = serde_json::to_string(&report).unwrap();
let decoded: BenchmarkReport = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.source, SourceKind::Paimon);
assert!(decoded.disclosure.contains("non-TPC"));
assert_eq!(decoded.datafusion_version, "54.0.0");
}
#[test]
fn cli_parses_a_paimon_run_without_a_parquet_data_path() {
let cli = Cli::try_parse_from([
"paimon-tpcds-bench",
"run",
"--source",
"paimon",
"--warehouse",
"/warehouse",
"--queries",
"/queries",
"--output",
"/report.json",
"--query",
"1,3-5",
])
.unwrap();
let Command::Run(args) = cli.command else {
panic!("expected run command");
};
assert_eq!(args.source, SourceKind::Paimon);
assert_eq!(args.query.as_deref(), Some("1,3-5"));
assert!(args.data.is_none());
}
#[test]
fn parquet_run_requires_a_data_path() {
let cli = Cli::try_parse_from([
"paimon-tpcds-bench",
"run",
"--source",
"parquet",
"--warehouse",
"/warehouse",
"--queries",
"/queries",
"--output",
"/report.json",
])
.unwrap();
let Command::Run(args) = cli.command else {
panic!("expected run command");
};
assert!(args.validate().unwrap_err().contains("--data"));
}
}