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