pgx/datum/
time_with_timezone.rs

1/*
2Portions Copyright 2019-2021 ZomboDB, LLC.
3Portions Copyright 2021-2022 Technology Concepts & Design, Inc. <support@tcdi.com>
4
5All rights reserved.
6
7Use of this source code is governed by the MIT license that can be found in the LICENSE file.
8*/
9
10use crate::datum::time::{Time, USECS_PER_DAY};
11use crate::{pg_sys, FromDatum, IntoDatum, PgBox};
12use pgx_sql_entity_graph::metadata::{
13    ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable,
14};
15
16#[derive(Debug, Clone)]
17#[repr(C)]
18pub struct TimeWithTimeZone {
19    t: Time,
20    /// America/Denver time in ISO:      -06:00
21    /// America/Denver time in Postgres: +21600
22    /// Yes, the sign is flipped, following POSIX instead of ISO. Don't overthink it.
23    tz_secs: i32,
24}
25
26impl FromDatum for TimeWithTimeZone {
27    #[inline]
28    unsafe fn from_polymorphic_datum(
29        datum: pg_sys::Datum,
30        is_null: bool,
31        typoid: pg_sys::Oid,
32    ) -> Option<TimeWithTimeZone> {
33        if is_null {
34            None
35        } else {
36            let timetz = PgBox::from_pg(datum.cast_mut_ptr::<pg_sys::TimeTzADT>());
37
38            let t = Time::from_polymorphic_datum(timetz.time.into(), false, typoid)
39                .expect("failed to convert TimeWithTimeZone");
40            let tz_secs = timetz.zone;
41
42            Some(TimeWithTimeZone { t, tz_secs })
43        }
44    }
45}
46
47impl IntoDatum for TimeWithTimeZone {
48    #[inline]
49    fn into_datum(self) -> Option<pg_sys::Datum> {
50        let mut timetz = unsafe { PgBox::<pg_sys::TimeTzADT>::alloc() };
51        timetz.zone = self.tz_secs;
52        timetz.time = self.t.0 as i64;
53
54        Some(timetz.into_pg().into())
55    }
56
57    fn type_oid() -> pg_sys::Oid {
58        pg_sys::TIMETZOID
59    }
60}
61
62impl TimeWithTimeZone {
63    /// Constructs a TimeWithTimeZone from `time` crate components.
64    #[deprecated(
65        since = "0.5.0",
66        note = "the repr of pgx::TimeWithTimeZone is no longer time::Time \
67    and this fn will be removed in a future version"
68    )]
69    #[cfg(feature = "time-crate")]
70    pub fn new(time: time::Time, at_tz_offset: time::UtcOffset) -> Self {
71        let (h, m, s, micro) = time.as_hms_micro();
72        let t = Time::from_hms_micro(h, m, s, micro).unwrap();
73        // Flip the sign, because time::Time uses the ISO sign convention
74        let tz_secs = -at_tz_offset.whole_seconds();
75        TimeWithTimeZone { t, tz_secs }
76    }
77
78    pub fn to_utc(self) -> Time {
79        let TimeWithTimeZone { t, tz_secs } = self;
80        let tz_micros = tz_secs as i64 * 1_000_000;
81        // tz_secs uses a flipped sign from the ISO tz string, so just add to get UTC
82        let t_unwrapped = t.0 as i64 + tz_micros;
83        Time(t_unwrapped.rem_euclid(USECS_PER_DAY as i64) as u64)
84    }
85}
86
87impl From<Time> for TimeWithTimeZone {
88    fn from(t: Time) -> TimeWithTimeZone {
89        TimeWithTimeZone { t, tz_secs: 0 }
90    }
91}
92
93impl serde::Serialize for TimeWithTimeZone {
94    fn serialize<S>(
95        &self,
96        serializer: S,
97    ) -> std::result::Result<<S as serde::Serializer>::Ok, <S as serde::Serializer>::Error>
98    where
99        S: serde::Serializer,
100    {
101        let cstr: Option<&core::ffi::CStr> = unsafe {
102            crate::direct_function_call(
103                pg_sys::timetz_out,
104                vec![Some(pg_sys::Datum::from(self as *const Self))],
105            )
106        };
107        serializer.serialize_str(cstr.and_then(|c| c.to_str().ok()).unwrap())
108    }
109}
110
111unsafe impl SqlTranslatable for TimeWithTimeZone {
112    fn argument_sql() -> Result<SqlMapping, ArgumentError> {
113        Ok(SqlMapping::literal("time with time zone"))
114    }
115    fn return_sql() -> Result<Returns, ReturnsError> {
116        Ok(Returns::One(SqlMapping::literal("time with time zone")))
117    }
118}