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