use std::collections::HashMap;
use faucet_core::{AuthSpec, Source};
use faucet_source_databricks::{DatabricksAuth, DatabricksReplication, DatabricksSourceConfig};
use serde_json::{Value, json};
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn base_config(uri: &str, sql: &str) -> DatabricksSourceConfig {
DatabricksSourceConfig {
workspace_url: uri.into(),
warehouse_id: "wh1".into(),
sql: sql.into(),
auth: AuthSpec::Inline(DatabricksAuth::Pat {
token: "tok".into(),
}),
catalog: None,
schema: None,
parameters: Vec::new(),
wait_timeout_secs: 50,
poll_interval_secs: 1,
batch_size: 1000,
replication: DatabricksReplication::Full,
state_key: None,
}
}
fn manifest(cols: &[(&str, &str)]) -> Value {
let columns: Vec<Value> = cols
.iter()
.enumerate()
.map(|(i, (n, t))| json!({ "name": n, "position": i, "type_name": t }))
.collect();
json!({ "format": "JSON_ARRAY", "schema": { "column_count": columns.len(), "columns": columns } })
}
async fn source(uri: &str, sql: &str) -> DatabricksSource {
DatabricksSource::new(base_config(uri, sql))
.unwrap()
.with_endpoint_base(uri)
}
use faucet_source_databricks::DatabricksSource;
#[tokio::test]
async fn inline_success_typed_decode() {
let server = MockServer::start().await;
let body = json!({
"statement_id": "s1",
"status": { "state": "SUCCEEDED" },
"manifest": manifest(&[("id", "LONG"), ("name", "STRING"), ("ok", "BOOLEAN")]),
"result": { "chunk_index": 0, "row_offset": 0, "row_count": 2,
"data_array": [["1", "alice", "true"], ["2", "bob", "false"]] }
});
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT * FROM t").await;
let rows = src.fetch_with_context(&HashMap::new()).await.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["id"], json!(1)); assert_eq!(rows[0]["name"], json!("alice"));
assert_eq!(rows[0]["ok"], json!(true)); assert_eq!(rows[1]["ok"], json!(false));
}
#[tokio::test]
async fn async_poll_pending_then_succeeded() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "s2", "status": { "state": "PENDING" }
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/2.0/sql/statements/s2"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "s2",
"status": { "state": "SUCCEEDED" },
"manifest": manifest(&[("n", "INT")]),
"result": { "data_array": [["7"]] }
})))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT 7").await;
let rows = src.fetch_with_context(&HashMap::new()).await.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["n"], json!(7));
}
#[tokio::test]
async fn multi_chunk_pagination() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "s3",
"status": { "state": "SUCCEEDED" },
"manifest": manifest(&[("id", "INT")]),
"result": { "data_array": [["1"], ["2"]],
"next_chunk_index": 1,
"next_chunk_internal_link": "/api/2.0/sql/statements/s3/result/chunks/1" }
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/2.0/sql/statements/s3/result/chunks/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"chunk_index": 1, "data_array": [["3"], ["4"]]
})))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT id FROM t").await;
let rows = src.fetch_with_context(&HashMap::new()).await.unwrap();
let ids: Vec<i64> = rows.iter().map(|r| r["id"].as_i64().unwrap()).collect();
assert_eq!(ids, vec![1, 2, 3, 4]); }
#[tokio::test]
async fn failed_statement_is_typed_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "s4",
"status": { "state": "FAILED",
"error": { "error_code": "BAD_REQUEST", "message": "no such table" } }
})))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT * FROM nope").await;
let err = src.fetch_with_context(&HashMap::new()).await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("FAILED"), "{msg}");
assert!(msg.contains("BAD_REQUEST"), "{msg}");
assert!(msg.contains("no such table"), "{msg}");
}
#[tokio::test]
async fn http_error_surfaces_status_and_body() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(403).set_body_string("permission denied"))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT 1").await;
let err = src.fetch_with_context(&HashMap::new()).await.unwrap_err();
assert!(err.to_string().contains("403"), "{}", err);
}
#[tokio::test]
async fn incremental_bookmark_round_trip() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r"^/api/2\.0/sql/statements$"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "s5",
"status": { "state": "SUCCEEDED" },
"manifest": manifest(&[("id", "INT"), ("ts", "STRING")]),
"result": { "data_array": [
["1", "2026-01-01"], ["2", "2026-03-01"], ["3", "2026-02-01"]
] }
})))
.mount(&server)
.await;
let mut cfg = base_config(&server.uri(), "SELECT id, ts FROM t WHERE ts > ${bookmark}");
cfg.replication = DatabricksReplication::Incremental {
column: "ts".into(),
initial_value: json!("2026-01-15"),
};
let src = DatabricksSource::new(cfg)
.unwrap()
.with_endpoint_base(server.uri());
assert!(src.state_key().is_some());
use futures::StreamExt;
let ctx = HashMap::new();
let mut pages = src.stream_pages(&ctx, 1000);
let mut all_rows = Vec::new();
let mut final_bookmark: Option<Value> = None;
while let Some(p) = pages.next().await {
let page = p.unwrap();
if page.bookmark.is_some() {
final_bookmark = page.bookmark.clone();
}
all_rows.extend(page.records);
}
let kept_ts: Vec<&str> = all_rows.iter().map(|r| r["ts"].as_str().unwrap()).collect();
assert!(kept_ts.contains(&"2026-03-01") && kept_ts.contains(&"2026-02-01"));
assert!(!kept_ts.contains(&"2026-01-01"));
assert_eq!(final_bookmark, Some(json!("2026-03-01")));
}
#[tokio::test]
async fn check_probe_hits_warehouse() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/2.0/sql/statements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"statement_id": "chk", "status": { "state": "SUCCEEDED" },
"manifest": manifest(&[("one", "INT")]), "result": { "data_array": [["1"]] }
})))
.mount(&server)
.await;
let src = source(&server.uri(), "SELECT 1").await;
let report = src
.check(&faucet_core::check::CheckContext::default())
.await
.unwrap();
assert!(
report
.probes
.iter()
.all(|p| matches!(p.status, faucet_core::check::ProbeStatus::Pass)),
"{report:?}"
);
}