Skip to main content

mz_repr/adt/
timestamp.rs

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