blob: 241866516e015d128a8dbf780c96341ee7454bdb [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::BTreeMap;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use serde::Deserialize;
/// Canonical table ordering for SQL output (matches TPC-H dependency order).
const TABLE_ORDER: &[&str] = &[
"nation", "region", "part", "supplier", "partsupp", "customer", "orders", "lineitem",
];
/// Common table definition shared across all scale factors (from tables.yaml).
#[derive(Deserialize)]
struct CommonTableConfig {
primary_key: String,
pre_combine_field: String,
record_size_estimate: u32,
}
/// Common tables file (tables.yaml).
#[derive(Deserialize)]
struct CommonConfig {
tables: BTreeMap<String, CommonTableConfig>,
}
/// Per-scale-factor overrides (sf*.yaml).
#[derive(Deserialize)]
struct ScaleFactorOverrides {
shuffle_parallelism: BTreeMap<String, u32>,
create_tables: SparkCommandConfig,
bench: BenchConfig,
}
/// Merged table config used at runtime.
pub struct TableConfig {
pub primary_key: String,
pub pre_combine_field: String,
pub record_size_estimate: u32,
pub shuffle_parallelism: u32,
}
pub struct ScaleFactorConfig {
pub tables: BTreeMap<String, TableConfig>,
pub create_tables: SparkCommandConfig,
pub bench: BenchConfig,
}
#[derive(Deserialize)]
pub struct SparkCommandConfig {
#[serde(default)]
pub spark_conf: BTreeMap<String, String>,
}
#[derive(Deserialize)]
pub struct BenchConfig {
#[serde(default)]
pub warmup: usize,
#[serde(default = "default_iterations")]
pub iterations: usize,
#[serde(default)]
pub spark_conf: BTreeMap<String, String>,
#[serde(default)]
pub datafusion_conf: DataFusionConfig,
}
#[derive(Deserialize, Default)]
pub struct DataFusionConfig {
/// Memory pool limit (e.g., "16g", "512m"); unlimited if not set.
/// Handled specially because it requires creating a memory pool at runtime.
pub memory_limit: Option<String>,
/// Additional DataFusion session config key-value pairs.
/// Keys use DataFusion's dotted config namespace (e.g., "datafusion.execution.target_partitions").
/// Values are passed directly to `SessionConfig::set()`.
#[serde(default, flatten)]
pub settings: BTreeMap<String, String>,
}
fn default_iterations() -> usize {
1
}
impl ScaleFactorConfig {
/// Supported scale factors that have config files.
const SUPPORTED: &[u64] = &[1, 10, 100];
/// Load common table definitions and per-SF overrides, then merge them.
pub fn load(scale_factor: f64) -> Result<Self, Box<dyn std::error::Error>> {
let effective_sf = if scale_factor < 1.0 {
1u64
} else {
let sf = scale_factor as u64;
if !Self::SUPPORTED.contains(&sf) {
return Err(format!(
"Unsupported scale factor {scale_factor}. Supported: {:?}",
Self::SUPPORTED
)
.into());
}
sf
};
let config_dir = std::env::var("TPCH_CONFIG_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("config"));
// Load common table definitions
let common_path = config_dir.join("tables.yaml");
let common_content = std::fs::read_to_string(&common_path)
.map_err(|e| format!("Failed to read {}: {e}", common_path.display()))?;
let common: CommonConfig = serde_yaml::from_str(&common_content)
.map_err(|e| format!("Failed to parse tables.yaml: {e}"))?;
// Load per-SF overrides
let sf_filename = format!("sf{effective_sf}.yaml");
let sf_path = config_dir.join(&sf_filename);
let sf_content = std::fs::read_to_string(&sf_path)
.map_err(|e| format!("Failed to read config {}: {e}", sf_path.display()))?;
let overrides: ScaleFactorOverrides = serde_yaml::from_str(&sf_content)
.map_err(|e| format!("Failed to parse config {sf_filename}: {e}"))?;
// Merge: common tables + per-SF shuffle_parallelism
let mut tables = BTreeMap::new();
for (name, common_table) in common.tables {
let shuffle_parallelism = overrides
.shuffle_parallelism
.get(&name)
.copied()
.unwrap_or(1);
tables.insert(
name,
TableConfig {
primary_key: common_table.primary_key,
pre_combine_field: common_table.pre_combine_field,
record_size_estimate: common_table.record_size_estimate,
shuffle_parallelism,
},
);
}
Ok(Self {
tables,
create_tables: overrides.create_tables,
bench: overrides.bench,
})
}
/// Generate CTAS SQL for creating Hudi tables from parquet sources.
pub fn render_ctas_sql(&self, parquet_base: &str, hudi_base: &str) -> String {
let mut sql = String::new();
for &name in TABLE_ORDER {
let Some(table) = self.tables.get(name) else {
continue;
};
// The catalog registration outlives the data it points at, so
// without this a rerun fails on the previous run's entry.
writeln!(sql, "DROP TABLE IF EXISTS {name};").unwrap();
writeln!(sql, "CREATE TABLE {name} USING hudi").unwrap();
writeln!(sql, "LOCATION '{hudi_base}/{name}'").unwrap();
writeln!(sql, "TBLPROPERTIES (").unwrap();
writeln!(sql, " type = 'cow',").unwrap();
writeln!(sql, " primaryKey = '{}',", table.primary_key).unwrap();
writeln!(sql, " preCombineField = '{}',", table.pre_combine_field).unwrap();
writeln!(sql, " 'hoodie.table.name' = '{name}',").unwrap();
writeln!(
sql,
" 'hoodie.bulkinsert.shuffle.parallelism' = '{}',",
table.shuffle_parallelism
)
.unwrap();
writeln!(
sql,
" 'hoodie.copyonwrite.record.size.estimate' = '{}'",
table.record_size_estimate
)
.unwrap();
writeln!(sql, ") AS SELECT * FROM parquet.`{parquet_base}/{name}/`;").unwrap();
writeln!(sql).unwrap();
}
sql
}
/// Generate benchmark SQL: table registrations followed by query iterations.
pub fn render_bench_sql(
&self,
hudi_base: &str,
query_nums: &[usize],
iterations: usize,
scale_factor: f64,
) -> Result<String, Box<dyn std::error::Error>> {
let mut sql = String::new();
// Register Hudi tables
for &name in TABLE_ORDER {
if self.tables.contains_key(name) {
writeln!(sql, "DROP TABLE IF EXISTS {name};").unwrap();
writeln!(
sql,
"CREATE TABLE {name} USING hudi LOCATION '{hudi_base}/{name}';"
)
.unwrap();
}
}
writeln!(sql).unwrap();
// Per-SF substitution values (TPC-H spec Section 2.4.11.3: FRACTION = 0.0001 / SF)
let q11_fraction = format!("{:.10}", 0.0001 / scale_factor);
// Add queries with bench markers
let queries_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("queries");
for &qn in query_nums {
let qfile = queries_dir.join(format!("q{qn}.sql"));
let query_sql = std::fs::read_to_string(&qfile)
.map_err(|e| format!("Failed to read q{qn}.sql: {e}"))?;
let query_sql = query_sql.replace("${Q11_FRACTION}", &q11_fraction);
for i in 1..=iterations {
writeln!(sql).unwrap();
writeln!(sql, "SELECT 'BENCH_MARKER q{qn} iter{i}' as marker;").unwrap();
write!(sql, "{query_sql}").unwrap();
if !query_sql.ends_with('\n') {
writeln!(sql).unwrap();
}
}
}
Ok(sql)
}
/// Generate spark-submit arguments for a given command, one per line.
pub fn render_spark_args(&self, command: &str) -> Result<Vec<String>, String> {
let spark_conf = match command {
"create-tables" => &self.create_tables.spark_conf,
"bench" => &self.bench.spark_conf,
_ => return Err(format!("Unknown command: {command}")),
};
let mut args = vec!["--master".to_string(), "local[*]".to_string()];
for (key, value) in spark_conf {
args.push("--conf".to_string());
args.push(format!("{key}={value}"));
}
Ok(args)
}
}