From f2414f841392172152e4723622aebad8b7a35b0c Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 19 Sep 2024 11:21:34 +0800 Subject: [PATCH 1/7] feat: support jiff integration for postgresql Signed-off-by: tison --- Cargo.lock | 27 +++++++++ Cargo.toml | 1 + sqlx-core/Cargo.toml | 1 + sqlx-core/src/types/mod.rs | 9 +++ sqlx-postgres/Cargo.toml | 4 +- sqlx-postgres/src/types/interval.rs | 87 +++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4d0481fa4c..4d585a5d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,6 +1923,31 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jiff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.52.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.31" @@ -3412,6 +3437,7 @@ dependencies = [ "hex", "indexmap 2.2.5", "ipnetwork", + "jiff", "log", "mac_address", "memchr", @@ -3673,6 +3699,7 @@ dependencies = [ "home", "ipnetwork", "itoa", + "jiff", "log", "mac_address", "md-5", diff --git a/Cargo.toml b/Cargo.toml index cfbdeff210..673da7a4c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ bigdecimal = "0.4.0" bit-vec = "0.6.3" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } ipnetwork = "0.20.0" +jiff = { version = "0.1.13" } mac_address = "1.1.5" rust_decimal = { version = "1.26.1", default-features = false, features = ["std"] } time = { version = "0.3.36", features = ["formatting", "parsing", "macros"] } diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 60f9573aae..81095590de 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -47,6 +47,7 @@ bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } +jiff = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index 25837b1e77..fd83527e20 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -46,6 +46,15 @@ pub mod chrono { }; } +#[cfg(feature = "jiff")] +#[cfg_attr(docsrs, doc(cfg(feature = "jiff")))] +pub mod jiff { + #[doc(no_inline)] + pub use jiff::{ + Zoned, Timestamp, tz::TimeZone, tz::Offset, civil::Date, civil::Time, civil::DateTime, + }; +} + #[cfg(feature = "bit-vec")] #[cfg_attr(docsrs, doc(cfg(feature = "bit-vec")))] #[doc(no_inline)] diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index 55a94eceb1..012f13863d 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -20,6 +20,7 @@ bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"] bit-vec = ["dep:bit-vec", "sqlx-core/bit-vec"] chrono = ["dep:chrono", "sqlx-core/chrono"] ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"] +jiff = ["dep:jiff", "sqlx-core/jiff"] mac_address = ["dep:mac_address", "sqlx-core/mac_address"] rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"] time = ["dep:time", "sqlx-core/time"] @@ -35,7 +36,7 @@ futures-util = { version = "0.3.19", default-features = false, features = ["allo # Cryptographic Primitives crc = "3.0.0" hkdf = "0.12.0" -hmac = { version = "0.12.0", default-features = false, features = ["reset"]} +hmac = { version = "0.12.0", default-features = false, features = ["reset"] } md-5 = { version = "0.10.0", default-features = false } rand = { version = "0.8.4", default-features = false, features = ["std", "std_rng"] } sha2 = { version = "0.10.0", default-features = false } @@ -45,6 +46,7 @@ bigdecimal = { workspace = true, optional = true } bit-vec = { workspace = true, optional = true } chrono = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } +jiff = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } diff --git a/sqlx-postgres/src/types/interval.rs b/sqlx-postgres/src/types/interval.rs index 52ab549915..9ef7a97c1a 100644 --- a/sqlx-postgres/src/types/interval.rs +++ b/sqlx-postgres/src/types/interval.rs @@ -112,6 +112,58 @@ impl TryFrom for PgInterval { } } +#[cfg(feature = "jiff")] +impl Type for jiff::SignedDuration { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL + } +} + +#[cfg(feature = "jiff")] +impl PgHasArrayType for jiff::SignedDuration { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL_ARRAY + } +} + +#[cfg(feature = "jiff")] +impl Encode<'_, Postgres> for jiff::SignedDuration { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + let pg_interval = PgInterval::try_from(*self)?; + pg_interval.encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + 2 * mem::size_of::() + } +} + +#[cfg(feature = "jiff")] +impl TryFrom for PgInterval { + type Error = BoxDynError; + + /// Convert a `jiff::SignedDuration` to a `PgInterval`. + /// + /// This returns an error if there is a loss of precision using nanoseconds or if there is a + /// microseconds overflow. + fn try_from(value: jiff::SignedDuration) -> Result { + if value.subsec_nanos() % 1000 != 0 { + return Err("PostgreSQL `INTERVAL` does not support nanoseconds precision".into()); + } + + let micros = value.as_micros(); + if micros >= i64::MIN as i128 && micros <= i64::MAX as i128 { + Ok(Self { + months: 0, + days: 0, + microseconds: micros as i64, + }) + } else { + Err("Overflow has occurred for PostgreSQL `INTERVAL`".into()) + } + } +} + #[cfg(feature = "chrono")] impl Type for chrono::Duration { fn type_info() -> PgTypeInfo { @@ -330,6 +382,41 @@ fn test_pginterval_std() { assert!(PgInterval::try_from(std::time::Duration::from_secs(20_000_000_000_000)).is_err()); } +#[test] +#[cfg(feature = "jiff")] +fn test_pginterval_jiff() { + // Case for positive duration + let interval = PgInterval { + days: 0, + months: 0, + microseconds: 27_000, + }; + assert_eq!( + &PgInterval::try_from(jiff::SignedDuration::from_micros(27_000)).unwrap(), + &interval + ); + + // Case for negative duration + let interval = PgInterval { + days: 0, + months: 0, + microseconds: -27_000, + }; + assert_eq!( + &PgInterval::try_from(jiff::SignedDuration::from_micros(-27_000)).unwrap(), + &interval + ); + + // Case when precision loss occurs + assert!(PgInterval::try_from(jiff::SignedDuration::from_nanos(27_000_001)).is_err()); + assert!(PgInterval::try_from(jiff::SignedDuration::from_nanos(-27_000_001)).is_err()); + + // Case when microseconds overflow occurs + assert!(PgInterval::try_from(jiff::SignedDuration::from_secs(10_000_000_000_000)).is_err()); + assert!(PgInterval::try_from(jiff::SignedDuration::from_secs(-10_000_000_000_000)).is_err()); +} + + #[test] #[cfg(feature = "chrono")] fn test_pginterval_chrono() { From a20b854bee975cfa2d54ad417351042fd1293382 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 19 Sep 2024 12:06:26 +0800 Subject: [PATCH 2/7] add date time types Signed-off-by: tison --- sqlx-postgres/src/types/jiff/date.rs | 50 +++++++++ sqlx-postgres/src/types/jiff/datetime.rs | 127 +++++++++++++++++++++++ sqlx-postgres/src/types/jiff/mod.rs | 3 + sqlx-postgres/src/types/jiff/time.rs | 45 ++++++++ sqlx-postgres/src/types/mod.rs | 3 + 5 files changed, 228 insertions(+) create mode 100644 sqlx-postgres/src/types/jiff/date.rs create mode 100644 sqlx-postgres/src/types/jiff/datetime.rs create mode 100644 sqlx-postgres/src/types/jiff/mod.rs create mode 100644 sqlx-postgres/src/types/jiff/time.rs diff --git a/sqlx-postgres/src/types/jiff/date.rs b/sqlx-postgres/src/types/jiff/date.rs new file mode 100644 index 0000000000..1da574ac54 --- /dev/null +++ b/sqlx-postgres/src/types/jiff/date.rs @@ -0,0 +1,50 @@ +use std::mem; + +use jiff::civil::Date; +use jiff::ToSpan; +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; + +impl Type for Date { + fn type_info() -> PgTypeInfo { + PgTypeInfo::DATE + } +} + +impl PgHasArrayType for Date { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::DATE_ARRAY + } +} + +impl Encode<'_, Postgres> for Date { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // DATE is encoded as the days since epoch + let days = (*self - postgres_epoch_date()).get_days(); + Encode::::encode(days, buf) + } + + fn size_hint(&self) -> usize { + mem::size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Date { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // DATE is encoded as the days since epoch + let days: i32 = Decode::::decode(value)?; + postgres_epoch_date() + days.days() + } + PgValueFormat::Text => Date::strptime("%Y-%m-%d", value.as_str()?)?, + }) + } +} + +const fn postgres_epoch_date() -> Date { + Date::constant(2000, 1, 1) +} diff --git a/sqlx-postgres/src/types/jiff/datetime.rs b/sqlx-postgres/src/types/jiff/datetime.rs new file mode 100644 index 0000000000..9742cdbf79 --- /dev/null +++ b/sqlx-postgres/src/types/jiff/datetime.rs @@ -0,0 +1,127 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use jiff::tz::TimeZone; +use jiff::{SignedDuration, Timestamp, Zoned}; +use std::mem; +use std::str::FromStr; + +impl Type for Timestamp { + fn type_info() -> PgTypeInfo { + PgTypeInfo::TIMESTAMP + } +} + +impl Type for Zoned { + fn type_info() -> PgTypeInfo { + PgTypeInfo::TIMESTAMPTZ + } +} + +impl PgHasArrayType for Timestamp { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::TIMESTAMP_ARRAY + } +} + +impl PgHasArrayType for Zoned { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::TIMESTAMPTZ_ARRAY + } +} + +impl Encode<'_, Postgres> for Timestamp { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // TIMESTAMP is encoded as the microseconds since the epoch + let micros = (*self - postgres_epoch_datetime()).get_microseconds(); + Encode::::encode(micros, buf) + } + + fn size_hint(&self) -> usize { + mem::size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Timestamp { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // TIMESTAMP is encoded as the microseconds since the epoch + let us = Decode::::decode(value)?; + postgres_epoch_datetime() + SignedDuration::from_micros(us) + } + PgValueFormat::Text => { + let s = value.as_str()?; + parse_timestamp_text(s)?.timestamp() + } + }) + } +} + +impl Encode<'_, Postgres> for Zoned { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + Encode::::encode(self.timestamp(), buf) + } + + fn size_hint(&self) -> usize { + mem::size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Zoned { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + let naive = >::decode(value)?; + naive.to_zoned(TimeZone::UTC) + } + PgValueFormat::Text => { + let s = value.as_str()?; + parse_timestamp_text(s)? + } + }) + } +} + +#[inline] +fn parse_timestamp_text(input: &str) -> Result { + Ok(Zoned::strptime( + if input.contains('+') || input.contains('-') { + // Contains a time-zone specifier + // This is given for timestamptz for some reason + // Postgres already guarantees this to always be UTC + "%Y-%m-%d %H:%M:%S%.f%#z" + } else { + "%Y-%m-%d %H:%M:%S%.f" + }, + input, + )?) +} + +#[inline] +fn postgres_epoch_datetime() -> Timestamp { + Timestamp::from_str("2000-01-01T00:00:00+00:00") + .expect("expected 2000-01-01T00:00:00+00:00 to be a valid Timestamp") +} + +#[test] +fn test_postgres_epoch_datetime() { + let epoch_datetime = jiff::civil::datetime(2000, 1, 1, 0, 0, 0, 0) + .intz("UTC") + .unwrap(); + assert_eq!(postgres_epoch_datetime(), epoch_datetime.timestamp()); +} +// +// #[test] +// fn test_parse_timestamp_text() { +// let zoned = Zoned::from_str("2004-10-19 10:23:54+02").unwrap(); +// assert_eq!( +// parse_timestamp_text("2024-09-19 03:58:43.152233+0000").unwrap(), +// zoned +// ); +// +// let zoned = Zoned::strptime("%Y-%m-%d %H:%M:%S%.f", "2021-01-01 00:00:00").unwrap(); +// assert_eq!(parse_timestamp_text("2021-01-01 00:00:00").unwrap(), zoned); +// } diff --git a/sqlx-postgres/src/types/jiff/mod.rs b/sqlx-postgres/src/types/jiff/mod.rs new file mode 100644 index 0000000000..bd27c4d2d5 --- /dev/null +++ b/sqlx-postgres/src/types/jiff/mod.rs @@ -0,0 +1,3 @@ +mod date; +mod datetime; +mod time; diff --git a/sqlx-postgres/src/types/jiff/time.rs b/sqlx-postgres/src/types/jiff/time.rs new file mode 100644 index 0000000000..a076368d4c --- /dev/null +++ b/sqlx-postgres/src/types/jiff/time.rs @@ -0,0 +1,45 @@ +use std::mem; +use jiff::civil::Time; +use jiff::SignedDuration; +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; + +impl Type for Time { + fn type_info() -> PgTypeInfo { + PgTypeInfo::TIME + } +} + +impl PgHasArrayType for Time { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::TIME_ARRAY + } +} + +impl Encode<'_, Postgres> for Time { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // TIME is encoded as the microseconds since midnight + let micros = (*self - Time::midnight()).get_microseconds(); + Encode::::encode(micros, buf) + } + + fn size_hint(&self) -> usize { + mem::size_of::() + } +} + +impl<'r> Decode<'r, Postgres> for Time { + fn decode(value: PgValueRef<'r>) -> Result { + Ok(match value.format() { + PgValueFormat::Binary => { + // TIME is encoded as the microseconds since midnight + let us: i64 = Decode::::decode(value)?; + Time::midnight() + SignedDuration::from_micros(us) + } + PgValueFormat::Text => Time::strptime("%H:%M:%S%.f", value.as_str()?)?, + }) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 846f1b731d..1e18e5e8fe 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -218,6 +218,9 @@ mod numeric; #[cfg(feature = "rust_decimal")] mod rust_decimal; +#[cfg(feature = "jiff")] +mod jiff; + #[cfg(feature = "chrono")] mod chrono; From 3e5f5d06bf407e3d6000f6f24f479939ca945f62 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 19 Sep 2024 12:37:07 +0800 Subject: [PATCH 3/7] rework jiff datetime Signed-off-by: tison --- sqlx-postgres/src/types/jiff/datetime.rs | 86 +++++++++--------------- 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/sqlx-postgres/src/types/jiff/datetime.rs b/sqlx-postgres/src/types/jiff/datetime.rs index 9742cdbf79..151fc1d328 100644 --- a/sqlx-postgres/src/types/jiff/datetime.rs +++ b/sqlx-postgres/src/types/jiff/datetime.rs @@ -3,12 +3,12 @@ use crate::encode::{Encode, IsNull}; use crate::error::BoxDynError; use crate::types::Type; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use jiff::civil::DateTime; use jiff::tz::TimeZone; -use jiff::{SignedDuration, Timestamp, Zoned}; +use jiff::{SignedDuration, Zoned}; use std::mem; -use std::str::FromStr; -impl Type for Timestamp { +impl Type for DateTime { fn type_info() -> PgTypeInfo { PgTypeInfo::TIMESTAMP } @@ -20,7 +20,7 @@ impl Type for Zoned { } } -impl PgHasArrayType for Timestamp { +impl PgHasArrayType for DateTime { fn array_type_info() -> PgTypeInfo { PgTypeInfo::TIMESTAMP_ARRAY } @@ -32,7 +32,7 @@ impl PgHasArrayType for Zoned { } } -impl Encode<'_, Postgres> for Timestamp { +impl Encode<'_, Postgres> for DateTime { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { // TIMESTAMP is encoded as the microseconds since the epoch let micros = (*self - postgres_epoch_datetime()).get_microseconds(); @@ -44,19 +44,34 @@ impl Encode<'_, Postgres> for Timestamp { } } -impl<'r> Decode<'r, Postgres> for Timestamp { +#[derive(Debug, thiserror::Error)] +#[error("error parsing datetime {squashed:?}")] +struct ParseError { + squashed: Vec, +} + +impl<'r> Decode<'r, Postgres> for DateTime { fn decode(value: PgValueRef<'r>) -> Result { - Ok(match value.format() { + match value.format() { PgValueFormat::Binary => { // TIMESTAMP is encoded as the microseconds since the epoch let us = Decode::::decode(value)?; - postgres_epoch_datetime() + SignedDuration::from_micros(us) + Ok(postgres_epoch_datetime() + SignedDuration::from_micros(us)) } PgValueFormat::Text => { - let s = value.as_str()?; - parse_timestamp_text(s)?.timestamp() + let input = value.as_str()?; + let mut squashed = vec![]; + match DateTime::strptime("%Y-%m-%d %H:%M:%S%.f", input) { + Ok(datetime) => return Ok(datetime), + Err(err) => squashed.push(err), + } + match DateTime::strptime("%Y-%m-%d %H:%M:%S%.f%#z", input) { + Ok(datetime) => return Ok(datetime), + Err(err) => squashed.push(err), + } + Err(Box::new(ParseError { squashed })) } - }) + } } } @@ -74,54 +89,17 @@ impl<'r> Decode<'r, Postgres> for Zoned { fn decode(value: PgValueRef<'r>) -> Result { Ok(match value.format() { PgValueFormat::Binary => { - let naive = >::decode(value)?; - naive.to_zoned(TimeZone::UTC) + let naive = >::decode(value)?; + naive.to_zoned(TimeZone::UTC)? } PgValueFormat::Text => { - let s = value.as_str()?; - parse_timestamp_text(s)? + let input = value.as_str()?; + Zoned::strptime("%Y-%m-%d %H:%M:%S%.f%#z", input)? } }) } } -#[inline] -fn parse_timestamp_text(input: &str) -> Result { - Ok(Zoned::strptime( - if input.contains('+') || input.contains('-') { - // Contains a time-zone specifier - // This is given for timestamptz for some reason - // Postgres already guarantees this to always be UTC - "%Y-%m-%d %H:%M:%S%.f%#z" - } else { - "%Y-%m-%d %H:%M:%S%.f" - }, - input, - )?) -} - -#[inline] -fn postgres_epoch_datetime() -> Timestamp { - Timestamp::from_str("2000-01-01T00:00:00+00:00") - .expect("expected 2000-01-01T00:00:00+00:00 to be a valid Timestamp") -} - -#[test] -fn test_postgres_epoch_datetime() { - let epoch_datetime = jiff::civil::datetime(2000, 1, 1, 0, 0, 0, 0) - .intz("UTC") - .unwrap(); - assert_eq!(postgres_epoch_datetime(), epoch_datetime.timestamp()); +const fn postgres_epoch_datetime() -> DateTime { + DateTime::constant(2000, 1, 1, 0, 0, 0, 0) } -// -// #[test] -// fn test_parse_timestamp_text() { -// let zoned = Zoned::from_str("2004-10-19 10:23:54+02").unwrap(); -// assert_eq!( -// parse_timestamp_text("2024-09-19 03:58:43.152233+0000").unwrap(), -// zoned -// ); -// -// let zoned = Zoned::strptime("%Y-%m-%d %H:%M:%S%.f", "2021-01-01 00:00:00").unwrap(); -// assert_eq!(parse_timestamp_text("2021-01-01 00:00:00").unwrap(), zoned); -// } From 872812ec04814b03cc876b96ce731602500ebd6b Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 20 Sep 2024 01:25:24 +0800 Subject: [PATCH 4/7] complete datetime Signed-off-by: tison --- sqlx-core/src/types/mod.rs | 4 +--- sqlx-postgres/src/types/jiff/datetime.rs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index fd83527e20..a567f84498 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -50,9 +50,7 @@ pub mod chrono { #[cfg_attr(docsrs, doc(cfg(feature = "jiff")))] pub mod jiff { #[doc(no_inline)] - pub use jiff::{ - Zoned, Timestamp, tz::TimeZone, tz::Offset, civil::Date, civil::Time, civil::DateTime, - }; + pub use jiff::{civil::Date, civil::DateTime, civil::Time, Timestamp}; } #[cfg(feature = "bit-vec")] diff --git a/sqlx-postgres/src/types/jiff/datetime.rs b/sqlx-postgres/src/types/jiff/datetime.rs index 151fc1d328..831a72a2ea 100644 --- a/sqlx-postgres/src/types/jiff/datetime.rs +++ b/sqlx-postgres/src/types/jiff/datetime.rs @@ -4,9 +4,10 @@ use crate::error::BoxDynError; use crate::types::Type; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; use jiff::civil::DateTime; -use jiff::tz::TimeZone; -use jiff::{SignedDuration, Zoned}; +use jiff::tz::{Offset, TimeZone}; +use jiff::{SignedDuration, Timestamp, Zoned}; use std::mem; +use std::str::FromStr; impl Type for DateTime { fn type_info() -> PgTypeInfo { @@ -75,9 +76,10 @@ impl<'r> Decode<'r, Postgres> for DateTime { } } -impl Encode<'_, Postgres> for Zoned { +impl Encode<'_, Postgres> for Timestamp { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - Encode::::encode(self.timestamp(), buf) + let datetime = Offset::UTC.to_datetime(*self); + Encode::::encode(datetime, buf) } fn size_hint(&self) -> usize { @@ -85,17 +87,14 @@ impl Encode<'_, Postgres> for Zoned { } } -impl<'r> Decode<'r, Postgres> for Zoned { +impl<'r> Decode<'r, Postgres> for Timestamp { fn decode(value: PgValueRef<'r>) -> Result { Ok(match value.format() { PgValueFormat::Binary => { let naive = >::decode(value)?; - naive.to_zoned(TimeZone::UTC)? - } - PgValueFormat::Text => { - let input = value.as_str()?; - Zoned::strptime("%Y-%m-%d %H:%M:%S%.f%#z", input)? + naive.to_zoned(TimeZone::UTC)?.timestamp() } + PgValueFormat::Text => Timestamp::from_str(value.as_str()?)?, }) } } From b26be7d1c01b3f411dd3e3bd9c72a17788e4f866 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 20 Sep 2024 01:33:25 +0800 Subject: [PATCH 5/7] more Signed-off-by: tison --- Cargo.toml | 1 + sqlx-postgres/src/types/range.rs | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 673da7a4c6..f4bcad9262 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros?/bigdecimal", "sqlx-mysql?/bi bit-vec = ["sqlx-core/bit-vec", "sqlx-macros?/bit-vec", "sqlx-postgres?/bit-vec"] chrono = ["sqlx-core/chrono", "sqlx-macros?/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"] ipnetwork = ["sqlx-core/ipnetwork", "sqlx-macros?/ipnetwork", "sqlx-postgres?/ipnetwork"] +jiff = ["sqlx-core/jiff", "sqlx-postgres?/jiff"] mac_address = ["sqlx-core/mac_address", "sqlx-macros?/mac_address", "sqlx-postgres?/mac_address"] rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] time = ["sqlx-core/time", "sqlx-macros?/time", "sqlx-mysql?/time", "sqlx-postgres?/time", "sqlx-sqlite?/time"] diff --git a/sqlx-postgres/src/types/range.rs b/sqlx-postgres/src/types/range.rs index 5e1346d86c..4e843dad0f 100644 --- a/sqlx-postgres/src/types/range.rs +++ b/sqlx-postgres/src/types/range.rs @@ -154,6 +154,39 @@ impl Type for PgRange { } } +#[cfg(feature = "jiff")] +impl Type for PgRange { + fn type_info() -> PgTypeInfo { + PgTypeInfo::DATE_RANGE + } + + fn compatible(ty: &PgTypeInfo) -> bool { + range_compatible::(ty) + } +} + +#[cfg(feature = "jiff")] +impl Type for PgRange { + fn type_info() -> PgTypeInfo { + PgTypeInfo::TS_RANGE + } + + fn compatible(ty: &PgTypeInfo) -> bool { + range_compatible::(ty) + } +} + +#[cfg(feature = "jiff")] +impl Type for PgRange { + fn type_info() -> PgTypeInfo { + PgTypeInfo::TSTZ_RANGE + } + + fn compatible(ty: &PgTypeInfo) -> bool { + range_compatible::(ty) + } +} + #[cfg(feature = "chrono")] impl Type for PgRange { fn type_info() -> PgTypeInfo { @@ -246,6 +279,27 @@ impl PgHasArrayType for PgRange { } } +#[cfg(feature = "jiff")] +impl PgHasArrayType for PgRange { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::DATE_RANGE_ARRAY + } +} + +#[cfg(feature = "jiff")] +impl PgHasArrayType for PgRange { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::TS_RANGE_ARRAY + } +} + +#[cfg(feature = "jiff")] +impl PgHasArrayType for PgRange { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::TSTZ_RANGE_ARRAY + } +} + #[cfg(feature = "chrono")] impl PgHasArrayType for PgRange { fn array_type_info() -> PgTypeInfo { From b44e935b03b5684cdb02a481a698416e51b30269 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 20 Sep 2024 01:47:54 +0800 Subject: [PATCH 6/7] fix days may error Signed-off-by: tison --- sqlx-postgres/src/types/jiff/date.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sqlx-postgres/src/types/jiff/date.rs b/sqlx-postgres/src/types/jiff/date.rs index 1da574ac54..4ce27e4fc9 100644 --- a/sqlx-postgres/src/types/jiff/date.rs +++ b/sqlx-postgres/src/types/jiff/date.rs @@ -1,12 +1,11 @@ use std::mem; -use jiff::civil::Date; -use jiff::ToSpan; use crate::decode::Decode; use crate::encode::{Encode, IsNull}; use crate::error::BoxDynError; use crate::types::Type; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use jiff::civil::Date; impl Type for Date { fn type_info() -> PgTypeInfo { @@ -38,7 +37,10 @@ impl<'r> Decode<'r, Postgres> for Date { PgValueFormat::Binary => { // DATE is encoded as the days since epoch let days: i32 = Decode::::decode(value)?; - postgres_epoch_date() + days.days() + let days = jiff::Span::new() + .try_days(days) + .map_err(|err| format!("value {days} overflow Postgres DATE: {err:?}"))?; + postgres_epoch_date() + days } PgValueFormat::Text => Date::strptime("%Y-%m-%d", value.as_str()?)?, }) From d52926133553650ac115fa03138c5561912b5e1a Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 20 Sep 2024 01:52:01 +0800 Subject: [PATCH 7/7] dont use zoned Signed-off-by: tison --- sqlx-postgres/src/types/jiff/datetime.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlx-postgres/src/types/jiff/datetime.rs b/sqlx-postgres/src/types/jiff/datetime.rs index 831a72a2ea..b4ba5dbd8e 100644 --- a/sqlx-postgres/src/types/jiff/datetime.rs +++ b/sqlx-postgres/src/types/jiff/datetime.rs @@ -5,7 +5,7 @@ use crate::types::Type; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; use jiff::civil::DateTime; use jiff::tz::{Offset, TimeZone}; -use jiff::{SignedDuration, Timestamp, Zoned}; +use jiff::{SignedDuration, Timestamp}; use std::mem; use std::str::FromStr; @@ -15,7 +15,7 @@ impl Type for DateTime { } } -impl Type for Zoned { +impl Type for Timestamp { fn type_info() -> PgTypeInfo { PgTypeInfo::TIMESTAMPTZ } @@ -27,7 +27,7 @@ impl PgHasArrayType for DateTime { } } -impl PgHasArrayType for Zoned { +impl PgHasArrayType for Timestamp { fn array_type_info() -> PgTypeInfo { PgTypeInfo::TIMESTAMPTZ_ARRAY }