pgrx/datum/
time_stamp_with_timezone.rs

1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10use super::{
11    Date, DateTimeConversionError, DateTimeParts, DateTimeTypeVisitor, FromDatum,
12    HasExtractableParts, Interval, IntoDatum, Timestamp, ToIsoString,
13};
14use crate::{direct_function_call, pg_sys};
15use pgrx_pg_sys::errcodes::PgSqlErrorCode;
16use pgrx_pg_sys::PgTryBuilder;
17use pgrx_sql_entity_graph::metadata::{
18    ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable,
19};
20use std::panic::{RefUnwindSafe, UnwindSafe};
21
22// taken from /include/datatype/timestamp.h
23const MIN_TIMESTAMP_USEC: i64 = -211_813_488_000_000_000;
24const END_TIMESTAMP_USEC: i64 = 9_223_371_331_200_000_000 - 1; // dec by 1 to accommodate exclusive range match pattern
25
26/// A safe wrapper around Postgres `TIMESTAMP WITH TIME ZONE` type, backed by a [`pg_sys::Timestamp`] integer value.
27#[derive(Debug, Copy, Clone)]
28#[repr(transparent)]
29pub struct TimestampWithTimeZone(pg_sys::TimestampTz);
30
31impl From<TimestampWithTimeZone> for pg_sys::TimestampTz {
32    #[inline]
33    fn from(value: TimestampWithTimeZone) -> Self {
34        value.0
35    }
36}
37
38/// Fallibly create a [`TimestampWithTimeZone`] from a Postgres [`pg_sys::TimestampTz`] value.
39impl TryFrom<pg_sys::TimestampTz> for TimestampWithTimeZone {
40    type Error = FromTimeError;
41
42    fn try_from(value: pg_sys::TimestampTz) -> Result<Self, Self::Error> {
43        match value {
44            i64::MIN | i64::MAX | MIN_TIMESTAMP_USEC..=END_TIMESTAMP_USEC => {
45                Ok(TimestampWithTimeZone(value))
46            }
47            _ => Err(FromTimeError::MicrosOutOfBounds),
48        }
49    }
50}
51
52impl TryFrom<pg_sys::Datum> for TimestampWithTimeZone {
53    type Error = FromTimeError;
54    fn try_from(datum: pg_sys::Datum) -> Result<Self, Self::Error> {
55        (datum.value() as pg_sys::TimestampTz).try_into()
56    }
57}
58
59/// Create a [`TimestampWithTimeZone`] from an existing [`Timestamp`] (which is understood to be
60/// in the "current time zone" and a time zone string.
61impl<Tz: AsRef<str> + UnwindSafe + RefUnwindSafe> TryFrom<(Timestamp, Tz)>
62    for TimestampWithTimeZone
63{
64    type Error = DateTimeConversionError;
65
66    fn try_from(value: (Timestamp, Tz)) -> Result<Self, Self::Error> {
67        let (ts, tz) = value;
68        TimestampWithTimeZone::with_timezone(
69            ts.year(),
70            ts.month(),
71            ts.day(),
72            ts.hour(),
73            ts.minute(),
74            ts.second(),
75            tz,
76        )
77    }
78}
79
80impl From<Date> for TimestampWithTimeZone {
81    fn from(value: Date) -> Self {
82        unsafe { direct_function_call(pg_sys::date_timestamptz, &[value.into_datum()]).unwrap() }
83    }
84}
85
86impl From<Timestamp> for TimestampWithTimeZone {
87    fn from(value: Timestamp) -> Self {
88        unsafe {
89            direct_function_call(pg_sys::timestamp_timestamptz, &[value.into_datum()]).unwrap()
90        }
91    }
92}
93
94impl IntoDatum for TimestampWithTimeZone {
95    fn into_datum(self) -> Option<pg_sys::Datum> {
96        Some(pg_sys::Datum::from(self.0))
97    }
98    fn type_oid() -> pg_sys::Oid {
99        pg_sys::TIMESTAMPTZOID
100    }
101}
102
103impl FromDatum for TimestampWithTimeZone {
104    unsafe fn from_polymorphic_datum(
105        datum: pg_sys::Datum,
106        is_null: bool,
107        _: pg_sys::Oid,
108    ) -> Option<Self>
109    where
110        Self: Sized,
111    {
112        if is_null {
113            None
114        } else {
115            Some(datum.try_into().expect("Error converting timestamp with time zone datum"))
116        }
117    }
118}
119
120impl TimestampWithTimeZone {
121    const NEG_INFINITY: pg_sys::TimestampTz = pg_sys::TimestampTz::MIN;
122    const INFINITY: pg_sys::TimestampTz = pg_sys::TimestampTz::MAX;
123
124    /// Construct a new [`TimestampWithTimeZone`] from its constituent parts.
125    ///
126    /// # Notes
127    ///
128    /// This function uses Postgres' "current time zone"
129    ///
130    /// # Errors
131    ///
132    /// Returns a [`DateTimeConversionError`] if any part is outside the bounds for that part
133    pub fn new(
134        year: i32,
135        month: u8,
136        day: u8,
137        hour: u8,
138        minute: u8,
139        second: f64,
140    ) -> Result<Self, DateTimeConversionError> {
141        let month: i32 = month as _;
142        let day: i32 = day as _;
143        let hour: i32 = hour as _;
144        let minute: i32 = minute as _;
145
146        PgTryBuilder::new(|| unsafe {
147            Ok(direct_function_call(
148                pg_sys::make_timestamptz,
149                &[
150                    year.into_datum(),
151                    month.into_datum(),
152                    day.into_datum(),
153                    hour.into_datum(),
154                    minute.into_datum(),
155                    second.into_datum(),
156                ],
157            )
158            .unwrap())
159        })
160        .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| {
161            Err(DateTimeConversionError::FieldOverflow)
162        })
163        .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| {
164            Err(DateTimeConversionError::InvalidFormat)
165        })
166        .execute()
167    }
168
169    /// Construct a new [`TimestampWithTimeZone`] from its constituent parts.
170    ///
171    /// Elides the overhead of trapping errors for out-of-bounds parts
172    ///
173    /// # Notes
174    ///
175    /// This function uses Postgres' "current time zone"
176    ///
177    /// # Panics
178    ///
179    /// This function panics if any part is out-of-bounds
180    pub fn new_unchecked(
181        year: isize,
182        month: u8,
183        day: u8,
184        hour: u8,
185        minute: u8,
186        second: f64,
187    ) -> Self {
188        let year: i32 = year as _;
189        let month: i32 = month as _;
190        let day: i32 = day as _;
191        let hour: i32 = hour as _;
192        let minute: i32 = minute as _;
193
194        unsafe {
195            direct_function_call(
196                pg_sys::make_timestamptz,
197                &[
198                    year.into_datum(),
199                    month.into_datum(),
200                    day.into_datum(),
201                    hour.into_datum(),
202                    minute.into_datum(),
203                    second.into_datum(),
204                ],
205            )
206            .unwrap()
207        }
208    }
209
210    /// Construct a new [`TimestampWithTimeZone`] from its constituent parts at a specific time zone
211    ///
212    /// # Errors
213    ///
214    /// Returns a [`DateTimeConversionError`] if any part is outside the bounds for that part
215    pub fn with_timezone<Tz: AsRef<str> + UnwindSafe + RefUnwindSafe>(
216        year: i32,
217        month: u8,
218        day: u8,
219        hour: u8,
220        minute: u8,
221        second: f64,
222        timezone: Tz,
223    ) -> Result<Self, DateTimeConversionError> {
224        let month: i32 = month as _;
225        let day: i32 = day as _;
226        let hour: i32 = hour as _;
227        let minute: i32 = minute as _;
228        let timezone_datum = timezone.as_ref().into_datum();
229
230        PgTryBuilder::new(|| unsafe {
231            Ok(direct_function_call(
232                pg_sys::make_timestamptz_at_timezone,
233                &[
234                    year.into_datum(),
235                    month.into_datum(),
236                    day.into_datum(),
237                    hour.into_datum(),
238                    minute.into_datum(),
239                    second.into_datum(),
240                    timezone_datum,
241                ],
242            )
243            .unwrap())
244        })
245        .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| {
246            Err(DateTimeConversionError::FieldOverflow)
247        })
248        .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| {
249            Err(DateTimeConversionError::InvalidFormat)
250        })
251        .catch_when(PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, |_| {
252            Err(DateTimeConversionError::UnknownTimezone(timezone.as_ref().to_string()))
253        })
254        .execute()
255    }
256
257    /// Construct a new [`TimestampWithTimeZone`] representing positive infinity
258    pub fn positive_infinity() -> Self {
259        Self(Self::INFINITY)
260    }
261
262    /// Construct a new [`TimestampWithTimeZone`] representing negative infinity
263    pub fn negative_infinity() -> Self {
264        Self(Self::NEG_INFINITY)
265    }
266
267    /// Does this [`TimestampWithTimeZone`] represent positive infinity?
268    #[inline]
269    pub fn is_infinity(&self) -> bool {
270        self.0 == Self::INFINITY
271    }
272
273    /// Does this [`TimestampWithTimeZone`] represent negative infinity?
274    #[inline]
275    pub fn is_neg_infinity(&self) -> bool {
276        self.0 == Self::NEG_INFINITY
277    }
278
279    /// Extract the `month`
280    pub fn month(&self) -> u8 {
281        self.extract_part(DateTimeParts::Month).unwrap().try_into().unwrap()
282    }
283
284    /// Extract the `day`
285    pub fn day(&self) -> u8 {
286        self.extract_part(DateTimeParts::Day).unwrap().try_into().unwrap()
287    }
288
289    /// Extract the `year`
290    pub fn year(&self) -> i32 {
291        self.extract_part(DateTimeParts::Year).unwrap().try_into().unwrap()
292    }
293
294    /// Extract the `hour`
295    pub fn hour(&self) -> u8 {
296        self.extract_part(DateTimeParts::Hour).unwrap().try_into().unwrap()
297    }
298
299    /// Extract the `minute`
300    pub fn minute(&self) -> u8 {
301        self.extract_part(DateTimeParts::Minute).unwrap().try_into().unwrap()
302    }
303
304    /// Extract the `second`
305    pub fn second(&self) -> f64 {
306        self.extract_part(DateTimeParts::Second).unwrap().try_into().unwrap()
307    }
308
309    /// Return the `microseconds` part.  This is not the time counted in microseconds, but the
310    /// fractional seconds
311    pub fn microseconds(&self) -> u32 {
312        self.extract_part(DateTimeParts::Microseconds).unwrap().try_into().unwrap()
313    }
314
315    /// Return the `hour`, `minute`, `second`, and `microseconds` as a Rust tuple
316    pub fn to_hms_micro(&self) -> (u8, u8, u8, u32) {
317        (self.hour(), self.minute(), self.second() as u8, self.microseconds())
318    }
319
320    /// Shift the [`Timestamp`] to the `UTC` time zone
321    pub fn to_utc(&self) -> Timestamp {
322        self.at_timezone("UTC").unwrap()
323    }
324
325    /// Shift the [`TimestampWithTimeZone`] to the specified time zone
326    ///
327    /// # Errors
328    ///
329    /// Returns a [`DateTimeConversionError`] if the specified time zone is invalid or if for some
330    /// other reason the underlying time cannot be represented in the specified time zone
331    pub fn at_timezone<Tz: AsRef<str> + UnwindSafe + RefUnwindSafe>(
332        &self,
333        timezone: Tz,
334    ) -> Result<Timestamp, DateTimeConversionError> {
335        let timezone_datum = timezone.as_ref().into_datum();
336        PgTryBuilder::new(|| unsafe {
337            Ok(direct_function_call(
338                pg_sys::timestamptz_zone,
339                &[timezone_datum, (*self).into_datum()],
340            )
341            .unwrap())
342        })
343        .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| {
344            Err(DateTimeConversionError::FieldOverflow)
345        })
346        .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| {
347            Err(DateTimeConversionError::InvalidFormat)
348        })
349        .catch_when(PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, |_| {
350            Err(DateTimeConversionError::UnknownTimezone(timezone.as_ref().to_string()))
351        })
352        .execute()
353    }
354
355    pub fn is_finite(&self) -> bool {
356        !matches!(self.0, pg_sys::TimestampTz::MIN | pg_sys::TimestampTz::MAX)
357    }
358
359    /// Truncate [`TimestampWithTimeZone`] to specified units
360    pub fn truncate(self, units: DateTimeParts) -> Self {
361        unsafe {
362            direct_function_call(
363                pg_sys::timestamptz_trunc,
364                &[units.into_datum(), self.into_datum()],
365            )
366            .unwrap()
367        }
368    }
369
370    /// Truncate [`TimestampWithTimeZone`] to specified units in specified time zone
371    pub fn truncate_with_time_zone<Tz: AsRef<str>>(self, units: DateTimeParts, zone: Tz) -> Self {
372        unsafe {
373            direct_function_call(
374                pg_sys::timestamptz_trunc_zone,
375                &[units.into_datum(), self.into_datum(), zone.as_ref().into_datum()],
376            )
377            .unwrap()
378        }
379    }
380
381    /// Subtract `other` from `self`, producing a “symbolic” result that uses years and months, rather than just days
382    pub fn age(&self, other: &TimestampWithTimeZone) -> Interval {
383        let ts_self: Timestamp = (*self).into();
384        let ts_other: Timestamp = (*other).into();
385        ts_self.age(&ts_other)
386    }
387
388    /// Return the backing [`pg_sys::TimestampTz`] value.
389    #[inline]
390    pub fn into_inner(self) -> pg_sys::TimestampTz {
391        self.0
392    }
393}
394
395#[derive(thiserror::Error, Debug, Clone, Copy)]
396pub enum FromTimeError {
397    #[error("timestamp value is negative infinity and shouldn't map to time::PrimitiveDateTime")]
398    NegInfinity,
399    #[error("timestamp value is negative infinity and shouldn't map to time::PrimitiveDateTime")]
400    Infinity,
401    #[error("time::PrimitiveDateTime was unable to convert this timestamp")]
402    TimeCrate,
403    #[error("microseconds outside of target microsecond range")]
404    MicrosOutOfBounds,
405    #[error("hours outside of target range")]
406    HoursOutOfBounds,
407    #[error("minutes outside of target range")]
408    MinutesOutOfBounds,
409    #[error("seconds outside of target range")]
410    SecondsOutOfBounds,
411}
412
413impl serde::Serialize for TimestampWithTimeZone {
414    /// Serialize this [`TimestampWithTimeZone`] in ISO form, compatible with most JSON parsers
415    fn serialize<S>(
416        &self,
417        serializer: S,
418    ) -> std::result::Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error>
419    where
420        S: serde::Serializer,
421    {
422        serializer
423            .serialize_str(&self.to_iso_string())
424            .map_err(|err| serde::ser::Error::custom(format!("formatting problem: {err:?}")))
425    }
426}
427
428impl<'de> serde::Deserialize<'de> for TimestampWithTimeZone {
429    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
430    where
431        D: serde::de::Deserializer<'de>,
432    {
433        deserializer.deserialize_str(DateTimeTypeVisitor::<Self>::new())
434    }
435}
436
437unsafe impl SqlTranslatable for TimestampWithTimeZone {
438    fn argument_sql() -> Result<SqlMapping, ArgumentError> {
439        Ok(SqlMapping::literal("timestamp with time zone"))
440    }
441    fn return_sql() -> Result<Returns, ReturnsError> {
442        Ok(Returns::One(SqlMapping::literal("timestamp with time zone")))
443    }
444}