dm_database_sqllog2db/config/
exporter.rs1use crate::error::{ConfigError, Error, Result};
2use serde::Deserialize;
3
4#[derive(Debug, Deserialize, Clone)]
5pub struct ExporterConfig {
6 pub csv: Option<CsvExporterConfig>,
7 pub sqlite: Option<SqliteExporterConfig>,
8}
9
10impl ExporterConfig {
11 pub(super) fn has_any(&self) -> bool {
12 self.csv.is_some() || self.sqlite.is_some()
13 }
14
15 pub fn validate(&self) -> Result<()> {
16 if !self.has_any() {
17 return Err(Error::Config(ConfigError::NoExporters));
18 }
19 if let Some(csv) = &self.csv {
20 csv.validate()?;
21 }
22 if let Some(sqlite) = &self.sqlite {
23 sqlite.validate()?;
24 }
25 Ok(())
26 }
27}
28
29impl Default for ExporterConfig {
30 fn default() -> Self {
31 Self {
32 csv: Some(CsvExporterConfig::default()),
33 sqlite: None,
34 }
35 }
36}
37
38#[derive(Debug, Deserialize, Clone)]
39pub struct CsvExporterConfig {
40 pub file: String,
41 #[serde(default = "default_true")]
42 pub overwrite: bool,
43 #[serde(default)]
44 pub append: bool,
45 #[serde(default = "default_true")]
47 pub include_performance_metrics: bool,
48}
49
50impl Default for CsvExporterConfig {
51 fn default() -> Self {
52 Self {
53 file: "outputs/sqllog.csv".to_string(),
54 overwrite: true,
55 append: false,
56 include_performance_metrics: true,
57 }
58 }
59}
60
61impl CsvExporterConfig {
62 pub fn validate(&self) -> Result<()> {
63 if self.file.trim().is_empty() {
64 return Err(Error::Config(ConfigError::InvalidValue {
65 field: "exporter.csv.file".to_string(),
66 value: self.file.clone(),
67 reason: "CSV output file path cannot be empty".to_string(),
68 }));
69 }
70 Ok(())
71 }
72}
73
74#[derive(Debug, Deserialize, Clone)]
75pub struct SqliteExporterConfig {
76 pub database_url: String,
77 #[serde(default = "default_table_name")]
78 pub table_name: String,
79 #[serde(default = "default_true")]
80 pub overwrite: bool,
81 #[serde(default)]
82 pub append: bool,
83 #[serde(default = "default_batch_size")]
84 pub batch_size: usize,
85}
86
87fn default_table_name() -> String {
88 "sqllog_records".to_string()
89}
90
91fn default_batch_size() -> usize {
92 10_000
93}
94
95impl Default for SqliteExporterConfig {
96 fn default() -> Self {
97 Self {
98 database_url: "export/sqllog2db.db".to_string(),
99 table_name: "sqllog_records".to_string(),
100 overwrite: true,
101 append: false,
102 batch_size: 10_000,
103 }
104 }
105}
106
107impl SqliteExporterConfig {
108 pub fn validate(&self) -> Result<()> {
109 if self.database_url.trim().is_empty() {
110 return Err(Error::Config(ConfigError::InvalidValue {
111 field: "exporter.sqlite.database_url".to_string(),
112 value: self.database_url.clone(),
113 reason: "SQLite database URL cannot be empty".to_string(),
114 }));
115 }
116 if self.table_name.trim().is_empty() {
117 return Err(Error::Config(ConfigError::InvalidValue {
118 field: "exporter.sqlite.table_name".to_string(),
119 value: self.table_name.clone(),
120 reason: "SQLite table name cannot be empty".to_string(),
121 }));
122 }
123 let is_valid_ident = {
124 let mut chars = self.table_name.chars();
125 chars
126 .next()
127 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
128 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
129 };
130 if !is_valid_ident {
131 return Err(Error::Config(ConfigError::InvalidValue {
132 field: "exporter.sqlite.table_name".to_string(),
133 value: self.table_name.clone(),
134 reason: "table name must match ^[a-zA-Z_][a-zA-Z0-9_]*$ (ASCII identifiers only)"
135 .to_string(),
136 }));
137 }
138 if self.batch_size == 0 {
139 return Err(ConfigError::InvalidValue {
140 field: "exporter.sqlite.batch_size".to_string(),
141 value: "0".to_string(),
142 reason: "batch_size must be greater than 0".to_string(),
143 }
144 .into());
145 }
146 Ok(())
147 }
148}
149
150fn default_true() -> bool {
151 true
152}