Skip to main content

pgrx/datetime/support/
mod.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::{Date, Interval, Time, TimeWithTimeZone, Timestamp, TimestampWithTimeZone};
11use crate::datum::{AnyNumeric, IntoDatum};
12use crate::{direct_function_call, pg_sys};
13use core::fmt::{Display, Formatter};
14use core::str::FromStr;
15use pgrx_pg_sys::errcodes::PgSqlErrorCode;
16use pgrx_pg_sys::{PgTryBuilder, pg_tz};
17use std::cmp::Ordering;
18use std::hash::{Hash, Hasher};
19use std::marker::PhantomData;
20
21mod ctor;
22mod ops;
23
24pub use ctor::*;
25
26pub const USECS_PER_SEC: i64 = 1_000_000;
27pub const USECS_PER_DAY: i64 = pg_sys::SECS_PER_DAY as i64 * USECS_PER_SEC;
28
29/// Tags to identify which "part" of a date or time-type value to extract or truncate to
30#[derive(Debug, Copy, Clone, Eq, PartialEq)]
31pub enum DateTimeParts {
32    /// The century
33    ///
34    /// The first century starts at 0001-01-01 00:00:00 AD, although they did not know it at the time.
35    /// This definition applies to all Gregorian calendar countries. There is no century number 0,
36    /// you go from -1 century to 1 century. If you disagree with this, please write your complaint
37    /// to: Pope, Cathedral Saint-Peter of Roma, Vatican.
38    Century,
39
40    /// For `timestamp` values, the day (of the month) field (1–31) ; for `interval values`, the
41    /// number of days
42    Day,
43
44    /// The year field divided by 10
45    Decade,
46
47    /// The day of the week as Sunday (0) to Saturday (6)
48    DayOfWeek,
49
50    /// The day of the year (1–365/366)
51    DayOfYear,
52
53    /// For timestamp with time zone values, the number of seconds since 1970-01-01 00:00:00 UTC
54    /// (negative for timestamps before that); for date and timestamp values, the nominal number of
55    /// seconds since 1970-01-01 00:00:00, without regard to time zone or daylight-savings rules; for
56    /// interval values, the total number of seconds in the interval
57    Epoch,
58
59    /// The hour field (0–23)
60    Hour,
61
62    /// The day of the week as Monday (1) to Sunday (7)
63    ///
64    /// This is identical to dow except for Sunday. This matches the ISO 8601 day of the week numbering.
65    ISODayOfWeek,
66
67    /// The ISO 8601 week-numbering year that the date falls in (not applicable to intervals)
68    ///
69    /// Each ISO 8601 week-numbering year begins with the Monday of the week containing the 4th of
70    /// January, so in early January or late December the ISO year may be different from the
71    /// Gregorian year. See the week field for more information.
72    ISOYear,
73
74    /// The *Julian Date* corresponding to the date or timestamp (not applicable to intervals).
75    /// Timestamps that are not local midnight result in a fractional value. See [Section B.7] for
76    /// more information.
77    ///
78    /// [Section B.7](https://www.postgresql.org/docs/current/datetime-julian-dates.html)
79    Julian,
80
81    /// The seconds field, including fractional parts, multiplied by 1 000 000; note that this
82    /// includes full seconds
83    Microseconds,
84
85    /// The millennium
86    Millennium,
87
88    /// The seconds field, including fractional parts, multiplied by 1000. Note that this includes
89    /// full seconds.
90    Milliseconds,
91
92    /// The minutes field (0–59)
93    Minute,
94
95    /// For `timestamp` values, the number of the month within the year (1–12) ; for `interval` values,
96    /// the number of months, modulo 12 (0–11)
97    Month,
98
99    /// The quarter of the year (1–4) that the date is in
100    Quarter,
101
102    /// The seconds field, including any fractional seconds
103    Second,
104
105    /// The time zone offset from UTC, measured in seconds. Positive values correspond to time zones
106    /// east of UTC, negative values to zones west of UTC. (Technically, PostgreSQL does not use UTC
107    /// because leap seconds are not handled.)
108    Timezone,
109
110    /// The hour component of the time zone offset
111    TimezoneHour,
112
113    /// The minute component of the time zone offset
114    TimezoneMinute,
115
116    /// The number of the ISO 8601 week-numbering week of the year. By definition, ISO weeks start on
117    /// Mondays and the first week of a year contains January 4 of that year. In other words, the
118    /// first Thursday of a year is in week 1 of that year.
119    ///
120    /// In the ISO week-numbering system, it is possible for early-January dates to be part of the
121    /// 52nd or 53rd week of the previous year, and for late-December dates to be part of the first
122    /// week of the next year. For example, 2005-01-01 is part of the 53rd week of year 2004, and
123    /// 2006-01-01 is part of the 52nd week of year 2005, while 2012-12-31 is part of the first week
124    /// of 2013. It's recommended to use the isoyear field together with week to get consistent results.
125    Week,
126
127    /// The year field. Keep in mind there is no `0 AD`, so subtracting BC years from AD years should
128    /// be done with care.
129    Year,
130}
131
132impl From<DateTimeParts> for &'static str {
133    /// Convert to Postgres' string representation of [`DateTimeParts`]
134    #[inline]
135    fn from(value: DateTimeParts) -> Self {
136        match value {
137            DateTimeParts::Century => "century",
138            DateTimeParts::Day => "day",
139            DateTimeParts::Decade => "decade",
140            DateTimeParts::DayOfWeek => "dow",
141            DateTimeParts::DayOfYear => "doy",
142            DateTimeParts::Epoch => "epoch",
143            DateTimeParts::Hour => "hour",
144            DateTimeParts::ISODayOfWeek => "isodow",
145            DateTimeParts::ISOYear => "isodoy",
146            DateTimeParts::Julian => "julian",
147            DateTimeParts::Microseconds => "microseconds",
148            DateTimeParts::Millennium => "millennium",
149            DateTimeParts::Milliseconds => "milliseconds",
150            DateTimeParts::Minute => "minute",
151            DateTimeParts::Month => "month",
152            DateTimeParts::Quarter => "quarter",
153            DateTimeParts::Second => "second",
154            DateTimeParts::Timezone => "timezone",
155            DateTimeParts::TimezoneHour => "timezone_hour",
156            DateTimeParts::TimezoneMinute => "timezone_minute",
157            DateTimeParts::Week => "week",
158            DateTimeParts::Year => "year",
159        }
160    }
161}
162
163impl Display for DateTimeParts {
164    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
165        let name: &'static str = (*self).into();
166        write!(f, "{name}")
167    }
168}
169
170impl IntoDatum for DateTimeParts {
171    #[inline]
172    fn into_datum(self) -> Option<pg_sys::Datum> {
173        let name: &'static str = self.into();
174        name.into_datum()
175    }
176
177    #[inline]
178    fn type_oid() -> pg_sys::Oid {
179        pg_sys::TEXTOID
180    }
181}
182
183mod seal {
184    #[doc(hidden)]
185    pub trait DateTimeType {}
186}
187
188pub trait HasExtractableParts: Clone + IntoDatum + seal::DateTimeType {
189    const EXTRACT_FUNCTION: unsafe fn(pg_sys::FunctionCallInfo) -> pg_sys::Datum;
190
191    /// Extract a [`DateTimeParts`] part from a date/time-like type
192    fn extract_part(&self, field: DateTimeParts) -> Option<AnyNumeric> {
193        unsafe {
194            let field_datum = field.into_datum();
195            #[cfg(feature = "pg13")]
196            let field_value: Option<f64> = direct_function_call(
197                Self::EXTRACT_FUNCTION,
198                &[field_datum, self.clone().into_datum()],
199            );
200            #[cfg(any(
201                feature = "pg14",
202                feature = "pg15",
203                feature = "pg16",
204                feature = "pg17",
205                feature = "pg18",
206                feature = "pg19"
207            ))]
208            let field_value: Option<AnyNumeric> = direct_function_call(
209                Self::EXTRACT_FUNCTION,
210                &[field_datum, self.clone().into_datum()],
211            );
212            // don't leak the TEXT datum we made
213            pg_sys::pfree(field_datum.unwrap().cast_mut_ptr());
214
215            #[cfg(feature = "pg13")]
216            {
217                field_value.map(|v| AnyNumeric::from_str(&format!("{v}")).unwrap())
218            }
219            #[cfg(not(feature = "pg13"))]
220            {
221                field_value
222            }
223        }
224    }
225}
226
227pub trait ToIsoString: IntoDatum + Sized + Display + seal::DateTimeType {
228    /// Encode of this date/time-like type into JSON string in ISO format using
229    /// optionally preallocated buffer 'buf'.
230    ///
231    /// # Notes
232    ///
233    /// Types `with time zone` use the Postgres globally configured time zone in the text representation
234    fn to_iso_string(self) -> String {
235        if Self::type_oid() == pg_sys::INTERVALOID {
236            // `Interval` is just represented in its string form
237            self.to_string()
238        } else {
239            unsafe {
240                let jsonb = pg_sys::JsonEncodeDateTime(
241                    std::ptr::null_mut(),
242                    self.into_datum().unwrap(),
243                    Self::type_oid(),
244                    std::ptr::null(),
245                );
246                let cstr = core::ffi::CStr::from_ptr(jsonb);
247                let as_string = cstr.to_str().unwrap().to_string();
248                pg_sys::pfree(jsonb.cast());
249
250                as_string
251            }
252        }
253    }
254
255    /// Encode of this date/time-like type into JSON string in ISO format using
256    /// optionally preallocated buffer 'buf'.
257    ///
258    /// # Notes
259    ///
260    /// This function is only available on Postgres v13 and greater
261    #[cfg(any(
262        feature = "pg13",
263        feature = "pg14",
264        feature = "pg15",
265        feature = "pg16",
266        feature = "pg17",
267        feature = "pg18",
268        feature = "pg19"
269    ))]
270    fn to_iso_string_with_timezone<Tz: AsRef<str>>(
271        self,
272        timezone: Tz,
273    ) -> Result<String, DateTimeConversionError> {
274        if Self::type_oid() == pg_sys::INTERVALOID {
275            // `Interval` is just represented in its string form
276            Ok(self.to_string())
277        } else {
278            let tzoffset = -get_timezone_offset(&timezone)?;
279
280            unsafe {
281                let jsonb = pg_sys::JsonEncodeDateTime(
282                    std::ptr::null_mut(),
283                    self.into_datum().unwrap(),
284                    Self::type_oid(),
285                    &tzoffset,
286                );
287                let cstr = core::ffi::CStr::from_ptr(jsonb);
288                let as_string = cstr.to_str().unwrap().to_string();
289                pg_sys::pfree(jsonb.cast());
290
291                Ok(as_string)
292            }
293        }
294    }
295}
296
297macro_rules! impl_wrappers {
298    ($ty:ty, $eq_fn:path, $cmp_fn:path, $hash_fn:path, $extract_fn:path, $input_fn:path, $output_fn:path) => {
299        impl seal::DateTimeType for $ty {}
300
301        impl Eq for $ty {}
302
303        impl PartialEq for $ty {
304            /// Uses the underlying Postgres "_eq()" function for this type
305            fn eq(&self, other: &Self) -> bool {
306                unsafe {
307                    direct_function_call($eq_fn, &[self.into_datum(), other.into_datum()]).unwrap()
308                }
309            }
310        }
311
312        impl Ord for $ty {
313            /// Uses the underlying Postgres "_cmp()" function for this type
314            fn cmp(&self, other: &Self) -> Ordering {
315                unsafe {
316                    match direct_function_call::<i32>(
317                        $cmp_fn,
318                        &[self.into_datum(), other.into_datum()],
319                    ) {
320                        Some(-1) => Ordering::Less,
321                        Some(0) => Ordering::Equal,
322                        Some(1) => Ordering::Greater,
323                        _ => panic!("unexpected response from {}", stringify!($cmp_fn)),
324                    }
325                }
326            }
327        }
328
329        impl PartialOrd for $ty {
330            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
331                Some(self.cmp(other))
332            }
333        }
334
335        impl Hash for $ty {
336            /// Uses the underlying Postgres "hash" function for this type
337            fn hash<H: Hasher>(&self, state: &mut H) {
338                let hash: i32 = unsafe {
339                    direct_function_call($hash_fn, &[self.clone().into_datum()]).unwrap()
340                };
341                state.write_i32(hash);
342            }
343        }
344
345        impl HasExtractableParts for $ty {
346            const EXTRACT_FUNCTION: unsafe fn(pg_sys::FunctionCallInfo) -> pg_sys::Datum =
347                $extract_fn;
348        }
349
350        impl ToIsoString for $ty {}
351
352        impl FromStr for $ty {
353            type Err = DateTimeConversionError;
354
355            /// Create this type from a string.
356            fn from_str(s: &str) -> Result<Self, Self::Err> {
357                use pgrx_pg_sys::AsPgCStr;
358                let cstr = s.as_pg_cstr();
359                let cstr_datum = pg_sys::Datum::from(cstr);
360                unsafe {
361                    let result = PgTryBuilder::new(|| {
362                        let result = direct_function_call::<$ty>(
363                            $input_fn,
364                            &[
365                                Some(cstr_datum),
366                                pgrx_pg_sys::InvalidOid.into_datum(),
367                                (-1i32).into_datum(),
368                            ],
369                        )
370                        .unwrap();
371                        Ok(result)
372                    })
373                    .catch_when(PgSqlErrorCode::ERRCODE_DATETIME_FIELD_OVERFLOW, |_| {
374                        Err(DateTimeConversionError::FieldOverflow)
375                    })
376                    .catch_when(PgSqlErrorCode::ERRCODE_INVALID_DATETIME_FORMAT, |_| {
377                        Err(DateTimeConversionError::InvalidFormat)
378                    })
379                    .catch_when(PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, |_| {
380                        Err(DateTimeConversionError::CannotParseTimezone)
381                    })
382                    .execute();
383                    pg_sys::pfree(cstr.cast());
384                    result
385                }
386            }
387        }
388
389        impl Display for $ty {
390            /// Uses the underlying "output" function to convert this type to a String
391            fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
392                let text: &core::ffi::CStr = unsafe {
393                    direct_function_call($output_fn, &[self.clone().into_datum()]).unwrap()
394                };
395                write!(f, "{}", text.to_str().unwrap())
396            }
397        }
398    };
399}
400
401#[cfg(feature = "pg13")]
402const DATE_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum = pg13::date_part;
403
404#[cfg(feature = "pg13")]
405mod pg13 {
406    use crate::prelude::*;
407
408    pub(super) unsafe fn date_part(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum {
409        // we need to first convert the `date` value into a `timestamp` value
410        // then call the `timestamp_part` function.
411        //
412        // this is essentially how the `date_part()` function is declared in the system catalogs
413        // for pg13:
414        /**
415            \sf date_part(text, date)
416            CREATE OR REPLACE FUNCTION pg_catalog.date_part(text, date)
417             RETURNS double precision
418             LANGUAGE sql
419             IMMUTABLE PARALLEL SAFE STRICT COST 1
420            AS $function$select pg_catalog.date_part($1, cast($2 as timestamp without time zone))$function$
421        */
422        use crate::fcinfo::*;
423        let timezone = pg_getarg_datum(fcinfo, 0);
424        let date = pg_getarg_datum(fcinfo, 1);
425        let timestamp = direct_function_call_as_datum(pg_sys::date_timestamp, &[date]);
426        direct_function_call_as_datum(pg_sys::timestamp_part, &[timezone, timestamp])
427            .unwrap_or_else(|| pg_sys::Datum::from(0))
428    }
429}
430
431#[cfg(any(
432    feature = "pg14",
433    feature = "pg15",
434    feature = "pg16",
435    feature = "pg17",
436    feature = "pg18",
437    feature = "pg19"
438))]
439const DATE_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
440    pg_sys::extract_date;
441impl_wrappers!(
442    Date,
443    pg_sys::date_eq,
444    pg_sys::date_cmp,
445    pg_sys::hashint8,
446    DATE_EXTRACT,
447    pg_sys::date_in,
448    pg_sys::date_out
449);
450
451#[cfg(feature = "pg13")]
452const TIME_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
453    pg_sys::time_part;
454#[cfg(any(
455    feature = "pg14",
456    feature = "pg15",
457    feature = "pg16",
458    feature = "pg17",
459    feature = "pg18",
460    feature = "pg19"
461))]
462const TIME_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
463    pg_sys::extract_time;
464
465impl_wrappers!(
466    Time,
467    pg_sys::time_eq,
468    pg_sys::time_cmp,
469    pg_sys::time_hash,
470    TIME_EXTRACT,
471    pg_sys::time_in,
472    pg_sys::time_out
473);
474
475#[cfg(feature = "pg13")]
476const TIMETZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
477    pg_sys::timetz_part;
478#[cfg(any(
479    feature = "pg14",
480    feature = "pg15",
481    feature = "pg16",
482    feature = "pg17",
483    feature = "pg18",
484    feature = "pg19"
485))]
486const TIMETZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
487    pg_sys::extract_timetz;
488
489impl_wrappers!(
490    TimeWithTimeZone,
491    pg_sys::timetz_eq,
492    pg_sys::timetz_cmp,
493    pg_sys::timetz_hash,
494    TIMETZ_EXTRACT,
495    pg_sys::timetz_in,
496    pg_sys::timetz_out
497);
498
499#[cfg(feature = "pg13")]
500const TIMESTAMP_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
501    pg_sys::timestamp_part;
502#[cfg(any(
503    feature = "pg14",
504    feature = "pg15",
505    feature = "pg16",
506    feature = "pg17",
507    feature = "pg18",
508    feature = "pg19"
509))]
510const TIMESTAMP_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
511    pg_sys::extract_timestamp;
512
513impl_wrappers!(
514    Timestamp,
515    pg_sys::timestamp_eq,
516    pg_sys::timestamp_cmp,
517    pg_sys::timestamp_hash,
518    TIMESTAMP_EXTRACT,
519    pg_sys::timestamp_in,
520    pg_sys::timestamp_out
521);
522
523#[cfg(feature = "pg13")]
524const TIMESTAMPTZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
525    pg_sys::timestamptz_part;
526#[cfg(any(
527    feature = "pg14",
528    feature = "pg15",
529    feature = "pg16",
530    feature = "pg17",
531    feature = "pg18",
532    feature = "pg19"
533))]
534const TIMESTAMPTZ_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
535    pg_sys::extract_timestamptz;
536
537impl_wrappers!(
538    TimestampWithTimeZone,
539    pg_sys::timestamp_eq,   // yes, this is correct
540    pg_sys::timestamp_cmp,  // yes, this is correct
541    pg_sys::timestamp_hash, // yes, this is correct
542    TIMESTAMPTZ_EXTRACT,
543    pg_sys::timestamptz_in,
544    pg_sys::timestamptz_out
545);
546
547#[cfg(feature = "pg13")]
548const INTERVAL_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
549    pg_sys::interval_part;
550#[cfg(any(
551    feature = "pg14",
552    feature = "pg15",
553    feature = "pg16",
554    feature = "pg17",
555    feature = "pg18",
556    feature = "pg19"
557))]
558const INTERVAL_EXTRACT: unsafe fn(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum =
559    pg_sys::extract_interval;
560
561impl_wrappers!(
562    Interval,
563    pg_sys::interval_eq,
564    pg_sys::interval_cmp,
565    pg_sys::interval_hash,
566    INTERVAL_EXTRACT,
567    pg_sys::interval_in,
568    pg_sys::interval_out
569);
570
571// ported from `v5.2/src/backend/utils/adt/date.c#3034`
572/// Calculate the timezone offset in seconds, from GMT, for the specified named time`zone`.
573///
574/// If for example, the `zone` is "EDT", which is GMT-4, then the result is `-14400`.  Similarly,
575/// if the `zone` is "CEST", which is GMT+2, then the result is `7200`.
576///
577/// ## Errors
578/// Returns a [`DateTimeConversionError`] if the specified timezone is unknown to Postgres
579#[cfg(any(feature = "pg16", feature = "pg17", feature = "pg18", feature = "pg19"))]
580pub fn get_timezone_offset<Tz: AsRef<str>>(zone: Tz) -> Result<i32, DateTimeConversionError> {
581    let zone = zone.as_ref();
582    PgTryBuilder::new(|| {
583        unsafe {
584            let tzname = alloc::ffi::CString::new(zone).unwrap();
585            let mut tz = 0;
586            let mut val = 0;
587            let mut tzp: *mut pg_tz = 0 as _;
588
589            let tztype = pg_sys::DecodeTimezoneName(tzname.as_ptr(), &mut val, &mut tzp);
590
591            if tztype == pg_sys::TZNAME_FIXED_OFFSET as i32 {
592                /* fixed-offset abbreviation */
593                tz = -val;
594            } else if tztype == pg_sys::TZNAME_DYNTZ as i32 {
595                /* dynamic-offset abbreviation, resolve using transaction start time */
596                let now = pg_sys::GetCurrentTransactionStartTimestamp();
597                let mut isdst = 0;
598
599                tz = pg_sys::DetermineTimeZoneAbbrevOffsetTS(now, tzname.as_ptr(), tzp, &mut isdst);
600            } else {
601                /* Get the offset-from-GMT that is valid now for the zone name */
602                let now = pg_sys::GetCurrentTransactionStartTimestamp();
603                let mut tm = Default::default();
604                let mut fsec = 0;
605
606                if pg_sys::timestamp2tm(now, &mut tz, &mut tm, &mut fsec, std::ptr::null_mut(), tzp)
607                    != 0
608                {
609                    return Err(DateTimeConversionError::FieldOverflow);
610                }
611            }
612            Ok(-tz)
613        }
614    })
615    .catch_when(PgSqlErrorCode::ERRCODE_INVALID_PARAMETER_VALUE, |_| {
616        Err(DateTimeConversionError::UnknownTimezone(zone.to_string()))
617    })
618    .execute()
619}
620
621#[cfg(any(feature = "pg13", feature = "pg14", feature = "pg15"))]
622pub fn get_timezone_offset<Tz: AsRef<str>>(zone: Tz) -> Result<i32, DateTimeConversionError> {
623    /*
624     * Look up the requested time zone.  First we look in the time zone
625     * abbreviation table (to handle cases like "EST"), and if that fails, we
626     * look in the time zone database (to handle cases like
627     * "America/New_York").  (This matches the order in which timestamp input
628     * checks the cases; it's important because the time zone database unwisely
629     * uses a few zone names that are identical to offset abbreviations.)
630     */
631    unsafe {
632        let mut tz = 0;
633        let tzname = alloc::ffi::CString::new(zone.as_ref()).unwrap();
634
635        let mut val = 0;
636        let mut tzp: *mut pg_tz = 0 as _;
637
638        /* DecodeTimezoneAbbrev requires lowercase input */
639        let lowzone =
640            pg_sys::downcase_truncate_identifier(tzname.as_ptr(), zone.as_ref().len() as _, false);
641
642        let tztype = pg_sys::DecodeTimezoneAbbrev(0, lowzone, &mut val, &mut tzp) as u32;
643
644        pg_sys::pfree(lowzone.cast());
645
646        if tztype == pg_sys::TZ || tztype == pg_sys::DTZ {
647            /* fixed-offset abbreviation */
648            tz = -val;
649        } else if tztype == pg_sys::DYNTZ {
650            /* dynamic-offset abbreviation, resolve using transaction start time */
651            let now = pg_sys::GetCurrentTransactionStartTimestamp();
652            let mut isdst = 0;
653
654            tz = pg_sys::DetermineTimeZoneAbbrevOffsetTS(now, tzname.as_ptr(), tzp, &mut isdst);
655        } else {
656            /* try it as a full zone name */
657            crate::warning!("tzname={}", tzname.to_str().unwrap());
658            tzp = pg_sys::pg_tzset(tzname.as_ptr());
659            if !tzp.is_null() {
660                /* Get the offset-from-GMT that is valid now for the zone */
661                let now = pg_sys::GetCurrentTransactionStartTimestamp();
662                let mut tm = Default::default();
663                let mut fsec = 0;
664
665                if pg_sys::timestamp2tm(now, &mut tz, &mut tm, &mut fsec, std::ptr::null_mut(), tzp)
666                    != 0
667                {
668                    return Err(DateTimeConversionError::FieldOverflow);
669                }
670            } else {
671                return Err(DateTimeConversionError::UnknownTimezone(zone.as_ref().to_string()));
672            }
673        }
674        Ok(-tz)
675    }
676}
677
678pub(crate) struct DateTimeTypeVisitor<T>(PhantomData<T>);
679
680impl<T> DateTimeTypeVisitor<T> {
681    pub fn new() -> Self {
682        DateTimeTypeVisitor(PhantomData)
683    }
684}
685
686impl<'a, T: FromStr + seal::DateTimeType> serde::de::Visitor<'a> for DateTimeTypeVisitor<T> {
687    type Value = T;
688
689    fn expecting(&self, formatter: &mut alloc::fmt::Formatter) -> alloc::fmt::Result {
690        formatter.write_str("a borrowed string")
691    }
692
693    fn visit_borrowed_str<E>(self, v: &'a str) -> Result<Self::Value, E>
694    where
695        E: serde::de::Error,
696    {
697        T::from_str(v).map_err(|_| {
698            serde::de::Error::invalid_value(serde::de::Unexpected::Other("invalid value"), &self)
699        })
700    }
701
702    fn visit_borrowed_bytes<E>(self, v: &'a [u8]) -> Result<Self::Value, E>
703    where
704        E: serde::de::Error,
705    {
706        let s = std::str::from_utf8(v)
707            .map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Bytes(v), &self))?;
708        self.visit_borrowed_str(s)
709    }
710}
711
712#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
713pub enum IntervalConversionError {
714    #[error("duration's total month count outside of valid i32::MIN..=i32::MAX range")]
715    DurationMonthsOutOfBounds,
716    #[error("Interval parts must all have the same sign")]
717    MismatchedSigns,
718    #[error("Negative Intervals cannot be converted into Durations")]
719    NegativeInterval,
720    #[error("Interval overflows Duration's u64 micros constructor")]
721    IntervalTooLarge,
722}
723
724/// A problem converting a value into a datetime format
725#[derive(thiserror::Error, Debug, PartialEq, Eq)]
726pub enum DateTimeConversionError {
727    /// ERRCODE_DATETIME_FIELD_OVERFLOW
728    #[error("Some part of the date or time is too large")]
729    FieldOverflow,
730    #[error("The date or time is not in the correct format")]
731    InvalidFormat,
732    #[error("`{0}` is not a known time zone")]
733    UnknownTimezone(String),
734    #[error("`{0} is not a valid time zone offset")]
735    InvalidTimezoneOffset(i32),
736    #[error("Encoded time zone string is unknown")]
737    CannotParseTimezone,
738    /// ERRCODE_DATETIME_VALUE_OUT_OF_RANGE
739    #[error("The value is out-of-range for valid instances of this datetime type")]
740    OutOfRange,
741}