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