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 pub column_mapping: MysqlColumnMapping,
36 #[serde(default = "default_batch_size")]
51 pub batch_size: usize,
52 pub max_connections: u32,
54 #[serde(flatten)]
62 pub write: WriteSpec,
63}
64
65fn default_batch_size() -> usize {
66 DEFAULT_BATCH_SIZE
67}
68
69impl std::fmt::Debug for MysqlSinkConfig {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("MysqlSinkConfig")
72 .field("connection_url", &"***")
73 .field("table_name", &self.table_name)
74 .field("column_mapping", &self.column_mapping)
75 .field("batch_size", &self.batch_size)
76 .field("max_connections", &self.max_connections)
77 .finish()
78 }
79}
80
81impl MysqlSinkConfig {
82 pub fn new(connection_url: impl Into<String>, table_name: impl Into<String>) -> Self {
84 Self {
85 connection_url: connection_url.into(),
86 table_name: table_name.into(),
87 column_mapping: MysqlColumnMapping::default(),
88 batch_size: DEFAULT_BATCH_SIZE,
89 max_connections: 5,
90 write: WriteSpec::default(),
91 }
92 }
93
94 pub fn column_mapping(mut self, mapping: MysqlColumnMapping) -> Self {
96 self.column_mapping = mapping;
97 self
98 }
99
100 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
106 self.batch_size = batch_size;
107 self
108 }
109
110 pub fn max_connections(mut self, n: u32) -> Self {
112 self.max_connections = n;
113 self
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn default_config() {
123 let config = MysqlSinkConfig::new("mysql://localhost/test", "events");
124 assert_eq!(config.table_name, "events");
125 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
126 assert!(matches!(
127 config.column_mapping,
128 MysqlColumnMapping::Json { ref column } if column == "data"
129 ));
130 }
131
132 #[test]
133 fn builder_methods() {
134 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
135 .column_mapping(MysqlColumnMapping::AutoMap)
136 .with_batch_size(100);
137 assert_eq!(config.batch_size, 100);
138 assert!(matches!(config.column_mapping, MysqlColumnMapping::AutoMap));
139 }
140
141 #[test]
142 fn with_batch_size_overrides_default() {
143 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").with_batch_size(250);
144 assert_eq!(config.batch_size, 250);
145 }
146
147 #[test]
148 fn json_custom_column() {
149 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").column_mapping(
150 MysqlColumnMapping::Json {
151 column: "payload".into(),
152 },
153 );
154 assert!(matches!(
155 config.column_mapping,
156 MysqlColumnMapping::Json { ref column } if column == "payload"
157 ));
158 }
159
160 #[test]
161 fn debug_masks_connection_url() {
162 let config = MysqlSinkConfig::new("mysql://secret:pass@host/db", "events");
163 let debug = format!("{config:?}");
164 assert!(debug.contains("***"));
165 assert!(!debug.contains("secret"));
166 assert!(!debug.contains("pass"));
167 }
168
169 #[test]
170 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
171 let config = MysqlSinkConfig::new("mysql://localhost/test", "events").with_batch_size(0);
172 assert_eq!(config.batch_size, 0);
173 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
174 }
175
176 #[test]
177 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
178 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
179 .with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
180 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
181 }
182
183 #[test]
184 fn batch_size_deserializes_from_json() {
185 let json = r#"{
186 "connection_url": "mysql://localhost/test",
187 "table_name": "events",
188 "column_mapping": {"json": {"column": "data"}},
189 "batch_size": 250,
190 "max_connections": 5
191 }"#;
192 let config: MysqlSinkConfig = serde_json::from_str(json).unwrap();
193 assert_eq!(config.batch_size, 250);
194 }
195
196 #[test]
197 fn batch_size_defaults_when_absent_in_json() {
198 let json = r#"{
199 "connection_url": "mysql://localhost/test",
200 "table_name": "events",
201 "column_mapping": {"json": {"column": "data"}},
202 "max_connections": 5
203 }"#;
204 let config: MysqlSinkConfig = serde_json::from_str(json).unwrap();
205 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
206 }
207
208 #[test]
209 fn with_batch_size_chaining() {
210 let config = MysqlSinkConfig::new("mysql://localhost/test", "events")
211 .with_batch_size(100)
212 .with_batch_size(2_000);
213 assert_eq!(config.batch_size, 2_000);
214 }
215}