From 49463a82e06c23f8e9d5e65d83ab31be884641e1 Mon Sep 17 00:00:00 2001 From: Eric Ridge Date: Tue, 23 May 2023 11:14:43 -0400 Subject: [PATCH] Date time overhaul (#1139) Working on overhauling pgrx's `Date`, `Time`, `TimeWithTimeZone`, `Timestamp`, `TimestampWithTimeZone`, and `Interval` types. We now delegate most everything back into Postgres so we're no longer responsible for re-implementing date/time math which is impossible to get correct. The various types can now actually be constructed, and the ones `with time zone` support construction at a specific timezone and also being shifted to a specific timezone. I've also ripped out support for the `time` crate -- the code around that is too much highly-specific knowledge between both Postgres and `time` in order to confidently maintain going forward. Users that use that will need to make their own wrappers to and implement `time` support themselves. Some key changes and features are: - [x] Better error handling -- need to invent a custom error type instead of the generic `PgSqlErrorCodes` - [x] More `impl From` conversions between the types - [x] `impl Add/Sub` - ~~`impl Index` if possible so that `let hour = timestamp[DateTimeParts::Hour];` ~~ not possible - [x] Drastically simplify the `Serialize` implementations and also properly implement the corresponding `Deserialize` trait - [x] doc comments - [x] additional unit tests - [x] top-level constructor functions like `now()`, `current_timestamp()`, etc. - [x] misc. functions from https://www.postgresql.org/docs/15/functions-datetime.html like `age` and `date_trunc` - [x] an example that shows off some of the simple usage --- .github/workflows/tests.yml | 3 + Cargo.lock | 150 ++--- Cargo.toml | 1 + README.md | 7 - pgrx-examples/datetime/.gitignore | 7 + pgrx-examples/datetime/Cargo.toml | 33 ++ pgrx-examples/datetime/README.md | 1 + pgrx-examples/datetime/datetime.control | 5 + pgrx-examples/datetime/src/lib.rs | 199 +++++++ pgrx-pg-sys/include/pg11.h | 2 + pgrx-pg-sys/include/pg12.h | 2 + pgrx-pg-sys/include/pg13.h | 1 + pgrx-pg-sys/include/pg14.h | 1 + pgrx-pg-sys/include/pg15.h | 1 + pgrx-tests/Cargo.toml | 2 - pgrx-tests/src/tests/datetime_tests.rs | 231 +++++--- pgrx/Cargo.toml | 1 - pgrx/src/datum/date.rs | 262 +++++---- pgrx/src/datum/datetime_support/ctor.rs | 128 +++++ pgrx/src/datum/datetime_support/mod.rs | 635 +++++++++++++++++++++ pgrx/src/datum/datetime_support/ops.rs | 367 ++++++++++++ pgrx/src/datum/interval.rs | 327 +++++++---- pgrx/src/datum/mod.rs | 2 + pgrx/src/datum/time.rs | 208 +++++-- pgrx/src/datum/time_stamp.rs | 341 +++++++---- pgrx/src/datum/time_stamp_with_timezone.rs | 475 ++++++++++----- pgrx/src/datum/time_with_timezone.rs | 321 +++++++++-- pgrx/src/prelude.rs | 6 +- 28 files changed, 3000 insertions(+), 719 deletions(-) create mode 100644 pgrx-examples/datetime/.gitignore create mode 100644 pgrx-examples/datetime/Cargo.toml create mode 100644 pgrx-examples/datetime/README.md create mode 100644 pgrx-examples/datetime/datetime.control create mode 100644 pgrx-examples/datetime/src/lib.rs create mode 100644 pgrx/src/datum/datetime_support/ctor.rs create mode 100644 pgrx/src/datum/datetime_support/mod.rs create mode 100644 pgrx/src/datum/datetime_support/ops.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4965b7697..1845419d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -191,6 +191,9 @@ jobs: - name: Run custom_sql example tests run: cargo test --package custom_sql --features "pg$PG_VER" --no-default-features + - name: Run datetime example tests + run: cargo test --package datetime --features "pg$PG_VER" --no-default-features + - name: Run errors example tests run: cargo test --package errors --features "pg$PG_VER" --no-default-features diff --git a/Cargo.lock b/Cargo.lock index 4ef3bc4f7..6ceaf7b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "bytea" @@ -411,9 +411,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.2.5" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1f23fa97e1d1641371b51f35535cb26959b8e27ab50d167a8b996b5bada819" +checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", "clap_derive", @@ -433,9 +433,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.5" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdc5d93c358224b4d6867ef1356d740de2303e9892edc06c5340daeccd96bab" +checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" dependencies = [ "anstream", "anstyle", @@ -455,7 +455,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -635,6 +635,15 @@ dependencies = [ "serde", ] +[[package]] +name = "datetime" +version = "0.1.0" +dependencies = [ + "pgrx", + "pgrx-tests", + "rand", +] + [[package]] name = "digest" version = "0.10.6" @@ -857,7 +866,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -1065,9 +1074,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -1086,9 +1095,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libflate" @@ -1137,9 +1146,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "lock_api" @@ -1361,7 +1370,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -1576,7 +1585,6 @@ dependencies = [ "serde_json", "sysinfo", "thiserror", - "time", ] [[package]] @@ -1629,9 +1637,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" @@ -1708,9 +1716,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" dependencies = [ "unicode-ident", ] @@ -1726,9 +1734,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -1913,9 +1921,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.18" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", @@ -2000,9 +2008,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" dependencies = [ "bitflags", "core-foundation", @@ -2013,9 +2021,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -2056,9 +2064,9 @@ checksum = "e6b44e8fc93a14e66336d230954dda83d18b4605ccace8fe09bc7514a71ad0bc" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] @@ -2087,13 +2095,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -2185,9 +2193,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d283f86695ae989d1e18440a943880967156325ba025f05049946bff47bcc2b" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", "windows-sys 0.48.0", @@ -2304,9 +2312,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ "proc-macro2", "quote", @@ -2397,7 +2405,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -2412,9 +2420,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -2424,15 +2432,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -2454,9 +2462,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", @@ -2486,7 +2494,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "socket2 0.5.2", + "socket2 0.5.3", "tokio", "tokio-util", ] @@ -2559,14 +2567,14 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -2707,9 +2715,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom", ] @@ -2766,9 +2774,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2776,24 +2784,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2801,28 +2809,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3027,9 +3035,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69af645a61644c6dd379ade8b77cc87efb5393c988707bad12d3c8e00c50f669" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] @@ -3045,9 +3053,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index cd734e4f2..ab7a634ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "pgrx-examples/custom_libname", "pgrx-examples/custom_types", "pgrx-examples/custom_sql", + "pgrx-examples/datetime", "pgrx-examples/errors", "pgrx-examples/nostd", "pgrx-examples/numeric", diff --git a/README.md b/README.md index 1ceaf3439..b4fa4c2a5 100644 --- a/README.md +++ b/README.md @@ -252,13 +252,6 @@ will be built into the `cargo-pgrx` subcommand and make use of https://github.co PGRX has optional feature flags for Rust code that do not involve configuring the version of Postgres used, but rather extend additional support for other kinds of Rust code. These are not included by default. -### "time-crate": interop with the `time` crate - -`pgrx` once used direct interop with the excellent [time crate][timecrate]. -However, due to complications involving performance and accurate interop with Postgres, -this feature is now considered deprecated in favor of a lower-overhead interop. -You may still request implementations of `TryFrom for pgrx::MatchingType` -and `From for pgrx::MatchingType` by enabling the `"time-crate"` feature. ### "unsafe-postgres": Allow compilation for Postgres forks that have a different ABI diff --git a/pgrx-examples/datetime/.gitignore b/pgrx-examples/datetime/.gitignore new file mode 100644 index 000000000..bdbe7939b --- /dev/null +++ b/pgrx-examples/datetime/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.idea/ +/target +*.iml +**/*.rs.bk +Cargo.lock +sql/arrays-1.0.sql diff --git a/pgrx-examples/datetime/Cargo.toml b/pgrx-examples/datetime/Cargo.toml new file mode 100644 index 000000000..198054499 --- /dev/null +++ b/pgrx-examples/datetime/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "datetime" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[features] +default = ["pg13"] +pg11 = ["pgrx/pg11", "pgrx-tests/pg11" ] +pg12 = ["pgrx/pg12", "pgrx-tests/pg12" ] +pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ] +pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ] +pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ] +pg_test = [] + +[dependencies] +pgrx = { path = "../../pgrx", default-features = false } +rand = "0.8" + +[dev-dependencies] +pgrx-tests = { path = "../../pgrx-tests" } + +# uncomment these if compiling outside of 'pgrx' +# [profile.dev] +# panic = "unwind" + +# [profile.release] +# panic = "unwind" +# opt-level = 3 +# lto = "fat" +# codegen-units = 1 diff --git a/pgrx-examples/datetime/README.md b/pgrx-examples/datetime/README.md new file mode 100644 index 000000000..74af39fdd --- /dev/null +++ b/pgrx-examples/datetime/README.md @@ -0,0 +1 @@ +Examples for working with Dates and Times with pgrx. \ No newline at end of file diff --git a/pgrx-examples/datetime/datetime.control b/pgrx-examples/datetime/datetime.control new file mode 100644 index 000000000..30b4797b3 --- /dev/null +++ b/pgrx-examples/datetime/datetime.control @@ -0,0 +1,5 @@ +comment = 'arrays: Created by pgrx' +default_version = '0.1.0' +module_pathname = '$libdir/datetime' +relocatable = false +superuser = false \ No newline at end of file diff --git a/pgrx-examples/datetime/src/lib.rs b/pgrx-examples/datetime/src/lib.rs new file mode 100644 index 000000000..39c8baf9f --- /dev/null +++ b/pgrx-examples/datetime/src/lib.rs @@ -0,0 +1,199 @@ +/* +Portions Copyright 2019-2021 ZomboDB, LLC. +Portions Copyright 2021-2022 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the MIT license that can be found in the LICENSE file. +*/ + +use pgrx::prelude::*; +use rand::Rng; + +pgrx::pg_module_magic!(); + +#[pg_extern(name = "to_iso_string", immutable, parallel_safe)] +fn to_iso_string(tsz: TimestampWithTimeZone) -> String { + tsz.to_iso_string() +} + +/// ```sql +/// datetime=# select +/// to_iso_string(now()) as my_timezone, +/// to_iso_string(now(), 'PDT') as "PDT", +/// to_iso_string(now(), 'MDT') as "MDT", +/// to_iso_string(now(), 'EDT') as "EDT", +/// to_iso_string(now(), 'UTC') as "UTC"; +/// -[ RECORD 1 ]--------------------------------- +/// my_timezone | 2023-05-19T12:38:06.343021-04:00 +/// PDT | 2023-05-19T09:38:06.343021-07:00 +/// MDT | 2023-05-19T10:38:06.343021-06:00 +/// EDT | 2023-05-19T12:38:06.343021-04:00 +/// UTC | 2023-05-19T16:38:06.343021+00:00 +/// ``` +#[cfg(not(any(feature = "pg11", feature = "pg12")))] +#[pg_extern(name = "to_iso_string", immutable, parallel_safe)] +fn to_iso_string_at_timezone( + tsz: TimestampWithTimeZone, + tz: String, +) -> Result { + tsz.to_iso_string_with_timezone(tz) +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_time(v: Time, i: Interval) -> Time { + v - i +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_tz(v: TimeWithTimeZone, i: Interval) -> TimeWithTimeZone { + v - i +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_tstz(v: TimestampWithTimeZone, i: Interval) -> TimestampWithTimeZone { + v - i +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_ts(v: Timestamp, i: Interval) -> Timestamp { + v - i +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_date(v: Date, i: Interval) -> Timestamp { + v - i +} + +#[pg_extern(name = "subtract_interval", immutable, parallel_safe)] +fn subtract_interval_interval(v: Interval, i: Interval) -> Interval { + v - i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_date(v: Date, i: Interval) -> Timestamp { + v + i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_time(v: Time, i: Interval) -> Time { + v + i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_tz(v: TimeWithTimeZone, i: Interval) -> TimeWithTimeZone { + v + i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_tstz(v: TimestampWithTimeZone, i: Interval) -> TimestampWithTimeZone { + v + i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_ts(v: Timestamp, i: Interval) -> Timestamp { + v + i +} + +#[pg_extern(name = "add_interval", immutable, parallel_safe)] +fn add_interval_interval(v: Interval, i: Interval) -> Interval { + v + i +} + +#[pg_extern(immutable, parallel_safe)] +fn mul_interval(i: Interval, v: f64) -> Interval { + i * v +} + +#[pg_extern(immutable, parallel_safe)] +fn div_interval(i: Interval, v: f64) -> Interval { + i / v +} + +#[pg_extern(parallel_safe)] +fn random_time() -> Result { + Time::new( + rand::thread_rng().gen_range(0..23), + rand::thread_rng().gen_range(0..59), + rand::thread_rng().gen_range(0..59) as f64, + ) +} + +#[pg_extern(parallel_safe)] +fn random_date() -> Date { + let year = rand::thread_rng().gen_range(1978..2023); + let month = rand::thread_rng().gen_range(1..12); + + loop { + let day = rand::thread_rng().gen_range(0..31); + match Date::new(year, month, day) { + // everything is good + Ok(date) => return date, + + // the random day could be greater than the random month has, so we'd get a FieldOverflow error + Err(DateTimeConversionError::FieldOverflow) => continue, + + // Something really unexpected happened + Err(e) => panic!("Error creating random date: {}", e), + } + } +} + +/// ```sql +/// datetime=# select compose_timestamp(random_date(), random_time()) from generate_series(1, 10); +/// compose_timestamp +/// --------------------- +/// 2001-06-21 19:27:04 +/// 1994-11-03 06:11:01 +/// 2012-08-08 14:39:04 +/// 2020-02-18 08:29:03 +/// 2012-04-06 06:41:02 +/// 2022-02-03 22:29:00 +/// 2003-04-08 16:27:03 +/// 1986-01-11 03:25:02 +/// 2003-02-01 03:06:02 +/// 2012-08-09 00:03:03 +/// (10 rows) +/// ``` +#[pg_extern(immutable, parallel_safe)] +fn compose_timestamp(d: Date, t: Time) -> Timestamp { + (d, t).into() +} + +#[pg_extern(immutable, parallel_safe)] +fn set_timezone(ts: Timestamp, timezone: String) -> Result { + let tsz: TimestampWithTimeZone = ts.into(); + tsz.at_timezone(timezone) +} + +/// ```sql +/// datetime=# begin; select pg_sleep(10); select * from all_times(); +/// BEGIN +/// Time: 0.261 ms +/// -[ RECORD 1 ] +/// pg_sleep | +/// +/// Time: 10010.623 ms (00:10.011) +/// -[ RECORD 1 ]---------+------------------------------ +/// now | 2023-05-19 12:45:51.032427-04 +/// transaction_timestamp | 2023-05-19 12:45:51.032427-04 +/// statement_timestamp | 2023-05-19 12:46:01.047298-04 +/// clock_timestamp | 2023-05-19 12:46:01.047415-04 +/// ``` +#[pg_extern] +fn all_times() -> TableIterator< + 'static, + ( + name!(now, TimestampWithTimeZone), + name!(transaction_timestamp, TimestampWithTimeZone), + name!(statement_timestamp, TimestampWithTimeZone), + name!(clock_timestamp, TimestampWithTimeZone), + ), +> { + TableIterator::new(std::iter::once(( + now(), + transaction_timestamp(), + statement_timestamp(), + clock_timestamp(), + ))) +} diff --git a/pgrx-pg-sys/include/pg11.h b/pgrx-pg-sys/include/pg11.h index 5e1421da7..f5d071a2e 100644 --- a/pgrx-pg-sys/include/pg11.h +++ b/pgrx-pg-sys/include/pg11.h @@ -90,6 +90,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "parser/parse_coerce.h" #include "parser/parser.h" #include "parser/parsetree.h" +#include "parser/scansup.h" #include "plpgsql.h" #include "postmaster/bgworker.h" #include "replication/logical.h" @@ -121,6 +122,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "utils/fmgrprotos.h" #include "utils/guc.h" #include "utils/json.h" +#include "utils/jsonapi.h" #include "utils/jsonb.h" #include "utils/lsyscache.h" #include "utils/memutils.h" diff --git a/pgrx-pg-sys/include/pg12.h b/pgrx-pg-sys/include/pg12.h index 2454da531..756c66bef 100644 --- a/pgrx-pg-sys/include/pg12.h +++ b/pgrx-pg-sys/include/pg12.h @@ -90,6 +90,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "parser/parse_coerce.h" #include "parser/parser.h" #include "parser/parsetree.h" +#include "parser/scansup.h" #include "plpgsql.h" #include "postmaster/bgworker.h" #include "replication/logical.h" @@ -120,6 +121,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "utils/geo_decls.h" #include "utils/guc.h" #include "utils/json.h" +#include "utils/jsonapi.h" #include "utils/jsonb.h" #include "utils/lsyscache.h" #include "utils/memutils.h" diff --git a/pgrx-pg-sys/include/pg13.h b/pgrx-pg-sys/include/pg13.h index 75d068c04..a3393e78b 100644 --- a/pgrx-pg-sys/include/pg13.h +++ b/pgrx-pg-sys/include/pg13.h @@ -90,6 +90,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "parser/parse_coerce.h" #include "parser/parser.h" #include "parser/parsetree.h" +#include "parser/scansup.h" #include "plpgsql.h" #include "postmaster/bgworker.h" #include "replication/logical.h" diff --git a/pgrx-pg-sys/include/pg14.h b/pgrx-pg-sys/include/pg14.h index 75d068c04..a3393e78b 100644 --- a/pgrx-pg-sys/include/pg14.h +++ b/pgrx-pg-sys/include/pg14.h @@ -90,6 +90,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "parser/parse_coerce.h" #include "parser/parser.h" #include "parser/parsetree.h" +#include "parser/scansup.h" #include "plpgsql.h" #include "postmaster/bgworker.h" #include "replication/logical.h" diff --git a/pgrx-pg-sys/include/pg15.h b/pgrx-pg-sys/include/pg15.h index 75d068c04..a3393e78b 100644 --- a/pgrx-pg-sys/include/pg15.h +++ b/pgrx-pg-sys/include/pg15.h @@ -90,6 +90,7 @@ Use of this source code is governed by the MIT license that can be found in the #include "parser/parse_coerce.h" #include "parser/parser.h" #include "parser/parsetree.h" +#include "parser/scansup.h" #include "plpgsql.h" #include "postmaster/bgworker.h" #include "replication/logical.h" diff --git a/pgrx-tests/Cargo.toml b/pgrx-tests/Cargo.toml index 4651b481c..e5d582b17 100644 --- a/pgrx-tests/Cargo.toml +++ b/pgrx-tests/Cargo.toml @@ -44,7 +44,6 @@ regex = "1.8.1" serde = "1.0.160" serde_json = "1.0.96" sysinfo = "0.28.4" -time = "0.3.20" eyre = "0.6.8" thiserror = "1.0" @@ -54,5 +53,4 @@ eyre = "0.6.8" # testing functions that return `eyre::Result` [dependencies.pgrx] path = "../pgrx" default-features = false -features = [ "time-crate" ] # testing purposes version = "=0.8.3" diff --git a/pgrx-tests/src/tests/datetime_tests.rs b/pgrx-tests/src/tests/datetime_tests.rs index 48480e5d5..13c3f2e92 100644 --- a/pgrx-tests/src/tests/datetime_tests.rs +++ b/pgrx-tests/src/tests/datetime_tests.rs @@ -8,8 +8,6 @@ Use of this source code is governed by the MIT license that can be found in the */ use pgrx::prelude::*; -use std::convert::TryFrom; -use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset}; #[pg_extern] fn accept_date(d: Date) -> Date { @@ -18,10 +16,7 @@ fn accept_date(d: Date) -> Date { #[pg_extern] fn accept_date_round_trip(d: Date) -> Date { - match TryInto::::try_into(d) { - Ok(date) => date.into(), - Err(pg_epoch_days) => Date::from_pg_epoch_days(pg_epoch_days.as_i32()), - } + d } #[pg_extern] @@ -52,40 +47,20 @@ fn accept_timestamp_with_time_zone(t: TimestampWithTimeZone) -> TimestampWithTim #[pg_extern] fn accept_timestamp_with_time_zone_offset_round_trip( t: TimestampWithTimeZone, -) -> Option { - match TryInto::::try_into(t) { - Ok(offset) => Some(offset.try_into().unwrap()), - Err(_) => None, - } +) -> TimestampWithTimeZone { + t } #[pg_extern] fn accept_timestamp_with_time_zone_datetime_round_trip( t: TimestampWithTimeZone, -) -> Option { - match TryInto::::try_into(t) { - Ok(datetime) => Some(datetime.try_into().unwrap()), - Err(_) => None, - } +) -> TimestampWithTimeZone { + t } #[pg_extern] fn return_3pm_mountain_time() -> TimestampWithTimeZone { - let datetime = PrimitiveDateTime::new( - time::Date::from_calendar_date(2020, time::Month::try_from(2).unwrap(), 19).unwrap(), - time::Time::from_hms(15, 0, 0).unwrap(), - ) - .assume_offset(UtcOffset::from_hms(-7, 0, 0).unwrap()); - - let three_pm: TimestampWithTimeZone = datetime.try_into().unwrap(); - - // this conversion will revert to UTC - let offset: time::OffsetDateTime = three_pm.try_into().unwrap(); - - // 3PM mountain time is 10PM UTC - assert_eq!(22, offset.hour()); - - datetime.try_into().unwrap() + TimestampWithTimeZone::with_timezone(2020, 2, 19, 15, 0, 0.0, "MST").unwrap() } #[pg_extern(sql = r#" @@ -107,8 +82,7 @@ fn accept_interval(interval: Interval) -> Interval { #[pg_extern] fn accept_interval_round_trip(interval: Interval) -> Interval { - let duration: time::Duration = interval.into(); - duration.try_into().expect("Error converting Duration to PgInterval") + interval } #[cfg(any(test, feature = "pg_test"))] @@ -117,33 +91,31 @@ mod tests { #[allow(unused_imports)] use crate as pgrx_tests; + use pgrx::datum::datetime_support::IntervalConversionError; use pgrx::prelude::*; + use pgrx::{get_timezone_offset, DateTimeConversionError}; use serde_json::*; use std::result::Result; + use std::str::FromStr; use std::time::Duration; - use time; - use time::PrimitiveDateTime; #[pg_test] fn test_to_pg_epoch_days() { - let d = time::Date::from_calendar_date(2000, time::Month::January, 2).unwrap(); - let date: Date = d.into(); + let date = Date::new(2000, 1, 2).unwrap(); assert_eq!(date.to_pg_epoch_days(), 1); } #[pg_test] fn test_to_posix_time() { - let d = time::Date::from_calendar_date(1970, time::Month::January, 2).unwrap(); - let date: Date = d.into(); + let date = Date::new(1970, 1, 2).unwrap(); assert_eq!(date.to_posix_time(), 86400); } #[pg_test] fn test_to_julian_days() { - let d = time::Date::from_calendar_date(2000, time::Month::January, 1).unwrap(); - let date: Date = d.into(); + let date = Date::new(2000, 1, 1).unwrap(); assert_eq!(date.to_julian_days(), pg_sys::POSTGRES_EPOCH_JDATE as i32); } @@ -151,26 +123,19 @@ mod tests { #[pg_test] #[allow(deprecated)] fn test_time_with_timezone_serialization() { - let time_with_timezone = TimeWithTimeZone::new( - time::Time::from_hms(12, 23, 34).unwrap(), - time::UtcOffset::from_hms(2, 0, 0).unwrap(), - ); + let time_with_timezone = TimeWithTimeZone::with_timezone(12, 23, 34.0, "CEST").unwrap(); let json = json!({ "time W/ Zone test": time_with_timezone }); - let (h, ..) = time_with_timezone.to_utc().to_hms_micro(); + let (h, ..) = time_with_timezone.at_timezone("UTC").unwrap().to_hms_micro(); assert_eq!(10, h); // however Postgres wants to format it is fine by us - assert_eq!(json!({"time W/ Zone test":"12:23:34+02"}), json); + assert_eq!(json!({"time W/ Zone test":"12:23:34+02:00"}), json); } #[pg_test] fn test_date_serialization() { - let date: Date = - time::Date::from_calendar_date(2020, time::Month::try_from(4).unwrap(), 07) - .unwrap() - .into(); - + let date: Date = Date::new(2020, 4, 7).unwrap(); let json = json!({ "date test": date }); assert_eq!(json!({"date test":"2020-04-07"}), json); @@ -316,12 +281,12 @@ mod tests { #[pg_test] fn test_return_3pm_mountain_time() -> Result<(), pgrx::spi::Error> { - let result = Spi::get_one::("SELECT return_3pm_mountain_time();")? - .expect("datum was null"); - - let offset: time::OffsetDateTime = result.try_into().unwrap(); + let result = Spi::get_one::( + "SET timezone TO 'UTC'; SELECT return_3pm_mountain_time();", + )? + .expect("datum was null"); - assert_eq!(22, offset.hour()); + assert_eq!(22, result.hour()); Ok(()) } @@ -332,9 +297,7 @@ mod tests { )? .expect("datum was null"); - let datetime: time::PrimitiveDateTime = ts.try_into().unwrap(); - - assert_eq!(datetime.hour(), 21); + assert_eq!(ts.to_utc().hour(), 21); Ok(()) } @@ -342,8 +305,7 @@ mod tests { fn test_is_timestamp_utc() -> Result<(), pgrx::spi::Error> { let ts = Spi::get_one::("SELECT '2020-02-18 14:08'::timestamp")? .expect("datum was null"); - let datetime: time::PrimitiveDateTime = ts.try_into().unwrap(); - assert_eq!(datetime.hour(), 14); + assert_eq!(ts.hour(), 14); Ok(()) } @@ -373,19 +335,8 @@ mod tests { #[pg_test] fn test_timestamp_with_timezone_serialization() { - let time_stamp_with_timezone: TimestampWithTimeZone = PrimitiveDateTime::new( - time::Date::from_calendar_date(2022, time::Month::try_from(2).unwrap(), 2).unwrap(), - time::Time::from_hms(16, 57, 11).unwrap(), - ) - .assume_offset( - time::UtcOffset::parse( - "+0200", - &time::format_description::parse("[offset_hour][offset_minute]").unwrap(), - ) - .unwrap(), - ) - .try_into() - .unwrap(); + let time_stamp_with_timezone = + TimestampWithTimeZone::with_timezone(2022, 2, 2, 16, 57, 11.0, "CEST").unwrap(); // prevents PG's timestamp serialization from imposing the local servers time zone Spi::run("SET TIME ZONE 'UTC'").expect("SPI failed"); @@ -400,11 +351,7 @@ mod tests { // prevents PG's timestamp serialization from imposing the local servers time zone Spi::run("SET TIME ZONE 'UTC'").expect("SPI failed"); - let datetime = PrimitiveDateTime::new( - time::Date::from_calendar_date(2020, time::Month::try_from(1).unwrap(), 1).unwrap(), - time::Time::from_hms(12, 34, 54).unwrap(), - ); - let ts: Timestamp = datetime.try_into().unwrap(); + let ts = Timestamp::new(2020, 1, 1, 12, 34, 54.0).unwrap(); let json = json!({ "time stamp test": ts }); assert_eq!(json!({"time stamp test":"2020-01-01T12:34:54"}), json); @@ -454,6 +401,18 @@ mod tests { Ok(()) } + #[rustfmt::skip] + #[pg_test] + fn test_from_str() -> Result<(), Box> { + assert_eq!(Time::new(12, 0, 0.0)?, Time::from_str("12:00:00")?); + assert_eq!(TimeWithTimeZone::with_timezone(12, 0, 0.0, "UTC")?, TimeWithTimeZone::from_str("12:00:00 UTC")?); + assert_eq!(Date::new(2023, 5, 13)?, Date::from_str("2023-5-13")?); + assert_eq!(Timestamp::new(2023, 5, 13, 4, 56, 42.0)?, Timestamp::from_str("2023-5-13 04:56:42")?); + assert_eq!(TimestampWithTimeZone::new(2023, 5, 13, 4, 56, 42.0)?, TimestampWithTimeZone::from_str("2023-5-13 04:56:42")?); + assert_eq!(Interval::from_months(1), Interval::from_str("1 month")?); + Ok(()) + } + #[pg_test] fn test_accept_interval_random() { let result = Spi::get_one::("SELECT accept_interval(interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds') = interval'1 year 2 months 3 days 4 hours 5 minutes 6 seconds';") @@ -484,7 +443,7 @@ mod tests { #[pg_test] fn test_interval_serialization() { - let interval = Interval::try_from_months_days_micros(3, 4, 5_000_000).unwrap(); + let interval = Interval::new(3, 4, 5_000_000).unwrap(); let json = json!({ "interval test": interval }); assert_eq!(json!({"interval test":"3 mons 4 days 00:00:05"}), json); @@ -492,9 +451,11 @@ mod tests { #[pg_test] fn test_duration_to_interval_err() { - use pgrx::IntervalConversionError; + use pgrx::datum::datetime_support::IntervalConversionError; // normal limit of i32::MAX months - let duration = time::Duration::days(pg_sys::DAYS_PER_MONTH as i64 * i32::MAX as i64); + let duration = Duration::from_secs( + pg_sys::DAYS_PER_MONTH as u64 * i32::MAX as u64 * pg_sys::SECS_PER_DAY as u64, + ); let result = TryInto::::try_into(duration); match result { @@ -503,8 +464,9 @@ mod tests { }; // one month too many, expect error - let duration = - time::Duration::days(pg_sys::DAYS_PER_MONTH as i64 * (i32::MAX as i64 + 1i64)); + let duration = Duration::from_secs( + pg_sys::DAYS_PER_MONTH as u64 * (i32::MAX as u64 + 1u64) * pg_sys::SECS_PER_DAY as u64, + ); let result = TryInto::::try_into(duration); match result { @@ -512,4 +474,103 @@ mod tests { _ => panic!("invalid duration -> interval conversion succeeded"), }; } + + #[pg_test] + fn test_timezone_offset_cest() { + assert_eq!(Ok(7200), get_timezone_offset("CEST")) + } + + #[pg_test] + fn test_timezone_offset_edt() { + assert_eq!(Ok(-14400), get_timezone_offset("US/Eastern")) + } + + #[pg_test] + fn test_timezone_offset_unknown() { + assert_eq!( + Err(DateTimeConversionError::UnknownTimezone(String::from("UNKNOWN TIMEZONE"))), + get_timezone_offset("UNKNOWN TIMEZONE") + ) + } + + #[pg_test] + fn test_interval_to_duration_conversion() { + let i = Interval::new(42, 6, 3).unwrap(); + let i_micros = i.as_micros(); + let d: Duration = i.try_into().unwrap(); + + assert_eq!(i_micros as u128, d.as_micros()) + } + + #[pg_test] + fn test_negative_interval_to_duration_conversion() { + let i = Interval::new(-42, -6, -3).unwrap(); + let d: Result = i.try_into(); + + assert_eq!(d, Err(IntervalConversionError::NegativeInterval)) + } + + #[pg_test] + fn test_duration_to_interval_conversion() { + let i: Interval = + Spi::get_one("select '3 months 5 days 22 seconds'::interval").unwrap().unwrap(); + + assert_eq!(i.months(), 3); + assert_eq!(i.days(), 5); + assert_eq!(i.micros(), 22_000_000); // 22 seconds + + let d = Duration::from_secs( + pg_sys::DAYS_PER_MONTH as u64 * 3u64 * pg_sys::SECS_PER_DAY as u64 // 3 months + + 5u64 * pg_sys::SECS_PER_DAY as u64 // 5 days + + 22u64, // 22 seconds more + ); + let i: Interval = d.try_into().unwrap(); + assert_eq!(i.months(), 3); + assert_eq!(i.days(), 5); + assert_eq!(i.micros(), 22_000_000); // 22 seconds + } + + #[pg_test] + fn test_interval_from_seconds() { + let i = Interval::from_seconds(32768.0); + assert_eq!("09:06:08", &i.to_string()); + + let i = Interval::from_str("32768 seconds"); + assert_eq!(i, Ok(Interval::from_seconds(32768.0))) + } + + #[pg_test] + fn test_interval_from_mismatched_signs() { + let i = Interval::from(Some(1), Some(-2), None, None, None, None, None); + assert_eq!(i, Err(IntervalConversionError::MismatchedSigns)) + } + + #[pg_test] + fn test_add_date_time() -> Result<(), Box> { + let date = Date::new(1978, 5, 13)?; + let time = Time::new(13, 33, 42.0)?; + let ts = date + time; + assert_eq!(&ts.to_string(), "1978-05-13 13:33:42"); + assert_eq!(ts, Timestamp::new(1978, 5, 13, 13, 33, 42.0)?); + Ok(()) + } + + #[pg_test] + fn test_add_time_interval() -> Result<(), Box> { + let time = Time::new(13, 33, 42.0)?; + let i = Interval::from_seconds(27.0); + let time = time + i; + assert_eq!(time, Time::new(13, 34, 9.0)?); + Ok(()) + } + + #[pg_test] + fn test_add_intervals() -> Result<(), Box> { + let b = Interval::from_months(6); + let c = Interval::from_days(15); + let a = Interval::from_micros(42); + let result = a + b + c; + assert_eq!(result, Interval::new(6, 15, 42)?); + Ok(()) + } } diff --git a/pgrx/Cargo.toml b/pgrx/Cargo.toml index d28700fd0..9ccdc58b7 100644 --- a/pgrx/Cargo.toml +++ b/pgrx/Cargo.toml @@ -23,7 +23,6 @@ pg12 = [ "pgrx-pg-sys/pg12" ] pg13 = [ "pgrx-pg-sys/pg13" ] pg14 = [ "pgrx-pg-sys/pg14" ] pg15 = [ "pgrx-pg-sys/pg15" ] -time-crate = ["dep:time"] 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 diff --git a/pgrx/src/datum/date.rs b/pgrx/src/datum/date.rs index c8532e15d..38c69a945 100644 --- a/pgrx/src/datum/date.rs +++ b/pgrx/src/datum/date.rs @@ -7,37 +7,68 @@ All rights reserved. Use of this source code is governed by the MIT license that can be found in the LICENSE file. */ -use crate::{pg_sys, FromDatum, IntoDatum}; -use core::ffi::CStr; use core::num::TryFromIntError; +use pgrx_pg_sys::errcodes::PgSqlErrorCode; +use pgrx_pg_sys::PgTryBuilder; use pgrx_sql_entity_graph::metadata::{ ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable, }; +use crate::datetime_support::{DateTimeParts, HasExtractableParts}; +use crate::{ + direct_function_call, pg_sys, DateTimeConversionError, FromDatum, IntoDatum, Timestamp, + TimestampWithTimeZone, ToIsoString, +}; + pub const POSTGRES_EPOCH_JDATE: i32 = pg_sys::POSTGRES_EPOCH_JDATE as i32; pub const UNIX_EPOCH_JDATE: i32 = pg_sys::UNIX_EPOCH_JDATE as i32; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +/// A safe wrapper around Postgres `DATE` type, backed by a [`pg_sys::DateADT`] integer value. +#[derive(Debug, Copy, Clone)] #[repr(transparent)] -pub struct Date(i32); +pub struct Date(pg_sys::DateADT); -impl TryFrom for Date { - type Error = TryFromIntError; - fn try_from(d: pg_sys::Datum) -> Result { - i32::try_from(d.value() as isize).map(|d| Date(d)) +/// Blindly create a [`Date]` from a Postgres [`pg_sys::DateADT`] value. +/// +/// Note that [`pg_sys::DateADT`] is just an `i32`, so using a random i32 could construct a date value +/// that ultimately Postgres doesn't understand +impl From for Date { + #[inline] + fn from(value: pg_sys::DateADT) -> Self { + Date(value) } } -impl IntoDatum for Date { - fn into_datum(self) -> Option { - Some(pg_sys::Datum::from(self.0)) +impl From for pg_sys::DateADT { + #[inline] + fn from(value: Date) -> Self { + value.0 } - fn type_oid() -> pg_sys::Oid { - pg_sys::DATEOID +} + +impl From for Date { + fn from(value: Timestamp) -> Self { + unsafe { direct_function_call(pg_sys::timestamp_date, &[value.into_datum()]).unwrap() } + } +} + +impl From for Date { + fn from(value: TimestampWithTimeZone) -> Self { + unsafe { direct_function_call(pg_sys::timestamptz_date, &[value.into_datum()]).unwrap() } + } +} + +impl TryFrom for Date { + type Error = TryFromIntError; + + #[inline] + fn try_from(datum: pg_sys::Datum) -> Result { + pg_sys::DateADT::try_from(datum.value() as isize).map(|d| Date(d)) } } impl FromDatum for Date { + #[inline] unsafe fn from_polymorphic_datum( datum: pg_sys::Datum, is_null: bool, @@ -54,30 +85,122 @@ impl FromDatum for Date { } } +impl IntoDatum for Date { + fn into_datum(self) -> Option { + Some(pg_sys::Datum::from(self.0)) + } + fn type_oid() -> pg_sys::Oid { + pg_sys::DATEOID + } +} + impl Date { - pub const NEG_INFINITY: Self = Date(i32::MIN); - pub const INFINITY: Self = Date(i32::MAX); + const NEG_INFINITY: pg_sys::DateADT = pg_sys::DateADT::MIN; + const INFINITY: pg_sys::DateADT = pg_sys::DateADT::MAX; + + /// Construct a new [`Date`] from its constituent parts. + /// + /// # Errors + /// + /// Returns a [`DateTimeConversionError`] if any of the specified parts don't fit within + /// the bounds of a standard date. + pub fn new(year: i32, month: u8, day: u8) -> Result { + let month: i32 = month as _; + let day: i32 = day as _; + + PgTryBuilder::new(|| unsafe { + Ok(direct_function_call( + pg_sys::make_date, + &[year.into_datum(), month.into_datum(), day.into_datum()], + ) + .unwrap()) + }) + .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| { + Err(DateTimeConversionError::FieldOverflow) + }) + .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| { + Err(DateTimeConversionError::InvalidFormat) + }) + .execute() + } + + /// Construct a new [`Date`] from its constituent parts. + /// + /// This function elides the error trapping overhead in the event of out-of-bounds parts. + /// + /// # Panics + /// + /// This function will panic, aborting the current transaction, if any part is out-of-bounds. + pub fn new_unchecked(year: isize, month: u8, day: u8) -> Self { + let year: i32 = year.try_into().expect("invalid year"); + let month: i32 = month.try_into().expect("invalid month"); + let day: i32 = day.try_into().expect("invalid day"); + + unsafe { + direct_function_call( + pg_sys::make_date, + &[year.into_datum(), month.into_datum(), day.into_datum()], + ) + .unwrap() + } + } + + /// Construct a new [`Date`] representing positive infinity + pub fn positive_infinity() -> Self { + Self(Self::INFINITY) + } + /// Construct a new [`Date`] representing negative infinity + pub fn negative_infinity() -> Self { + Self(Self::NEG_INFINITY) + } + + /// Create a new [`Date`] from an integer value from Postgres' epoch, in days. + /// + /// # Safety + /// + /// This function is unsafe as you must guarantee `pg_epoch_days` is valid. You'll always + /// get a fully constructed [`Date`] in return, but it may not be something Postgres actually + /// understands. #[inline] - pub fn from_pg_epoch_days(pg_epoch_days: i32) -> Date { + pub unsafe fn from_pg_epoch_days(pg_epoch_days: i32) -> Date { Date(pg_epoch_days) } + /// Extract the `month` + pub fn month(&self) -> u8 { + self.extract_part(DateTimeParts::Month).unwrap().try_into().unwrap() + } + + /// Extract the `day` + pub fn day(&self) -> u8 { + self.extract_part(DateTimeParts::Day).unwrap().try_into().unwrap() + } + + /// Extract the `year` + pub fn year(&self) -> i32 { + self.extract_part(DateTimeParts::Year).unwrap().try_into().unwrap() + } + + /// Does this [`Date`] represent positive infinity? #[inline] pub fn is_infinity(&self) -> bool { - self == &Self::INFINITY + self.0 == Self::INFINITY } + /// Does this [`Date`] represent negative infinity? #[inline] pub fn is_neg_infinity(&self) -> bool { - self == &Self::NEG_INFINITY + self.0 == Self::NEG_INFINITY } + /// Return the Julian days value of this [`Date`] #[inline] pub fn to_julian_days(&self) -> i32 { self.0 + POSTGRES_EPOCH_JDATE } + /// Return the Postgres epoch days value of this [`Date`] #[inline] pub fn to_pg_epoch_days(&self) -> i32 { self.0 @@ -89,71 +212,27 @@ impl Date { self.0 + POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE } + /// Return the date as a stack-allocated [`libc::time_t`] instance #[inline] pub fn to_posix_time(&self) -> libc::time_t { let secs_per_day: libc::time_t = pg_sys::SECS_PER_DAY.try_into().expect("couldn't fit time into time_t"); libc::time_t::from(self.to_unix_epoch_days()) * secs_per_day } -} - -#[cfg(feature = "time-crate")] -pub use with_time_crate::TryFromDateError; - -#[cfg(feature = "time-crate")] -mod with_time_crate { - use crate::{Date, POSTGRES_EPOCH_JDATE}; - use core::fmt::{Display, Formatter}; - use std::error::Error; - - #[derive(Debug, PartialEq, Clone)] - #[non_exhaustive] - pub struct TryFromDateError(pub Date); - - impl TryFromDateError { - #[inline] - pub fn into_inner(self) -> Date { - self.0 - } - - #[inline] - pub fn as_i32(&self) -> i32 { - self.0 .0 - } - } - - impl Display for TryFromDateError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "`{}` is not compatible with `time::Date`", self.0 .0) - } - } - - impl Error for TryFromDateError {} - impl From for Date { - #[inline] - fn from(date: time::Date) -> Self { - Date::from_pg_epoch_days(date.to_julian_day() - POSTGRES_EPOCH_JDATE) - } + pub fn is_finite(&self) -> bool { + unsafe { direct_function_call(pg_sys::date_finite, &[self.into_datum()]).unwrap() } } - impl TryFrom for time::Date { - type Error = TryFromDateError; - fn try_from(date: Date) -> Result { - const INNER_RANGE_BEGIN: i32 = time::Date::MIN.to_julian_day(); - const INNER_RANGE_END: i32 = time::Date::MAX.to_julian_day(); - match date.0 { - INNER_RANGE_BEGIN..=INNER_RANGE_END => { - time::Date::from_julian_day(date.0 + POSTGRES_EPOCH_JDATE) - .or_else(|_e| Err(TryFromDateError(date))) - } - _ => Err(TryFromDateError(date)), - } - } + /// Return the backing [`pg_sy::DateADT`] value. + #[inline] + pub fn into_inner(self) -> pg_sys::DateADT { + self.0 } } impl serde::Serialize for Date { + /// Serialize this [`Date`] in ISO form, compatible with most JSON parsers fn serialize( &self, serializer: S, @@ -161,39 +240,18 @@ impl serde::Serialize for Date { where S: serde::Serializer, { - let cstr; - assert!(pg_sys::MAXDATELEN > 0); // free at runtime - const BUF_LEN: usize = pg_sys::MAXDATELEN as usize * 2; - let mut buffer = [0u8; BUF_LEN]; - let buf = buffer.as_mut_slice().as_mut_ptr().cast::(); - // SAFETY: This provides a quite-generous writing pad to Postgres - // and Postgres has promised to use far less than this. - unsafe { - match self { - &Self::NEG_INFINITY | &Self::INFINITY => { - pg_sys::EncodeSpecialDate(self.0, buf); - } - _ => { - let mut pg_tm: pg_sys::pg_tm = Default::default(); - pg_sys::j2date( - &self.0 + POSTGRES_EPOCH_JDATE, - &mut pg_tm.tm_year, - &mut pg_tm.tm_mon, - &mut pg_tm.tm_mday, - ); - pg_sys::EncodeDateOnly(&mut pg_tm, pg_sys::USE_XSD_DATES as i32, buf) - } - } - assert!(buffer[BUF_LEN - 1] == 0); - cstr = CStr::from_ptr(buf); - } - - /* This unwrap is fine as Postgres won't ever write invalid UTF-8, - because Postgres only writes ASCII - */ serializer - .serialize_str(cstr.to_str().unwrap()) - .map_err(|e| serde::ser::Error::custom(format!("Date formatting problem: {:?}", e))) + .serialize_str(&self.to_iso_string()) + .map_err(|e| serde::ser::Error::custom(format!("formatting problem: {:?}", e))) + } +} + +impl<'de> serde::Deserialize<'de> for Date { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(crate::DateTimeTypeVisitor::::new()) } } diff --git a/pgrx/src/datum/datetime_support/ctor.rs b/pgrx/src/datum/datetime_support/ctor.rs new file mode 100644 index 000000000..24b595f8d --- /dev/null +++ b/pgrx/src/datum/datetime_support/ctor.rs @@ -0,0 +1,128 @@ +/* +Portions Copyright 2019-2021 ZomboDB, LLC. +Portions Copyright 2021-2022 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the MIT license that can be found in the LICENSE file. +*/ +//! Exposes constructor methods for creating [`TimestampWithTimeZone`]s based on the various +//! ways Postgres likes to interpret the "current time". +use crate::{direct_function_call, pg_sys, Date, IntoDatum, Timestamp, TimestampWithTimeZone}; + +/// Current date and time (start of current transaction) +pub fn now() -> TimestampWithTimeZone { + unsafe { pg_sys::GetCurrentTransactionStartTimestamp().try_into().unwrap() } +} + +/// Current date and time (start of current transaction) +/// +/// This is the same as [`now()`]. +pub fn transaction_timestamp() -> TimestampWithTimeZone { + now() +} + +/// Current date and time (start of current statement) +pub fn statement_timestamp() -> TimestampWithTimeZone { + unsafe { pg_sys::GetCurrentStatementStartTimestamp().try_into().unwrap() } +} + +/// Get the current operating system time (changes during statement execution) +/// +/// Result is in the form of a [`TimestampWithTimeZone`] value, and is expressed to the +/// full precision of the `gettimeofday()` syscall +pub fn clock_timestamp() -> TimestampWithTimeZone { + unsafe { pg_sys::GetCurrentTimestamp().try_into().unwrap() } +} + +pub enum TimestampPrecision { + /// Resulting timestamp is given to the full available precision + Full, + + /// Resulting timestamp to be rounded to that many fractional digits in the seconds field + Rounded(i32), +} + +/// Helper to convert a [`TimestampPrecision`] into a Postgres "typemod" integer +impl From for i32 { + fn from(value: TimestampPrecision) -> Self { + match value { + TimestampPrecision::Full => -1, + TimestampPrecision::Rounded(p) => p, + } + } +} + +/// Current date (changes during statement execution) +pub fn current_date() -> Date { + current_timestamp(TimestampPrecision::Full).into() +} + +/// Current time (changes during statement execution) +pub fn current_time() -> Date { + current_timestamp(TimestampPrecision::Full).into() +} + +/// implements CURRENT_TIMESTAMP, CURRENT_TIMESTAMP(n) (changes during statement execution) +pub fn current_timestamp(precision: TimestampPrecision) -> TimestampWithTimeZone { + unsafe { pg_sys::GetSQLCurrentTimestamp(precision.into()).try_into().unwrap() } +} + +/// implements LOCALTIMESTAMP, LOCALTIMESTAMP(n) +pub fn local_timestamp(precision: TimestampPrecision) -> Timestamp { + unsafe { pg_sys::GetSQLLocalTimestamp(precision.into()).try_into().unwrap() } +} + +/// Returns the current time as String (changes during statement execution) +pub fn time_of_day() -> String { + unsafe { direct_function_call(pg_sys::timeofday, &[]).unwrap() } +} + +/// Convert Unix epoch (seconds since 1970-01-01 00:00:00+00) to [`TimestampWithTimeZone`] +pub fn to_timestamp(epoch_seconds: f64) -> TimestampWithTimeZone { + unsafe { + direct_function_call(pg_sys::float8_timestamptz, &[epoch_seconds.into_datum()]).unwrap() + } +} + +/// “bins” the input timestamp into the specified interval (the stride) aligned with a specified origin. +/// +/// `source` is a value expression of type [`Timestamp`]. +/// `stride` is a value expression of type [`Interval`]. +/// +/// The return value is likewise of type [`Timestamp`], and it marks the beginning of the bin into +/// which the source is placed. +/// +/// # Notes +/// +/// Only available on Postgres v14 and greater. +/// +/// In the case of full units (1 minute, 1 hour, etc.), it gives the same result as the analogous +/// `date_trunc()` function, but the difference is that [`date_bin()`] can truncate to an arbitrary +/// interval. +/// +/// The stride interval must be greater than zero and cannot contain units of month or larger. +/// +/// # Examples +/// +/// ```sql +/// SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01'); +/// Result: 2020-02-11 15:30:00 +/// +/// SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01 00:02:30'); +/// Result: 2020-02-11 15:32:30 +/// ``` +#[cfg(any(features = "pg14", features = "pg15"))] +pub fn date_bin( + stride: crate::datum::interval::Interval, + source: Timestamp, + origin: Timestamp, +) -> Timestamp { + unsafe { + direct_function_call( + pg_sys::date_bin, + &[stride.into_datum(), source.into_datum(), origin.into_datum()], + ) + .unwrap() + } +} diff --git a/pgrx/src/datum/datetime_support/mod.rs b/pgrx/src/datum/datetime_support/mod.rs new file mode 100644 index 000000000..c6ca09393 --- /dev/null +++ b/pgrx/src/datum/datetime_support/mod.rs @@ -0,0 +1,635 @@ +/* +Portions Copyright 2019-2021 ZomboDB, LLC. +Portions Copyright 2021-2022 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the MIT license that can be found in the LICENSE file. +*/ +use crate::{ + direct_function_call, pg_sys, AnyNumeric, Date, Interval, IntoDatum, Time, TimeWithTimeZone, + Timestamp, TimestampWithTimeZone, +}; +use core::fmt::{Display, Formatter}; +use core::str::FromStr; +use pgrx_pg_sys::errcodes::PgSqlErrorCode; +use pgrx_pg_sys::{pg_tz, PgTryBuilder}; +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; + +mod ctor; +mod ops; + +pub use ctor::*; +pub use ops::*; + +/// Tags to identify which "part" of a date or time-type value to extract or truncate to +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum DateTimeParts { + /// The century + /// + /// The first century starts at 0001-01-01 00:00:00 AD, although they did not know it at the time. + /// This definition applies to all Gregorian calendar countries. There is no century number 0, + /// you go from -1 century to 1 century. If you disagree with this, please write your complaint + /// to: Pope, Cathedral Saint-Peter of Roma, Vatican. + Century, + + /// For `timestamp` values, the day (of the month) field (1–31) ; for `interval values`, the + /// number of days + Day, + + /// The year field divided by 10 + Decade, + + /// The day of the week as Sunday (0) to Saturday (6) + DayOfWeek, + + /// The day of the year (1–365/366) + DayOfYear, + + /// For timestamp with time zone values, the number of seconds since 1970-01-01 00:00:00 UTC + /// (negative for timestamps before that); for date and timestamp values, the nominal number of + /// seconds since 1970-01-01 00:00:00, without regard to timezone or daylight-savings rules; for + /// interval values, the total number of seconds in the interval + Epoch, + + /// The hour field (0–23) + Hour, + + /// The day of the week as Monday (1) to Sunday (7) + /// + /// This is identical to dow except for Sunday. This matches the ISO 8601 day of the week numbering. + ISODayOfWeek, + + /// The ISO 8601 week-numbering year that the date falls in (not applicable to intervals) + /// + /// Each ISO 8601 week-numbering year begins with the Monday of the week containing the 4th of + /// January, so in early January or late December the ISO year may be different from the + /// Gregorian year. See the week field for more information. + ISOYear, + + /// The *Julian Date* corresponding to the date or timestamp (not applicable to intervals). + /// Timestamps that are not local midnight result in a fractional value. See [Section B.7] for + /// more information. + /// + /// [Section B.7](https://www.postgresql.org/docs/current/datetime-julian-dates.html) + Julian, + + /// The seconds field, including fractional parts, multiplied by 1 000 000; note that this + /// includes full seconds + Microseconds, + + /// The millennium + Millennium, + + /// The seconds field, including fractional parts, multiplied by 1000. Note that this includes + /// full seconds. + Milliseconds, + + /// The minutes field (0–59) + Minute, + + /// For `timestamp` values, the number of the month within the year (1–12) ; for `interval` values, + /// the number of months, modulo 12 (0–11) + Month, + + /// The quarter of the year (1–4) that the date is in + Quarter, + + /// The seconds field, including any fractional seconds + Second, + + /// The time zone offset from UTC, measured in seconds. Positive values correspond to time zones + /// east of UTC, negative values to zones west of UTC. (Technically, PostgreSQL does not use UTC + /// because leap seconds are not handled.) + Timezone, + + /// The hour component of the time zone offset + TimezoneHour, + + /// The minute component of the time zone offset + TimezoneMinute, + + /// The number of the ISO 8601 week-numbering week of the year. By definition, ISO weeks start on + /// Mondays and the first week of a year contains January 4 of that year. In other words, the + /// first Thursday of a year is in week 1 of that year. + /// + /// In the ISO week-numbering system, it is possible for early-January dates to be part of the + /// 52nd or 53rd week of the previous year, and for late-December dates to be part of the first + /// week of the next year. For example, 2005-01-01 is part of the 53rd week of year 2004, and + /// 2006-01-01 is part of the 52nd week of year 2005, while 2012-12-31 is part of the first week + /// of 2013. It's recommended to use the isoyear field together with week to get consistent results. + Week, + + /// The year field. Keep in mind there is no `0 AD`, so subtracting BC years from AD years should + /// be done with care. + Year, +} + +impl From for &'static str { + /// Convert to Postgres' string representation of a [`DateTimePart`] + #[inline] + fn from(value: DateTimeParts) -> Self { + match value { + DateTimeParts::Century => "century", + DateTimeParts::Day => "day", + DateTimeParts::Decade => "decade", + DateTimeParts::DayOfWeek => "dow", + DateTimeParts::DayOfYear => "doy", + DateTimeParts::Epoch => "epoch", + DateTimeParts::Hour => "hour", + DateTimeParts::ISODayOfWeek => "isodow", + DateTimeParts::ISOYear => "isodoy", + DateTimeParts::Julian => "julian", + DateTimeParts::Microseconds => "microseconds", + DateTimeParts::Millennium => "millennium", + DateTimeParts::Milliseconds => "milliseconds", + DateTimeParts::Minute => "minute", + DateTimeParts::Month => "month", + DateTimeParts::Quarter => "quarter", + DateTimeParts::Second => "second", + DateTimeParts::Timezone => "timezone", + DateTimeParts::TimezoneHour => "timezone_hour", + DateTimeParts::TimezoneMinute => "timezone_minute", + DateTimeParts::Week => "week", + DateTimeParts::Year => "year", + } + } +} + +impl Display for DateTimeParts { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let name: &'static str = (*self).into(); + write!(f, "{}", name) + } +} + +impl IntoDatum for DateTimeParts { + #[inline] + fn into_datum(self) -> Option { + let name: &'static str = self.into(); + name.into_datum() + } + + #[inline] + fn type_oid() -> pg_sys::Oid { + pg_sys::TEXTOID + } +} + +mod seal { + #[doc(hidden)] + pub trait DateTimeType {} +} + +pub trait HasExtractableParts: Clone + IntoDatum + seal::DateTimeType { + const EXTRACT_FUNCTION: unsafe fn(pg_sys::FunctionCallInfo) -> pg_sys::Datum; + + /// Extract a [`DateTimeParts`] part from a date/time-like type + fn extract_part(&self, field: DateTimeParts) -> Option { + unsafe { + let field_datum = field.into_datum(); + #[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] + let field_value: Option = direct_function_call( + Self::EXTRACT_FUNCTION, + &[field_datum, self.clone().into_datum()], + ); + #[cfg(any(feature = "pg14", feature = "pg15"))] + let field_value: Option = direct_function_call( + Self::EXTRACT_FUNCTION, + &[field_datum, self.clone().into_datum()], + ); + // don't leak the TEXT datum we made + pg_sys::pfree(field_datum.unwrap().cast_mut_ptr()); + field_value.map(|v| v.try_into().unwrap()) + } + } +} + +pub trait ToIsoString: IntoDatum + Sized + Display + seal::DateTimeType { + /// Encode of this date/time-like type into JSON string in ISO format using + /// optionally preallocated buffer 'buf'. + /// + /// # Notes + /// + /// Types `with time zone` use the Postgres globally configured time zone in the text representation + fn to_iso_string(self) -> String { + if Self::type_oid() == pg_sys::INTERVALOID { + // `Interval` is just represented in its string form + self.to_string() + } else { + unsafe { + #[cfg(any(feature = "pg11", feature = "pg12"))] + let jsonb = pg_sys::JsonEncodeDateTime( + std::ptr::null_mut(), + self.into_datum().unwrap(), + Self::type_oid(), + ); + #[cfg(any(feature = "pg13", feature = "pg14", feature = "pg15"))] + let jsonb = pg_sys::JsonEncodeDateTime( + std::ptr::null_mut(), + self.into_datum().unwrap(), + Self::type_oid(), + std::ptr::null(), + ); + let cstr = core::ffi::CStr::from_ptr(jsonb); + let as_string = cstr.to_str().unwrap().to_string(); + pg_sys::pfree(jsonb.cast()); + + as_string + } + } + } + + /// Encode of this date/time-like type into JSON string in ISO format using + /// optionally preallocated buffer 'buf'. + /// + /// # Notes + /// + /// This function is only available on Postgres v13 and greater + #[cfg(any(feature = "pg13", feature = "pg14", feature = "pg15"))] + fn to_iso_string_with_timezone>( + self, + timezone: Tz, + ) -> Result { + if Self::type_oid() == pg_sys::INTERVALOID { + // `Interval` is just represented in its string form + Ok(self.to_string()) + } else { + let tzoffset = -get_timezone_offset(&timezone)?; + + unsafe { + let jsonb = pg_sys::JsonEncodeDateTime( + std::ptr::null_mut(), + self.into_datum().unwrap(), + Self::type_oid(), + &tzoffset, + ); + let cstr = core::ffi::CStr::from_ptr(jsonb); + let as_string = cstr.to_str().unwrap().to_string(); + pg_sys::pfree(jsonb.cast()); + + Ok(as_string) + } + } + } +} + +macro_rules! impl_wrappers { + ($ty:ty, $eq_fn:path, $cmp_fn:path, $hash_fn:path, $extract_fn:path, $input_fn:path, $output_fn:path) => { + impl seal::DateTimeType for $ty {} + + impl Eq for $ty {} + + impl PartialEq for $ty { + /// Uses the underlying Postgres "_eq()" function for this type + fn eq(&self, other: &Self) -> bool { + unsafe { + direct_function_call($eq_fn, &[self.into_datum(), other.into_datum()]).unwrap() + } + } + } + + impl Ord for $ty { + /// Uses the underlying Postgres "_cmp()" function for this type + fn cmp(&self, other: &Self) -> Ordering { + unsafe { + match direct_function_call::( + $cmp_fn, + &[self.into_datum(), other.into_datum()], + ) { + Some(-1) => Ordering::Less, + Some(0) => Ordering::Equal, + Some(1) => Ordering::Greater, + _ => panic!("unexpected response from {}", stringify!($cmp_fn)), + } + } + } + } + + impl PartialOrd for $ty { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Hash for $ty { + /// Uses the underlying Postgres "hash" function for this type + fn hash(&self, state: &mut H) { + let hash: i32 = unsafe { + direct_function_call($hash_fn, &[self.clone().into_datum()]).unwrap() + }; + state.write_i32(hash); + } + } + + impl HasExtractableParts for $ty { + const EXTRACT_FUNCTION: unsafe fn(pg_sys::FunctionCallInfo) -> pg_sys::Datum = + $extract_fn; + } + + impl ToIsoString for $ty {} + + impl FromStr for $ty { + type Err = DateTimeConversionError; + + /// Create this type from a string. + fn from_str(s: &str) -> Result { + use pgrx_pg_sys::AsPgCStr; + let cstr = s.as_pg_cstr(); + let cstr_datum = pg_sys::Datum::from(cstr); + unsafe { + let result = PgTryBuilder::new(|| { + let result = direct_function_call::<$ty>( + $input_fn, + &[ + Some(cstr_datum), + pgrx_pg_sys::InvalidOid.into_datum(), + (-1i32).into_datum(), + ], + ) + .unwrap(); + Ok(result) + }) + .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| { + Err(DateTimeConversionError::FieldOverflow) + }) + .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| { + Err(DateTimeConversionError::InvalidFormat) + }) + .catch_when(PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, |_| { + Err(DateTimeConversionError::CannotParseTimezone) + }) + .execute(); + pg_sys::pfree(cstr.cast()); + result + } + } + } + + impl Display for $ty { + /// Uses the underlying "output" function to convert this type to a String + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let text: &core::ffi::CStr = unsafe { + direct_function_call($output_fn, &[self.clone().into_datum()]).unwrap() + }; + write!(f, "{}", text.to_str().unwrap()) + } + } + }; +} + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const DATE_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg11_13::date_part; + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +mod pg11_13 { + use crate as pgrx; // for [pg_guard] + use crate::prelude::*; + + #[pg_guard] + pub(super) unsafe fn date_part(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum { + // we need to first convert the `date` value into a `timestamp` value + // then call the `timestamp_part` function. + // + // this is essentially how the `date_part()` function is declared in the system catalogs + // for pg11-13: + /** + \sf date_part(text, date) + CREATE OR REPLACE FUNCTION pg_catalog.date_part(text, date) + RETURNS double precision + LANGUAGE sql + IMMUTABLE PARALLEL SAFE STRICT COST 1 + AS $function$select pg_catalog.date_part($1, cast($2 as timestamp without time zone))$function$ + */ + use crate::fcinfo::*; + let timezone = pg_getarg_datum(fcinfo, 0); + let date = pg_getarg_datum(fcinfo, 1); + let timestamp = direct_function_call_as_datum(pg_sys::date_timestamp, &[date]); + direct_function_call_as_datum(pg_sys::timestamp_part, &[timezone, timestamp]) + .unwrap_or_else(|| pg_sys::Datum::from(0)) + } +} + +#[cfg(any(feature = "pg14", feature = "pg15"))] +const DATE_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_date; +impl_wrappers!( + Date, + pg_sys::date_eq, + pg_sys::date_cmp, + pg_sys::hashint8, + DATE_EXTRACT, + pg_sys::date_in, + pg_sys::date_out +); + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const TIME_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::time_part; +#[cfg(any(feature = "pg14", feature = "pg15"))] +const TIME_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_time; + +impl_wrappers!( + Time, + pg_sys::time_eq, + pg_sys::time_cmp, + pg_sys::time_hash, + TIME_EXTRACT, + pg_sys::time_in, + pg_sys::time_out +); + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const TIMETZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::timetz_part; +#[cfg(any(feature = "pg14", feature = "pg15"))] +const TIMETZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_timetz; + +impl_wrappers!( + TimeWithTimeZone, + pg_sys::timetz_eq, + pg_sys::timetz_cmp, + pg_sys::timetz_hash, + TIMETZ_EXTRACT, + pg_sys::timetz_in, + pg_sys::timetz_out +); + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const TIMESTAMP_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::timestamp_part; +#[cfg(any(feature = "pg14", feature = "pg15"))] +const TIMESTAMP_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_timestamp; + +impl_wrappers!( + Timestamp, + pg_sys::timestamp_eq, + pg_sys::timestamp_cmp, + pg_sys::timestamp_hash, + TIMESTAMP_EXTRACT, + pg_sys::timestamp_in, + pg_sys::timestamp_out +); + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const TIMESTAMPTZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::timestamptz_part; +#[cfg(any(feature = "pg14", feature = "pg15"))] +const TIMESTAMPTZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_timestamptz; + +impl_wrappers!( + TimestampWithTimeZone, + pg_sys::timestamp_eq, // yes, this is correct + pg_sys::timestamp_cmp, // yes, this is correct + pg_sys::timestamp_hash, // yes, this is correct + TIMESTAMPTZ_EXTRACT, + pg_sys::timestamptz_in, + pg_sys::timestamptz_out +); + +#[cfg(any(feature = "pg11", feature = "pg12", feature = "pg13"))] +const INTERVAL_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::interval_part; +#[cfg(any(feature = "pg14", feature = "pg15"))] +const INTERVAL_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = + pg_sys::extract_interval; + +impl_wrappers!( + Interval, + pg_sys::interval_eq, + pg_sys::interval_cmp, + pg_sys::interval_hash, + INTERVAL_EXTRACT, + pg_sys::interval_in, + pg_sys::interval_out +); + +// ported from `v5.2/src/backend/utils/adt/date.c#3034` +/// Calculate the timezone offset in seconds, from GMT, for the specified named time`zone`. +/// +/// If for example, the `zone` is "EDT", which is GMT-4, then the result is `-14400`. Similarly, +/// if the `zone` is "CEST", which is GMT+2, then the result is `7200`. +/// +/// ## Errors +/// +/// Returns a [`DateTimeConversionError`] if the specified timezone is unknown to Postgres +pub fn get_timezone_offset>(zone: Tz) -> Result { + /* + * Look up the requested timezone. First we look in the timezone + * abbreviation table (to handle cases like "EST"), and if that fails, we + * look in the timezone database (to handle cases like + * "America/New_York"). (This matches the order in which timestamp input + * checks the cases; it's important because the timezone database unwisely + * uses a few zone names that are identical to offset abbreviations.) + */ + unsafe { + let mut tz = 0; + let tzname = alloc::ffi::CString::new(zone.as_ref()).unwrap(); + let lowzone; + let tztype: u32; + let mut val = 0; + let mut tzp: *mut pg_tz = 0 as _; + + /* DecodeTimezoneAbbrev requires lowercase input */ + lowzone = + pg_sys::downcase_truncate_identifier(tzname.as_ptr(), zone.as_ref().len() as _, false); + tztype = pg_sys::DecodeTimezoneAbbrev(0, lowzone, &mut val, &mut tzp) as u32; + pg_sys::pfree(lowzone.cast()); + + if tztype == pg_sys::TZ || tztype == pg_sys::DTZ { + /* fixed-offset abbreviation */ + tz = -val; + } else if tztype == pg_sys::DYNTZ { + /* dynamic-offset abbreviation, resolve using transaction start time */ + let now = pg_sys::GetCurrentTransactionStartTimestamp(); + let mut isdst = 0; + + tz = pg_sys::DetermineTimeZoneAbbrevOffsetTS(now, tzname.as_ptr(), tzp, &mut isdst); + } else { + /* try it as a full zone name */ + tzp = pg_sys::pg_tzset(tzname.as_ptr()); + if !tzp.is_null() { + /* Get the offset-from-GMT that is valid now for the zone */ + let now = pg_sys::GetCurrentTransactionStartTimestamp(); + let mut tm = Default::default(); + let mut fsec = 0; + + if pg_sys::timestamp2tm(now, &mut tz, &mut tm, &mut fsec, std::ptr::null_mut(), tzp) + != 0 + { + return Err(DateTimeConversionError::FieldOverflow); + } + } else { + return Err(DateTimeConversionError::UnknownTimezone(zone.as_ref().to_string())); + } + } + Ok(-tz) + } +} + +pub(crate) struct DateTimeTypeVisitor(PhantomData); + +impl DateTimeTypeVisitor { + pub fn new() -> Self { + DateTimeTypeVisitor(PhantomData) + } +} + +impl<'a, T: FromStr + seal::DateTimeType> serde::de::Visitor<'a> for DateTimeTypeVisitor { + type Value = T; + + fn expecting(&self, formatter: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + formatter.write_str("a borrowed string") + } + + fn visit_borrowed_str(self, v: &'a str) -> Result + where + E: serde::de::Error, + { + T::from_str(v).map_err(|_| { + serde::de::Error::invalid_value(serde::de::Unexpected::Other("invalid value"), &self) + }) + } + + fn visit_borrowed_bytes(self, v: &'a [u8]) -> Result + where + E: serde::de::Error, + { + let s = std::str::from_utf8(v) + .map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Bytes(v), &self))?; + self.visit_borrowed_str(s) + } +} + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum IntervalConversionError { + #[error("duration's total month count outside of valid i32::MIN..=i32::MAX range")] + DurationMonthsOutOfBounds, + #[error("Interval parts must all have the same sign")] + MismatchedSigns, + #[error("Negative Intervals cannot be converted into Durations")] + NegativeInterval, + #[error("Interval overflows Duration's u64 micros constructor")] + IntervalTooLarge, +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum DateTimeConversionError { + #[error("Some part of the date or time is too large")] + FieldOverflow, + #[error("THe date or time is not in the correct format")] + InvalidFormat, + #[error("`{0}` is not a known timezone")] + UnknownTimezone(String), + #[error("`{0} is not a valid timezone offset")] + InvalidTimezoneOffset(Interval), + #[error("Encoded timezone string is unknown")] + CannotParseTimezone, +} diff --git a/pgrx/src/datum/datetime_support/ops.rs b/pgrx/src/datum/datetime_support/ops.rs new file mode 100644 index 000000000..fe8316f86 --- /dev/null +++ b/pgrx/src/datum/datetime_support/ops.rs @@ -0,0 +1,367 @@ +/* +Portions Copyright 2019-2021 ZomboDB, LLC. +Portions Copyright 2021-2022 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the MIT license that can be found in the LICENSE file. +*/ +use crate::{ + direct_function_call, pg_sys, Date, Interval, IntoDatum, Time, TimeWithTimeZone, Timestamp, + TimestampWithTimeZone, +}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +impl Sub for Date { + type Output = Date; + + fn sub(self, rhs: i32) -> Self::Output { + unsafe { + direct_function_call(pg_sys::date_mii, &[self.into_datum(), rhs.into_datum()]).unwrap() + } + } +} + +impl Add for Date { + type Output = Date; + + fn add(self, rhs: i32) -> Self::Output { + unsafe { + direct_function_call(pg_sys::date_pli, &[self.into_datum(), rhs.into_datum()]).unwrap() + } + } +} + +impl Add for i32 { + type Output = Date; + + fn add(self, rhs: Date) -> Self::Output { + rhs + self + } +} + +impl Div for Interval { + type Output = Interval; + + fn div(self, rhs: f64) -> Self::Output { + unsafe { + direct_function_call(pg_sys::interval_div, &[self.as_datum(), rhs.into_datum()]) + .unwrap() + } + } +} + +impl DivAssign for Interval { + fn div_assign(&mut self, rhs: f64) { + *self = *self / rhs + } +} + +impl Sub for Interval { + type Output = Interval; + + fn sub(self, rhs: Self) -> Self::Output { + unsafe { + direct_function_call(pg_sys::interval_mi, &[self.as_datum(), rhs.as_datum()]).unwrap() + } + } +} + +impl SubAssign for Interval { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs + } +} + +impl Mul for Interval { + type Output = Interval; + + fn mul(self, rhs: f64) -> Self::Output { + unsafe { + direct_function_call(pg_sys::interval_mul, &[self.as_datum(), rhs.into_datum()]) + .unwrap() + } + } +} + +impl Mul for f64 { + type Output = Interval; + + fn mul(self, rhs: Interval) -> Self::Output { + rhs * self + } +} + +impl MulAssign for Interval { + fn mul_assign(&mut self, rhs: f64) { + *self = *self * rhs + } +} + +impl Add for Interval { + type Output = Interval; + + fn add(self, rhs: Self) -> Self::Output { + unsafe { + direct_function_call(pg_sys::interval_pl, &[self.as_datum(), rhs.as_datum()]).unwrap() + } + } +} + +impl AddAssign for Interval { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs + } +} + +impl Neg for Interval { + type Output = Interval; + + fn neg(self) -> Self::Output { + unsafe { direct_function_call(pg_sys::interval_um, &[self.into_datum()]).unwrap() } + } +} + +impl Sub for Time { + type Output = Interval; + + fn sub(self, rhs: Self) -> Self::Output { + unsafe { + direct_function_call(pg_sys::time_mi_time, &[self.into_datum(), rhs.into_datum()]) + .unwrap() + } + } +} + +impl Sub for Time { + type Output = Time; + + fn sub(self, rhs: Interval) -> Self::Output { + unsafe { + direct_function_call(pg_sys::time_mi_interval, &[self.into_datum(), rhs.as_datum()]) + .unwrap() + } + } +} + +impl Add for Time { + type Output = Time; + + fn add(self, rhs: Interval) -> Self::Output { + unsafe { + direct_function_call(pg_sys::time_pl_interval, &[self.into_datum(), rhs.as_datum()]) + .unwrap() + } + } +} + +impl Add