diff --git a/Cargo.toml b/Cargo.toml index d068f7c828..5954b0dca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,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 2020148e41..34b4d0f00d 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,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 405723d688..2ee19a97de 100644 --- a/pgrx-tests/Cargo.toml +++ b/pgrx-tests/Cargo.toml @@ -39,6 +39,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" ] nightly = [ "pgrx/nightly" ] [package.metadata.docs.rs] @@ -67,6 +68,7 @@ postgres = "0.19.7" proptest = { version = "1", optional = true } 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 fb1ed66035..ec017445be 100644 --- a/pgrx/Cargo.toml +++ b/pgrx/Cargo.toml @@ -28,15 +28,16 @@ crate-type = [ "rlib" ] [features] default = [ "cshim" ] +chrono = [ "dep:chrono" ] cshim = [ "pgrx-pg-sys/cshim" ] pg12 = [ "pgrx-pg-sys/pg12" ] pg13 = [ "pgrx-pg-sys/pg13" ] pg14 = [ "pgrx-pg-sys/pg14" ] pg15 = [ "pgrx-pg-sys/pg15" ] pg16 = [ "pgrx-pg-sys/pg16" ] +nightly = [] # For features and functionality which require nightly Rust - for example, std::mem::allocator. 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 different -nightly = [] # For features and functionality which require nightly Rust - for example, std::mem::allocator. [package.metadata.docs.rs] features = ["pg14", "cshim"] @@ -60,6 +61,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