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_micro_opt(
262 self.hour(),
263 self.minute(),
264 self.second(),
265 self.nanosecond() / 1_000,
266 )
267 .unwrap();
268
269 Self::new(self.date(), time)
270 }
271
272 fn truncate_milliseconds(&self) -> Self {
273 let time = NaiveTime::from_hms_milli_opt(
274 self.hour(),
275 self.minute(),
276 self.second(),
277 self.nanosecond() / 1_000_000,
278 )
279 .unwrap();
280
281 Self::new(self.date(), time)
282 }
283
284 fn truncate_second(&self) -> Self {
285 let time = NaiveTime::from_hms_opt(self.hour(), self.minute(), self.second()).unwrap();
286
287 Self::new(self.date(), time)
288 }
289
290 fn truncate_minute(&self) -> Self {
291 Self::new(
292 self.date(),
293 NaiveTime::from_hms_opt(self.hour(), self.minute(), 0).unwrap(),
294 )
295 }
296
297 fn truncate_hour(&self) -> Self {
298 Self::new(
299 self.date(),
300 NaiveTime::from_hms_opt(self.hour(), 0, 0).unwrap(),
301 )
302 }
303
304 fn truncate_day(&self) -> Self {
305 Self::new(self.date(), NaiveTime::from_hms_opt(0, 0, 0).unwrap())
306 }
307
308 fn truncate_week(&self) -> Result<Self, TimestampError> {
309 let num_days_from_monday = i64::from(self.date().weekday().num_days_from_monday());
310 let new_date = NaiveDate::from_ymd_opt(self.year(), self.month(), self.day())
311 .unwrap()
312 .checked_sub_signed(
313 Duration::try_days(num_days_from_monday).ok_or(TimestampError::OutOfRange)?,
314 )
315 .ok_or(TimestampError::OutOfRange)?;
316 Ok(Self::new(
317 new_date,
318 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
319 ))
320 }
321
322 fn truncate_month(&self) -> Self {
323 Self::new(
324 NaiveDate::from_ymd_opt(self.year(), self.month(), 1).unwrap(),
325 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
326 )
327 }
328
329 fn truncate_quarter(&self) -> Self {
330 let month = self.month();
331 let quarter = if month <= 3 {
332 1
333 } else if month <= 6 {
334 4
335 } else if month <= 9 {
336 7
337 } else {
338 10
339 };
340
341 Self::new(
342 NaiveDate::from_ymd_opt(self.year(), quarter, 1).unwrap(),
343 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
344 )
345 }
346
347 fn truncate_year(&self) -> Self {
348 Self::new(
349 NaiveDate::from_ymd_opt(self.year(), 1, 1).unwrap(),
350 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
351 )
352 }
353 fn truncate_decade(&self) -> Self {
354 Self::new(
355 NaiveDate::from_ymd_opt(self.year() - self.year().rem_euclid(10), 1, 1).unwrap(),
356 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
357 )
358 }
359 fn truncate_century(&self) -> Self {
360 Self::new(
362 NaiveDate::from_ymd_opt(
363 if self.year() > 0 {
364 self.year() - (self.year() - 1) % 100
365 } else {
366 self.year() - self.year() % 100 - 99
367 },
368 1,
369 1,
370 )
371 .unwrap(),
372 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
373 )
374 }
375 fn truncate_millennium(&self) -> Self {
376 Self::new(
378 NaiveDate::from_ymd_opt(
379 if self.year() > 0 {
380 self.year() - (self.year() - 1) % 1000
381 } else {
382 self.year() - self.year() % 1000 - 999
383 },
384 1,
385 1,
386 )
387 .unwrap(),
388 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
389 )
390 }
391
392 fn date(&self) -> NaiveDate;
394
395 fn date_time(&self) -> NaiveDateTime;
397
398 fn from_date_time(dt: NaiveDateTime) -> Self;
400
401 fn timezone_offset(&self) -> &'static str;
403
404 fn timezone_hours(&self) -> &'static str;
407
408 fn timezone_minutes(&self) -> &'static str;
411
412 fn timezone_name(&self, caps: bool) -> &'static str;
415
416 fn checked_add_signed(self, rhs: Duration) -> Option<Self>;
418
419 fn checked_sub_signed(self, rhs: Duration) -> Option<Self>;
421}
422
423impl TryFrom<Datum<'_>> for NaiveDateTime {
424 type Error = ();
425
426 #[inline]
427 fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
428 match from {
429 Datum::Timestamp(dt) => Ok(dt.t),
430 _ => Err(()),
431 }
432 }
433}
434
435impl TryFrom<Datum<'_>> for DateTime<Utc> {
436 type Error = ();
437
438 #[inline]
439 fn try_from(from: Datum<'_>) -> Result<Self, Self::Error> {
440 match from {
441 Datum::TimestampTz(dt_tz) => Ok(dt_tz.t),
442 _ => Err(()),
443 }
444 }
445}
446
447impl TimestampLike for chrono::NaiveDateTime {
448 fn new(date: NaiveDate, time: NaiveTime) -> Self {
449 NaiveDateTime::new(date, time)
450 }
451
452 fn date(&self) -> NaiveDate {
453 self.date()
454 }
455
456 fn date_time(&self) -> NaiveDateTime {
457 self.clone()
458 }
459
460 fn from_date_time(dt: NaiveDateTime) -> NaiveDateTime {
461 dt
462 }
463
464 fn timestamp(&self) -> i64 {
465 self.and_utc().timestamp()
466 }
467
468 fn timestamp_subsec_micros(&self) -> u32 {
469 self.and_utc().timestamp_subsec_micros()
470 }
471
472 fn timezone_offset(&self) -> &'static str {
473 "+00"
474 }
475
476 fn timezone_hours(&self) -> &'static str {
477 "+00"
478 }
479
480 fn timezone_minutes(&self) -> &'static str {
481 "00"
482 }
483
484 fn timezone_name(&self, _caps: bool) -> &'static str {
485 ""
486 }
487
488 fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
489 self.checked_add_signed(rhs)
490 }
491
492 fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
493 self.checked_sub_signed(rhs)
494 }
495}
496
497impl TimestampLike for chrono::DateTime<chrono::Utc> {
498 fn new(date: NaiveDate, time: NaiveTime) -> Self {
499 Self::from_date_time(NaiveDateTime::new(date, time))
500 }
501
502 fn date(&self) -> NaiveDate {
503 self.naive_utc().date()
504 }
505
506 fn date_time(&self) -> NaiveDateTime {
507 self.naive_utc()
508 }
509
510 fn from_date_time(dt: NaiveDateTime) -> Self {
511 DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc)
512 }
513
514 fn timestamp(&self) -> i64 {
515 self.timestamp()
516 }
517
518 fn timestamp_subsec_micros(&self) -> u32 {
519 self.timestamp_subsec_micros()
520 }
521
522 fn timezone_offset(&self) -> &'static str {
523 "+00"
524 }
525
526 fn timezone_hours(&self) -> &'static str {
527 "+00"
528 }
529
530 fn timezone_minutes(&self) -> &'static str {
531 "00"
532 }
533
534 fn timezone_name(&self, caps: bool) -> &'static str {
535 if caps { "UTC" } else { "utc" }
536 }
537
538 fn checked_add_signed(self, rhs: Duration) -> Option<Self> {
539 self.checked_add_signed(rhs)
540 }
541
542 fn checked_sub_signed(self, rhs: Duration) -> Option<Self> {
543 self.checked_sub_signed(rhs)
544 }
545}
546
547#[derive(Debug, Error)]
548pub enum TimestampError {
549 #[error("timestamp out of range")]
550 OutOfRange,
551}
552
553#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
554pub struct CheckedTimestamp<T> {
555 t: T,
556}
557
558impl<T: Serialize> Serialize for CheckedTimestamp<T> {
559 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
560 where
561 S: Serializer,
562 {
563 self.t.serialize(serializer)
564 }
565}
566
567pub static LOW_DATE: LazyLock<NaiveDate> =
582 LazyLock::new(|| NaiveDate::from_ymd_opt(-4713, 12, 31).unwrap());
583pub static HIGH_DATE: LazyLock<NaiveDate> =
584 LazyLock::new(|| NaiveDate::from_ymd_opt(262142, 12, 31).unwrap());
585
586impl<T: TimestampLike> CheckedTimestamp<T> {
587 pub fn from_timestamplike(t: T) -> Result<Self, TimestampError> {
588 let d = t.date();
589 if d < *LOW_DATE {
590 return Err(TimestampError::OutOfRange);
591 }
592 if d > *HIGH_DATE {
593 return Err(TimestampError::OutOfRange);
594 }
595 Ok(Self { t })
596 }
597
598 pub fn checked_add_signed(self, rhs: Duration) -> Option<T> {
599 self.t.checked_add_signed(rhs)
600 }
601
602 pub fn checked_sub_signed(self, rhs: Duration) -> Option<T> {
603 self.t.checked_sub_signed(rhs)
604 }
605
606 pub fn diff_as(&self, other: &Self, unit: DateTimePart) -> Result<i64, TimestampError> {
612 const QUARTERS_PER_YEAR: i64 = 4;
613 const DAYS_PER_WEEK: i64 = 7;
614
615 fn diff_inner<U>(
616 a: &CheckedTimestamp<U>,
617 b: &CheckedTimestamp<U>,
618 unit: DateTimePart,
619 ) -> Option<i64>
620 where
621 U: TimestampLike,
622 {
623 match unit {
624 DateTimePart::Millennium => {
625 i64::cast_from(a.millennium()).checked_sub(i64::cast_from(b.millennium()))
626 }
627 DateTimePart::Century => {
628 i64::cast_from(a.century()).checked_sub(i64::cast_from(b.century()))
629 }
630 DateTimePart::Decade => {
631 i64::cast_from(a.decade()).checked_sub(i64::cast_from(b.decade()))
632 }
633 DateTimePart::Year => {
634 i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))
635 }
636 DateTimePart::Quarter => {
637 let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
638 let quarters = years.checked_mul(QUARTERS_PER_YEAR)?;
639 let diff = i64::cast_from(a.quarter()) - i64::cast_from(b.quarter());
640 quarters.checked_add(diff)
641 }
642 DateTimePart::Month => {
643 let years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
644 let months = years.checked_mul(MONTHS_PER_YEAR)?;
645 let diff = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
646 months.checked_add(diff)
647 }
648 DateTimePart::Week => {
649 let diff = a.clone() - b.clone();
650 diff.num_days().checked_div(DAYS_PER_WEEK)
651 }
652 DateTimePart::Day => {
653 let diff = a.clone() - b.clone();
654 Some(diff.num_days())
655 }
656 DateTimePart::Hour => {
657 let diff = a.clone() - b.clone();
658 Some(diff.num_hours())
659 }
660 DateTimePart::Minute => {
661 let diff = a.clone() - b.clone();
662 Some(diff.num_minutes())
663 }
664 DateTimePart::Second => {
665 let diff = a.clone() - b.clone();
666 Some(diff.num_seconds())
667 }
668 DateTimePart::Milliseconds => {
669 let diff = a.clone() - b.clone();
670 Some(diff.num_milliseconds())
671 }
672 DateTimePart::Microseconds => {
673 let diff = a.clone() - b.clone();
674 diff.num_microseconds()
675 }
676 }
677 }
678
679 diff_inner(self, other, unit).ok_or(TimestampError::OutOfRange)
680 }
681
682 pub fn age(&self, other: &Self) -> Result<Interval, TimestampError> {
686 fn num_days_in_month<T: TimestampLike>(dt: &CheckedTimestamp<T>) -> Option<i64> {
688 let last_day = NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)?
691 .checked_add_months(Months::new(1))?
692 .checked_sub_days(Days::new(1))?
693 .day();
694
695 Some(CastFrom::cast_from(last_day))
696 }
697
698 fn age_inner<U: TimestampLike>(
701 a: &CheckedTimestamp<U>,
702 b: &CheckedTimestamp<U>,
703 ) -> Option<Interval> {
704 let mut nanos =
705 i64::cast_from(a.nanosecond()).checked_sub(i64::cast_from(b.nanosecond()))?;
706 let mut seconds = i64::cast_from(a.second()).checked_sub(i64::cast_from(b.second()))?;
707 let mut minutes = i64::cast_from(a.minute()).checked_sub(i64::cast_from(b.minute()))?;
708 let mut hours = i64::cast_from(a.hour()).checked_sub(i64::cast_from(b.hour()))?;
709 let mut days = i64::cast_from(a.day()).checked_sub(i64::cast_from(b.day()))?;
710 let mut months = i64::cast_from(a.month()).checked_sub(i64::cast_from(b.month()))?;
711 let mut years = i64::cast_from(a.year()).checked_sub(i64::cast_from(b.year()))?;
712
713 if a < b {
715 nanos = nanos.checked_neg()?;
716 seconds = seconds.checked_neg()?;
717 minutes = minutes.checked_neg()?;
718 hours = hours.checked_neg()?;
719 days = days.checked_neg()?;
720 months = months.checked_neg()?;
721 years = years.checked_neg()?;
722 }
723
724 while nanos < 0 {
726 nanos = nanos.checked_add(NANOSECONDS_PER_SECOND)?;
727 seconds = seconds.checked_sub(1)?;
728 }
729 while seconds < 0 {
730 seconds = seconds.checked_add(SECONDS_PER_MINUTE)?;
731 minutes = minutes.checked_sub(1)?;
732 }
733 while minutes < 0 {
734 minutes = minutes.checked_add(MINUTES_PER_HOUR)?;
735 hours = hours.checked_sub(1)?;
736 }
737 while hours < 0 {
738 hours = hours.checked_add(HOURS_PER_DAY)?;
739 days = days.checked_sub(1)?
740 }
741 while days < 0 {
742 if a < b {
743 days = num_days_in_month(a).and_then(|x| days.checked_add(x))?;
744 } else {
745 days = num_days_in_month(b).and_then(|x| days.checked_add(x))?;
746 }
747 months = months.checked_sub(1)?;
748 }
749 while months < 0 {
750 months = months.checked_add(MONTHS_PER_YEAR)?;
751 years = years.checked_sub(1)?;
752 }
753
754 if a < b {
756 nanos = nanos.checked_neg()?;
757 seconds = seconds.checked_neg()?;
758 minutes = minutes.checked_neg()?;
759 hours = hours.checked_neg()?;
760 days = days.checked_neg()?;
761 months = months.checked_neg()?;
762 years = years.checked_neg()?;
763 }
764
765 let months = i32::try_from(years * MONTHS_PER_YEAR + months).ok()?;
766 let days = i32::try_from(days).ok()?;
767 let micros = Duration::nanoseconds(
768 nanos
769 .checked_add(seconds.checked_mul(NANOSECONDS_PER_SECOND)?)?
770 .checked_add(minutes.checked_mul(NANOSECONDS_PER_MINUTE)?)?
771 .checked_add(hours.checked_mul(NANOSECONDS_PER_HOUR)?)?,
772 )
773 .num_microseconds()?;
774
775 Some(Interval {
776 months,
777 days,
778 micros,
779 })
780 }
781
782 age_inner(self, other).ok_or(TimestampError::OutOfRange)
784 }
785
786 pub fn round_to_precision(
788 &self,
789 precision: Option<TimestampPrecision>,
790 ) -> Result<CheckedTimestamp<T>, TimestampError> {
791 let precision = precision.map(|p| p.into_u8()).unwrap_or(MAX_PRECISION);
792 let power = MAX_PRECISION
794 .checked_sub(precision)
795 .expect("precision fits in micros");
796 let round_to_micros = 10_i64.pow(power.into());
797
798 let mut original = self.date_time();
799 let nanoseconds = original.and_utc().timestamp_subsec_nanos();
800 original = original.truncate_microseconds();
803 let seventh_digit = (nanoseconds % 1_000) / 100;
807 assert!(seventh_digit < 10);
808 if seventh_digit >= 5 {
809 original = original + Duration::microseconds(1);
810 }
811 let stamp = original.and_utc().timestamp_micros();
814 let dt = {
815 let delta_down = stamp % round_to_micros;
816 if delta_down == 0 {
817 original
818 } else {
819 let (delta_up, delta_down) = if delta_down < 0 {
820 (delta_down.abs(), round_to_micros - delta_down.abs())
821 } else {
822 (round_to_micros - delta_down, delta_down)
823 };
824 if delta_up <= delta_down {
825 original + Duration::microseconds(delta_up)
826 } else {
827 original - Duration::microseconds(delta_down)
828 }
829 }
830 };
831
832 let t = T::from_date_time(dt);
833 Self::from_timestamplike(t)
834 }
835}
836
837impl TryFrom<NaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
838 type Error = TimestampError;
839
840 fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
841 Self::from_timestamplike(value)
842 }
843}
844
845impl TryFrom<DateTime<Utc>> for CheckedTimestamp<DateTime<Utc>> {
846 type Error = TimestampError;
847
848 fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
849 Self::from_timestamplike(value)
850 }
851}
852
853impl<T: TimestampLike> std::ops::Deref for CheckedTimestamp<T> {
854 type Target = T;
855
856 #[inline]
857 fn deref(&self) -> &T {
858 &self.t
859 }
860}
861
862impl From<CheckedTimestamp<NaiveDateTime>> for NaiveDateTime {
863 fn from(val: CheckedTimestamp<NaiveDateTime>) -> Self {
864 val.t
865 }
866}
867
868impl From<CheckedTimestamp<DateTime<Utc>>> for DateTime<Utc> {
869 fn from(val: CheckedTimestamp<DateTime<Utc>>) -> Self {
870 val.t
871 }
872}
873
874impl CheckedTimestamp<NaiveDateTime> {
875 pub fn to_naive(&self) -> NaiveDateTime {
876 self.t
877 }
878}
879
880impl CheckedTimestamp<DateTime<Utc>> {
881 pub fn to_naive(&self) -> NaiveDateTime {
882 self.t.date_naive().and_time(self.t.time())
883 }
884}
885
886impl Display for CheckedTimestamp<NaiveDateTime> {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 self.t.fmt(f)
889 }
890}
891
892impl Display for CheckedTimestamp<DateTime<Utc>> {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 self.t.fmt(f)
895 }
896}
897
898impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<NaiveDateTime> {
899 fn into_proto(&self) -> ProtoNaiveDateTime {
900 self.t.into_proto()
901 }
902
903 fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
904 Ok(Self {
905 t: NaiveDateTime::from_proto(proto)?,
906 })
907 }
908}
909
910impl RustType<ProtoNaiveDateTime> for CheckedTimestamp<DateTime<Utc>> {
911 fn into_proto(&self) -> ProtoNaiveDateTime {
912 self.t.into_proto()
913 }
914
915 fn from_proto(proto: ProtoNaiveDateTime) -> Result<Self, TryFromProtoError> {
916 Ok(Self {
917 t: DateTime::<Utc>::from_proto(proto)?,
918 })
919 }
920}
921
922impl<T: Sub<Output = Duration>> Sub<CheckedTimestamp<T>> for CheckedTimestamp<T> {
923 type Output = Duration;
924
925 #[inline]
926 fn sub(self, rhs: CheckedTimestamp<T>) -> Duration {
927 self.t - rhs.t
928 }
929}
930
931impl<T: Sub<Duration, Output = T>> Sub<Duration> for CheckedTimestamp<T> {
932 type Output = T;
933
934 #[inline]
935 fn sub(self, rhs: Duration) -> T {
936 self.t - rhs
937 }
938}
939
940#[cfg(any(test, feature = "proptest"))]
941impl Arbitrary for CheckedTimestamp<NaiveDateTime> {
942 type Parameters = ();
943 type Strategy = BoxedStrategy<CheckedTimestamp<NaiveDateTime>>;
944
945 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
946 arb_naive_date_time()
947 .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
948 .boxed()
949 }
950}
951
952#[cfg(any(test, feature = "proptest"))]
953impl Arbitrary for CheckedTimestamp<DateTime<Utc>> {
954 type Parameters = ();
955 type Strategy = BoxedStrategy<CheckedTimestamp<DateTime<Utc>>>;
956
957 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
958 arb_utc_date_time()
959 .prop_map(|dt| CheckedTimestamp::try_from(dt).unwrap())
960 .boxed()
961 }
962}
963
964#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
969pub struct PackedNaiveDateTime([u8; Self::SIZE]);
970
971#[allow(clippy::as_conversions)]
975impl FixedSizeCodec<NaiveDateTime> for PackedNaiveDateTime {
976 const SIZE: usize = 16;
977
978 fn as_bytes(&self) -> &[u8] {
979 &self.0
980 }
981
982 fn from_bytes(slice: &[u8]) -> Result<Self, String> {
983 let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
984 format!(
985 "size for PackedNaiveDateTime is {} bytes, got {}",
986 Self::SIZE,
987 slice.len()
988 )
989 })?;
990 Ok(PackedNaiveDateTime(buf))
991 }
992
993 #[inline]
994 fn from_value(value: NaiveDateTime) -> Self {
995 let mut buf = [0u8; 16];
996
997 let year = (value.year() as u32) ^ (0x8000_0000u32);
1000 let ordinal = value.ordinal();
1001 let secs = value.num_seconds_from_midnight();
1002 let nano = value.nanosecond();
1003
1004 buf[..4].copy_from_slice(&year.to_be_bytes());
1005 buf[4..8].copy_from_slice(&ordinal.to_be_bytes());
1006 buf[8..12].copy_from_slice(&secs.to_be_bytes());
1007 buf[12..].copy_from_slice(&nano.to_be_bytes());
1008
1009 PackedNaiveDateTime(buf)
1010 }
1011
1012 #[inline]
1013 fn into_value(self) -> NaiveDateTime {
1014 let mut year = [0u8; 4];
1015 year.copy_from_slice(&self.0[..4]);
1016 let year = u32::from_be_bytes(year) ^ 0x8000_0000u32;
1017
1018 let mut ordinal = [0u8; 4];
1019 ordinal.copy_from_slice(&self.0[4..8]);
1020 let ordinal = u32::from_be_bytes(ordinal);
1021
1022 let mut secs = [0u8; 4];
1023 secs.copy_from_slice(&self.0[8..12]);
1024 let secs = u32::from_be_bytes(secs);
1025
1026 let mut nano = [0u8; 4];
1027 nano.copy_from_slice(&self.0[12..]);
1028 let nano = u32::from_be_bytes(nano);
1029
1030 let date = NaiveDate::from_yo_opt(year as i32, ordinal)
1031 .expect("NaiveDate roundtrips with PackedNaiveDateTime");
1032 let time = NaiveTime::from_num_seconds_from_midnight_opt(secs, nano)
1033 .expect("NaiveTime roundtrips with PackedNaiveDateTime");
1034
1035 NaiveDateTime::new(date, time)
1036 }
1037}
1038
1039#[cfg(test)]
1040mod test {
1041 use super::*;
1042 use itertools::Itertools;
1043 use mz_ore::assert_err;
1044 use proptest::prelude::*;
1045
1046 #[mz_ore::test]
1047 fn test_max_age() {
1048 let low = CheckedTimestamp::try_from(
1049 LOW_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1050 )
1051 .unwrap();
1052 let high = CheckedTimestamp::try_from(
1053 HIGH_DATE.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1054 )
1055 .unwrap();
1056
1057 let years = HIGH_DATE.year() - LOW_DATE.year();
1058 let months = years * 12;
1059
1060 let result = high.age(&low).unwrap();
1062 assert_eq!(result, Interval::new(months, 0, 0));
1063
1064 let result = low.age(&high).unwrap();
1066 assert_eq!(result, Interval::new(-months, 0, 0));
1067 }
1068
1069 fn assert_round_to_precision(
1070 dt: CheckedTimestamp<NaiveDateTime>,
1071 precision: u8,
1072 expected: i64,
1073 ) {
1074 let updated = dt
1075 .round_to_precision(Some(TimestampPrecision(precision)))
1076 .unwrap();
1077 assert_eq!(expected, updated.and_utc().timestamp_micros());
1078 }
1079
1080 #[mz_ore::test]
1081 fn test_round_to_precision() {
1082 let date = CheckedTimestamp::try_from(
1083 NaiveDate::from_ymd_opt(1970, 1, 1)
1084 .unwrap()
1085 .and_hms_nano_opt(0, 0, 0, 123456789)
1086 .unwrap(),
1087 )
1088 .unwrap();
1089 assert_round_to_precision(date, 0, 0);
1090 assert_round_to_precision(date, 1, 100000);
1091 assert_round_to_precision(date, 2, 120000);
1092 assert_round_to_precision(date, 3, 123000);
1093 assert_round_to_precision(date, 4, 123500);
1094 assert_round_to_precision(date, 5, 123460);
1095 assert_round_to_precision(date, 6, 123457);
1096
1097 let low =
1098 CheckedTimestamp::try_from(LOW_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1099 .unwrap();
1100 assert_round_to_precision(low, 0, -210863606400000000);
1101 assert_round_to_precision(low, 1, -210863606399900000);
1102 assert_round_to_precision(low, 2, -210863606399880000);
1103 assert_round_to_precision(low, 3, -210863606399877000);
1104 assert_round_to_precision(low, 4, -210863606399876500);
1105 assert_round_to_precision(low, 5, -210863606399876540);
1106 assert_round_to_precision(low, 6, -210863606399876543);
1107
1108 let high =
1109 CheckedTimestamp::try_from(HIGH_DATE.and_hms_nano_opt(0, 0, 0, 123456789).unwrap())
1110 .unwrap();
1111 assert_round_to_precision(high, 0, 8210266790400000000);
1112 assert_round_to_precision(high, 1, 8210266790400100000);
1113 assert_round_to_precision(high, 2, 8210266790400120000);
1114 assert_round_to_precision(high, 3, 8210266790400123000);
1115 assert_round_to_precision(high, 4, 8210266790400123500);
1116 assert_round_to_precision(high, 5, 8210266790400123460);
1117 assert_round_to_precision(high, 6, 8210266790400123457);
1118 }
1119
1120 #[mz_ore::test]
1121 fn test_precision_edge_cases() {
1122 #[allow(clippy::disallowed_methods)] let result = std::panic::catch_unwind(|| {
1124 let date = CheckedTimestamp::try_from(
1125 DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1126 )
1127 .unwrap();
1128 let _ = date.round_to_precision(Some(TimestampPrecision(7)));
1129 });
1130 assert_err!(result);
1131
1132 let date = CheckedTimestamp::try_from(
1133 DateTime::from_timestamp_micros(123456).unwrap().naive_utc(),
1134 )
1135 .unwrap();
1136 let date = date.round_to_precision(None).unwrap();
1137 assert_eq!(123456, date.and_utc().timestamp_micros());
1138 }
1139
1140 #[mz_ore::test]
1141 fn test_equality_with_same_precision() {
1142 let date1 =
1143 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456).unwrap()).unwrap();
1144 let date1 = date1
1145 .round_to_precision(Some(TimestampPrecision(0)))
1146 .unwrap();
1147
1148 let date2 =
1149 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1150 let date2 = date2
1151 .round_to_precision(Some(TimestampPrecision(0)))
1152 .unwrap();
1153 assert_eq!(date1, date2);
1154 }
1155
1156 #[mz_ore::test]
1157 fn test_equality_with_different_precisions() {
1158 let date1 =
1159 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123500000).unwrap()).unwrap();
1160 let date1 = date1
1161 .round_to_precision(Some(TimestampPrecision(5)))
1162 .unwrap();
1163
1164 let date2 =
1165 CheckedTimestamp::try_from(DateTime::from_timestamp(0, 123456789).unwrap()).unwrap();
1166 let date2 = date2
1167 .round_to_precision(Some(TimestampPrecision(4)))
1168 .unwrap();
1169 assert_eq!(date1, date2);
1170 }
1171
1172 proptest! {
1173 #[mz_ore::test]
1174 #[cfg_attr(miri, ignore)] fn test_age_naive(a: CheckedTimestamp<NaiveDateTime>, b: CheckedTimestamp<NaiveDateTime>) {
1176 let result = a.age(&b);
1177 prop_assert!(result.is_ok());
1178 }
1179
1180 #[mz_ore::test]
1181 #[cfg_attr(miri, ignore)] fn test_age_utc(a: CheckedTimestamp<DateTime<Utc>>, b: CheckedTimestamp<DateTime<Utc>>) {
1183 let result = a.age(&b);
1184 prop_assert!(result.is_ok());
1185 }
1186 }
1187
1188 #[mz_ore::test]
1189 fn proptest_packed_naive_date_time_roundtrips() {
1190 proptest!(|(timestamp in arb_naive_date_time())| {
1191 let packed = PackedNaiveDateTime::from_value(timestamp);
1192 let rnd = packed.into_value();
1193 prop_assert_eq!(timestamp, rnd);
1194 });
1195 }
1196
1197 #[mz_ore::test]
1198 fn proptest_packed_naive_date_time_sort_order() {
1199 let strat = proptest::collection::vec(arb_naive_date_time(), 0..128);
1200 proptest!(|(mut times in strat)| {
1201 let mut packed: Vec<_> = times
1202 .iter()
1203 .copied()
1204 .map(PackedNaiveDateTime::from_value)
1205 .collect();
1206
1207 times.sort();
1208 packed.sort();
1209
1210 for (time, packed) in times.into_iter().zip_eq(packed.into_iter()) {
1211 let rnd = packed.into_value();
1212 prop_assert_eq!(time, rnd);
1213 }
1214 });
1215 }
1216}