sqlx_mysql/types/
mysql_time.rs

1//! The [`MysqlTime`] type.
2
3use crate::protocol::text::ColumnType;
4use crate::{MySql, MySqlTypeInfo, MySqlValueFormat};
5use bytes::{Buf, BufMut};
6use sqlx_core::database::Database;
7use sqlx_core::decode::Decode;
8use sqlx_core::encode::{Encode, IsNull};
9use sqlx_core::error::BoxDynError;
10use sqlx_core::types::Type;
11use std::cmp::Ordering;
12use std::fmt::{Debug, Display, Formatter, Write};
13use std::time::Duration;
14
15// Similar to `PgInterval`
16/// Container for a MySQL `TIME` value, which may be an interval or a time-of-day.
17///
18/// Allowed range is `-838:59:59.0` to `838:59:59.0`.
19///
20/// If this value is used for a time-of-day, the range should be `00:00:00.0` to `23:59:59.999999`.
21/// You can use [`Self::is_valid_time_of_day()`] to check this easily.
22///
23/// * [MySQL Manual 13.2.3: The TIME Type](https://dev.mysql.com/doc/refman/8.3/en/time.html)
24/// * [MariaDB Manual: TIME](https://mariadb.com/kb/en/time/)
25#[derive(Debug, Copy, Clone, Eq, PartialEq)]
26pub struct MySqlTime {
27    pub(crate) sign: MySqlTimeSign,
28    pub(crate) magnitude: TimeMagnitude,
29}
30
31// By using a subcontainer for the actual time magnitude,
32// we can still use a derived `Ord` implementation and just flip the comparison for negative values.
33#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
34pub(crate) struct TimeMagnitude {
35    pub(crate) hours: u32,
36    pub(crate) minutes: u8,
37    pub(crate) seconds: u8,
38    pub(crate) microseconds: u32,
39}
40
41const MAGNITUDE_ZERO: TimeMagnitude = TimeMagnitude {
42    hours: 0,
43    minutes: 0,
44    seconds: 0,
45    microseconds: 0,
46};
47
48/// Maximum magnitude (positive or negative).
49const MAGNITUDE_MAX: TimeMagnitude = TimeMagnitude {
50    hours: MySqlTime::HOURS_MAX,
51    minutes: 59,
52    seconds: 59,
53    // Surprisingly this is not 999_999 which is why `MySqlTimeError::SubsecondExcess`.
54    microseconds: 0,
55};
56
57/// The sign for a [`MySqlTime`] type.
58#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
59pub enum MySqlTimeSign {
60    // The protocol actually specifies negative as 1 and positive as 0,
61    // but by specifying variants this way we can derive `Ord` and it works as expected.
62    /// The interval is negative (invalid for time-of-day values).
63    Negative,
64    /// The interval is positive, or represents a time-of-day.
65    Positive,
66}
67
68/// Errors returned by [`MySqlTime::new()`].
69#[derive(Debug, thiserror::Error)]
70pub enum MySqlTimeError {
71    /// A field of [`MySqlTime`] exceeded its max range.
72    #[error("`MySqlTime` field `{field}` cannot exceed {max}, got {value}")]
73    FieldRange {
74        field: &'static str,
75        max: u32,
76        value: u64,
77    },
78    /// Error returned for time magnitudes (positive or negative) between `838:59:59.0` and `839:00:00.0`.
79    ///
80    /// Other range errors should be covered by [`Self::FieldRange`] for the `hours` field.
81    ///
82    /// For applications which can tolerate rounding, a valid truncated value is provided.
83    #[error(
84        "`MySqlTime` cannot exceed +/-838:59:59.000000; got {sign}838:59:59.{microseconds:06}"
85    )]
86    SubsecondExcess {
87        /// The sign of the magnitude.
88        sign: MySqlTimeSign,
89        /// The number of microseconds over the maximum.
90        microseconds: u32,
91        /// The truncated value,
92        /// either [`MySqlTime::MIN`] if negative or [`MySqlTime::MAX`] if positive.
93        truncated: MySqlTime,
94    },
95    /// MySQL coerces `-00:00:00` to `00:00:00` but this API considers that an error.
96    ///
97    /// For applications which can tolerate coercion, you can convert this error to [`MySqlTime::ZERO`].
98    #[error("attempted to construct a `MySqlTime` value of negative zero")]
99    NegativeZero,
100}
101
102impl MySqlTime {
103    /// The `MySqlTime` value corresponding to `TIME '0:00:00.0'` (zero).
104    pub const ZERO: Self = MySqlTime {
105        sign: MySqlTimeSign::Positive,
106        magnitude: MAGNITUDE_ZERO,
107    };
108
109    /// The `MySqlTime` value corresponding to `TIME '838:59:59.0'` (max value).
110    pub const MAX: Self = MySqlTime {
111        sign: MySqlTimeSign::Positive,
112        magnitude: MAGNITUDE_MAX,
113    };
114
115    /// The `MySqlTime` value corresponding to `TIME '-838:59:59.0'` (min value).
116    pub const MIN: Self = MySqlTime {
117        sign: MySqlTimeSign::Negative,
118        // Same magnitude, opposite sign.
119        magnitude: MAGNITUDE_MAX,
120    };
121
122    // The maximums for the other values are self-evident, but not necessarily this one.
123    pub(crate) const HOURS_MAX: u32 = 838;
124
125    /// Construct a [`MySqlTime`] that is valid for use as a `TIME` value.
126    ///
127    /// ### Errors
128    /// * [`MySqlTimeError::NegativeZero`] if all fields are 0 but `sign` is [`MySqlTimeSign::Negative`].
129    /// * [`MySqlTimeError::FieldRange`] if any field is out of range:
130    ///     * `hours > 838`
131    ///     * `minutes > 59`
132    ///     * `seconds > 59`
133    ///     * `microseconds > 999_999`
134    /// * [`MySqlTimeError::SubsecondExcess`] if the magnitude is less than one second over the maximum.
135    ///     * Durations 839 hours or greater are covered by `FieldRange`.
136    pub fn new(
137        sign: MySqlTimeSign,
138        hours: u32,
139        minutes: u8,
140        seconds: u8,
141        microseconds: u32,
142    ) -> Result<Self, MySqlTimeError> {
143        macro_rules! check_fields {
144            ($($name:ident: $max:expr),+ $(,)?) => {
145                $(
146                    if $name > $max {
147                        return Err(MySqlTimeError::FieldRange {
148                            field: stringify!($name),
149                            max: $max as u32,
150                            value: $name as u64
151                        })
152                    }
153                )+
154            }
155        }
156
157        check_fields!(
158            hours: Self::HOURS_MAX,
159            minutes: 59,
160            seconds: 59,
161            microseconds: 999_999
162        );
163
164        let values = TimeMagnitude {
165            hours,
166            minutes,
167            seconds,
168            microseconds,
169        };
170
171        if sign.is_negative() && values == MAGNITUDE_ZERO {
172            return Err(MySqlTimeError::NegativeZero);
173        }
174
175        // This is only `true` if less than 1 second over the maximum magnitude
176        if values > MAGNITUDE_MAX {
177            return Err(MySqlTimeError::SubsecondExcess {
178                sign,
179                microseconds,
180                truncated: if sign.is_positive() {
181                    Self::MAX
182                } else {
183                    Self::MIN
184                },
185            });
186        }
187
188        Ok(Self {
189            sign,
190            magnitude: values,
191        })
192    }
193
194    /// Update the `sign` of this value.
195    pub fn with_sign(self, sign: MySqlTimeSign) -> Self {
196        Self { sign, ..self }
197    }
198
199    /// Return the sign (positive or negative) for this TIME value.
200    pub fn sign(&self) -> MySqlTimeSign {
201        self.sign
202    }
203
204    /// Returns `true` if `self` is zero (equal to [`Self::ZERO`]).
205    pub fn is_zero(&self) -> bool {
206        self == &Self::ZERO
207    }
208
209    /// Returns `true` if `self` is positive or zero, `false` if negative.
210    pub fn is_positive(&self) -> bool {
211        self.sign.is_positive()
212    }
213
214    /// Returns `true` if `self` is negative, `false` if positive or zero.
215    pub fn is_negative(&self) -> bool {
216        self.sign.is_positive()
217    }
218
219    /// Returns `true` if this interval is a valid time-of-day.
220    ///
221    /// If `true`, the sign is positive and `hours` is not greater than 23.
222    pub fn is_valid_time_of_day(&self) -> bool {
223        self.sign.is_positive() && self.hours() < 24
224    }
225
226    /// Get the total number of hours in this interval, from 0 to 838.
227    ///
228    /// If this value represents a time-of-day, the range is 0 to 23.
229    pub fn hours(&self) -> u32 {
230        self.magnitude.hours
231    }
232
233    /// Get the number of minutes in this interval, from 0 to 59.
234    pub fn minutes(&self) -> u8 {
235        self.magnitude.minutes
236    }
237
238    /// Get the number of seconds in this interval, from 0 to 59.
239    pub fn seconds(&self) -> u8 {
240        self.magnitude.seconds
241    }
242
243    /// Get the number of seconds in this interval, from 0 to 999,999.
244    pub fn microseconds(&self) -> u32 {
245        self.magnitude.microseconds
246    }
247
248    /// Convert this TIME value to a [`std::time::Duration`].
249    ///
250    /// Returns `None` if this value is negative (cannot be represented).
251    pub fn to_duration(&self) -> Option<Duration> {
252        self.is_positive()
253            .then(|| Duration::new(self.whole_seconds() as u64, self.subsec_nanos()))
254    }
255
256    /// Get the whole number of seconds (`seconds + (minutes * 60) + (hours * 3600)`) in this time.
257    ///
258    /// Sign is ignored.
259    pub(crate) fn whole_seconds(&self) -> u32 {
260        // If `hours` does not exceed 838 then this cannot overflow.
261        self.hours() * 3600 + self.minutes() as u32 * 60 + self.seconds() as u32
262    }
263
264    #[cfg_attr(not(any(feature = "time", feature = "chrono")), allow(dead_code))]
265    pub(crate) fn whole_seconds_signed(&self) -> i64 {
266        self.whole_seconds() as i64 * self.sign.signum() as i64
267    }
268
269    pub(crate) fn subsec_nanos(&self) -> u32 {
270        self.microseconds() * 1000
271    }
272
273    fn encoded_len(&self) -> u8 {
274        if self.is_zero() {
275            0
276        } else if self.microseconds() == 0 {
277            8
278        } else {
279            12
280        }
281    }
282}
283
284impl PartialOrd<MySqlTime> for MySqlTime {
285    fn partial_cmp(&self, other: &MySqlTime) -> Option<Ordering> {
286        Some(self.cmp(other))
287    }
288}
289
290impl Ord for MySqlTime {
291    fn cmp(&self, other: &Self) -> Ordering {
292        // If the sides have different signs, we just need to compare those.
293        if self.sign != other.sign {
294            return self.sign.cmp(&other.sign);
295        }
296
297        // We've checked that both sides have the same sign
298        match self.sign {
299            MySqlTimeSign::Positive => self.magnitude.cmp(&other.magnitude),
300            // Reverse the comparison for negative values (smaller negative magnitude = greater)
301            MySqlTimeSign::Negative => other.magnitude.cmp(&self.magnitude),
302        }
303    }
304}
305
306impl Display for MySqlTime {
307    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
308        let TimeMagnitude {
309            hours,
310            minutes,
311            seconds,
312            microseconds,
313        } = self.magnitude;
314
315        // Obeys the `+` flag.
316        Display::fmt(&self.sign(), f)?;
317
318        write!(f, "{hours}:{minutes:02}:{seconds:02}")?;
319
320        // Write microseconds if not zero or a nonzero precision was explicitly requested.
321        if f.precision().map_or(microseconds != 0, |it| it != 0) {
322            f.write_char('.')?;
323
324            let mut remaining_precision = f.precision();
325            let mut remainder = microseconds;
326            let mut power_of_10 = 10u32.pow(5);
327
328            // Write digits from most-significant to least, up to the requested precision.
329            while remainder > 0 && remaining_precision != Some(0) {
330                let digit = remainder / power_of_10;
331                // 1 % 1 = 0
332                remainder %= power_of_10;
333                power_of_10 /= 10;
334
335                write!(f, "{digit}")?;
336
337                if let Some(remaining_precision) = &mut remaining_precision {
338                    *remaining_precision = remaining_precision.saturating_sub(1);
339                }
340            }
341
342            // If any requested precision remains, pad with zeroes.
343            if let Some(precision) = remaining_precision.filter(|it| *it != 0) {
344                write!(f, "{:0precision$}", 0)?;
345            }
346        }
347
348        Ok(())
349    }
350}
351
352impl Type<MySql> for MySqlTime {
353    fn type_info() -> MySqlTypeInfo {
354        MySqlTypeInfo::binary(ColumnType::Time)
355    }
356}
357
358impl<'r> Decode<'r, MySql> for MySqlTime {
359    fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
360        match value.format() {
361            MySqlValueFormat::Binary => {
362                let mut buf = value.as_bytes()?;
363
364                // Row decoding should have left the length byte on the front.
365                if buf.is_empty() {
366                    return Err("empty buffer".into());
367                }
368
369                let length = buf.get_u8();
370
371                // MySQL specifies that if all fields are 0 then the length is 0 and no further data is sent
372                // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
373                if length == 0 {
374                    return Ok(Self::ZERO);
375                }
376
377                if !matches!(buf.len(), 8 | 12) {
378                    return Err(format!(
379                        "expected 8 or 12 bytes for TIME value, got {}",
380                        buf.len()
381                    )
382                    .into());
383                }
384
385                let sign = MySqlTimeSign::from_byte(buf.get_u8())?;
386                // The wire protocol includes days but the text format doesn't. Isn't that crazy?
387                let days = buf.get_u32_le();
388                let hours = buf.get_u8();
389                let minutes = buf.get_u8();
390                let seconds = buf.get_u8();
391
392                let microseconds = if !buf.is_empty() { buf.get_u32_le() } else { 0 };
393
394                let whole_hours = days
395                    .checked_mul(24)
396                    .and_then(|days_to_hours| days_to_hours.checked_add(hours as u32))
397                    .ok_or("overflow calculating whole hours from `days * 24 + hours`")?;
398
399                Ok(Self::new(
400                    sign,
401                    whole_hours,
402                    minutes,
403                    seconds,
404                    microseconds,
405                )?)
406            }
407            MySqlValueFormat::Text => parse(value.as_str()?),
408        }
409    }
410}
411
412impl<'q> Encode<'q, MySql> for MySqlTime {
413    fn encode_by_ref(
414        &self,
415        buf: &mut <MySql as Database>::ArgumentBuffer<'q>,
416    ) -> Result<IsNull, BoxDynError> {
417        if self.is_zero() {
418            buf.put_u8(0);
419            return Ok(IsNull::No);
420        }
421
422        buf.put_u8(self.encoded_len());
423        buf.put_u8(self.sign.to_byte());
424
425        let TimeMagnitude {
426            hours: whole_hours,
427            minutes,
428            seconds,
429            microseconds,
430        } = self.magnitude;
431
432        let days = whole_hours / 24;
433        let hours = (whole_hours % 24) as u8;
434
435        buf.put_u32_le(days);
436        buf.put_u8(hours);
437        buf.put_u8(minutes);
438        buf.put_u8(seconds);
439
440        if microseconds != 0 {
441            buf.put_u32_le(microseconds);
442        }
443
444        Ok(IsNull::No)
445    }
446
447    fn size_hint(&self) -> usize {
448        self.encoded_len() as usize + 1
449    }
450}
451
452/// Convert [`MySqlTime`] from [`std::time::Duration`].
453///
454/// ### Note: Precision Truncation
455/// [`Duration`] supports nanosecond precision, but MySQL `TIME` values only support microsecond
456/// precision.
457///
458/// For simplicity, higher precision values are truncated when converting.
459/// If you prefer another rounding mode instead, you should apply that to the `Duration` first.
460///
461/// See also: [MySQL Manual, section 13.2.6: Fractional Seconds in Time Values](https://dev.mysql.com/doc/refman/8.3/en/fractional-seconds.html)
462///
463/// ### Errors:
464/// Returns [`MySqlTimeError::FieldRange`] if the given duration is longer than `838:59:59.999999`.
465///
466impl TryFrom<Duration> for MySqlTime {
467    type Error = MySqlTimeError;
468
469    fn try_from(value: Duration) -> Result<Self, Self::Error> {
470        let hours = value.as_secs() / 3600;
471        let rem_seconds = value.as_secs() % 3600;
472        let minutes = (rem_seconds / 60) as u8;
473        let seconds = (rem_seconds % 60) as u8;
474
475        // Simply divides by 1000
476        let microseconds = value.subsec_micros();
477
478        Self::new(
479            MySqlTimeSign::Positive,
480            hours.try_into().map_err(|_| MySqlTimeError::FieldRange {
481                field: "hours",
482                max: Self::HOURS_MAX,
483                value: hours,
484            })?,
485            minutes,
486            seconds,
487            microseconds,
488        )
489    }
490}
491
492impl MySqlTimeSign {
493    fn from_byte(b: u8) -> Result<Self, BoxDynError> {
494        match b {
495            0 => Ok(Self::Positive),
496            1 => Ok(Self::Negative),
497            other => Err(format!("expected 0 or 1 for TIME sign byte, got {other}").into()),
498        }
499    }
500
501    fn to_byte(self) -> u8 {
502        match self {
503            // We can't use `#[repr(u8)]` because this is opposite of the ordering we want from `Ord`
504            Self::Negative => 1,
505            Self::Positive => 0,
506        }
507    }
508
509    fn signum(&self) -> i32 {
510        match self {
511            Self::Negative => -1,
512            Self::Positive => 1,
513        }
514    }
515
516    /// Returns `true` if positive, `false` if negative.
517    pub fn is_positive(&self) -> bool {
518        matches!(self, Self::Positive)
519    }
520
521    /// Returns `true` if negative, `false` if positive.
522    pub fn is_negative(&self) -> bool {
523        matches!(self, Self::Negative)
524    }
525}
526
527impl Display for MySqlTimeSign {
528    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
529        match self {
530            Self::Positive if f.sign_plus() => f.write_char('+'),
531            Self::Negative => f.write_char('-'),
532            _ => Ok(()),
533        }
534    }
535}
536
537impl Type<MySql> for Duration {
538    fn type_info() -> MySqlTypeInfo {
539        MySqlTime::type_info()
540    }
541}
542
543impl<'r> Decode<'r, MySql> for Duration {
544    fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
545        let time = MySqlTime::decode(value)?;
546
547        time.to_duration().ok_or_else(|| {
548            format!("`std::time::Duration` can only decode positive TIME values; got {time}").into()
549        })
550    }
551}
552
553// Not exposing this as a `FromStr` impl currently because `MySqlTime` is not designed to be
554// a general interchange type.
555fn parse(text: &str) -> Result<MySqlTime, BoxDynError> {
556    let mut segments = text.split(':');
557
558    let hours = segments
559        .next()
560        .ok_or("expected hours segment, got nothing")?;
561
562    let minutes = segments
563        .next()
564        .ok_or("expected minutes segment, got nothing")?;
565
566    let seconds = segments
567        .next()
568        .ok_or("expected seconds segment, got nothing")?;
569
570    // Include the sign in parsing for convenience;
571    // the allowed range of whole hours is much smaller than `i32`'s positive range.
572    let hours: i32 = hours
573        .parse()
574        .map_err(|e| format!("error parsing hours from {text:?} (segment {hours:?}): {e}"))?;
575
576    let sign = if hours.is_negative() {
577        MySqlTimeSign::Negative
578    } else {
579        MySqlTimeSign::Positive
580    };
581
582    let hours = hours.unsigned_abs();
583
584    let minutes: u8 = minutes
585        .parse()
586        .map_err(|e| format!("error parsing minutes from {text:?} (segment {minutes:?}): {e}"))?;
587
588    let (seconds, microseconds): (u8, u32) = if let Some((seconds, microseconds)) =
589        seconds.split_once('.')
590    {
591        (
592            seconds.parse().map_err(|e| {
593                format!("error parsing seconds from {text:?} (segment {seconds:?}): {e}")
594            })?,
595            parse_microseconds(microseconds).map_err(|e| {
596                format!("error parsing microseconds from {text:?} (segment {microseconds:?}): {e}")
597            })?,
598        )
599    } else {
600        (
601            seconds.parse().map_err(|e| {
602                format!("error parsing seconds from {text:?} (segment {seconds:?}): {e}")
603            })?,
604            0,
605        )
606    };
607
608    Ok(MySqlTime::new(sign, hours, minutes, seconds, microseconds)?)
609}
610
611/// Parse microseconds from a fractional seconds string.
612fn parse_microseconds(micros: &str) -> Result<u32, BoxDynError> {
613    const EXPECTED_DIGITS: usize = 6;
614
615    match micros.len() {
616        0 => Err("empty string".into()),
617        len @ ..=EXPECTED_DIGITS => {
618            // Fewer than 6 digits, multiply to the correct magnitude
619            let micros: u32 = micros.parse()?;
620            // cast cannot overflow
621            #[allow(clippy::cast_possible_truncation)]
622            Ok(micros * 10u32.pow((EXPECTED_DIGITS - len) as u32))
623        }
624        // More digits than expected, truncate
625        _ => Ok(micros[..EXPECTED_DIGITS].parse()?),
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::MySqlTime;
632    use crate::types::MySqlTimeSign;
633
634    use super::parse_microseconds;
635
636    #[test]
637    fn test_display() {
638        assert_eq!(MySqlTime::ZERO.to_string(), "0:00:00");
639
640        assert_eq!(format!("{:.0}", MySqlTime::ZERO), "0:00:00");
641
642        assert_eq!(format!("{:.3}", MySqlTime::ZERO), "0:00:00.000");
643
644        assert_eq!(format!("{:.6}", MySqlTime::ZERO), "0:00:00.000000");
645
646        assert_eq!(format!("{:.9}", MySqlTime::ZERO), "0:00:00.000000000");
647
648        assert_eq!(format!("{:.0}", MySqlTime::MAX), "838:59:59");
649
650        assert_eq!(format!("{:.3}", MySqlTime::MAX), "838:59:59.000");
651
652        assert_eq!(format!("{:.6}", MySqlTime::MAX), "838:59:59.000000");
653
654        assert_eq!(format!("{:.9}", MySqlTime::MAX), "838:59:59.000000000");
655
656        assert_eq!(format!("{:+.0}", MySqlTime::MAX), "+838:59:59");
657
658        assert_eq!(format!("{:+.3}", MySqlTime::MAX), "+838:59:59.000");
659
660        assert_eq!(format!("{:+.6}", MySqlTime::MAX), "+838:59:59.000000");
661
662        assert_eq!(format!("{:+.9}", MySqlTime::MAX), "+838:59:59.000000000");
663
664        assert_eq!(format!("{:.0}", MySqlTime::MIN), "-838:59:59");
665
666        assert_eq!(format!("{:.3}", MySqlTime::MIN), "-838:59:59.000");
667
668        assert_eq!(format!("{:.6}", MySqlTime::MIN), "-838:59:59.000000");
669
670        assert_eq!(format!("{:.9}", MySqlTime::MIN), "-838:59:59.000000000");
671
672        let positive = MySqlTime::new(MySqlTimeSign::Positive, 123, 45, 56, 890011).unwrap();
673
674        assert_eq!(positive.to_string(), "123:45:56.890011");
675        assert_eq!(format!("{positive:.0}"), "123:45:56");
676        assert_eq!(format!("{positive:.3}"), "123:45:56.890");
677        assert_eq!(format!("{positive:.6}"), "123:45:56.890011");
678        assert_eq!(format!("{positive:.9}"), "123:45:56.890011000");
679
680        assert_eq!(format!("{positive:+.0}"), "+123:45:56");
681        assert_eq!(format!("{positive:+.3}"), "+123:45:56.890");
682        assert_eq!(format!("{positive:+.6}"), "+123:45:56.890011");
683        assert_eq!(format!("{positive:+.9}"), "+123:45:56.890011000");
684
685        let negative = MySqlTime::new(MySqlTimeSign::Negative, 123, 45, 56, 890011).unwrap();
686
687        assert_eq!(negative.to_string(), "-123:45:56.890011");
688        assert_eq!(format!("{negative:.0}"), "-123:45:56");
689        assert_eq!(format!("{negative:.3}"), "-123:45:56.890");
690        assert_eq!(format!("{negative:.6}"), "-123:45:56.890011");
691        assert_eq!(format!("{negative:.9}"), "-123:45:56.890011000");
692    }
693
694    #[test]
695    fn test_parse_microseconds() {
696        assert_eq!(parse_microseconds("010").unwrap(), 10_000);
697
698        assert_eq!(parse_microseconds("0100000000").unwrap(), 10_000);
699
700        assert_eq!(parse_microseconds("890").unwrap(), 890_000);
701
702        assert_eq!(parse_microseconds("0890").unwrap(), 89_000);
703
704        assert_eq!(
705            // Case in point about not exposing this:
706            // we always truncate excess precision because it's simpler than rounding
707            // and MySQL should never return a higher precision.
708            parse_microseconds("123456789").unwrap(),
709            123456,
710        );
711    }
712}