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, FixedOffset, Months, NaiveDate, NaiveDateTime, NaiveTime,
28 Utc,
29};
30use chrono::Timelike;
31use mz_ore::cast::{self, CastFrom};
32use mz_persist_types::columnar::FixedSizeCodec;
33use mz_proto::chrono::ProtoNaiveDateTime;
34use mz_proto::{RustType, TryFromProtoError};
35#[cfg(any(test, feature = "proptest"))]
36use proptest::arbitrary::Arbitrary;
37#[cfg(any(test, feature = "proptest"))]
38use proptest::strategy::{BoxedStrategy, Strategy};
39use serde::{Deserialize, Serialize, Serializer};
40use thiserror::Error;
41
42use crate::Datum;
43use crate::adt::datetime::DateTimePart;
44use crate::adt::interval::Interval;
45use crate::adt::numeric::DecimalLike;
46#[cfg(any(test, feature = "proptest"))]
47use crate::scalar::{arb_naive_date_time, arb_utc_date_time};
48
49include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.timestamp.rs"));
50
51const MONTHS_PER_YEAR: i64 = cast::u16_to_i64(Interval::MONTH_PER_YEAR);
52const HOURS_PER_DAY: i64 = cast::u16_to_i64(Interval::HOUR_PER_DAY);
53const MINUTES_PER_HOUR: i64 = cast::u16_to_i64(Interval::MINUTE_PER_HOUR);
54const SECONDS_PER_MINUTE: i64 = cast::u16_to_i64(Interval::SECOND_PER_MINUTE);
55
56const NANOSECONDS_PER_HOUR: i64 = NANOSECONDS_PER_MINUTE * MINUTES_PER_HOUR;
57const NANOSECONDS_PER_MINUTE: i64 = NANOSECONDS_PER_SECOND * SECONDS_PER_MINUTE;
58const NANOSECONDS_PER_SECOND: i64 = 10i64.pow(9);
59
60pub const MAX_PRECISION: u8 = 6;
61
62#[derive(
70 Debug,
71 Clone,
72 Copy,
73 Eq,
74 PartialEq,
75 Ord,
76 PartialOrd,
77 Hash,
78 Serialize,
79 Deserialize
80)]
81pub struct TimestampPrecision(pub(crate) u8);
82
83impl TimestampPrecision {
84 pub fn into_u8(self) -> u8 {
86 self.0
87 }
88}
89
90#[cfg(any(test, feature = "proptest"))]
91impl Arbitrary for TimestampPrecision {
92 type Parameters = ();
93 type Strategy = BoxedStrategy<TimestampPrecision>;
94
95 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
96 (0..=MAX_PRECISION).prop_map(TimestampPrecision).boxed()
97 }
98}
99
100impl TryFrom<i64> for TimestampPrecision {
101 type Error = InvalidTimestampPrecisionError;
102
103 fn try_from(max_precision: i64) -> Result<Self, Self::Error> {
104 match u8::try_from(max_precision) {
105 Ok(max_precision) if max_precision <= MAX_PRECISION => {
106 Ok(TimestampPrecision(max_precision))
107 }
108 _ => Err(InvalidTimestampPrecisionError),
109 }
110 }
111}
112
113impl RustType<ProtoTimestampPrecision> for TimestampPrecision {
114 fn into_proto(&self) -> ProtoTimestampPrecision {
115 ProtoTimestampPrecision {
116 value: self.0.into_proto(),
117 }
118 }
119
120 fn from_proto(proto: ProtoTimestampPrecision) -> Result<Self, TryFromProtoError> {
124 TimestampPrecision::try_from(i64::from(proto.value)).map_err(|e| {
125 TryFromProtoError::InvalidFieldError(format!(
126 "ProtoTimestampPrecision::value {}: {e}",
127 proto.value
128 ))
129 })
130 }
131}
132
133#[derive(Debug, Clone)]
138pub struct InvalidTimestampPrecisionError;
139
140impl fmt::Display for InvalidTimestampPrecisionError {
141 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
142 write!(
143 f,
144 "precision for type timestamp or timestamptz must be between 0 and {}",
145 MAX_PRECISION
146 )
147 }
148}
149
150impl Error for InvalidTimestampPrecisionError {}
151
152pub trait TimeLike: chrono::Timelike {
154 fn extract_epoch<T>(&self) -> T
155 where
156 T: DecimalLike,
157 {
158 T::from(self.hour() * 60 * 60 + self.minute() * 60) + self.extract_second::<T>()
159 }
160
161 fn extract_second<T>(&self) -> T
162 where
163 T: DecimalLike,
164 {
165 let s = T::from(self.second());
166 let ns = T::from(self.nanosecond()) / T::from(1e9);
167 s + ns
168 }
169
170 fn extract_millisecond<T>(&self) -> T
171 where
172 T: DecimalLike,
173 {
174 let s = T::from(self.second() * 1_000);
175 let ns = T::from(self.nanosecond()) / T::from(1e6);
176 s + ns
177 }
178
179 fn extract_microsecond<T>(&self) -> T
180 where
181 T: DecimalLike,
182 {
183 let s = T::from(self.second() * 1_000_000);
184 let ns = T::from(self.nanosecond()) / T::from(1e3);
185 s + ns
186 }
187}
188
189impl<T> TimeLike for T where T: chrono::Timelike {}
190
191pub trait DateLike: chrono::Datelike {
193 fn extract_epoch(&self) -> i64 {
194 let naive_date = NaiveDate::from_ymd_opt(self.year(), self.month(), self.day())
195 .unwrap()
196 .and_hms_opt(0, 0, 0)
197 .unwrap();
198 naive_date.and_utc().timestamp()
199 }
200
201 fn extract_year(&self) -> i32 {
205 let year = self.year();
206 if year <= 0 { year - 1 } else { year }
207 }
208
209 fn millennium(&self) -> i32 {
210 (self.year() + if self.year() > 0 { 999 } else { -1_000 }) / 1_000
211 }
212
213 fn century(&self) -> i32 {
214 (self.year() + if self.year() > 0 { 99 } else { -100 }) / 100
215 }
216
217 fn decade(&self) -> i32 {
218 self.year().div_euclid(10)
219 }
220
221 fn iso_week_number(&self) -> u32 {
226 self.iso_week().week()
227 }
228
229 fn day_of_week(&self) -> u32 {
230 self.weekday().num_days_from_sunday()
231 }
232
233 fn iso_day_of_week(&self) -> u32 {
234 self.weekday().number_from_monday()
235 }
236}
237
238impl<T> DateLike for T where T: chrono::Datelike {}
239
240pub trait TimestampLike:
243 Clone
244 + PartialOrd
245 + std::ops::Add<Duration, Output = Self>
246 + std::ops::Sub<Duration, Output = Self>
247 + std::ops::Sub<Output = Duration>
248 + for<'a> TryInto<Datum<'a>, Error = TimestampError>
249 + for<'a> TryFrom<Datum<'a>, Error = ()>
250 + TimeLike
251 + DateLike
252{
253 fn new(date: NaiveDate, time: NaiveTime) -> Self;
254
255 fn weekday0(&self) -> usize {
258 usize::cast_from(self.weekday().num_days_from_sunday())
259 }
260
261 fn iso_year_ce(&self) -> u32 {
263 let year = self.iso_week().year();
264 if year < 1 {
265 u32::try_from(1 - year).expect("known to be positive")
266 } else {
267 u32::try_from(year).expect("known to be positive")
268 }
269 }
270
271 fn timestamp(&self) -> i64;
272
273 fn timestamp_subsec_micros(&self) -> u32;
274
275 fn extract_epoch<T>(&self) -> T
276 where
277 T: DecimalLike,
278 {
279 T::lossy_from(self.timestamp()) + T::from(self.timestamp_subsec_micros()) / T::from(1e6)
280 }
281
282 fn truncate_microseconds(&self) -> Self {
283 let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second())
289 .and_then(|t| t.with_nanosecond((self.nanosecond() / 1_000) * 1_000))
290 .expect("hour/minute/second/nanosecond came from a valid time");
291
292 Self::new(self.date(), time)
293 }
294
295 fn truncate_milliseconds(&self) -> Self {
296 let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second())
297 .and_then(|t| t.with_nanosecond((self.nanosecond() / 1_000_000) * 1_000_000))
298 .expect("hour/minute/second/nanosecond came from a valid time");
299
300 Self::new(self.date(), time)
301 }
302
303 fn truncate_second(&self) -> Self {
304 let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second()).unwrap();
305
306 Self::new(self.date(), time)
307 }
308
309 fn truncate_minute(&self) -> Self {
310 Self::new(
311 self.date(),
312 NaiveTime::from_hms_opt(self.hour(), self.minute(), 0).unwrap(),
313 )
314 }
315
316 fn truncate_hour(&self) -> Self {
317 Self::new(
318 self.date(),
319 NaiveTime::from_hms_opt(self.hour(), 0, 0).unwrap(),
320 )
321 }
322
323 fn truncate_day(&self) -> Self {
324 Self::new(self.date(), NaiveTime::from_hms_opt(0, 0, 0).unwrap())
325 }
326
327 fn truncate_week(&self) -> Result<Self, TimestampError> {
328 let num_days_from_monday = i64::from(self.date().weekday().num_days_from_monday());
329 let new_date = NaiveDate::from_ymd_opt(self.year(), self.month(), self.day())
330 .unwrap()
331 .checked_sub_signed(
332 Duration::try_days(num_days_from_monday).ok_or(TimestampError::OutOfRange)?,
333 )
334 .ok_or(TimestampError::OutOfRange)?;
335 Ok(Self::new(
336 new_date,
337 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
338 ))
339 }
340
341 fn truncate_month(&self) -> Self {
342 Self::new(
343 NaiveDate::from_ymd_opt(self.year(), self.month(), 1).unwrap(),
344 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
345 )
346 }
347
348 fn truncate_quarter(&self) -> Self {
349 let month = self.month();
350 let quarter = if month <= 3 {
351 1
352 } else if month <= 6 {
353 4
354 } else if month <= 9 {
355 7
356 } else {
357 10
358 };
359
360 Self::new(
361 NaiveDate::from_ymd_opt(self.year(), quarter, 1).unwrap(),
362 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
363 )
364 }
365
366 fn truncate_year(&self) -> Self {
367 Self::new(
368 NaiveDate::from_ymd_opt(self.year(), 1, 1).unwrap(),
369 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
370 )
371 }
372 fn truncate_decade(&self) -> Self {
373 Self::new(
374 NaiveDate::from_ymd_opt(self.year() - self.year().rem_euclid(10), 1, 1).unwrap(),
375 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
376 )
377 }
378 fn truncate_century(&self) -> Self {
379 Self::new(
381 NaiveDate::from_ymd_opt(
382 if self.year() > 0 {
383 self.year() - (self.year() - 1) % 100
384 } else {
385 self.year() - self.year() % 100 - 99
386 },
387 1,
388 1,
389 )
390 .unwrap(),
391 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
392 )
393 }
394 fn truncate_millennium(&self) -> Self {
395 Self::new(
397 NaiveDate::from_ymd_opt(
398 if self.year() > 0 {
399 self.year() - (self.year() - 1) % 1000
400 } else {
401 self.year() - self.year() % 1000 - 999
402 },
403 1,
404 1,
405 )
406 .unwrap(),
407 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
408 )
409 }
410
411 fn date(&self) -> NaiveDate;
413
414 fn date_time(&self) -> NaiveDateTime;
416
417 fn from_date_time(dt: NaiveDateTime) -> Self;
419
420 fn timezone_offset(&self) -> &'static str;
422
423 fn timezone_hours(&self) -> &'static str;
426
427 fn timezone_minutes(&self) -> &'static str;
430
431 fn timezone_name(&self, caps: bool) -> &'static str;
434
435 fn checked_add_signed(self, rhs: Duration) -> Option<Self>;
437
438 fn checked_sub_signed(self, rhs: Duration) -> Option<Self>;
440}
441
442impl TryFrom<Datum<'_>> for NaiveDateTime {
443 type Error = ();
444
445 #[inline]
446 fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
447 match from {
448 Datum::Timestamp(dt) => Ok(dt.t),
449 _ => Err(()),
450 }
451 }
452}
453
454impl TryFrom<Datum<'_>> for DateTime<Utc> {
455 type Error = ();
456
457 #[inline]
458 fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
459 match from {
460 Datum::TimestampTz(dt_tz) => Ok(dt_tz.t),
461 _ => Err(()),
462 }
463 }
464}
465
466impl TimestampLike for chrono::NaiveDateTime {
467 fn new(date: NaiveDate, time: NaiveTime) -> Self {
468 NaiveDateTime::new(date, time)
469 }
470
471 fn date(&self) -> NaiveDate {
472 self.date()
473 }
474
475 fn date_time(&self) -> NaiveDateTime {
476 self.clone()
477 }
478
479 fn from_date_time(dt: NaiveDateTime) -> NaiveDateTime {
480 dt
481 }
482
483 fn timestamp(&self) -> i64 {
484 self.and_utc().timestamp()
485 }
486
487 fn timestamp_subsec_micros(&self) -> u32 {
488 self.and_utc().timestamp_subsec_micros()
489 }
490
491 fn timezone_offset(&self) -> &'static str {
492 "+00"
493 }
494
495 fn timezone_hours(&self) -> &'static str {
496 "+00"
497 }
498
499 fn timezone_minutes(&self) -> &'static str {
500 "00"
501 }
502
503 fn timezone_name(&self, _caps: bool) -> &'static str {
504 ""
505 }
506
507 fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
508 self.checked_add_signed(rhs)
509 }
510
511 fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
512 self.checked_sub_signed(rhs)
513 }
514}
515
516impl TimestampLike for chrono::DateTime<chrono::Utc> {
517 fn new(date: NaiveDate, time: NaiveTime) -> Self {
518 Self::from_date_time(NaiveDateTime::new(date, time))
519 }
520
521 fn date(&self) -> NaiveDate {
522 self.naive_utc().date()
523 }
524
525 fn date_time(&self) -> NaiveDateTime {
526 self.naive_utc()
527 }
528
529 fn from_date_time(dt: NaiveDateTime) -> Self {
530 DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)
531 }
532
533 fn timestamp(&self) -> i64 {
534 self.timestamp()
535 }
536
537 fn timestamp_subsec_micros(&self) -> u32 {
538 self.timestamp_subsec_micros()
539 }
540
541 fn timezone_offset(&self) -> &'static str {
542 "+00"
543 }
544
545 fn timezone_hours(&self) -> &'static str {
546 "+00"
547 }
548
549 fn timezone_minutes(&self) -> &'static str {
550 "00"
551 }
552
553 fn timezone_name(&self, caps: bool) -> &'static str {
554 if caps { "UTC" } else { "utc" }
555 }
556
557 fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
558 self.checked_add_signed(rhs)
559 }
560
561 fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
562 self.checked_sub_signed(rhs)
563 }
564}
565
566#[derive(Debug, Error)]
567pub enum TimestampError {
568 #[error("timestamp out of range")]
569 OutOfRange,
570}
571
572#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
573pub struct CheckedTimestamp<T> {
574 t: T,
575}
576
577impl<T: Serialize> Serialize for CheckedTimestamp<T> {
578 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
579 where
580 S: Serializer,
581 {
582 self.t.serialize(serializer)
583 }
584}
585
586pub static LOW_DATE: LazyLock<NaiveDate> =
601 LazyLock::new(|| NaiveDate::from_ymd_opt(-4713, 12, 31).unwrap());
602pub static HIGH_DATE: LazyLock<NaiveDate> =
603 LazyLock::new(|| NaiveDate::from_ymd_opt(262142, 12, 31).unwrap());
604
605impl<T: TimestampLike> CheckedTimestamp<T> {
606 pub fn from_timestamplike(t: T) -> Result<Self, TimestampError> {
607 let d = t.date();
608 if d < *LOW_DATE {
609 return Err(TimestampError::OutOfRange);
610 }
611 if d > *HIGH_DATE {
612 return Err(TimestampError::OutOfRange);
613 }
614 Ok(Self { t })
615 }
616
617 pub fn checked_add_signed(self, rhs: Duration) -> Option<T> {
618 self.t.checked_add_signed(rhs)
619 }
620
621 pub fn checked_sub_signed(self, rhs: Duration) -> Option<T> {
622 self.t.checked_sub_signed(rhs)
623 }
624
625 pub fn diff_as(&self, other: &Self, unit: DateTimePart) -> Result<i64, TimestampError> {
631 const QUARTERS_PER_YEAR: i64 = 4;
632 const DAYS_PER_WEEK: i64 = 7;
633
634 fn diff_inner<U>(
635 a: &CheckedTimestamp<U>,
636 b: &CheckedTimestamp<U>,
637 unit: DateTimePart,
638 ) -> Option<i64>
639 where
640 U: TimestampLike,
641 {
642 match unit {
643 DateTimePart::Millennium => {
644 i64::cast_from(a.millennium()).checked_sub(i64::cast_from(b.millennium()))
645 }
646 DateTimePart::Century => {
647 i64::cast_from(a.century()).checked_sub(i64::cast_from(b.century()))
648 }
649 DateTimePart::Decade => {
650 i64::cast_from(a.decade()).checked_sub(i64::cast_from(b.decade()))
651 }
652 DateTimePart::Year => {
653 i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))
654 }
655 DateTimePart::Quarter => {
656 let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
657 let quarters = years.checked_mul(QUARTERS_PER_YEAR)?;
658 let diff = i64::cast_from(a.quarter()) - i64::cast_from(b.quarter());
659 quarters.checked_add(diff)
660 }
661 DateTimePart::Month => {
662 let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
663 let months = years.checked_mul(MONTHS_PER_YEAR)?;
664 let diff = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
665 months.checked_add(diff)
666 }
667 DateTimePart::Week => {
668 let diff = a.clone() - b.clone();
669 diff.num_days().checked_div(DAYS_PER_WEEK)
670 }
671 DateTimePart::Day => {
672 let diff = a.clone() - b.clone();
673 Some(diff.num_days())
674 }
675 DateTimePart::Hour => {
676 let diff = a.clone() - b.clone();
677 Some(diff.num_hours())
678 }
679 DateTimePart::Minute => {
680 let diff = a.clone() - b.clone();
681 Some(diff.num_minutes())
682 }
683 DateTimePart::Second => {
684 let diff = a.clone() - b.clone();
685 Some(diff.num_seconds())
686 }
687 DateTimePart::Milliseconds => {
688 let diff = a.clone() - b.clone();
689 Some(diff.num_milliseconds())
690 }
691 DateTimePart::Microseconds => {
692 let diff = a.clone() - b.clone();
693 diff.num_microseconds()
694 }
695 }
696 }
697
698 diff_inner(self, other, unit).ok_or(TimestampError::OutOfRange)
699 }
700
701 pub fn age(&self, other: &Self) -> Result<Interval, TimestampError> {
705 fn num_days_in_month<T: TimestampLike>(dt: &CheckedTimestamp<T>) -> Option<i64> {
707 let last_day = NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)?
710 .checked_add_months(Months::new(1))?
711 .checked_sub_days(Days::new(1))?
712 .day();
713
714 Some(CastFrom::cast_from(last_day))
715 }
716
717 fn age_inner<U: TimestampLike>(
720 a: &CheckedTimestamp<U>,
721 b: &CheckedTimestamp<U>,
722 ) -> Option<Interval> {
723 let mut nanos =
724 i64::cast_from(a.nanosecond()).checked_sub(i64::cast_from(b.nanosecond()))?;
725 let mut seconds = i64::cast_from(a.second()).checked_sub(i64::cast_from(b.second()))?;
726 let mut minutes = i64::cast_from(a.minute()).checked_sub(i64::cast_from(b.minute()))?;
727 let mut hours = i64::cast_from(a.hour()).checked_sub(i64::cast_from(b.hour()))?;
728 let mut days = i64::cast_from(a.day()).checked_sub(i64::cast_from(b.day()))?;
729 let mut months = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
730 let mut years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
731
732 if a < b {
734 nanos = nanos.checked_neg()?;
735 seconds = seconds.checked_neg()?;
736 minutes = minutes.checked_neg()?;
737 hours = hours.checked_neg()?;
738 days = days.checked_neg()?;
739 months = months.checked_neg()?;
740 years = years.checked_neg()?;
741 }
742
743 while nanos < 0 {
745 nanos = nanos.checked_add(NANOSECONDS_PER_SECOND)?;
746 seconds = seconds.checked_sub(1)?;
747 }
748 while seconds < 0 {
749 seconds = seconds.checked_add(SECONDS_PER_MINUTE)?;
750 minutes = minutes.checked_sub(1)?;
751 }
752 while minutes < 0 {
753 minutes = minutes.checked_add(MINUTES_PER_HOUR)?;
754 hours = hours.checked_sub(1)?;
755 }
756 while hours < 0 {
757 hours = hours.checked_add(HOURS_PER_DAY)?;
758 days = days.checked_sub(1)?
759 }
760 while days < 0 {
761 if a < b {
762 days = num_days_in_month(a).and_then(|x| days.checked_add(x))?;
763 } else {
764 days = num_days_in_month(b).and_then(|x| days.checked_add(x))?;
765 }
766 months = months.checked_sub(1)?;
767 }
768 while months < 0 {
769 months = months.checked_add(MONTHS_PER_YEAR)?;
770 years = years.checked_sub(1)?;
771 }
772
773 if a < b {
775 nanos = nanos.checked_neg()?;
776 seconds = seconds.checked_neg()?;
777 minutes = minutes.checked_neg()?;
778 hours = hours.checked_neg()?;
779 days = days.checked_neg()?;
780 months = months.checked_neg()?;
781 years = years.checked_neg()?;
782 }
783
784 let months = i32::try_from(years * MONTHS_PER_YEAR + months).ok()?;
785 let days = i32::try_from(days).ok()?;
786 let micros = Duration::nanoseconds(
787 nanos
788 .checked_add(seconds.checked_mul(NANOSECONDS_PER_SECOND)?)?
789 .checked_add(minutes.checked_mul(NANOSECONDS_PER_MINUTE)?)?
790 .checked_add(hours.checked_mul(NANOSECONDS_PER_HOUR)?)?,
791 )
792 .num_microseconds()?;
793
794 Some(Interval {
795 months,
796 days,
797 micros,
798 })
799 }
800
801 age_inner(self, other).ok_or(TimestampError::OutOfRange)
803 }
804
805 pub fn round_to_precision(
811 &self,
812 precision: Option<TimestampPrecision>,
813 ) -> Result<CheckedTimestamp<T>, TimestampError> {
814 let precision = precision.map(|p| p.into_u8()).unwrap_or(MAX_PRECISION);
815 let power = MAX_PRECISION
817 .checked_sub(precision)
818 .expect("precision fits in micros");
819 let round_to_micros = 10_i64.pow(power.into());
820
821 let mut original = self.date_time();
822 let nanoseconds = original.and_utc().timestamp_subsec_nanos();
823 original = original.truncate_microseconds();
826 let seventh_digit = (nanoseconds % 1_000) / 100;
830 assert!(seventh_digit < 10);
831 if seventh_digit >= 5 {
832 original = original
835 .checked_add_signed(Duration::microseconds(1))
836 .ok_or(TimestampError::OutOfRange)?;
837 }
838 let stamp = original.and_utc().timestamp_micros();
841 let dt = {
842 let delta_down = stamp % round_to_micros;
843 if delta_down == 0 {
844 original
845 } else {
846 let (delta_up, delta_down) = if delta_down < 0 {
847 (delta_down.abs(), round_to_micros - delta_down.abs())
848 } else {
849 (round_to_micros - delta_down, delta_down)
850 };
851 if delta_up <= delta_down {
854 original.checked_add_signed(Duration::microseconds(delta_up))
855 } else {
856 original.checked_sub_signed(Duration::microseconds(delta_down))
857 }
858 .ok_or(TimestampError::OutOfRange)?
859 }
860 };
861
862 let t = T::from_date_time(dt);
863 Self::from_timestamplike(t)
864 }
865}
866
867impl TryFrom<NaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
868 type Error = TimestampError;
869
870 fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
871 Self::from_timestamplike(value)
872 }
873}
874
875impl TryFrom<DateTime<Utc>> for CheckedTimestamp<DateTime<Utc>> {
876 type Error = TimestampError;
877
878 fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
879 Self::from_timestamplike(value)
880 }
881}
882
883impl<T: TimestampLike> std::ops::Deref for CheckedTimestamp<T> {
884 type Target = T;
885
886 #[inline]
887 fn deref(&self) -> &T {
888 &self.t
889 }
890}
891
892impl From<CheckedTimestamp<NaiveDateTime>> for NaiveDateTime {
893 fn from(val: CheckedTimestamp<NaiveDateTime>) -> Self {
894 val.t
895 }
896}
897
898impl From<CheckedTimestamp<DateTime<Utc>>> for DateTime<Utc> {
899 fn from(val: CheckedTimestamp<DateTime<Utc>>) -> Self {
900 val.t
901 }
902}
903
904impl CheckedTimestamp<NaiveDateTime> {
905 pub fn to_naive(&self) -> NaiveDateTime {
906 self.t
907 }
908}
909
910impl CheckedTimestamp<DateTime<Utc>> {
911 pub fn to_naive(&self) -> NaiveDateTime {
912 self.t.date_naive().and_time(self.t.time())
913 }
914}
915
916impl Display for CheckedTimestamp<NaiveDateTime> {
917 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
918 self.t.fmt(f)
919 }
920}
921
922impl Display for CheckedTimestamp<DateTime<Utc>> {
923 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
924 self.t.fmt(f)
925 }
926}
927
928impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
929 fn into_proto(&self) -> ProtoNaiveDateTime {
930 self.t.into_proto()
931 }
932
933 fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
934 CheckedTimestamp::from_timestamplike(NaiveDateTime::from_proto(proto)?)
938 .map_err(|err| TryFromProtoError::InvalidFieldError(err.to_string()))
939 }
940}
941
942impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<DateTime<Utc>> {
943 fn into_proto(&self) -> ProtoNaiveDateTime {
944 self.t.into_proto()
945 }
946
947 fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
948 CheckedTimestamp::from_timestamplike(DateTime::<Utc>::from_proto(proto)?)
949 .map_err(|err| TryFromProtoError::InvalidFieldError(err.to_string()))
950 }
951}
952
953impl<T: Sub<Output = Duration>> Sub<CheckedTimestamp<T>> for CheckedTimestamp<T> {
954 type Output = Duration;
955
956 #[inline]
957 fn sub(self, rhs: CheckedTimestamp<T>) -> Duration {
958 self.t - rhs.t
959 }
960}
961
962impl<T: Sub<Duration, Output = T>> Sub<Duration> for CheckedTimestamp<T> {
963 type Output = T;
964
965 #[inline]
966 fn sub(self, rhs: Duration) -> T {
967 self.t - rhs
968 }
969}
970
971#[cfg(any(test, feature = "proptest"))]
972impl Arbitrary for CheckedTimestamp<NaiveDateTime> {
973 type Parameters = ();
974 type Strategy = BoxedStrategy<CheckedTimestamp<NaiveDateTime>>;
975
976 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
977 arb_naive_date_time()
978 .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
979 .boxed()
980 }
981}
982
983#[cfg(any(test, feature = "proptest"))]
984impl Arbitrary for CheckedTimestamp<DateTime<Utc>> {
985 type Parameters = ();
986 type Strategy = BoxedStrategy<CheckedTimestamp<DateTime<Utc>>>;
987
988 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
989 arb_utc_date_time()
990 .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
991 .boxed()
992 }
993}
994
995#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
1000pub struct PackedNaiveDateTime([u8; Self::SIZE]);
1001
1002#[allow(clippy::as_conversions)]
1006impl FixedSizeCodec<NaiveDateTime> for PackedNaiveDateTime {
1007 const SIZE: usize = 16;
1008
1009 fn as_bytes(&self) -> &[u8] {
1010 &self.0
1011 }
1012
1013 fn from_bytes(slice: &[u8]) -> Result<Self, String> {
1014 let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
1015 format!(
1016 "size for PackedNaiveDateTime is {} bytes, got {}",
1017 Self::SIZE,
1018 slice.len()
1019 )
1020 })?;
1021 Ok(PackedNaiveDateTime(buf))
1022 }
1023
1024 #[inline]
1025 fn from_value(value: NaiveDateTime) -> Self {
1026 let mut buf = [0u8; 16];
1027
1028 let year = (value.year() as u32) ^ (0x8000_0000u32);
1031 let ordinal = value.ordinal();
1032 let secs = value.num_seconds_from_midnight();
1033 let nano = value.nanosecond();
1034
1035 buf[..4].copy_from_slice(&year.to_be_bytes());
1036 buf[4..8].copy_from_slice(&ordinal.to_be_bytes());
1037 buf[8..12].copy_from_slice(&secs.to_be_bytes());
1038 buf[12..].copy_from_slice(&nano.to_be_bytes());
1039
1040 PackedNaiveDateTime(buf)
1041 }
1042
1043 #[inline]
1044 fn into_value(self) -> NaiveDateTime {
1045 let mut year = [0u8; 4];
1046 year.copy_from_slice(&self.0[..4]);
1047 let year = u32::from_be_bytes(year) ^ 0x8000_0000u32;
1048
1049 let mut ordinal = [0u8; 4];
1050 ordinal.copy_from_slice(&self.0[4..8]);
1051 let ordinal = u32::from_be_bytes(ordinal);
1052
1053 let mut secs = [0u8; 4];
1054 secs.copy_from_slice(&self.0[8..12]);
1055 let secs = u32::from_be_bytes(secs);
1056
1057 let mut nano = [0u8; 4];
1058 nano.copy_from_slice(&self.0[12..]);
1059 let nano = u32::from_be_bytes(nano);
1060
1061 let date = NaiveDate::from_yo_opt(year as i32, ordinal)
1062 .expect("NaiveDate roundtrips with PackedNaiveDateTime");
1063 let time = NaiveTime::from_num_seconds_from_midnight_opt(secs, nano)
1064 .expect("NaiveTime roundtrips with PackedNaiveDateTime");
1065
1066 NaiveDateTime::new(date, time)
1067 }
1068}
1069
1070pub fn checked_add_with_leapsecond(
1077 lhs: &NaiveDateTime,
1078 rhs: &FixedOffset,
1079) -> Option<NaiveDateTime> {
1080 checked_offset_with_leapsecond(lhs, rhs.local_minus_utc())
1081}
1082
1083pub fn checked_sub_with_leapsecond(
1087 lhs: &NaiveDateTime,
1088 rhs: &FixedOffset,
1089) -> Option<NaiveDateTime> {
1090 checked_offset_with_leapsecond(lhs, rhs.local_minus_utc().checked_neg()?)
1091}
1092
1093fn checked_offset_with_leapsecond(lhs: &NaiveDateTime, seconds: i32) -> Option<NaiveDateTime> {
1095 let nanos = lhs.nanosecond();
1098 let whole = lhs.with_nanosecond(0).expect("0 is a valid nanosecond");
1099 let dt = whole.checked_add_signed(Duration::try_seconds(i64::from(seconds))?)?;
1100 if nanos >= 1_000_000_000 && dt.second() != 59 {
1108 dt.checked_add_signed(Duration::nanoseconds(i64::from(nanos)))
1109 } else {
1110 Some(
1111 dt.with_nanosecond(nanos)
1112 .expect("nanos came from a NaiveTime"),
1113 )
1114 }
1115}
1116
1117#[cfg(test)]
1118mod test {
1119 use super::*;
1120 use itertools::Itertools;
1121 use mz_ore::{assert_err, assert_ok};
1122 use proptest::prelude::*;
1123
1124 #[mz_ore::test]
1125 fn test_max_age() {
1126 let low = CheckedTimestamp::try_from(
1127 LOW_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1128 )
1129 .unwrap();
1130 let high = CheckedTimestamp::try_from(
1131 HIGH_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1132 )
1133 .unwrap();
1134
1135 let years = HIGH_DATE.year() - LOW_DATE.year();
1136 let months = years * 12;
1137
1138 let result = high.age(&low).unwrap();
1140 assert_eq!(result, Interval::new(months, 0, 0));
1141
1142 let result = low.age(&high).unwrap();
1144 assert_eq!(result, Interval::new(-months, 0, 0));
1145 }
1146
1147 fn assert_round_to_precision(
1148 dt: CheckedTimestamp<NaiveDateTime>,
1149 precision: u8,
1150 expected: i64,
1151 ) {
1152 let updated = dt
1153 .round_to_precision(Some(TimestampPrecision(precision)))
1154 .unwrap();
1155 assert_eq!(expected, updated.and_utc().timestamp_micros());
1156 }
1157
1158 #[mz_ore::test]
1159 fn test_round_to_precision() {
1160 let date = CheckedTimestamp::try_from(
1161 NaiveDate::from_ymd_opt(1970, 1, 1)
1162 .unwrap()
1163 .and_hms_nano_opt(0, 0, 0, 123456789)
1164 .unwrap(),
1165 )
1166 .unwrap();
1167 assert_round_to_precision(date, 0, 0);
1168 assert_round_to_precision(date, 1, 100000);
1169 assert_round_to_precision(date, 2, 120000);
1170 assert_round_to_precision(date, 3, 123000);
1171 assert_round_to_precision(date, 4, 123500);
1172 assert_round_to_precision(date, 5, 123460);
1173 assert_round_to_precision(date, 6, 123457);
1174
1175 let low =
1176 CheckedTimestamp::try_from(LOW_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1177 .unwrap();
1178 assert_round_to_precision(low, 0, -210863606400000000);
1179 assert_round_to_precision(low, 1, -210863606399900000);
1180 assert_round_to_precision(low, 2, -210863606399880000);
1181 assert_round_to_precision(low, 3, -210863606399877000);
1182 assert_round_to_precision(low, 4, -210863606399876500);
1183 assert_round_to_precision(low, 5, -210863606399876540);
1184 assert_round_to_precision(low, 6, -210863606399876543);
1185
1186 let high =
1187 CheckedTimestamp::try_from(HIGH_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1188 .unwrap();
1189 assert_round_to_precision(high, 0, 8210266790400000000);
1190 assert_round_to_precision(high, 1, 8210266790400100000);
1191 assert_round_to_precision(high, 2, 8210266790400120000);
1192 assert_round_to_precision(high, 3, 8210266790400123000);
1193 assert_round_to_precision(high, 4, 8210266790400123500);
1194 assert_round_to_precision(high, 5, 8210266790400123460);
1195 assert_round_to_precision(high, 6, 8210266790400123457);
1196 }
1197
1198 #[mz_ore::test]
1199 fn test_round_to_precision_leap_second_off_minute() {
1200 let leap = NaiveDate::from_ymd_opt(3, 3, 17)
1207 .unwrap()
1208 .and_hms_opt(12, 30, 56)
1209 .unwrap()
1210 .with_nanosecond(1_000_000_000)
1211 .unwrap();
1212 let ts = CheckedTimestamp::try_from(leap).unwrap();
1213 for precision in [None, Some(0), Some(3), Some(6)] {
1214 ts.round_to_precision(precision.map(TimestampPrecision))
1215 .unwrap();
1216 }
1217 }
1218
1219 #[mz_ore::test]
1220 fn test_checked_offset_with_leapsecond_branches() {
1221 let leap = NaiveDate::from_ymd_opt(2024, 6, 30)
1226 .unwrap()
1227 .and_hms_nano_opt(23, 59, 59, 1_500_000_000)
1228 .unwrap();
1229 let second = FixedOffset::east_opt(1).unwrap();
1230 let minute = FixedOffset::east_opt(60).unwrap();
1231
1232 assert_eq!(
1235 checked_add_with_leapsecond(&leap, &second).unwrap(),
1236 NaiveDate::from_ymd_opt(2024, 7, 1)
1237 .unwrap()
1238 .and_hms_nano_opt(0, 0, 1, 500_000_000)
1239 .unwrap()
1240 );
1241 assert_eq!(
1242 checked_sub_with_leapsecond(&leap, &second).unwrap(),
1243 NaiveDate::from_ymd_opt(2024, 6, 30)
1244 .unwrap()
1245 .and_hms_nano_opt(23, 59, 59, 500_000_000)
1246 .unwrap()
1247 );
1248
1249 assert_eq!(
1251 checked_add_with_leapsecond(&leap, &minute).unwrap(),
1252 NaiveDate::from_ymd_opt(2024, 7, 1)
1253 .unwrap()
1254 .and_hms_nano_opt(0, 0, 59, 1_500_000_000)
1255 .unwrap()
1256 );
1257 }
1258
1259 #[mz_ore::test]
1260 fn test_round_to_precision_high_date_overflow() {
1261 for (nanos, precision) in [
1270 (999_999_500, None),
1271 (500_000_000, Some(0)),
1272 (950_000_000, Some(1)),
1273 (995_000_000, Some(2)),
1274 (999_500_000, Some(3)),
1275 (999_950_000, Some(4)),
1276 (999_995_000, Some(5)),
1277 (999_999_500, Some(6)),
1278 ] {
1279 let ts =
1280 CheckedTimestamp::try_from(HIGH_DATE.and_hms_nano_opt(23, 59, 59, nanos).unwrap())
1281 .unwrap();
1282 assert!(
1283 matches!(
1284 ts.round_to_precision(precision.map(TimestampPrecision)),
1285 Err(TimestampError::OutOfRange)
1286 ),
1287 "rounding {nanos}ns to {precision:?} should report out of range"
1288 );
1289 }
1290
1291 let ts =
1294 CheckedTimestamp::try_from(HIGH_DATE.and_hms_nano_opt(23, 59, 59, 400_000).unwrap())
1295 .unwrap();
1296 assert_ok!(ts.round_to_precision(Some(TimestampPrecision(3))));
1297 }
1298
1299 #[mz_ore::test]
1300 fn test_precision_edge_cases() {
1301 #[allow(clippy::disallowed_methods)] let result = std::panic::catch_unwind(|| {
1303 let date = CheckedTimestamp::try_from(
1304 DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1305 )
1306 .unwrap();
1307 let _ = date.round_to_precision(Some(TimestampPrecision(7)));
1308 });
1309 assert_err!(result);
1310
1311 let date = CheckedTimestamp::try_from(
1312 DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1313 )
1314 .unwrap();
1315 let date = date.round_to_precision(None).unwrap();
1316 assert_eq!(123456, date.and_utc().timestamp_micros());
1317 }
1318
1319 #[mz_ore::test]
1320 fn test_equality_with_same_precision() {
1321 let date1 =
1322 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456).unwrap()).unwrap();
1323 let date1 = date1
1324 .round_to_precision(Some(TimestampPrecision(0)))
1325 .unwrap();
1326
1327 let date2 =
1328 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1329 let date2 = date2
1330 .round_to_precision(Some(TimestampPrecision(0)))
1331 .unwrap();
1332 assert_eq!(date1, date2);
1333 }
1334
1335 #[mz_ore::test]
1336 fn test_equality_with_different_precisions() {
1337 let date1 =
1338 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123500000).unwrap()).unwrap();
1339 let date1 = date1
1340 .round_to_precision(Some(TimestampPrecision(5)))
1341 .unwrap();
1342
1343 let date2 =
1344 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1345 let date2 = date2
1346 .round_to_precision(Some(TimestampPrecision(4)))
1347 .unwrap();
1348 assert_eq!(date1, date2);
1349 }
1350
1351 proptest! {
1352 #[mz_ore::test]
1353 #[cfg_attr(miri, ignore)] fn test_age_naive(a: CheckedTimestamp<NaiveDateTime>, b: CheckedTimestamp<NaiveDateTime>) {
1355 let result = a.age(&b);
1356 prop_assert!(result.is_ok());
1357 }
1358
1359 #[mz_ore::test]
1360 #[cfg_attr(miri, ignore)] fn test_age_utc(a: CheckedTimestamp<DateTime<Utc>>, b: CheckedTimestamp<DateTime<Utc>>) {
1362 let result = a.age(&b);
1363 prop_assert!(result.is_ok());
1364 }
1365 }
1366
1367 #[mz_ore::test]
1368 fn proptest_packed_naive_date_time_roundtrips() {
1369 proptest!(|(timestamp in arb_naive_date_time())| {
1370 let packed = PackedNaiveDateTime::from_value(timestamp);
1371 let rnd = packed.into_value();
1372 prop_assert_eq!(timestamp, rnd);
1373 });
1374 }
1375
1376 #[mz_ore::test]
1377 #[cfg_attr(miri, ignore)] fn proptest_packed_naive_date_time_sort_order() {
1379 let strat = proptest::collection::vec(arb_naive_date_time(), 0..128);
1380 proptest!(|(mut times in strat)| {
1381 let mut packed: Vec<_> = times
1382 .iter()
1383 .copied()
1384 .map(PackedNaiveDateTime::from_value)
1385 .collect();
1386
1387 times.sort();
1388 packed.sort();
1389
1390 for (time, packed) in times.into_iter().zip_eq(packed.into_iter()) {
1391 let rnd = packed.into_value();
1392 prop_assert_eq!(time, rnd);
1393 }
1394 });
1395 }
1396}