use crate::wire::consts::sql_type;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Short(i16),
Int(i32),
BigInt(i64),
Float(f32),
Double(f64),
Text(String),
Bytes(Vec<u8>),
Blob(u64),
Array(u64),
Date(i32),
Time(u32),
Timestamp(i32, u32),
Int128(i128),
DecFloat(crate::decfloat::DecFloat),
TimeTz(TimeTz),
TimestampTz(TimestampTz),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeTz {
pub utc_time: u32,
pub zone: u16,
pub offset: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimestampTz {
pub utc_date: i32,
pub utc_time: u32,
pub zone: u16,
pub offset: i16,
}
const FB_TIME_UNITS_PER_DAY: i64 = 24 * 3600 * FB_TIME_UNITS_PER_SEC as i64;
impl TimeTz {
pub fn zone_name(&self) -> Option<&'static str> {
crate::tz::zone_name(self.zone)
}
pub fn zone_label(&self) -> String {
crate::tz::zone_label(self.zone)
}
pub fn local(&self) -> CivilTime {
let units = (self.utc_time as i64 + self.offset as i64 * 60 * FB_TIME_UNITS_PER_SEC as i64)
.rem_euclid(FB_TIME_UNITS_PER_DAY) as u32;
Value::Time(units).as_civil_time().unwrap()
}
}
impl TimestampTz {
pub fn zone_name(&self) -> Option<&'static str> {
crate::tz::zone_name(self.zone)
}
pub fn zone_label(&self) -> String {
crate::tz::zone_label(self.zone)
}
pub fn local(&self) -> CivilTimestamp {
let total = self.utc_date as i64 * FB_TIME_UNITS_PER_DAY
+ self.utc_time as i64
+ self.offset as i64 * 60 * FB_TIME_UNITS_PER_SEC as i64;
let date = total.div_euclid(FB_TIME_UNITS_PER_DAY);
let time = total.rem_euclid(FB_TIME_UNITS_PER_DAY) as u32;
CivilTimestamp {
date: Value::Date(date as i32).as_civil_date().unwrap(),
time: Value::Time(time).as_civil_time().unwrap(),
}
}
}
const FB_EPOCH_TO_UNIX_DAYS: i32 = 40587;
const FB_TIME_UNITS_PER_SEC: u32 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CivilDate {
pub year: i32,
pub month: u32,
pub day: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CivilTime {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub frac: u32,
}
impl CivilTime {
pub fn nanos(&self) -> u32 {
self.frac * 100_000
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CivilTimestamp {
pub date: CivilDate,
pub time: CivilTime,
}
fn civil_from_unix_days(z: i64) -> CivilDate {
let z = z + 719_468;
let era = (if z >= 0 { z } else { z - 146_096 }) / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u32; let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let year = if month <= 2 { y + 1 } else { y };
CivilDate {
year: year as i32,
month,
day,
}
}
fn unix_days_from_civil(d: CivilDate) -> i64 {
let y = d.year as i64 - if d.month <= 2 { 1 } else { 0 };
let era = (if y >= 0 { y } else { y - 399 }) / 400;
let yoe = y - era * 400; let m = d.month as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.day as i64 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe - 719_468
}
impl CivilDate {
pub fn to_fb_days(self) -> i32 {
(unix_days_from_civil(self) as i32) + FB_EPOCH_TO_UNIX_DAYS
}
}
impl CivilTime {
pub fn to_fb_time(self) -> u32 {
((self.hour * 3600 + self.minute * 60 + self.second) * FB_TIME_UNITS_PER_SEC) + self.frac
}
}
impl Value {
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn date(year: i32, month: u32, day: u32) -> Value {
Value::Date(CivilDate { year, month, day }.to_fb_days())
}
pub fn time(hour: u32, minute: u32, second: u32, frac: u32) -> Value {
Value::Time(
CivilTime {
hour,
minute,
second,
frac,
}
.to_fb_time(),
)
}
pub fn timestamp(date: CivilDate, time: CivilTime) -> Value {
Value::Timestamp(date.to_fb_days(), time.to_fb_time())
}
pub fn as_civil_date(&self) -> Option<CivilDate> {
match self {
Value::Date(d) | Value::Timestamp(d, _) => Some(civil_from_unix_days(
*d as i64 - FB_EPOCH_TO_UNIX_DAYS as i64,
)),
_ => None,
}
}
pub fn as_civil_time(&self) -> Option<CivilTime> {
let t = match self {
Value::Time(t) | Value::Timestamp(_, t) => *t,
_ => return None,
};
let frac = t % FB_TIME_UNITS_PER_SEC;
let secs = t / FB_TIME_UNITS_PER_SEC;
Some(CivilTime {
hour: secs / 3600,
minute: (secs % 3600) / 60,
second: secs % 60,
frac,
})
}
pub fn as_civil_timestamp(&self) -> Option<CivilTimestamp> {
match self {
Value::Timestamp(..) => Some(CivilTimestamp {
date: self.as_civil_date()?,
time: self.as_civil_time()?,
}),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Short(v) => Some(*v as i64),
Value::Int(v) => Some(*v as i64),
Value::BigInt(v) => Some(*v),
Value::Int128(v) => i64::try_from(*v).ok(),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::Text(s) => Some(s),
_ => None,
}
}
}
impl From<bool> for Value {
fn from(v: bool) -> Self {
Value::Bool(v)
}
}
impl From<i16> for Value {
fn from(v: i16) -> Self {
Value::Short(v)
}
}
impl From<i32> for Value {
fn from(v: i32) -> Self {
Value::Int(v)
}
}
impl From<i64> for Value {
fn from(v: i64) -> Self {
Value::BigInt(v)
}
}
impl From<i128> for Value {
fn from(v: i128) -> Self {
Value::Int128(v)
}
}
impl From<f32> for Value {
fn from(v: f32) -> Self {
Value::Float(v)
}
}
impl From<f64> for Value {
fn from(v: f64) -> Self {
Value::Double(v)
}
}
impl From<String> for Value {
fn from(v: String) -> Self {
Value::Text(v)
}
}
impl From<&str> for Value {
fn from(v: &str) -> Self {
Value::Text(v.to_string())
}
}
impl From<Vec<u8>> for Value {
fn from(v: Vec<u8>) -> Self {
Value::Bytes(v)
}
}
impl From<&[u8]> for Value {
fn from(v: &[u8]) -> Self {
Value::Bytes(v.to_vec())
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveDate> for CivilDate {
fn from(v: chrono::NaiveDate) -> Self {
use chrono::Datelike;
CivilDate {
year: v.year(),
month: v.month(),
day: v.day(),
}
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveTime> for CivilTime {
fn from(v: chrono::NaiveTime) -> Self {
use chrono::Timelike;
CivilTime {
hour: v.hour(),
minute: v.minute(),
second: v.second(),
frac: v.nanosecond() / 100_000,
}
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveDateTime> for CivilTimestamp {
fn from(v: chrono::NaiveDateTime) -> Self {
CivilTimestamp {
date: v.date().into(),
time: v.time().into(),
}
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveDate> for Value {
fn from(v: chrono::NaiveDate) -> Self {
Value::Date(CivilDate::from(v).to_fb_days())
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveTime> for Value {
fn from(v: chrono::NaiveTime) -> Self {
Value::Time(CivilTime::from(v).to_fb_time())
}
}
#[cfg(feature = "chrono")]
impl From<chrono::NaiveDateTime> for Value {
fn from(v: chrono::NaiveDateTime) -> Self {
Value::timestamp(v.date().into(), v.time().into())
}
}
#[cfg(feature = "chrono")]
impl TryFrom<&Value> for chrono::NaiveDate {
type Error = crate::Error;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let d = v
.as_civil_date()
.ok_or_else(|| crate::Error::protocol("expected a DATE/TIMESTAMP value"))?;
chrono::NaiveDate::from_ymd_opt(d.year, d.month, d.day)
.ok_or_else(|| crate::Error::protocol("DATE value is out of chrono range"))
}
}
#[cfg(feature = "chrono")]
impl TryFrom<Value> for chrono::NaiveDate {
type Error = crate::Error;
fn try_from(v: Value) -> Result<Self, Self::Error> {
chrono::NaiveDate::try_from(&v)
}
}
#[cfg(feature = "chrono")]
impl TryFrom<&Value> for chrono::NaiveTime {
type Error = crate::Error;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let t = v
.as_civil_time()
.ok_or_else(|| crate::Error::protocol("expected a TIME/TIMESTAMP value"))?;
chrono::NaiveTime::from_hms_nano_opt(t.hour, t.minute, t.second, t.nanos())
.ok_or_else(|| crate::Error::protocol("TIME value is out of chrono range"))
}
}
#[cfg(feature = "chrono")]
impl TryFrom<Value> for chrono::NaiveTime {
type Error = crate::Error;
fn try_from(v: Value) -> Result<Self, Self::Error> {
chrono::NaiveTime::try_from(&v)
}
}
#[cfg(feature = "chrono")]
impl TryFrom<&Value> for chrono::NaiveDateTime {
type Error = crate::Error;
fn try_from(v: &Value) -> Result<Self, Self::Error> {
let ts = v
.as_civil_timestamp()
.ok_or_else(|| crate::Error::protocol("expected a TIMESTAMP value"))?;
let date = chrono::NaiveDate::from_ymd_opt(ts.date.year, ts.date.month, ts.date.day)
.ok_or_else(|| crate::Error::protocol("TIMESTAMP date is out of chrono range"))?;
date.and_hms_nano_opt(
ts.time.hour,
ts.time.minute,
ts.time.second,
ts.time.nanos(),
)
.ok_or_else(|| crate::Error::protocol("TIMESTAMP time is out of chrono range"))
}
}
#[cfg(feature = "chrono")]
impl TryFrom<Value> for chrono::NaiveDateTime {
type Error = crate::Error;
fn try_from(v: Value) -> Result<Self, Self::Error> {
chrono::NaiveDateTime::try_from(&v)
}
}
#[derive(Debug, Clone, Default)]
pub struct ColumnMeta {
pub index: usize,
pub sql_type: i32,
pub sub_type: i32,
pub scale: i32,
pub length: i32,
pub nullable: bool,
pub field: String,
pub relation: String,
pub alias: String,
pub owner: String,
}
impl ColumnMeta {
pub fn name(&self) -> &str {
if self.alias.is_empty() {
&self.field
} else {
&self.alias
}
}
pub(crate) fn xdr_len(&self) -> usize {
match sql_type::base(self.sql_type) {
sql_type::TEXT => align4(self.length as usize),
sql_type::VARYING => 4 + align4(self.length as usize),
sql_type::SHORT | sql_type::LONG => 4,
sql_type::INT64 => 8,
sql_type::INT128 => 16,
sql_type::FLOAT => 4,
sql_type::DOUBLE | sql_type::D_FLOAT => 8,
sql_type::TYPE_DATE | sql_type::TYPE_TIME => 4,
sql_type::TIMESTAMP => 8,
sql_type::BLOB | sql_type::QUAD | sql_type::ARRAY => 8,
sql_type::BOOLEAN => 4,
sql_type::DEC16 => 8,
sql_type::DEC34 => 16,
sql_type::TIME_TZ | sql_type::TIME_TZ_EX => 12,
sql_type::TIMESTAMP_TZ | sql_type::TIMESTAMP_TZ_EX => 16,
_ => 8,
}
}
}
#[inline]
pub(crate) fn align4(n: usize) -> usize {
(n + 3) & !3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_roundtrip_and_known_points() {
assert_eq!(
Value::Date(0).as_civil_date(),
Some(CivilDate {
year: 1858,
month: 11,
day: 17
})
);
assert_eq!(
Value::Date(40_587).as_civil_date(),
Some(CivilDate {
year: 1970,
month: 1,
day: 1
})
);
for (y, m, d) in [
(1858, 11, 17),
(1970, 1, 1),
(2000, 2, 29),
(2026, 6, 20),
(1, 1, 1),
(2400, 12, 31),
] {
let v = Value::date(y, m, d);
assert_eq!(
v.as_civil_date(),
Some(CivilDate {
year: y,
month: m,
day: d
})
);
}
}
#[test]
fn time_roundtrip() {
assert_eq!(
Value::Time(0).as_civil_time(),
Some(CivilTime {
hour: 0,
minute: 0,
second: 0,
frac: 0
})
);
let raw = (23 * 3600 + 59 * 60 + 59) * 10_000 + 9999;
assert_eq!(
Value::Time(raw).as_civil_time(),
Some(CivilTime {
hour: 23,
minute: 59,
second: 59,
frac: 9999
})
);
let v = Value::time(13, 45, 30, 1234);
let ct = v.as_civil_time().unwrap();
assert_eq!((ct.hour, ct.minute, ct.second, ct.frac), (13, 45, 30, 1234));
assert_eq!(ct.nanos(), 123_400_000);
}
#[test]
fn timestamp_splits_date_and_time() {
let date = CivilDate {
year: 2026,
month: 6,
day: 20,
};
let time = CivilTime {
hour: 9,
minute: 30,
second: 15,
frac: 0,
};
let v = Value::timestamp(date, time);
let ts = v.as_civil_timestamp().unwrap();
assert_eq!(ts.date, date);
assert_eq!(ts.time, time);
assert_eq!(Value::Date(0).as_civil_timestamp(), None);
assert_eq!(v.as_civil_date(), Some(date));
assert_eq!(v.as_civil_time(), Some(time));
}
#[test]
fn value_from_rust_primitives() {
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(Value::from(7_i16), Value::Short(7));
assert_eq!(Value::from(42_i32), Value::Int(42));
assert_eq!(Value::from(99_i64), Value::BigInt(99));
assert_eq!(Value::from(123_i128), Value::Int128(123));
assert_eq!(Value::from(1.5_f32), Value::Float(1.5));
assert_eq!(Value::from(2.5_f64), Value::Double(2.5));
assert_eq!(Value::from("Ana"), Value::Text("Ana".to_string()));
assert_eq!(
Value::from("Bruno".to_string()),
Value::Text("Bruno".to_string())
);
assert_eq!(Value::from(vec![1_u8, 2, 3]), Value::Bytes(vec![1, 2, 3]));
assert_eq!(Value::from(&[4_u8, 5][..]), Value::Bytes(vec![4, 5]));
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_naive_values_convert_to_driver_values() {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
let date = NaiveDate::from_ymd_opt(2026, 6, 23).unwrap();
assert_eq!(Value::from(date), Value::date(2026, 6, 23));
let time = NaiveTime::from_hms_nano_opt(14, 5, 6, 123_456_789).unwrap();
assert_eq!(Value::from(time), Value::time(14, 5, 6, 1234));
let timestamp = NaiveDateTime::new(date, time);
assert_eq!(
Value::from(timestamp),
Value::timestamp(
CivilDate {
year: 2026,
month: 6,
day: 23
},
CivilTime {
hour: 14,
minute: 5,
second: 6,
frac: 1234
}
)
);
}
#[cfg(feature = "chrono")]
#[test]
fn chrono_naive_values_convert_from_driver_values() {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
let date = NaiveDate::try_from(&Value::date(2026, 6, 23)).unwrap();
assert_eq!(date, NaiveDate::from_ymd_opt(2026, 6, 23).unwrap());
let time = NaiveTime::try_from(&Value::time(14, 5, 6, 1234)).unwrap();
assert_eq!(
time,
NaiveTime::from_hms_nano_opt(14, 5, 6, 123_400_000).unwrap()
);
let timestamp = Value::timestamp(
CivilDate {
year: 2026,
month: 6,
day: 23,
},
CivilTime {
hour: 14,
minute: 5,
second: 6,
frac: 1234,
},
);
let timestamp = NaiveDateTime::try_from(×tamp).unwrap();
assert_eq!(
timestamp,
NaiveDate::from_ymd_opt(2026, 6, 23)
.unwrap()
.and_hms_nano_opt(14, 5, 6, 123_400_000)
.unwrap()
);
}
}