Skip to main content

mz_repr/adt/
interval.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! A time interval abstract data type.
11
12use std::fmt::{self, Write};
13use std::sync::LazyLock;
14use std::time::Duration;
15
16use anyhow::{anyhow, bail};
17use mz_persist_types::columnar::FixedSizeCodec;
18use mz_proto::{RustType, TryFromProtoError};
19use num_traits::CheckedMul;
20#[cfg(any(test, feature = "proptest"))]
21use proptest::prelude::{Arbitrary, BoxedStrategy, Strategy, any};
22use serde::{Deserialize, Serialize};
23
24use crate::adt::datetime::DateTimeField;
25use crate::adt::numeric::{DecimalLike, Numeric};
26
27include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.interval.rs"));
28
29/// An interval of time meant to express SQL intervals.
30///
31/// Obtained by parsing an `INTERVAL '<value>' <unit> [TO <precision>]`.
32#[derive(
33    Debug,
34    Clone,
35    Copy,
36    PartialEq,
37    Eq,
38    PartialOrd,
39    Ord,
40    Serialize,
41    Hash,
42    Deserialize
43)]
44pub struct Interval {
45    /// A possibly negative number of months for field types like `YEAR`
46    pub months: i32,
47    /// A possibly negative number of days.
48    ///
49    /// Irrespective of values, `days` will not be carried over into `months`.
50    pub days: i32,
51    /// A timespan represented in microseconds.
52    ///
53    /// Irrespective of values, `micros` will not be carried over into `days` or
54    /// `months`.
55    pub micros: i64,
56}
57
58impl Default for Interval {
59    fn default() -> Self {
60        Self {
61            months: 0,
62            days: 0,
63            micros: 0,
64        }
65    }
66}
67
68impl RustType<ProtoInterval> for Interval {
69    fn into_proto(&self) -> ProtoInterval {
70        ProtoInterval {
71            months: self.months,
72            days: self.days,
73            micros: self.micros,
74        }
75    }
76
77    fn from_proto(proto: ProtoInterval) -> Result<Self, TryFromProtoError> {
78        Ok(Interval {
79            months: proto.months,
80            days: proto.days,
81            micros: proto.micros,
82        })
83    }
84}
85
86impl num_traits::ops::checked::CheckedNeg for Interval {
87    fn checked_neg(&self) -> Option<Self> {
88        if let (Some(months), Some(days), Some(micros)) = (
89            self.months.checked_neg(),
90            self.days.checked_neg(),
91            self.micros.checked_neg(),
92        ) {
93            Some(Self::new(months, days, micros))
94        } else {
95            None
96        }
97    }
98}
99
100impl std::str::FromStr for Interval {
101    type Err = anyhow::Error;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        crate::strconv::parse_interval(s).map_err(|e| anyhow!(e))
105    }
106}
107
108static MONTH_OVERFLOW_ERROR: LazyLock<String> = LazyLock::new(|| {
109    format!(
110        "Overflows maximum months; cannot exceed {}/{} microseconds",
111        i32::MAX,
112        i32::MIN,
113    )
114});
115static DAY_OVERFLOW_ERROR: LazyLock<String> = LazyLock::new(|| {
116    format!(
117        "Overflows maximum days; cannot exceed {}/{} microseconds",
118        i32::MAX,
119        i32::MIN,
120    )
121});
122pub static USECS_PER_DAY: LazyLock<i64> = LazyLock::new(|| {
123    Interval::convert_date_time_unit(DateTimeField::Day, DateTimeField::Microseconds, 1i64).unwrap()
124});
125
126#[derive(Debug, Clone)]
127pub enum RoundBehavior {
128    Truncate,
129    Nearest,
130}
131
132impl Interval {
133    pub const CENTURY_PER_MILLENNIUM: u16 = 10;
134    pub const DECADE_PER_CENTURY: u16 = 10;
135    pub const YEAR_PER_DECADE: u16 = 10;
136    pub const MONTH_PER_YEAR: u16 = 12;
137    // Interval type considers 30 days == 1 month
138    pub const DAY_PER_MONTH: u16 = 30;
139    // Interval type considers 24 hours == 1 day
140    pub const HOUR_PER_DAY: u16 = 24;
141    pub const MINUTE_PER_HOUR: u16 = 60;
142    pub const SECOND_PER_MINUTE: u16 = 60;
143    pub const MILLISECOND_PER_SECOND: u16 = 1_000;
144    pub const MICROSECOND_PER_MILLISECOND: u16 = 1_000;
145    pub const NANOSECOND_PER_MICROSECOND: u16 = 1_000;
146    // PostgreSQL actually has a bug where when using EXTRACT it truncates this value to 365, but
147    // when using date_part it does not truncate this value. Therefore our EXTRACT function may differ
148    // from PostgreSQL.
149    // EXTRACT: https://github.com/postgres/postgres/blob/c2e8bd27519f47ff56987b30eb34a01969b9a9e8/src/backend/utils/adt/timestamp.c#L5270-L5273
150    // date_part: https://github.com/postgres/postgres/blob/c2e8bd27519f47ff56987b30eb34a01969b9a9e8/src/backend/utils/adt/timestamp.c#L5301
151    pub const EPOCH_DAYS_PER_YEAR: f64 = 365.25;
152
153    /// Constructs a new `Interval` with the specified units of time.
154    pub const fn new(months: i32, days: i32, micros: i64) -> Interval {
155        Interval {
156            months,
157            days,
158            micros,
159        }
160    }
161
162    /// Converts a `Duration` to an `Interval`. The resulting `Interval` will only have
163    /// microseconds. Errors if
164    /// - the number of microseconds doesn't fit in i64, or
165    /// - the `Duration` involves fractional microseconds.
166    pub fn from_duration(duration: &Duration) -> Result<Interval, anyhow::Error> {
167        if duration.subsec_nanos() % 1000 != 0 {
168            return Err(anyhow!(
169                "cannot convert Duration to Interval due to fractional microseconds"
170            ));
171        }
172        Ok(Interval {
173            months: 0,
174            days: 0,
175            micros: duration.as_micros().try_into()?,
176        })
177    }
178
179    /// Converts a `chrono::Duration` to an `Interval`. The resulting `Interval` will only have
180    /// microseconds, with the nanoseconds truncated.
181    pub fn from_chrono_duration(duration: chrono::Duration) -> Result<Self, anyhow::Error> {
182        let Some(micros) = duration.num_microseconds() else {
183            bail!("cannot convert Duration to Interval due to overflowed microseconds");
184        };
185        Ok(Self {
186            months: 0,
187            days: 0,
188            micros,
189        })
190    }
191
192    pub fn checked_add(&self, other: &Self) -> Option<Self> {
193        let months = match self.months.checked_add(other.months) {
194            Some(m) => m,
195            None => return None,
196        };
197        let days = match self.days.checked_add(other.days) {
198            Some(d) => d,
199            None => return None,
200        };
201        let micros = match self.micros.checked_add(other.micros) {
202            Some(us) => us,
203            None => return None,
204        };
205
206        Some(Self::new(months, days, micros))
207    }
208
209    pub fn checked_mul(&self, other: f64) -> Option<Self> {
210        self.checked_op(other, |f1, f2| f1 * f2)
211    }
212
213    pub fn checked_div(&self, other: f64) -> Option<Self> {
214        self.checked_op(other, |f1, f2| f1 / f2)
215    }
216
217    // TODO(benesch): the use of `as` in this function looks very sketchy.
218    // Rewrite.
219    #[allow(clippy::as_conversions)]
220    fn checked_op<F1>(&self, other: f64, op: F1) -> Option<Self>
221    where
222        F1: Fn(f64, f64) -> f64,
223    {
224        let months = op(f64::from(self.months), other);
225        if months.is_nan()
226            || months.is_infinite()
227            || months < i32::MIN.into()
228            || months > i32::MAX.into()
229        {
230            return None;
231        }
232
233        let days =
234            op(f64::from(self.days), other) + months.fract() * f64::from(Self::DAY_PER_MONTH);
235        if days.is_nan() || days.is_infinite() || days < i32::MIN.into() || days > i32::MAX.into() {
236            return None;
237        }
238
239        let micros = op(self.micros as f64, other)
240            + days.fract()
241                * f64::from(Self::HOUR_PER_DAY)
242                * f64::from(Self::MINUTE_PER_HOUR)
243                * f64::from(Self::SECOND_PER_MINUTE)
244                * f64::from(Self::MILLISECOND_PER_SECOND)
245                * f64::from(Self::MICROSECOND_PER_MILLISECOND);
246
247        if micros.is_nan()
248            || micros.is_infinite()
249            || Numeric::from(micros) < Numeric::from(i64::MIN)
250            || Numeric::from(micros) > Numeric::from(i64::MAX)
251        {
252            return None;
253        }
254
255        Some(Self::new(months as i32, days as i32, micros as i64))
256    }
257
258    /// Computes the millennium part of the interval.
259    ///
260    /// The millennium part is the number of whole millennia in the interval. For example,
261    /// this function returns `3` for the interval `3400 years`.
262    pub fn millennia(&self) -> i32 {
263        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Millennium, self.months)
264            .unwrap()
265    }
266
267    /// Computes the century part of the interval.
268    ///
269    /// The century part is the number of whole centuries in the interval. For example,
270    /// this function returns `3` for the interval `340 years`.
271    pub fn centuries(&self) -> i32 {
272        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Century, self.months)
273            .unwrap()
274    }
275
276    /// Computes the decade part of the interval.
277    ///
278    /// The decade part is the number of whole decades in the interval. For example,
279    /// this function returns `3` for the interval `34 years`.
280    pub fn decades(&self) -> i32 {
281        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Decade, self.months)
282            .unwrap()
283    }
284
285    /// Computes the year part of the interval.
286    ///
287    /// The year part is the number of whole years in the interval. For example,
288    /// this function returns `3` for the interval `3 years 4 months`.
289    pub fn years(&self) -> i32 {
290        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Year, self.months)
291            .unwrap()
292    }
293
294    /// Computes the quarter part of the interval.
295    ///
296    /// The quarter part is obtained from taking the number of whole months modulo 12,
297    /// and assigning quarter #1 for months 0-2, #2 for 3-5, #3 for 6-8 and #4 for 9-11.
298    /// For example, this function returns `4` for the interval `11 months`.
299    pub fn quarters(&self) -> i32 {
300        self.months() / 3 + 1
301    }
302
303    /// Computes the month part of the interval.
304    ///
305    /// The month part is the number of whole months in the interval, modulo 12.
306    /// For example, this function returns `4` for the interval `3 years 4
307    /// months`.
308    pub fn months(&self) -> i32 {
309        self.months % i32::from(Self::MONTH_PER_YEAR)
310    }
311
312    /// Computes the day part of the interval.
313    ///
314    /// The day part is the number of whole days in the interval. For example,
315    /// this function returns `5` for the interval `5 days 4 hours 3 minutes
316    /// 2.1 seconds`.
317    pub fn days(&self) -> i64 {
318        self.days.into()
319    }
320
321    /// Computes the hour part of the interval.
322    ///
323    /// The hour part is the number of whole hours in the interval, modulo 24.
324    /// For example, this function returns `4` for the interval `5 days 4
325    /// hours 3 minutes 2.1 seconds`.
326    pub fn hours(&self) -> i64 {
327        Self::convert_date_time_unit(
328            DateTimeField::Microseconds,
329            DateTimeField::Hour,
330            self.micros,
331        )
332        .unwrap()
333            % i64::from(Self::HOUR_PER_DAY)
334    }
335
336    /// Computes the minute part of the interval.
337    ///
338    /// The minute part is the number of whole minutes in the interval, modulo
339    /// 60. For example, this function returns `3` for the interval `5 days 4
340    /// hours 3 minutes 2.1 seconds`.
341    pub fn minutes(&self) -> i64 {
342        Self::convert_date_time_unit(
343            DateTimeField::Microseconds,
344            DateTimeField::Minute,
345            self.micros,
346        )
347        .unwrap()
348            % i64::from(Self::MINUTE_PER_HOUR)
349    }
350
351    /// Computes the second part of the interval.
352    ///
353    /// The second part is the number of fractional seconds in the interval,
354    /// modulo 60.0.
355    pub fn seconds<T>(&self) -> T
356    where
357        T: DecimalLike,
358    {
359        T::lossy_from(self.micros % 60_000_000) / T::from(1e6)
360    }
361
362    /// Computes the second part of the interval displayed in milliseconds.
363    ///
364    /// The second part is the number of fractional seconds in the interval,
365    /// modulo 60.0.
366    pub fn milliseconds<T>(&self) -> T
367    where
368        T: DecimalLike,
369    {
370        T::lossy_from(self.micros % 60_000_000) / T::from(1e3)
371    }
372
373    /// Computes the second part of the interval displayed in microseconds.
374    ///
375    /// The second part is the number of fractional seconds in the interval,
376    /// modulo 60.0.
377    pub fn microseconds<T>(&self) -> T
378    where
379        T: DecimalLike,
380    {
381        T::lossy_from(self.micros % 60_000_000)
382    }
383
384    /// Computes the nanosecond part of the interval.
385    pub fn nanoseconds(&self) -> i32 {
386        (self.micros % 1_000_000 * 1_000).try_into().unwrap()
387    }
388
389    /// Computes the total number of epoch seconds in the interval.
390    /// When extracting an epoch, PostgreSQL considers a year
391    /// 365.25 days.
392    pub fn as_epoch_seconds<T>(&self) -> T
393    where
394        T: DecimalLike,
395    {
396        let days = T::from(self.years()) * T::from(Self::EPOCH_DAYS_PER_YEAR)
397            + T::from(self.months()) * T::from(Self::DAY_PER_MONTH)
398            + T::from(self.days);
399        let seconds = days
400            * T::from(Self::HOUR_PER_DAY)
401            * T::from(Self::MINUTE_PER_HOUR)
402            * T::from(Self::SECOND_PER_MINUTE);
403
404        seconds
405            + T::lossy_from(self.micros)
406                / (T::from(Self::MICROSECOND_PER_MILLISECOND)
407                    * T::from(Self::MILLISECOND_PER_SECOND))
408    }
409
410    /// Computes the total number of microseconds in the interval.
411    pub fn as_microseconds(&self) -> i128 {
412        // unwrap is safe because i32::MAX/i32::MIN number of months will not overflow an i128 when
413        // converted to microseconds.
414        Self::convert_date_time_unit(
415            DateTimeField::Month,
416            DateTimeField::Microseconds,
417            i128::from(self.months),
418        ).unwrap() +
419        // unwrap is safe because i32::MAX/i32::MIN number of days will not overflow an i128 when
420        // converted to microseconds.
421        Self::convert_date_time_unit(
422            DateTimeField::Day,
423            DateTimeField::Microseconds,
424            i128::from(self.days),
425        ).unwrap() +
426        i128::from(self.micros)
427    }
428
429    /// Computes the total number of milliseconds in the interval. Discards fractional milliseconds!
430    pub fn as_milliseconds(&self) -> i128 {
431        self.as_microseconds() / 1000
432    }
433
434    /// Converts this `Interval`'s duration into `chrono::Duration`.
435    pub fn duration_as_chrono(&self) -> chrono::Duration {
436        use chrono::Duration;
437        Duration::try_days(self.days.into()).unwrap() + Duration::microseconds(self.micros)
438    }
439
440    pub fn duration(&self) -> Result<Duration, anyhow::Error> {
441        if self.months != 0 {
442            bail!("cannot convert interval with months to duration");
443        }
444        if self.is_negative() {
445            bail!("cannot convert negative interval to duration");
446        }
447        // Phrase the overflow ourselves rather than letting `TryFromIntError`
448        // through. Its `Display` is std's, and std has reworded it between
449        // toolchains, which would otherwise rewrite a user-facing error message
450        // out from under us.
451        let Ok(micros) = u64::try_from(self.as_microseconds()) else {
452            bail!("interval is too large to convert to duration");
453        };
454        Ok(Duration::from_micros(micros))
455    }
456
457    /// Truncate the "tail" of the interval, removing all time units less than `f`.
458    /// # Arguments
459    /// - `f`: Round the interval down to the specified time unit.
460    /// - `fsec_max_precision`: If `Some(x)`, keep only `x` places of microsecond precision.
461    ///    Must be `(0,6)`.
462    ///
463    /// # Errors
464    /// - If `fsec_max_precision` is not None or within (0,6).
465    pub fn truncate_low_fields(
466        &mut self,
467        f: DateTimeField,
468        fsec_max_precision: Option<u64>,
469        round_behavior: RoundBehavior,
470    ) -> Result<(), anyhow::Error> {
471        use DateTimeField::*;
472        match f {
473            Millennium => {
474                self.months -= self.months % (12 * 1000);
475                self.days = 0;
476                self.micros = 0;
477            }
478            Century => {
479                self.months -= self.months % (12 * 100);
480                self.days = 0;
481                self.micros = 0;
482            }
483            Decade => {
484                self.months -= self.months % (12 * 10);
485                self.days = 0;
486                self.micros = 0;
487            }
488            Year => {
489                self.months -= self.months % 12;
490                self.days = 0;
491                self.micros = 0;
492            }
493            Month => {
494                self.days = 0;
495                self.micros = 0;
496            }
497            // Round microseconds.
498            Second => {
499                let default_precision = 6;
500                let precision = match fsec_max_precision {
501                    Some(p) => p,
502                    None => default_precision,
503                };
504
505                if precision > default_precision {
506                    bail!(
507                        "SECOND precision must be (0, 6), have SECOND({})",
508                        precision
509                    )
510                }
511
512                let precision = match u32::try_from(precision) {
513                    Ok(p) => p,
514                    Err(_) => bail!(
515                        "SECOND precision must be (0, 6), have SECOND({})",
516                        precision
517                    ),
518                };
519                // Truncate sub-second part.
520                let remainder = self.micros % 10_i64.pow(6 - precision);
521                self.micros -= remainder;
522                // Check if value should round up/down to nearest fractional place.
523                if matches!(round_behavior, RoundBehavior::Nearest)
524                    && u64::from(precision) != default_precision
525                {
526                    let rounding_digit = remainder / 10_i64.pow(5 - precision);
527                    let micros = if rounding_digit > 4 {
528                        self.micros.checked_add(10_i64.pow(6 - precision))
529                    } else if rounding_digit < -4 {
530                        self.micros.checked_sub(10_i64.pow(6 - precision))
531                    } else {
532                        Some(self.micros)
533                    };
534                    let Some(micros) = micros else {
535                        bail!("interval field value out of range: \"{self}\"");
536                    };
537                    self.micros = micros;
538                }
539            }
540            Day => {
541                self.micros = 0;
542            }
543            Hour | Minute | Milliseconds | Microseconds => {
544                self.micros -= self.micros % f.micros_multiplier();
545            }
546        }
547        Ok(())
548    }
549
550    /// Returns a new Interval with only the time component
551    pub fn as_time_interval(&self) -> Self {
552        Self::new(0, 0, self.micros)
553    }
554
555    /// Returns true if combining all fields results in a negative number, false otherwise
556    pub fn is_negative(&self) -> bool {
557        self.as_microseconds() < 0
558    }
559
560    /// Convert val from source unit to dest unit. Does not maintain fractional values.
561    /// Returns None if the result overflows/underflows.
562    ///
563    /// WARNING: Due to the fact that Intervals consider months to have 30 days, you may get
564    /// unexpected and incorrect results when trying to convert from a non-year type to a year type
565    /// and vice versa. For example from years to days.
566    pub fn convert_date_time_unit<T>(
567        source: DateTimeField,
568        dest: DateTimeField,
569        val: T,
570    ) -> Option<T>
571    where
572        T: From<u16> + CheckedMul + std::ops::DivAssign,
573    {
574        if source < dest {
575            Self::convert_date_time_unit_increasing(source, dest, val)
576        } else if source > dest {
577            Self::convert_date_time_unit_decreasing(source, dest, val)
578        } else {
579            Some(val)
580        }
581    }
582
583    fn convert_date_time_unit_increasing<T>(
584        source: DateTimeField,
585        dest: DateTimeField,
586        val: T,
587    ) -> Option<T>
588    where
589        T: From<u16> + std::ops::DivAssign,
590    {
591        let mut cur_unit = source;
592        let mut res = val;
593        while cur_unit < dest {
594            let divisor: T = match cur_unit {
595                DateTimeField::Millennium => 1.into(),
596                DateTimeField::Century => Self::CENTURY_PER_MILLENNIUM.into(),
597                DateTimeField::Decade => Self::DECADE_PER_CENTURY.into(),
598                DateTimeField::Year => Self::YEAR_PER_DECADE.into(),
599                DateTimeField::Month => Self::MONTH_PER_YEAR.into(),
600                DateTimeField::Day => Self::DAY_PER_MONTH.into(),
601                DateTimeField::Hour => Self::HOUR_PER_DAY.into(),
602                DateTimeField::Minute => Self::MINUTE_PER_HOUR.into(),
603                DateTimeField::Second => Self::SECOND_PER_MINUTE.into(),
604                DateTimeField::Milliseconds => Self::MILLISECOND_PER_SECOND.into(),
605                DateTimeField::Microseconds => Self::MICROSECOND_PER_MILLISECOND.into(),
606            };
607            res /= divisor;
608            cur_unit = cur_unit.next_largest();
609        }
610
611        Some(res)
612    }
613
614    fn convert_date_time_unit_decreasing<T>(
615        source: DateTimeField,
616        dest: DateTimeField,
617        val: T,
618    ) -> Option<T>
619    where
620        T: From<u16> + CheckedMul,
621    {
622        let mut cur_unit = source;
623        let mut res = val;
624        while cur_unit > dest {
625            let multiplier: T = match cur_unit {
626                DateTimeField::Millennium => Self::CENTURY_PER_MILLENNIUM.into(),
627                DateTimeField::Century => Self::DECADE_PER_CENTURY.into(),
628                DateTimeField::Decade => Self::YEAR_PER_DECADE.into(),
629                DateTimeField::Year => Self::MONTH_PER_YEAR.into(),
630                DateTimeField::Month => Self::DAY_PER_MONTH.into(),
631                DateTimeField::Day => Self::HOUR_PER_DAY.into(),
632                DateTimeField::Hour => Self::MINUTE_PER_HOUR.into(),
633                DateTimeField::Minute => Self::SECOND_PER_MINUTE.into(),
634                DateTimeField::Second => Self::MILLISECOND_PER_SECOND.into(),
635                DateTimeField::Milliseconds => Self::MICROSECOND_PER_MILLISECOND.into(),
636                DateTimeField::Microseconds => 1.into(),
637            };
638            res = match res.checked_mul(&multiplier) {
639                Some(r) => r,
640                None => return None,
641            };
642            cur_unit = cur_unit.next_smallest();
643        }
644
645        Some(res)
646    }
647
648    /// Adjust interval so 'days' contains less than 30 days, adding the excess to 'months'.
649    pub fn justify_days(&self) -> Result<Self, anyhow::Error> {
650        let days_per_month = i32::from(Self::DAY_PER_MONTH);
651        let (mut months, mut days) = Self::justify_days_inner(self.months, self.days)?;
652        if months > 0 && days < 0 {
653            days += days_per_month;
654            months -= 1;
655        } else if months < 0 && days > 0 {
656            days -= days_per_month;
657            months += 1;
658        }
659
660        Ok(Self::new(months, days, self.micros))
661    }
662
663    fn justify_days_inner(months: i32, days: i32) -> Result<(i32, i32), anyhow::Error> {
664        let days_per_month = i32::from(Self::DAY_PER_MONTH);
665        let whole_month = days / days_per_month;
666        let days = days - whole_month * days_per_month;
667
668        let months = months
669            .checked_add(whole_month)
670            .ok_or_else(|| anyhow!(&*MONTH_OVERFLOW_ERROR))?;
671
672        Ok((months, days))
673    }
674
675    /// Adjust interval so 'micros' contains less than a whole day, adding the excess to 'days'.
676    pub fn justify_hours(&self) -> Result<Self, anyhow::Error> {
677        let (mut days, mut micros) = Self::justify_hours_inner(self.days, self.micros)?;
678        if days > 0 && micros < 0 {
679            micros += &*USECS_PER_DAY;
680            days -= 1;
681        } else if days < 0 && micros > 0 {
682            micros -= &*USECS_PER_DAY;
683            days += 1;
684        }
685
686        Ok(Self::new(self.months, days, micros))
687    }
688
689    fn justify_hours_inner(days: i32, micros: i64) -> Result<(i32, i64), anyhow::Error> {
690        let days = i32::try_from(micros / &*USECS_PER_DAY)
691            .ok()
692            .and_then(|d| days.checked_add(d))
693            .ok_or_else(|| anyhow!(&*DAY_OVERFLOW_ERROR))?;
694        let micros = micros % &*USECS_PER_DAY;
695
696        Ok((days, micros))
697    }
698
699    /// Adjust interval so 'days' contains less than 30 days, adding the excess to 'months'.
700    /// Adjust interval so 'micros' contains less than a whole day, adding the excess to 'days'.
701    /// Also, the sign bit on all three fields is made equal, so either all three fields are negative or all are positive.
702    pub fn justify_interval(&self) -> Result<Self, anyhow::Error> {
703        let days_per_month = i32::from(Self::DAY_PER_MONTH);
704        let mut months = self.months;
705        let mut days = self.days;
706        let micros = self.micros;
707        // We justify days twice to try to avoid an intermediate overflow of days if it would be
708        // able to fit in months.
709        if (days > 0 && micros > 0) || (days < 0 && micros < 0) {
710            let (m, d) = Self::justify_days_inner(self.months, self.days)?;
711            months = m;
712            days = d;
713        }
714        let (days, mut micros) = Self::justify_hours_inner(days, micros)?;
715        let (mut months, mut days) = Self::justify_days_inner(months, days)?;
716
717        if months > 0 && (days < 0 || (days == 0 && micros < 0)) {
718            days += days_per_month;
719            months -= 1;
720        } else if months < 0 && (days > 0 || (days == 0 && micros > 0)) {
721            days -= days_per_month;
722            months += 1;
723        }
724
725        if days > 0 && micros < 0 {
726            micros += &*USECS_PER_DAY;
727            days -= 1;
728        } else if days < 0 && micros > 0 {
729            micros -= &*USECS_PER_DAY;
730            days += 1;
731        }
732
733        Ok(Self::new(months, days, micros))
734    }
735}
736
737/// Format an interval in a human form
738///
739/// Example outputs:
740///
741/// * 1 year 2 mons 5 days 03:04:00
742/// * -1 year +5 days +18:59:29.3
743/// * 00:00:00
744impl fmt::Display for Interval {
745    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
746        let neg_months = self.months < 0;
747        let years = (self.months / 12).abs();
748        let months = (self.months % 12).abs();
749
750        let neg_days = self.days < 0;
751        let days = i64::from(self.days).abs();
752
753        let mut nanos = self.nanoseconds().abs();
754        let mut secs = (self.micros / 1_000_000).abs();
755
756        let sec_per_hr = 60 * 60;
757        let hours = secs / sec_per_hr;
758        secs %= sec_per_hr;
759
760        let sec_per_min = 60;
761        let minutes = secs / sec_per_min;
762        secs %= sec_per_min;
763
764        if years > 0 {
765            if neg_months {
766                f.write_char('-')?;
767            }
768            write!(f, "{} year", years)?;
769            if years > 1 || neg_months {
770                f.write_char('s')?;
771            }
772        }
773
774        if months > 0 {
775            if years != 0 {
776                f.write_char(' ')?;
777            }
778            if neg_months {
779                f.write_char('-')?;
780            }
781            write!(f, "{} mon", months)?;
782            if months > 1 || neg_months {
783                f.write_char('s')?;
784            }
785        }
786
787        if days != 0 {
788            if years > 0 || months > 0 {
789                f.write_char(' ')?;
790            }
791            if neg_months && !neg_days {
792                f.write_char('+')?;
793            }
794            write!(f, "{} day", self.days)?;
795            if self.days != 1 {
796                f.write_char('s')?;
797            }
798        }
799
800        let non_zero_hmsn = hours > 0 || minutes > 0 || secs > 0 || nanos > 0;
801
802        if (years == 0 && months == 0 && days == 0) || non_zero_hmsn {
803            if years > 0 || months > 0 || days > 0 {
804                f.write_char(' ')?;
805            }
806            if self.micros < 0 && non_zero_hmsn {
807                f.write_char('-')?;
808            } else if neg_days || (days == 0 && neg_months) {
809                f.write_char('+')?;
810            }
811            write!(f, "{:02}:{:02}:{:02}", hours, minutes, secs)?;
812            if nanos > 0 {
813                let mut width = 9;
814                while nanos % 10 == 0 {
815                    width -= 1;
816                    nanos /= 10;
817                }
818                write!(f, ".{:0width$}", nanos, width = width)?;
819            }
820        }
821
822        Ok(())
823    }
824}
825
826#[cfg(any(test, feature = "proptest"))]
827impl Arbitrary for Interval {
828    type Strategy = BoxedStrategy<Self>;
829    type Parameters = ();
830
831    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
832        (
833            any::<i32>(),
834            any::<i32>(),
835            ((((i64::from(i32::MIN) * 60) - 59) * 60) * 1_000_000 - 59_999_999
836                ..(((i64::from(i32::MAX) * 60) + 59) * 60) * 1_000_000 + 59_999_999),
837        )
838            .prop_map(|(months, days, micros)| Interval {
839                months,
840                days,
841                micros,
842            })
843            .boxed()
844    }
845}
846
847/// An encoded packed variant of [`Interval`].
848///
849/// We uphold the variant that [`PackedInterval`] sorts the same as [`Interval`].
850#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
851pub struct PackedInterval([u8; Self::SIZE]);
852
853// `as` conversions are okay here because we're doing bit level logic to make
854// sure the sort order of the packed binary is correct. This is implementation
855// is proptest-ed below.
856#[allow(clippy::as_conversions)]
857impl FixedSizeCodec<Interval> for PackedInterval {
858    const SIZE: usize = 16;
859
860    fn as_bytes(&self) -> &[u8] {
861        &self.0[..]
862    }
863
864    fn from_bytes(slice: &[u8]) -> Result<Self, String> {
865        let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
866            format!(
867                "size for PackedInterval is {} bytes, got {}",
868                Self::SIZE,
869                slice.len()
870            )
871        })?;
872        Ok(PackedInterval(buf))
873    }
874
875    #[inline]
876    fn from_value(value: Interval) -> Self {
877        let mut buf = [0u8; 16];
878
879        // Note: We XOR the values to get correct sorting of negative values.
880
881        let months = (value.months as u32) ^ (0x8000_0000u32);
882        let days = (value.days as u32) ^ (0x8000_0000u32);
883        let micros = (value.micros as u64) ^ (0x8000_0000_0000_0000u64);
884
885        buf[..4].copy_from_slice(&months.to_be_bytes());
886        buf[4..8].copy_from_slice(&days.to_be_bytes());
887        buf[8..].copy_from_slice(&micros.to_be_bytes());
888
889        PackedInterval(buf)
890    }
891
892    #[inline]
893    fn into_value(self) -> Interval {
894        // Note: We XOR the values to get correct sorting of negative values.
895
896        let mut months = [0; 4];
897        months.copy_from_slice(&self.0[..4]);
898        let months = u32::from_be_bytes(months) ^ 0x8000_0000u32;
899
900        let mut days = [0; 4];
901        days.copy_from_slice(&self.0[4..8]);
902        let days = u32::from_be_bytes(days) ^ 0x8000_0000u32;
903
904        let mut micros = [0; 8];
905        micros.copy_from_slice(&self.0[8..]);
906        let micros = u64::from_be_bytes(micros) ^ 0x8000_0000_0000_0000u64;
907
908        Interval {
909            months: months as i32,
910            days: days as i32,
911            micros: micros as i64,
912        }
913    }
914}
915
916#[cfg(test)]
917mod test {
918    use mz_ore::assert_ok;
919    use mz_proto::protobuf_roundtrip;
920
921    use super::*;
922    use proptest::prelude::*;
923
924    #[mz_ore::test]
925    fn interval_fmt() {
926        fn mon(mon: i32) -> String {
927            Interval {
928                months: mon,
929                ..Default::default()
930            }
931            .to_string()
932        }
933
934        assert_eq!(mon(1), "1 mon");
935        assert_eq!(mon(12), "1 year");
936        assert_eq!(mon(13), "1 year 1 mon");
937        assert_eq!(mon(24), "2 years");
938        assert_eq!(mon(25), "2 years 1 mon");
939        assert_eq!(mon(26), "2 years 2 mons");
940
941        fn dur(days: i32, micros: i64) -> String {
942            Interval::new(0, days, micros).to_string()
943        }
944        assert_eq!(&dur(2, 0), "2 days");
945        assert_eq!(&dur(2, 3 * 60 * 60 * 1_000_000), "2 days 03:00:00");
946        assert_eq!(
947            &dur(
948                2,
949                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
950            ),
951            "2 days 03:45:06"
952        );
953        assert_eq!(
954            &dur(2, (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000)),
955            "2 days 03:45:00"
956        );
957        assert_eq!(&dur(2, 6 * 1_000_000), "2 days 00:00:06");
958        assert_eq!(
959            &dur(2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
960            "2 days 00:45:06"
961        );
962        assert_eq!(
963            &dur(2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
964            "2 days 03:00:06"
965        );
966        assert_eq!(
967            &dur(
968                0,
969                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
970            ),
971            "03:45:06"
972        );
973        assert_eq!(
974            &dur(0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
975            "03:00:06"
976        );
977        assert_eq!(&dur(0, 3 * 60 * 60 * 1_000_000), "03:00:00");
978        assert_eq!(&dur(0, (45 * 60 * 1_000_000) + (6 * 1_000_000)), "00:45:06");
979        assert_eq!(&dur(0, 45 * 60 * 1_000_000), "00:45:00");
980        assert_eq!(&dur(0, 6 * 1_000_000), "00:00:06");
981
982        assert_eq!(&dur(-2, -6 * 1_000_000), "-2 days -00:00:06");
983        assert_eq!(
984            &dur(-2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
985            "-2 days -00:45:06"
986        );
987        assert_eq!(
988            &dur(-2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
989            "-2 days -03:00:06"
990        );
991        assert_eq!(
992            &dur(
993                0,
994                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
995            ),
996            "-03:45:06"
997        );
998        assert_eq!(
999            &dur(0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1000            "-03:00:06"
1001        );
1002        assert_eq!(&dur(0, -3 * 60 * 60 * 1_000_000), "-03:00:00");
1003        assert_eq!(
1004            &dur(0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1005            "-00:45:06"
1006        );
1007        assert_eq!(&dur(0, -45 * 60 * 1_000_000), "-00:45:00");
1008        assert_eq!(&dur(0, -6 * 1_000_000), "-00:00:06");
1009
1010        fn mon_dur(mon: i32, days: i32, micros: i64) -> String {
1011            Interval::new(mon, days, micros).to_string()
1012        }
1013        assert_eq!(&mon_dur(1, 2, 6 * 1_000_000), "1 mon 2 days 00:00:06");
1014        assert_eq!(
1015            &mon_dur(1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1016            "1 mon 2 days 00:45:06"
1017        );
1018        assert_eq!(
1019            &mon_dur(1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1020            "1 mon 2 days 03:00:06"
1021        );
1022        assert_eq!(
1023            &mon_dur(
1024                26,
1025                0,
1026                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
1027            ),
1028            "2 years 2 mons 03:45:06"
1029        );
1030        assert_eq!(
1031            &mon_dur(26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1032            "2 years 2 mons 03:00:06"
1033        );
1034        assert_eq!(
1035            &mon_dur(26, 0, 3 * 60 * 60 * 1_000_000),
1036            "2 years 2 mons 03:00:00"
1037        );
1038        assert_eq!(
1039            &mon_dur(26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1040            "2 years 2 mons 00:45:06"
1041        );
1042        assert_eq!(
1043            &mon_dur(26, 0, 45 * 60 * 1_000_000),
1044            "2 years 2 mons 00:45:00"
1045        );
1046        assert_eq!(&mon_dur(26, 0, 6 * 1_000_000), "2 years 2 mons 00:00:06");
1047
1048        assert_eq!(
1049            &mon_dur(26, -2, -6 * 1_000_000),
1050            "2 years 2 mons -2 days -00:00:06"
1051        );
1052        assert_eq!(
1053            &mon_dur(26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1054            "2 years 2 mons -2 days -00:45:06"
1055        );
1056        assert_eq!(
1057            &mon_dur(26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1058            "2 years 2 mons -2 days -03:00:06"
1059        );
1060        assert_eq!(
1061            &mon_dur(
1062                26,
1063                0,
1064                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
1065            ),
1066            "2 years 2 mons -03:45:06"
1067        );
1068        assert_eq!(
1069            &mon_dur(26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1070            "2 years 2 mons -03:00:06"
1071        );
1072        assert_eq!(
1073            &mon_dur(26, 0, -3 * 60 * 60 * 1_000_000),
1074            "2 years 2 mons -03:00:00"
1075        );
1076        assert_eq!(
1077            &mon_dur(26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1078            "2 years 2 mons -00:45:06"
1079        );
1080        assert_eq!(
1081            &mon_dur(26, 0, -45 * 60 * 1_000_000),
1082            "2 years 2 mons -00:45:00"
1083        );
1084        assert_eq!(&mon_dur(26, 0, -6 * 1_000_000), "2 years 2 mons -00:00:06");
1085
1086        assert_eq!(&mon_dur(-1, 2, 6 * 1_000_000), "-1 mons +2 days 00:00:06");
1087        assert_eq!(
1088            &mon_dur(-1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1089            "-1 mons +2 days 00:45:06"
1090        );
1091        assert_eq!(
1092            &mon_dur(-1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1093            "-1 mons +2 days 03:00:06"
1094        );
1095        assert_eq!(
1096            &mon_dur(
1097                -26,
1098                0,
1099                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
1100            ),
1101            "-2 years -2 mons +03:45:06"
1102        );
1103        assert_eq!(
1104            &mon_dur(-26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1105            "-2 years -2 mons +03:00:06"
1106        );
1107        assert_eq!(
1108            &mon_dur(-26, 0, 3 * 60 * 60 * 1_000_000),
1109            "-2 years -2 mons +03:00:00"
1110        );
1111        assert_eq!(
1112            &mon_dur(-26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1113            "-2 years -2 mons +00:45:06"
1114        );
1115        assert_eq!(
1116            &mon_dur(-26, 0, 45 * 60 * 1_000_000),
1117            "-2 years -2 mons +00:45:00"
1118        );
1119        assert_eq!(
1120            &mon_dur(-26, 0, 6 * 1_000_000),
1121            "-2 years -2 mons +00:00:06"
1122        );
1123
1124        assert_eq!(
1125            &mon_dur(-26, -2, -6 * 1_000_000),
1126            "-2 years -2 mons -2 days -00:00:06"
1127        );
1128        assert_eq!(
1129            &mon_dur(-26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1130            "-2 years -2 mons -2 days -00:45:06"
1131        );
1132        assert_eq!(
1133            &mon_dur(-26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1134            "-2 years -2 mons -2 days -03:00:06"
1135        );
1136        assert_eq!(
1137            &mon_dur(
1138                -26,
1139                0,
1140                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
1141            ),
1142            "-2 years -2 mons -03:45:06"
1143        );
1144        assert_eq!(
1145            &mon_dur(-26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1146            "-2 years -2 mons -03:00:06"
1147        );
1148        assert_eq!(
1149            &mon_dur(-26, 0, -3 * 60 * 60 * 1_000_000),
1150            "-2 years -2 mons -03:00:00"
1151        );
1152        assert_eq!(
1153            &mon_dur(-26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1154            "-2 years -2 mons -00:45:06"
1155        );
1156        assert_eq!(
1157            &mon_dur(-26, 0, -45 * 60 * 1_000_000),
1158            "-2 years -2 mons -00:45:00"
1159        );
1160        assert_eq!(
1161            &mon_dur(-26, 0, -6 * 1_000_000),
1162            "-2 years -2 mons -00:00:06"
1163        );
1164    }
1165
1166    #[mz_ore::test]
1167    fn test_interval_value_truncate_low_fields() {
1168        use DateTimeField::*;
1169
1170        let mut test_cases = [
1171            (
1172                Year,
1173                None,
1174                (
1175                    321,
1176                    7,
1177                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1178                ),
1179                (26 * 12, 0, 0),
1180            ),
1181            (
1182                Month,
1183                None,
1184                (
1185                    321,
1186                    7,
1187                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1188                ),
1189                (321, 0, 0),
1190            ),
1191            (
1192                Day,
1193                None,
1194                (
1195                    321,
1196                    7,
1197                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1198                ),
1199                (321, 7, 0),
1200            ),
1201            (
1202                Hour,
1203                None,
1204                (
1205                    321,
1206                    7,
1207                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1208                ),
1209                (321, 7, 13 * 60 * 60 * 1_000_000),
1210            ),
1211            (
1212                Minute,
1213                None,
1214                (
1215                    321,
1216                    7,
1217                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1218                ),
1219                (321, 7, (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000)),
1220            ),
1221            (
1222                Second,
1223                None,
1224                (
1225                    321,
1226                    7,
1227                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1228                ),
1229                (
1230                    321,
1231                    7,
1232                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1233                ),
1234            ),
1235            (
1236                Second,
1237                Some(1),
1238                (
1239                    321,
1240                    7,
1241                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1242                ),
1243                (
1244                    321,
1245                    7,
1246                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 300_000,
1247                ),
1248            ),
1249            (
1250                Second,
1251                Some(0),
1252                (
1253                    321,
1254                    7,
1255                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1256                ),
1257                (
1258                    321,
1259                    7,
1260                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000),
1261                ),
1262            ),
1263        ];
1264
1265        for test in test_cases.iter_mut() {
1266            let mut i = Interval::new((test.2).0, (test.2).1, (test.2).2);
1267            let j = Interval::new((test.3).0, (test.3).1, (test.3).2);
1268
1269            i.truncate_low_fields(test.0, test.1, RoundBehavior::Nearest)
1270                .unwrap();
1271
1272            if i != j {
1273                panic!(
1274                    "test_interval_value_truncate_low_fields failed on {} \n actual: {:?} \n expected: {:?}",
1275                    test.0, i, j
1276                );
1277            }
1278        }
1279    }
1280
1281    #[mz_ore::test]
1282    fn test_convert_date_time_unit() {
1283        assert_eq!(
1284            Some(1_123_200_000_000),
1285            Interval::convert_date_time_unit(
1286                DateTimeField::Day,
1287                DateTimeField::Microseconds,
1288                13i64
1289            )
1290        );
1291
1292        assert_eq!(
1293            Some(3_558_399_705),
1294            Interval::convert_date_time_unit(
1295                DateTimeField::Milliseconds,
1296                DateTimeField::Month,
1297                i64::MAX
1298            )
1299        );
1300
1301        assert_eq!(
1302            None,
1303            Interval::convert_date_time_unit(
1304                DateTimeField::Minute,
1305                DateTimeField::Second,
1306                i32::MAX
1307            )
1308        );
1309
1310        assert_eq!(
1311            Some(1),
1312            Interval::convert_date_time_unit(DateTimeField::Day, DateTimeField::Year, 365)
1313        );
1314
1315        // Strange behavior due to months having 30 days
1316        assert_eq!(
1317            Some(360),
1318            Interval::convert_date_time_unit(DateTimeField::Year, DateTimeField::Day, 1)
1319        );
1320    }
1321
1322    #[mz_ore::test]
1323    fn proptest_packed_interval_roundtrips() {
1324        fn roundtrip_interval(og: Interval) {
1325            let packed = PackedInterval::from_value(og);
1326            let rnd = packed.into_value();
1327
1328            assert_eq!(og, rnd);
1329        }
1330
1331        proptest!(|(interval in any::<Interval>())| {
1332            roundtrip_interval(interval);
1333        });
1334    }
1335
1336    #[mz_ore::test]
1337    #[cfg_attr(miri, ignore)] // too slow
1338    fn proptest_packed_interval_sorts() {
1339        fn sort_intervals(mut og: Vec<Interval>) {
1340            let mut packed: Vec<_> = og.iter().copied().map(PackedInterval::from_value).collect();
1341
1342            og.sort();
1343            packed.sort();
1344
1345            let rnd: Vec<_> = packed.into_iter().map(PackedInterval::into_value).collect();
1346
1347            assert_eq!(og, rnd);
1348        }
1349
1350        proptest!(|(interval in any::<Vec<Interval>>())| {
1351            sort_intervals(interval);
1352        });
1353    }
1354
1355    // `Interval` <-> `ProtoInterval` is a field-for-field copy today, so this only
1356    // bites once the two structs drift: a field added to `Interval` but not carried
1357    // through `ProtoInterval` silently decodes as that field's default. NOTE: the
1358    // guard is blind unless `Interval::arbitrary` also generates the new field, so
1359    // extend the strategy alongside the field.
1360    proptest! {
1361        #[mz_ore::test]
1362        fn interval_protobuf_roundtrip(expect in any::<Interval>()) {
1363            let actual = protobuf_roundtrip::<_, ProtoInterval>(&expect);
1364            assert_ok!(actual);
1365            assert_eq!(actual.unwrap(), expect);
1366        }
1367    }
1368}