faucet_sink_mysql/
config.rs1use faucet_core::{DEFAULT_BATCH_SIZE, WriteSpec};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum MysqlColumnMapping {
11 Json { column: String },
14 AutoMap,
17}
18
19impl Default for MysqlColumnMapping {
20 fn default() -> Self {
21 Self::Json {
22 column: "data".into(),
23 }
24 }
25}
26
27#[derive(Clone, Serialize, Deserialize, JsonSchema)]
29pub struct MysqlSinkConfig {
30 pub connection_url: String,
32 pub table_name: String,
34 #[serde(default)]
36 pub column_mapping: MysqlColumnMapping,
37 #[serde(default = "default_batch_size")]
52 pub batch_size: usize,
53 #[serde(default = "default_max_connections")]
58 pub max_connections: u32,
59 #[serde(flatten)]
67 pub write: WriteSpec,
68}
69
70fn default_batch_size() -> usize {
71 DEFAULT_BATCH_SIZE
72}
73
74fn default_max_connections() -> u32 {
75 5
76}
77
78impl std::fmt::Debug for MysqlSinkConfig {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("MysqlSinkConfig")
81 .field("connection_url", &"***")
82 .field("table_name", &self.table_name)
83 .field("column_mapping", &self.column_mapping)
84 .field("batch_size", &self.batch_size)
85 .field("max_connections", &self.max_connections)
86 .finish()
87 }
88}
89
90impl MysqlSinkConfig {
91 pub fn new(connection_url: impl Into<String>, table_name: impl Into<String>) -> Self {
93 Self {
94 connection_url: connection_url.into(),
95 table_name: table_name.into(),
96 column_mapping: MysqlColumnMapping::default(),
97 batch_size: DEFAULT_BATCH_SIZE,
98 max_connections: 5,
99 write: WriteSpec::default(),
100 }
101 }
102
103 pub fn column_mapping(mut self, mapping: MysqlColumnMapping) -> Self {
105 self.column_mapping = mapping;
106 self
107 }
108
109 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
115 self.batch_size = batch_size;
116 self
117 }
118
119 pub fn max_connections(mut self, n: u32) -> Self {
121 self.max_connections = n;
122 self
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn default_config() {
132 let config = MysqlSinkConfig::new("mysql://localhost/test", "events");
133 assert_eq!(config.table_name, "events");
134 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
135 assert!(matches!(
136 config.column_mapping,
137 MysqlColumnMapping::Json { ref column } if column == "data"
138 ));
139 }
140
141 #[test]
142 fn builder_methods() {
143 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
144 .column_mapping(MysqlColumnMapping::AutoMap)
145 .with_batch_size(100);
146 assert_eq!(config.batch_size, 100);
147 assert!(matches!(config.column_mapping, MysqlColumnMapping::AutoMap));
148 }
149
150 #[test]
151 fn with_batch_size_overrides_default() {
152 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").with_batch_size(250);
153 assert_eq!(config.batch_size, 250);
154 }
155
156 #[test]
157 fn json_custom_column() {
158 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").column_mapping(
159 MysqlColumnMapping::Json {
160 column: "payload".into(),
161 },
162 );
163 assert!(matches!(
164 config.column_mapping,
165 MysqlColumnMapping::Json { ref column } if column == "payload"
166 ));
167 }
168
169 #[test]
170 fn debug_masks_connection_url() {
171 let config = MysqlSinkConfig::new("mysql://secret:pass@host/db", "events");
172 let debug = format!("{config:?}");
173 assert!(debug.contains("***"));
174 assert!(!debug.contains("secret"));
175 assert!(!debug.contains("pass"));
176 }
177
178 #[test]
179 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
180 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").with_batch_size(0);
181 assert_eq!(config.batch_size, 0);
182 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
183 }
184
185 #[test]
186 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
187 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
188 .with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
189 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
190 }
191
192 #[test]
193 fn batch_size_deserializes_from_json() {
194 let json = r#"{
195 "connection_url": "mysql://localhost/test",
196 "table_name": "events",
197 "column_mapping": {"json": {"column": "data"}},
198 "batch_size": 250,
199 "max_connections": 5
200 }"#;
201 let config: MysqlSinkConfig = serde_json::from_str(json).unwrap();
202 assert_eq!(config.batch_size, 250);
203 }
204
205 #[test]
206 fn batch_size_defaults_when_absent_in_json() {
207 let json = r#"{
208 "connection_url": "mysql://localhost/test",
209 "table_name": "events",
210 "column_mapping": {"json": {"column": "data"}},
211 "max_connections": 5
212 }"#;
213 let config: MysqlSinkConfig = serde_json::from_str(json).unwrap();
214 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
215 }
216
217 #[test]
218 fn with_batch_size_chaining() {
219 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
220 .with_batch_size(100)
221 .with_batch_size(2_000);
222 assert_eq!(config.batch_size, 2_000);
223 }
224
225 #[test]
226 fn max_connections_and_column_mapping_default_when_absent_in_json() {
227 let json = r#"{
230 "connection_url": "mysql://localhost/test",
231 "table_name": "events"
232 }"#;
233 let config: MysqlSinkConfig = serde_json::from_str(json).unwrap();
234 assert_eq!(config.max_connections, 5);
235 assert!(matches!(
236 config.column_mapping,
237 MysqlColumnMapping::Json { .. }
238 ));
239 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
240 }
241}