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