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_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#[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 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#[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
138pub 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
177pub 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 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
218pub 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 fn weekday0(&self) -> usize {
236 usize::cast_from(self.weekday().num_days_from_sunday())
237 }
238
239 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 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 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 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 fn date(&self) -> NaiveDate;
391
392 fn date_time(&self) -> NaiveDateTime;
394
395 fn from_date_time(dt: NaiveDateTime) -> Self;
397
398 fn timezone_offset(&self) -> &'static str;
400
401 fn timezone_hours(&self) -> &'static str;
404
405 fn timezone_minutes(&self) -> &'static str;
408
409 fn timezone_name(&self, caps: bool) -> &'static str;
412
413 fn checked_add_signed(self, rhs: Duration) -> Option<Self>;
415
416 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
564pub 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 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 pub fn age(&self, other: &Self) -> Result<Interval, TimestampError> {
683 fn num_days_in_month<T: TimestampLike>(dt: &CheckedTimestamp<T>) -> Option<i64> {
685 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 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 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 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 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 age_inner(self, other).ok_or(TimestampError::OutOfRange)
781 }
782
783 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 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 original = original.truncate_microseconds();
800 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 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 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#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
967pub struct PackedNaiveDateTime([u8; Self::SIZE]);
968
969#[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 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 let result = high.age(&low).unwrap();
1060 assert_eq!(result, Interval::new(months, 0, 0));
1061
1062 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 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)] 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)] 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)] 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}