1use crate::progress::IndicatifProgress;
11use crate::{
12 Compression, OutputFormat, Table, TpchGenerator, TpchGeneratorBuilder,
13 DEFAULT_PARQUET_ROW_GROUP_BYTES,
14};
15use clap::builder::TypedValueParser;
16use clap::{ArgAction, Parser};
17use log::{info, LevelFilter};
18use std::io::{self, IsTerminal};
19use std::path::PathBuf;
20use std::str::FromStr;
21use std::sync::Arc;
22
23#[derive(Parser)]
24#[command(name = "tpchgen")]
25#[command(version)]
26#[command(
27 about = "TPC-H Data Generator",
29 long_about = r#"
31TPCH Data Generator (https://github.com/clflushopt/tpchgen-rs)
32
33By default each table is written to a single file named <output_dir>/<table>.<format>
34
35If `--part` option is specified, each table is written to a subdirectory in
36multiple files named <output_dir>/<table>/<table>.<part>.<format>
37
38Examples
39
40# Generate all tables at scale factor 1 (1GB) in TBL format (default) to /tmp/tpch directory:
41
42tpchgen-cli -s 1 --output-dir=/tmp/tpch
43
44# Generate all tables in CSV format:
45
46tpchgen-cli csv -s 1 --output-dir=/tmp/tpch
47
48# Generate scale factor one in CSV format with tab delimiter:
49
50tpchgen-cli csv -s 1 --delimiter='\t' --output-dir=/tmp/tpch
51
52# Generate the lineitem table at scale factor 100 in 10 Apache Parquet files to
53# /tmp/tpch/lineitem:
54
55tpchgen-cli parquet -s 100 --tables=lineitem --parts=10 --output-dir=/tmp/tpch
56
57# Generate scale factor one in current directory, seeing debug output
58
59RUST_LOG=debug tpchgen-cli -s 1 --output-dir=/tmp/tpch
60"#,
61 args_conflicts_with_subcommands = true
62)]
63pub struct Cli {
64 #[command(subcommand)]
65 command: Option<Commands>,
66
67 #[command(flatten)]
71 args: TopLevelArgs,
72}
73
74#[derive(clap::Subcommand)]
75enum Commands {
76 Tbl(TblArgs),
78 Csv(CsvArgs),
80 Parquet(ParquetArgs),
82}
83
84#[derive(clap::Args)]
85struct CommonArgs {
86 #[arg(short, long, default_value_t = 1.)]
88 scale_factor: f64,
89
90 #[arg(short, long, default_value = ".")]
92 output_dir: PathBuf,
93
94 #[arg(short = 'T', long = "tables", value_delimiter = ',', value_parser = TableValueParser)]
96 tables: Option<Vec<Table>>,
97
98 #[arg(short, long)]
100 parts: Option<i32>,
101
102 #[arg(long)]
104 part: Option<i32>,
105
106 #[arg(short, long, default_value_t = num_cpus::get())]
108 num_threads: usize,
109
110 #[arg(short, long, default_value_t = false, conflicts_with = "quiet")]
115 verbose: bool,
116
117 #[arg(short, long, default_value_t = false, conflicts_with = "verbose")]
119 quiet: bool,
120
121 #[arg(long, default_value_t = false)]
123 stdout: bool,
124
125 #[arg(long = "no-progress", action = ArgAction::SetFalse, default_value_t = true)]
130 progress_bars_enabled: bool,
131}
132
133impl CommonArgs {
134 fn builder(self, format: OutputFormat) -> TpchGeneratorBuilder {
137 let progress = self
138 .should_show_progress_bars()
139 .then(|| Arc::new(IndicatifProgress::new()));
140
141 let mut builder = TpchGenerator::builder()
142 .with_scale_factor(self.scale_factor)
143 .with_output_dir(self.output_dir)
144 .with_format(format)
145 .with_num_threads(self.num_threads)
146 .with_stdout(self.stdout);
147
148 if let Some(tables) = self.tables {
149 builder = builder.with_tables(tables);
150 }
151 if let Some(parts) = self.parts {
152 builder = builder.with_parts(parts);
153 }
154 if let Some(part) = self.part {
155 builder = builder.with_part(part);
156 }
157
158 configure_logging(
159 self.verbose,
160 self.quiet,
161 progress.as_ref().map(|progress| progress.log_writer()),
162 );
163 if let Some(progress) = progress {
164 builder = builder.with_progress_tracker(progress);
165 }
166
167 builder
168 }
169
170 fn should_show_progress_bars(&self) -> bool {
171 self.progress_bars_enabled && !self.quiet && !self.stdout && io::stderr().is_terminal()
175 }
176}
177
178#[derive(clap::Args)]
179struct TopLevelArgs {
180 #[command(flatten)]
181 common: CommonArgs,
182
183 #[arg(short, long, hide = true)]
187 format: Option<OutputFormat>,
188
189 #[arg(short = 'c', long, hide = true)]
191 parquet_compression: Option<Compression>,
192
193 #[arg(long, hide = true)]
195 parquet_row_group_bytes: Option<i64>,
196}
197
198#[derive(clap::Args)]
199struct TblArgs {
200 #[command(flatten)]
201 common: CommonArgs,
202}
203
204#[derive(clap::Args)]
205struct CsvArgs {
206 #[command(flatten)]
207 common: CommonArgs,
208
209 #[arg(long, default_value = ",", value_parser = parse_delimiter)]
216 delimiter: char,
217}
218
219#[derive(clap::Args)]
220struct ParquetArgs {
221 #[command(flatten)]
222 common: CommonArgs,
223
224 #[arg(short = 'c', long, default_value = "SNAPPY")]
238 compression: Compression,
239
240 #[arg(long, default_value_t = DEFAULT_PARQUET_ROW_GROUP_BYTES)]
253 row_group_bytes: i64,
254}
255
256fn parse_delimiter(s: &str) -> Result<char, String> {
261 let parsed = match s {
263 "\\t" => '\t',
264 "\\n" => '\n',
265 "\\r" => '\r',
266 "\\\\" => '\\',
267 _ => {
268 let chars: Vec<char> = s.chars().collect();
270 if chars.len() != 1 {
271 return Err(format!(
272 "Delimiter must be a single character or escape sequence (\\t, \\n, \\r, \\\\), got: '{}'",
273 s
274 ));
275 }
276 chars[0]
277 }
278 };
279 if !parsed.is_ascii() {
280 return Err(format!(
281 "Delimiter must be an ASCII character, got: '{}'",
282 parsed
283 ));
284 }
285 Ok(parsed)
286}
287
288#[derive(Debug, Clone)]
290struct TableValueParser;
291
292impl TypedValueParser for TableValueParser {
293 type Value = Table;
294
295 fn parse_ref(
297 &self,
298 cmd: &clap::Command,
299 _: Option<&clap::Arg>,
300 value: &std::ffi::OsStr,
301 ) -> Result<Self::Value, clap::Error> {
302 let value = value
303 .to_str()
304 .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd))?;
305 Table::from_str(value)
306 .map_err(|_| clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd))
307 }
308
309 fn possible_values(
310 &self,
311 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
312 Some(Box::new(
313 [
314 clap::builder::PossibleValue::new("region").help("Region table (alias: r)"),
315 clap::builder::PossibleValue::new("nation").help("Nation table (alias: n)"),
316 clap::builder::PossibleValue::new("supplier").help("Supplier table (alias: s)"),
317 clap::builder::PossibleValue::new("customer").help("Customer table (alias: c)"),
318 clap::builder::PossibleValue::new("part").help("Part table (alias: P)"),
319 clap::builder::PossibleValue::new("partsupp").help("PartSupp table (alias: S)"),
320 clap::builder::PossibleValue::new("orders").help("Orders table (alias: O)"),
321 clap::builder::PossibleValue::new("lineitem").help("LineItem table (alias: L)"),
322 ]
323 .into_iter(),
324 ))
325 }
326}
327
328impl Cli {
329 pub async fn run(self) -> io::Result<()> {
331 match self.command {
332 Some(Commands::Tbl(args)) => args.run().await,
333 Some(Commands::Csv(args)) => args.run().await,
334 Some(Commands::Parquet(args)) => args.run().await,
335 None => self.run_default().await,
336 }
337 }
338
339 async fn run_default(self) -> io::Result<()> {
340 let (format, subcommand) = if let Some(format) = self.args.format {
342 let subcommand = match format {
343 OutputFormat::Parquet => "parquet",
344 OutputFormat::Csv => "csv",
345 OutputFormat::Tbl => "tbl",
346 };
347 (format, Some(subcommand))
348 } else {
349 (OutputFormat::Tbl, None)
350 };
351
352 let mut builder = self.args.common.builder(format);
353 if let Some(subcommand) = subcommand {
354 log::warn!(
355 "The --format flag will be removed in v4.0.0. Use `tpchgen-cli {subcommand}` instead."
356 );
357 }
358
359 if let Some(parquet_compression) = self.args.parquet_compression {
360 if format == OutputFormat::Parquet {
361 log::warn!("The --parquet-compression flag is deprecated. Use 'tpchgen-cli parquet --compression=...' instead");
362 builder = builder.with_parquet_compression(parquet_compression);
363 } else {
364 log::warn!("--parquet-compression ignored: output format is not parquet");
365 }
366 }
367
368 if let Some(parquet_row_group_bytes) = self.args.parquet_row_group_bytes {
369 if format == OutputFormat::Parquet {
370 log::warn!("The --parquet-row-group-bytes flag is deprecated. Use 'tpchgen-cli parquet --row-group-bytes=...' instead");
371 builder = builder.with_parquet_row_group_bytes(parquet_row_group_bytes);
372 } else {
373 log::warn!("--parquet-row-group-bytes ignored: output format is not parquet");
374 }
375 }
376
377 builder.build().generate().await
378 }
379}
380
381impl TblArgs {
382 async fn run(self) -> io::Result<()> {
383 self.common
384 .builder(OutputFormat::Tbl)
385 .build()
386 .generate()
387 .await
388 }
389}
390
391impl CsvArgs {
392 async fn run(self) -> io::Result<()> {
393 self.common
394 .builder(OutputFormat::Csv)
395 .with_csv_delimiter(self.delimiter)
396 .build()
397 .generate()
398 .await
399 }
400}
401
402impl ParquetArgs {
403 async fn run(self) -> io::Result<()> {
404 self.common
405 .builder(OutputFormat::Parquet)
406 .with_parquet_compression(self.compression)
407 .with_parquet_row_group_bytes(self.row_group_bytes)
408 .build()
409 .generate()
410 .await
411 }
412}
413
414fn configure_logging(
415 verbose: bool,
416 quiet: bool,
417 log_writer: Option<Box<dyn io::Write + Send + 'static>>,
418) {
419 let mut builder = env_logger::builder();
420 if quiet {
421 builder.filter_level(LevelFilter::Error);
423 } else if verbose {
424 builder.filter_level(LevelFilter::Info);
425 } else {
426 builder.filter_level(LevelFilter::Warn).parse_default_env();
428 }
429 if let Some(log_writer) = log_writer {
430 builder.target(env_logger::Target::Pipe(log_writer));
431 }
432
433 builder.init();
434
435 if verbose {
436 info!("Verbose output enabled (ignoring RUST_LOG environment variable)");
437 }
438}