Dates, Times and Timestamps

VectorReader and VectorWriter move DuckDB's temporal types as the raw integers DuckDB stores:

SQL typeStorageAccessor
DATEi32 — days since 1970-01-01read_date / write_date
TIMEi64 — microseconds since midnightread_time / write_time
TIMETZpacked u64read_time_tz / write_time_tz
TIMESTAMPi64 — microseconds since the epochread_timestamp / write_timestamp
TIMESTAMPTZi64 — microseconds since the epoch, UTCread_timestamp_tz / write_timestamp_tz
TIMESTAMP_Si64 — secondsread_timestamp_s / write_timestamp_s
TIMESTAMP_MSi64 — millisecondsread_timestamp_ms / write_timestamp_ms
TIMESTAMP_NSi64 — nanosecondsread_timestamp_ns / write_timestamp_ns
INTERVAL{ months: i32, days: i32, micros: i64 }read_interval / write_interval

Turning those integers into year/month/day means implementing the proleptic Gregorian calendar, and getting it to agree with DuckDB's SQL semantics exactly rather than approximately. DuckDB already exposes the conversions, and they are in the stable prefix of the C API, so quack_rs::datetime wraps them rather than reimplementing anything.

Decomposing and composing

use quack_rs::datetime;

// DATE -> calendar date
let days = unsafe { reader.read_date(row) };
let date = unsafe { datetime::date_from_days(days) };
println!("{:04}-{:02}-{:02}", date.year, date.month, date.day);

// …and back
let days = unsafe { datetime::date_to_days(date) };
unsafe { writer.write_date(row, days) };

Time, TimeTz and Timestamp work the same way:

let ts = unsafe { datetime::timestamp_from_micros(reader.read_timestamp(row)) };
assert_eq!(ts.time.micros % 1_000, 0);   // ts.date and ts.time are plain structs

let micros = unsafe { datetime::timestamp_to_micros(ts) };

TIMETZ is a packed 64-bit value, not a plain integer — build and read it through the helpers rather than by hand:

let bits = unsafe { datetime::time_tz_bits(12 * 3_600 * 1_000_000, -5 * 3_600) };
unsafe { writer.write_time_tz(row, bits) };

let decoded = unsafe { datetime::time_tz_from_bits(reader.read_time_tz(row)) };
assert_eq!(decoded.offset_seconds, -5 * 3_600);

Infinity

DuckDB reserves two values of DATE and of TIMESTAMP for infinity and -infinity. Decomposing one into a calendar date is meaningless, so check first:

let days = unsafe { reader.read_date(row) };
if unsafe { datetime::is_finite_date(days) } {
    let date = unsafe { datetime::date_from_days(days) };
    // …
}

Note the exact values, which are easy to get wrong:

ConstantValue
DATE_INFINITY_DAYSi32::MAX
DATE_NEGATIVE_INFINITY_DAYS-i32::MAX
TIMESTAMP_INFINITY_MICROSi64::MAX
TIMESTAMP_NEGATIVE_INFINITY_MICROS-i64::MAX

Negative infinity is -i32::MAX, not i32::MIN. i32::MIN is an ordinary (if absurd) finite date, and treating it as infinity would silently drop real rows.

DECIMAL

DECIMAL is stored in the narrowest integer that fits its declared width, so the width has to travel with the value:

Declared widthPhysical storage
1 – 4i16
5 – 9i32
10 – 18i64
19 – 38i128

read_decimal / write_decimal take the width and pick the right one. Get it from the column's LogicalType:

let logical = unsafe { quack_rs::vector::vector_get_column_type(vec) };
let width = unsafe { logical.decimal_width() };
let scale = unsafe { logical.decimal_scale() };

let unscaled = unsafe { reader.read_decimal(row, width) };
// The represented number is unscaled / 10^scale.
unsafe { writer.write_decimal(row, width, unscaled * 2) };

datetime::f64_to_decimal and datetime::decimal_to_f64 convert through DuckDB's own routines when a floating-point view is what you want.

Wide integers

HUGEINT is { lower: u64, upper: i64 } and UHUGEINT is two u64s. read_i128 / write_i128 and read_u128 / write_u128 handle the halves; datetime::hugeint_to_f64 and friends match DuckDB's own conversion behaviour including its rounding.