use faucet_core::{AuthSpec, DEFAULT_BATCH_SIZE, FaucetError};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DatabricksReplication {
#[default]
Full,
Incremental {
column: String,
initial_value: Value,
},
}
fn default_wait_timeout() -> u64 {
50
}
fn default_poll_interval() -> u64 {
1
}
fn default_batch_size() -> usize {
DEFAULT_BATCH_SIZE
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum DatabricksAuth {
Pat {
token: String,
},
Token {
token: String,
},
}
impl DatabricksAuth {
pub fn authorization_value(&self) -> String {
match self {
DatabricksAuth::Pat { token } | DatabricksAuth::Token { token } => {
format!("Bearer {token}")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DatabricksParam {
pub name: String,
#[serde(default)]
pub value: Value,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub param_type: Option<String>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct DatabricksSourceConfig {
pub workspace_url: String,
pub warehouse_id: String,
pub sql: String,
pub auth: AuthSpec<DatabricksAuth>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(default)]
pub parameters: Vec<DatabricksParam>,
#[serde(default = "default_wait_timeout")]
pub wait_timeout_secs: u64,
#[serde(default = "default_poll_interval")]
pub poll_interval_secs: u64,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default)]
pub replication: DatabricksReplication,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_key: Option<String>,
}
impl DatabricksSourceConfig {
pub fn validate(&self) -> Result<(), FaucetError> {
if self.workspace_url.trim().is_empty() {
return Err(FaucetError::Config(
"databricks: `workspace_url` must not be empty".into(),
));
}
if self.warehouse_id.trim().is_empty() {
return Err(FaucetError::Config(
"databricks: `warehouse_id` must not be empty".into(),
));
}
if self.sql.trim().is_empty() {
return Err(FaucetError::Config(
"databricks: `sql` must not be empty".into(),
));
}
if self.wait_timeout_secs != 0 && !(5..=50).contains(&self.wait_timeout_secs) {
return Err(FaucetError::Config(format!(
"databricks: `wait_timeout_secs` must be 0 or between 5 and 50 (got {})",
self.wait_timeout_secs
)));
}
faucet_core::validate_batch_size(self.batch_size)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn base() -> DatabricksSourceConfig {
DatabricksSourceConfig {
workspace_url: "https://x.cloud.databricks.com".into(),
warehouse_id: "wh1".into(),
sql: "SELECT 1".into(),
auth: AuthSpec::Inline(DatabricksAuth::Pat { token: "t".into() }),
catalog: None,
schema: None,
parameters: Vec::new(),
wait_timeout_secs: default_wait_timeout(),
poll_interval_secs: default_poll_interval(),
batch_size: DEFAULT_BATCH_SIZE,
replication: DatabricksReplication::Full,
state_key: None,
}
}
#[test]
fn valid_config_passes() {
base().validate().unwrap();
}
#[test]
fn auth_is_bearer_for_both_variants() {
assert_eq!(
DatabricksAuth::Pat {
token: "abc".into()
}
.authorization_value(),
"Bearer abc"
);
assert_eq!(
DatabricksAuth::Token {
token: "xyz".into()
}
.authorization_value(),
"Bearer xyz"
);
}
#[test]
fn rejects_empty_required_fields() {
let mut c = base();
c.workspace_url = " ".into();
assert!(c.validate().is_err());
let mut c = base();
c.warehouse_id = "".into();
assert!(c.validate().is_err());
let mut c = base();
c.sql = "".into();
assert!(c.validate().is_err());
}
#[test]
fn rejects_bad_wait_timeout() {
let mut c = base();
c.wait_timeout_secs = 3; assert!(c.validate().is_err());
c.wait_timeout_secs = 51;
assert!(c.validate().is_err());
c.wait_timeout_secs = 0; assert!(c.validate().is_ok());
}
#[test]
fn rejects_oversized_batch() {
let mut c = base();
c.batch_size = faucet_core::MAX_BATCH_SIZE + 1;
assert!(c.validate().is_err());
}
#[test]
fn deserializes_full_shape() {
let v = json!({
"workspace_url": "https://x.cloud.databricks.com",
"warehouse_id": "wh1",
"sql": "SELECT * FROM t WHERE id > :min",
"auth": { "type": "pat", "config": { "token": "tok" } },
"catalog": "main",
"schema": "sales",
"parameters": [{ "name": "min", "value": 10, "type": "INT" }],
"wait_timeout_secs": 30,
"batch_size": 500
});
let c: DatabricksSourceConfig = serde_json::from_value(v).unwrap();
assert_eq!(c.warehouse_id, "wh1");
assert_eq!(c.catalog.as_deref(), Some("main"));
assert_eq!(c.parameters.len(), 1);
assert_eq!(c.parameters[0].name, "min");
assert_eq!(c.parameters[0].param_type.as_deref(), Some("INT"));
c.validate().unwrap();
}
}