1use crate::constants::LOG_LEVELS;
2use crate::error::{ConfigError, Error, Result};
3use serde::Deserialize;
4use std::path::{Path, PathBuf};
5
6#[cfg(any(
8 feature = "sqlite",
9 feature = "duckdb",
10 feature = "postgres",
11 feature = "dm"
12))]
13fn default_table_name() -> String {
14 "sqllog_records".to_string()
15}
16
17#[cfg(any(
19 feature = "sqlite",
20 feature = "duckdb",
21 feature = "postgres",
22 feature = "dm"
23))]
24fn default_true() -> bool {
25 true
26}
27
28#[cfg(feature = "postgres")]
30fn default_postgres_host() -> String {
31 "localhost".to_string()
32}
33
34#[cfg(feature = "postgres")]
36fn default_postgres_port() -> u16 {
37 5432
38}
39
40#[cfg(feature = "postgres")]
42fn default_postgres_username() -> String {
43 "postgres".to_string()
44}
45
46#[cfg(feature = "postgres")]
48fn default_postgres_database() -> String {
49 "sqllog".to_string()
50}
51
52#[cfg(feature = "postgres")]
54fn default_postgres_schema() -> String {
55 "public".to_string()
56}
57
58#[cfg_attr(feature = "csv", derive(Default))]
59#[derive(Debug, Deserialize, Clone)]
60pub struct Config {
61 #[serde(default)]
63 pub sqllog: SqllogConfig,
64 pub error: ErrorConfig,
65 pub logging: LoggingConfig,
66 pub features: FeaturesConfig,
67 pub exporter: ExporterConfig,
68}
69
70impl Config {
71 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
73 let path = path.as_ref();
74 let content = std::fs::read_to_string(path)
75 .map_err(|_| Error::Config(ConfigError::NotFound(path.to_path_buf())))?;
76 Self::from_str(&content, path.to_path_buf())
77 }
78
79 pub fn from_str(content: &str, path: PathBuf) -> Result<Self> {
81 let config: Config = toml::from_str(content).map_err(|e| {
82 Error::Config(ConfigError::ParseFailed {
83 path,
84 reason: e.to_string(),
85 })
86 })?;
87
88 config.validate()?;
90
91 Ok(config)
92 }
93
94 pub fn validate(&self) -> Result<()> {
96 self.logging.validate()?;
98
99 self.exporter.validate()?;
101
102 self.sqllog.validate()?;
104
105 Ok(())
106 }
107}
108
109#[derive(Debug, Deserialize, Clone)]
111pub struct SqllogConfig {
112 pub directory: String,
114}
115
116impl Default for SqllogConfig {
117 fn default() -> Self {
118 Self {
119 directory: "sqllogs".to_string(),
120 }
121 }
122}
123
124impl SqllogConfig {
125 #[must_use]
127 pub fn directory(&self) -> &str {
128 &self.directory
129 }
130
131 pub fn validate(&self) -> Result<()> {
133 if self.directory.trim().is_empty() {
134 return Err(Error::Config(ConfigError::InvalidValue {
135 field: "sqllog.directory".to_string(),
136 value: self.directory.clone(),
137 reason: "Input directory cannot be empty".to_string(),
138 }));
139 }
140 Ok(())
141 }
142}
143
144#[derive(Debug, Deserialize, Clone)]
145pub struct ErrorConfig {
146 pub file: String,
148}
149
150impl ErrorConfig {
151 #[must_use]
153 pub fn file(&self) -> &str {
154 &self.file
155 }
156}
157
158impl Default for ErrorConfig {
159 fn default() -> Self {
160 Self {
161 file: "export/errors.log".to_string(),
162 }
163 }
164}
165
166#[derive(Debug, Deserialize, Clone)]
167pub struct LoggingConfig {
168 pub file: String,
170 pub level: String,
171 #[serde(default = "default_retention_days")]
172 pub retention_days: usize,
173}
174
175fn default_retention_days() -> usize {
176 7
177}
178
179impl LoggingConfig {
180 #[must_use]
182 pub fn file(&self) -> &str {
183 &self.file
184 }
185
186 #[must_use]
188 pub fn level(&self) -> &str {
189 &self.level
190 }
191
192 #[must_use]
194 pub fn retention_days(&self) -> usize {
195 self.retention_days
196 }
197
198 pub fn validate(&self) -> Result<()> {
200 if !LOG_LEVELS
201 .iter()
202 .any(|&l| l.eq_ignore_ascii_case(self.level.as_str()))
203 {
204 return Err(Error::Config(ConfigError::InvalidLogLevel {
205 level: self.level.clone(),
206 valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
207 }));
208 }
209
210 if self.retention_days == 0 || self.retention_days > 365 {
212 return Err(Error::Config(ConfigError::InvalidValue {
213 field: "logging.retention_days".to_string(),
214 value: self.retention_days.to_string(),
215 reason: "Retention days must be between 1 and 365".to_string(),
216 }));
217 }
218
219 Ok(())
220 }
221}
222
223impl Default for LoggingConfig {
224 fn default() -> Self {
225 Self {
226 file: "logs/sqllog2db.log".to_string(),
227 level: "info".to_string(),
228 retention_days: 7,
229 }
230 }
231}
232
233#[derive(Debug, Deserialize, Clone)]
235pub struct ReplaceParametersFeature {
236 pub enable: bool,
237 pub symbols: Option<Vec<String>>,
238}
239
240#[derive(Debug, Deserialize, Clone, Default)]
241pub struct FeaturesConfig {
242 #[serde(default)]
244 pub replace_parameters: Option<ReplaceParametersFeature>,
245}
246
247impl FeaturesConfig {
248 #[must_use]
250 pub fn should_replace_sql_parameters(&self) -> bool {
251 self.replace_parameters.as_ref().is_some_and(|f| f.enable)
252 }
253}
254
255#[derive(Debug, Deserialize, Clone)]
256pub struct ExporterConfig {
257 #[cfg(feature = "csv")]
258 pub csv: Option<CsvExporter>,
259 #[cfg(feature = "parquet")]
260 pub parquet: Option<ParquetExporter>,
261 #[cfg(feature = "jsonl")]
262 pub jsonl: Option<JsonlExporter>,
263 #[cfg(feature = "sqlite")]
264 pub sqlite: Option<SqliteExporter>,
265 #[cfg(feature = "duckdb")]
266 pub duckdb: Option<DuckdbExporter>,
267 #[cfg(feature = "postgres")]
268 pub postgres: Option<PostgresExporter>,
269 #[cfg(feature = "dm")]
270 pub dm: Option<DmExporter>,
271}
272
273impl ExporterConfig {
274 #[cfg(feature = "csv")]
276 #[must_use]
277 pub fn csv(&self) -> Option<&CsvExporter> {
278 self.csv.as_ref()
279 }
280
281 #[cfg(feature = "parquet")]
282 #[must_use]
284 pub fn parquet(&self) -> Option<&ParquetExporter> {
285 self.parquet.as_ref()
286 }
287
288 #[cfg(feature = "jsonl")]
289 #[must_use]
291 pub fn jsonl(&self) -> Option<&JsonlExporter> {
292 self.jsonl.as_ref()
293 }
294
295 #[cfg(feature = "sqlite")]
296 #[must_use]
298 pub fn sqlite(&self) -> Option<&SqliteExporter> {
299 self.sqlite.as_ref()
300 }
301
302 #[cfg(feature = "duckdb")]
303 #[must_use]
305 pub fn duckdb(&self) -> Option<&DuckdbExporter> {
306 self.duckdb.as_ref()
307 }
308
309 #[cfg(feature = "postgres")]
310 #[must_use]
312 pub fn postgres(&self) -> Option<&PostgresExporter> {
313 self.postgres.as_ref()
314 }
315
316 #[cfg(feature = "dm")]
317 #[must_use]
319 pub fn dm(&self) -> Option<&DmExporter> {
320 self.dm.as_ref()
321 }
322
323 #[must_use]
325 pub fn has_exporters(&self) -> bool {
326 let mut found = false;
327 #[cfg(feature = "csv")]
328 {
329 found = found || self.csv.is_some();
330 }
331 #[cfg(feature = "parquet")]
332 {
333 found = found || self.parquet.is_some();
334 }
335 #[cfg(feature = "jsonl")]
336 {
337 found = found || self.jsonl.is_some();
338 }
339 #[cfg(feature = "sqlite")]
340 {
341 found = found || self.sqlite.is_some();
342 }
343 #[cfg(feature = "duckdb")]
344 {
345 found = found || self.duckdb.is_some();
346 }
347 #[cfg(feature = "postgres")]
348 {
349 found = found || self.postgres.is_some();
350 }
351 #[cfg(feature = "dm")]
352 {
353 found = found || self.dm.is_some();
354 }
355 found
356 }
357
358 #[must_use]
360 pub fn total_exporters(&self) -> usize {
361 let mut count = 0;
362 #[cfg(feature = "csv")]
363 {
364 if self.csv.is_some() {
365 count += 1;
366 }
367 }
368 #[cfg(feature = "parquet")]
369 {
370 if self.parquet.is_some() {
371 count += 1;
372 }
373 }
374 #[cfg(feature = "jsonl")]
375 {
376 if self.jsonl.is_some() {
377 count += 1;
378 }
379 }
380 #[cfg(feature = "sqlite")]
381 {
382 if self.sqlite.is_some() {
383 count += 1;
384 }
385 }
386 #[cfg(feature = "duckdb")]
387 {
388 if self.duckdb.is_some() {
389 count += 1;
390 }
391 }
392 #[cfg(feature = "postgres")]
393 {
394 if self.postgres.is_some() {
395 count += 1;
396 }
397 }
398 #[cfg(feature = "dm")]
399 {
400 if self.dm.is_some() {
401 count += 1;
402 }
403 }
404 count
405 }
406
407 pub fn validate(&self) -> Result<()> {
409 if !self.has_exporters() {
410 return Err(Error::Config(ConfigError::NoExporters));
411 }
412
413 let total = self.total_exporters();
414 if total > 1 {
415 eprintln!("Warning: {total} exporters configured, but only one is supported.");
416 eprintln!(
417 "Will use the first exporter by priority: CSV > Parquet > JSONL > SQLite > DuckDB > PostgreSQL > DM"
418 );
419 }
420
421 Ok(())
422 }
423}
424
425impl Default for ExporterConfig {
426 fn default() -> Self {
427 Self {
428 #[cfg(feature = "csv")]
429 csv: Some(CsvExporter::default()),
430 #[cfg(feature = "parquet")]
431 parquet: Some(ParquetExporter::default()),
432 #[cfg(feature = "jsonl")]
433 jsonl: None,
434 #[cfg(feature = "sqlite")]
435 sqlite: None,
436 #[cfg(feature = "duckdb")]
437 duckdb: None,
438 #[cfg(feature = "postgres")]
439 postgres: None,
440 #[cfg(feature = "dm")]
441 dm: None,
442 }
443 }
444}
445
446#[cfg(feature = "parquet")]
447#[derive(Debug, Deserialize, Clone)]
448pub struct ParquetExporter {
449 pub file: String,
451 pub overwrite: bool,
453 pub row_group_size: Option<usize>,
455 pub use_dictionary: Option<bool>,
457}
458
459#[cfg(feature = "parquet")]
460impl Default for ParquetExporter {
461 fn default() -> Self {
462 Self {
463 file: "export/sqllog2db.parquet".to_string(),
464 overwrite: true,
465 row_group_size: Some(100_000),
466 use_dictionary: Some(true),
467 }
468 }
469}
470
471#[cfg(feature = "csv")]
472#[derive(Debug, Deserialize, Clone)]
473pub struct CsvExporter {
474 pub file: String,
476 pub overwrite: bool,
478 pub append: bool,
480}
481
482#[cfg(feature = "csv")]
483impl Default for CsvExporter {
484 fn default() -> Self {
485 Self {
486 file: "outputs/sqllog.csv".to_string(),
487 overwrite: true,
488 append: false,
489 }
490 }
491}
492
493#[cfg(feature = "jsonl")]
494#[derive(Debug, Deserialize, Clone)]
495pub struct JsonlExporter {
496 pub file: String,
498 pub overwrite: bool,
500 pub append: bool,
502}
503
504#[cfg(feature = "jsonl")]
505impl Default for JsonlExporter {
506 fn default() -> Self {
507 Self {
508 file: "export/sqllog2db.jsonl".to_string(),
509 overwrite: true,
510 append: false,
511 }
512 }
513}
514
515#[cfg(feature = "sqlite")]
516#[derive(Debug, Deserialize, Clone)]
517pub struct SqliteExporter {
518 pub database_url: String,
520 #[serde(default = "default_table_name")]
522 pub table_name: String,
523 #[serde(default = "default_true")]
525 pub overwrite: bool,
526 #[serde(default)]
528 pub append: bool,
529}
530
531#[cfg(feature = "sqlite")]
532impl Default for SqliteExporter {
533 fn default() -> Self {
534 Self {
535 database_url: "export/sqllog2db.db".to_string(),
536 table_name: "sqllog_records".to_string(),
537 overwrite: true,
538 append: false,
539 }
540 }
541}
542
543#[cfg(feature = "duckdb")]
544#[derive(Debug, Deserialize, Clone)]
545pub struct DuckdbExporter {
546 pub database_url: String,
548 #[serde(default = "default_table_name")]
550 pub table_name: String,
551 #[serde(default = "default_true")]
553 pub overwrite: bool,
554 #[serde(default)]
556 pub append: bool,
557}
558
559#[cfg(feature = "duckdb")]
560impl Default for DuckdbExporter {
561 fn default() -> Self {
562 Self {
563 database_url: "export/sqllog2db.duckdb".to_string(),
564 table_name: "sqllog_records".to_string(),
565 overwrite: true,
566 append: false,
567 }
568 }
569}
570
571#[cfg(feature = "postgres")]
572#[derive(Debug, Deserialize, Clone)]
573pub struct PostgresExporter {
574 #[serde(default = "default_postgres_host")]
576 pub host: String,
577 #[serde(default = "default_postgres_port")]
579 pub port: u16,
580 #[serde(default = "default_postgres_username")]
582 pub username: String,
583 pub password: String,
585 #[serde(default = "default_postgres_database")]
587 pub database: String,
588 #[serde(default = "default_postgres_schema")]
590 pub schema: String,
591 #[serde(default = "default_table_name")]
593 pub table_name: String,
594 #[serde(default = "default_true")]
596 pub overwrite: bool,
597 #[serde(default)]
599 pub append: bool,
600}
601
602#[cfg(feature = "postgres")]
603impl Default for PostgresExporter {
604 fn default() -> Self {
605 Self {
606 host: "localhost".to_string(),
607 port: 5432,
608 username: "postgres".to_string(),
609 password: "postgres".to_string(),
610 database: "sqllog".to_string(),
611 schema: "public".to_string(),
612 table_name: "sqllog_records".to_string(),
613 overwrite: true,
614 append: false,
615 }
616 }
617}
618
619#[cfg(feature = "postgres")]
620impl PostgresExporter {
621 #[must_use]
623 pub fn connection_string(&self) -> String {
624 if self.password.is_empty() {
625 format!(
626 "host={} port={} user={} dbname={}",
627 self.host, self.port, self.username, self.database
628 )
629 } else {
630 format!(
631 "host={} port={} user={} password={} dbname={}",
632 self.host, self.port, self.username, self.password, self.database
633 )
634 }
635 }
636}
637
638#[cfg(feature = "dm")]
639#[derive(Debug, Deserialize, Clone)]
640pub struct DmExporter {
641 pub userid: String,
643 #[serde(default = "default_table_name")]
645 pub table_name: String,
646 pub control_file: String,
648 pub log_dir: String,
650}
651
652#[cfg(feature = "dm")]
653impl Default for DmExporter {
654 fn default() -> Self {
655 Self {
656 userid: "SYSDBA/SYSDBA@localhost:5236".to_string(),
657 table_name: "sqllog_records".to_string(),
658 control_file: "export/sqllog.ctl".to_string(),
659 log_dir: "export/log".to_string(),
660 }
661 }
662}