1use 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
22const MIN_TIMESTAMP_USEC: i64 = -211_813_488_000_000_000;
24const END_TIMESTAMP_USEC: i64 = 9_223_371_331_200_000_000 - 1; #[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
38impl 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
59impl<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 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 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 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 pub fn positive_infinity() -> Self {
259 Self(Self::INFINITY)
260 }
261
262 pub fn negative_infinity() -> Self {
264 Self(Self::NEG_INFINITY)
265 }
266
267 #[inline]
269 pub fn is_infinity(&self) -> bool {
270 self.0 == Self::INFINITY
271 }
272
273 #[inline]
275 pub fn is_neg_infinity(&self) -> bool {
276 self.0 == Self::NEG_INFINITY
277 }
278
279 pub fn month(&self) -> u8 {
281 self.extract_part(DateTimeParts::Month).unwrap().try_into().unwrap()
282 }
283
284 pub fn day(&self) -> u8 {
286 self.extract_part(DateTimeParts::Day).unwrap().try_into().unwrap()
287 }
288
289 pub fn year(&self) -> i32 {
291 self.extract_part(DateTimeParts::Year).unwrap().try_into().unwrap()
292 }
293
294 pub fn hour(&self) -> u8 {
296 self.extract_part(DateTimeParts::Hour).unwrap().try_into().unwrap()
297 }
298
299 pub fn minute(&self) -> u8 {
301 self.extract_part(DateTimeParts::Minute).unwrap().try_into().unwrap()
302 }
303
304 pub fn second(&self) -> f64 {
306 self.extract_part(DateTimeParts::Second).unwrap().try_into().unwrap()
307 }
308
309 pub fn microseconds(&self) -> u32 {
312 self.extract_part(DateTimeParts::Microseconds).unwrap().try_into().unwrap()
313 }
314
315 pub fn to_hms_micro(&self) -> (u8, u8, u8, u32) {
317 (self.hour(), self.minute(), self.second() as u8, self.microseconds())
318 }
319
320 pub fn to_utc(&self) -> Timestamp {
322 self.at_timezone("UTC").unwrap()
323 }
324
325 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 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 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 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 #[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 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}