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, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum DatabricksReplication {
15 #[default]
17 Full,
18 Incremental {
25 column: String,
27 initial_value: Value,
29 },
30}
31
32fn default_wait_timeout() -> u64 {
33 50
34}
35
36fn default_poll_interval() -> u64 {
37 1
38}
39
40fn default_batch_size() -> usize {
41 DEFAULT_BATCH_SIZE
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53#[serde(tag = "type", content = "config", rename_all = "snake_case")]
54pub enum DatabricksAuth {
55 Pat {
57 token: String,
59 },
60 Token {
62 token: String,
64 },
65}
66
67impl DatabricksAuth {
68 pub fn authorization_value(&self) -> String {
70 match self {
71 DatabricksAuth::Pat { token } | DatabricksAuth::Token { token } => {
72 format!("Bearer {token}")
73 }
74 }
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
80pub struct DatabricksParam {
81 pub name: String,
83 #[serde(default)]
86 pub value: Value,
87 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
90 pub param_type: Option<String>,
91}
92
93#[derive(Clone, Serialize, Deserialize, JsonSchema)]
98pub struct DatabricksSourceConfig {
99 pub workspace_url: String,
101 pub warehouse_id: String,
103 pub sql: String,
107 pub auth: AuthSpec<DatabricksAuth>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub catalog: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub schema: Option<String>,
115 #[serde(default)]
117 pub parameters: Vec<DatabricksParam>,
118 #[serde(default = "default_wait_timeout")]
121 pub wait_timeout_secs: u64,
122 #[serde(default = "default_poll_interval")]
124 pub poll_interval_secs: u64,
125 #[serde(default = "default_batch_size")]
127 pub batch_size: usize,
128 #[serde(default)]
140 pub arrow_native: bool,
141 #[serde(default)]
143 pub replication: DatabricksReplication,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub state_key: Option<String>,
148}
149
150impl DatabricksSourceConfig {
151 pub fn validate(&self) -> Result<(), FaucetError> {
153 if self.workspace_url.trim().is_empty() {
154 return Err(FaucetError::Config(
155 "databricks: `workspace_url` must not be empty".into(),
156 ));
157 }
158 if self.warehouse_id.trim().is_empty() {
159 return Err(FaucetError::Config(
160 "databricks: `warehouse_id` must not be empty".into(),
161 ));
162 }
163 if self.sql.trim().is_empty() {
164 return Err(FaucetError::Config(
165 "databricks: `sql` must not be empty".into(),
166 ));
167 }
168 if self.wait_timeout_secs != 0 && !(5..=50).contains(&self.wait_timeout_secs) {
170 return Err(FaucetError::Config(format!(
171 "databricks: `wait_timeout_secs` must be 0 or between 5 and 50 (got {})",
172 self.wait_timeout_secs
173 )));
174 }
175 faucet_core::validate_batch_size(self.batch_size)?;
176 if self.arrow_native {
177 if !cfg!(feature = "arrow") {
178 return Err(FaucetError::Config(
179 "databricks: `arrow_native` requires the crate-local `arrow` feature to be \
180 enabled"
181 .into(),
182 ));
183 }
184 if !matches!(self.replication, DatabricksReplication::Full) {
185 return Err(FaucetError::Config(
186 "databricks: `arrow_native` currently supports only `replication: full` — the \
187 columnar path does not run the client-side incremental filter"
188 .into(),
189 ));
190 }
191 }
192 Ok(())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use serde_json::json;
200
201 fn base() -> DatabricksSourceConfig {
202 DatabricksSourceConfig {
203 workspace_url: "https://x.cloud.databricks.com".into(),
204 warehouse_id: "wh1".into(),
205 sql: "SELECT 1".into(),
206 auth: AuthSpec::Inline(DatabricksAuth::Pat { token: "t".into() }),
207 catalog: None,
208 schema: None,
209 parameters: Vec::new(),
210 wait_timeout_secs: default_wait_timeout(),
211 poll_interval_secs: default_poll_interval(),
212 batch_size: DEFAULT_BATCH_SIZE,
213 arrow_native: false,
214 replication: DatabricksReplication::Full,
215 state_key: None,
216 }
217 }
218
219 #[test]
220 fn valid_config_passes() {
221 base().validate().unwrap();
222 }
223
224 #[cfg(feature = "arrow")]
225 #[test]
226 fn arrow_native_full_passes_but_incremental_rejected() {
227 let mut c = base();
228 c.arrow_native = true;
229 c.validate().unwrap();
230 c.replication = DatabricksReplication::Incremental {
231 column: "ts".into(),
232 initial_value: json!("2026-01-01"),
233 };
234 let err = c.validate().unwrap_err();
235 assert!(err.to_string().contains("arrow_native"));
236 }
237
238 #[cfg(not(feature = "arrow"))]
239 #[test]
240 fn arrow_native_requires_feature() {
241 let mut c = base();
242 c.arrow_native = true;
243 let err = c.validate().unwrap_err();
244 assert!(err.to_string().contains("arrow"));
245 }
246
247 #[test]
248 fn auth_is_bearer_for_both_variants() {
249 assert_eq!(
250 DatabricksAuth::Pat {
251 token: "abc".into()
252 }
253 .authorization_value(),
254 "Bearer abc"
255 );
256 assert_eq!(
257 DatabricksAuth::Token {
258 token: "xyz".into()
259 }
260 .authorization_value(),
261 "Bearer xyz"
262 );
263 }
264
265 #[test]
266 fn rejects_empty_required_fields() {
267 let mut c = base();
268 c.workspace_url = " ".into();
269 assert!(c.validate().is_err());
270 let mut c = base();
271 c.warehouse_id = "".into();
272 assert!(c.validate().is_err());
273 let mut c = base();
274 c.sql = "".into();
275 assert!(c.validate().is_err());
276 }
277
278 #[test]
279 fn rejects_bad_wait_timeout() {
280 let mut c = base();
281 c.wait_timeout_secs = 3; assert!(c.validate().is_err());
283 c.wait_timeout_secs = 51;
284 assert!(c.validate().is_err());
285 c.wait_timeout_secs = 0; assert!(c.validate().is_ok());
287 }
288
289 #[test]
290 fn rejects_oversized_batch() {
291 let mut c = base();
292 c.batch_size = faucet_core::MAX_BATCH_SIZE + 1;
293 assert!(c.validate().is_err());
294 }
295
296 #[test]
297 fn deserializes_full_shape() {
298 let v = json!({
299 "workspace_url": "https://x.cloud.databricks.com",
300 "warehouse_id": "wh1",
301 "sql": "SELECT * FROM t WHERE id > :min",
302 "auth": { "type": "pat", "config": { "token": "tok" } },
303 "catalog": "main",
304 "schema": "sales",
305 "parameters": [{ "name": "min", "value": 10, "type": "INT" }],
306 "wait_timeout_secs": 30,
307 "batch_size": 500
308 });
309 let c: DatabricksSourceConfig = serde_json::from_value(v).unwrap();
310 assert_eq!(c.warehouse_id, "wh1");
311 assert_eq!(c.catalog.as_deref(), Some("main"));
312 assert_eq!(c.parameters.len(), 1);
313 assert_eq!(c.parameters[0].name, "min");
314 assert_eq!(c.parameters[0].param_type.as_deref(), Some("INT"));
315 c.validate().unwrap();
316 }
317}