use std::cmp::Ordering;
use std::str::FromStr;
use rudb_common::{
Error, ErrorCode, LogicalType, PhysicalType, Result, Value, civil_from_days, days_from_civil,
};
use rudb_vector::{Data, Form, Vector};
use crate::datetime::MICROS_PER_DAY;
use crate::fallback::{self, Kernel};
use crate::number::{approximate, digits, fit, integral, pow10, rescale};
use crate::shape::{identity, nulls_of};
pub fn cast(input: &Vector, target: &LogicalType, try_cast: bool) -> Result<Vector> {
if input.logical_type() == target {
return Ok(input.clone());
}
if input.is_empty() {
return Ok(Vector::constant(target.clone(), Value::Null, 0));
}
if input.form() == Form::Constant {
let single = cast_value(&input.value_at(0), target, try_cast)?;
return Ok(Vector::constant(target.clone(), single, input.len()));
}
if let Some(vector) = swept(input, target) {
return Ok(vector);
}
fallback::record(Kernel::Cast, input.form(), input.form());
let mut values = Vec::with_capacity(input.len());
for index in 0..input.len() {
values.push(cast_value(&input.value_at(index), target, try_cast)?);
}
Vector::from_values(target.clone(), &values)
}
fn swept(input: &Vector, target: &LogicalType) -> Option<Vector> {
let from = numeric(input.logical_type())?;
let into = numeric(target)?;
let rows = input.len();
let physical = target.physical();
let converted = match input.form() {
Form::Flat => {
let data = input.data()?;
if data.len() < rows {
return None;
}
convert_run(data, identity, rows, from, into, physical)?
}
Form::Dictionary => {
let (codes, values) = input.dictionary_parts()?;
if codes.len() < rows {
return None;
}
convert_run(values.data()?, |index| codes[index] as usize, rows, from, into, physical)?
}
_ => return None,
};
Some(Vector::flat(target.clone(), converted).ok()?.with_validity(nulls_of(input)))
}
#[derive(Clone, Copy)]
enum Numeric {
Exact { scale: u8, width: Option<u8> },
Approximate { single: bool },
}
fn numeric(ty: &LogicalType) -> Option<Numeric> {
match *ty {
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::HugeInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt => Some(Numeric::Exact { scale: 0, width: None }),
LogicalType::Decimal { width, scale } => Some(Numeric::Exact { scale, width: Some(width) }),
LogicalType::Float => Some(Numeric::Approximate { single: true }),
LogicalType::Double => Some(Numeric::Approximate { single: false }),
_ => None,
}
}
fn convert_run<M: Fn(usize) -> usize>(
data: &Data,
at: M,
rows: usize,
from: Numeric,
into: Numeric,
physical: PhysicalType,
) -> Option<Data> {
match (from, into) {
(Numeric::Exact { scale: was, .. }, Numeric::Exact { scale: now, width: None })
if was == now =>
{
straight(data, at, rows, physical)
}
(Numeric::Exact { scale: was, .. }, Numeric::Exact { scale: now, width }) => {
let mut run = exact_run(data, at, rows)?;
restage(&mut run, was, now)?;
exact_out(run, width, physical)
}
(Numeric::Exact { scale, .. }, Numeric::Approximate { single }) => {
loosened(data, at, rows, scale, single)
}
(Numeric::Approximate { .. }, Numeric::Exact { scale, width }) => {
let run = float_run(data, at, rows)?;
exact_out(tighten(&run, scale)?, width, physical)
}
(Numeric::Approximate { .. }, Numeric::Approximate { single }) => {
let run = float_run(data, at, rows)?;
approximate_out(run, single)
}
}
}
fn straight<M: Fn(usize) -> usize>(
data: &Data,
at: M,
rows: usize,
physical: PhysicalType,
) -> Option<Data> {
macro_rules! fitted {
($values:expr, $variant:path, $ty:ty) => {{
let values = $values;
let mut out = Vec::with_capacity(rows);
for index in 0..rows {
out.push(<$ty>::try_from(values[at(index)]).ok()?);
}
$variant(out.into())
}};
}
macro_rules! by_target {
($values:expr, $(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
match physical {
$(PhysicalType::$variant => fitted!($values, Data::$variant, $native),)+
_ => return None,
}
};
}
macro_rules! by_source {
($(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
match data {
$(Data::$variant(values) => {
rudb_vector::for_each_layout!(exact, by_target, values)
})+
_ => return None,
}
};
}
Some(rudb_vector::for_each_layout!(exact, by_source))
}
fn exact_run<M: Fn(usize) -> usize>(data: &Data, at: M, rows: usize) -> Option<Vec<i128>> {
macro_rules! widened {
($(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
match data {
$(Data::$variant(values) => {
(0..rows).map(|index| i128::from(values[at(index)])).collect()
})+
_ => return None,
}
};
}
Some(rudb_vector::for_each_layout!(exact, widened))
}
fn float_run<M: Fn(usize) -> usize>(data: &Data, at: M, rows: usize) -> Option<Vec<f64>> {
Some(match data {
Data::Float32(values) => (0..rows).map(|index| f64::from(values[at(index)])).collect(),
Data::Float64(values) => (0..rows).map(|index| values[at(index)]).collect(),
_ => return None,
})
}
fn restage(run: &mut [i128], was: u8, now: u8) -> Option<()> {
match now.cmp(&was) {
Ordering::Equal => {}
Ordering::Greater => {
let factor = pow10(now - was);
for slot in run.iter_mut() {
*slot = slot.checked_mul(factor)?;
}
}
Ordering::Less => {
let factor = pow10(was - now);
let half = factor / 2;
for slot in run.iter_mut() {
let shifted = if *slot >= 0 { *slot + half } else { *slot - half };
*slot = shifted / factor;
}
}
}
Some(())
}
#[expect(
clippy::cast_precision_loss,
reason = "a wide integer past 2^53 losing digits is what a double is, and this is the float path"
)]
fn loosened<M: Fn(usize) -> usize>(
data: &Data,
at: M,
rows: usize,
scale: u8,
single: bool,
) -> Option<Data> {
macro_rules! doubles {
($(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
match data {
$(Data::$variant(values) => doubles!(@run values),)+
_ => return None,
}
};
(@run $values:expr) => {{
let values = $values;
let mut out = Vec::with_capacity(rows);
if scale == 0 {
for index in 0..rows {
out.push(values[at(index)] as f64);
}
} else {
let factor = pow10(scale) as f64;
for index in 0..rows {
out.push(values[at(index)] as f64 / factor);
}
}
out
}};
}
let run: Vec<f64> = rudb_vector::for_each_layout!(exact, doubles);
approximate_out(run, single)
}
#[expect(
clippy::cast_possible_truncation,
reason = "the bound checked on the line above is what decides whether the value fits"
)]
fn tighten(run: &[f64], scale: u8) -> Option<Vec<i128>> {
let factor = pow10(scale) as f64;
let mut out = Vec::with_capacity(run.len());
for &number in run {
let scaled = (number * factor).round();
if !(-1.7014118346046923e38..=1.7014118346046923e38).contains(&scaled) {
return None;
}
out.push(scaled as i128);
}
Some(out)
}
fn exact_out(run: Vec<i128>, width: Option<u8>, physical: PhysicalType) -> Option<Data> {
if let Some(width) = width {
let limit = pow10(width).unsigned_abs();
if run.iter().any(|&whole| whole.unsigned_abs() >= limit) {
return None;
}
}
macro_rules! narrowed {
($(($variant:ident, $native:ty, $zero:expr)),+ $(,)?) => {
match physical {
$(PhysicalType::$variant => {
let mut out = Vec::with_capacity(run.len());
for &whole in &run {
out.push(<$native>::try_from(whole).ok()?);
}
Data::$variant(out.into())
})+
PhysicalType::Int128 => Data::Int128(run.into()),
_ => return None,
}
};
}
Some(rudb_vector::for_each_layout!(narrow, narrowed))
}
#[expect(
clippy::cast_possible_truncation,
reason = "narrowing to a float is what a cast to FLOAT is, and the line below catches the loss"
)]
fn approximate_out(run: Vec<f64>, single: bool) -> Option<Data> {
if !single {
return Some(Data::Float64(run.into()));
}
let mut out = Vec::with_capacity(run.len());
for number in run {
let narrowed = number as f32;
if narrowed.is_infinite() && number.is_finite() {
return None;
}
out.push(narrowed);
}
Some(Data::Float32(out.into()))
}
pub fn cast_value(value: &Value, target: &LogicalType, try_cast: bool) -> Result<Value> {
if value.is_null() || matches!(target, LogicalType::Null) {
return Ok(Value::Null);
}
if &value.logical_type() == target {
return Ok(value.clone());
}
match convert(value, target) {
Ok(converted) => Ok(converted),
Err(error) if try_cast && recoverable(&error) => Ok(Value::Null),
Err(error) => Err(error),
}
}
fn recoverable(error: &Error) -> bool {
matches!(error.code(), ErrorCode::Conversion | ErrorCode::OutOfRange)
}
fn convert(value: &Value, target: &LogicalType) -> Result<Value> {
match target {
LogicalType::Boolean => to_boolean(value),
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::HugeInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::UHugeInt => to_integer(value, target),
LogicalType::Float => to_float(value),
LogicalType::Double => to_double(value),
LogicalType::Decimal { width, scale } => to_decimal(value, *width, *scale),
LogicalType::Varchar => Ok(Value::Varchar(value.to_string())),
LogicalType::Blob => to_blob(value),
LogicalType::Date => to_date(value),
LogicalType::Timestamp => to_timestamp(value),
other => {
Err(Error::not_implemented(format!("a cast from {} to {other}", value.logical_type())))
}
}
}
fn out_of_range(value: &Value, target: &LogicalType) -> Error {
Error::conversion(format!(
"Type {} with value {value} can't be cast because the value is out of range for the destination type {}",
value.logical_type().physical_name(),
target.physical_name()
))
}
fn not_convertible(text: &str, target: &LogicalType) -> Error {
if matches!(target, LogicalType::Decimal { .. }) {
return Error::conversion(format!("Could not convert string \"{text}\" to {target}"));
}
Error::conversion(format!("Could not convert string '{text}' to {}", target.physical_name()))
}
fn no_cast(value: &Value, target: &LogicalType) -> Error {
Error::conversion(format!("Unimplemented type for cast ({} -> {target})", value.logical_type()))
}
fn no_decimal(value: &Value, target: &LogicalType) -> Error {
let written = match value {
Value::Decimal { .. } => {
return Error::conversion(format!(
"Casting value \"{value}\" to type {target} failed: value is out of range!"
));
}
Value::Float(real) => format!("{real:.6}"),
Value::Double(real) => format!("{real:.6}"),
other => other.to_string(),
};
Error::conversion(format!("Could not cast value {written} to {target}"))
}
fn no_integer(whole: i128, target: &LogicalType) -> Error {
Error::conversion(format!(
"Failed to cast decimal value {whole} to type {}",
target.physical_name()
))
}
fn to_boolean(value: &Value) -> Result<Value> {
if let Value::Varchar(text) = value {
return match text.trim().to_ascii_lowercase().as_str() {
"true" | "t" | "yes" | "y" | "1" => Ok(Value::Boolean(true)),
"false" | "f" | "no" | "n" | "0" => Ok(Value::Boolean(false)),
_ => Err(not_convertible(text, &LogicalType::Boolean)),
};
}
match integral(value) {
Some(whole) => Ok(Value::Boolean(whole != 0)),
None => match approximate(value) {
Some(number) => Ok(Value::Boolean(number != 0.0)),
None => Err(no_cast(value, &LogicalType::Boolean)),
},
}
}
fn to_integer(value: &Value, target: &LogicalType) -> Result<Value> {
if let Value::Varchar(text) = value {
let whole = parse_integer(text).ok_or_else(|| not_convertible(text, target))?;
return fit(whole, target).ok_or_else(|| not_convertible(text, target));
}
if let Value::Decimal { unscaled, scale, .. } = *value {
let whole = rounded_decimal(unscaled, scale);
return fit(whole, target).ok_or_else(|| no_integer(whole, target));
}
let whole = match integral(value) {
Some(whole) => whole,
None => rounded(value, target)?,
};
fit(whole, target).ok_or_else(|| out_of_range(value, target))
}
fn rounded_decimal(unscaled: i128, scale: u8) -> i128 {
let factor = pow10(scale);
let half = factor / 2;
let shifted = if unscaled >= 0 { unscaled + half } else { unscaled - half };
shifted / factor
}
fn rounded(value: &Value, target: &LogicalType) -> Result<i128> {
let number = approximate(value).ok_or_else(|| no_cast(value, target))?;
if !number.is_finite() {
return Err(out_of_range(value, target));
}
let number = number.round();
#[expect(
clippy::cast_possible_truncation,
reason = "the range check below is what decides whether the value fits"
)]
if (-1.7014118346046923e38..=1.7014118346046923e38).contains(&number) {
Ok(number as i128)
} else {
Err(out_of_range(value, target))
}
}
fn parse_integer(text: &str) -> Option<i128> {
text.trim().parse::<i128>().ok()
}
fn to_float(value: &Value) -> Result<Value> {
if let Value::Varchar(text) = value {
let written =
text.trim().parse::<f64>().map_err(|_| not_convertible(text, &LogicalType::Float))?;
return Ok(Value::Float(narrowed(written)));
}
let number = approximate(value).ok_or_else(|| no_cast(value, &LogicalType::Float))?;
let single = narrowed(number);
if single.is_infinite() && number.is_finite() {
return Err(out_of_range(value, &LogicalType::Float));
}
Ok(Value::Float(single))
}
#[expect(
clippy::cast_possible_truncation,
reason = "narrowing to a float is what a cast to FLOAT is"
)]
fn narrowed(number: f64) -> f32 {
number as f32
}
fn to_double(value: &Value) -> Result<Value> {
let number = match value {
Value::Varchar(text) => {
text.trim().parse::<f64>().map_err(|_| not_convertible(text, &LogicalType::Double))?
}
_ => approximate(value).ok_or_else(|| no_cast(value, &LogicalType::Double))?,
};
Ok(Value::Double(number))
}
fn to_decimal(value: &Value, width: u8, scale: u8) -> Result<Value> {
let target = LogicalType::Decimal { width, scale };
if let Value::Varchar(text) = value {
let unscaled = parse_decimal(text, scale).ok_or_else(|| not_convertible(text, &target))?;
if digits(unscaled) > width {
return Err(not_convertible(text, &target));
}
return Ok(Value::Decimal { unscaled, width, scale });
}
let unscaled = match value {
Value::Decimal { unscaled, scale: from, .. } => rescale(*unscaled, *from, scale),
_ => match integral(value) {
Some(whole) => whole.checked_mul(pow10(scale)),
None => {
let number = approximate(value).ok_or_else(|| no_cast(value, &target))?;
if !number.is_finite() {
return Err(no_decimal(value, &target));
}
#[expect(
clippy::cast_possible_truncation,
reason = "the width check below is what decides whether the value fits"
)]
let scaled = (number * pow10(scale) as f64).round() as i128;
Some(scaled)
}
},
};
let unscaled = unscaled.ok_or_else(|| no_decimal(value, &target))?;
if digits(unscaled) > width {
return Err(no_decimal(value, &target));
}
Ok(Value::Decimal { unscaled, width, scale })
}
fn parse_decimal(text: &str, scale: u8) -> Option<i128> {
let text = text.trim();
let (sign, body) = match text.strip_prefix('-') {
Some(rest) => (-1i128, rest),
None => (1i128, text.strip_prefix('+').unwrap_or(text)),
};
let (whole, fraction) = match body.split_once('.') {
Some((whole, fraction)) => (whole, fraction),
None => (body, ""),
};
if whole.is_empty() && fraction.is_empty() {
return None;
}
if !whole.bytes().chain(fraction.bytes()).all(|byte| byte.is_ascii_digit()) {
return None;
}
let written: i128 = format!("{whole}{fraction}").parse().ok()?;
let scaled = rescale(written, u8::try_from(fraction.len()).ok()?, scale)?;
Some(sign * scaled)
}
fn to_blob(value: &Value) -> Result<Value> {
let Value::Varchar(text) = value else {
return Err(no_cast(value, &LogicalType::Blob));
};
let escape = |what: &str| {
Error::conversion(format!(
"Invalid hex escape code encountered in string -> blob conversion of string \"{text}\": {what}"
))
};
let source = text.as_bytes();
let mut out = Vec::with_capacity(source.len());
let mut at = 0;
while at < source.len() {
let byte = source[at];
if byte == b'\\' {
let Some(code) = source.get(at + 1..at + 4) else {
return Err(escape("unterminated escape code at end of blob"));
};
let (high, low) = (hex(code[1]), hex(code[2]));
match (code[0], high, low) {
(b'x', Some(high), Some(low)) => out.push(high * 16 + low),
_ => {
return Err(escape(&String::from_utf8_lossy(&source[at..at + 4])));
}
}
at += 4;
continue;
}
if !byte.is_ascii() {
return Err(Error::conversion(format!(
"Invalid byte encountered in STRING -> BLOB conversion of string \"{text}\". All non-ascii characters must be escaped with hex codes (e.g. \\xAA)"
)));
}
out.push(byte);
at += 1;
}
Ok(Value::Blob(out))
}
fn hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn to_date(value: &Value) -> Result<Value> {
match value {
Value::Timestamp(micros) => i32::try_from(micros.div_euclid(MICROS_PER_DAY))
.map(Value::Date)
.map_err(|_| out_of_range(value, &LogicalType::Date)),
Value::Varchar(text) => match parse_date(text) {
Ok(days) => Ok(Value::Date(days)),
Err(fault) => Err(fault.said("date", text, "(YYYY-MM-DD)")),
},
_ => Err(no_cast(value, &LogicalType::Date)),
}
}
fn to_timestamp(value: &Value) -> Result<Value> {
match value {
Value::Date(days) => Ok(Value::Timestamp(i64::from(*days) * MICROS_PER_DAY)),
Value::Varchar(text) => match parse_timestamp(text) {
Ok(micros) => Ok(Value::Timestamp(micros)),
Err(fault) => Err(fault.said("timestamp", text, TIMESTAMP_FORMAT)),
},
_ => Err(no_cast(value, &LogicalType::Timestamp)),
}
}
const TIMESTAMP_FORMAT: &str = "(YYYY-MM-DD HH:MM[:SS[.US]][±HH[:MM[:SS]]| ZONE])";
type Parsed<T> = std::result::Result<T, Fault>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Fault {
Format,
Range,
}
impl Fault {
fn said(self, what: &str, text: &str, format: &str) -> Error {
match self {
Self::Format => Error::conversion(format!(
"invalid {what} field format: \"{text}\", expected format is {format}"
)),
Self::Range => {
Error::conversion(format!("{what} field value out of range: \"{text}\""))
}
}
}
}
fn parse_date(text: &str) -> Parsed<i32> {
let (date, time) = split_time(text.trim());
let days = parse_day(date)?;
if let Some(time) = time {
parse_time(time)?;
}
Ok(days)
}
fn parse_timestamp(text: &str) -> Parsed<i64> {
let (date, time) = split_time(text.trim());
let days = i64::from(parse_day(date)?);
let micros = match time {
None => 0,
Some(time) => parse_time(time)?,
};
days.checked_mul(MICROS_PER_DAY).and_then(|start| start.checked_add(micros)).ok_or(Fault::Range)
}
fn split_time(text: &str) -> (&str, Option<&str>) {
match text.split_once([' ', 'T']) {
Some((date, time)) => (date, Some(time)),
None => (text, None),
}
}
fn parse_day(text: &str) -> Parsed<i32> {
let mut parts = text.split('-');
let year: i32 = field(parts.next())?;
let month: u32 = field(parts.next())?;
let day: u32 = field(parts.next())?;
if parts.next().is_some() {
return Err(Fault::Format);
}
if !(1..=12).contains(&month) || day < 1 || day > days_in_month(year, month) {
return Err(Fault::Range);
}
let days = days_from_civil(year, month, day);
if days == i32::MAX || days == i32::MIN || civil_from_days(days) != (year, month, day) {
return Err(Fault::Range);
}
Ok(days)
}
fn field<T: FromStr>(part: Option<&str>) -> Parsed<T> {
part.ok_or(Fault::Format)?.parse().map_err(|_| Fault::Format)
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
_ => 28,
}
}
fn parse_time(text: &str) -> Parsed<i64> {
let (clock, fraction) = match text.split_once('.') {
Some((clock, fraction)) => (clock, Some(fraction)),
None => (text, None),
};
let mut parts = clock.split(':');
let hours: i64 = field(parts.next())?;
let minutes: i64 = field(parts.next())?;
let seconds: i64 = field(parts.next().or(Some("0")))?;
if parts.next().is_some() {
return Err(Fault::Format);
}
if !(0..=24).contains(&hours) {
return Err(Fault::Range);
}
if !(0..60).contains(&minutes) || !(0..60).contains(&seconds) {
return Err(Fault::Format);
}
let micros = match fraction {
None => 0,
Some(digits) => {
if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(Fault::Format);
}
let padded = format!("{digits:0<6}");
padded.get(..6).ok_or(Fault::Format)?.parse::<i64>().map_err(|_| Fault::Format)?
}
};
let since_midnight = ((hours * 60 + minutes) * 60 + seconds) * 1_000_000 + micros;
if since_midnight > MICROS_PER_DAY {
return Err(Fault::Range);
}
Ok(since_midnight)
}
#[cfg(test)]
mod tests {
use super::*;
fn cast_to(value: Value, target: &LogicalType) -> Result<Value> {
cast_value(&value, target, false)
}
#[test]
fn null_casts_to_null_and_never_fails() {
let cast = cast_to(Value::Null, &LogicalType::Integer).expect("null casts");
assert_eq!(cast, Value::Null);
}
#[test]
fn a_number_that_fits_widens_and_one_that_does_not_says_so() {
assert_eq!(
cast_to(Value::Integer(7), &LogicalType::BigInt).expect("7 fits"),
Value::BigInt(7)
);
let error = cast_to(Value::BigInt(40_000), &LogicalType::SmallInt)
.expect_err("40000 is not a smallint");
assert!(error.message().contains("out of range"), "{error}");
assert_eq!(error.code(), ErrorCode::Conversion);
}
#[test]
fn a_try_cast_that_does_not_fit_is_null_and_one_that_is_unimplemented_still_raises() {
let fitted = cast_value(&Value::BigInt(40_000), &LogicalType::SmallInt, true)
.expect("try_cast swallows the range failure");
assert_eq!(fitted, Value::Null);
let error = cast_value(&Value::Integer(1), &LogicalType::Interval, true)
.expect_err("try_cast does not invent an interval");
assert_eq!(error.code(), ErrorCode::NotImplemented);
}
#[test]
fn a_float_casts_to_an_integer_by_rounding_rather_than_by_truncating() {
assert_eq!(
cast_to(Value::Double(1.5), &LogicalType::Integer).expect("rounds"),
Value::Integer(2)
);
assert_eq!(
cast_to(Value::Double(-1.5), &LogicalType::Integer).expect("rounds away from zero"),
Value::Integer(-2)
);
}
#[test]
fn a_string_that_is_a_number_casts_and_one_that_is_not_does_not() {
assert_eq!(
cast_to(Value::Varchar(" 42 ".into()), &LogicalType::Integer).expect("42"),
Value::Integer(42)
);
let error = cast_to(Value::Varchar("nope".into()), &LogicalType::Integer)
.expect_err("nope is not a number");
assert!(error.message().contains("Could not convert"), "{error}");
}
#[test]
fn anything_prints_itself_when_it_casts_to_a_string() {
assert_eq!(
cast_to(Value::Boolean(true), &LogicalType::Varchar).expect("prints"),
Value::Varchar("true".into())
);
assert_eq!(
cast_to(Value::Date(0), &LogicalType::Varchar).expect("prints"),
Value::Varchar("1970-01-01".into())
);
}
#[test]
fn a_decimal_keeps_its_value_across_a_change_of_scale() {
let target = LogicalType::decimal(10, 2).expect("a legal decimal");
let widened =
cast_to(Value::Decimal { unscaled: 5, width: 4, scale: 1 }, &target).expect("rescales");
assert_eq!(widened, Value::Decimal { unscaled: 50, width: 10, scale: 2 });
let written =
cast_to(Value::Varchar("3.14159".into()), &target).expect("rounds to two places");
assert_eq!(written, Value::Decimal { unscaled: 314, width: 10, scale: 2 });
}
#[test]
fn a_decimal_that_needs_more_digits_than_its_width_is_caught() {
let target = LogicalType::decimal(3, 2).expect("a legal decimal");
let error = cast_to(Value::Integer(100), &target).expect_err("100.00 needs five digits");
assert_eq!(error.message(), "Could not cast value 100 to DECIMAL(3,2)");
}
#[test]
fn a_failed_cast_says_the_sentence_duckdb_says() {
let decimal = LogicalType::decimal(4, 1).expect("a legal decimal");
let said = |value: Value, target: &LogicalType| {
cast_to(value, target).expect_err("this does not cast").message().to_string()
};
assert_eq!(
said(Value::Varchar("abc".into()), &LogicalType::TinyInt),
"Could not convert string 'abc' to INT8"
);
assert_eq!(
said(Value::Varchar("300".into()), &LogicalType::TinyInt),
"Could not convert string '300' to INT8"
);
assert_eq!(
said(Value::Varchar("abc".into()), &decimal),
"Could not convert string \"abc\" to DECIMAL(4,1)"
);
assert_eq!(
said(Value::Integer(300), &LogicalType::TinyInt),
"Type INT32 with value 300 can't be cast because the value is out of range for the destination type INT8"
);
assert_eq!(
said(Value::Decimal { unscaled: 9999, width: 4, scale: 1 }, &LogicalType::TinyInt),
"Failed to cast decimal value 1000 to type INT8"
);
assert_eq!(
said(Value::Integer(200_000), &decimal),
"Could not cast value 200000 to DECIMAL(4,1)"
);
assert_eq!(
said(Value::Double(1.5e30), &decimal),
"Could not cast value 1499999999999999889089448902656.000000 to DECIMAL(4,1)"
);
assert_eq!(
said(Value::Decimal { unscaled: 2_000_005, width: 7, scale: 1 }, &decimal),
"Casting value \"200000.5\" to type DECIMAL(4,1) failed: value is out of range!"
);
assert_eq!(
said(Value::Date(0), &LogicalType::Integer),
"Unimplemented type for cast (DATE -> INTEGER)"
);
}
#[test]
fn a_pair_with_no_cast_is_null_under_try_cast_and_a_missing_target_is_not() {
let refused = cast_value(&Value::Date(0), &LogicalType::Integer, true)
.expect("try_cast swallows a pair duckdb has no cast for");
assert_eq!(refused, Value::Null);
let error = cast_value(&Value::Integer(1), &LogicalType::Interval, true)
.expect_err("try_cast does not invent an interval");
assert_eq!(error.code(), ErrorCode::NotImplemented);
}
#[test]
fn a_written_number_too_big_for_a_float_is_an_infinity() {
let written = cast_to(Value::Varchar("1e40".into()), &LogicalType::Float).expect("inf");
assert_eq!(written, Value::Float(f32::INFINITY));
let error =
cast_to(Value::Double(1e40), &LogicalType::Float).expect_err("1e40 is not a float");
assert!(error.message().contains("out of range"), "{error}");
}
#[test]
fn a_written_date_and_a_written_timestamp_read_back() {
assert_eq!(
cast_to(Value::Varchar("2013-07-15".into()), &LogicalType::Date).expect("a date"),
Value::Date(days_from_civil(2013, 7, 15))
);
let stamp =
cast_to(Value::Varchar("2013-07-15 10:30:00.5".into()), &LogicalType::Timestamp)
.expect("a timestamp");
let expected = i64::from(days_from_civil(2013, 7, 15)) * MICROS_PER_DAY
+ 10 * 3_600_000_000
+ 30 * 60_000_000
+ 500_000;
assert_eq!(stamp, Value::Timestamp(expected));
}
#[test]
fn a_date_that_is_not_a_date_is_refused_rather_than_guessed_at() {
for text in ["2013-07", "yesterday", "2013-07-15-01"] {
let error = cast_to(Value::Varchar(text.into()), &LogicalType::Date)
.expect_err("this is not a date");
assert_eq!(
error.message(),
format!("invalid date field format: \"{text}\", expected format is (YYYY-MM-DD)")
);
}
for text in ["2013-13-01", "2021-02-29", "2021-04-31"] {
let error =
cast_to(Value::Varchar(text.into()), &LogicalType::Date).expect_err("no such day");
assert_eq!(error.message(), format!("date field value out of range: \"{text}\""));
}
}
#[test]
fn a_date_takes_a_time_it_does_not_keep() {
let kept = cast_to(Value::Varchar(" 2020-02-29 10:30:00 ".into()), &LogicalType::Date)
.expect("a leap day with a time on it");
assert_eq!(kept, Value::Date(days_from_civil(2020, 2, 29)));
let error = cast_to(Value::Varchar("2020-02-29 10:70:00".into()), &LogicalType::Date)
.expect_err("seventy minutes past ten is not a time");
assert_eq!(
error.message(),
"invalid date field format: \"2020-02-29 10:70:00\", expected format is (YYYY-MM-DD)"
);
}
#[test]
fn the_end_of_the_day_is_a_time_and_a_moment_after_it_is_not() {
let midnight = cast_to(Value::Varchar("2020-01-01 24:00:00".into()), &LogicalType::Date)
.expect("the end of the first is still the first");
assert_eq!(midnight, Value::Date(days_from_civil(2020, 1, 1)));
for (text, said) in [
("2020-01-01 24:00:01", "date field value out of range: \"2020-01-01 24:00:01\""),
("2020-01-01 25:00:00", "date field value out of range: \"2020-01-01 25:00:00\""),
(
"2020-01-01 10:00:60",
"invalid date field format: \"2020-01-01 10:00:60\", expected format is (YYYY-MM-DD)",
),
] {
let error = cast_to(Value::Varchar(text.into()), &LogicalType::Date)
.expect_err("this is not a time");
assert_eq!(error.message(), said);
}
}
#[test]
fn a_timestamp_that_is_not_one_says_so_in_its_own_words() {
let rolled = cast_to(Value::Varchar("2020-01-01 24:00:00".into()), &LogicalType::Timestamp)
.expect("the end of the first is the start of the second");
assert_eq!(
rolled,
Value::Timestamp(i64::from(days_from_civil(2020, 1, 2)) * MICROS_PER_DAY)
);
let missing = cast_to(Value::Varchar("2020-01-01".into()), &LogicalType::Timestamp)
.expect("a day with no time on it is midnight");
assert_eq!(
missing,
Value::Timestamp(i64::from(days_from_civil(2020, 1, 1)) * MICROS_PER_DAY)
);
for (text, said) in [
("2020-01-01 24:00:01", "timestamp field value out of range: \"2020-01-01 24:00:01\""),
("2021-02-29 10:00:00", "timestamp field value out of range: \"2021-02-29 10:00:00\""),
(
"abc",
"invalid timestamp field format: \"abc\", expected format is (YYYY-MM-DD HH:MM[:SS[.US]][±HH[:MM[:SS]]| ZONE])",
),
] {
let error = cast_to(Value::Varchar(text.into()), &LogicalType::Timestamp)
.expect_err("this is not a timestamp");
assert_eq!(error.message(), said);
}
}
#[test]
fn a_constant_vector_costs_one_conversion() {
let input = Vector::constant(LogicalType::Integer, Value::Integer(3), 1024);
let cast = cast(&input, &LogicalType::BigInt, false).expect("widens");
assert_eq!(cast.form(), Form::Constant);
assert_eq!(cast.len(), 1024);
assert_eq!(cast.value_at(1000), Value::BigInt(3));
}
#[test]
fn a_cast_to_the_type_it_already_is_is_the_same_vector() {
let input = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Null, Value::Integer(3)],
)
.expect("three integers");
let cast = cast(&input, &LogicalType::Integer, false).expect("free");
assert_eq!(cast, input);
}
#[test]
fn a_null_in_a_vector_stays_null_across_a_cast() {
let input = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Null, Value::Integer(3)],
)
.expect("three integers");
let cast = cast(&input, &LogicalType::Varchar, false).expect("prints");
assert_eq!(cast.value_at(0), Value::Varchar("1".into()));
assert_eq!(cast.value_at(1), Value::Null);
}
fn oracle(input: &Vector, target: &LogicalType, try_cast: bool) -> Result<Vector> {
let mut values = Vec::with_capacity(input.len());
for index in 0..input.len() {
values.push(cast_value(&input.value_at(index), target, try_cast)?);
}
Vector::from_values(target.clone(), &values)
}
fn agrees(input: &Vector, target: &LogicalType) {
let what = format!("{} to {target}", input.logical_type());
match (cast(input, target, false), oracle(input, target, false)) {
(Ok(fast), Ok(slow)) => assert_eq!(fast, slow, "{what}"),
(Err(fast), Err(slow)) => assert_eq!(fast.message(), slow.message(), "{what}"),
(Ok(fast), Err(slow)) => {
panic!("{what}: the sweep answered {fast:?} and the loop said {slow}")
}
(Err(fast), Ok(slow)) => {
panic!("{what}: the sweep said {fast} and the loop answered {slow:?}")
}
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, bound: u64) -> u64 {
self.next() % bound
}
}
fn small(rng: &mut Rng) -> i64 {
if rng.below(8) == 0 {
rng.below(300_000) as i64 - 150_000
} else {
rng.below(201) as i64 - 100
}
}
fn sample(ty: &LogicalType, rng: &mut Rng) -> Value {
match *ty {
LogicalType::TinyInt => Value::TinyInt((small(rng) % 128) as i8),
LogicalType::SmallInt => Value::SmallInt((small(rng) % 32_768) as i16),
LogicalType::Integer => Value::Integer(small(rng) as i32),
LogicalType::BigInt => Value::BigInt(small(rng)),
LogicalType::HugeInt => Value::HugeInt(i128::from(small(rng))),
LogicalType::UTinyInt => Value::UTinyInt((small(rng).unsigned_abs() % 256) as u8),
LogicalType::USmallInt => Value::USmallInt((small(rng).unsigned_abs() % 65_536) as u16),
LogicalType::UInteger => Value::UInteger(small(rng).unsigned_abs() as u32),
LogicalType::UBigInt => Value::UBigInt(small(rng).unsigned_abs()),
LogicalType::Float => Value::Float(small(rng) as f32 / 4.0),
LogicalType::Double => Value::Double(small(rng) as f64 / 8.0),
LogicalType::Decimal { width, scale } => {
Value::Decimal { unscaled: i128::from(small(rng)) % pow10(width), width, scale }
}
ref other => panic!("the generator has no values for {other}"),
}
}
#[test]
fn every_numeric_pair_agrees_with_the_row_at_a_time_path() {
let mut rng = Rng(0x5eed_cabb_a9e0_0001);
let types: [LogicalType; 15] = [
LogicalType::TinyInt,
LogicalType::SmallInt,
LogicalType::Integer,
LogicalType::BigInt,
LogicalType::HugeInt,
LogicalType::UTinyInt,
LogicalType::USmallInt,
LogicalType::UInteger,
LogicalType::UBigInt,
LogicalType::Float,
LogicalType::Double,
LogicalType::decimal(4, 1).expect("a legal decimal"),
LogicalType::decimal(9, 2).expect("a legal decimal"),
LogicalType::decimal(18, 4).expect("a legal decimal"),
LogicalType::decimal(30, 6).expect("a legal decimal"),
];
let len = 37;
for from in &types {
for nulls in [0u64, 1, 3] {
let values: Vec<Value> = (0..len)
.map(|_| {
if nulls > 0 && rng.below(nulls + 1) == 0 {
Value::Null
} else {
sample(from, &mut rng)
}
})
.collect();
let flat = Vector::from_values(from.clone(), &values).expect("a flat vector");
let codes: Vec<u32> = (0..len).map(|_| rng.below(len as u64) as u32).collect();
let dictionary =
Vector::dictionary(codes, flat.clone()).expect("codes are in range");
for into in &types {
if into == from {
continue;
}
agrees(&flat, into);
agrees(&dictionary, into);
}
}
}
}
#[test]
fn a_numeric_cast_does_not_reach_the_row_at_a_time_path_and_a_string_one_does() {
fallback::reset();
let input = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Null, Value::Integer(3)],
)
.expect("three integers");
for target in [
LogicalType::BigInt,
LogicalType::Double,
LogicalType::Float,
LogicalType::decimal(18, 3).expect("a legal decimal"),
] {
cast(&input, &target, false).expect("widens");
}
assert_eq!(fallback::count(Kernel::Cast, Form::Flat, Form::Flat), 0);
cast(&input, &LogicalType::Varchar, false).expect("prints");
assert_eq!(fallback::count(Kernel::Cast, Form::Flat, Form::Flat), 1);
fallback::reset();
}
#[test]
fn one_value_that_does_not_fit_sends_the_whole_vector_back_to_the_loop() {
let input = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Integer(40_000), Value::Integer(3)],
)
.expect("three integers");
let error =
cast(&input, &LogicalType::SmallInt, false).expect_err("40000 is not a smallint");
assert!(error.message().contains("40000"), "{error}");
let tried = cast(&input, &LogicalType::SmallInt, true).expect("try_cast nulls it out");
assert_eq!(tried.value_at(0), Value::SmallInt(1));
assert_eq!(tried.value_at(1), Value::Null);
assert_eq!(tried.value_at(2), Value::SmallInt(3));
}
#[test]
fn moving_a_run_between_scales_rounds_the_way_one_value_at_a_time_rounds() {
let two = LogicalType::decimal(9, 2).expect("a legal decimal");
let input = Vector::from_values(
two.clone(),
&[
Value::Decimal { unscaled: 155, width: 9, scale: 2 },
Value::Decimal { unscaled: -155, width: 9, scale: 2 },
Value::Decimal { unscaled: 100, width: 9, scale: 2 },
],
)
.expect("three decimals");
let whole = cast(&input, &LogicalType::Integer, false).expect("rounds");
assert_eq!(whole.value_at(0), Value::Integer(2));
assert_eq!(whole.value_at(1), Value::Integer(-2));
assert_eq!(whole.value_at(2), Value::Integer(1));
let wider = cast(&input, &LogicalType::decimal(18, 5).expect("a legal decimal"), false)
.expect("rescales up");
assert_eq!(wider.value_at(0), Value::Decimal { unscaled: 155_000, width: 18, scale: 5 });
let back = cast(&whole, &two, false).expect("rescales back");
assert_eq!(back.value_at(0), Value::Decimal { unscaled: 200, width: 9, scale: 2 });
}
#[test]
fn text_casts_to_a_blob_through_the_escapes_and_not_through_its_own_bytes() {
let blob = |text: &str| cast_to(Value::Varchar(text.into()), &LogicalType::Blob);
assert_eq!(blob("\\x41\\x42").expect("two escapes"), Value::Blob(b"AB".to_vec()));
assert_eq!(blob("abc").expect("plain ascii"), Value::Blob(b"abc".to_vec()));
assert_eq!(blob("").expect("the empty string"), Value::Blob(Vec::new()));
assert_eq!(blob("\\xff\\x00").expect("either case, both ends"), Value::Blob(vec![255, 0]));
assert_eq!(
blob("a\\x0Ab").expect("an escape in the middle"),
Value::Blob(b"a\nb".to_vec())
);
}
#[test]
fn a_text_a_blob_cannot_read_says_which_part_it_could_not_read() {
let blob = |text: &str| {
cast_to(Value::Varchar(text.into()), &LogicalType::Blob).expect_err("not a blob")
};
assert!(blob("\\xZZ").message().contains("\\xZZ"), "{}", blob("\\xZZ"));
assert!(blob("\\x4").message().contains("unterminated escape code at end of blob"));
assert!(
blob("é").message().contains("All non-ascii characters must be escaped"),
"{}",
blob("é")
);
assert_eq!(blob("\\xZZ").code(), ErrorCode::Conversion);
}
#[test]
fn a_null_behind_a_dictionary_code_is_still_a_null_after_the_sweep() {
let values = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(7), Value::Null, Value::Integer(9)],
)
.expect("three integers");
let input = Vector::dictionary(vec![2, 1, 0, 1], values).expect("codes are in range");
let widened = cast(&input, &LogicalType::BigInt, false).expect("widens");
assert_eq!(widened.value_at(0), Value::BigInt(9));
assert_eq!(widened.value_at(1), Value::Null);
assert_eq!(widened.value_at(2), Value::BigInt(7));
assert_eq!(widened.value_at(3), Value::Null);
}
}