Skip to main content

mz_repr/adt/
timestamp.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9//
10// Portions of this file are derived from the PostgreSQL project. The original
11// source code was retrieved on June 1, 2023 from:
12//
13//     https://github.com/postgres/postgres/blob/REL_15_3/src/backend/utils/adt/timestamp.c
14//
15// The original source code is subject to the terms of the PostgreSQL license, a
16// copy of which can be found in the LICENSE file at the root of this
17// repository.
18
19//! Methods for checked timestamp operations.
20
21use std::error::Error;
22use std::fmt::{self, Display};
23use std::ops::Sub;
24use std::sync::LazyLock;
25
26use ::chrono::{
27    DateTime, Datelike, Days, Duration, Months, NaiveDate, NaiveDateTime, NaiveTime, Utc,
28};
29use chrono::Timelike;
30use mz_ore::cast::{self, CastFrom};
31use mz_persist_types::columnar::FixedSizeCodec;
32use mz_proto::chrono::ProtoNaiveDateTime;
33use mz_proto::{ProtoType, RustType, TryFromProtoError};
34#[cfg(any(test, feature = "proptest"))]
35use proptest::arbitrary::Arbitrary;
36#[cfg(any(test, feature = "proptest"))]
37use proptest::strategy::{BoxedStrategy, Strategy};
38#[cfg(any(test, feature = "proptest"))]
39use proptest_derive::Arbitrary;
40use serde::{Deserialize, Serialize, Serializer};
41use thiserror::Error;
42
43use crate::Datum;
44use crate::adt::datetime::DateTimePart;
45use crate::adt::interval::Interval;
46use crate::adt::numeric::DecimalLike;
47#[cfg(any(test, feature = "proptest"))]
48use crate::scalar::{arb_naive_date_time, arb_utc_date_time};
49
50include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.timestamp.rs"));
51
52const MONTHS_PER_YEAR: i64 = cast::u16_to_i64(Interval::MONTH_PER_YEAR);
53const HOURS_PER_DAY: i64 = cast::u16_to_i64(Interval::HOUR_PER_DAY);
54const MINUTES_PER_HOUR: i64 = cast::u16_to_i64(Interval::MINUTE_PER_HOUR);
55const SECONDS_PER_MINUTE: i64 = cast::u16_to_i64(Interval::SECOND_PER_MINUTE);
56
57const NANOSECONDS_PER_HOUR: i64 = NANOSECONDS_PER_MINUTE * MINUTES_PER_HOUR;
58const NANOSECONDS_PER_MINUTE: i64 = NANOSECONDS_PER_SECOND * SECONDS_PER_MINUTE;
59const NANOSECONDS_PER_SECOND: i64 = 10i64.pow(9);
60
61pub const MAX_PRECISION: u8 = 6;
62
63/// The `max_precision` of a [`SqlScalarType::Timestamp`] or
64/// [`SqlScalarType::TimestampTz`].
65///
66/// This newtype wrapper ensures that the length is within the valid range.
67///
68/// [`SqlScalarType::Timestamp`]: crate::SqlScalarType::Timestamp
69/// [`SqlScalarType::TimestampTz`]: crate::SqlScalarType::TimestampTz
70#[derive(
71    Debug,
72    Clone,
73    Copy,
74    Eq,
75    PartialEq,
76    Ord,
77    PartialOrd,
78    Hash,
79    Serialize,
80    Deserialize
81)]
82#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
83pub struct TimestampPrecision(pub(crate) u8);
84
85impl TimestampPrecision {
86    /// Consumes the newtype wrapper, returning the inner `u8`.
87    pub fn into_u8(self) -> u8 {
88        self.0
89    }
90}
91
92impl TryFrom<i64> for TimestampPrecision {
93    type Error = InvalidTimestampPrecisionError;
94
95    fn try_from(max_precision: i64) -> Result<Self, Self::Error> {
96        match u8::try_from(max_precision) {
97            Ok(max_precision) if max_precision <= MAX_PRECISION => {
98                Ok(TimestampPrecision(max_precision))
99            }
100            _ => Err(InvalidTimestampPrecisionError),
101        }
102    }
103}
104
105impl RustType<ProtoTimestampPrecision> for TimestampPrecision {
106    fn into_proto(&self) -> ProtoTimestampPrecision {
107        ProtoTimestampPrecision {
108            value: self.0.into_proto(),
109        }
110    }
111
112    fn from_proto(proto: ProtoTimestampPrecision) -> Result<Self, TryFromProtoError> {
113        Ok(TimestampPrecision(proto.value.into_rust()?))
114    }
115}
116
117/// The error returned when constructing a [`VarCharMaxLength`] from an invalid
118/// value.
119///
120/// [`VarCharMaxLength`]: crate::adt::varchar::VarCharMaxLength
121#[derive(Debug, Clone)]
122pub struct InvalidTimestampPrecisionError;
123
124impl fmt::Display for InvalidTimestampPrecisionError {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        write!(
127            f,
128            "precision for type timestamp or timestamptz must be between 0 and {}",
129            MAX_PRECISION
130        )
131    }
132}
133
134impl Error for InvalidTimestampPrecisionError {}
135
136/// Common set of methods for time component.
137pub trait TimeLike: chrono::Timelike {
138    fn extract_epoch<T>(&self) -> T
139    where
140        T: DecimalLike,
141    {
142        T::from(self.hour() * 60 * 60 + self.minute() * 60) + self.extract_second::<T>()
143    }
144
145    fn extract_second<T>(&self) -> T
146    where
147        T: DecimalLike,
148    {
149        let s = T::from(self.second());
150        let ns = T::from(self.nanosecond()) / T::from(1e9);
151        s + ns
152    }
153
154    fn extract_millisecond<T>(&self) -> T
155    where
156        T: DecimalLike,
157    {
158        let s = T::from(self.second() * 1_000);
159        let ns = T::from(self.nanosecond()) / T::from(1e6);
160        s + ns
161    }
162
163    fn extract_microsecond<T>(&self) -> T
164    where
165        T: DecimalLike,
166    {
167        let s = T::from(self.second() * 1_000_000);
168        let ns = T::from(self.nanosecond()) / T::from(1e3);
169        s + ns
170    }
171}
172
173impl<T> TimeLike for T where T: chrono::Timelike {}
174
175/// Common set of methods for date component.
176pub trait DateLike: chrono::Datelike {
177    fn extract_epoch(&self) -> i64 {
178        let naive_date = NaiveDate::from_ymd_opt(self.year(), self.month(), self.day())
179            .unwrap()
180            .and_hms_opt(0, 0, 0)
181            .unwrap();
182        naive_date.and_utc().timestamp()
183    }
184
185    /// The year in SQL's numbering, where 1 BC is year -1 and there is no
186    /// year 0. Chrono's `year` uses astronomical numbering, where 1 BC is
187    /// year 0.
188    fn extract_year(&self) -> i32 {
189        let year = self.year();
190        if year <= 0 { year - 1 } else { year }
191    }
192
193    fn millennium(&self) -> i32 {
194        (self.year() + if self.year() > 0 { 999 } else { -1_000 }) / 1_000
195    }
196
197    fn century(&self) -> i32 {
198        (self.year() + if self.year() > 0 { 99 } else { -100 }) / 100
199    }
200
201    fn decade(&self) -> i32 {
202        self.year().div_euclid(10)
203    }
204
205    /// Extract the iso week of the year
206    ///
207    /// Note that because isoweeks are defined in terms of January 4th, Jan 1 is only in week
208    /// 1 about half of the time
209    fn iso_week_number(&self) -> u32 {
210        self.iso_week().week()
211    }
212
213    fn day_of_week(&self) -> u32 {
214        self.weekday().num_days_from_sunday()
215    }
216
217    fn iso_day_of_week(&self) -> u32 {
218        self.weekday().number_from_monday()
219    }
220}
221
222impl<T> DateLike for T where T: chrono::Datelike {}
223
224/// A timestamp with both a date and a time component, but not necessarily a
225/// timezone component.
226pub trait TimestampLike:
227    Clone
228    + PartialOrd
229    + std::ops::Add<Duration, Output = Self>
230    + std::ops::Sub<Duration, Output = Self>
231    + std::ops::Sub<Output = Duration>
232    + for<'a> TryInto<Datum<'a>, Error = TimestampError>
233    + for<'a> TryFrom<Datum<'a>, Error = ()>
234    + TimeLike
235    + DateLike
236{
237    fn new(date: NaiveDate, time: NaiveTime) -> Self;
238
239    /// Returns the weekday as a `usize` between 0 and 6, where 0 represents
240    /// Sunday and 6 represents Saturday.
241    fn weekday0(&self) -> usize {
242        usize::cast_from(self.weekday().num_days_from_sunday())
243    }
244
245    /// Like [`chrono::Datelike::year_ce`], but works on the ISO week system.
246    fn iso_year_ce(&self) -> u32 {
247        let year = self.iso_week().year();
248        if year < 1 {
249            u32::try_from(1 - year).expect("known to be positive")
250        } else {
251            u32::try_from(year).expect("known to be positive")
252        }
253    }
254
255    fn timestamp(&self) -> i64;
256
257    fn timestamp_subsec_micros(&self) -> u32;
258
259    fn extract_epoch<T>(&self) -> T
260    where
261        T: DecimalLike,
262    {
263        T::lossy_from(self.timestamp()) + T::from(self.timestamp_subsec_micros()) / T::from(1e6)
264    }
265
266    fn truncate_microseconds(&self) -> Self {
267        // Use `with_nanosecond` rather than `from_hms_micro_opt`: the latter only
268        // accepts a leap-second sub-second (>= 1s) when `sec == 59`, so a value
269        // carrying chrono's leap representation at any other second (reachable
270        // from a parsed `:60` literal) would be `None` and panic. `with_nanosecond`
271        // accepts the whole [0, 2s) range, preserving the value.
272        let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second())
273            .and_then(|t| t.with_nanosecond((self.nanosecond() / 1_000) * 1_000))
274            .expect("hour/minute/second/nanosecond came from a valid time");
275
276        Self::new(self.date(), time)
277    }
278
279    fn truncate_milliseconds(&self) -> Self {
280        let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second())
281            .and_then(|t| t.with_nanosecond((self.nanosecond() / 1_000_000) * 1_000_000))
282            .expect("hour/minute/second/nanosecond came from a valid time");
283
284        Self::new(self.date(), time)
285    }
286
287    fn truncate_second(&self) -> Self {
288        let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second()).unwrap();
289
290        Self::new(self.date(), time)
291    }
292
293    fn truncate_minute(&self) -> Self {
294        Self::new(
295            self.date(),
296            NaiveTime::from_hms_opt(self.hour(), self.minute(), 0).unwrap(),
297        )
298    }
299
300    fn truncate_hour(&self) -> Self {
301        Self::new(
302            self.date(),
303            NaiveTime::from_hms_opt(self.hour(), 0, 0).unwrap(),
304        )
305    }
306
307    fn truncate_day(&self) -> Self {
308        Self::new(self.date(), NaiveTime::from_hms_opt(0, 0, 0).unwrap())
309    }
310
311    fn truncate_week(&self) -> Result<Self, TimestampError> {
312        let num_days_from_monday = i64::from(self.date().weekday().num_days_from_monday());
313        let new_date = NaiveDate::from_ymd_opt(self.year(), self.month(), self.day())
314            .unwrap()
315            .checked_sub_signed(
316                Duration::try_days(num_days_from_monday).ok_or(TimestampError::OutOfRange)?,
317            )
318            .ok_or(TimestampError::OutOfRange)?;
319        Ok(Self::new(
320            new_date,
321            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
322        ))
323    }
324
325    fn truncate_month(&self) -> Self {
326        Self::new(
327            NaiveDate::from_ymd_opt(self.year(), self.month(), 1).unwrap(),
328            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
329        )
330    }
331
332    fn truncate_quarter(&self) -> Self {
333        let month = self.month();
334        let quarter = if month <= 3 {
335            1
336        } else if month <= 6 {
337            4
338        } else if month <= 9 {
339            7
340        } else {
341            10
342        };
343
344        Self::new(
345            NaiveDate::from_ymd_opt(self.year(), quarter, 1).unwrap(),
346            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
347        )
348    }
349
350    fn truncate_year(&self) -> Self {
351        Self::new(
352            NaiveDate::from_ymd_opt(self.year(), 1, 1).unwrap(),
353            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
354        )
355    }
356    fn truncate_decade(&self) -> Self {
357        Self::new(
358            NaiveDate::from_ymd_opt(self.year() - self.year().rem_euclid(10), 1, 1).unwrap(),
359            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
360        )
361    }
362    fn truncate_century(&self) -> Self {
363        // Expects the first year of the century, meaning 2001 instead of 2000.
364        Self::new(
365            NaiveDate::from_ymd_opt(
366                if self.year() > 0 {
367                    self.year() - (self.year() - 1) % 100
368                } else {
369                    self.year() - self.year() % 100 - 99
370                },
371                1,
372                1,
373            )
374            .unwrap(),
375            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
376        )
377    }
378    fn truncate_millennium(&self) -> Self {
379        // Expects the first year of the millennium, meaning 2001 instead of 2000.
380        Self::new(
381            NaiveDate::from_ymd_opt(
382                if self.year() > 0 {
383                    self.year() - (self.year() - 1) % 1000
384                } else {
385                    self.year() - self.year() % 1000 - 999
386                },
387                1,
388                1,
389            )
390            .unwrap(),
391            NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
392        )
393    }
394
395    /// Return the date component of the timestamp
396    fn date(&self) -> NaiveDate;
397
398    /// Return the date and time of the timestamp
399    fn date_time(&self) -> NaiveDateTime;
400
401    /// Return the date and time of the timestamp
402    fn from_date_time(dt: NaiveDateTime) -> Self;
403
404    /// Returns a string representing the timezone's offset from UTC.
405    fn timezone_offset(&self) -> &'static str;
406
407    /// Returns a string representing the hour portion of the timezone's offset
408    /// from UTC.
409    fn timezone_hours(&self) -> &'static str;
410
411    /// Returns a string representing the minute portion of the timezone's
412    /// offset from UTC.
413    fn timezone_minutes(&self) -> &'static str;
414
415    /// Returns the abbreviated name of the timezone with the specified
416    /// capitalization.
417    fn timezone_name(&self, caps: bool) -> &'static str;
418
419    /// Adds given Duration to the current date and time.
420    fn checked_add_signed(self, rhs: Duration) -> Option<Self>;
421
422    /// Subtracts given Duration from the current date and time.
423    fn checked_sub_signed(self, rhs: Duration) -> Option<Self>;
424}
425
426impl TryFrom<Datum<'_>> for NaiveDateTime {
427    type Error = ();
428
429    #[inline]
430    fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
431        match from {
432            Datum::Timestamp(dt) => Ok(dt.t),
433            _ => Err(()),
434        }
435    }
436}
437
438impl TryFrom<Datum<'_>> for DateTime<Utc> {
439    type Error = ();
440
441    #[inline]
442    fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
443        match from {
444            Datum::TimestampTz(dt_tz) => Ok(dt_tz.t),
445            _ => Err(()),
446        }
447    }
448}
449
450impl TimestampLike for chrono::NaiveDateTime {
451    fn new(date: NaiveDate, time: NaiveTime) -> Self {
452        NaiveDateTime::new(date, time)
453    }
454
455    fn date(&self) -> NaiveDate {
456        self.date()
457    }
458
459    fn date_time(&self) -> NaiveDateTime {
460        self.clone()
461    }
462
463    fn from_date_time(dt: NaiveDateTime) -> NaiveDateTime {
464        dt
465    }
466
467    fn timestamp(&self) -> i64 {
468        self.and_utc().timestamp()
469    }
470
471    fn timestamp_subsec_micros(&self) -> u32 {
472        self.and_utc().timestamp_subsec_micros()
473    }
474
475    fn timezone_offset(&self) -> &'static str {
476        "+00"
477    }
478
479    fn timezone_hours(&self) -> &'static str {
480        "+00"
481    }
482
483    fn timezone_minutes(&self) -> &'static str {
484        "00"
485    }
486
487    fn timezone_name(&self, _caps: bool) -> &'static str {
488        ""
489    }
490
491    fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
492        self.checked_add_signed(rhs)
493    }
494
495    fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
496        self.checked_sub_signed(rhs)
497    }
498}
499
500impl TimestampLike for chrono::DateTime<chrono::Utc> {
501    fn new(date: NaiveDate, time: NaiveTime) -> Self {
502        Self::from_date_time(NaiveDateTime::new(date, time))
503    }
504
505    fn date(&self) -> NaiveDate {
506        self.naive_utc().date()
507    }
508
509    fn date_time(&self) -> NaiveDateTime {
510        self.naive_utc()
511    }
512
513    fn from_date_time(dt: NaiveDateTime) -> Self {
514        DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)
515    }
516
517    fn timestamp(&self) -> i64 {
518        self.timestamp()
519    }
520
521    fn timestamp_subsec_micros(&self) -> u32 {
522        self.timestamp_subsec_micros()
523    }
524
525    fn timezone_offset(&self) -> &'static str {
526        "+00"
527    }
528
529    fn timezone_hours(&self) -> &'static str {
530        "+00"
531    }
532
533    fn timezone_minutes(&self) -> &'static str {
534        "00"
535    }
536
537    fn timezone_name(&self, caps: bool) -> &'static str {
538        if caps { "UTC" } else { "utc" }
539    }
540
541    fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
542        self.checked_add_signed(rhs)
543    }
544
545    fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
546        self.checked_sub_signed(rhs)
547    }
548}
549
550#[derive(Debug, Error)]
551pub enum TimestampError {
552    #[error("timestamp out of range")]
553    OutOfRange,
554}
555
556#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
557pub struct CheckedTimestamp<T> {
558    t: T,
559}
560
561impl<T: Serialize> Serialize for CheckedTimestamp<T> {
562    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
563    where
564        S: Serializer,
565    {
566        self.t.serialize(serializer)
567    }
568}
569
570// We support intersection of the limits of Postgres, Avro, and chrono dates:
571// the set of dates that are representable in all used formats.
572//
573// - Postgres supports 4713 BC to 294276 AD (any time on those days inclusive).
574// - Avro supports i64 milliseconds since the Unix epoch: -292275055-05-16
575// 16:47:04.192 to 292278994-08-17 07:12:55.807.
576// - Avro also supports i64 microseconds since the Unix epoch: -290308-12-21
577//   19:59:05.224192 to 294247-01-10 04:00:54.775807.
578// - chrono's NaiveDate supports January 1, 262144 BCE to December 31, 262142
579//   CE.
580//
581// Thus on the low end we have 4713-12-31 BC from Postgres, and on the high end
582// 262142-12-31 from chrono.
583
584pub static LOW_DATE: LazyLock<NaiveDate> =
585    LazyLock::new(|| NaiveDate::from_ymd_opt(-4713, 12, 31).unwrap());
586pub static HIGH_DATE: LazyLock<NaiveDate> =
587    LazyLock::new(|| NaiveDate::from_ymd_opt(262142, 12, 31).unwrap());
588
589impl<T: TimestampLike> CheckedTimestamp<T> {
590    pub fn from_timestamplike(t: T) -> Result<Self, TimestampError> {
591        let d = t.date();
592        if d < *LOW_DATE {
593            return Err(TimestampError::OutOfRange);
594        }
595        if d > *HIGH_DATE {
596            return Err(TimestampError::OutOfRange);
597        }
598        Ok(Self { t })
599    }
600
601    pub fn checked_add_signed(self, rhs: Duration) -> Option<T> {
602        self.t.checked_add_signed(rhs)
603    }
604
605    pub fn checked_sub_signed(self, rhs: Duration) -> Option<T> {
606        self.t.checked_sub_signed(rhs)
607    }
608
609    /// Returns the difference between `self` and the provided [`CheckedTimestamp`] as a number of
610    /// "unit"s.
611    ///
612    /// Note: used for `DATEDIFF(...)`, which isn't a Postgres function, but is in a number of
613    /// other databases.
614    pub fn diff_as(&self, other: &Self, unit: DateTimePart) -> Result<i64, TimestampError> {
615        const QUARTERS_PER_YEAR: i64 = 4;
616        const DAYS_PER_WEEK: i64 = 7;
617
618        fn diff_inner<U>(
619            a: &CheckedTimestamp<U>,
620            b: &CheckedTimestamp<U>,
621            unit: DateTimePart,
622        ) -> Option<i64>
623        where
624            U: TimestampLike,
625        {
626            match unit {
627                DateTimePart::Millennium => {
628                    i64::cast_from(a.millennium()).checked_sub(i64::cast_from(b.millennium()))
629                }
630                DateTimePart::Century => {
631                    i64::cast_from(a.century()).checked_sub(i64::cast_from(b.century()))
632                }
633                DateTimePart::Decade => {
634                    i64::cast_from(a.decade()).checked_sub(i64::cast_from(b.decade()))
635                }
636                DateTimePart::Year => {
637                    i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))
638                }
639                DateTimePart::Quarter => {
640                    let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
641                    let quarters = years.checked_mul(QUARTERS_PER_YEAR)?;
642                    let diff = i64::cast_from(a.quarter()) - i64::cast_from(b.quarter());
643                    quarters.checked_add(diff)
644                }
645                DateTimePart::Month => {
646                    let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
647                    let months = years.checked_mul(MONTHS_PER_YEAR)?;
648                    let diff = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
649                    months.checked_add(diff)
650                }
651                DateTimePart::Week => {
652                    let diff = a.clone() - b.clone();
653                    diff.num_days().checked_div(DAYS_PER_WEEK)
654                }
655                DateTimePart::Day => {
656                    let diff = a.clone() - b.clone();
657                    Some(diff.num_days())
658                }
659                DateTimePart::Hour => {
660                    let diff = a.clone() - b.clone();
661                    Some(diff.num_hours())
662                }
663                DateTimePart::Minute => {
664                    let diff = a.clone() - b.clone();
665                    Some(diff.num_minutes())
666                }
667                DateTimePart::Second => {
668                    let diff = a.clone() - b.clone();
669                    Some(diff.num_seconds())
670                }
671                DateTimePart::Milliseconds => {
672                    let diff = a.clone() - b.clone();
673                    Some(diff.num_milliseconds())
674                }
675                DateTimePart::Microseconds => {
676                    let diff = a.clone() - b.clone();
677                    diff.num_microseconds()
678                }
679            }
680        }
681
682        diff_inner(self, other, unit).ok_or(TimestampError::OutOfRange)
683    }
684
685    /// Implementation was roughly ported from Postgres's `timestamp.c`.
686    ///
687    /// <https://github.com/postgres/postgres/blob/REL_15_3/src/backend/utils/adt/timestamp.c#L3631>
688    pub fn age(&self, other: &Self) -> Result<Interval, TimestampError> {
689        /// Returns the number of days in the month for which the [`CheckedTimestamp`] is in.
690        fn num_days_in_month<T: TimestampLike>(dt: &CheckedTimestamp<T>) -> Option<i64> {
691            // Creates a new Date in the same month and year as our original timestamp. Adds one
692            // month then subtracts one day, to get the last day of our original month.
693            let last_day = NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)?
694                .checked_add_months(Months::new(1))?
695                .checked_sub_days(Days::new(1))?
696                .day();
697
698            Some(CastFrom::cast_from(last_day))
699        }
700
701        /// All of the `checked_*` functions return `Option<T>`, so we do all of the math in this
702        /// inner function so we can use the `?` operator, maping to a `TimestampError` at the end.
703        fn age_inner<U: TimestampLike>(
704            a: &CheckedTimestamp<U>,
705            b: &CheckedTimestamp<U>,
706        ) -> Option<Interval> {
707            let mut nanos =
708                i64::cast_from(a.nanosecond()).checked_sub(i64::cast_from(b.nanosecond()))?;
709            let mut seconds = i64::cast_from(a.second()).checked_sub(i64::cast_from(b.second()))?;
710            let mut minutes = i64::cast_from(a.minute()).checked_sub(i64::cast_from(b.minute()))?;
711            let mut hours = i64::cast_from(a.hour()).checked_sub(i64::cast_from(b.hour()))?;
712            let mut days = i64::cast_from(a.day()).checked_sub(i64::cast_from(b.day()))?;
713            let mut months = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
714            let mut years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
715
716            // Flip sign if necessary.
717            if a < b {
718                nanos = nanos.checked_neg()?;
719                seconds = seconds.checked_neg()?;
720                minutes = minutes.checked_neg()?;
721                hours = hours.checked_neg()?;
722                days = days.checked_neg()?;
723                months = months.checked_neg()?;
724                years = years.checked_neg()?;
725            }
726
727            // Carry negative fields into the next higher field.
728            while nanos < 0 {
729                nanos = nanos.checked_add(NANOSECONDS_PER_SECOND)?;
730                seconds = seconds.checked_sub(1)?;
731            }
732            while seconds < 0 {
733                seconds = seconds.checked_add(SECONDS_PER_MINUTE)?;
734                minutes = minutes.checked_sub(1)?;
735            }
736            while minutes < 0 {
737                minutes = minutes.checked_add(MINUTES_PER_HOUR)?;
738                hours = hours.checked_sub(1)?;
739            }
740            while hours < 0 {
741                hours = hours.checked_add(HOURS_PER_DAY)?;
742                days = days.checked_sub(1)?
743            }
744            while days < 0 {
745                if a < b {
746                    days = num_days_in_month(a).and_then(|x| days.checked_add(x))?;
747                } else {
748                    days = num_days_in_month(b).and_then(|x| days.checked_add(x))?;
749                }
750                months = months.checked_sub(1)?;
751            }
752            while months < 0 {
753                months = months.checked_add(MONTHS_PER_YEAR)?;
754                years = years.checked_sub(1)?;
755            }
756
757            // Revert the sign back, if we flipped it originally.
758            if a < b {
759                nanos = nanos.checked_neg()?;
760                seconds = seconds.checked_neg()?;
761                minutes = minutes.checked_neg()?;
762                hours = hours.checked_neg()?;
763                days = days.checked_neg()?;
764                months = months.checked_neg()?;
765                years = years.checked_neg()?;
766            }
767
768            let months = i32::try_from(years * MONTHS_PER_YEAR + months).ok()?;
769            let days = i32::try_from(days).ok()?;
770            let micros = Duration::nanoseconds(
771                nanos
772                    .checked_add(seconds.checked_mul(NANOSECONDS_PER_SECOND)?)?
773                    .checked_add(minutes.checked_mul(NANOSECONDS_PER_MINUTE)?)?
774                    .checked_add(hours.checked_mul(NANOSECONDS_PER_HOUR)?)?,
775            )
776            .num_microseconds()?;
777
778            Some(Interval {
779                months,
780                days,
781                micros,
782            })
783        }
784
785        // If at any point we overflow, map to a TimestampError.
786        age_inner(self, other).ok_or(TimestampError::OutOfRange)
787    }
788
789    /// Rounds the timestamp to the specified number of digits of precision.
790    pub fn round_to_precision(
791        &self,
792        precision: Option<TimestampPrecision>,
793    ) -> Result<CheckedTimestamp<T>, TimestampError> {
794        let precision = precision.map(|p| p.into_u8()).unwrap_or(MAX_PRECISION);
795        // maximum precision is micros
796        let power = MAX_PRECISION
797            .checked_sub(precision)
798            .expect("precision fits in micros");
799        let round_to_micros = 10_i64.pow(power.into());
800
801        let mut original = self.date_time();
802        let nanoseconds = original.and_utc().timestamp_subsec_nanos();
803        // truncating to microseconds does not round it up
804        // i.e. 123456789 will be truncated to 123456
805        original = original.truncate_microseconds();
806        // depending upon the 7th digit here, we'll be
807        // adding 1 millisecond
808        // so eventually 123456789 will be rounded to 123457
809        let seventh_digit = (nanoseconds % 1_000) / 100;
810        assert!(seventh_digit < 10);
811        if seventh_digit >= 5 {
812            original = original + Duration::microseconds(1);
813        }
814        // this is copied from [`chrono::round::duration_round`]
815        // but using microseconds instead of nanoseconds precision
816        let stamp = original.and_utc().timestamp_micros();
817        let dt = {
818            let delta_down = stamp % round_to_micros;
819            if delta_down == 0 {
820                original
821            } else {
822                let (delta_up, delta_down) = if delta_down < 0 {
823                    (delta_down.abs(), round_to_micros - delta_down.abs())
824                } else {
825                    (round_to_micros - delta_down, delta_down)
826                };
827                if delta_up <= delta_down {
828                    original + Duration::microseconds(delta_up)
829                } else {
830                    original - Duration::microseconds(delta_down)
831                }
832            }
833        };
834
835        let t = T::from_date_time(dt);
836        Self::from_timestamplike(t)
837    }
838}
839
840impl TryFrom<NaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
841    type Error = TimestampError;
842
843    fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
844        Self::from_timestamplike(value)
845    }
846}
847
848impl TryFrom<DateTime<Utc>> for CheckedTimestamp<DateTime<Utc>> {
849    type Error = TimestampError;
850
851    fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
852        Self::from_timestamplike(value)
853    }
854}
855
856impl<T: TimestampLike> std::ops::Deref for CheckedTimestamp<T> {
857    type Target = T;
858
859    #[inline]
860    fn deref(&self) -> &T {
861        &self.t
862    }
863}
864
865impl From<CheckedTimestamp<NaiveDateTime>> for NaiveDateTime {
866    fn from(val: CheckedTimestamp<NaiveDateTime>) -> Self {
867        val.t
868    }
869}
870
871impl From<CheckedTimestamp<DateTime<Utc>>> for DateTime<Utc> {
872    fn from(val: CheckedTimestamp<DateTime<Utc>>) -> Self {
873        val.t
874    }
875}
876
877impl CheckedTimestamp<NaiveDateTime> {
878    pub fn to_naive(&self) -> NaiveDateTime {
879        self.t
880    }
881}
882
883impl CheckedTimestamp<DateTime<Utc>> {
884    pub fn to_naive(&self) -> NaiveDateTime {
885        self.t.date_naive().and_time(self.t.time())
886    }
887}
888
889impl Display for CheckedTimestamp<NaiveDateTime> {
890    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
891        self.t.fmt(f)
892    }
893}
894
895impl Display for CheckedTimestamp<DateTime<Utc>> {
896    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897        self.t.fmt(f)
898    }
899}
900
901impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
902    fn into_proto(&self) -> ProtoNaiveDateTime {
903        self.t.into_proto()
904    }
905
906    fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
907        // Go through `from_timestamplike` so out-of-range values are
908        // rejected here. Pushing them into a `Row` succeeds, but
909        // `read_datum` would panic when reconstructing the timestamp.
910        CheckedTimestamp::from_timestamplike(NaiveDateTime::from_proto(proto)?)
911            .map_err(|err| TryFromProtoError::InvalidFieldError(err.to_string()))
912    }
913}
914
915impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<DateTime<Utc>> {
916    fn into_proto(&self) -> ProtoNaiveDateTime {
917        self.t.into_proto()
918    }
919
920    fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
921        CheckedTimestamp::from_timestamplike(DateTime::<Utc>::from_proto(proto)?)
922            .map_err(|err| TryFromProtoError::InvalidFieldError(err.to_string()))
923    }
924}
925
926impl<T: Sub<Output = Duration>> Sub<CheckedTimestamp<T>> for CheckedTimestamp<T> {
927    type Output = Duration;
928
929    #[inline]
930    fn sub(self, rhs: CheckedTimestamp<T>) -> Duration {
931        self.t - rhs.t
932    }
933}
934
935impl<T: Sub<Duration, Output = T>> Sub<Duration> for CheckedTimestamp<T> {
936    type Output = T;
937
938    #[inline]
939    fn sub(self, rhs: Duration) -> T {
940        self.t - rhs
941    }
942}
943
944#[cfg(any(test, feature = "proptest"))]
945impl Arbitrary for CheckedTimestamp<NaiveDateTime> {
946    type Parameters = ();
947    type Strategy = BoxedStrategy<CheckedTimestamp<NaiveDateTime>>;
948
949    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
950        arb_naive_date_time()
951            .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
952            .boxed()
953    }
954}
955
956#[cfg(any(test, feature = "proptest"))]
957impl Arbitrary for CheckedTimestamp<DateTime<Utc>> {
958    type Parameters = ();
959    type Strategy = BoxedStrategy<CheckedTimestamp<DateTime<Utc>>>;
960
961    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
962        arb_utc_date_time()
963            .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
964            .boxed()
965    }
966}
967
968/// An encoded packed variant of [`NaiveDateTime`].
969///
970/// We uphold the invariant that [`PackedNaiveDateTime`] sorts the same as
971/// [`NaiveDateTime`].
972#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
973pub struct PackedNaiveDateTime([u8; Self::SIZE]);
974
975// `as` conversions are okay here because we're doing bit level logic to make
976// sure the sort order of the packed binary is correct. This is implementation
977// is proptest-ed below.
978#[allow(clippy::as_conversions)]
979impl FixedSizeCodec<NaiveDateTime> for PackedNaiveDateTime {
980    const SIZE: usize = 16;
981
982    fn as_bytes(&self) -> &[u8] {
983        &self.0
984    }
985
986    fn from_bytes(slice: &[u8]) -> Result<Self, String> {
987        let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
988            format!(
989                "size for PackedNaiveDateTime is {} bytes, got {}",
990                Self::SIZE,
991                slice.len()
992            )
993        })?;
994        Ok(PackedNaiveDateTime(buf))
995    }
996
997    #[inline]
998    fn from_value(value: NaiveDateTime) -> Self {
999        let mut buf = [0u8; 16];
1000
1001        // Note: We XOR the values to get correct sorting of negative values.
1002
1003        let year = (value.year() as u32) ^ (0x8000_0000u32);
1004        let ordinal = value.ordinal();
1005        let secs = value.num_seconds_from_midnight();
1006        let nano = value.nanosecond();
1007
1008        buf[..4].copy_from_slice(&year.to_be_bytes());
1009        buf[4..8].copy_from_slice(&ordinal.to_be_bytes());
1010        buf[8..12].copy_from_slice(&secs.to_be_bytes());
1011        buf[12..].copy_from_slice(&nano.to_be_bytes());
1012
1013        PackedNaiveDateTime(buf)
1014    }
1015
1016    #[inline]
1017    fn into_value(self) -> NaiveDateTime {
1018        let mut year = [0u8; 4];
1019        year.copy_from_slice(&self.0[..4]);
1020        let year = u32::from_be_bytes(year) ^ 0x8000_0000u32;
1021
1022        let mut ordinal = [0u8; 4];
1023        ordinal.copy_from_slice(&self.0[4..8]);
1024        let ordinal = u32::from_be_bytes(ordinal);
1025
1026        let mut secs = [0u8; 4];
1027        secs.copy_from_slice(&self.0[8..12]);
1028        let secs = u32::from_be_bytes(secs);
1029
1030        let mut nano = [0u8; 4];
1031        nano.copy_from_slice(&self.0[12..]);
1032        let nano = u32::from_be_bytes(nano);
1033
1034        let date = NaiveDate::from_yo_opt(year as i32, ordinal)
1035            .expect("NaiveDate roundtrips with PackedNaiveDateTime");
1036        let time = NaiveTime::from_num_seconds_from_midnight_opt(secs, nano)
1037            .expect("NaiveTime roundtrips with PackedNaiveDateTime");
1038
1039        NaiveDateTime::new(date, time)
1040    }
1041}
1042
1043#[cfg(test)]
1044mod test {
1045    use super::*;
1046    use itertools::Itertools;
1047    use mz_ore::assert_err;
1048    use proptest::prelude::*;
1049
1050    #[mz_ore::test]
1051    fn test_max_age() {
1052        let low = CheckedTimestamp::try_from(
1053            LOW_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1054        )
1055        .unwrap();
1056        let high = CheckedTimestamp::try_from(
1057            HIGH_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1058        )
1059        .unwrap();
1060
1061        let years = HIGH_DATE.year() - LOW_DATE.year();
1062        let months = years * 12;
1063
1064        // Test high - low.
1065        let result = high.age(&low).unwrap();
1066        assert_eq!(result, Interval::new(months, 0, 0));
1067
1068        // Test low - high.
1069        let result = low.age(&high).unwrap();
1070        assert_eq!(result, Interval::new(-months, 0, 0));
1071    }
1072
1073    fn assert_round_to_precision(
1074        dt: CheckedTimestamp<NaiveDateTime>,
1075        precision: u8,
1076        expected: i64,
1077    ) {
1078        let updated = dt
1079            .round_to_precision(Some(TimestampPrecision(precision)))
1080            .unwrap();
1081        assert_eq!(expected, updated.and_utc().timestamp_micros());
1082    }
1083
1084    #[mz_ore::test]
1085    fn test_round_to_precision() {
1086        let date = CheckedTimestamp::try_from(
1087            NaiveDate::from_ymd_opt(1970, 1, 1)
1088                .unwrap()
1089                .and_hms_nano_opt(0, 0, 0, 123456789)
1090                .unwrap(),
1091        )
1092        .unwrap();
1093        assert_round_to_precision(date, 0, 0);
1094        assert_round_to_precision(date, 1, 100000);
1095        assert_round_to_precision(date, 2, 120000);
1096        assert_round_to_precision(date, 3, 123000);
1097        assert_round_to_precision(date, 4, 123500);
1098        assert_round_to_precision(date, 5, 123460);
1099        assert_round_to_precision(date, 6, 123457);
1100
1101        let low =
1102            CheckedTimestamp::try_from(LOW_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1103                .unwrap();
1104        assert_round_to_precision(low, 0, -210863606400000000);
1105        assert_round_to_precision(low, 1, -210863606399900000);
1106        assert_round_to_precision(low, 2, -210863606399880000);
1107        assert_round_to_precision(low, 3, -210863606399877000);
1108        assert_round_to_precision(low, 4, -210863606399876500);
1109        assert_round_to_precision(low, 5, -210863606399876540);
1110        assert_round_to_precision(low, 6, -210863606399876543);
1111
1112        let high =
1113            CheckedTimestamp::try_from(HIGH_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1114                .unwrap();
1115        assert_round_to_precision(high, 0, 8210266790400000000);
1116        assert_round_to_precision(high, 1, 8210266790400100000);
1117        assert_round_to_precision(high, 2, 8210266790400120000);
1118        assert_round_to_precision(high, 3, 8210266790400123000);
1119        assert_round_to_precision(high, 4, 8210266790400123500);
1120        assert_round_to_precision(high, 5, 8210266790400123460);
1121        assert_round_to_precision(high, 6, 8210266790400123457);
1122    }
1123
1124    #[mz_ore::test]
1125    fn test_round_to_precision_leap_second_off_minute() {
1126        // Regression: parsing a `:60` literal can leave chrono's leap-second
1127        // representation (sub-second >= 1s) on a second other than `:59` (after
1128        // time-zone math). Rounding such a value, as the string->timestamp cast
1129        // does, must not panic. `truncate_microseconds`/`truncate_milliseconds`
1130        // previously rebuilt the time with `from_hms_{micro,milli}_opt`, whose
1131        // leap sub-second range is only valid at `:59`, and unwrapped the `None`.
1132        let leap = NaiveDate::from_ymd_opt(3, 3, 17)
1133            .unwrap()
1134            .and_hms_opt(12, 30, 56)
1135            .unwrap()
1136            .with_nanosecond(1_000_000_000)
1137            .unwrap();
1138        let ts = CheckedTimestamp::try_from(leap).unwrap();
1139        for precision in [None, Some(0), Some(3), Some(6)] {
1140            ts.round_to_precision(precision.map(TimestampPrecision))
1141                .unwrap();
1142        }
1143    }
1144
1145    #[mz_ore::test]
1146    fn test_precision_edge_cases() {
1147        #[allow(clippy::disallowed_methods)] // not using enhanced panic handler in tests
1148        let result = std::panic::catch_unwind(|| {
1149            let date = CheckedTimestamp::try_from(
1150                DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1151            )
1152            .unwrap();
1153            let _ = date.round_to_precision(Some(TimestampPrecision(7)));
1154        });
1155        assert_err!(result);
1156
1157        let date = CheckedTimestamp::try_from(
1158            DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1159        )
1160        .unwrap();
1161        let date = date.round_to_precision(None).unwrap();
1162        assert_eq!(123456, date.and_utc().timestamp_micros());
1163    }
1164
1165    #[mz_ore::test]
1166    fn test_equality_with_same_precision() {
1167        let date1 =
1168            CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456).unwrap()).unwrap();
1169        let date1 = date1
1170            .round_to_precision(Some(TimestampPrecision(0)))
1171            .unwrap();
1172
1173        let date2 =
1174            CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1175        let date2 = date2
1176            .round_to_precision(Some(TimestampPrecision(0)))
1177            .unwrap();
1178        assert_eq!(date1, date2);
1179    }
1180
1181    #[mz_ore::test]
1182    fn test_equality_with_different_precisions() {
1183        let date1 =
1184            CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123500000).unwrap()).unwrap();
1185        let date1 = date1
1186            .round_to_precision(Some(TimestampPrecision(5)))
1187            .unwrap();
1188
1189        let date2 =
1190            CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1191        let date2 = date2
1192            .round_to_precision(Some(TimestampPrecision(4)))
1193            .unwrap();
1194        assert_eq!(date1, date2);
1195    }
1196
1197    proptest! {
1198        #[mz_ore::test]
1199        #[cfg_attr(miri, ignore)] // slow
1200        fn test_age_naive(a: CheckedTimestamp<NaiveDateTime>, b: CheckedTimestamp<NaiveDateTime>) {
1201            let result = a.age(&b);
1202            prop_assert!(result.is_ok());
1203        }
1204
1205        #[mz_ore::test]
1206        #[cfg_attr(miri, ignore)] // slow
1207        fn test_age_utc(a: CheckedTimestamp<DateTime<Utc>>, b: CheckedTimestamp<DateTime<Utc>>) {
1208            let result = a.age(&b);
1209            prop_assert!(result.is_ok());
1210        }
1211    }
1212
1213    #[mz_ore::test]
1214    fn proptest_packed_naive_date_time_roundtrips() {
1215        proptest!(|(timestamp in arb_naive_date_time())| {
1216            let packed = PackedNaiveDateTime::from_value(timestamp);
1217            let rnd = packed.into_value();
1218            prop_assert_eq!(timestamp, rnd);
1219        });
1220    }
1221
1222    #[mz_ore::test]
1223    fn proptest_packed_naive_date_time_sort_order() {
1224        let strat = proptest::collection::vec(arb_naive_date_time(), 0..128);
1225        proptest!(|(mut times in strat)| {
1226            let mut packed: Vec<_> = times
1227                .iter()
1228                .copied()
1229                .map(PackedNaiveDateTime::from_value)
1230                .collect();
1231
1232            times.sort();
1233            packed.sort();
1234
1235            for (time, packed) in times.into_iter().zip_eq(packed.into_iter()) {
1236                let rnd = packed.into_value();
1237                prop_assert_eq!(time, rnd);
1238            }
1239        });
1240    }
1241}