From d389f30c0e5fc7f3e61579c6d0103537fbca3f7a Mon Sep 17 00:00:00 2001 From: vados Date: Sun, 10 Mar 2024 21:55:27 +0900 Subject: [PATCH] feat(pgrx): add chrono feature for easy conversions This commit adds a `chrono` feature flag to `pgrx`, which enables conversions between `pgrx` native date/time (with or without timezone) and `chrono` types like `NaiveDateTime`. --- Cargo.lock | 63 +++++++ Cargo.toml | 1 + README.md | 3 + pgrx-tests/Cargo.toml | 2 + pgrx-tests/src/tests/chrono_tests.rs | 75 +++++++++ pgrx-tests/src/tests/mod.rs | 2 + pgrx/Cargo.toml | 2 + pgrx/src/datum/datetime_support/chrono.rs | 194 ++++++++++++++++++++++ pgrx/src/datum/datetime_support/mod.rs | 3 + 9 files changed, 345 insertions(+) create mode 100644 pgrx-tests/src/tests/chrono_tests.rs create mode 100644 pgrx/src/datum/datetime_support/chrono.rs diff --git a/Cargo.lock b/Cargo.lock index 935f00576a..13a243fb95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -427,6 +442,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -994,6 +1023,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1437,6 +1489,7 @@ dependencies = [ "atomic-traits", "bitflags 2.4.2", "bitvec", + "chrono", "enum-map", "heapless", "libc", @@ -1522,6 +1575,7 @@ dependencies = [ name = "pgrx-tests" version = "0.12.0-alpha.1" dependencies = [ + "chrono", "clap-cargo", "eyre", "libc", @@ -2903,6 +2957,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 02085b7719..6f6cf1a722 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ thiserror = "1" unescape = "0.1.0" # for escaped-character-handling url = "2.4.1" # the non-existent std::web walkdir = "2" # directory recursion +chrono = "0.4.35" # conversions to chrono data structures [profile.dev] # Only include line tables in debuginfo. This reduces the size of target/ (after diff --git a/README.md b/README.md index 1f9455f229..023ebc7b45 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,9 @@ ASCII nor UTF-8 (as Postgres will then accept but ignore non-ASCII bytes). For best results, always use PGRX with UTF-8, and set database encodings explicitly upon database creation. +To easily convert `pgrx` temporal types (`pgrx::TimestampWithTimezone`, etc) +to [`chrono`] compatible types, enable the `chrono` feature. + ## Digging Deeper - [cargo-pgrx sub-command](cargo-pgrx/) diff --git a/pgrx-tests/Cargo.toml b/pgrx-tests/Cargo.toml index 9edd561aee..2485a88bc0 100644 --- a/pgrx-tests/Cargo.toml +++ b/pgrx-tests/Cargo.toml @@ -38,6 +38,7 @@ pg_test = [ ] proptest = [ "dep:proptest" ] cshim = [ "pgrx/cshim" ] no-schema-generation = [ "pgrx/no-schema-generation", "pgrx-macros/no-schema-generation" ] +chrono = [ "dep:chrono", "pgrx/chrono" ] [package.metadata.docs.rs] features = ["pg14", "proptest"] @@ -65,6 +66,7 @@ serde = "1.0" serde_json = "1.0" sysinfo = "0.29.10" rand = "0.8.5" +chrono = { workspace = true, optional = true } [dependencies.pgrx] # Not unified in workspace due to default-features key path = "../pgrx" diff --git a/pgrx-tests/src/tests/chrono_tests.rs b/pgrx-tests/src/tests/chrono_tests.rs new file mode 100644 index 0000000000..7fe65ac61a --- /dev/null +++ b/pgrx-tests/src/tests/chrono_tests.rs @@ -0,0 +1,75 @@ +//! Tests for the `chrono` features of `cargo-pgrx` +//! +#![cfg(feature = "chrono")] + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + #[allow(unused_imports)] + use crate as pgrx_tests; + + use std::result::Result; + + use chrono::{Datelike as _, Timelike as _, Utc}; + + use pgrx::pg_test; + use pgrx::DateTimeConversionError; + + // Utility class for errors + type DtcResult = Result; + + /// Ensure simple conversion ([`pgrx::Date`] -> [`chrono::NaiveDate`]) works + #[pg_test] + fn chrono_simple_date_conversion() -> DtcResult<()> { + let original = pgrx::Date::new(1970, 1, 1)?; + let d = chrono::NaiveDate::try_from(original)?; + assert_eq!(d.year(), original.year(), "year matches"); + assert_eq!(d.month(), 1, "month matches"); + assert_eq!(d.day(), 1, "day matches"); + let backwards = pgrx::Date::try_from(d)?; + assert_eq!(backwards, original); + Ok(()) + } + + /// Ensure simple conversion ([`pgrx::Time`] -> [`chrono::NaiveTime`]) works + #[pg_test] + fn chrono_simple_time_conversion() -> DtcResult<()> { + let original = pgrx::Time::new(12, 1, 59.0000001)?; + let d = chrono::NaiveTime::try_from(original)?; + assert_eq!(d.hour(), 12, "hours match"); + assert_eq!(d.minute(), 1, "minutes match"); + assert_eq!(d.second(), 59, "seconds match"); + assert_eq!(d.nanosecond(), 0, "nanoseconds are zero (pg only supports microseconds)"); + let backwards = pgrx::Time::try_from(d)?; + assert_eq!(backwards, original); + Ok(()) + } + + /// Ensure simple conversion ([`pgrx::Timestamp`] -> [`chrono::NaiveDateTime`]) works + #[pg_test] + fn chrono_simple_timestamp_conversion() -> DtcResult<()> { + let original = pgrx::Timestamp::new(1970, 1, 1, 1, 1, 1.0)?; + let d = chrono::NaiveDateTime::try_from(original)?; + assert_eq!(d.hour(), 1, "hours match"); + assert_eq!(d.minute(), 1, "minutes match"); + assert_eq!(d.second(), 1, "seconds match"); + assert_eq!(d.nanosecond(), 0, "nanoseconds are zero (pg only supports microseconds)"); + let backwards = pgrx::Timestamp::try_from(d)?; + assert_eq!(backwards, original, "NaiveDateTime -> Timestamp return conversion failed"); + Ok(()) + } + + /// Ensure simple conversion ([`pgrx::TimestampWithTimeZone`] -> [`chrono::DateTime`]) works + #[pg_test] + fn chrono_simple_datetime_with_time_zone_conversion() -> DtcResult<()> { + let original = pgrx::TimestampWithTimeZone::with_timezone(1970, 1, 1, 1, 1, 1.0, "utc")?; + let d = chrono::DateTime::::try_from(original)?; + assert_eq!(d.hour(), 1, "hours match"); + assert_eq!(d.minute(), 1, "minutes match"); + assert_eq!(d.second(), 1, "seconds match"); + assert_eq!(d.nanosecond(), 0, "nanoseconds are zero (pg only supports microseconds)"); + let backwards = pgrx::TimestampWithTimeZone::try_from(d)?; + assert_eq!(backwards, original); + Ok(()) + } +} diff --git a/pgrx-tests/src/tests/mod.rs b/pgrx-tests/src/tests/mod.rs index f433f217c2..b3c12671b8 100644 --- a/pgrx-tests/src/tests/mod.rs +++ b/pgrx-tests/src/tests/mod.rs @@ -14,6 +14,8 @@ mod attributes_tests; mod bgworker_tests; mod bytea_tests; mod cfg_tests; +#[cfg(feature = "chrono")] +mod chrono_tests; mod composite_type_tests; mod datetime_tests; mod default_arg_value_tests; diff --git a/pgrx/Cargo.toml b/pgrx/Cargo.toml index 8a14757bbb..55451edfff 100644 --- a/pgrx/Cargo.toml +++ b/pgrx/Cargo.toml @@ -35,6 +35,7 @@ pg15 = [ "pgrx-pg-sys/pg15" ] pg16 = [ "pgrx-pg-sys/pg16" ] no-schema-generation = ["pgrx-macros/no-schema-generation", "pgrx-sql-entity-graph/no-schema-generation"] unsafe-postgres = [] # when trying to compile against something that looks like Postgres but claims to be diffent +chrono = [ "dep:chrono" ] [package.metadata.docs.rs] features = ["pg14", "cshim"] @@ -59,6 +60,7 @@ enum-map = "2.6.3" atomic-traits = "0.3.0" # PgAtomic and shmem init bitflags = "2.4.0" # BackgroundWorker bitvec = "1.0" # processing array nullbitmaps +chrono = { workspace = true, optional = true } # Conversions to chrono date time types heapless = "0.8" # shmem and PgLwLock libc.workspace = true # FFI type compat seahash = "4.1.0" # derive(PostgresHash) diff --git a/pgrx/src/datum/datetime_support/chrono.rs b/pgrx/src/datum/datetime_support/chrono.rs new file mode 100644 index 0000000000..2104ada8b8 --- /dev/null +++ b/pgrx/src/datum/datetime_support/chrono.rs @@ -0,0 +1,194 @@ +//! This module contains implementations and functionality that enables [`pgrx`] types (ex. [`pgrx::datum::Date`]) +//! to be converted to [`chrono`] data types (ex. [`chrono::Date`]) +//! +//! Note that `chrono` has no reasonable analog for the `time with timezone` (i.e. [`pgrx::TimeWithTimeZone`]), so there are no added conversions for that type outside of the ones already implemented. +#![cfg(feature = "chrono")] + +use core::convert::Infallible; +use core::num::TryFromIntError; +use std::convert::TryFrom; + +use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc}; + +use crate::datum::datetime_support::DateTimeConversionError; +use crate::datum::{Date, Time, Timestamp, TimestampWithTimeZone}; + +/// Convenience type for [`Result`]s that fail with a [`DateTimeConversionError`] +type DtcResult = Result; + +impl From for DateTimeConversionError { + fn from(_tfie: TryFromIntError) -> Self { + DateTimeConversionError::FieldOverflow + } +} + +impl From for DateTimeConversionError { + fn from(_i: Infallible) -> Self { + DateTimeConversionError::FieldOverflow + } +} + +impl TryFrom for NaiveDate { + type Error = DateTimeConversionError; + + fn try_from(d: Date) -> DtcResult { + NaiveDate::from_ymd_opt(d.year(), d.month().into(), d.day().into()) + .ok_or_else(|| DateTimeConversionError::InvalidFormat) + } +} + +impl TryFrom for Date { + type Error = DateTimeConversionError; + + fn try_from(d: NaiveDate) -> DtcResult { + let month = u8::try_from(d.month())?; + let day = u8::try_from(d.day())?; + Date::new(d.year(), month, day) + } +} + +/// Note: conversions from Postgres' `time` type [`pgrx::Time`] to [`chrono::NaiveTime`] +/// incur a loss of precision as Postgres only exposes microseconds. +impl TryFrom