1use faucet_core::DEFAULT_BATCH_SIZE;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum PostgresColumnMapping {
11 Jsonb { column: String },
14 AutoMap,
17}
18
19impl Default for PostgresColumnMapping {
20 fn default() -> Self {
21 Self::Jsonb {
22 column: "data".into(),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "snake_case")]
30pub enum PostgresWriteMethod {
31 #[default]
34 Insert,
35 Copy,
48}
49
50#[derive(Clone, Serialize, Deserialize, JsonSchema)]
52pub struct PostgresSinkConfig {
53 pub connection_url: String,
55 pub table_name: String,
57 #[serde(default)]
66 pub schema: Option<String>,
67 #[serde(default)]
70 pub column_mapping: PostgresColumnMapping,
71 #[serde(default = "default_batch_size")]
92 pub batch_size: usize,
93 #[serde(default = "default_max_connections")]
99 pub max_connections: u32,
100 #[serde(flatten)]
104 pub write: faucet_core::WriteSpec,
105 #[serde(default)]
111 pub write_method: PostgresWriteMethod,
112}
113
114fn default_batch_size() -> usize {
115 DEFAULT_BATCH_SIZE
116}
117
118fn default_max_connections() -> u32 {
119 5
120}
121
122impl std::fmt::Debug for PostgresSinkConfig {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.debug_struct("PostgresSinkConfig")
125 .field("connection_url", &"***")
126 .field("table_name", &self.table_name)
127 .field("schema", &self.schema)
128 .field("column_mapping", &self.column_mapping)
129 .field("batch_size", &self.batch_size)
130 .field("max_connections", &self.max_connections)
131 .field("write_method", &self.write_method)
132 .finish()
133 }
134}
135
136impl PostgresSinkConfig {
137 pub fn new(connection_url: impl Into<String>, table_name: impl Into<String>) -> Self {
139 Self {
140 connection_url: connection_url.into(),
141 table_name: table_name.into(),
142 schema: None,
143 column_mapping: PostgresColumnMapping::default(),
144 batch_size: DEFAULT_BATCH_SIZE,
145 max_connections: 5,
146 write: faucet_core::WriteSpec::default(),
147 write_method: PostgresWriteMethod::default(),
148 }
149 }
150
151 pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
154 self.schema = Some(schema.into());
155 self
156 }
157
158 pub fn column_mapping(mut self, mapping: PostgresColumnMapping) -> Self {
160 self.column_mapping = mapping;
161 self
162 }
163
164 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
170 self.batch_size = batch_size;
171 self
172 }
173
174 pub fn max_connections(mut self, n: u32) -> Self {
176 self.max_connections = n;
177 self
178 }
179
180 pub fn with_write_method(mut self, method: PostgresWriteMethod) -> Self {
183 self.write_method = method;
184 self
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn default_config() {
194 let config = PostgresSinkConfig::new("postgres://localhost/test", "events");
195 assert_eq!(config.table_name, "events");
196 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
197 assert!(matches!(
198 config.column_mapping,
199 PostgresColumnMapping::Jsonb { ref column } if column == "data"
200 ));
201 }
202
203 #[test]
204 fn builder_methods() {
205 let config = PostgresSinkConfig::new("postgres://localhost/test", "events")
206 .column_mapping(PostgresColumnMapping::AutoMap)
207 .with_batch_size(100);
208 assert_eq!(config.batch_size, 100);
209 assert!(matches!(
210 config.column_mapping,
211 PostgresColumnMapping::AutoMap
212 ));
213 }
214
215 #[test]
216 fn jsonb_custom_column() {
217 let config = PostgresSinkConfig::new("postgres://localhost/test", "events").column_mapping(
218 PostgresColumnMapping::Jsonb {
219 column: "payload".into(),
220 },
221 );
222 assert!(matches!(
223 config.column_mapping,
224 PostgresColumnMapping::Jsonb { ref column } if column == "payload"
225 ));
226 }
227
228 #[test]
229 fn with_batch_size_overrides_default() {
230 let config =
231 PostgresSinkConfig::new("postgres://localhost/test", "events").with_batch_size(250);
232 assert_eq!(config.batch_size, 250);
233 }
234
235 #[test]
236 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
237 let config =
238 PostgresSinkConfig::new("postgres://localhost/test", "events").with_batch_size(0);
239 assert_eq!(config.batch_size, 0);
240 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
241 }
242
243 #[test]
244 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
245 let config = PostgresSinkConfig::new("postgres://localhost/test", "events")
246 .with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
247 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
248 }
249
250 #[test]
251 fn batch_size_deserializes_from_json() {
252 let json = r#"{
253 "connection_url": "postgres://localhost/test",
254 "table_name": "events",
255 "column_mapping": {"jsonb": {"column": "data"}},
256 "batch_size": 250,
257 "max_connections": 5
258 }"#;
259 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
260 assert_eq!(config.batch_size, 250);
261 }
262
263 #[test]
264 fn batch_size_defaults_when_absent_in_json() {
265 let json = r#"{
266 "connection_url": "postgres://localhost/test",
267 "table_name": "events",
268 "column_mapping": {"jsonb": {"column": "data"}},
269 "max_connections": 5
270 }"#;
271 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
272 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
273 }
274
275 #[test]
276 fn config_builder_chaining() {
277 let config = PostgresSinkConfig::new("postgres://localhost/test", "events")
278 .with_batch_size(100)
279 .with_batch_size(250);
280 assert_eq!(config.batch_size, 250);
281 }
282
283 #[test]
284 fn max_connections_and_column_mapping_default_when_absent_in_json() {
285 let json = r#"{
289 "connection_url": "postgres://localhost/test",
290 "table_name": "events"
291 }"#;
292 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
293 assert_eq!(config.max_connections, 5);
294 assert!(matches!(
295 config.column_mapping,
296 PostgresColumnMapping::Jsonb { .. }
297 ));
298 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
299 }
300
301 #[test]
302 fn max_connections_deserializes_when_present() {
303 let json = r#"{
304 "connection_url": "postgres://localhost/test",
305 "table_name": "events",
306 "max_connections": 20
307 }"#;
308 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
309 assert_eq!(config.max_connections, 20);
310 }
311
312 #[test]
313 fn write_method_defaults_to_insert() {
314 let config = PostgresSinkConfig::new("postgres://localhost/test", "events");
315 assert_eq!(config.write_method, PostgresWriteMethod::Insert);
316
317 let json = r#"{
319 "connection_url": "postgres://localhost/test",
320 "table_name": "events"
321 }"#;
322 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
323 assert_eq!(config.write_method, PostgresWriteMethod::Insert);
324 }
325
326 #[test]
327 fn write_method_serde_round_trips() {
328 let json = r#"{
329 "connection_url": "postgres://localhost/test",
330 "table_name": "events",
331 "write_method": "copy"
332 }"#;
333 let config: PostgresSinkConfig = serde_json::from_str(json).unwrap();
334 assert_eq!(config.write_method, PostgresWriteMethod::Copy);
335 let text = serde_json::to_string(&config).unwrap();
336 assert!(text.contains("\"write_method\":\"copy\""));
337 }
338
339 #[test]
340 fn with_write_method_builder() {
341 let config = PostgresSinkConfig::new("postgres://localhost/test", "events")
342 .with_write_method(PostgresWriteMethod::Copy);
343 assert_eq!(config.write_method, PostgresWriteMethod::Copy);
344 }
345}