use rudb_common::{Error, Field, LogicalType, Result};
#[must_use]
pub fn widen(one: &LogicalType, other: &LogicalType) -> LogicalType {
if one == other {
return one.clone();
}
let numeric = |ty: &LogicalType| matches!(ty, LogicalType::BigInt | LogicalType::Double);
if numeric(one) && numeric(other) {
return LogicalType::Double;
}
LogicalType::Varchar
}
pub fn across(sniffed: &[(String, Vec<Field>)]) -> Result<Vec<Field>> {
let Some((main, first)) = sniffed.first() else { return Ok(Vec::new()) };
let mut fields = first.clone();
for (path, held) in &sniffed[1..] {
for field in &mut fields {
let found = held
.iter()
.find(|column| column.name == field.name)
.ok_or_else(|| mismatch(main, path, &field.name))?;
field.ty = widen(&field.ty, &found.ty);
}
}
Ok(fields)
}
#[must_use]
pub fn mismatch(main: &str, current: &str, missing: &str) -> Error {
Error::invalid_input(format!(
"Schema mismatch between globbed files.\nMain file schema: {main}\nCurrent file: \
{current}\nColumn with name: \"{missing}\" is missing\nPotential Fixes \n* Consider \
setting union_by_name=true.\n* Consider setting files_to_sniff to a higher value (e.g., \
files_to_sniff = -1)"
))
}
#[cfg(test)]
mod tests {
use super::*;
fn named(name: &str, ty: LogicalType) -> Field {
Field::new(name, ty)
}
#[test]
fn the_only_pair_that_widens_to_anything_but_text_is_the_numeric_one() {
assert_eq!(widen(&LogicalType::BigInt, &LogicalType::BigInt), LogicalType::BigInt);
assert_eq!(widen(&LogicalType::BigInt, &LogicalType::Double), LogicalType::Double);
assert_eq!(widen(&LogicalType::Double, &LogicalType::BigInt), LogicalType::Double);
assert_eq!(widen(&LogicalType::Boolean, &LogicalType::BigInt), LogicalType::Varchar);
assert_eq!(widen(&LogicalType::Date, &LogicalType::BigInt), LogicalType::Varchar);
assert_eq!(widen(&LogicalType::Date, &LogicalType::Timestamp), LogicalType::Varchar);
}
#[test]
fn the_first_file_fixes_the_names_and_every_file_contributes_a_type() {
let sniffed = vec![
(
"one.csv".to_string(),
vec![named("a", LogicalType::BigInt), named("b", LogicalType::BigInt)],
),
(
"two.csv".to_string(),
vec![named("a", LogicalType::Double), named("b", LogicalType::BigInt)],
),
];
let fields = across(&sniffed).expect("agrees");
assert_eq!(fields[0].ty, LogicalType::Double);
assert_eq!(fields[1].ty, LogicalType::BigInt);
assert_eq!(fields[0].name, "a");
}
#[test]
fn a_column_that_is_not_in_a_later_file_is_duckdbs_own_complaint() {
let sniffed = vec![
("one.csv".to_string(), vec![named("a", LogicalType::BigInt)]),
("two.csv".to_string(), vec![named("z", LogicalType::BigInt)]),
];
let error = across(&sniffed).unwrap_err();
assert!(error.message().starts_with("Schema mismatch between globbed files."), "{error}");
assert!(error.message().contains("Main file schema: one.csv"), "{error}");
assert!(error.message().contains("Column with name: \"a\" is missing"), "{error}");
assert!(error.message().contains("union_by_name=true"), "{error}");
}
#[test]
fn a_column_order_that_differs_between_files_is_matched_by_name_and_not_by_position() {
let sniffed = vec![
(
"one.csv".to_string(),
vec![named("a", LogicalType::BigInt), named("b", LogicalType::Varchar)],
),
(
"two.csv".to_string(),
vec![named("b", LogicalType::Varchar), named("a", LogicalType::Double)],
),
];
let fields = across(&sniffed).expect("agrees");
assert_eq!(fields[0].name, "a");
assert_eq!(fields[0].ty, LogicalType::Double);
}
}