use std::borrow::Cow;
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, MICROS_PER_HOUR, MICROS_PER_MINUTE, MICROS_PER_SECOND, NEWEST_TIMESTAMP,
OLDEST_TIMESTAMP, days_in_month,
};
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 | ErrorCode::InvalidInput)
}
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::Time => to_time(value),
LogicalType::Timestamp => to_timestamp(value),
LogicalType::Interval => to_interval(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> {
if let Some(whole) = parse_radix(text) {
return Some(whole);
}
shifted(&written_number(text)?, 0)
}
fn parse_radix(text: &str) -> Option<i128> {
let (radix, digits) = match text.get(..2)? {
"0x" | "0X" => (16, &text[2..]),
"0b" | "0B" => (2, &text[2..]),
_ => return None,
};
let digits = without_separators(digits, u8::is_ascii_alphanumeric)?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
return None;
}
i128::from_str_radix(&digits, radix).ok()
}
struct Written {
negative: bool,
digits: String,
scale: i32,
exponent: i32,
}
fn written_number(text: &str) -> Option<Written> {
let text = without_separators(text.trim(), u8::is_ascii_digit)?;
let (negative, body) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text.strip_prefix('+').unwrap_or(&text)),
};
let (body, exponent) = match body.split_once(['e', 'E']) {
Some((body, written)) => (body, written.parse::<i32>().ok()?),
None => (body, 0),
};
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;
}
Some(Written {
negative,
digits: format!("{whole}{fraction}"),
scale: i32::try_from(fraction.len()).ok()?,
exponent,
})
}
fn without_separators(text: &str, digit: fn(&u8) -> bool) -> Option<Cow<'_, str>> {
if !text.contains('_') {
return Some(Cow::Borrowed(text));
}
let bytes = text.as_bytes();
for (at, byte) in bytes.iter().enumerate() {
if *byte != b'_' {
continue;
}
let before = at.checked_sub(1).and_then(|before| bytes.get(before));
let between = matches!((before, bytes.get(at + 1)), (Some(before), Some(after)) if digit(before) && digit(after));
if !between {
return None;
}
}
Some(Cow::Owned(text.replace('_', "")))
}
fn shifted(written: &Written, places: i32) -> Option<i128> {
let digits = written.digits.trim_start_matches('0');
let shift = written.exponent.checked_sub(written.scale)?.checked_add(places)?;
let whole = if digits.is_empty() {
0
} else if let Ok(zeros) = usize::try_from(shift) {
if digits.len() + zeros > 39 {
return None;
}
format!("{digits}{}", "0".repeat(zeros)).parse().ok()?
} else {
cut(digits, usize::try_from(shift.checked_neg()?).ok()?)?
};
Some(if written.negative { -whole } else { whole })
}
fn cut(digits: &str, dropped: usize) -> Option<i128> {
let Some(kept) = digits.len().checked_sub(dropped) else {
return Some(0);
};
let whole: i128 = if kept == 0 { 0 } else { digits[..kept].parse().ok()? };
let rounds_up = digits.as_bytes().get(kept).is_some_and(|digit| *digit >= b'5');
whole.checked_add(i128::from(rounds_up))
}
fn to_float(value: &Value) -> Result<Value> {
if let Value::Varchar(text) = value {
let written =
parse_approximate(text).ok_or_else(|| 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) => {
parse_approximate(text).ok_or_else(|| not_convertible(text, &LogicalType::Double))?
}
_ => approximate(value).ok_or_else(|| no_cast(value, &LogicalType::Double))?,
};
Ok(Value::Double(number))
}
fn parse_approximate(text: &str) -> Option<f64> {
without_separators(text.trim(), u8::is_ascii_digit)?.parse().ok()
}
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> {
shifted(&written_number(text)?, i32::from(scale))
}
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.for_date().said("date", text, "(YYYY-MM-DD)")),
},
_ => Err(no_cast(value, &LogicalType::Date)),
}
}
fn to_time(value: &Value) -> Result<Value> {
match value {
Value::Timestamp(micros) => Ok(Value::Time(micros.rem_euclid(MICROS_PER_DAY))),
Value::Varchar(text) => parse_clock(text).map(Value::Time).ok_or_else(|| bad_time(text)),
_ => Err(no_cast(value, &LogicalType::Time)),
}
}
fn bad_time(text: &str) -> Error {
Error::conversion(format!(
"time field value out of range: \"{text}\", expected format is ([YYYY-MM-DD ]HH:MM:SS[.MS])"
))
}
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,
Zone,
}
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}\""))
}
Self::Zone => Error::conversion(format!(
"{what} field value \"{text}\" has a timestamp that is not UTC."
)),
}
}
fn for_date(self) -> Self {
match self {
Self::Zone => Self::Format,
other => other,
}
}
}
fn parse_date(text: &str) -> Parsed<i32> {
let (date, era, time) = split_parts(text.trim())?;
let days = parse_day(date, era)?;
if let Some(time) = time {
parse_time(time)?;
}
Ok(days)
}
fn parse_timestamp(text: &str) -> Parsed<i64> {
let (date, era, time) = split_parts(text.trim())?;
let days = i64::from(parse_day(date, era)?);
let micros = match time {
None => 0,
Some(time) => parse_time(time)?,
};
let stamp = days
.checked_mul(MICROS_PER_DAY)
.and_then(|start| start.checked_add(micros))
.ok_or(Fault::Range)?;
if !(OLDEST_TIMESTAMP..=NEWEST_TIMESTAMP).contains(&stamp) {
return Err(Fault::Range);
}
Ok(stamp)
}
fn split_time(text: &str) -> (&str, Option<&str>) {
match text.split_once([' ', 'T']) {
Some((date, time)) => (date, Some(time)),
None => (text, None),
}
}
fn split_parts(text: &str) -> Parsed<(&str, bool, Option<&str>)> {
let (date, rest) = split_time(text);
let Some(rest) = rest else { return Ok((date, false, None)) };
let marked = rest.get(..BC.len()).is_some_and(|head| head.eq_ignore_ascii_case(BC));
if !marked {
return Ok((date, false, Some(rest.trim_start())));
}
let after = &rest[BC.len()..];
match after.chars().next() {
None => Ok((date, true, None)),
Some(separator @ (' ' | 'T')) => {
Ok((date, true, Some(after[separator.len_utf8()..].trim_start())))
}
Some(_) => Err(Fault::Format),
}
}
const BC: &str = "(BC)";
fn parse_day(text: &str, era: bool) -> Parsed<i32> {
let (signed, rest) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text),
};
let mut parts = rest.split('-');
let written: 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);
}
let year = match (era, signed) {
(true, false) if written >= 1 => 1 - written,
(true, _) => return Err(Fault::Format),
(false, true) => -written,
(false, false) => written,
};
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 !(i32::MIN + 2..=i32::MAX - 1).contains(&days) || civil_from_days(days) != (year, month, day)
{
return Err(Fault::Range);
}
Ok(days)
}
fn field<T: FromStr>(part: Option<&str>) -> Parsed<T> {
let part = part.ok_or(Fault::Format)?;
if !part.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(Fault::Format);
}
part.parse().map_err(|_| Fault::Format)
}
fn parse_time(text: &str) -> Parsed<i64> {
let (clock, zone) = split_zone(text);
let micros = parse_clock_fields(clock)?;
parse_zone(zone)?;
Ok(micros)
}
fn split_zone(text: &str) -> (&str, &str) {
let end =
text.find(|c: char| !c.is_ascii_digit() && c != ':' && c != '.').unwrap_or(text.len());
text.split_at(end)
}
fn parse_zone(text: &str) -> Parsed<()> {
let zone = text.trim_end();
let Some(first) = zone.chars().next() else {
return Ok(());
};
if let Some(name) = zone.strip_prefix(' ') {
return if name.is_empty() || name.contains(' ') { Err(Fault::Format) } else { Ok(()) };
}
if zone == "Z" {
return Ok(());
}
if first != '+' && first != '-' {
return Err(Fault::Format);
}
match offset_width(zone) {
None => Err(Fault::Zone),
Some(width) if width < zone.len() => Err(Fault::Format),
Some(_) => Ok(()),
}
}
fn offset_width(text: &str) -> Option<usize> {
let bytes = text.as_bytes();
let mut at = 1 + two_digits(bytes.get(1..)?)?;
for _ in 0..2 {
if bytes.get(at) != Some(&b':') {
break;
}
at += 1 + two_digits(bytes.get(at + 1..)?)?;
}
Some(at)
}
fn two_digits(bytes: &[u8]) -> Option<usize> {
matches!(bytes, [first, second, ..] if first.is_ascii_digit() && second.is_ascii_digit())
.then_some(2)
}
fn parse_clock_fields(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)
}
fn parse_clock(text: &str) -> Option<i64> {
let text = text.trim_start();
let clock = match split_parts(text).ok()? {
(day, era, rest) if day.contains('-') && !day.contains(':') => {
parse_day(day, era).ok()?;
match rest {
None => return Some(0),
Some(rest) => return parse_time(rest).ok().map(|micros| micros % MICROS_PER_DAY),
}
}
_ => text,
};
clock_micros(clock)
}
fn clock_micros(text: &str) -> Option<i64> {
let mut rest = text;
let hours = number(&mut rest)?;
if !eat(&mut rest, b':') {
return None;
}
let minutes = number(&mut rest)?;
let mut seconds = 0;
let mut micros = 0;
if eat(&mut rest, b':') && !rest.is_empty() {
seconds = number(&mut rest)?;
if eat(&mut rest, b'.') {
micros = fraction(rest);
}
}
if !(0..=24).contains(&hours) || !(0..60).contains(&minutes) || !(0..60).contains(&seconds) {
return None;
}
let since_midnight = ((hours * 60 + minutes) * 60 + seconds) * 1_000_000 + micros;
(since_midnight <= MICROS_PER_DAY).then_some(since_midnight)
}
fn number(rest: &mut &str) -> Option<i64> {
let digits = rest.len() - rest.trim_start_matches(|c: char| c.is_ascii_digit()).len();
let (number, tail) = rest.split_at(digits);
*rest = tail;
number.parse().ok()
}
fn eat(rest: &mut &str, byte: u8) -> bool {
let Some(tail) = rest.strip_prefix(byte as char) else {
return false;
};
*rest = tail;
true
}
fn fraction(text: &str) -> i64 {
let digits: String = text.chars().take_while(char::is_ascii_digit).take(6).collect();
if digits.is_empty() {
return 0;
}
format!("{digits:0<6}").parse().unwrap_or(0)
}
fn to_interval(value: &Value) -> Result<Value> {
match value {
Value::Varchar(text) => parse_interval(text),
_ => Err(no_cast(value, &LogicalType::Interval)),
}
}
const NANOS_PER_SECOND: i64 = 1_000_000_000;
const DAYS_PER_MONTH: i64 = 30;
#[derive(Clone, Copy)]
enum Unit {
Years(i64),
Quarter,
Month,
Days(i64),
Micros(i64),
Microsecond,
}
fn unit_of(word: &str) -> Option<Unit> {
let unit = match word.to_ascii_lowercase().as_str() {
"millennium" | "millennia" | "millenniums" | "mil" | "mils" => Unit::Years(12_000),
"century" | "centuries" | "cent" | "c" => Unit::Years(1_200),
"decade" | "decades" | "dec" | "decs" => Unit::Years(120),
"year" | "years" | "yr" | "yrs" | "y" => Unit::Years(12),
"quarter" | "quarters" => Unit::Quarter,
"month" | "months" | "mon" | "mons" => Unit::Month,
"week" | "weeks" | "weekofyear" | "w" => Unit::Days(7),
"day" | "days" | "dayofmonth" | "d" => Unit::Days(1),
"hour" | "hours" | "hr" | "hrs" | "h" => Unit::Micros(MICROS_PER_HOUR),
"minute" | "minutes" | "min" | "mins" | "m" => Unit::Micros(MICROS_PER_MINUTE),
"second" | "seconds" | "sec" | "secs" | "s" => Unit::Micros(MICROS_PER_SECOND),
"millisecond" | "milliseconds" | "msec" | "msecs" | "ms" => Unit::Micros(1_000),
"microsecond" | "microseconds" | "usec" | "usecs" | "us" => Unit::Microsecond,
_ => return None,
};
Some(unit)
}
const UNCOUNTABLE: &[&str] = &[
"dayofweek",
"dayofyear",
"dow",
"doy",
"epoch",
"era",
"isodow",
"isoyear",
"jd",
"julian",
"timezone",
"weekday",
"yearweek",
];
#[derive(Default)]
struct Counts {
months: i32,
days: i32,
micros: i64,
}
impl Counts {
fn add(&mut self, (months, days, micros): (i64, i64, i64), negative: bool) -> Result<()> {
let signed = |count: i64| if negative { -count } else { count };
self.months = self.months.checked_add(narrow(signed(months))?).ok_or_else(too_wide)?;
self.days = self.days.checked_add(narrow(signed(days))?).ok_or_else(too_wide)?;
self.micros = self.micros.checked_add(signed(micros)).ok_or_else(too_wide)?;
Ok(())
}
fn value(self, ago: bool) -> Result<Value> {
let Self { months, days, micros } = self;
if !ago {
return Ok(Value::Interval { months, days, micros });
}
Ok(Value::Interval {
months: months.checked_neg().ok_or_else(too_wide)?,
days: days.checked_neg().ok_or_else(too_wide)?,
micros: micros.checked_neg().ok_or_else(too_wide)?,
})
}
}
pub(crate) fn narrow(count: i64) -> Result<i32> {
i32::try_from(count).map_err(|_| {
Error::invalid_input(format!(
"Type INT64 with value {count} can't be cast because the value is out of range for the destination type INT32"
))
})
}
fn too_wide() -> Error {
Error::out_of_range("interval value is out of range")
}
fn parse_interval(text: &str) -> Result<Value> {
let refused = || not_convertible(text, &LogicalType::Interval);
let mut rest = text;
let mut counts = Counts::default();
let mut items = 0usize;
loop {
trim(&mut rest);
if rest.is_empty() {
if items == 0 {
return Err(refused());
}
return counts.value(false);
}
if items > 0 {
let word = run(rest, char::is_ascii_alphabetic);
if word.eq_ignore_ascii_case("ago") {
let mut after = &rest[word.len()..];
trim(&mut after);
if after.is_empty() {
return counts.value(true);
}
}
}
let negative = eat(&mut rest, b'-');
let written = run(rest, char::is_ascii_digit);
if written.is_empty() {
return Err(refused());
}
rest = &rest[written.len()..];
let count: i64 = written.parse().map_err(|_| {
Error::invalid_input(format!("Could not convert string '{written}' to INT64"))
})?;
let mut fraction = 0;
let pointed = eat(&mut rest, b'.');
if pointed {
fraction = nanos(&mut rest);
}
if !pointed && eat(&mut rest, b':') {
let micros = clock(&mut rest, count).ok_or_else(refused)?;
counts.add((0, 0, micros), negative)?;
return counts.value(false);
}
trim(&mut rest);
let word = run(rest, char::is_ascii_alphabetic);
rest = &rest[word.len()..];
let unit = match (word, items, rest.is_empty()) {
("", 0, true) => Unit::Micros(MICROS_PER_SECOND),
("", _, true) => return Err(unknown_unit("")),
("", _, false) => return Err(refused()),
(word, _, _) => unit_of(word).ok_or_else(|| unknown_unit(word))?,
};
counts.add(spread(unit, count, fraction)?, negative)?;
items += 1;
}
}
fn unknown_unit(word: &str) -> Error {
if UNCOUNTABLE.contains(&word.to_ascii_lowercase().as_str()) {
return Error::conversion(format!(
"extract specifier \"{word}\" not supported for interval"
));
}
Error::conversion(format!("extract specifier \"{word}\" not recognized"))
}
fn spread(unit: Unit, count: i64, fraction: i64) -> Result<(i64, i64, i64)> {
let whole = |per: i64| count.checked_mul(per).ok_or_else(too_wide);
let counted = match unit {
Unit::Years(per) => {
(whole(per)?.checked_add(fraction * per / NANOS_PER_SECOND).ok_or_else(too_wide)?, 0, 0)
}
Unit::Quarter => {
let carried = fraction * 3;
let months = whole(3)?.checked_add(carried / NANOS_PER_SECOND).ok_or_else(too_wide)?;
let left = i128::from(carried % NANOS_PER_SECOND * DAYS_PER_MONTH);
(months, divided(left, i128::from(NANOS_PER_SECOND)), 0)
}
Unit::Month => {
let (days, micros) = poured(fraction * DAYS_PER_MONTH);
(count, days, micros)
}
Unit::Days(per) => {
let (days, micros) = poured(fraction * per);
(0, whole(per)?.checked_add(days).ok_or_else(too_wide)?, micros)
}
Unit::Micros(per) => {
let fraction = divided(i128::from(fraction * per), i128::from(NANOS_PER_SECOND));
(0, 0, whole(per)?.checked_add(fraction).ok_or_else(too_wide)?)
}
Unit::Microsecond => (0, 0, count),
};
Ok(counted)
}
fn poured(nanos: i64) -> (i64, i64) {
let left = i128::from(nanos % NANOS_PER_SECOND) * i128::from(MICROS_PER_DAY);
(nanos / NANOS_PER_SECOND, divided(left, i128::from(NANOS_PER_SECOND)))
}
fn divided(numerator: i128, by: i128) -> i64 {
i64::try_from((numerator + by / 2) / by).unwrap_or(i64::MAX)
}
fn clock(rest: &mut &str, hours: i64) -> Option<i64> {
let minutes = counted(rest)?;
let mut seconds = 0;
let mut micros = 0;
if eat(rest, b':') {
seconds = counted(rest)?;
if eat(rest, b'.') {
micros = fraction(rest);
}
}
if !(0..60).contains(&minutes) || !(0..60).contains(&seconds) {
return None;
}
hours
.checked_mul(3_600)?
.checked_add(minutes * 60 + seconds)?
.checked_mul(MICROS_PER_SECOND)?
.checked_add(micros)
}
fn counted(rest: &mut &str) -> Option<i64> {
if rest.is_empty() {
return Some(0);
}
number(rest)
}
fn nanos(rest: &mut &str) -> i64 {
let written = run(rest, char::is_ascii_digit);
*rest = &rest[written.len()..];
let read: String = written.chars().take(9).collect();
if read.is_empty() {
return 0;
}
format!("{read:0<9}").parse().unwrap_or(0)
}
fn run(text: &str, keep: fn(&char) -> bool) -> &str {
let end = text.find(|character: char| !keep(&character)).unwrap_or(text.len());
&text[..end]
}
fn trim(rest: &mut &str) {
*rest = rest.trim_start_matches(|character: char| character.is_ascii_whitespace());
}
#[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::Bit, true)
.expect_err("try_cast does not invent a bit string");
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 a_string_spells_a_whole_number_with_a_point_an_exponent_a_separator_or_a_radix() {
for (text, expected) in [
("1.5", 2),
("2.5", 3),
("-2.5", -3),
("1.4", 1),
("1.", 1),
(".5", 1),
("-.5", -1),
("9223372036854775807.4", 9_223_372_036_854_775_807),
("1e3", 1000),
("1E3", 1000),
("1e+3", 1000),
("1.5e2", 150),
("1e-3", 0),
("5e-1", 1),
("-5e-1", -1),
("0e100", 0),
("1e18", 1_000_000_000_000_000_000),
("1_000", 1000),
("1_0_0", 100),
("1_000.5", 1001),
("1e1_0", 10_000_000_000),
("1_0e2", 1000),
("0x10", 16),
("0X10", 16),
("0xa_b", 171),
("0b101", 5),
("0B1_01", 5),
(" 1 ", 1),
(" 1e3", 1000),
("1e3 ", 1000),
] {
let whole = cast_to(Value::Varchar(text.into()), &LogicalType::BigInt);
assert_eq!(whole.as_ref().ok(), Some(&Value::BigInt(expected)), "{text}: {whole:?}");
}
let nines = format!("1.{}", "9".repeat(40));
assert_eq!(
cast_to(Value::Varchar(nines), &LogicalType::BigInt).expect("forty nines round up"),
Value::BigInt(2)
);
assert_eq!(
cast_to(Value::Varchar("1e30".into()), &LogicalType::HugeInt).expect("a hugeint"),
Value::HugeInt(1_000_000_000_000_000_000_000_000_000_000)
);
}
#[test]
fn a_string_that_spells_a_whole_number_badly_is_still_refused() {
for text in [
"1e",
"1e3.5",
"1.5e",
".",
"-",
"_100",
"1_",
"1__0",
"0x_10",
"0x10_",
"-0x10",
"+0x10",
"0o10",
"0x",
" 0x10 ",
"0x8000000000000000",
"1e39",
] {
let error = cast_to(Value::Varchar(text.into()), &LogicalType::BigInt)
.expect_err("this is not a whole number");
assert_eq!(error.message(), format!("Could not convert string '{text}' to INT64"));
}
for (text, target) in [
("1e18", LogicalType::Integer),
("127.5", LogicalType::TinyInt),
("1e39", LogicalType::HugeInt),
] {
cast_to(Value::Varchar(text.into()), &target).expect_err("this does not fit");
}
}
#[test]
fn a_written_double_and_a_written_decimal_take_every_spelling_but_the_radix() {
let target = LogicalType::decimal(10, 2).expect("a legal decimal");
for (text, expected) in [
("1e3", 100_000),
("1_000", 100_000),
("1.5e2", 15_000),
("1e-3", 0),
("-1_0.005", -1001),
] {
let written = cast_to(Value::Varchar(text.into()), &target);
let expected = Value::Decimal { unscaled: expected, width: 10, scale: 2 };
assert_eq!(written.as_ref().ok(), Some(&expected), "{text}: {written:?}");
}
assert_eq!(
cast_to(Value::Varchar("1_000".into()), &LogicalType::Double).expect("a double"),
Value::Double(1000.0)
);
for target in [LogicalType::Double, target] {
cast_to(Value::Varchar("0x10".into()), &target).expect_err("no radix out here");
}
}
#[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::Bit, true)
.expect_err("try_cast does not invent a bit string");
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_year_at_or_before_zero_is_a_date_before_christ() {
for (text, printed) in [
("0000-01-01", "0001-01-01 (BC)"),
("-0000-01-01", "0001-01-01 (BC)"),
("-0001-01-01", "0002-01-01 (BC)"),
("-2020-03-04", "2021-03-04 (BC)"),
("0001-01-01 (BC)", "0001-01-01 (BC)"),
("0001-01-01 (bc)", "0001-01-01 (BC)"),
("2021-01-01 (BC) ", "2021-01-01 (BC)"),
("0000-02-29", "0001-02-29 (BC)"),
("-0004-02-29", "0005-02-29 (BC)"),
("-5877641-06-25", "5877642-06-25 (BC)"),
("5877642-06-25 (BC)", "5877642-06-25 (BC)"),
] {
let date = cast_to(Value::Varchar(text.into()), &LogicalType::Date).expect(text);
assert_eq!(date.to_string(), printed, "{text}");
}
for text in ["0001-01-01 (BC) 10:00:00", "0001-01-01 (BC) 10:00:00", "-0001-01-01 10:00"] {
let stamp = cast_to(Value::Varchar(text.into()), &LogicalType::Timestamp).expect(text);
assert!(stamp.to_string().ends_with(" (BC) 10:00:00"), "{stamp}");
}
let time = cast_to(Value::Varchar("0001-01-01 (BC) 10:00:00".into()), &LogicalType::Time)
.expect("ten in the morning");
assert_eq!(time, Value::Time(10 * 3_600_000_000));
}
#[test]
fn an_era_written_some_other_way_is_not_a_date() {
for text in [
"2021-01-01(BC)",
"2021-01-01 (BC)",
"2021-01-01 (BC)x",
"2021-01-01 (BC)10:00:00",
"2021-01-01 BC",
"2021-01-01 (AD)",
"2021-01-01 (BC) (BC)",
"0000-01-01 (BC)",
"-2021-01-01 (BC)",
"+2021-01-01",
"2021-+01-01",
] {
let error =
cast_to(Value::Varchar(text.into()), &LogicalType::Date).expect_err("no such day");
assert_eq!(
error.message(),
format!("invalid date field format: \"{text}\", expected format is (YYYY-MM-DD)")
);
}
for text in ["-0001-02-29", "-5877641-06-24", "5877642-06-24 (BC)"] {
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_zone_on_the_end_of_a_written_timestamp_is_read_and_thrown_away() {
let morning = Value::Timestamp(
i64::from(days_from_civil(2020, 1, 1)) * MICROS_PER_DAY + 10 * 3_600_000_000,
);
for zone in [
"",
" ",
" ",
"Z",
"Z ",
"+05",
"+05 ",
"-05:30",
"+05:30:15",
"+99:00",
"+05:70",
" +05",
" 05",
" zzz",
" zzz ",
" Asia/Ho_Chi_Minh",
" +",
" z",
] {
let text = format!("2020-01-01 10:00:00{zone}");
let stamp = cast_to(Value::Varchar(text.clone()), &LogicalType::Timestamp);
assert_eq!(stamp.as_ref().ok(), Some(&morning), "{text}: {stamp:?}");
let day = cast_to(Value::Varchar(text.clone()), &LogicalType::Date);
assert_eq!(
day.as_ref().ok(),
Some(&Value::Date(days_from_civil(2020, 1, 1))),
"{text}"
);
}
for zone in ["+2", "-0", "+05:", "+05:3"] {
let text = format!("2020-01-01 10:00:00{zone}");
let error = cast_to(Value::Varchar(text.clone()), &LogicalType::Timestamp)
.expect_err("this is not an offset");
assert_eq!(
error.message(),
format!("timestamp field value \"{text}\" has a timestamp that is not UTC.")
);
let error = cast_to(Value::Varchar(text.clone()), &LogicalType::Date)
.expect_err("this is not an offset");
assert_eq!(
error.message(),
format!("invalid date field format: \"{text}\", expected format is (YYYY-MM-DD)")
);
}
for zone in
["x", "z", "Zx", "+123", "+05x", "+05 zzz", " zzz", " UTC junk", "+05:30:15:20"]
{
let text = format!("2020-01-01 10:00:00{zone}");
let error = cast_to(Value::Varchar(text.clone()), &LogicalType::Timestamp)
.expect_err("this is not a zone");
assert_eq!(
error.message(),
format!(
"invalid timestamp field format: \"{text}\", expected format is {TIMESTAMP_FORMAT}"
)
);
}
for (text, said) in [
(
"2020-01-01 25:00:00+2",
"timestamp field value out of range: \"2020-01-01 25:00:00+2\"",
),
(
"2020-01-01 -05:00",
"invalid timestamp field format: \"2020-01-01 -05:00\", 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_written_time_is_read_with_all_the_slack_duckdb_reads_it_with() {
let at = |hours: i64, minutes: i64, seconds: i64, micros: i64| {
Value::Time(((hours * 60 + minutes) * 60 + seconds) * 1_000_000 + micros)
};
for (text, expected) in [
("12:34:56", at(12, 34, 56, 0)),
("12:34:56.123456", at(12, 34, 56, 123_456)),
("12:34:56.5", at(12, 34, 56, 500_000)),
("12:34:56.1234567", at(12, 34, 56, 123_456)),
("12:34", at(12, 34, 0, 0)),
("12:34:", at(12, 34, 0, 0)),
("12:34:56.", at(12, 34, 56, 0)),
("12:34:56.abc", at(12, 34, 56, 0)),
("1:2:3", at(1, 2, 3, 0)),
("0:0:0", at(0, 0, 0, 0)),
(" 12:34:56 ", at(12, 34, 56, 0)),
("24:00:00", at(24, 0, 0, 0)),
("12:34:56 UTC", at(12, 34, 56, 0)),
("12:34:56+05:30", at(12, 34, 56, 0)),
("12:34:56-05", at(12, 34, 56, 0)),
("12:34:56abc", at(12, 34, 56, 0)),
("2024-01-02 03:04:05", at(3, 4, 5, 0)),
("2024-01-02T03:04:05", at(3, 4, 5, 0)),
("2020-01-02", at(0, 0, 0, 0)),
("2020-01-01 10:00:00 zzz", at(10, 0, 0, 0)),
("2020-01-01 10:00:00+05:30", at(10, 0, 0, 0)),
("2020-01-01 10:00:00.123456789 zzz", at(10, 0, 0, 123_456)),
("2020-01-01 24:00:00", at(0, 0, 0, 0)),
] {
let time = cast_to(Value::Varchar(text.into()), &LogicalType::Time);
assert_eq!(time.as_ref().ok(), Some(&expected), "{text}: {time:?}");
}
}
#[test]
fn a_written_time_that_is_not_one_says_the_only_thing_duckdb_says_about_it() {
for text in [
"abc",
"12",
"24:00:01",
"25:00:00",
"10:70:00",
"10:00:60",
"12::56",
"1234:56",
"12:34:abc",
"-01:00:00",
"24:00:00.000001",
"2024-13-02 03:04:05",
"2020-01-02 ",
"2020-01-01 10:00:00+2",
"2020-01-01 10:00:00 zzz",
"2020-01-01 zzz",
] {
let error = cast_to(Value::Varchar(text.into()), &LogicalType::Time)
.expect_err("this is not a time");
assert_eq!(
error.message(),
format!(
"time field value out of range: \"{text}\", expected format is ([YYYY-MM-DD ]HH:MM:SS[.MS])"
)
);
}
}
#[test]
fn a_timestamp_casts_to_the_time_of_day_it_is() {
let stamp = Value::Timestamp(i64::from(days_from_civil(2024, 1, 2)) * MICROS_PER_DAY + 5);
assert_eq!(cast_to(stamp, &LogicalType::Time).expect("a time"), Value::Time(5));
let before = Value::Timestamp(-1);
assert_eq!(
cast_to(before, &LogicalType::Time).expect("a time"),
Value::Time(MICROS_PER_DAY - 1),
"the last microsecond of 1969 is the last microsecond of the day"
);
for value in [Value::Date(0), Value::Integer(1)] {
let written = value.logical_type();
let error = cast_to(value, &LogicalType::Time).expect_err("no cast for this");
assert_eq!(error.message(), format!("Unimplemented type for cast ({written} -> TIME)"));
}
}
#[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);
}
fn written(text: &str) -> String {
cast_to(Value::Varchar(text.into()), &LogicalType::Interval)
.unwrap_or_else(|error| panic!("{text} is an interval: {error}"))
.to_string()
}
#[test]
fn an_interval_reads_the_counts_and_the_unit_words_written_inside_the_string() {
for (text, expected) in [
("1 day", "1 day"),
("1 Days", "1 day"),
("1 c", "100 years"),
("1 cent", "100 years"),
("1 centuries", "100 years"),
("1 microseconds", "00:00:00.000001"),
("1 weekofyear", "7 days"),
("1 dayofmonth", "1 day"),
("5", "00:00:05"),
("1.5", "00:00:01.5"),
("-1.5", "-00:00:01.5"),
("1 day 1 day", "2 days"),
("1.5 day 1.5 day", "2 days 24:00:00"),
("1 day -2 hours", "1 day -02:00:00"),
("1000 months 1000 months", "166 years 8 months"),
("1 DAY AGO", "-1 day"),
("1 day ago", "-1 day"),
("-1 day ago", "1 day"),
] {
assert_eq!(written(text), expected, "{text}");
}
}
#[test]
fn an_intervals_fraction_lands_where_the_unit_it_was_written_on_says_it_lands() {
for (text, expected) in [
("1.1 years", "1 year 1 month"),
("1.5 years", "1 year 6 months"),
("-1.1 years", "-1 year -1 month"),
("0.1 months", "3 days"),
("0.5 months", "15 days"),
("1.01 months", "1 month 07:12:00"),
("1.25 months", "1 month 7 days 12:00:00"),
("0.033333333 months", "23:59:59.999136"),
("0.999999999 months", "29 days 23:59:59.997408"),
("-1.25 months", "-1 month -7 days -12:00:00"),
("1.01 quarters", "3 months 1 day"),
("1.1 quarters", "3 months 9 days"),
("1.25 quarters", "3 months 23 days"),
("0.9 quarters", "2 months 21 days"),
("-1.25 quarters", "-3 months -23 days"),
("1.5 weeks", "10 days 12:00:00"),
("0.999999999 weeks", "6 days 23:59:59.999395"),
("0.9 days", "21:36:00"),
("0.999999999 days", "23:59:59.999914"),
("0.999999999 hours", "00:59:59.999996"),
("0.9999995 s", "00:00:01"),
("0.9999994 s", "00:00:00.999999"),
("-0.9999995 s", "-00:00:01"),
("0.1234567891 s", "00:00:00.123457"),
("1.5 ms", "00:00:00.0015"),
("0.5 ms", "00:00:00.0005"),
("2.5 us", "00:00:00.000002"),
("1.999999999 microseconds", "00:00:00.000001"),
] {
assert_eq!(written(text), expected, "{text}");
}
}
#[test]
fn an_interval_with_a_clock_in_it_reads_the_clock_and_ignores_whatever_follows() {
for (text, expected) in [
("1:02", "01:02:00"),
("0:0:0.5", "00:00:00.5"),
("1:2:3.", "01:02:03"),
("1:", "01:00:00"),
("1:2:", "01:02:00"),
("1:2:3.123456789", "01:02:03.123456"),
("1:2:3.9999999", "01:02:03.999999"),
("1:2:3:4", "01:02:03"),
("24:00:00", "24:00:00"),
("100:00:00", "100:00:00"),
("1:2:3 ago", "01:02:03"),
("-1:2:3", "-01:02:03"),
("1 day 01:02:03", "1 day 01:02:03"),
("1 day 1:2:3 2 hours", "1 day 01:02:03"),
] {
assert_eq!(written(text), expected, "{text}");
}
}
#[test]
fn a_string_that_is_not_an_interval_fails_in_duckdbs_words_and_try_casts_to_null() {
for (text, expected) in [
("", "Could not convert string '' to INTERVAL"),
(" ", "Could not convert string ' ' to INTERVAL"),
("1,2 days", "Could not convert string '1,2 days' to INTERVAL"),
("99:99:99", "Could not convert string '99:99:99' to INTERVAL"),
("1.5:2:3", "Could not convert string '1.5:2:3' to INTERVAL"),
("1:2: ", "Could not convert string '1:2: ' to INTERVAL"),
("1 day ago ago", "Could not convert string '1 day ago ago' to INTERVAL"),
("1 day 5", "extract specifier \"\" not recognized"),
("1 ago", "extract specifier \"ago\" not recognized"),
("1 XyZ", "extract specifier \"XyZ\" not recognized"),
("1 DOW", "extract specifier \"DOW\" not supported for interval"),
(
"2147483648 days",
"Type INT64 with value 2147483648 can't be cast because the value is out of range for the destination type INT32",
),
(
"2000000000 weeks",
"Type INT64 with value 14000000000 can't be cast because the value is out of range for the destination type INT32",
),
("9223372036854775808 us", "Could not convert string '9223372036854775808' to INT64"),
("1073741824 days 1073741824 days", "interval value is out of range"),
("9223372036854775807 years", "interval value is out of range"),
] {
let value = Value::Varchar(text.into());
let error = cast_to(value.clone(), &LogicalType::Interval).expect_err(text);
assert_eq!(error.message(), expected, "{text}");
let tried = cast_value(&value, &LogicalType::Interval, true).expect(text);
assert_eq!(tried, Value::Null, "{text}");
}
}
}