1use crate::tpch_cli::{Compression, DEFAULT_PARQUET_ROW_GROUP_BYTES};
3use clap::{ArgAction, Args, Subcommand};
4use std::fmt;
5use std::path::PathBuf;
6use tpcdsgen::config::{CompatMode, Session, SessionBuilder, Table};
7use tpcdsgen::error::InvalidOptionError;
8
9pub mod dat;
10
11type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
12const NOT_IMPLEMENTED: &str = "TPC-DS data generation is not yet implemented";
13
14#[derive(Args)]
15#[command(version)]
16#[command(args_conflicts_with_subcommands = true)]
17pub struct Cli {
18 #[command(subcommand)]
19 command: Option<Commands>,
20
21 #[command(flatten)]
22 args: DatArgs,
23}
24
25#[derive(Subcommand)]
26enum Commands {
27 Dat(DatArgs),
29 Csv(CsvArgs),
31 Parquet(ParquetArgs),
33}
34
35#[derive(Args)]
36struct DatArgs {
37 #[command(flatten)]
38 common: CommonArgs,
39}
40
41#[derive(Args)]
42struct CsvArgs {
43 #[command(flatten)]
44 common: CommonArgs,
45
46 #[arg(long, default_value = ",", value_parser = parse_delimiter)]
53 delimiter: char,
54}
55
56#[derive(Args)]
57struct ParquetArgs {
58 #[command(flatten)]
59 common: CommonArgs,
60
61 #[arg(short = 'c', long, default_value = "SNAPPY")]
75 compression: Compression,
76
77 #[arg(long, default_value_t = DEFAULT_PARQUET_ROW_GROUP_BYTES)]
90 row_group_bytes: i64,
91}
92
93#[derive(Args)]
94pub struct CommonArgs {
95 #[arg(short, long, default_value_t = 1.)]
97 scale_factor: f64,
98
99 #[arg(short, long, default_value = ".")]
101 output_dir: PathBuf,
102
103 #[arg(short = 'T', long = "tables", value_delimiter = ',')]
105 tables: Option<Vec<String>>,
106
107 #[arg(long, default_value_t = CompatMode::Trino)]
109 compat: CompatMode,
110
111 #[arg(short, long, default_value_t = false, conflicts_with = "quiet")]
116 verbose: bool,
117
118 #[arg(short, long, default_value_t = false, conflicts_with = "verbose")]
120 quiet: bool,
121
122 #[arg(long = "no-progress", action = ArgAction::SetFalse, default_value_t = true)]
126 progress_bars_enabled: bool,
127}
128
129impl Cli {
130 pub fn run(self) -> Result<()> {
131 match self.command {
132 Some(Commands::Dat(args)) => args.run(),
133 Some(Commands::Csv(args)) => args.run(),
134 Some(Commands::Parquet(args)) => args.run(),
135 None => self.args.run(),
136 }
137 }
138}
139
140impl DatArgs {
141 fn run(self) -> Result<()> {
142 self.common.run_dat()
143 }
144}
145
146impl CsvArgs {
147 fn run(self) -> Result<()> {
148 let _ = self.delimiter;
149 self.common.run_not_implemented()
150 }
151}
152
153impl ParquetArgs {
154 fn run(self) -> Result<()> {
155 let _ = (self.compression, self.row_group_bytes);
156 self.common.run_not_implemented()
157 }
158}
159
160impl CommonArgs {
161 fn run_dat(self) -> Result<()> {
162 let _ = self.progress_bars_enabled;
163 std::fs::create_dir_all(&self.output_dir)?;
164 if let Some(tables) = &self.tables {
165 for table in tables {
166 self.run_dat_for_table(Some(table.clone()))?;
167 }
168 } else {
169 self.run_dat_for_table(None)?;
170 }
171
172 Ok(())
173 }
174
175 fn run_dat_for_table(&self, table: Option<String>) -> Result<()> {
176 let session = self.to_session(table)?;
177 dat::generate(&session)
178 }
179
180 fn to_session(&self, table: Option<String>) -> Result<Session> {
181 let table = table
182 .as_deref()
183 .map(|table| {
184 table
185 .parse::<Table>()
186 .map_err(|_| InvalidOptionError::new("table", table))
187 })
188 .transpose()?;
189
190 let command_line_arguments = std::env::args().collect::<Vec<_>>().join(" ");
192
193 let mut builder = SessionBuilder::new()
194 .with_scale_factor(self.scale_factor)
195 .with_target_directory(self.output_dir.to_string_lossy())
196 .with_compat_mode(self.compat)
197 .with_command_line_arguments(command_line_arguments);
198
199 if let Some(table) = table {
200 builder = builder.with_table(table);
201 }
202
203 Ok(builder.build()?)
204 }
205
206 fn run_not_implemented(self) -> Result<()> {
207 let _ = self;
208 Err(Box::new(NotImplemented))
209 }
210}
211
212struct NotImplemented;
213
214impl fmt::Display for NotImplemented {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 f.write_str(NOT_IMPLEMENTED)
217 }
218}
219
220impl fmt::Debug for NotImplemented {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 fmt::Display::fmt(self, f)
223 }
224}
225
226impl std::error::Error for NotImplemented {}
227
228fn parse_delimiter(s: &str) -> std::result::Result<char, String> {
229 let parsed = match s {
230 "\\t" => '\t',
231 "\\n" => '\n',
232 "\\r" => '\r',
233 "\\\\" => '\\',
234 _ => {
235 let chars: Vec<char> = s.chars().collect();
236 if chars.len() != 1 {
237 return Err(format!(
238 "Delimiter must be a single character or escape sequence (\\t, \\n, \\r, \\\\), got: '{}'",
239 s
240 ));
241 }
242 chars[0]
243 }
244 };
245 if !parsed.is_ascii() {
246 return Err(format!(
247 "Delimiter must be an ASCII character, got: '{}'",
248 parsed
249 ));
250 }
251 Ok(parsed)
252}