Skip to main content

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}