1use faucet_core::{AuthSpec, DEFAULT_BATCH_SIZE};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::time::Duration;
8
9pub use faucet_common_snowflake::SnowflakeAuth;
11
12fn default_statement_timeout() -> Duration {
13 Duration::from_secs(60)
14}
15
16fn default_poll_timeout() -> Duration {
17 Duration::from_secs(300)
18}
19
20fn default_batch_size() -> usize {
21 DEFAULT_BATCH_SIZE
22}
23
24#[derive(Clone, Serialize, Deserialize, JsonSchema)]
26pub struct SnowflakeSourceConfig {
27 pub account: String,
29 pub warehouse: String,
31 pub database: String,
33 pub schema: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub role: Option<String>,
38 pub auth: AuthSpec<SnowflakeAuth>,
43 pub query: String,
48 #[serde(default)]
53 pub params: Vec<Value>,
54 #[serde(
59 default = "default_statement_timeout",
60 with = "faucet_core::config::duration_secs"
61 )]
62 #[schemars(with = "u64")]
63 pub statement_timeout: Duration,
64 #[serde(
72 default = "default_poll_timeout",
73 with = "faucet_core::config::duration_secs"
74 )]
75 #[schemars(with = "u64")]
76 pub poll_timeout: Duration,
77 #[serde(default = "default_batch_size")]
90 pub batch_size: usize,
91}
92
93impl std::fmt::Debug for SnowflakeSourceConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("SnowflakeSourceConfig")
96 .field("account", &self.account)
97 .field("warehouse", &self.warehouse)
98 .field("database", &self.database)
99 .field("schema", &self.schema)
100 .field("role", &self.role)
101 .field("auth", &self.auth)
102 .field("query", &self.query)
103 .field("params", &self.params)
104 .field("statement_timeout", &self.statement_timeout)
105 .field("poll_timeout", &self.poll_timeout)
106 .field("batch_size", &self.batch_size)
107 .finish()
108 }
109}
110
111impl SnowflakeSourceConfig {
112 pub fn new(
114 account: impl Into<String>,
115 warehouse: impl Into<String>,
116 database: impl Into<String>,
117 schema: impl Into<String>,
118 auth: SnowflakeAuth,
119 query: impl Into<String>,
120 ) -> Self {
121 Self {
122 account: account.into(),
123 warehouse: warehouse.into(),
124 database: database.into(),
125 schema: schema.into(),
126 role: None,
127 auth: AuthSpec::Inline(auth),
128 query: query.into(),
129 params: Vec::new(),
130 statement_timeout: default_statement_timeout(),
131 poll_timeout: default_poll_timeout(),
132 batch_size: DEFAULT_BATCH_SIZE,
133 }
134 }
135
136 pub fn with_role(mut self, role: impl Into<String>) -> Self {
138 self.role = Some(role.into());
139 self
140 }
141
142 pub fn with_params(mut self, params: Vec<Value>) -> Self {
144 self.params = params;
145 self
146 }
147
148 pub fn with_statement_timeout(mut self, timeout: Duration) -> Self {
150 self.statement_timeout = timeout;
151 self
152 }
153
154 pub fn with_poll_timeout(mut self, timeout: Duration) -> Self {
157 self.poll_timeout = timeout;
158 self
159 }
160
161 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
166 self.batch_size = batch_size;
167 self
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use serde_json::json;
175
176 fn sample_config() -> SnowflakeSourceConfig {
177 SnowflakeSourceConfig::new(
178 "xy12345",
179 "COMPUTE_WH",
180 "MY_DB",
181 "PUBLIC",
182 SnowflakeAuth::OAuth { token: "t".into() },
183 "SELECT * FROM events",
184 )
185 }
186
187 #[test]
188 fn default_config() {
189 let cfg = sample_config();
190 assert_eq!(cfg.account, "xy12345");
191 assert_eq!(cfg.warehouse, "COMPUTE_WH");
192 assert_eq!(cfg.database, "MY_DB");
193 assert_eq!(cfg.schema, "PUBLIC");
194 assert!(cfg.role.is_none());
195 assert!(cfg.params.is_empty());
196 assert_eq!(cfg.statement_timeout, Duration::from_secs(60));
197 assert_eq!(cfg.poll_timeout, Duration::from_secs(300));
198 assert_eq!(cfg.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
199 }
200
201 #[test]
202 fn builders_compose() {
203 let cfg = sample_config()
204 .with_role("ANALYST")
205 .with_params(vec![json!(42)])
206 .with_statement_timeout(Duration::from_secs(15))
207 .with_batch_size(500);
208 assert_eq!(cfg.role.as_deref(), Some("ANALYST"));
209 assert_eq!(cfg.params, vec![json!(42)]);
210 assert_eq!(cfg.statement_timeout, Duration::from_secs(15));
211 assert_eq!(cfg.batch_size, 500);
212 }
213
214 #[test]
215 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
216 let cfg = sample_config().with_batch_size(0);
217 assert_eq!(cfg.batch_size, 0);
218 assert!(faucet_core::validate_batch_size(cfg.batch_size).is_ok());
219 }
220
221 #[test]
222 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
223 let cfg = sample_config().with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
224 assert!(faucet_core::validate_batch_size(cfg.batch_size).is_err());
225 }
226
227 #[test]
228 fn deserializes_minimal_json() {
229 let json = r#"{
230 "account": "xy12345",
231 "warehouse": "WH",
232 "database": "DB",
233 "schema": "PUBLIC",
234 "auth": {"type": "oauth", "config": {"token": "t"}},
235 "query": "SELECT 1"
236 }"#;
237 let cfg: SnowflakeSourceConfig = serde_json::from_str(json).unwrap();
238 assert_eq!(cfg.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
239 assert_eq!(cfg.statement_timeout, Duration::from_secs(60));
240 assert!(cfg.role.is_none());
241 assert!(cfg.params.is_empty());
242 }
243
244 #[test]
245 fn deserializes_all_optional_fields() {
246 let json = r#"{
247 "account": "xy12345",
248 "warehouse": "WH",
249 "database": "DB",
250 "schema": "PUBLIC",
251 "role": "ANALYST",
252 "auth": {"type": "oauth", "config": {"token": "t"}},
253 "query": "SELECT $1",
254 "params": [42],
255 "statement_timeout": 15,
256 "batch_size": 250
257 }"#;
258 let cfg: SnowflakeSourceConfig = serde_json::from_str(json).unwrap();
259 assert_eq!(cfg.role.as_deref(), Some("ANALYST"));
260 assert_eq!(cfg.params, vec![json!(42)]);
261 assert_eq!(cfg.statement_timeout, Duration::from_secs(15));
262 assert_eq!(cfg.batch_size, 250);
263 }
264
265 #[test]
266 fn debug_masks_auth_secrets() {
267 let cfg = SnowflakeSourceConfig::new(
268 "acct",
269 "wh",
270 "db",
271 "schema",
272 SnowflakeAuth::KeyPair {
273 user: "alice".into(),
274 private_key_pem: "PRIVATE-KEY-DATA".into(),
275 },
276 "SELECT 1",
277 );
278 let dbg = format!("{cfg:?}");
279 assert!(dbg.contains("***"));
280 assert!(!dbg.contains("PRIVATE-KEY-DATA"));
281 }
282}