diff --git a/Cargo.lock b/Cargo.lock index 06a3fff7..ae4637b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,7 @@ dependencies = [ "fallible-iterator", "fallible-streaming-iterator", "hashlink", + "jiff", "libduckdb-sys", "num", "num-integer", @@ -1644,6 +1645,47 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2642,6 +2684,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 21d8e1e6..88d9f268 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ fallible-iterator = "0.3" fallible-streaming-iterator = "0.1" flate2 = "1.0" hashlink = "0.10" +jiff = "0.2.15" num = { version = "0.4", default-features = false } num-integer = "0.1.46" pkg-config = "0.3.24" diff --git a/crates/duckdb/Cargo.toml b/crates/duckdb/Cargo.toml index 21dbf88a..7c437c5a 100644 --- a/crates/duckdb/Cargo.toml +++ b/crates/duckdb/Cargo.toml @@ -45,6 +45,7 @@ duckdb-loadable-macros = { workspace = true, optional = true } fallible-iterator = { workspace = true } fallible-streaming-iterator = { workspace = true } hashlink = { workspace = true } +jiff = { workspace = true, optional = true } libduckdb-sys = { workspace = true } num = { workspace = true, features = ["std"], optional = true } num-integer = { workspace = true } diff --git a/crates/duckdb/src/types/chrono.rs b/crates/duckdb/src/types/chrono.rs deleted file mode 100644 index 122f890b..00000000 --- a/crates/duckdb/src/types/chrono.rs +++ /dev/null @@ -1,363 +0,0 @@ -//! Convert most of the [Time Strings](https://duckdb.org/docs/stable/sql/functions/date) to chrono types. - -use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; -use num_integer::Integer; - -use crate::{ - types::{FromSql, FromSqlError, FromSqlResult, TimeUnit, ToSql, ToSqlOutput, ValueRef}, - Result, -}; - -use super::Value; - -/// ISO 8601 calendar date without timezone => "YYYY-MM-DD" -impl ToSql for NaiveDate { - #[inline] - fn to_sql(&self) -> Result> { - let date_str = self.format("%F").to_string(); - Ok(ToSqlOutput::from(date_str)) - } -} - -/// "YYYY-MM-DD" => ISO 8601 calendar date without timezone. -impl FromSql for NaiveDate { - #[inline] - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - Ok(NaiveDateTime::column_result(value)?.date()) - } -} - -/// ISO 8601 time without timezone => "HH:MM:SS.SSS" -impl ToSql for NaiveTime { - #[inline] - fn to_sql(&self) -> Result> { - let date_str = self.format("%T%.f").to_string(); - Ok(ToSqlOutput::from(date_str)) - } -} - -/// "HH:MM"/"HH:MM:SS"/"HH:MM:SS.SSS" => ISO 8601 time without timezone. -impl FromSql for NaiveTime { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - Ok(NaiveDateTime::column_result(value)?.time()) - } -} - -/// ISO 8601 combined date and time without timezone => -/// "YYYY-MM-DD HH:MM:SS.SSS" -impl ToSql for NaiveDateTime { - #[inline] - fn to_sql(&self) -> Result> { - let date_str = self.format("%F %T%.f").to_string(); - Ok(ToSqlOutput::from(date_str)) - } -} - -/// "YYYY-MM-DD HH:MM:SS"/"YYYY-MM-DD HH:MM:SS.SSS" => ISO 8601 combined date -/// and time without timezone. ("YYYY-MM-DDTHH:MM:SS"/"YYYY-MM-DDTHH:MM:SS.SSS" -/// also supported) -impl FromSql for NaiveDateTime { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Timestamp(tu, t) => { - let (secs, nsecs) = match tu { - TimeUnit::Second => (t, 0), - TimeUnit::Millisecond => (t / 1000, (t % 1000) * 1_000_000), - TimeUnit::Microsecond => (t / 1_000_000, (t % 1_000_000) * 1000), - TimeUnit::Nanosecond => (t / 1_000_000_000, t % 1_000_000_000), - }; - Ok(DateTime::from_timestamp(secs, nsecs as u32).unwrap().naive_utc()) - } - ValueRef::Date32(d) => Ok(DateTime::from_timestamp(24 * 3600 * (d as i64), 0).unwrap().naive_utc()), - ValueRef::Time64(TimeUnit::Microsecond, d) => Ok(DateTime::from_timestamp( - d / 1_000_000, - ((d % 1_000_000) * 1_000) as u32, - ) - .unwrap() - .naive_utc()), - ValueRef::Text(s) => { - let mut s = std::str::from_utf8(s).unwrap(); - let format = match s.len() { - //23:56:04 - 8 => "%T", - //2016-02-23 - 10 => "%F", - //13:38:47.144 - 12 => "%T%.f", - //2016-02-23 23:56:04 - 19 => "%F %T", - //2016-02-23 23:56:04.789 - 23 => "%F %T%.f", - //2016-02-23 23:56:04.789+00:00 - 29 => "%F %T%.f%:z", - _ => { - //2016-02-23 - s = &s[..10]; - "%F" - } - }; - Self::parse_from_str(s, format).map_err(|err| FromSqlError::Other(Box::new(err))) - } - _ => Err(FromSqlError::InvalidType), - } - } -} - -/// Date and time with time zone => UTC RFC3339 timestamp -/// ("YYYY-MM-DD HH:MM:SS.SSS+00:00"). -impl ToSql for DateTime { - #[inline] - fn to_sql(&self) -> Result> { - let date_str = self.with_timezone(&Utc).format("%F %T%.f%:z").to_string(); - Ok(ToSqlOutput::from(date_str)) - } -} - -/// RFC3339 ("YYYY-MM-DD HH:MM:SS.SSS[+-]HH:MM") into `DateTime`. -impl FromSql for DateTime { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - NaiveDateTime::column_result(value).map(|dt| Utc.from_utc_datetime(&dt)) - } -} - -/// RFC3339 ("YYYY-MM-DD HH:MM:SS.SSS[+-]HH:MM") into `DateTime`. -impl FromSql for DateTime { - #[inline] - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - let utc_dt = DateTime::::column_result(value)?; - Ok(utc_dt.with_timezone(&Local)) - } -} - -impl FromSql for Duration { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Interval { months, days, nanos } => { - let days = days + (months * 30); - let (additional_seconds, nanos) = nanos.div_mod_floor(&NANOS_PER_SECOND); - let seconds = additional_seconds + (i64::from(days) * 24 * 3600); - - match nanos.try_into() { - Ok(nanos) => { - if let Some(duration) = Self::new(seconds, nanos) { - Ok(duration) - } else { - Err(FromSqlError::Other("Invalid duration".into())) - } - } - Err(err) => Err(FromSqlError::Other(format!("Invalid duration: {err}").into())), - } - } - _ => Err(FromSqlError::InvalidType), - } - } -} - -const DAYS_PER_MONTH: i64 = 30; -const SECONDS_PER_DAY: i64 = 24 * 3600; -const NANOS_PER_SECOND: i64 = 1_000_000_000; -const NANOS_PER_DAY: i64 = SECONDS_PER_DAY * NANOS_PER_SECOND; - -impl ToSql for Duration { - fn to_sql(&self) -> Result> { - let nanos = self.num_nanoseconds().unwrap(); - let (days, nanos) = nanos.div_mod_floor(&NANOS_PER_DAY); - let (months, days) = days.div_mod_floor(&DAYS_PER_MONTH); - Ok(ToSqlOutput::Owned(Value::Interval { - months: months.try_into().unwrap(), - days: days.try_into().unwrap(), - nanos, - })) - } -} - -#[cfg(test)] -mod test { - use crate::{ - types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef}, - Connection, Result, - }; - use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Utc}; - - fn checked_memory_handle() -> Result { - let db = Connection::open_in_memory()?; - db.execute_batch("CREATE TABLE foo (d DATE, t Text, i INTEGER, f FLOAT, b TIMESTAMP, tt time)")?; - Ok(db) - } - - #[test] - fn test_naive_time() -> Result<()> { - let db = checked_memory_handle()?; - let time = NaiveTime::from_hms_micro_opt(23, 56, 4, 12_345).unwrap(); - db.execute("INSERT INTO foo (tt) VALUES (?)", [time])?; - - let s: String = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; - assert_eq!("23:56:04.012345", s); - let t: NaiveTime = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; - assert_eq!(time, t); - Ok(()) - } - - #[test] - fn test_naive_date() -> Result<()> { - let db = checked_memory_handle()?; - let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); - db.execute("INSERT INTO foo (d) VALUES (?)", [date])?; - - let s: String = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; - assert_eq!("2016-02-23", s); - let t: NaiveDate = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; - assert_eq!(date, t); - Ok(()) - } - - #[test] - fn test_naive_date_time() -> Result<()> { - let db = checked_memory_handle()?; - let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); - let time = NaiveTime::from_hms_opt(23, 56, 4).unwrap(); - let dt = NaiveDateTime::new(date, time); - - db.execute("INSERT INTO foo (b) VALUES (?)", [dt])?; - - let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!("2016-02-23 23:56:04", s); - let v: NaiveDateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!(dt, v); - - db.execute( - "UPDATE foo set b = strftime(cast(b as datetime), '%Y-%m-%d %H:%M:%S')", - [], - )?; // "YYYY-MM-DD HH:MM:SS" - let hms: NaiveDateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!(dt, hms); - Ok(()) - } - - #[test] - fn test_date_time_utc() -> Result<()> { - let db = checked_memory_handle()?; - let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); - let time = NaiveTime::from_hms_milli_opt(23, 56, 4, 789).unwrap(); - let dt = NaiveDateTime::new(date, time); - let utc = Utc.from_utc_datetime(&dt); - - db.execute("INSERT INTO foo (b) VALUES (?)", [utc])?; - - let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!("2016-02-23 23:56:04.789", s); - - let v1: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!(utc, v1); - - let v2: DateTime = db.query_row("SELECT '2016-02-23 23:56:04.789'", [], |r| r.get(0))?; - assert_eq!(utc, v2); - - let v3: DateTime = db.query_row("SELECT '2016-02-23 23:56:04'", [], |r| r.get(0))?; - assert_eq!(utc - Duration::try_milliseconds(789).unwrap(), v3); - - let v4: DateTime = db.query_row("SELECT '2016-02-23 23:56:04.789+00:00'", [], |r| r.get(0))?; - assert_eq!(utc, v4); - Ok(()) - } - - #[test] - fn test_time_delta_roundtrip() { - roundtrip_type(TimeDelta::new(3600, 0).unwrap()); - roundtrip_type(TimeDelta::new(3600, 1000).unwrap()); - } - - #[test] - fn test_time_delta() -> Result<()> { - let db = checked_memory_handle()?; - let td = TimeDelta::new(3600, 0).unwrap(); - - let row: Result = db.query_row("SELECT ?", [td], |row| Ok(row.get(0)))?; - - assert_eq!(row.unwrap(), td); - - Ok(()) - } - - fn roundtrip_type(td: T) { - let sqled = td.to_sql().unwrap(); - let value = match sqled { - ToSqlOutput::Borrowed(v) => v, - ToSqlOutput::Owned(ref v) => ValueRef::from(v), - }; - let reversed = FromSql::column_result(value).unwrap(); - - assert_eq!(td, reversed); - } - - #[test] - fn test_date_time_local() -> Result<()> { - let db = checked_memory_handle()?; - let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); - let time = NaiveTime::from_hms_milli_opt(23, 56, 4, 789).unwrap(); - let dt = NaiveDateTime::new(date, time); - let local = Local.from_local_datetime(&dt).single().unwrap(); - - db.execute("INSERT INTO foo (b) VALUES (?)", [local])?; - - let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!(DateTime::::from(local).format("%F %T%.f").to_string(), s); - - let v: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; - assert_eq!(local, v); - Ok(()) - } - - #[test] - fn test_duckdb_datetime_functions() -> Result<()> { - let db = checked_memory_handle()?; - let result: Result = db.query_row("SELECT CURRENT_DATE", [], |r| r.get(0)); - assert!(result.is_ok()); - let result: Result = db.query_row("SELECT CURRENT_TIMESTAMP", [], |r| r.get(0)); - assert!(result.is_ok()); - let result: Result> = db.query_row("SELECT CURRENT_TIMESTAMP", [], |r| r.get(0)); - assert!(result.is_ok()); - let result: Result = db.query_row("SELECT CURRENT_TIME", [], |r| r.get(0)); - assert!(result.is_ok()); - Ok(()) - } - - #[test] - fn test_naive_date_time_param() -> Result<()> { - let db = checked_memory_handle()?; - let fixed_time = NaiveDateTime::parse_from_str("2023-01-01 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); - let result: Result = db.query_row( - "SELECT 1 WHERE ?::TIMESTAMP BETWEEN (TIMESTAMP '2023-01-01 11:59:00') AND (TIMESTAMP '2023-01-01 12:01:00')", - [fixed_time], - |r| r.get(0), - ); - assert!(result.is_ok()); - Ok(()) - } - - #[test] - fn test_date_time_param() -> Result<()> { - let db = checked_memory_handle()?; - let fixed_time = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap(); - let result: Result = db.query_row( - "SELECT 1 WHERE ?::TIMESTAMPTZ BETWEEN (TIMESTAMPTZ '2023-01-01 11:59:00+00:00') AND (TIMESTAMPTZ '2023-01-01 12:01:00+00:00')", - [fixed_time], - |r| r.get(0), - ); - assert!(result.is_ok()); - Ok(()) - } - - #[test] - fn test_lenient_parse_timezone() { - // Not supported - assert!(matches!( - DateTime::::column_result(ValueRef::Text(b"1970-01-01T00:00:00Z")), - Err(FromSqlError::Other(_)) - )); - assert!(matches!( - DateTime::::column_result(ValueRef::Text(b"1970-01-01T00:00:00+00")), - Err(FromSqlError::Other(_)) - )); - } -} diff --git a/crates/duckdb/src/types/from_sql.rs b/crates/duckdb/src/types/from_sql.rs index 4065243d..97470eba 100644 --- a/crates/duckdb/src/types/from_sql.rs +++ b/crates/duckdb/src/types/from_sql.rs @@ -194,12 +194,20 @@ impl FromSql for String { match value { #[cfg(feature = "chrono")] ValueRef::Date32(_) => Ok(chrono::NaiveDate::column_result(value)?.format("%F").to_string()), + #[cfg(all(not(feature = "chrono"), feature = "jiff"))] + ValueRef::Date32(_) => Ok(jiff::civil::Date::column_result(value)?.strftime("%F").to_string()), #[cfg(feature = "chrono")] ValueRef::Time64(..) => Ok(chrono::NaiveTime::column_result(value)?.format("%T%.f").to_string()), + #[cfg(all(not(feature = "chrono"), feature = "jiff"))] + ValueRef::Time64(..) => Ok(jiff::civil::Time::column_result(value)?.strftime("%T%.f").to_string()), #[cfg(feature = "chrono")] ValueRef::Timestamp(..) => Ok(chrono::NaiveDateTime::column_result(value)? .format("%F %T%.f") .to_string()), + #[cfg(all(not(feature = "chrono"), feature = "jiff"))] + ValueRef::Timestamp(..) => Ok(jiff::civil::DateTime::column_result(value)? + .strftime("%F %T%.f") + .to_string()), _ => value.as_str().map(ToString::to_string), } } diff --git a/crates/duckdb/src/types/mod.rs b/crates/duckdb/src/types/mod.rs index 427831b3..ab354884 100644 --- a/crates/duckdb/src/types/mod.rs +++ b/crates/duckdb/src/types/mod.rs @@ -14,11 +14,11 @@ pub use self::{ use arrow::datatypes::DataType; use std::fmt; -#[cfg(feature = "chrono")] -mod chrono; mod from_sql; #[cfg(feature = "serde_json")] mod serde_json; +#[cfg(any(feature = "chrono", feature = "jiff"))] +mod time_libraries; mod to_sql; #[cfg(feature = "url")] mod url; diff --git a/crates/duckdb/src/types/time_libraries.rs b/crates/duckdb/src/types/time_libraries.rs new file mode 100644 index 00000000..f09376a8 --- /dev/null +++ b/crates/duckdb/src/types/time_libraries.rs @@ -0,0 +1,894 @@ +//! Convert most of the [Time Strings](https://duckdb.org/docs/stable/sql/functions/date) to chrono types. + +#[cfg(feature = "chrono")] +use chrono::{Local, TimeZone, Utc}; +use num_integer::Integer; + +use crate::{ + types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}, + Result, +}; + +macro_rules! iso_8601_datetime_format { + ($s:expr) => {{ + match $s.len() { + //2016-02-23 23:56:04 + 19 => ($s, "%F %T"), + //2016-02-23 23:56:04.789 + 23 => ($s, "%F %T%.f"), + //2016-02-23 23:56:04.789+00:00 + _ => ($s, "%F %T%.f%:z"), + } + }}; +} + +macro_rules! iso_8601_date_format { + ($s:expr) => {{ + match $s.len() { + //2016-02-23 + 10 => ($s, "%F"), + _ => { + //2016-02-23 + (&$s[..10], "%F") + } + } + }}; +} + +macro_rules! iso_8601_time_format { + ($s:expr) => {{ + match $s.len() { + //23:56 + 5 => ($s, "%H:%M"), + //23:56:04 + 8 => ($s, "%T"), + //13:38:47.144 + _ => ($s, "%T%.f"), + } + }}; +} + +/// ISO 8601 calendar date without timezone => "YYYY-MM-DD" +#[cfg(feature = "chrono")] +impl ToSql for chrono::NaiveDate { + #[inline] + fn to_sql(&self) -> Result> { + let date_str = self.format("%F").to_string(); + Ok(ToSqlOutput::from(date_str)) + } +} + +/// ISO 8601 calendar date without timezone => "YYYY-MM-DD" +#[cfg(feature = "jiff")] +impl ToSql for jiff::civil::Date { + #[inline] + fn to_sql(&self) -> Result> { + let date_str = self.strftime("%F").to_string(); + Ok(ToSqlOutput::from(date_str)) + } +} + +/// "YYYY-MM-DD" => ISO 8601 calendar date without timezone. +#[cfg(feature = "chrono")] +impl FromSql for chrono::NaiveDate { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if let ValueRef::Date32(days) = value { + return Ok(Self::from_epoch_days(days).unwrap()); + } + + if value.as_timestamp().is_ok() { + return chrono::NaiveDateTime::column_result(value).map(|dt| dt.date()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_date_format!(s); + return chrono::NaiveDate::parse_from_str(s, format).map_err(|e| FromSqlError::Other(e.into())); + } + + Err(FromSqlError::InvalidType) + } +} + +/// "YYYY-MM-DD" => ISO 8601 calendar date without timezone. +#[cfg(feature = "jiff")] +impl FromSql for jiff::civil::Date { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if let ValueRef::Date32(days) = value { + return jiff::civil::date(1970, 1, 1) + .checked_add(jiff::Span::new().days(days)) + .map_err(|e| FromSqlError::Other(e.into())); + } + + if value.as_timestamp().is_ok() { + return Ok(jiff::Zoned::column_result(value)?.date()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_date_format!(s); + return jiff::civil::Date::strptime(format, s).map_err(|e| FromSqlError::Other(e.into())); + } + + Err(FromSqlError::InvalidType) + } +} + +/// ISO 8601 time without timezone => "HH:MM:SS.SSS" +#[cfg(feature = "chrono")] +impl ToSql for chrono::NaiveTime { + #[inline] + fn to_sql(&self) -> Result> { + let time_str = self.format("%T%.f").to_string(); + Ok(ToSqlOutput::from(time_str)) + } +} + +/// ISO 8601 time without timezone => "HH:MM:SS.SSS" +#[cfg(feature = "jiff")] +impl ToSql for jiff::civil::Time { + #[inline] + fn to_sql(&self) -> Result> { + let time_str = self.strftime("%T%.f").to_string(); + Ok(ToSqlOutput::from(time_str)) + } +} + +/// "HH:MM"/"HH:MM:SS"/"HH:MM:SS.SSS" => ISO 8601 time without timezone. +#[cfg(feature = "chrono")] +impl FromSql for chrono::NaiveTime { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if value.as_timestamp().is_ok() { + return Ok(chrono::NaiveDateTime::column_result(value)?.time()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_time_format!(s); + return chrono::NaiveTime::parse_from_str(s, format).map_err(|e| FromSqlError::Other(e.into())); + } + + Err(FromSqlError::InvalidType) + } +} + +/// "HH:MM"/"HH:MM:SS"/"HH:MM:SS.SSS" => ISO 8601 time without timezone. +#[cfg(feature = "jiff")] +impl FromSql for jiff::civil::Time { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if value.as_timestamp().is_ok() { + return Ok(jiff::Zoned::column_result(value)?.time()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_time_format!(s); + return jiff::civil::Time::strptime(format, s).map_err(|e| FromSqlError::Other(e.into())); + } + + Err(FromSqlError::InvalidType) + } +} + +/// ISO 8601 combined date and time without timezone => +/// "YYYY-MM-DD HH:MM:SS.SSS" +#[cfg(feature = "chrono")] +impl ToSql for chrono::NaiveDateTime { + #[inline] + fn to_sql(&self) -> Result> { + let date_time_str = self.format("%F %T%.f").to_string(); + Ok(ToSqlOutput::from(date_time_str)) + } +} + +/// ISO 8601 combined date and time without timezone => +/// "YYYY-MM-DD HH:MM:SS.SSS" +#[cfg(feature = "jiff")] +impl ToSql for jiff::civil::DateTime { + #[inline] + fn to_sql(&self) -> Result> { + let date_time_str = self.strftime("%F %T%.f").to_string(); + Ok(ToSqlOutput::from(date_time_str)) + } +} + +/// "YYYY-MM-DD HH:MM:SS"/"YYYY-MM-DD HH:MM:SS.SSS" => ISO 8601 combined date +/// and time without timezone. ("YYYY-MM-DDTHH:MM:SS"/"YYYY-MM-DDTHH:MM:SS.SSS" +/// also supported) +#[cfg(feature = "chrono")] +impl FromSql for chrono::NaiveDateTime { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + use chrono::DateTime; + + if let Ok((secs, nsecs)) = value.as_timestamp() { + return Ok(DateTime::from_timestamp(secs, nsecs as u32).unwrap().naive_utc()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_datetime_format!(s); + return Self::parse_from_str(s, format).map_err(|err| FromSqlError::Other(Box::new(err))); + } + + Err(FromSqlError::InvalidType) + } +} + +/// "YYYY-MM-DD HH:MM:SS"/"YYYY-MM-DD HH:MM:SS.SSS" => ISO 8601 combined date +/// and time with or without timezone. +#[cfg(feature = "jiff")] +impl FromSql for jiff::Zoned { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + use jiff::civil; + + if let Ok((secs, nsecs)) = value.as_timestamp() { + return Ok(jiff::Timestamp::new(secs, nsecs as i32) + .unwrap() + .to_zoned(jiff::tz::TimeZone::UTC)); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_datetime_format!(s); + match s.len() { + //2016-02-23 23:56:04 + //2016-02-23 23:56:04.789 + 19 | 23 => { + return civil::DateTime::strptime(format, s) + .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC)) + .map_err(|err| FromSqlError::Other(Box::new(err))) + } + //2016-02-23 23:56:04.789+00:00 + _ => return Self::strptime(format, s).map_err(|err| FromSqlError::Other(Box::new(err))), + } + } + + Err(FromSqlError::InvalidType) + } +} + +/// "YYYY-MM-DD HH:MM:SS"/"YYYY-MM-DD HH:MM:SS.SSS" => ISO 8601 combined date +/// and time without timezone. +#[cfg(feature = "jiff")] +impl FromSql for jiff::civil::DateTime { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if value.as_timestamp().is_ok() { + return Ok(jiff::Zoned::column_result(value)?.datetime()); + } + + if let Ok(s) = value.as_str() { + let (s, format) = iso_8601_datetime_format!(s); + match s.len() { + //2016-02-23 23:56:04 + //2016-02-23 23:56:04.789 + 19 | 23 => return Self::strptime(format, s).map_err(|err| FromSqlError::Other(Box::new(err))), + //2016-02-23 23:56:04.789+00:00 + _ => return Ok(jiff::Zoned::column_result(value)?.datetime()), + } + } + + Err(FromSqlError::InvalidType) + } +} + +/// Date and time with time zone => UTC ISO 8601 timestamp +/// ("YYYY-MM-DD HH:MM:SS.SSS+00:00"). +#[cfg(feature = "chrono")] +impl ToSql for chrono::DateTime { + #[inline] + fn to_sql(&self) -> Result> { + let date_time_str = self.with_timezone(&Utc).format("%F %T%.f%:z").to_string(); + Ok(ToSqlOutput::from(date_time_str)) + } +} + +/// Date and time with time zone => UTC ISO 8601 timestamp +/// ("YYYY-MM-DD HH:MM:SS.SSS+00:00"). +#[cfg(feature = "jiff")] +impl ToSql for jiff::Zoned { + #[inline] + fn to_sql(&self) -> Result> { + let date_time_str = self.strftime("%F %T%.f%:z").to_string(); + Ok(ToSqlOutput::from(date_time_str)) + } +} + +/// Date and time without time zone => UTC ISO 8601 timestamp +/// ("YYYY-MM-DD HH:MM:SS.SSS"). +#[cfg(feature = "jiff")] +impl ToSql for jiff::Timestamp { + #[inline] + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::Borrowed(ValueRef::Timestamp( + super::TimeUnit::Nanosecond, + self.as_nanosecond() + .try_into() + .map_err(|_| FromSqlError::OutOfRange(self.as_nanosecond()))?, + ))) + } +} + +/// ISO 8601 ("YYYY-MM-DD HH:MM:SS.SSS[+-]HH:MM") into `DateTime`. +#[cfg(feature = "chrono")] +impl FromSql for chrono::DateTime { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + chrono::NaiveDateTime::column_result(value).map(|dt| Utc.from_utc_datetime(&dt)) + } +} + +/// ISO 8601 ("YYYY-MM-DD HH:MM:SS.SSS[+-]HH:MM") into `Timestamp`. +#[cfg(feature = "jiff")] +impl FromSql for jiff::Timestamp { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if let Ok((secs, nsecs)) = value.as_timestamp() { + Ok(jiff::Timestamp::new(secs, nsecs as i32).unwrap()) + } else { + Ok(jiff::Zoned::column_result(value)?.timestamp()) + } + } +} + +/// ISO 8601 ("YYYY-MM-DD HH:MM:SS.SSS[+-]HH:MM") into `DateTime`. +#[cfg(feature = "chrono")] +impl FromSql for chrono::DateTime { + #[inline] + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let utc_dt = chrono::DateTime::::column_result(value)?; + Ok(utc_dt.with_timezone(&Local)) + } +} + +#[cfg(feature = "chrono")] +impl FromSql for chrono::TimeDelta { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Interval { months, days, nanos } => { + let days = days + (months * 30); + let (additional_seconds, nanos) = nanos.div_mod_floor(&NANOS_PER_SECOND); + let seconds = additional_seconds + (i64::from(days) * 24 * 3600); + + match nanos.try_into() { + Ok(nanos) => { + if let Some(duration) = Self::new(seconds, nanos) { + Ok(duration) + } else { + Err(FromSqlError::Other("Invalid duration".into())) + } + } + Err(err) => Err(FromSqlError::Other(format!("Invalid duration: {err}").into())), + } + } + _ => Err(FromSqlError::InvalidType), + } + } +} + +/// Interval => Balanced `jiff::Span` +#[cfg(feature = "jiff")] +impl FromSql for jiff::Span { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Interval { months, days, nanos } => { + let (years, months) = months.div_mod_floor(&12); + let (weeks, days) = days.div_mod_floor(&7); + let (hours, nanos) = nanos.div_mod_floor(&3_600_000_000_000); + let (minutes, nanos) = nanos.div_mod_floor(&60_000_000_000); + let (seconds, nanos) = nanos.div_mod_floor(&1_000_000_000); + let (milliseconds, nanos) = nanos.div_mod_floor(&1_000_000); + let (microseconds, nanos) = nanos.div_mod_floor(&1_000); + Ok(jiff::Span::new() + .years(years) + .months(months) + .weeks(weeks) + .days(days) + .hours(hours) + .minutes(minutes) + .seconds(seconds) + .milliseconds(milliseconds) + .microseconds(microseconds) + .nanoseconds(nanos)) + } + _ => Err(FromSqlError::InvalidType), + } + } +} + +const DAYS_PER_MONTH: i64 = 30; +const SECONDS_PER_DAY: i64 = 24 * 3600; +const NANOS_PER_SECOND: i64 = 1_000_000_000; +const NANOS_PER_DAY: i64 = SECONDS_PER_DAY * NANOS_PER_SECOND; + +/// Loads the interval and converts to a duration assuming +/// that there are 30 days in a month, and 24 hours in a day. +/// Use `jiff::Span` for more accurate conversions +#[cfg(feature = "jiff")] +impl FromSql for jiff::SignedDuration { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Interval { months, days, nanos } => { + let days = months as i64 * DAYS_PER_MONTH + days as i64; + Ok(jiff::SignedDuration::from_nanos(days * NANOS_PER_DAY + nanos)) + } + _ => Err(FromSqlError::InvalidType), + } + } +} + +#[cfg(feature = "chrono")] +impl ToSql for chrono::Duration { + fn to_sql(&self) -> Result> { + let nanos = self.num_nanoseconds().unwrap(); + let (days, nanos) = nanos.div_mod_floor(&NANOS_PER_DAY); + let (months, days) = days.div_mod_floor(&DAYS_PER_MONTH); + Ok(ToSqlOutput::Borrowed(ValueRef::Interval { + months: months.try_into().unwrap(), + days: days.try_into().unwrap(), + nanos, + })) + } +} + +#[cfg(feature = "jiff")] +impl ToSql for jiff::Span { + fn to_sql(&self) -> Result> { + let months = self.get_years() as i32 * 12 + self.get_months(); + let days = self.get_weeks() * 7 + self.get_days(); + let nanos = self.get_hours() as i64 * 3_600_000_000_000 + + self.get_minutes() * 60_000_000_000 + + self.get_seconds() * 1_000_000_000 + + self.get_milliseconds() * 1_000_000 + + self.get_microseconds() * 1_000 + + self.get_nanoseconds(); + Ok(ToSqlOutput::Borrowed(ValueRef::Interval { months, days, nanos })) + } +} + +/// Will store the duration in nanoseconds as an interval. Not using the +/// month or day units. This function doesn't work with durations longer +/// than i64::MAX nanoseconds (292 years). To store durations larger than +/// that, use a `jiff::Span` +#[cfg(feature = "jiff")] +impl ToSql for jiff::SignedDuration { + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::Borrowed(ValueRef::Interval { + months: 0, + days: 0, + nanos: self + .as_nanos() + .try_into() + .map_err(|_| FromSqlError::OutOfRange(self.as_nanos()))?, + })) + } +} + +#[cfg(test)] +mod test { + use crate::{ + types::{FromSql, ToSql, ToSqlOutput, ValueRef}, + Connection, Result, + }; + + fn checked_memory_handle() -> Result { + let db = Connection::open_in_memory()?; + db.execute_batch("CREATE TABLE foo (d DATE, t Text, i INTEGER, f FLOAT, b TIMESTAMP, tt time)")?; + Ok(db) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_naive_time() -> Result<()> { + use chrono::{Duration, NaiveTime}; + + let db = checked_memory_handle()?; + let time = NaiveTime::from_hms_micro_opt(23, 56, 4, 12_345).unwrap(); + db.execute("INSERT INTO foo (tt) VALUES (?)", [time])?; + + let s: String = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; + assert_eq!("23:56:04.012345", s); + let t: NaiveTime = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; + assert_eq!(time, t); + let t: NaiveTime = db.query_row("SELECT '23:56:04.012345'", [], |r| r.get(0))?; + assert_eq!(time, t); + let t: NaiveTime = db.query_row("SELECT '23:56:04'", [], |r| r.get(0))?; + assert_eq!(time - Duration::microseconds(12345), t); + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_civil_time() -> Result<()> { + use jiff::{civil::Time, ToSpan}; + + let db = checked_memory_handle()?; + let time = Time::new(23, 56, 4, 12_345_000).unwrap(); + db.execute("INSERT INTO foo (tt) VALUES (?)", [time])?; + + let s: String = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; + assert_eq!("23:56:04.012345", s); + let t: Time = db.query_row("SELECT tt FROM foo", [], |r| r.get(0))?; + assert_eq!(time, t); + let t: Time = db.query_row("SELECT '23:56:04.012345'", [], |r| r.get(0))?; + assert_eq!(time, t); + let t: Time = db.query_row("SELECT '23:56:04'", [], |r| r.get(0))?; + assert_eq!(time.saturating_sub(12345.microseconds()), t); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_naive_date() -> Result<()> { + use chrono::NaiveDate; + + let db = checked_memory_handle()?; + let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); + db.execute("INSERT INTO foo (d) VALUES (?)", [date])?; + + let s: String = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23", s); + let t: NaiveDate = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; + assert_eq!(date, t); + let t: NaiveDate = db.query_row("SELECT '2016-02-23'", [], |r| r.get(0))?; + assert_eq!(date, t); + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_civil_date() -> Result<()> { + use jiff::civil::{date, Date}; + + let db = checked_memory_handle()?; + let date = date(2016, 2, 23); + db.execute("INSERT INTO foo (d) VALUES (?)", [date])?; + + let s: String = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23", s); + let t: Date = db.query_row("SELECT d FROM foo", [], |r| r.get(0))?; + assert_eq!(date, t); + let t: Date = db.query_row("SELECT '2016-02-23'", [], |r| r.get(0))?; + assert_eq!(date, t); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_naive_date_time() -> Result<()> { + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + let db = checked_memory_handle()?; + let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); + let time = NaiveTime::from_hms_opt(23, 56, 4).unwrap(); + let dt = NaiveDateTime::new(date, time); + + db.execute("INSERT INTO foo (b) VALUES (?)", [dt])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23 23:56:04", s); + let v: NaiveDateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(dt, v); + + db.execute( + "UPDATE foo set b = strftime(cast(b as datetime), '%Y-%m-%d %H:%M:%S')", + [], + )?; // "YYYY-MM-DD HH:MM:SS" + let hms: NaiveDateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(dt, hms); + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_civil_date_time() -> Result<()> { + use jiff::civil::DateTime; + + let db = checked_memory_handle()?; + let dt = DateTime::new(2016, 2, 23, 23, 56, 4, 0).unwrap(); + + db.execute("INSERT INTO foo (b) VALUES (?)", [dt])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23 23:56:04", s); + let v: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(dt, v); + + db.execute( + "UPDATE foo set b = strftime(cast(b as datetime), '%Y-%m-%d %H:%M:%S')", + [], + )?; // "YYYY-MM-DD HH:MM:SS" + let hms: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(dt, hms); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_date_time_utc() -> Result<()> { + use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + + let db = checked_memory_handle()?; + let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); + let time = NaiveTime::from_hms_milli_opt(23, 56, 4, 789).unwrap(); + let dt = NaiveDateTime::new(date, time); + let utc = Utc.from_utc_datetime(&dt); + + db.execute("INSERT INTO foo (b) VALUES (?)", [utc])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23 23:56:04.789", s); + + let v1: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(utc, v1); + + let v2: DateTime = db.query_row("SELECT '2016-02-23 23:56:04.789'", [], |r| r.get(0))?; + assert_eq!(utc, v2); + + let v3: DateTime = db.query_row("SELECT '2016-02-23 23:56:04'", [], |r| r.get(0))?; + assert_eq!(utc - Duration::try_milliseconds(789).unwrap(), v3); + + let v4: DateTime = db.query_row("SELECT '2016-02-23 23:56:04.789+00:00'", [], |r| r.get(0))?; + assert_eq!(utc, v4); + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_timestamp() -> Result<()> { + use jiff::{Timestamp, ToSpan}; + + let db = checked_memory_handle()?; + let timestamp = Timestamp::new(1760996605, 789_000_000).unwrap(); + + db.execute("INSERT INTO foo (b) VALUES (?)", [×tamp])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!("2025-10-20 21:43:25.789", s); + + let v1: Timestamp = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(timestamp, v1); + + let v2: Timestamp = db.query_row("SELECT '2025-10-20 21:43:25.789'", [], |r| r.get(0))?; + assert_eq!(timestamp, v2); + + let v3: Timestamp = db.query_row("SELECT '2025-10-20 21:43:25'", [], |r| r.get(0))?; + assert_eq!(timestamp.saturating_sub(789.milliseconds()).unwrap(), v3); + + let v4: Timestamp = db.query_row("SELECT '2025-10-20 21:43:25.789+00:00'", [], |r| r.get(0))?; + assert_eq!(timestamp, v4); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_time_delta_roundtrip() { + use chrono::TimeDelta; + + roundtrip_type(TimeDelta::new(3600, 0).unwrap()); + roundtrip_type(TimeDelta::new(3600, 1000).unwrap()); + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_signed_duration_roundtrip() { + use jiff::SignedDuration; + + roundtrip_type(SignedDuration::new(3600, 0)); + roundtrip_type(SignedDuration::new(3600, 1000)); + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_time_delta() -> Result<()> { + use chrono::TimeDelta; + + let db = checked_memory_handle()?; + let td = TimeDelta::new(3600, 0).unwrap(); + + let row: Result = db.query_row("SELECT ?", [td], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), td); + + let row: Result = db.query_row("SELECT INTERVAL 1 HOUR", [], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), td); + + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_signed_duration() -> Result<()> { + use jiff::SignedDuration; + + let db = checked_memory_handle()?; + let td = SignedDuration::new(3600, 0); + + let row: Result = db.query_row("SELECT ?", [td], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), td); + + let row: Result = db.query_row("SELECT INTERVAL 1 HOUR", [], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), td); + + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_span() -> Result<()> { + use jiff::Span; + + let db = checked_memory_handle()?; + let td = Span::new() + .years(1) + .months(2) + .weeks(3) + .days(4) + .hours(5) + .minutes(6) + .seconds(7) + .milliseconds(8) + .microseconds(9); + + let row: Result = db.query_row("SELECT ?", [td], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), td.fieldwise()); + + let row: Result = db.query_row("SELECT INTERVAL 3 YEARS", [], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), Span::new().years(3).fieldwise()); + + let row: Result = db.query_row("SELECT INTERVAL 5 WEEKS", [], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), Span::new().weeks(5).fieldwise()); + + let row: Result = db.query_row("SELECT INTERVAL 9 DAYS", [], |row| Ok(row.get(0)))?; + + assert_eq!(row.unwrap(), Span::new().weeks(1).days(2).fieldwise()); + + let row: Result = db.query_row("SELECT INTERVAL 18367008 MILLISECONDS", [], |row| Ok(row.get(0)))?; + + assert_eq!( + row.unwrap(), + Span::new() + .hours(5) + .minutes(6) + .seconds(7) + .milliseconds(8) + .microseconds(0) + .nanoseconds(0) + .fieldwise() + ); + + Ok(()) + } + + fn roundtrip_type(td: T) { + let sqled = td.to_sql().unwrap(); + let value = match sqled { + ToSqlOutput::Borrowed(v) => v, + ToSqlOutput::Owned(ref v) => ValueRef::from(v), + }; + let reversed = FromSql::column_result(value).unwrap(); + + assert_eq!(td, reversed); + } + + #[test] + #[cfg(feature = "chrono")] + fn test_chrono_date_time_local() -> Result<()> { + use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; + + let db = checked_memory_handle()?; + let date = NaiveDate::from_ymd_opt(2016, 2, 23).unwrap(); + let time = NaiveTime::from_hms_milli_opt(23, 56, 4, 789).unwrap(); + let dt = NaiveDateTime::new(date, time); + let local = Local.from_local_datetime(&dt).single().unwrap(); + + db.execute("INSERT INTO foo (b) VALUES (?)", [local])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(DateTime::::from(local).format("%F %T%.f").to_string(), s); + + let v: DateTime = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(local, v); + Ok(()) + } + + #[test] + #[cfg(feature = "jiff")] + fn test_jiff_zoned() -> Result<()> { + use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned}; + + let db = checked_memory_handle()?; + let utc = DateTime::new(2016, 2, 23, 23, 56, 4, 789_000_000) + .unwrap() + .to_zoned(TimeZone::UTC) + .unwrap(); + + db.execute("INSERT INTO foo (b) VALUES (?)", [&utc])?; + + let s: String = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!("2016-02-23 23:56:04.789", s); + + let v1: Zoned = db.query_row("SELECT b FROM foo", [], |r| r.get(0))?; + assert_eq!(utc, v1); + + let v2: Zoned = db.query_row("SELECT '2016-02-23 23:56:04.789'", [], |r| r.get(0))?; + assert_eq!(utc, v2); + + let v3: Zoned = db.query_row("SELECT '2016-02-23 23:56:04'", [], |r| r.get(0))?; + assert_eq!(utc.saturating_sub(789.milliseconds()), v3); + + let v4: Zoned = db.query_row("SELECT '2016-02-23 23:56:04.789+00:00'", [], |r| r.get(0))?; + assert_eq!(utc, v4); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_duckdb_datetime_functions() -> Result<()> { + use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; + + let db = checked_memory_handle()?; + let result: Result = db.query_row("SELECT CURRENT_DATE", [], |r| r.get(0)); + assert!(result.is_ok()); + let result: Result = db.query_row("SELECT CURRENT_TIMESTAMP", [], |r| r.get(0)); + assert!(result.is_ok()); + let result: Result> = db.query_row("SELECT CURRENT_TIMESTAMP", [], |r| r.get(0)); + assert!(result.is_ok()); + let result: Result = db.query_row("SELECT CURRENT_TIME", [], |r| r.get(0)); + assert!(result.is_ok()); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_naive_date_time_param() -> Result<()> { + use chrono::NaiveDateTime; + + let db = checked_memory_handle()?; + let fixed_time = NaiveDateTime::parse_from_str("2023-01-01 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let result: Result = db.query_row( + "SELECT 1 WHERE ?::TIMESTAMP BETWEEN (TIMESTAMP '2023-01-01 11:59:00') AND (TIMESTAMP '2023-01-01 12:01:00')", + [fixed_time], + |r| r.get(0), + ); + assert!(result.is_ok()); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_date_time_param() -> Result<()> { + use chrono::{TimeZone, Utc}; + + let db = checked_memory_handle()?; + let fixed_time = Utc.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap(); + let result: Result = db.query_row( + "SELECT 1 WHERE ?::TIMESTAMPTZ BETWEEN (TIMESTAMPTZ '2023-01-01 11:59:00+00:00') AND (TIMESTAMPTZ '2023-01-01 12:01:00+00:00')", + [fixed_time], + |r| r.get(0), + ); + assert!(result.is_ok()); + Ok(()) + } + + #[test] + #[cfg(feature = "chrono")] + fn test_lenient_parse_timezone() { + use crate::types::FromSqlError; + use chrono::{DateTime, Utc}; + + // Not supported + assert!(matches!( + DateTime::::column_result(ValueRef::Text(b"1970-01-01T00:00:00Z")), + Err(FromSqlError::Other(_)) + )); + assert!(matches!( + DateTime::::column_result(ValueRef::Text(b"1970-01-01T00:00:00+00")), + Err(FromSqlError::Other(_)) + )); + } +} diff --git a/crates/duckdb/src/types/value_ref.rs b/crates/duckdb/src/types/value_ref.rs index 93fee657..29052429 100644 --- a/crates/duckdb/src/types/value_ref.rs +++ b/crates/duckdb/src/types/value_ref.rs @@ -189,6 +189,27 @@ impl<'a> ValueRef<'a> { _ => Err(FromSqlError::InvalidType), } } + + /// If `self` is case `Timestamp`, returns the timestamp as (secs, nsecs). + /// Otherwise, returns [`Err(Error::InvalidColumnType)`](crate::Error::InvalidColumnType). + #[inline] + pub fn as_timestamp(&self) -> FromSqlResult<(i64, i64)> { + match *self { + ValueRef::Timestamp(tu, t) | ValueRef::Time64(tu, t) => { + let (secs, nsecs) = match tu { + TimeUnit::Second => (t, 0), + TimeUnit::Millisecond => (t / 1000, (t % 1000) * 1_000_000), + TimeUnit::Microsecond => (t / 1_000_000, (t % 1_000_000) * 1000), + TimeUnit::Nanosecond => (t / 1_000_000_000, t % 1_000_000_000), + }; + Ok((secs, nsecs)) + } + // Correct because UTC does not have DST, thus all days are + // 24 hours + ValueRef::Date32(d) => Ok((24 * 3600 * (d as i64), 0)), + _ => Err(FromSqlError::InvalidType), + } + } } impl From> for Value {