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