1use libduckdb_sys::{
49 duckdb_date, duckdb_date_struct, duckdb_decimal, duckdb_decimal_to_double,
50 duckdb_double_to_decimal, duckdb_double_to_hugeint, duckdb_double_to_uhugeint,
51 duckdb_from_date, duckdb_from_time, duckdb_from_time_tz, duckdb_from_timestamp, duckdb_hugeint,
52 duckdb_hugeint_to_double, duckdb_is_finite_date, duckdb_is_finite_timestamp,
53 duckdb_is_finite_timestamp_ms, duckdb_is_finite_timestamp_ns, duckdb_is_finite_timestamp_s,
54 duckdb_time, duckdb_time_struct, duckdb_time_tz, duckdb_timestamp, duckdb_timestamp_ms,
55 duckdb_timestamp_ns, duckdb_timestamp_s, duckdb_timestamp_struct, duckdb_to_date,
56 duckdb_to_time, duckdb_to_timestamp, duckdb_uhugeint, duckdb_uhugeint_to_double,
57};
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub struct Date {
64 pub year: i32,
66 pub month: i8,
68 pub day: i8,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74pub struct Time {
75 pub hour: i8,
77 pub min: i8,
79 pub sec: i8,
81 pub micros: i32,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
87pub struct TimeTz {
88 pub time: Time,
90 pub offset_seconds: i32,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct Timestamp {
97 pub date: Date,
99 pub time: Time,
101}
102
103impl From<duckdb_date_struct> for Date {
104 fn from(value: duckdb_date_struct) -> Self {
105 Self {
106 year: value.year,
107 month: value.month,
108 day: value.day,
109 }
110 }
111}
112
113impl From<Date> for duckdb_date_struct {
114 fn from(value: Date) -> Self {
115 Self {
116 year: value.year,
117 month: value.month,
118 day: value.day,
119 }
120 }
121}
122
123impl From<duckdb_time_struct> for Time {
124 fn from(value: duckdb_time_struct) -> Self {
125 Self {
126 hour: value.hour,
127 min: value.min,
128 sec: value.sec,
129 micros: value.micros,
130 }
131 }
132}
133
134impl From<Time> for duckdb_time_struct {
135 fn from(value: Time) -> Self {
136 Self {
137 hour: value.hour,
138 min: value.min,
139 sec: value.sec,
140 micros: value.micros,
141 }
142 }
143}
144
145pub const DATE_INFINITY_DAYS: i32 = i32::MAX;
149
150pub const DATE_NEGATIVE_INFINITY_DAYS: i32 = -i32::MAX;
155
156pub const TIMESTAMP_INFINITY_MICROS: i64 = i64::MAX;
161
162pub const TIMESTAMP_NEGATIVE_INFINITY_MICROS: i64 = -i64::MAX;
167
168#[must_use]
180pub unsafe fn date_from_days(days: i32) -> Date {
181 unsafe { duckdb_from_date(duckdb_date { days }) }.into()
183}
184
185#[must_use]
191pub unsafe fn date_to_days(date: Date) -> i32 {
192 unsafe { duckdb_to_date(date.into()) }.days
194}
195
196#[must_use]
202pub unsafe fn is_finite_date(days: i32) -> bool {
203 unsafe { duckdb_is_finite_date(duckdb_date { days }) }
205}
206
207#[must_use]
215pub unsafe fn time_from_micros(micros: i64) -> Time {
216 unsafe { duckdb_from_time(duckdb_time { micros }) }.into()
218}
219
220#[must_use]
226pub unsafe fn time_to_micros(time: Time) -> i64 {
227 unsafe { duckdb_to_time(time.into()) }.micros
229}
230
231#[must_use]
240pub unsafe fn time_tz_bits(micros_since_midnight: i64, offset_seconds: i32) -> u64 {
241 unsafe { libduckdb_sys::duckdb_create_time_tz(micros_since_midnight, offset_seconds) }.bits
243}
244
245#[must_use]
251pub unsafe fn time_tz_from_bits(bits: u64) -> TimeTz {
252 let raw = unsafe { duckdb_from_time_tz(duckdb_time_tz { bits }) };
254 TimeTz {
255 time: raw.time.into(),
256 offset_seconds: raw.offset,
257 }
258}
259
260#[must_use]
270pub unsafe fn timestamp_from_micros(micros: i64) -> Timestamp {
271 let raw: duckdb_timestamp_struct =
273 unsafe { duckdb_from_timestamp(duckdb_timestamp { micros }) };
274 Timestamp {
275 date: raw.date.into(),
276 time: raw.time.into(),
277 }
278}
279
280#[must_use]
286pub unsafe fn timestamp_to_micros(timestamp: Timestamp) -> i64 {
287 let raw = duckdb_timestamp_struct {
288 date: timestamp.date.into(),
289 time: timestamp.time.into(),
290 };
291 unsafe { duckdb_to_timestamp(raw) }.micros
293}
294
295#[must_use]
302pub unsafe fn is_finite_timestamp(micros: i64) -> bool {
303 unsafe { duckdb_is_finite_timestamp(duckdb_timestamp { micros }) }
305}
306
307#[must_use]
313pub unsafe fn is_finite_timestamp_s(seconds: i64) -> bool {
314 unsafe { duckdb_is_finite_timestamp_s(duckdb_timestamp_s { seconds }) }
316}
317
318#[must_use]
324pub unsafe fn is_finite_timestamp_ms(millis: i64) -> bool {
325 unsafe { duckdb_is_finite_timestamp_ms(duckdb_timestamp_ms { millis }) }
327}
328
329#[must_use]
335pub unsafe fn is_finite_timestamp_ns(nanos: i64) -> bool {
336 unsafe { duckdb_is_finite_timestamp_ns(duckdb_timestamp_ns { nanos }) }
338}
339
340#[must_use]
348pub unsafe fn hugeint_to_f64(value: i128) -> f64 {
349 let raw = duckdb_hugeint {
350 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
351 lower: value as u64,
352 #[allow(clippy::cast_possible_truncation)]
353 upper: (value >> 64) as i64,
354 };
355 unsafe { duckdb_hugeint_to_double(raw) }
357}
358
359#[must_use]
365pub unsafe fn f64_to_hugeint(value: f64) -> i128 {
366 let raw = unsafe { duckdb_double_to_hugeint(value) };
368 (i128::from(raw.upper) << 64) | i128::from(raw.lower)
369}
370
371#[must_use]
377pub unsafe fn uhugeint_to_f64(value: u128) -> f64 {
378 let raw = duckdb_uhugeint {
379 #[allow(clippy::cast_possible_truncation)]
380 lower: value as u64,
381 #[allow(clippy::cast_possible_truncation)]
382 upper: (value >> 64) as u64,
383 };
384 unsafe { duckdb_uhugeint_to_double(raw) }
386}
387
388#[must_use]
394pub unsafe fn f64_to_uhugeint(value: f64) -> u128 {
395 let raw = unsafe { duckdb_double_to_uhugeint(value) };
397 (u128::from(raw.upper) << 64) | u128::from(raw.lower)
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
402pub struct Decimal {
403 pub width: u8,
405 pub scale: u8,
407 pub value: i128,
409}
410
411#[must_use]
417pub unsafe fn f64_to_decimal(value: f64, width: u8, scale: u8) -> Decimal {
418 let raw = unsafe { duckdb_double_to_decimal(value, width, scale) };
420 Decimal {
421 width: raw.width,
422 scale: raw.scale,
423 value: (i128::from(raw.value.upper) << 64) | i128::from(raw.value.lower),
424 }
425}
426
427#[must_use]
433pub unsafe fn decimal_to_f64(decimal: Decimal) -> f64 {
434 let raw = duckdb_decimal {
435 width: decimal.width,
436 scale: decimal.scale,
437 value: duckdb_hugeint {
438 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
439 lower: decimal.value as u64,
440 #[allow(clippy::cast_possible_truncation)]
441 upper: (decimal.value >> 64) as i64,
442 },
443 };
444 unsafe { duckdb_decimal_to_double(raw) }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn date_struct_round_trips_through_ffi_types() {
454 let date = Date {
455 year: 2026,
456 month: 8,
457 day: 18,
458 };
459 let raw: duckdb_date_struct = date.into();
460 assert_eq!(raw.year, 2026);
461 assert_eq!(raw.month, 8);
462 assert_eq!(raw.day, 18);
463 assert_eq!(Date::from(raw), date);
464 }
465
466 #[test]
467 fn time_struct_round_trips_through_ffi_types() {
468 let time = Time {
469 hour: 23,
470 min: 59,
471 sec: 58,
472 micros: 123_456,
473 };
474 let raw: duckdb_time_struct = time.into();
475 assert_eq!(Time::from(raw), time);
476 }
477
478 #[test]
479 fn decimal_is_ordered_and_hashable() {
480 use std::collections::HashSet;
481 let a = Decimal {
482 width: 18,
483 scale: 3,
484 value: 1_500,
485 };
486 let b = Decimal {
487 width: 18,
488 scale: 3,
489 value: 2_500,
490 };
491 assert!(a < b);
492 let set: HashSet<Decimal> = [a, b, a].into_iter().collect();
493 assert_eq!(set.len(), 2);
494 }
495}
496
497#[cfg(all(test, feature = "_duckdb-testing"))]
499mod live_tests {
500 use super::*;
501 use crate::testing::InMemoryDb;
502
503 #[test]
504 fn epoch_day_zero_is_1970_01_01() {
505 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
506 let date = unsafe { date_from_days(0) };
508 assert_eq!(
509 date,
510 Date {
511 year: 1970,
512 month: 1,
513 day: 1
514 }
515 );
516 }
517
518 #[test]
519 fn date_round_trips_across_leap_years_and_bce() {
520 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
521 for days in [
522 -1_000_000_i32,
523 -719_162, -1,
525 0,
526 1,
527 59, 10_957, 11_017, 20_685, 1_000_000,
532 ] {
533 let date = unsafe { date_from_days(days) };
535 assert_eq!(unsafe { date_to_days(date) }, days, "round trip for {days}");
536 }
537 }
538
539 #[test]
540 fn duckdb_agrees_with_our_conversion() {
541 let db = InMemoryDb::open().expect("open in-memory DuckDB");
544 for days in [0_i32, 20_685, -719_162] {
545 let sql =
548 format!("SELECT strftime(DATE '1970-01-01' + INTERVAL ({days}) DAY, '%Y-%m-%d')");
549 let expected: String = db.query_one(&sql).expect("query");
550 let date = unsafe { date_from_days(days) };
552 let actual = format!("{:04}-{:02}-{:02}", date.year, date.month, date.day);
553 assert_eq!(actual, expected, "for {days} days since the epoch");
554 }
555 }
556
557 #[test]
558 fn infinity_sentinels_match_the_documented_constants() {
559 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
560 unsafe {
562 assert!(is_finite_date(0));
563 assert!(!is_finite_date(DATE_INFINITY_DAYS));
564 assert!(!is_finite_date(DATE_NEGATIVE_INFINITY_DAYS));
565 assert!(is_finite_date(i32::MIN));
569
570 assert!(is_finite_timestamp(0));
571 assert!(!is_finite_timestamp(TIMESTAMP_INFINITY_MICROS));
572 assert!(!is_finite_timestamp(TIMESTAMP_NEGATIVE_INFINITY_MICROS));
573 assert!(is_finite_timestamp(i64::MIN));
574
575 assert!(is_finite_timestamp_s(0));
576 assert!(is_finite_timestamp_ms(0));
577 assert!(is_finite_timestamp_ns(0));
578 assert!(!is_finite_timestamp_s(TIMESTAMP_INFINITY_MICROS));
579 assert!(!is_finite_timestamp_ms(TIMESTAMP_INFINITY_MICROS));
580 assert!(!is_finite_timestamp_ns(TIMESTAMP_INFINITY_MICROS));
581 }
582 }
583
584 #[test]
585 fn duckdb_sql_agrees_that_the_sentinels_are_infinite() {
586 let db = InMemoryDb::open().expect("open in-memory DuckDB");
587 let rendered: String = db
588 .query_one("SELECT ('infinity'::DATE)::VARCHAR")
589 .expect("query");
590 assert_eq!(rendered, "infinity");
591 assert!(!unsafe { is_finite_date(DATE_INFINITY_DAYS) });
593 }
594
595 #[test]
596 fn time_round_trips_including_microsecond_precision() {
597 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
598 for micros in [0_i64, 1, 999_999, 1_000_000, 86_399_999_999] {
599 let time = unsafe { time_from_micros(micros) };
601 assert_eq!(unsafe { time_to_micros(time) }, micros, "for {micros} us");
602 }
603 let end_of_day = unsafe { time_from_micros(86_399_999_999) };
605 assert_eq!(
606 end_of_day,
607 Time {
608 hour: 23,
609 min: 59,
610 sec: 59,
611 micros: 999_999
612 }
613 );
614 }
615
616 #[test]
617 fn timestamp_round_trips() {
618 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
619 for micros in [0_i64, 1, -1, 1_700_000_000_000_000, -1_700_000_000_000_000] {
620 let ts = unsafe { timestamp_from_micros(micros) };
622 assert_eq!(
623 unsafe { timestamp_to_micros(ts) },
624 micros,
625 "for {micros} us"
626 );
627 }
628 }
629
630 #[test]
631 fn time_tz_round_trips_with_offset() {
632 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
633 unsafe {
635 let bits = time_tz_bits(12 * 3_600 * 1_000_000, -5 * 3_600);
636 let decoded = time_tz_from_bits(bits);
637 assert_eq!(decoded.time.hour, 12);
638 assert_eq!(decoded.offset_seconds, -5 * 3_600);
639 }
640 }
641
642 #[test]
643 fn hugeint_conversions_match_duckdb() {
644 let db = InMemoryDb::open().expect("open in-memory DuckDB");
645 unsafe {
647 assert!((hugeint_to_f64(0) - 0.0).abs() < f64::EPSILON);
648 assert!((hugeint_to_f64(1) - 1.0).abs() < f64::EPSILON);
649 assert!((hugeint_to_f64(-1) + 1.0).abs() < f64::EPSILON);
650 assert_eq!(f64_to_hugeint(42.0), 42);
651 assert_eq!(f64_to_hugeint(-42.0), -42);
652 assert_eq!(f64_to_uhugeint(42.0), 42);
653 assert!((uhugeint_to_f64(u128::from(u64::MAX)) - 1.844_674_407_370_955e19).abs() < 1e6);
654 }
655 let expected: f64 = db
657 .query_one("SELECT (-170141183460469231731687303715884105728)::HUGEINT::DOUBLE")
658 .expect("query");
659 let actual = unsafe { hugeint_to_f64(i128::MIN) };
661 assert!(
662 (actual - expected).abs() / expected.abs() < 1e-12,
663 "{actual} != {expected}"
664 );
665 }
666
667 #[test]
668 fn decimal_conversions_preserve_width_and_scale() {
669 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
670 unsafe {
672 let decimal = f64_to_decimal(12.345, 18, 3);
673 assert_eq!(decimal.width, 18);
674 assert_eq!(decimal.scale, 3);
675 assert_eq!(decimal.value, 12_345);
676 assert!((decimal_to_f64(decimal) - 12.345).abs() < 1e-9);
677
678 let negative = f64_to_decimal(-12.345, 18, 3);
679 assert_eq!(negative.value, -12_345);
680 assert!((decimal_to_f64(negative) + 12.345).abs() < 1e-9);
681 }
682 }
683}