use rudb_common::{LogicalType, Value};
use rudb_kernels::cast_value;
pub const LADDER: [LogicalType; 5] = [
LogicalType::Boolean,
LogicalType::BigInt,
LogicalType::Double,
LogicalType::Date,
LogicalType::Timestamp,
];
pub const SAMPLE: usize = 20480;
#[must_use]
pub fn column(values: &[Option<&str>]) -> LogicalType {
if values.iter().all(Option::is_none) {
return LogicalType::Varchar;
}
for candidate in LADDER {
if values.iter().flatten().all(|text| fits(text, &candidate)) {
return candidate;
}
}
LogicalType::Varchar
}
#[must_use]
pub fn fits(text: &str, candidate: &LogicalType) -> bool {
match candidate {
LogicalType::Boolean => is_boolean(text),
LogicalType::BigInt if !numeric(text) => false,
LogicalType::Double if !numeric(text) => false,
_ => {
let value = Value::Varchar(text.to_string());
matches!(cast_value(&value, candidate, true), Ok(converted) if !converted.is_null())
}
}
}
fn is_boolean(text: &str) -> bool {
["true", "false", "t", "f", "yes", "no"]
.iter()
.any(|spelling| text.trim().eq_ignore_ascii_case(spelling))
}
fn numeric(text: &str) -> bool {
let text = text.trim();
let mut bytes = text.bytes();
match bytes.next() {
Some(b'+') => false,
Some(b'0') => !matches!(bytes.next(), Some(byte) if byte.is_ascii_digit()),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn of(values: &[&str]) -> LogicalType {
let values: Vec<Option<&str>> = values.iter().map(|text| Some(*text)).collect();
column(&values)
}
#[test]
fn each_rung_of_the_ladder_is_the_type_duckdb_sniffs_for_it() {
assert_eq!(of(&["true", "false"]), LogicalType::Boolean);
assert_eq!(of(&["1", "2"]), LogicalType::BigInt);
assert_eq!(of(&["1.5", "2"]), LogicalType::Double);
assert_eq!(of(&["2020-01-02", "2021-03-04"]), LogicalType::Date);
assert_eq!(of(&["2020-01-02 03:04:05"]), LogicalType::Timestamp);
assert_eq!(of(&["1", "x"]), LogicalType::Varchar);
}
#[test]
fn a_column_of_times_is_a_varchar_here_and_a_time_in_duckdb() {
assert_eq!(of(&["03:04:05"]), LogicalType::Varchar);
let value = Value::Varchar("03:04:05".into());
assert!(
cast_value(&value, &LogicalType::Time, true).is_err(),
"the cast exists now, so TIME belongs back in the ladder"
);
}
#[test]
fn a_column_of_nothing_but_nulls_is_a_varchar() {
assert_eq!(column(&[None, None]), LogicalType::Varchar);
assert_eq!(column(&[]), LogicalType::Varchar);
}
#[test]
fn a_null_in_a_column_does_not_change_what_the_rest_of_it_is() {
assert_eq!(column(&[Some("1"), None, Some("2")]), LogicalType::BigInt);
}
#[test]
fn ones_and_zeroes_are_numbers_rather_than_flags() {
assert_eq!(of(&["0", "1"]), LogicalType::BigInt);
}
#[test]
fn the_boolean_spellings_are_the_six_the_binary_takes_and_no_more() {
for yes in ["true", "TRUE", "True", "t", "T", "yes", "Yes"] {
assert_eq!(of(&[yes]), LogicalType::Boolean, "{yes}");
}
for no in ["on", "off", "y", "n"] {
assert_eq!(of(&[no]), LogicalType::Varchar, "{no}");
}
}
#[test]
fn a_zero_padded_number_stays_the_text_it_was_written_as() {
assert_eq!(of(&["007", "008"]), LogicalType::Varchar);
assert_eq!(of(&["00"]), LogicalType::Varchar);
assert_eq!(of(&["0"]), LogicalType::BigInt);
assert_eq!(of(&["-007"]), LogicalType::BigInt);
}
#[test]
fn a_leading_plus_is_not_a_number_to_the_sniffer() {
assert_eq!(of(&["+1"]), LogicalType::Varchar);
}
#[test]
fn space_around_a_number_does_not_stop_it_being_one() {
assert_eq!(of(&[" 1", "2 "]), LogicalType::BigInt);
}
#[test]
fn a_whole_number_too_big_for_a_bigint_widens_to_a_double() {
assert_eq!(of(&["99999999999999999999"]), LogicalType::Double);
}
}