use std::fmt;
use crate::types::LogicalType;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
Null,
Boolean(bool),
TinyInt(i8),
SmallInt(i16),
Integer(i32),
BigInt(i64),
HugeInt(i128),
UTinyInt(u8),
USmallInt(u16),
UInteger(u32),
UBigInt(u64),
UHugeInt(u128),
Float(f32),
Double(f64),
Decimal {
unscaled: i128,
width: u8,
scale: u8,
},
Varchar(String),
Blob(Vec<u8>),
Date(i32),
Time(i64),
Timestamp(i64),
Interval {
months: i32,
days: i32,
micros: i64,
},
List {
element: LogicalType,
values: Vec<Value>,
},
Struct(Vec<(String, Value)>),
}
impl Value {
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use]
pub fn logical_type(&self) -> LogicalType {
match self {
Self::Null => LogicalType::Null,
Self::Boolean(_) => LogicalType::Boolean,
Self::TinyInt(_) => LogicalType::TinyInt,
Self::SmallInt(_) => LogicalType::SmallInt,
Self::Integer(_) => LogicalType::Integer,
Self::BigInt(_) => LogicalType::BigInt,
Self::HugeInt(_) => LogicalType::HugeInt,
Self::UTinyInt(_) => LogicalType::UTinyInt,
Self::USmallInt(_) => LogicalType::USmallInt,
Self::UInteger(_) => LogicalType::UInteger,
Self::UBigInt(_) => LogicalType::UBigInt,
Self::UHugeInt(_) => LogicalType::UHugeInt,
Self::Float(_) => LogicalType::Float,
Self::Double(_) => LogicalType::Double,
Self::Decimal { width, scale, .. } => {
LogicalType::Decimal { width: *width, scale: *scale }
}
Self::Varchar(_) => LogicalType::Varchar,
Self::Blob(_) => LogicalType::Blob,
Self::Date(_) => LogicalType::Date,
Self::Time(_) => LogicalType::Time,
Self::Timestamp(_) => LogicalType::Timestamp,
Self::Interval { .. } => LogicalType::Interval,
Self::List { element, .. } => LogicalType::list(element.clone()),
Self::Struct(fields) => LogicalType::Struct(
fields
.iter()
.map(|(name, value)| crate::types::Field::new(name, value.logical_type()))
.collect(),
),
}
}
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
match *self {
Self::TinyInt(v) => Some(i64::from(v)),
Self::SmallInt(v) => Some(i64::from(v)),
Self::Integer(v) => Some(i64::from(v)),
Self::BigInt(v) => Some(v),
Self::UTinyInt(v) => Some(i64::from(v)),
Self::USmallInt(v) => Some(i64::from(v)),
Self::UInteger(v) => Some(i64::from(v)),
Self::UBigInt(v) => i64::try_from(v).ok(),
Self::HugeInt(v) => i64::try_from(v).ok(),
Self::UHugeInt(v) => i64::try_from(v).ok(),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match *self {
Self::Boolean(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Varchar(v) => Some(v),
_ => None,
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Null => f.write_str("NULL"),
Self::Boolean(v) => f.write_str(if *v { "true" } else { "false" }),
Self::TinyInt(v) => write!(f, "{v}"),
Self::SmallInt(v) => write!(f, "{v}"),
Self::Integer(v) => write!(f, "{v}"),
Self::BigInt(v) => write!(f, "{v}"),
Self::HugeInt(v) => write!(f, "{v}"),
Self::UTinyInt(v) => write!(f, "{v}"),
Self::USmallInt(v) => write!(f, "{v}"),
Self::UInteger(v) => write!(f, "{v}"),
Self::UBigInt(v) => write!(f, "{v}"),
Self::UHugeInt(v) => write!(f, "{v}"),
Self::Float(v) => write_float(f, f64::from(*v)),
Self::Double(v) => write_float(f, *v),
Self::Decimal { unscaled, scale, .. } => write_decimal(f, *unscaled, *scale),
Self::Varchar(v) => f.write_str(v),
Self::Blob(v) => write_blob(f, v),
Self::Date(v) => write_date(f, *v),
Self::Time(v) => write_time(f, *v),
Self::Timestamp(v) => write_timestamp(f, *v),
Self::Interval { months, days, micros } => write_interval(f, *months, *days, *micros),
Self::List { values, .. } => {
f.write_str("[")?;
for (index, value) in values.iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{value}")?;
}
f.write_str("]")
}
Self::Struct(fields) => {
f.write_str("{")?;
for (index, (name, value)) in fields.iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "'{name}': {value}")?;
}
f.write_str("}")
}
}
}
}
fn write_float(f: &mut fmt::Formatter<'_>, value: f64) -> fmt::Result {
if value.is_nan() {
return f.write_str("nan");
}
if value.is_infinite() {
return f.write_str(if value > 0.0 { "inf" } else { "-inf" });
}
let text = format!("{value}");
f.write_str(text.strip_suffix(".0").unwrap_or(&text))
}
fn write_decimal(f: &mut fmt::Formatter<'_>, unscaled: i128, scale: u8) -> fmt::Result {
if scale == 0 {
return write!(f, "{unscaled}");
}
let negative = unscaled < 0;
let digits = unscaled.unsigned_abs().to_string();
let scale = usize::from(scale);
let (whole, fraction) = if digits.len() > scale {
let split = digits.len() - scale;
(digits[..split].to_string(), digits[split..].to_string())
} else {
("0".to_string(), format!("{:0>scale$}", digits))
};
if negative {
f.write_str("-")?;
}
write!(f, "{whole}.{fraction}")
}
fn write_blob(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
for &byte in bytes {
if byte.is_ascii_graphic() || byte == b' ' {
write!(f, "{}", byte as char)?;
} else {
write!(f, "\\x{byte:02X}")?;
}
}
Ok(())
}
#[must_use]
pub fn civil_from_days(days: i32) -> (i32, u32, u32) {
let z = i64::from(days) + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let day_of_era = z - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let shifted_month = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
let month = if shifted_month < 10 { shifted_month + 3 } else { shifted_month - 9 };
let year = if month <= 2 { year + 1 } else { year };
#[expect(clippy::cast_possible_truncation, reason = "the ranges are 1 to 12 and 1 to 31")]
(year as i32, month as u32, day as u32)
}
#[must_use]
pub fn days_from_civil(year: i32, month: u32, day: u32) -> i32 {
let year = i64::from(year) - i64::from(month <= 2);
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let month = i64::from(month);
let shifted_month = if month > 2 { month - 3 } else { month + 9 };
let day_of_year = (153 * shifted_month + 2) / 5 + i64::from(day) - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
#[expect(clippy::cast_possible_truncation, reason = "a date in i32 range stays in i32 range")]
((era * 146_097 + day_of_era - 719_468) as i32)
}
fn write_date(f: &mut fmt::Formatter<'_>, days: i32) -> fmt::Result {
let (year, month, day) = civil_from_days(days);
if year < 0 {
write!(f, "{:04}-{month:02}-{day:02} (BC)", -year + 1)
} else {
write!(f, "{year:04}-{month:02}-{day:02}")
}
}
fn write_time(f: &mut fmt::Formatter<'_>, micros: i64) -> fmt::Result {
let seconds = micros.div_euclid(1_000_000);
let fraction = micros.rem_euclid(1_000_000);
let (hours, minutes, seconds) = (seconds / 3600, (seconds / 60) % 60, seconds % 60);
write!(f, "{hours:02}:{minutes:02}:{seconds:02}")?;
if fraction != 0 {
let text = format!("{fraction:06}");
write!(f, ".{}", text.trim_end_matches('0'))?;
}
Ok(())
}
fn write_timestamp(f: &mut fmt::Formatter<'_>, micros: i64) -> fmt::Result {
const MICROS_PER_DAY: i64 = 86_400 * 1_000_000;
let days = micros.div_euclid(MICROS_PER_DAY);
let within_day = micros.rem_euclid(MICROS_PER_DAY);
let Ok(days) = i32::try_from(days) else {
return f.write_str("timestamp out of range");
};
write_date(f, days)?;
f.write_str(" ")?;
write_time(f, within_day)
}
fn write_interval(f: &mut fmt::Formatter<'_>, months: i32, days: i32, micros: i64) -> fmt::Result {
let mut wrote = false;
let space = |f: &mut fmt::Formatter<'_>, wrote: &mut bool| -> fmt::Result {
if *wrote {
f.write_str(" ")?;
}
*wrote = true;
Ok(())
};
let (years, rest_months) = (months / 12, months % 12);
if years != 0 {
space(f, &mut wrote)?;
write!(f, "{years} year{}", plural(years))?;
}
if rest_months != 0 {
space(f, &mut wrote)?;
write!(f, "{rest_months} month{}", plural(rest_months))?;
}
if days != 0 {
space(f, &mut wrote)?;
write!(f, "{days} day{}", plural(days))?;
}
if micros != 0 || !wrote {
space(f, &mut wrote)?;
if micros < 0 {
f.write_str("-")?;
}
write_time(f, micros.abs())?;
}
Ok(())
}
fn plural(n: i32) -> &'static str {
if n == 1 || n == -1 { "" } else { "s" }
}
#[cfg(test)]
mod tests {
use super::{Value, civil_from_days, days_from_civil};
use crate::types::LogicalType;
#[test]
fn a_value_knows_its_own_type() {
assert_eq!(Value::Integer(1).logical_type(), LogicalType::Integer);
assert_eq!(Value::Null.logical_type(), LogicalType::Null);
let list = Value::List { element: LogicalType::Varchar, values: Vec::new() };
assert_eq!(list.logical_type(), LogicalType::list(LogicalType::Varchar));
}
#[test]
fn the_date_conversion_is_its_own_inverse() {
for days in days_from_civil(1600, 1, 1)..days_from_civil(2400, 1, 1) {
let (year, month, day) = civil_from_days(days);
assert_eq!(days_from_civil(year, month, day), days, "{year}-{month}-{day}");
}
}
#[test]
fn the_epoch_is_where_it_should_be() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(Value::Date(0).to_string(), "1970-01-01");
assert_eq!(Value::Date(19_723).to_string(), "2024-01-01");
assert_eq!(Value::Date(19_737).to_string(), "2024-01-15");
}
#[test]
fn a_leap_day_is_a_day() {
assert_eq!(civil_from_days(days_from_civil(2024, 2, 29)), (2024, 2, 29));
assert_eq!(days_from_civil(1900, 3, 1) - days_from_civil(1900, 2, 28), 1);
assert_eq!(days_from_civil(2000, 3, 1) - days_from_civil(2000, 2, 28), 2);
}
#[test]
fn times_print_with_the_trailing_zeros_trimmed() {
assert_eq!(Value::Time(0).to_string(), "00:00:00");
assert_eq!(Value::Time(3_723_000_000).to_string(), "01:02:03");
assert_eq!(Value::Time(3_723_500_000).to_string(), "01:02:03.5");
assert_eq!(Value::Time(3_723_000_001).to_string(), "01:02:03.000001");
}
#[test]
fn a_timestamp_before_the_epoch_borrows_from_the_day() {
assert_eq!(Value::Timestamp(-1).to_string(), "1969-12-31 23:59:59.999999");
assert_eq!(Value::Timestamp(0).to_string(), "1970-01-01 00:00:00");
}
#[test]
fn a_decimal_prints_at_its_scale() {
let d = |unscaled, scale| Value::Decimal { unscaled, width: 18, scale }.to_string();
assert_eq!(d(1234, 2), "12.34");
assert_eq!(d(-1234, 2), "-12.34");
assert_eq!(d(5, 3), "0.005");
assert_eq!(d(-5, 3), "-0.005");
assert_eq!(d(1234, 0), "1234");
assert_eq!(d(1_000_000, 6), "1.000000");
}
#[test]
fn an_integral_float_prints_without_the_rust_trailing_zero() {
assert_eq!(Value::Double(1.0).to_string(), "1");
assert_eq!(Value::Double(1.5).to_string(), "1.5");
assert_eq!(Value::Double(f64::INFINITY).to_string(), "inf");
assert_eq!(Value::Float(0.5).to_string(), "0.5");
}
#[test]
fn an_interval_keeps_months_days_and_micros_apart() {
let i = |months, days, micros| Value::Interval { months, days, micros }.to_string();
assert_eq!(i(14, 3, 3_723_000_000), "1 year 2 months 3 days 01:02:03");
assert_eq!(i(1, 0, 0), "1 month");
assert_eq!(i(0, 0, 0), "00:00:00");
assert_eq!(i(0, 0, -1_000_000), "-00:00:01");
}
#[test]
fn a_blob_escapes_what_is_not_printable() {
assert_eq!(Value::Blob(b"ok".to_vec()).to_string(), "ok");
assert_eq!(Value::Blob(vec![0, 1, b'a']).to_string(), "\\x00\\x01a");
}
#[test]
fn a_limit_that_does_not_fit_is_none_rather_than_clamped() {
assert_eq!(Value::Integer(5).as_i64(), Some(5));
assert_eq!(Value::UBigInt(u64::MAX).as_i64(), None);
assert_eq!(Value::Varchar("5".into()).as_i64(), None);
}
}