faucet_source_databricks/
config.rs1use faucet_core::{AuthSpec, DEFAULT_BATCH_SIZE, FaucetError};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum ResultDisposition {
13 #[default]
16 ExternalLinks,
17 Inline,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum DatabricksReplication {
29 #[default]
31 Full,
32 Incremental {
39 column: String,
41 initial_value: Value,
43 },
44}
45
46fn default_wait_timeout() -> u64 {
47 50
48}
49
50fn default_statement_timeout() -> u64 {
51 3600
52}
53
54fn default_poll_interval() -> u64 {
55 1
56}
57
58fn default_batch_size() -> usize {
59 DEFAULT_BATCH_SIZE
60}
61
62#[derive(Clone, Serialize, Deserialize, JsonSchema)]
75#[serde(tag = "type", content = "config", rename_all = "snake_case")]
76pub enum DatabricksAuth {
77 Pat {
79 token: String,
81 },
82 Token {
84 token: String,
86 },
87}
88
89impl std::fmt::Debug for DatabricksAuth {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 faucet_common_databricks::DatabricksAuth::from(self.clone()).fmt(f)
92 }
93}
94
95impl DatabricksAuth {
96 pub fn authorization_value(&self) -> String {
98 faucet_common_databricks::DatabricksAuth::from(self.clone()).authorization_value()
99 }
100}
101
102impl From<DatabricksAuth> for faucet_common_databricks::DatabricksAuth {
103 fn from(a: DatabricksAuth) -> Self {
104 match a {
105 DatabricksAuth::Pat { token } => Self::Pat { token },
106 DatabricksAuth::Token { token } => Self::Token { token },
107 }
108 }
109}
110
111pub(crate) fn shared_auth(
113 auth: &AuthSpec<DatabricksAuth>,
114) -> AuthSpec<faucet_common_databricks::DatabricksAuth> {
115 match auth {
116 AuthSpec::Inline(a) => AuthSpec::Inline(a.clone().into()),
117 AuthSpec::Reference(r) => AuthSpec::Reference(r.clone()),
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
123#[serde(deny_unknown_fields)]
124pub struct DatabricksParam {
125 pub name: String,
127 #[serde(default)]
130 pub value: Value,
131 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
134 pub param_type: Option<String>,
135}
136
137#[derive(Clone, Serialize, Deserialize, JsonSchema)]
142#[serde(deny_unknown_fields)]
143pub struct DatabricksSourceConfig {
144 pub workspace_url: String,
146 pub warehouse_id: String,
148 pub sql: String,
152 pub auth: AuthSpec<DatabricksAuth>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub catalog: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub schema: Option<String>,
160 #[serde(default)]
162 pub parameters: Vec<DatabricksParam>,
163 #[serde(default = "default_wait_timeout")]
166 pub wait_timeout_secs: u64,
167 #[serde(default = "default_poll_interval")]
169 pub poll_interval_secs: u64,
170 #[serde(default = "default_statement_timeout")]
173 pub statement_timeout_secs: u64,
174 #[serde(default = "default_batch_size")]
182 pub batch_size: usize,
183 #[serde(default)]
195 pub arrow_native: bool,
196 #[serde(default)]
202 pub result_disposition: ResultDisposition,
203 #[serde(default)]
205 pub replication: DatabricksReplication,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub state_key: Option<String>,
210}
211
212impl DatabricksSourceConfig {
213 pub fn validate(&self) -> Result<(), FaucetError> {
215 if self.workspace_url.trim().is_empty() {
216 return Err(FaucetError::Config(
217 "databricks: `workspace_url` must not be empty".into(),
218 ));
219 }
220 if self.warehouse_id.trim().is_empty() {
221 return Err(FaucetError::Config(
222 "databricks: `warehouse_id` must not be empty".into(),
223 ));
224 }
225 if self.sql.trim().is_empty() {
226 return Err(FaucetError::Config(
227 "databricks: `sql` must not be empty".into(),
228 ));
229 }
230 if self.wait_timeout_secs != 0 && !(5..=50).contains(&self.wait_timeout_secs) {
232 return Err(FaucetError::Config(format!(
233 "databricks: `wait_timeout_secs` must be 0 or between 5 and 50 (got {})",
234 self.wait_timeout_secs
235 )));
236 }
237 faucet_core::validate_batch_size(self.batch_size)?;
238 if self.arrow_native {
239 if !cfg!(feature = "arrow") {
240 return Err(FaucetError::Config(
241 "databricks: `arrow_native` requires the crate-local `arrow` feature to be \
242 enabled"
243 .into(),
244 ));
245 }
246 if !matches!(self.replication, DatabricksReplication::Full) {
247 return Err(FaucetError::Config(
248 "databricks: `arrow_native` currently supports only `replication: full` — the \
249 columnar path does not run the client-side incremental filter"
250 .into(),
251 ));
252 }
253 }
254 Ok(())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use serde_json::json;
262
263 fn base() -> DatabricksSourceConfig {
264 DatabricksSourceConfig {
265 workspace_url: "https://x.cloud.databricks.com".into(),
266 warehouse_id: "wh1".into(),
267 sql: "SELECT 1".into(),
268 auth: AuthSpec::Inline(DatabricksAuth::Pat { token: "t".into() }),
269 catalog: None,
270 schema: None,
271 parameters: Vec::new(),
272 wait_timeout_secs: default_wait_timeout(),
273 poll_interval_secs: default_poll_interval(),
274 statement_timeout_secs: default_statement_timeout(),
275 batch_size: DEFAULT_BATCH_SIZE,
276 arrow_native: false,
277 result_disposition: ResultDisposition::default(),
278 replication: DatabricksReplication::Full,
279 state_key: None,
280 }
281 }
282
283 #[test]
284 fn valid_config_passes() {
285 base().validate().unwrap();
286 }
287
288 #[cfg(feature = "arrow")]
289 #[test]
290 fn arrow_native_full_passes_but_incremental_rejected() {
291 let mut c = base();
292 c.arrow_native = true;
293 c.validate().unwrap();
294 c.replication = DatabricksReplication::Incremental {
295 column: "ts".into(),
296 initial_value: json!("2026-01-01"),
297 };
298 let err = c.validate().unwrap_err();
299 assert!(err.to_string().contains("arrow_native"));
300 }
301
302 #[cfg(not(feature = "arrow"))]
303 #[test]
304 fn arrow_native_requires_feature() {
305 let mut c = base();
306 c.arrow_native = true;
307 let err = c.validate().unwrap_err();
308 assert!(err.to_string().contains("arrow"));
309 }
310
311 #[test]
312 fn auth_is_bearer_for_both_variants() {
313 assert_eq!(
314 DatabricksAuth::Pat {
315 token: "abc".into()
316 }
317 .authorization_value(),
318 "Bearer abc"
319 );
320 assert_eq!(
321 DatabricksAuth::Token {
322 token: "xyz".into()
323 }
324 .authorization_value(),
325 "Bearer xyz"
326 );
327 let dbg = format!(
328 "{:?}",
329 DatabricksAuth::Token {
330 token: "xyz".into()
331 }
332 );
333 assert!(!dbg.contains("xyz"));
334 let r = shared_auth(&AuthSpec::Reference(faucet_core::AuthReference {
335 name: "p".into(),
336 }));
337 assert!(matches!(r, AuthSpec::Reference(_)));
338 }
339
340 #[test]
341 fn rejects_empty_required_fields() {
342 let mut c = base();
343 c.workspace_url = " ".into();
344 assert!(c.validate().is_err());
345 let mut c = base();
346 c.warehouse_id = "".into();
347 assert!(c.validate().is_err());
348 let mut c = base();
349 c.sql = "".into();
350 assert!(c.validate().is_err());
351 }
352
353 #[test]
354 fn rejects_bad_wait_timeout() {
355 let mut c = base();
356 c.wait_timeout_secs = 3; assert!(c.validate().is_err());
358 c.wait_timeout_secs = 51;
359 assert!(c.validate().is_err());
360 c.wait_timeout_secs = 0; assert!(c.validate().is_ok());
362 }
363
364 #[test]
365 fn rejects_oversized_batch() {
366 let mut c = base();
367 c.batch_size = faucet_core::MAX_BATCH_SIZE + 1;
368 assert!(c.validate().is_err());
369 }
370
371 #[test]
372 fn deserializes_full_shape() {
373 let v = json!({
374 "workspace_url": "https://x.cloud.databricks.com",
375 "warehouse_id": "wh1",
376 "sql": "SELECT * FROM t WHERE id > :min",
377 "auth": { "type": "pat", "config": { "token": "tok" } },
378 "catalog": "main",
379 "schema": "sales",
380 "parameters": [{ "name": "min", "value": 10, "type": "INT" }],
381 "wait_timeout_secs": 30,
382 "batch_size": 500
383 });
384 let c: DatabricksSourceConfig = serde_json::from_value(v).unwrap();
385 assert_eq!(c.warehouse_id, "wh1");
386 assert_eq!(c.catalog.as_deref(), Some("main"));
387 assert_eq!(c.parameters.len(), 1);
388 assert_eq!(c.parameters[0].name, "min");
389 assert_eq!(c.parameters[0].param_type.as_deref(), Some("INT"));
390 c.validate().unwrap();
391 }
392}