From 71cb68b2f47cbf9b99724616dcc4d9e205399450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Carr?= Date: Sat, 27 Jun 2020 17:36:06 -0700 Subject: [PATCH 1/2] feat(postgres) Create bindings for PgInterval --- sqlx-core/src/postgres/type_info.rs | 19 ++- sqlx-core/src/postgres/types/interval.rs | 147 +++++++++++++++++++++++ sqlx-core/src/postgres/types/mod.rs | 3 + sqlx-macros/src/database/postgres.rs | 2 + tests/postgres/types.rs | 30 ++++- 5 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 sqlx-core/src/postgres/types/interval.rs diff --git a/sqlx-core/src/postgres/type_info.rs b/sqlx-core/src/postgres/type_info.rs index 303fd264fc..88afdc91ee 100644 --- a/sqlx-core/src/postgres/type_info.rs +++ b/sqlx-core/src/postgres/type_info.rs @@ -83,6 +83,8 @@ pub enum PgType { TimeArray, Timestamptz, TimestamptzArray, + Interval, + IntervalArray, NumericArray, Timetz, TimetzArray, @@ -92,7 +94,6 @@ pub enum PgType { VarbitArray, Numeric, Record, - Interval, RecordArray, Uuid, UuidArray, @@ -285,6 +286,8 @@ impl PgType { 1183 => PgType::TimeArray, 1184 => PgType::Timestamptz, 1185 => PgType::TimestamptzArray, + 1186 => PgType::Interval, + 1187 => PgType::IntervalArray, 1231 => PgType::NumericArray, 1266 => PgType::Timetz, 1270 => PgType::TimetzArray, @@ -294,7 +297,6 @@ impl PgType { 1563 => PgType::VarbitArray, 1700 => PgType::Numeric, 2249 => PgType::Record, - 2281 => PgType::Interval, 2287 => PgType::RecordArray, 2950 => PgType::Uuid, 2951 => PgType::UuidArray, @@ -389,6 +391,8 @@ impl PgType { PgType::TimeArray => 1183, PgType::Timestamptz => 1184, PgType::TimestamptzArray => 1185, + PgType::Interval => 1186, + PgType::IntervalArray => 1187, PgType::NumericArray => 1231, PgType::Timetz => 1266, PgType::TimetzArray => 1270, @@ -398,7 +402,6 @@ impl PgType { PgType::VarbitArray => 1563, PgType::Numeric => 1700, PgType::Record => 2249, - PgType::Interval => 2281, PgType::RecordArray => 2287, PgType::Uuid => 2950, PgType::UuidArray => 2951, @@ -488,6 +491,8 @@ impl PgType { PgType::TimeArray => "TIME[]", PgType::Timestamptz => "TIMESTAMPTZ", PgType::TimestamptzArray => "TIMESTAMPTZ[]", + PgType::Interval => "INTERVAL", + PgType::IntervalArray => "INTERVAL[]", PgType::NumericArray => "NUMERIC[]", PgType::Timetz => "TIMETZ", PgType::TimetzArray => "TIMETZ[]", @@ -497,7 +502,6 @@ impl PgType { PgType::VarbitArray => "VARBIT[]", PgType::Numeric => "NUMERIC", PgType::Record => "RECORD", - PgType::Interval => "INTERVAL", PgType::RecordArray => "RECORD[]", PgType::Uuid => "UUID", PgType::UuidArray => "UUID[]", @@ -584,6 +588,8 @@ impl PgType { PgType::TimeArray => "_time", PgType::Timestamptz => "timestamptz", PgType::TimestamptzArray => "_timestamptz", + PgType::Interval => "interval", + PgType::IntervalArray => "_interval", PgType::NumericArray => "_numeric", PgType::Timetz => "timetz", PgType::TimetzArray => "_timetz", @@ -593,7 +599,6 @@ impl PgType { PgType::VarbitArray => "_varbit", PgType::Numeric => "numeric", PgType::Record => "record", - PgType::Interval => "interval", PgType::RecordArray => "_record", PgType::Uuid => "uuid", PgType::UuidArray => "_uuid", @@ -680,6 +685,8 @@ impl PgType { PgType::TimeArray => &PgTypeKind::Array(PgTypeInfo(PgType::Time)), PgType::Timestamptz => &PgTypeKind::Simple, PgType::TimestamptzArray => &PgTypeKind::Array(PgTypeInfo(PgType::Timestamptz)), + PgType::Interval => &PgTypeKind::Simple, + PgType::IntervalArray => &PgTypeKind::Array(PgTypeInfo(PgType::Interval)), PgType::NumericArray => &PgTypeKind::Array(PgTypeInfo(PgType::Numeric)), PgType::Timetz => &PgTypeKind::Simple, PgType::TimetzArray => &PgTypeKind::Array(PgTypeInfo(PgType::Timetz)), @@ -689,7 +696,6 @@ impl PgType { PgType::VarbitArray => &PgTypeKind::Array(PgTypeInfo(PgType::Varbit)), PgType::Numeric => &PgTypeKind::Simple, PgType::Record => &PgTypeKind::Simple, - PgType::Interval => &PgTypeKind::Simple, PgType::RecordArray => &PgTypeKind::Array(PgTypeInfo(PgType::Record)), PgType::Uuid => &PgTypeKind::Simple, PgType::UuidArray => &PgTypeKind::Array(PgTypeInfo(PgType::Uuid)), @@ -866,6 +872,7 @@ impl PgTypeInfo { // time interval pub(crate) const INTERVAL: Self = Self(PgType::Interval); + pub(crate) const INTERVAL_ARRAY: Self = Self(PgType::IntervalArray); // // geometric types diff --git a/sqlx-core/src/postgres/types/interval.rs b/sqlx-core/src/postgres/types/interval.rs new file mode 100644 index 0000000000..f0794068a3 --- /dev/null +++ b/sqlx-core/src/postgres/types/interval.rs @@ -0,0 +1,147 @@ +use std::mem; + +use byteorder::{NetworkEndian, ReadBytesExt}; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use crate::types::Type; + +/// PostgreSQL INTERVAL type binding +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct PgInterval { + pub months: i32, + pub days: i32, + pub microseconds: i64, +} + +impl Type for PgInterval { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL + } +} + +impl Type for [PgInterval] { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL_ARRAY + } +} + +impl<'de> Decode<'de, Postgres> for PgInterval { + fn decode(value: PgValueRef<'de>) -> Result { + match value.format() { + PgValueFormat::Binary => { + let mut buf = value.as_bytes()?; + let microseconds = buf.read_i64::()?; + let days = buf.read_i32::()?; + let months = buf.read_i32::()?; + Ok(PgInterval { + months, + days, + microseconds, + }) + } + PgValueFormat::Text => Err("INTERVAL Text format unsuported".into()), + } + } +} + +impl Encode<'_, Postgres> for PgInterval { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { + if let IsNull::Yes = Encode::::encode(&self.microseconds, buf) { + return IsNull::Yes; + } + if let IsNull::Yes = Encode::::encode(&self.days, buf) { + return IsNull::Yes; + } + if let IsNull::Yes = Encode::::encode(&self.months, buf) { + return IsNull::Yes; + } + IsNull::No + } + + fn size_hint(&self) -> usize { + 2 * mem::size_of::() + } +} + +#[test] +fn test_encode_interval() { + let mut buf = PgArgumentBuffer::default(); + + let interval = PgInterval { + months: 0, + days: 0, + microseconds: 0, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!(&**buf, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + buf.clear(); + + let interval = PgInterval { + months: 0, + days: 0, + microseconds: 1_000, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!(&**buf, [0, 0, 0, 0, 0, 0, 3, 232, 0, 0, 0, 0, 0, 0, 0, 0]); + buf.clear(); + + let interval = PgInterval { + months: 0, + days: 0, + microseconds: 1_000_000, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!(&**buf, [0, 0, 0, 0, 0, 15, 66, 64, 0, 0, 0, 0, 0, 0, 0, 0]); + buf.clear(); + + let interval = PgInterval { + months: 0, + days: 0, + microseconds: 3_600_000_000, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!( + &**buf, + [0, 0, 0, 0, 214, 147, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + buf.clear(); + + let interval = PgInterval { + months: 0, + days: 1, + microseconds: 0, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!(&**buf, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]); + buf.clear(); + + let interval = PgInterval { + months: 1, + days: 0, + microseconds: 0, + }; + assert!(matches!( + Encode::::encode(&interval, &mut buf), + IsNull::No + )); + assert_eq!(&**buf, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + buf.clear(); +} diff --git a/sqlx-core/src/postgres/types/mod.rs b/sqlx-core/src/postgres/types/mod.rs index b65c29d49d..a3eed3160a 100644 --- a/sqlx-core/src/postgres/types/mod.rs +++ b/sqlx-core/src/postgres/types/mod.rs @@ -12,6 +12,7 @@ //! | `f64` | DOUBLE PRECISION, FLOAT8 | //! | `&str`, `String` | VARCHAR, CHAR(N), TEXT, NAME | //! | `&[u8]`, `Vec` | BYTEA | +//! | `sqlx::postgres::types::PgInterval` | INTERVAL | //! //! ### [`chrono`](https://crates.io/crates/chrono) //! @@ -137,6 +138,7 @@ mod bool; mod bytes; mod float; mod int; +mod interval; mod range; mod record; mod str; @@ -163,6 +165,7 @@ mod json; #[cfg(feature = "ipnetwork")] mod ipnetwork; +pub use interval::PgInterval; pub use range::PgRange; // used in derive(Type) for `struct` diff --git a/sqlx-macros/src/database/postgres.rs b/sqlx-macros/src/database/postgres.rs index f68eff6118..3394588a4a 100644 --- a/sqlx-macros/src/database/postgres.rs +++ b/sqlx-macros/src/database/postgres.rs @@ -41,6 +41,8 @@ impl_database_ext! { #[cfg(feature = "time")] sqlx::types::time::OffsetDateTime, + sqlx::postgres::types::PgInterval, + #[cfg(feature = "bigdecimal")] sqlx::types::BigDecimal, diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index a6518c2efc..bf6f7e9b65 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -2,7 +2,7 @@ extern crate time_ as time; use std::ops::Bound; -use sqlx::postgres::types::PgRange; +use sqlx::postgres::types::{PgInterval, PgRange}; use sqlx::postgres::Postgres; use sqlx_test::{test_decode_type, test_prepared_type, test_type}; @@ -360,3 +360,31 @@ test_type!(int4range>(Postgres, "'[1,2)'::int4range" == PgRange::from((INC1, EXC2)), "'[1,2]'::int4range" == PgRange::from((INC1, EXC3)), )); + +test_prepared_type!(interval( + Postgres, + "INTERVAL '1h'" + == PgInterval { + months: 0, + days: 0, + microseconds: 3_600_000_000 + }, + "INTERVAL '-1 hours'" + == PgInterval { + months: 0, + days: 0, + microseconds: -3_600_000_000 + }, + "INTERVAL '3 months 12 days 1h 15 minutes 10 second '" + == PgInterval { + months: 3, + days: 12, + microseconds: (3_600 + 15 * 60 + 10) * 1_000_000 + }, + "INTERVAL '03:10:20.116100'" + == PgInterval { + months: 0, + days: 0, + microseconds: (3 * 3_600 + 10 * 60 + 20) * 1_000_000 + 116100 + }, +)); From 523f650340f5c6f4e20c4b9b0fcc0c525573ba88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Carr?= Date: Sat, 27 Jun 2020 17:42:46 -0700 Subject: [PATCH 2/2] feat(postgres) Add support for std::time::Duration, time::Duration & chrono::Duration --- sqlx-core/src/postgres/types/interval.rs | 260 +++++++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/sqlx-core/src/postgres/types/interval.rs b/sqlx-core/src/postgres/types/interval.rs index f0794068a3..cc7c726547 100644 --- a/sqlx-core/src/postgres/types/interval.rs +++ b/sqlx-core/src/postgres/types/interval.rs @@ -2,6 +2,9 @@ use std::mem; use byteorder::{NetworkEndian, ReadBytesExt}; +#[cfg(any(feature = "chrono", feature = "time"))] +use std::convert::TryFrom; + use crate::decode::Decode; use crate::encode::{Encode, IsNull}; use crate::error::BoxDynError; @@ -66,6 +69,222 @@ impl Encode<'_, Postgres> for PgInterval { } } +impl PgInterval { + /// Convert a `std::time::Duration` object to a `PgInterval` object but truncate the remaining nanoseconds. + /// + /// Returns an error if there is a microseconds overflow. + /// + /// # Example + /// + /// ``` + /// use sqlx_core::postgres::types::PgInterval; + /// let interval = PgInterval::truncate_nanos_std(std::time::Duration::from_secs(3_600)).unwrap(); + /// assert_eq!(interval, PgInterval { months: 0, days: 0, microseconds: 3_600_000_000 }); + /// ``` + pub fn truncate_nanos_std(value: std::time::Duration) -> Result { + let microseconds = i64::try_from(value.as_micros())?; + Ok(Self { + months: 0, + days: 0, + microseconds, + }) + } + /// Convert a `time::Duration` object to a `PgInterval` object but truncate the remaining nanoseconds. + /// + /// Returns an error if there is a microseconds overflow. + /// + /// # Example + /// + /// ``` + /// use sqlx_core::postgres::types::PgInterval; + /// let interval = PgInterval::truncate_nanos_time(time::Duration::seconds(3_600)).unwrap(); + /// assert_eq!(interval, PgInterval { months: 0, days: 0, microseconds: 3_600_000_000 }); + /// ``` + #[cfg(feature = "time")] + pub fn truncate_nanos_time(value: time::Duration) -> Result { + let microseconds = i64::try_from(value.whole_microseconds())?; + Ok(Self { + months: 0, + days: 0, + microseconds, + }) + } + + /// Convert a `chrono::Duration` object to a `PgInterval` object but truncates the remaining nanoseconds. + /// Returns an error if there is a microseconds overflow. + /// + /// # Example + /// + /// ``` + /// use sqlx_core::postgres::types::PgInterval; + /// let interval = PgInterval::truncate_nanos_chrono(chrono::Duration::seconds(3_600)).unwrap(); + /// assert_eq!(interval, PgInterval { months: 0, days: 0, microseconds: 3_600_000_000 }); + /// ``` + #[cfg(feature = "chrono")] + pub fn truncate_nanos_chrono(value: chrono::Duration) -> Result { + let microseconds = value.num_microseconds().ok_or("Microseconds overflow")?; + Ok(Self { + months: 0, + days: 0, + microseconds, + }) + } +} + +impl Type for std::time::Duration { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL + } +} + +impl Type for [std::time::Duration] { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL_ARRAY + } +} + +impl Encode<'_, Postgres> for std::time::Duration { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { + let pg_interval = + PgInterval::try_from(*self).expect("Failed to encode std::time::Duration"); + pg_interval.encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + 2 * mem::size_of::() + } +} + +impl TryFrom for PgInterval { + type Error = BoxDynError; + + /// Convert a `std::time::Duration` to a `PgInterval` + /// + /// This returns an error if there is a loss of precision using nanoseconds or if there is a + /// microsecond overflow + /// + /// To do lossy conversion use `PgInterval::truncate_nanos_std()`. + fn try_from(value: std::time::Duration) -> Result { + match value.as_nanos() { + n if n % 1000 != 0 => { + Err("PostgreSQL INTERVAL does not support nanoseconds precision".into()) + } + _ => Ok(Self { + months: 0, + days: 0, + microseconds: i64::try_from(value.as_micros())?, + }), + } + } +} + +#[cfg(feature = "chrono")] +impl Type for chrono::Duration { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL + } +} + +#[cfg(feature = "chrono")] +impl Type for [chrono::Duration] { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL_ARRAY + } +} + +#[cfg(feature = "chrono")] +impl Encode<'_, Postgres> for chrono::Duration { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { + let pg_interval = PgInterval::try_from(*self).expect("Failed to encode chrono::Duration"); + pg_interval.encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + 2 * mem::size_of::() + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for PgInterval { + type Error = BoxDynError; + + /// Convert a `chrono::Duration` to a `PgInterval` + /// + /// This returns an error if there is a loss of precision using nanoseconds or if there is a + /// microsecond or nanosecond overflow + /// + /// To do a lossy conversion use `PgInterval::truncate_nanos_chrono()`. + fn try_from(value: chrono::Duration) -> Result { + let microseconds = value.num_microseconds().ok_or("Microseconds overflow")?; + match value + .checked_sub(&chrono::Duration::microseconds(microseconds)) + .ok_or("Microseconds overflow")? + .num_nanoseconds() + .ok_or("Nanoseconds overflow")? + { + 0 => Ok(Self { + months: 0, + days: 0, + microseconds, + }), + _ => Err("PostgreSQL INTERVAL does not support nanoseconds precision".into()), + } + } +} + +#[cfg(feature = "time")] +impl Type for time::Duration { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL + } +} + +#[cfg(feature = "time")] +impl Type for [time::Duration] { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INTERVAL_ARRAY + } +} + +#[cfg(feature = "time")] +impl Encode<'_, Postgres> for time::Duration { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { + let pg_interval = PgInterval::try_from(*self).expect("Failed to encode time::Duration"); + pg_interval.encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + 2 * mem::size_of::() + } +} + +#[cfg(feature = "time")] +impl TryFrom for PgInterval { + type Error = BoxDynError; + + /// Convert a `time::Duration` to a `PgInterval` + /// + /// This returns an error if there is a loss of precision using nanoseconds or if there is a + /// microsecond overflow + /// + /// To do a lossy conversion use `PgInterval::time_truncate_nanos()`. + fn try_from(value: time::Duration) -> Result { + let microseconds = i64::try_from(value.whole_microseconds())?; + match value + .checked_sub(time::Duration::microseconds(microseconds)) + .ok_or("Microseconds overflow")? + .subsec_nanoseconds() + { + 0 => Ok(Self { + months: 0, + days: 0, + microseconds, + }), + _ => Err("PostgreSQL INTERVAL does not support nanoseconds precision".into()), + } + } +} + #[test] fn test_encode_interval() { let mut buf = PgArgumentBuffer::default(); @@ -145,3 +364,44 @@ fn test_encode_interval() { assert_eq!(&**buf, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); buf.clear(); } + +#[test] +fn test_pginterval_std() { + let interval = PgInterval { + days: 0, + months: 0, + microseconds: 27_000, + }; + assert_eq!( + &PgInterval::try_from(std::time::Duration::from_micros(27_000)).unwrap(), + &interval + ); +} + +#[test] +#[cfg(feature = "chrono")] +fn test_pginterval_chrono() { + let interval = PgInterval { + days: 0, + months: 0, + microseconds: 27_000, + }; + assert_eq!( + &PgInterval::try_from(chrono::Duration::microseconds(27_000)).unwrap(), + &interval + ); +} + +#[test] +#[cfg(feature = "time")] +fn test_pginterval_time() { + let interval = PgInterval { + days: 0, + months: 0, + microseconds: 27_000, + }; + assert_eq!( + &PgInterval::try_from(time::Duration::microseconds(27_000)).unwrap(), + &interval + ); +}