use libduckdb_sys::{
duckdb_date, duckdb_date_struct, duckdb_decimal, duckdb_decimal_to_double,
duckdb_double_to_decimal, duckdb_double_to_hugeint, duckdb_double_to_uhugeint,
duckdb_from_date, duckdb_from_time, duckdb_from_time_tz, duckdb_from_timestamp, duckdb_hugeint,
duckdb_hugeint_to_double, duckdb_is_finite_date, duckdb_is_finite_timestamp,
duckdb_is_finite_timestamp_ms, duckdb_is_finite_timestamp_ns, duckdb_is_finite_timestamp_s,
duckdb_time, duckdb_time_struct, duckdb_time_tz, duckdb_timestamp, duckdb_timestamp_ms,
duckdb_timestamp_ns, duckdb_timestamp_s, duckdb_timestamp_struct, duckdb_to_date,
duckdb_to_time, duckdb_to_timestamp, duckdb_uhugeint, duckdb_uhugeint_to_double,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date {
pub year: i32,
pub month: i8,
pub day: i8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Time {
pub hour: i8,
pub min: i8,
pub sec: i8,
pub micros: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TimeTz {
pub time: Time,
pub offset_seconds: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp {
pub date: Date,
pub time: Time,
}
impl From<duckdb_date_struct> for Date {
fn from(value: duckdb_date_struct) -> Self {
Self {
year: value.year,
month: value.month,
day: value.day,
}
}
}
impl From<Date> for duckdb_date_struct {
fn from(value: Date) -> Self {
Self {
year: value.year,
month: value.month,
day: value.day,
}
}
}
impl From<duckdb_time_struct> for Time {
fn from(value: duckdb_time_struct) -> Self {
Self {
hour: value.hour,
min: value.min,
sec: value.sec,
micros: value.micros,
}
}
}
impl From<Time> for duckdb_time_struct {
fn from(value: Time) -> Self {
Self {
hour: value.hour,
min: value.min,
sec: value.sec,
micros: value.micros,
}
}
}
pub const DATE_INFINITY_DAYS: i32 = i32::MAX;
pub const DATE_NEGATIVE_INFINITY_DAYS: i32 = -i32::MAX;
pub const TIMESTAMP_INFINITY_MICROS: i64 = i64::MAX;
pub const TIMESTAMP_NEGATIVE_INFINITY_MICROS: i64 = -i64::MAX;
#[must_use]
pub unsafe fn date_from_days(days: i32) -> Date {
unsafe { duckdb_from_date(duckdb_date { days }) }.into()
}
#[must_use]
pub unsafe fn date_to_days(date: Date) -> i32 {
unsafe { duckdb_to_date(date.into()) }.days
}
#[must_use]
pub unsafe fn is_finite_date(days: i32) -> bool {
unsafe { duckdb_is_finite_date(duckdb_date { days }) }
}
#[must_use]
pub unsafe fn time_from_micros(micros: i64) -> Time {
unsafe { duckdb_from_time(duckdb_time { micros }) }.into()
}
#[must_use]
pub unsafe fn time_to_micros(time: Time) -> i64 {
unsafe { duckdb_to_time(time.into()) }.micros
}
#[must_use]
pub unsafe fn time_tz_bits(micros_since_midnight: i64, offset_seconds: i32) -> u64 {
unsafe { libduckdb_sys::duckdb_create_time_tz(micros_since_midnight, offset_seconds) }.bits
}
#[must_use]
pub unsafe fn time_tz_from_bits(bits: u64) -> TimeTz {
let raw = unsafe { duckdb_from_time_tz(duckdb_time_tz { bits }) };
TimeTz {
time: raw.time.into(),
offset_seconds: raw.offset,
}
}
#[must_use]
pub unsafe fn timestamp_from_micros(micros: i64) -> Timestamp {
let raw: duckdb_timestamp_struct =
unsafe { duckdb_from_timestamp(duckdb_timestamp { micros }) };
Timestamp {
date: raw.date.into(),
time: raw.time.into(),
}
}
#[must_use]
pub unsafe fn timestamp_to_micros(timestamp: Timestamp) -> i64 {
let raw = duckdb_timestamp_struct {
date: timestamp.date.into(),
time: timestamp.time.into(),
};
unsafe { duckdb_to_timestamp(raw) }.micros
}
#[must_use]
pub unsafe fn is_finite_timestamp(micros: i64) -> bool {
unsafe { duckdb_is_finite_timestamp(duckdb_timestamp { micros }) }
}
#[must_use]
pub unsafe fn is_finite_timestamp_s(seconds: i64) -> bool {
unsafe { duckdb_is_finite_timestamp_s(duckdb_timestamp_s { seconds }) }
}
#[must_use]
pub unsafe fn is_finite_timestamp_ms(millis: i64) -> bool {
unsafe { duckdb_is_finite_timestamp_ms(duckdb_timestamp_ms { millis }) }
}
#[must_use]
pub unsafe fn is_finite_timestamp_ns(nanos: i64) -> bool {
unsafe { duckdb_is_finite_timestamp_ns(duckdb_timestamp_ns { nanos }) }
}
#[must_use]
pub unsafe fn hugeint_to_f64(value: i128) -> f64 {
let raw = duckdb_hugeint {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
lower: value as u64,
#[allow(clippy::cast_possible_truncation)]
upper: (value >> 64) as i64,
};
unsafe { duckdb_hugeint_to_double(raw) }
}
#[must_use]
pub unsafe fn f64_to_hugeint(value: f64) -> i128 {
let raw = unsafe { duckdb_double_to_hugeint(value) };
(i128::from(raw.upper) << 64) | i128::from(raw.lower)
}
#[must_use]
pub unsafe fn uhugeint_to_f64(value: u128) -> f64 {
let raw = duckdb_uhugeint {
#[allow(clippy::cast_possible_truncation)]
lower: value as u64,
#[allow(clippy::cast_possible_truncation)]
upper: (value >> 64) as u64,
};
unsafe { duckdb_uhugeint_to_double(raw) }
}
#[must_use]
pub unsafe fn f64_to_uhugeint(value: f64) -> u128 {
let raw = unsafe { duckdb_double_to_uhugeint(value) };
(u128::from(raw.upper) << 64) | u128::from(raw.lower)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Decimal {
pub width: u8,
pub scale: u8,
pub value: i128,
}
#[must_use]
pub unsafe fn f64_to_decimal(value: f64, width: u8, scale: u8) -> Decimal {
let raw = unsafe { duckdb_double_to_decimal(value, width, scale) };
Decimal {
width: raw.width,
scale: raw.scale,
value: (i128::from(raw.value.upper) << 64) | i128::from(raw.value.lower),
}
}
#[must_use]
pub unsafe fn decimal_to_f64(decimal: Decimal) -> f64 {
let raw = duckdb_decimal {
width: decimal.width,
scale: decimal.scale,
value: duckdb_hugeint {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
lower: decimal.value as u64,
#[allow(clippy::cast_possible_truncation)]
upper: (decimal.value >> 64) as i64,
},
};
unsafe { duckdb_decimal_to_double(raw) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_struct_round_trips_through_ffi_types() {
let date = Date {
year: 2026,
month: 8,
day: 18,
};
let raw: duckdb_date_struct = date.into();
assert_eq!(raw.year, 2026);
assert_eq!(raw.month, 8);
assert_eq!(raw.day, 18);
assert_eq!(Date::from(raw), date);
}
#[test]
fn time_struct_round_trips_through_ffi_types() {
let time = Time {
hour: 23,
min: 59,
sec: 58,
micros: 123_456,
};
let raw: duckdb_time_struct = time.into();
assert_eq!(Time::from(raw), time);
}
#[test]
fn decimal_is_ordered_and_hashable() {
use std::collections::HashSet;
let a = Decimal {
width: 18,
scale: 3,
value: 1_500,
};
let b = Decimal {
width: 18,
scale: 3,
value: 2_500,
};
assert!(a < b);
let set: HashSet<Decimal> = [a, b, a].into_iter().collect();
assert_eq!(set.len(), 2);
}
}
#[cfg(all(test, feature = "_duckdb-testing"))]
mod live_tests {
use super::*;
use crate::testing::InMemoryDb;
#[test]
fn epoch_day_zero_is_1970_01_01() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
let date = unsafe { date_from_days(0) };
assert_eq!(
date,
Date {
year: 1970,
month: 1,
day: 1
}
);
}
#[test]
fn date_round_trips_across_leap_years_and_bce() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
for days in [
-1_000_000_i32,
-719_162, -1,
0,
1,
59, 10_957, 11_017, 20_685, 1_000_000,
] {
let date = unsafe { date_from_days(days) };
assert_eq!(unsafe { date_to_days(date) }, days, "round trip for {days}");
}
}
#[test]
fn duckdb_agrees_with_our_conversion() {
let db = InMemoryDb::open().expect("open in-memory DuckDB");
for days in [0_i32, 20_685, -719_162] {
let sql =
format!("SELECT strftime(DATE '1970-01-01' + INTERVAL ({days}) DAY, '%Y-%m-%d')");
let expected: String = db.query_one(&sql).expect("query");
let date = unsafe { date_from_days(days) };
let actual = format!("{:04}-{:02}-{:02}", date.year, date.month, date.day);
assert_eq!(actual, expected, "for {days} days since the epoch");
}
}
#[test]
fn infinity_sentinels_match_the_documented_constants() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
unsafe {
assert!(is_finite_date(0));
assert!(!is_finite_date(DATE_INFINITY_DAYS));
assert!(!is_finite_date(DATE_NEGATIVE_INFINITY_DAYS));
assert!(is_finite_date(i32::MIN));
assert!(is_finite_timestamp(0));
assert!(!is_finite_timestamp(TIMESTAMP_INFINITY_MICROS));
assert!(!is_finite_timestamp(TIMESTAMP_NEGATIVE_INFINITY_MICROS));
assert!(is_finite_timestamp(i64::MIN));
assert!(is_finite_timestamp_s(0));
assert!(is_finite_timestamp_ms(0));
assert!(is_finite_timestamp_ns(0));
assert!(!is_finite_timestamp_s(TIMESTAMP_INFINITY_MICROS));
assert!(!is_finite_timestamp_ms(TIMESTAMP_INFINITY_MICROS));
assert!(!is_finite_timestamp_ns(TIMESTAMP_INFINITY_MICROS));
}
}
#[test]
fn duckdb_sql_agrees_that_the_sentinels_are_infinite() {
let db = InMemoryDb::open().expect("open in-memory DuckDB");
let rendered: String = db
.query_one("SELECT ('infinity'::DATE)::VARCHAR")
.expect("query");
assert_eq!(rendered, "infinity");
assert!(!unsafe { is_finite_date(DATE_INFINITY_DAYS) });
}
#[test]
fn time_round_trips_including_microsecond_precision() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
for micros in [0_i64, 1, 999_999, 1_000_000, 86_399_999_999] {
let time = unsafe { time_from_micros(micros) };
assert_eq!(unsafe { time_to_micros(time) }, micros, "for {micros} us");
}
let end_of_day = unsafe { time_from_micros(86_399_999_999) };
assert_eq!(
end_of_day,
Time {
hour: 23,
min: 59,
sec: 59,
micros: 999_999
}
);
}
#[test]
fn timestamp_round_trips() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
for micros in [0_i64, 1, -1, 1_700_000_000_000_000, -1_700_000_000_000_000] {
let ts = unsafe { timestamp_from_micros(micros) };
assert_eq!(
unsafe { timestamp_to_micros(ts) },
micros,
"for {micros} us"
);
}
}
#[test]
fn time_tz_round_trips_with_offset() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
unsafe {
let bits = time_tz_bits(12 * 3_600 * 1_000_000, -5 * 3_600);
let decoded = time_tz_from_bits(bits);
assert_eq!(decoded.time.hour, 12);
assert_eq!(decoded.offset_seconds, -5 * 3_600);
}
}
#[test]
fn hugeint_conversions_match_duckdb() {
let db = InMemoryDb::open().expect("open in-memory DuckDB");
unsafe {
assert!((hugeint_to_f64(0) - 0.0).abs() < f64::EPSILON);
assert!((hugeint_to_f64(1) - 1.0).abs() < f64::EPSILON);
assert!((hugeint_to_f64(-1) + 1.0).abs() < f64::EPSILON);
assert_eq!(f64_to_hugeint(42.0), 42);
assert_eq!(f64_to_hugeint(-42.0), -42);
assert_eq!(f64_to_uhugeint(42.0), 42);
assert!((uhugeint_to_f64(u128::from(u64::MAX)) - 1.844_674_407_370_955e19).abs() < 1e6);
}
let expected: f64 = db
.query_one("SELECT (-170141183460469231731687303715884105728)::HUGEINT::DOUBLE")
.expect("query");
let actual = unsafe { hugeint_to_f64(i128::MIN) };
assert!(
(actual - expected).abs() / expected.abs() < 1e-12,
"{actual} != {expected}"
);
}
#[test]
fn decimal_conversions_preserve_width_and_scale() {
let _db = InMemoryDb::open().expect("open in-memory DuckDB");
unsafe {
let decimal = f64_to_decimal(12.345, 18, 3);
assert_eq!(decimal.width, 18);
assert_eq!(decimal.scale, 3);
assert_eq!(decimal.value, 12_345);
assert!((decimal_to_f64(decimal) - 12.345).abs() < 1e-9);
let negative = f64_to_decimal(-12.345, 18, 3);
assert_eq!(negative.value, -12_345);
assert!((decimal_to_f64(negative) + 12.345).abs() < 1e-9);
}
}
}