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