use serde_json::{Map, Value};
pub use faucet_common_databricks::ResultColumn as ColumnInfo;
pub fn row_to_json(row: &[Value], columns: &[ColumnInfo]) -> Value {
let mut obj = Map::with_capacity(columns.len());
for (i, col) in columns.iter().enumerate() {
obj.insert(col.name.clone(), cell_to_json(row.get(i), &col.type_name));
}
Value::Object(obj)
}
pub fn check_unique_columns(columns: &[ColumnInfo]) -> Result<(), faucet_core::FaucetError> {
let mut seen = std::collections::HashSet::new();
for col in columns {
if !seen.insert(col.name.as_str()) {
return Err(faucet_core::FaucetError::Source(format!(
"databricks: the query returns two columns named {:?}; alias them \
(`SELECT o.id AS order_id, c.id AS customer_id …`) so neither is lost",
col.name
)));
}
}
Ok(())
}
fn cell_to_json(cell: Option<&Value>, type_name: &str) -> Value {
let Some(cell) = cell else {
return Value::Null;
};
if cell.is_null() {
return Value::Null;
}
let s = match cell {
Value::String(s) => s.as_str(),
other => return other.clone(),
};
match type_name.to_ascii_uppercase().as_str() {
"BOOLEAN" => match s {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
_ => Value::String(s.to_owned()),
},
"BYTE" | "SHORT" | "INT" | "LONG" => parse_int(s),
"FLOAT" | "DOUBLE" => parse_float(s),
"DECIMAL" => Value::String(s.to_owned()),
"ARRAY" | "MAP" | "STRUCT" => faucet_core::fidelity::parse_json(s.as_bytes())
.unwrap_or_else(|_| Value::String(s.to_owned())),
_ => Value::String(s.to_owned()),
}
}
fn parse_int(s: &str) -> Value {
match s.parse::<i64>() {
Ok(n) => Value::Number(n.into()),
Err(_) => Value::String(s.to_owned()),
}
}
fn parse_float(s: &str) -> Value {
match s.parse::<f64>() {
Ok(f) => faucet_core::fidelity::float64(f),
Err(_) => Value::String(s.to_owned()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_fidelity_corpus_decodes_unchanged() {
use faucet_conformance::fidelity;
let sent = fidelity::flat_corpus();
let mut cols = Vec::new();
let mut cells = Vec::new();
for (name, v) in sent.as_object().unwrap() {
let (ty, cell) = match v {
Value::Null => ("STRING", Value::Null),
Value::Bool(b) => ("BOOLEAN", json!(b.to_string())),
Value::Number(n) if n.is_f64() => ("DOUBLE", json!(n.to_string())),
Value::Number(n) => ("LONG", json!(n.to_string())),
Value::String(s) if name.starts_with("canonical_decimal") => ("DECIMAL", json!(s)),
Value::String(s) => ("STRING", json!(s)),
Value::Array(_) => ("ARRAY", json!(v.to_string())),
Value::Object(_) => ("STRUCT", json!(v.to_string())),
};
cols.push(ColumnInfo {
name: name.clone(),
type_name: ty.into(),
});
cells.push(cell);
}
let landed = row_to_json(&cells, &cols);
fidelity::assert_round_trip(&[sent], &[landed], fidelity::Tolerance::exact());
}
#[test]
fn duplicate_column_names_are_refused() {
assert!(check_unique_columns(&cols()).is_ok());
let mut dup = cols();
dup.push(dup[0].clone());
let err = check_unique_columns(&dup).unwrap_err().to_string();
assert!(err.contains("two columns named \"id\""), "{err}");
}
fn cols() -> Vec<ColumnInfo> {
[
"id:LONG",
"price:DECIMAL",
"ok:BOOLEAN",
"score:DOUBLE",
"name:STRING",
"dt:DATE",
]
.iter()
.map(|s| {
let (n, t) = s.split_once(':').unwrap();
ColumnInfo {
name: n.into(),
type_name: t.into(),
}
})
.collect()
}
#[test]
fn decodes_row_by_type() {
let row = vec![
json!("42"),
json!("71433.16"),
json!("true"),
json!("1.5"),
json!("alice"),
json!("2026-01-01"),
];
let obj = row_to_json(&row, &cols());
assert_eq!(obj["id"], json!(42));
assert_eq!(obj["price"], json!("71433.16")); assert_eq!(obj["ok"], json!(true));
assert_eq!(obj["score"], json!(1.5));
assert_eq!(obj["name"], json!("alice"));
assert_eq!(obj["dt"], json!("2026-01-01"));
}
#[test]
fn null_and_missing_cells_are_null() {
let row = vec![Value::Null, json!("9.9")]; let obj = row_to_json(&row, &cols());
assert_eq!(obj["id"], Value::Null);
assert_eq!(obj["price"], json!("9.9"));
assert_eq!(obj["ok"], Value::Null); assert_eq!(obj["dt"], Value::Null);
}
#[test]
fn huge_long_kept_as_string() {
let c = [ColumnInfo {
name: "n".into(),
type_name: "LONG".into(),
}];
let obj = row_to_json(&[json!("123456789012345678901234")], &c);
assert_eq!(obj["n"], json!("123456789012345678901234"));
}
#[test]
fn non_finite_float_is_a_counted_null() {
let c = [ColumnInfo {
name: "f".into(),
type_name: "DOUBLE".into(),
}];
assert_eq!(row_to_json(&[json!("NaN")], &c)["f"], Value::Null);
assert_eq!(row_to_json(&[json!("Infinity")], &c)["f"], Value::Null);
let c = [ColumnInfo {
name: "a".into(),
type_name: "ARRAY".into(),
}];
assert_eq!(
row_to_json(&[json!("[12345678901234567890.5]")], &c)["a"],
json!(["12345678901234567890.5"])
);
}
#[test]
fn nested_types_parsed_or_fallback() {
let c = [ColumnInfo {
name: "a".into(),
type_name: "ARRAY".into(),
}];
assert_eq!(row_to_json(&[json!("[1,2,3]")], &c)["a"], json!([1, 2, 3]));
assert_eq!(row_to_json(&[json!("[oops")], &c)["a"], json!("[oops"));
}
#[test]
fn boolean_and_int_edge_values() {
let c = [ColumnInfo {
name: "b".into(),
type_name: "BOOLEAN".into(),
}];
assert_eq!(row_to_json(&[json!("maybe")], &c)["b"], json!("maybe"));
let ci = [ColumnInfo {
name: "i".into(),
type_name: "INT".into(),
}];
assert_eq!(
row_to_json(&[json!("not-a-num")], &ci)["i"],
json!("not-a-num")
);
}
}