diff --git a/postgres-types/Cargo.toml b/postgres-types/Cargo.toml index 4fd69f61..8fc6ed10 100644 --- a/postgres-types/Cargo.toml +++ b/postgres-types/Cargo.toml @@ -22,6 +22,7 @@ with-geo-types-0_7 = ["geo-types-0_7"] with-serde_json-1 = ["serde-1", "serde_json-1"] with-uuid-0_8 = ["uuid-08"] with-time-0_2 = ["time-02"] +with-time-0_3 = ["time-03"] [dependencies] bytes = "1.0" @@ -40,3 +41,4 @@ serde-1 = { version = "1.0", package = "serde", optional = true } serde_json-1 = { version = "1.0", package = "serde_json", optional = true } uuid-08 = { version = "0.8", package = "uuid", optional = true } time-02 = { version = "0.2", package = "time", optional = true } +time-03 = { version = "0.3", package = "time", default-features = false, optional = true } diff --git a/postgres-types/src/lib.rs b/postgres-types/src/lib.rs index 4dd87c71..2a953db2 100644 --- a/postgres-types/src/lib.rs +++ b/postgres-types/src/lib.rs @@ -209,6 +209,8 @@ mod geo_types_07; mod serde_json_1; #[cfg(feature = "with-time-0_2")] mod time_02; +#[cfg(feature = "with-time-0_3")] +mod time_03; #[cfg(feature = "with-uuid-0_8")] mod uuid_08; diff --git a/postgres-types/src/time_03.rs b/postgres-types/src/time_03.rs new file mode 100644 index 00000000..f136fab7 --- /dev/null +++ b/postgres-types/src/time_03.rs @@ -0,0 +1,108 @@ +use bytes::BytesMut; +use postgres_protocol::types; +use std::convert::TryFrom; +use std::error::Error; +use time_03::{Date, Duration, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; + +use crate::{FromSql, IsNull, ToSql, Type}; + +fn base() -> PrimitiveDateTime { + PrimitiveDateTime::new(Date::from_ordinal_date(2000, 1).unwrap(), Time::MIDNIGHT) +} + +impl<'a> FromSql<'a> for PrimitiveDateTime { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + let t = types::timestamp_from_sql(raw)?; + Ok(base() + Duration::microseconds(t)) + } + + accepts!(TIMESTAMP); +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result> { + let time = match i64::try_from((*self - base()).whole_microseconds()) { + Ok(time) => time, + Err(_) => return Err("value too large to transmit".into()), + }; + types::timestamp_to_sql(time, w); + Ok(IsNull::No) + } + + accepts!(TIMESTAMP); + to_sql_checked!(); +} + +impl<'a> FromSql<'a> for OffsetDateTime { + fn from_sql(type_: &Type, raw: &[u8]) -> Result> { + let primitive = PrimitiveDateTime::from_sql(type_, raw)?; + Ok(primitive.assume_utc()) + } + + accepts!(TIMESTAMPTZ); +} + +impl ToSql for OffsetDateTime { + fn to_sql( + &self, + type_: &Type, + w: &mut BytesMut, + ) -> Result> { + let utc_datetime = self.to_offset(UtcOffset::UTC); + let date = utc_datetime.date(); + let time = utc_datetime.time(); + let primitive = PrimitiveDateTime::new(date, time); + primitive.to_sql(type_, w) + } + + accepts!(TIMESTAMPTZ); + to_sql_checked!(); +} + +impl<'a> FromSql<'a> for Date { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + let jd = types::date_from_sql(raw)?; + Ok(base().date() + Duration::days(i64::from(jd))) + } + + accepts!(DATE); +} + +impl ToSql for Date { + fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result> { + let jd = (*self - base().date()).whole_days(); + if jd > i64::from(i32::max_value()) || jd < i64::from(i32::min_value()) { + return Err("value too large to transmit".into()); + } + + types::date_to_sql(jd as i32, w); + Ok(IsNull::No) + } + + accepts!(DATE); + to_sql_checked!(); +} + +impl<'a> FromSql<'a> for Time { + fn from_sql(_: &Type, raw: &[u8]) -> Result> { + let usec = types::time_from_sql(raw)?; + Ok(Time::MIDNIGHT + Duration::microseconds(usec)) + } + + accepts!(TIME); +} + +impl ToSql for Time { + fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result> { + let delta = *self - Time::MIDNIGHT; + let time = match i64::try_from(delta.whole_microseconds()) { + Ok(time) => time, + Err(_) => return Err("value too large to transmit".into()), + }; + types::time_to_sql(time, w); + Ok(IsNull::No) + } + + accepts!(TIME); + to_sql_checked!(); +} diff --git a/postgres/Cargo.toml b/postgres/Cargo.toml index ca1d0b23..3d1c2023 100644 --- a/postgres/Cargo.toml +++ b/postgres/Cargo.toml @@ -31,6 +31,7 @@ with-geo-types-0_7 = ["tokio-postgres/with-geo-types-0_7"] with-serde_json-1 = ["tokio-postgres/with-serde_json-1"] with-uuid-0_8 = ["tokio-postgres/with-uuid-0_8"] with-time-0_2 = ["tokio-postgres/with-time-0_2"] +with-time-0_3 = ["tokio-postgres/with-time-0_3"] [dependencies] bytes = "1.0" diff --git a/postgres/src/lib.rs b/postgres/src/lib.rs index 7d96bfd9..a599532e 100644 --- a/postgres/src/lib.rs +++ b/postgres/src/lib.rs @@ -61,7 +61,8 @@ //! | `with-geo-types-0_7` | Enable support for the 0.7 version of the `geo-types` crate. | [geo-types](https://crates.io/crates/geo-types/0.7.0) 0.7 | no | //! | `with-serde_json-1` | Enable support for the `serde_json` crate. | [serde_json](https://crates.io/crates/serde_json) 1.0 | no | //! | `with-uuid-0_8` | Enable support for the `uuid` crate. | [uuid](https://crates.io/crates/uuid) 0.8 | no | -//! | `with-time-0_2` | Enable support for the `time` crate. | [time](https://crates.io/crates/time) 0.2 | no | +//! | `with-time-0_2` | Enable support for the 0.2 version of the `time` crate. | [time](https://crates.io/crates/time/0.2.0) 0.2 | no | +//! | `with-time-0_3` | Enable support for the 0.3 version of the `time` crate. | [time](https://crates.io/crates/time/0.3.0) 0.3 | no | #![warn(clippy::all, rust_2018_idioms, missing_docs)] pub use fallible_iterator; diff --git a/tokio-postgres/Cargo.toml b/tokio-postgres/Cargo.toml index 1bedf6b5..d35a323a 100644 --- a/tokio-postgres/Cargo.toml +++ b/tokio-postgres/Cargo.toml @@ -37,6 +37,7 @@ with-geo-types-0_7 = ["postgres-types/with-geo-types-0_7"] with-serde_json-1 = ["postgres-types/with-serde_json-1"] with-uuid-0_8 = ["postgres-types/with-uuid-0_8"] with-time-0_2 = ["postgres-types/with-time-0_2"] +with-time-0_3 = ["postgres-types/with-time-0_3"] [dependencies] async-trait = "0.1" @@ -70,4 +71,4 @@ serde-1 = { version = "1.0", package = "serde" } serde_json-1 = { version = "1.0", package = "serde_json" } uuid-08 = { version = "0.8", package = "uuid" } time-02 = { version = "0.2", package = "time" } - +time-03 = { version = "0.3", package = "time", features = ["parsing"] } diff --git a/tokio-postgres/src/lib.rs b/tokio-postgres/src/lib.rs index 6dd0b015..e9516e0b 100644 --- a/tokio-postgres/src/lib.rs +++ b/tokio-postgres/src/lib.rs @@ -112,7 +112,8 @@ //! | `with-geo-types-0_7` | Enable support for the 0.7 version of the `geo-types` crate. | [geo-types](https://crates.io/crates/geo-types/0.7.0) 0.7 | no | //! | `with-serde_json-1` | Enable support for the `serde_json` crate. | [serde_json](https://crates.io/crates/serde_json) 1.0 | no | //! | `with-uuid-0_8` | Enable support for the `uuid` crate. | [uuid](https://crates.io/crates/uuid) 0.8 | no | -//! | `with-time-0_2` | Enable support for the `time` crate. | [time](https://crates.io/crates/time) 0.2 | no | +//! | `with-time-0_2` | Enable support for the 0.2 version of the `time` crate. | [time](https://crates.io/crates/time/0.2.0) 0.2 | no | +//! | `with-time-0_3` | Enable support for the 0.3 version of the `time` crate. | [time](https://crates.io/crates/time/0.3.0) 0.3 | no | #![doc(html_root_url = "https://docs.rs/tokio-postgres/0.7")] #![warn(rust_2018_idioms, clippy::all, missing_docs)] diff --git a/tokio-postgres/tests/test/types/mod.rs b/tokio-postgres/tests/test/types/mod.rs index 54a111b3..604e2de3 100644 --- a/tokio-postgres/tests/test/types/mod.rs +++ b/tokio-postgres/tests/test/types/mod.rs @@ -29,6 +29,8 @@ mod geo_types_07; mod serde_json_1; #[cfg(feature = "with-time-0_2")] mod time_02; +#[cfg(feature = "with-time-0_3")] +mod time_03; #[cfg(feature = "with-uuid-0_8")] mod uuid_08; diff --git a/tokio-postgres/tests/test/types/time_03.rs b/tokio-postgres/tests/test/types/time_03.rs new file mode 100644 index 00000000..df013c9b --- /dev/null +++ b/tokio-postgres/tests/test/types/time_03.rs @@ -0,0 +1,149 @@ +use time_03::{format_description, OffsetDateTime, PrimitiveDateTime}; +use tokio_postgres::types::{Date, Timestamp}; + +use crate::types::test_type; + +// time 0.2 does not [yet?] support parsing fractional seconds +// https://github.com/time-rs/time/issues/226 + +#[tokio::test] +async fn test_primitive_date_time_params() { + fn make_check(time: &str) -> (Option, &str) { + let format = + format_description::parse("'[year]-[month]-[day] [hour]:[minute]:[second]'").unwrap(); + (Some(PrimitiveDateTime::parse(time, &format).unwrap()), time) + } + test_type( + "TIMESTAMP", + &[ + make_check("'1970-01-01 00:00:00'"), // .010000000 + make_check("'1965-09-25 11:19:33'"), // .100314000 + make_check("'2010-02-09 23:11:45'"), // .120200000 + (None, "NULL"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_with_special_primitive_date_time_params() { + fn make_check(time: &str) -> (Timestamp, &str) { + let format = + format_description::parse("'[year]-[month]-[day] [hour]:[minute]:[second]'").unwrap(); + ( + Timestamp::Value(PrimitiveDateTime::parse(time, &format).unwrap()), + time, + ) + } + test_type( + "TIMESTAMP", + &[ + make_check("'1970-01-01 00:00:00'"), // .010000000 + make_check("'1965-09-25 11:19:33'"), // .100314000 + make_check("'2010-02-09 23:11:45'"), // .120200000 + (Timestamp::PosInfinity, "'infinity'"), + (Timestamp::NegInfinity, "'-infinity'"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_offset_date_time_params() { + fn make_check(time: &str) -> (Option, &str) { + let format = + format_description::parse("'[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]'").unwrap(); + (Some(OffsetDateTime::parse(time, &format).unwrap()), time) + } + test_type( + "TIMESTAMP WITH TIME ZONE", + &[ + make_check("'1970-01-01 00:00:00 +0000'"), // .010000000 + make_check("'1965-09-25 11:19:33 +0000'"), // .100314000 + make_check("'2010-02-09 23:11:45 +0000'"), // .120200000 + (None, "NULL"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_with_special_offset_date_time_params() { + fn make_check(time: &str) -> (Timestamp, &str) { + let format = + format_description::parse("'[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]'").unwrap(); + ( + Timestamp::Value(OffsetDateTime::parse(time, &format).unwrap()), + time, + ) + } + test_type( + "TIMESTAMP WITH TIME ZONE", + &[ + make_check("'1970-01-01 00:00:00 +0000'"), // .010000000 + make_check("'1965-09-25 11:19:33 +0000'"), // .100314000 + make_check("'2010-02-09 23:11:45 +0000'"), // .120200000 + (Timestamp::PosInfinity, "'infinity'"), + (Timestamp::NegInfinity, "'-infinity'"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_date_params() { + fn make_check(date: &str) -> (Option, &str) { + let format = format_description::parse("'[year]-[month]-[day]'").unwrap(); + (Some(time_03::Date::parse(date, &format).unwrap()), date) + } + test_type( + "DATE", + &[ + make_check("'1970-01-01'"), + make_check("'1965-09-25'"), + make_check("'2010-02-09'"), + (None, "NULL"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_with_special_date_params() { + fn make_check(date: &str) -> (Date, &str) { + let format = format_description::parse("'[year]-[month]-[day]'").unwrap(); + ( + Date::Value(time_03::Date::parse(date, &format).unwrap()), + date, + ) + } + test_type( + "DATE", + &[ + make_check("'1970-01-01'"), + make_check("'1965-09-25'"), + make_check("'2010-02-09'"), + (Date::PosInfinity, "'infinity'"), + (Date::NegInfinity, "'-infinity'"), + ], + ) + .await; +} + +#[tokio::test] +async fn test_time_params() { + fn make_check(time: &str) -> (Option, &str) { + let format = format_description::parse("'[hour]:[minute]:[second]'").unwrap(); + (Some(time_03::Time::parse(time, &format).unwrap()), time) + } + test_type( + "TIME", + &[ + make_check("'00:00:00'"), // .010000000 + make_check("'11:19:33'"), // .100314000 + make_check("'23:11:45'"), // .120200000 + (None, "NULL"), + ], + ) + .await; +}