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