1use 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
31pub enum DateTimeParts {
32 Century,
39
40 Day,
43
44 Decade,
46
47 DayOfWeek,
49
50 DayOfYear,
52
53 Epoch,
58
59 Hour,
61
62 ISODayOfWeek,
66
67 ISOYear,
73
74 Julian,
80
81 Microseconds,
84
85 Millennium,
87
88 Milliseconds,
91
92 Minute,
94
95 Month,
98
99 Quarter,
101
102 Second,
104
105 Timezone,
109
110 TimezoneHour,
112
113 TimezoneMinute,
115
116 Week,
126
127 Year,
130}
131
132impl From<DateTimeParts> for &'static str {
133 #[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 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 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 fn to_iso_string(self) -> String {
235 if Self::type_oid() == pg_sys::INTERVALOID {
236 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 #[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 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 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 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 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 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 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 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, pg_sys::timestamp_cmp, pg_sys::timestamp_hash, 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#[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 tz = -val;
594 } else if tztype == pg_sys::TZNAME_DYNTZ as i32 {
595 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 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 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 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 tz = -val;
649 } else if tztype == pg_sys::DYNTZ {
650 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 crate::warning!("tzname={}", tzname.to_str().unwrap());
658 tzp = pg_sys::pg_tzset(tzname.as_ptr());
659 if !tzp.is_null() {
660 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#[derive(thiserror::Error, Debug, PartialEq, Eq)]
726pub enum DateTimeConversionError {
727 #[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 #[error("The value is out-of-range for valid instances of this datetime type")]
740 OutOfRange,
741}