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