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] 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 f0794068..cc7c7265 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 + ); +}