tpchgen_cli/lib.rs
1//! TPC-H Data Generator Library
2//!
3//! This crate provides both a command-line tool and a library for generating
4//! TPC-H benchmark data in various formats (TBL, CSV, Parquet).
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use tpchgen_cli::{TpchGenerator, Table, OutputFormat};
10//! use std::path::PathBuf;
11//!
12//! # async fn example() -> std::io::Result<()> {
13//! let generator = TpchGenerator::builder()
14//! .with_scale_factor(10.0)
15//! .with_output_dir(PathBuf::from("./data"))
16//! .with_tables(vec![Table::Customer, Table::Orders])
17//! .with_format(OutputFormat::Parquet)
18//! .with_num_threads(8)
19//! .build();
20//!
21//! generator.generate().await?;
22//! # Ok(())
23//! # }
24//! ```
25
26pub use crate::plan::{GenerationPlan, DEFAULT_PARQUET_ROW_GROUP_BYTES};
27pub use ::parquet::basic::Compression;
28
29pub mod csv;
30pub mod generate;
31pub mod output_plan;
32pub mod parquet;
33pub mod plan;
34#[cfg(not(feature = "progress"))]
35mod progress;
36#[cfg(feature = "progress")]
37pub mod progress;
38pub mod runner;
39pub mod statistics;
40pub mod tbl;
41#[cfg(feature = "indicatif-progress")]
42pub mod tpch_cli;
43
44use crate::generate::Sink;
45use crate::parquet::IntoSize;
46#[cfg(feature = "progress")]
47use crate::progress::ProgressTracker;
48use crate::statistics::WriteStatistics;
49use std::fmt::Display;
50use std::fs::File;
51use std::io::{self, BufWriter, Stdout, Write};
52use std::str::FromStr;
53#[cfg(feature = "progress")]
54use std::sync::Arc;
55
56/// Wrapper around a buffer writer that counts the number of buffers and bytes written
57pub struct WriterSink<W: Write> {
58 statistics: WriteStatistics,
59 inner: W,
60}
61
62impl<W: Write> WriterSink<W> {
63 pub fn new(inner: W) -> Self {
64 Self {
65 inner,
66 statistics: WriteStatistics::new("buffers"),
67 }
68 }
69}
70
71impl<W: Write + Send> Sink for WriterSink<W> {
72 fn sink(&mut self, buffer: &[u8]) -> Result<(), io::Error> {
73 self.statistics.increment_chunks(1);
74 self.statistics.increment_bytes(buffer.len());
75 self.inner.write_all(buffer)
76 }
77
78 fn flush(mut self) -> Result<(), io::Error> {
79 self.inner.flush()
80 }
81}
82
83impl IntoSize for BufWriter<Stdout> {
84 fn into_size(self) -> Result<usize, io::Error> {
85 // we can't get the size of stdout, so just return 0
86 Ok(0)
87 }
88}
89
90impl IntoSize for BufWriter<File> {
91 fn into_size(self) -> Result<usize, io::Error> {
92 let file = self.into_inner()?;
93 let metadata = file.metadata()?;
94 Ok(metadata.len() as usize)
95 }
96}
97
98/// TPC-H table types
99///
100/// Represents the 8 tables in the TPC-H benchmark schema.
101/// Tables are ordered by size (smallest to largest at SF=1).
102#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
103pub enum Table {
104 /// Nation table (25 rows)
105 Nation,
106 /// Region table (5 rows)
107 Region,
108 /// Part table (200,000 rows at SF=1)
109 Part,
110 /// Supplier table (10,000 rows at SF=1)
111 Supplier,
112 /// Part-Supplier relationship table (800,000 rows at SF=1)
113 Partsupp,
114 /// Customer table (150,000 rows at SF=1)
115 Customer,
116 /// Orders table (1,500,000 rows at SF=1)
117 Orders,
118 /// Line item table (6,000,000 rows at SF=1)
119 Lineitem,
120}
121
122impl Display for Table {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{}", self.name())
125 }
126}
127
128impl FromStr for Table {
129 type Err = &'static str;
130
131 /// Returns the table enum value from the given string full name or abbreviation
132 ///
133 /// The original dbgen tool allows some abbreviations to mean two different tables
134 /// like 'p' which aliases to both 'part' and 'partsupp'. This implementation does
135 /// not support this since it just adds unnecessary complexity and confusion so we
136 /// only support the exclusive abbreviations.
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 match s {
139 "n" | "nation" => Ok(Table::Nation),
140 "r" | "region" => Ok(Table::Region),
141 "s" | "supplier" => Ok(Table::Supplier),
142 "P" | "part" => Ok(Table::Part),
143 "S" | "partsupp" => Ok(Table::Partsupp),
144 "c" | "customer" => Ok(Table::Customer),
145 "O" | "orders" => Ok(Table::Orders),
146 "L" | "lineitem" => Ok(Table::Lineitem),
147 _ => Err("Invalid table name {s}"),
148 }
149 }
150}
151
152impl Table {
153 fn name(&self) -> &'static str {
154 match self {
155 Table::Nation => "nation",
156 Table::Region => "region",
157 Table::Part => "part",
158 Table::Supplier => "supplier",
159 Table::Partsupp => "partsupp",
160 Table::Customer => "customer",
161 Table::Orders => "orders",
162 Table::Lineitem => "lineitem",
163 }
164 }
165}
166
167/// Output format for generated data
168///
169/// # Format Details
170///
171/// - **TBL**: Pipe-delimited format compatible with original dbgen tool
172/// - **CSV**: Comma-separated values with proper escaping
173/// - **Parquet**: Columnar Apache Parquet format with configurable compression
174#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
175pub enum OutputFormat {
176 /// TBL format (pipe-delimited, dbgen-compatible)
177 Tbl,
178 /// CSV format (comma-separated values)
179 Csv,
180 /// Apache Parquet format (columnar, compressed)
181 Parquet,
182}
183
184impl FromStr for OutputFormat {
185 type Err = String;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 match s.to_lowercase().as_str() {
189 "tbl" => Ok(OutputFormat::Tbl),
190 "csv" => Ok(OutputFormat::Csv),
191 "parquet" => Ok(OutputFormat::Parquet),
192 _ => Err(format!(
193 "Invalid output format: {s}. Valid formats are: tbl, csv, parquet"
194 )),
195 }
196 }
197}
198
199impl Display for OutputFormat {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 OutputFormat::Tbl => write!(f, "tbl"),
203 OutputFormat::Csv => write!(f, "csv"),
204 OutputFormat::Parquet => write!(f, "parquet"),
205 }
206 }
207}
208
209/// Configuration for TPC-H data generation
210///
211/// This struct holds all the parameters needed to generate TPC-H benchmark data.
212/// It's typically not constructed directly - use [`TpchGeneratorBuilder`] instead.
213///
214/// # Examples
215///
216/// ```no_run
217/// use tpchgen_cli::{GeneratorConfig, OutputFormat};
218///
219/// // Usually you would use TpchGenerator::builder() instead
220/// let config = GeneratorConfig {
221/// scale_factor: 10.0,
222/// ..Default::default()
223/// };
224/// ```
225#[derive(Debug, Clone)]
226pub struct GeneratorConfig {
227 /// Scale factor (e.g., 1.0 for 1GB, 10.0 for 10GB)
228 pub scale_factor: f64,
229 /// Output directory for generated files
230 pub output_dir: std::path::PathBuf,
231 /// Tables to generate (if None, generates all tables)
232 pub tables: Option<Vec<Table>>,
233 /// Output format (TBL, CSV, or Parquet)
234 pub format: OutputFormat,
235 /// Number of threads for parallel generation
236 pub num_threads: usize,
237 /// Parquet compression format
238 pub parquet_compression: Compression,
239 /// Target row group size in bytes for Parquet files
240 pub parquet_row_group_bytes: i64,
241 /// Number of partitions to generate (if None, generates a single file per table)
242 pub parts: Option<i32>,
243 /// Specific partition to generate (1-based, requires parts to be set)
244 pub part: Option<i32>,
245 /// Write output to stdout instead of files
246 pub stdout: bool,
247 /// CSV delimiter character (only applies to CSV format)
248 pub csv_delimiter: char,
249}
250
251impl Default for GeneratorConfig {
252 fn default() -> Self {
253 Self {
254 scale_factor: 1.0,
255 output_dir: std::path::PathBuf::from("."),
256 tables: None,
257 format: OutputFormat::Tbl,
258 num_threads: num_cpus::get(),
259 parquet_compression: Compression::SNAPPY,
260 parquet_row_group_bytes: DEFAULT_PARQUET_ROW_GROUP_BYTES,
261 parts: None,
262 part: None,
263 stdout: false,
264 csv_delimiter: ',',
265 }
266 }
267}
268
269/// TPC-H data generator
270///
271/// The main entry point for generating TPC-H benchmark data.
272/// Use the builder pattern via [`TpchGenerator::builder()`] to configure and create instances.
273///
274/// # Examples
275///
276/// ```no_run
277/// use tpchgen_cli::{TpchGenerator, Table, OutputFormat};
278/// use std::path::PathBuf;
279/// use ::parquet::basic::ZstdLevel;
280/// # async fn example() -> std::io::Result<()> {
281/// // Generate all tables at scale factor 1 in TBL format
282/// TpchGenerator::builder()
283/// .with_scale_factor(1.0)
284/// .with_output_dir(PathBuf::from("./data"))
285/// .build()
286/// .generate()
287/// .await?;
288///
289/// // Generate specific tables in Parquet format with compression
290/// TpchGenerator::builder()
291/// .with_scale_factor(10.0)
292/// .with_output_dir(PathBuf::from("./benchmark_data"))
293/// .with_tables(vec![Table::Orders, Table::Lineitem])
294/// .with_format(OutputFormat::Parquet)
295/// .with_parquet_compression(tpchgen_cli::Compression::ZSTD(ZstdLevel::try_new(1).unwrap()))
296/// .with_num_threads(16)
297/// .build()
298/// .generate()
299/// .await?;
300/// # Ok(())
301/// # }
302/// ```
303pub struct TpchGenerator {
304 config: GeneratorConfig,
305 #[cfg(feature = "progress")]
306 progress_tracker: Option<Arc<dyn ProgressTracker>>,
307}
308
309impl TpchGenerator {
310 /// Create a new builder for configuring the generator
311 ///
312 /// This is the recommended way to construct a [`TpchGenerator`].
313 ///
314 /// # Examples
315 ///
316 /// ```no_run
317 /// use tpchgen_cli::TpchGenerator;
318 ///
319 /// let generator = TpchGenerator::builder()
320 /// .with_scale_factor(1.0)
321 /// .build();
322 /// ```
323 pub fn builder() -> TpchGeneratorBuilder {
324 TpchGeneratorBuilder::new()
325 }
326
327 /// Generate TPC-H data with the configured settings
328 ///
329 /// This async method performs the actual data generation, creating files
330 /// in the configured output directory (or writing to stdout if configured).
331 ///
332 /// # Returns
333 ///
334 /// - `Ok(())` on successful generation
335 /// - `Err(io::Error)` if file I/O or generation fails
336 ///
337 /// # Examples
338 ///
339 /// ```no_run
340 /// use tpchgen_cli::TpchGenerator;
341 ///
342 /// # async fn example() -> std::io::Result<()> {
343 /// TpchGenerator::builder()
344 /// .with_scale_factor(1.0)
345 /// .build()
346 /// .generate()
347 /// .await?;
348 /// # Ok(())
349 /// # }
350 /// ```
351 pub async fn generate(self) -> io::Result<()> {
352 use crate::output_plan::OutputPlanGenerator;
353 use crate::runner::PlanRunner;
354 use log::info;
355 use std::time::Instant;
356 use tpchgen::distribution::Distributions;
357 use tpchgen::text::TextPool;
358
359 let config = self.config;
360 #[cfg(feature = "progress")]
361 let progress_tracker = self.progress_tracker;
362
363 // Create output directory if it doesn't exist and we are not writing to stdout
364 if !config.stdout {
365 std::fs::create_dir_all(&config.output_dir)?;
366 }
367
368 // Determine which tables to generate
369 let tables: Vec<Table> = if let Some(tables) = config.tables {
370 tables
371 } else {
372 vec![
373 Table::Nation,
374 Table::Region,
375 Table::Part,
376 Table::Supplier,
377 Table::Partsupp,
378 Table::Customer,
379 Table::Orders,
380 Table::Lineitem,
381 ]
382 };
383
384 // Determine what files to generate
385 let mut output_plan_generator = OutputPlanGenerator::new(
386 config.format,
387 config.scale_factor,
388 config.parquet_compression,
389 config.parquet_row_group_bytes,
390 config.stdout,
391 config.output_dir,
392 config.csv_delimiter,
393 );
394
395 for table in tables {
396 output_plan_generator.generate_plans(table, config.part, config.parts)?;
397 }
398 let output_plans = output_plan_generator.build();
399
400 // Force the creation of the distributions and text pool so it doesn't
401 // get charged to the first table
402 let start = Instant::now();
403 Distributions::static_default();
404 TextPool::get_or_init_default();
405 let elapsed = start.elapsed();
406 info!("Created static distributions and text pools in {elapsed:?}");
407
408 // Run
409 let runner = PlanRunner::new(output_plans, config.num_threads);
410 #[cfg(feature = "progress")]
411 let runner = if let Some(tracker) = progress_tracker {
412 runner.with_progress_tracker(tracker)
413 } else {
414 runner
415 };
416 runner.run().await?;
417 info!("Generation complete!");
418 Ok(())
419 }
420}
421
422/// Builder for constructing a [`TpchGenerator`]
423///
424/// Provides a fluent interface for configuring TPC-H data generation parameters.
425/// All builder methods can be chained, and calling [`build()`](TpchGeneratorBuilder::build)
426/// produces a [`TpchGenerator`] ready to generate data.
427///
428/// # Defaults
429///
430/// - Scale factor: 1.0
431/// - Output directory: current directory (".")
432/// - Tables: all 8 tables
433/// - Format: TBL
434/// - Threads: number of CPUs
435/// - Parquet compression: SNAPPY
436/// - Row group size: 7MB
437///
438/// # Examples
439///
440/// ```no_run
441/// use tpchgen_cli::{TpchGenerator, Table, OutputFormat, Compression};
442/// use std::path::PathBuf;
443/// use ::parquet::basic::ZstdLevel;
444///
445/// # async fn example() -> std::io::Result<()> {
446/// let generator = TpchGenerator::builder()
447/// .with_scale_factor(100.0)
448/// .with_output_dir(PathBuf::from("/data/tpch"))
449/// .with_tables(vec![Table::Lineitem, Table::Orders])
450/// .with_format(OutputFormat::Parquet)
451/// .with_parquet_compression(Compression::ZSTD(ZstdLevel::try_new(3).unwrap()))
452/// .with_num_threads(32)
453/// .build();
454///
455/// generator.generate().await?;
456/// # Ok(())
457/// # }
458/// ```
459#[derive(Debug, Clone)]
460pub struct TpchGeneratorBuilder {
461 config: GeneratorConfig,
462 #[cfg(feature = "progress")]
463 progress_tracker: Option<Arc<dyn ProgressTracker>>,
464}
465
466impl TpchGeneratorBuilder {
467 /// Create a new builder with default configuration
468 ///
469 /// # Examples
470 ///
471 /// ```
472 /// use tpchgen_cli::TpchGeneratorBuilder;
473 ///
474 /// let builder = TpchGeneratorBuilder::new();
475 /// ```
476 pub fn new() -> Self {
477 Self {
478 config: GeneratorConfig::default(),
479 #[cfg(feature = "progress")]
480 progress_tracker: None,
481 }
482 }
483
484 /// Returns the scale factor.
485 pub fn scale_factor(&self) -> f64 {
486 self.config.scale_factor
487 }
488
489 /// Set the scale factor (e.g., 1.0 for 1GB, 10.0 for 10GB)
490 pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
491 self.config.scale_factor = scale_factor;
492 self
493 }
494
495 /// Set the output directory
496 pub fn with_output_dir(mut self, output_dir: impl Into<std::path::PathBuf>) -> Self {
497 self.config.output_dir = output_dir.into();
498 self
499 }
500
501 /// Set which tables to generate (default: all tables)
502 pub fn with_tables(mut self, tables: Vec<Table>) -> Self {
503 self.config.tables = Some(tables);
504 self
505 }
506
507 /// Set the output format (default: TBL)
508 pub fn with_format(mut self, format: OutputFormat) -> Self {
509 self.config.format = format;
510 self
511 }
512
513 /// Set the number of threads for parallel generation (default: number of CPUs)
514 pub fn with_num_threads(mut self, num_threads: usize) -> Self {
515 self.config.num_threads = num_threads;
516 self
517 }
518
519 /// Set Parquet compression format (default: SNAPPY)
520 pub fn with_parquet_compression(mut self, compression: Compression) -> Self {
521 self.config.parquet_compression = compression;
522 self
523 }
524
525 /// Set target row group size in bytes for Parquet files (default: 7MB)
526 pub fn with_parquet_row_group_bytes(mut self, bytes: i64) -> Self {
527 self.config.parquet_row_group_bytes = bytes;
528 self
529 }
530
531 /// Set the number of partitions to generate
532 pub fn with_parts(mut self, parts: i32) -> Self {
533 self.config.parts = Some(parts);
534 self
535 }
536
537 /// Set the specific partition to generate (1-based, requires parts to be set)
538 pub fn with_part(mut self, part: i32) -> Self {
539 self.config.part = Some(part);
540 self
541 }
542
543 /// Write output to stdout instead of files
544 pub fn with_stdout(mut self, stdout: bool) -> Self {
545 self.config.stdout = stdout;
546 self
547 }
548
549 /// Set the CSV delimiter character (only applies to CSV format, default: ',')
550 pub fn with_csv_delimiter(mut self, delimiter: char) -> Self {
551 self.config.csv_delimiter = delimiter;
552 self
553 }
554
555 /// Attach a custom [`ProgressTracker`] to receive generation progress updates.
556 ///
557 /// The runner calls [`ProgressTracker::finish`] on successful completion.
558 /// Trackers that need error or panic cleanup should use `Drop` as a
559 /// fallback. See [`progress`] for the full contract and examples.
560 #[cfg(feature = "progress")]
561 pub fn with_progress_tracker(mut self, tracker: Arc<dyn ProgressTracker>) -> Self {
562 self.progress_tracker = Some(tracker);
563 self
564 }
565
566 /// Build the [`TpchGenerator`] with the configured settings
567 pub fn build(self) -> TpchGenerator {
568 TpchGenerator {
569 config: self.config,
570 #[cfg(feature = "progress")]
571 progress_tracker: self.progress_tracker,
572 }
573 }
574}
575
576impl Default for TpchGeneratorBuilder {
577 fn default() -> Self {
578 Self::new()
579 }
580}
581
582#[cfg(all(test, feature = "progress"))]
583mod tests {
584 use super::*;
585 use crate::progress::ProgressTracker;
586 use std::sync::{
587 atomic::{AtomicU64, Ordering},
588 Arc, Mutex,
589 };
590
591 #[derive(Debug, Default)]
592 struct RecordingProgress {
593 registered: Mutex<Vec<(Table, u64)>>,
594 increments: Mutex<Vec<(Table, u64)>>,
595 finishes: AtomicU64,
596 }
597
598 impl ProgressTracker for RecordingProgress {
599 fn register(&self, table: Table, total_units: u64) {
600 self.registered.lock().unwrap().push((table, total_units));
601 }
602
603 fn increment(&self, table: Table, units: u64) {
604 self.increments.lock().unwrap().push((table, units));
605 }
606
607 fn finish(&self) {
608 self.finishes.fetch_add(1, Ordering::Relaxed);
609 }
610 }
611
612 #[tokio::test]
613 async fn builder_passes_custom_progress_tracker_to_runner() {
614 let output_dir = tempfile::tempdir().unwrap();
615 let tracker = Arc::new(RecordingProgress::default());
616 let progress: Arc<dyn ProgressTracker> = tracker.clone();
617
618 TpchGenerator::builder()
619 .with_output_dir(output_dir.path())
620 .with_tables(vec![Table::Region])
621 .with_num_threads(1)
622 .with_progress_tracker(progress)
623 .build()
624 .generate()
625 .await
626 .unwrap();
627
628 assert_eq!(
629 *tracker.registered.lock().unwrap(),
630 vec![(Table::Region, 1)]
631 );
632 assert_eq!(
633 *tracker.increments.lock().unwrap(),
634 vec![(Table::Region, 1)]
635 );
636 assert_eq!(tracker.finishes.load(Ordering::Relaxed), 1);
637 }
638}