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 #[schemars(extend("x-faucet-resource" = "url"))]
146 pub workspace_url: String,
147 pub warehouse_id: String,
149 pub sql: String,
153 pub auth: AuthSpec<DatabricksAuth>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub catalog: Option<String>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub schema: Option<String>,
161 #[serde(default)]
163 pub parameters: Vec<DatabricksParam>,
164 #[serde(default = "default_wait_timeout")]
167 pub wait_timeout_secs: u64,
168 #[serde(default = "default_poll_interval")]
170 pub poll_interval_secs: u64,
171 #[serde(default = "default_statement_timeout")]
174 pub statement_timeout_secs: u64,
175 #[serde(default = "default_batch_size")]
183 pub batch_size: usize,
184 #[serde(default)]
196 pub arrow_native: bool,
197 #[serde(default)]
203 pub result_disposition: ResultDisposition,
204 #[serde(default)]
206 pub replication: DatabricksReplication,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub state_key: Option<String>,
211}
212
213impl DatabricksSourceConfig {
214 pub fn validate(&self) -> Result<(), FaucetError> {
216 if self.workspace_url.trim().is_empty() {
217 return Err(FaucetError::Config(
218 "databricks: `workspace_url` must not be empty".into(),
219 ));
220 }
221 if self.warehouse_id.trim().is_empty() {
222 return Err(FaucetError::Config(
223 "databricks: `warehouse_id` must not be empty".into(),
224 ));
225 }
226 if self.sql.trim().is_empty() {
227 return Err(FaucetError::Config(
228 "databricks: `sql` must not be empty".into(),
229 ));
230 }
231 if self.wait_timeout_secs != 0 && !(5..=50).contains(&self.wait_timeout_secs) {
233 return Err(FaucetError::Config(format!(
234 "databricks: `wait_timeout_secs` must be 0 or between 5 and 50 (got {})",
235 self.wait_timeout_secs
236 )));
237 }
238 faucet_core::validate_batch_size(self.batch_size)?;
239 if self.arrow_native {
240 if !cfg!(feature = "arrow") {
241 return Err(FaucetError::Config(
242 "databricks: `arrow_native` requires the crate-local `arrow` feature to be \
243 enabled"
244 .into(),
245 ));
246 }
247 if !matches!(self.replication, DatabricksReplication::Full) {
248 return Err(FaucetError::Config(
249 "databricks: `arrow_native` currently supports only `replication: full` — the \
250 columnar path does not run the client-side incremental filter"
251 .into(),
252 ));
253 }
254 }
255 Ok(())
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use serde_json::json;
263
264 fn base() -> DatabricksSourceConfig {
265 DatabricksSourceConfig {
266 workspace_url: "https://x.cloud.databricks.com".into(),
267 warehouse_id: "wh1".into(),
268 sql: "SELECT 1".into(),
269 auth: AuthSpec::Inline(DatabricksAuth::Pat { token: "t".into() }),
270 catalog: None,
271 schema: None,
272 parameters: Vec::new(),
273 wait_timeout_secs: default_wait_timeout(),
274 poll_interval_secs: default_poll_interval(),
275 statement_timeout_secs: default_statement_timeout(),
276 batch_size: DEFAULT_BATCH_SIZE,
277 arrow_native: false,
278 result_disposition: ResultDisposition::default(),
279 replication: DatabricksReplication::Full,
280 state_key: None,
281 }
282 }
283
284 #[test]
285 fn valid_config_passes() {
286 base().validate().unwrap();
287 }
288
289 #[cfg(feature = "arrow")]
290 #[test]
291 fn arrow_native_full_passes_but_incremental_rejected() {
292 let mut c = base();
293 c.arrow_native = true;
294 c.validate().unwrap();
295 c.replication = DatabricksReplication::Incremental {
296 column: "ts".into(),
297 initial_value: json!("2026-01-01"),
298 };
299 let err = c.validate().unwrap_err();
300 assert!(err.to_string().contains("arrow_native"));
301 }
302
303 #[cfg(not(feature = "arrow"))]
304 #[test]
305 fn arrow_native_requires_feature() {
306 let mut c = base();
307 c.arrow_native = true;
308 let err = c.validate().unwrap_err();
309 assert!(err.to_string().contains("arrow"));
310 }
311
312 #[test]
313 fn auth_is_bearer_for_both_variants() {
314 assert_eq!(
315 DatabricksAuth::Pat {
316 token: "abc".into()
317 }
318 .authorization_value(),
319 "Bearer abc"
320 );
321 assert_eq!(
322 DatabricksAuth::Token {
323 token: "xyz".into()
324 }
325 .authorization_value(),
326 "Bearer xyz"
327 );
328 let dbg = format!(
329 "{:?}",
330 DatabricksAuth::Token {
331 token: "xyz".into()
332 }
333 );
334 assert!(!dbg.contains("xyz"));
335 let r = shared_auth(&AuthSpec::Reference(faucet_core::AuthReference {
336 name: "p".into(),
337 }));
338 assert!(matches!(r, AuthSpec::Reference(_)));
339 }
340
341 #[test]
342 fn rejects_empty_required_fields() {
343 let mut c = base();
344 c.workspace_url = " ".into();
345 assert!(c.validate().is_err());
346 let mut c = base();
347 c.warehouse_id = "".into();
348 assert!(c.validate().is_err());
349 let mut c = base();
350 c.sql = "".into();
351 assert!(c.validate().is_err());
352 }
353
354 #[test]
355 fn rejects_bad_wait_timeout() {
356 let mut c = base();
357 c.wait_timeout_secs = 3; assert!(c.validate().is_err());
359 c.wait_timeout_secs = 51;
360 assert!(c.validate().is_err());
361 c.wait_timeout_secs = 0; assert!(c.validate().is_ok());
363 }
364
365 #[test]
366 fn rejects_oversized_batch() {
367 let mut c = base();
368 c.batch_size = faucet_core::MAX_BATCH_SIZE + 1;
369 assert!(c.validate().is_err());
370 }
371
372 #[test]
373 fn deserializes_full_shape() {
374 let v = json!({
375 "workspace_url": "https://x.cloud.databricks.com",
376 "warehouse_id": "wh1",
377 "sql": "SELECT * FROM t WHERE id > :min",
378 "auth": { "type": "pat", "config": { "token": "tok" } },
379 "catalog": "main",
380 "schema": "sales",
381 "parameters": [{ "name": "min", "value": 10, "type": "INT" }],
382 "wait_timeout_secs": 30,
383 "batch_size": 500
384 });
385 let c: DatabricksSourceConfig = serde_json::from_value(v).unwrap();
386 assert_eq!(c.warehouse_id, "wh1");
387 assert_eq!(c.catalog.as_deref(), Some("main"));
388 assert_eq!(c.parameters.len(), 1);
389 assert_eq!(c.parameters[0].name, "min");
390 assert_eq!(c.parameters[0].param_type.as_deref(), Some("INT"));
391 c.validate().unwrap();
392 }
393}