1use 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
26pub struct MySqlTime {
27 pub(crate) sign: MySqlTimeSign,
28 pub(crate) magnitude: TimeMagnitude,
29}
30
31#[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
48const MAGNITUDE_MAX: TimeMagnitude = TimeMagnitude {
50 hours: MySqlTime::HOURS_MAX,
51 minutes: 59,
52 seconds: 59,
53 microseconds: 0,
55};
56
57#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
59pub enum MySqlTimeSign {
60 Negative,
64 Positive,
66}
67
68#[derive(Debug, thiserror::Error)]
70pub enum MySqlTimeError {
71 #[error("`MySqlTime` field `{field}` cannot exceed {max}, got {value}")]
73 FieldRange {
74 field: &'static str,
75 max: u32,
76 value: u64,
77 },
78 #[error(
84 "`MySqlTime` cannot exceed +/-838:59:59.000000; got {sign}838:59:59.{microseconds:06}"
85 )]
86 SubsecondExcess {
87 sign: MySqlTimeSign,
89 microseconds: u32,
91 truncated: MySqlTime,
94 },
95 #[error("attempted to construct a `MySqlTime` value of negative zero")]
99 NegativeZero,
100}
101
102impl MySqlTime {
103 pub const ZERO: Self = MySqlTime {
105 sign: MySqlTimeSign::Positive,
106 magnitude: MAGNITUDE_ZERO,
107 };
108
109 pub const MAX: Self = MySqlTime {
111 sign: MySqlTimeSign::Positive,
112 magnitude: MAGNITUDE_MAX,
113 };
114
115 pub const MIN: Self = MySqlTime {
117 sign: MySqlTimeSign::Negative,
118 magnitude: MAGNITUDE_MAX,
120 };
121
122 pub(crate) const HOURS_MAX: u32 = 838;
124
125 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 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 pub fn with_sign(self, sign: MySqlTimeSign) -> Self {
196 Self { sign, ..self }
197 }
198
199 pub fn sign(&self) -> MySqlTimeSign {
201 self.sign
202 }
203
204 pub fn is_zero(&self) -> bool {
206 self == &Self::ZERO
207 }
208
209 pub fn is_positive(&self) -> bool {
211 self.sign.is_positive()
212 }
213
214 pub fn is_negative(&self) -> bool {
216 self.sign.is_positive()
217 }
218
219 pub fn is_valid_time_of_day(&self) -> bool {
223 self.sign.is_positive() && self.hours() < 24
224 }
225
226 pub fn hours(&self) -> u32 {
230 self.magnitude.hours
231 }
232
233 pub fn minutes(&self) -> u8 {
235 self.magnitude.minutes
236 }
237
238 pub fn seconds(&self) -> u8 {
240 self.magnitude.seconds
241 }
242
243 pub fn microseconds(&self) -> u32 {
245 self.magnitude.microseconds
246 }
247
248 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 pub(crate) fn whole_seconds(&self) -> u32 {
260 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 self.sign != other.sign {
294 return self.sign.cmp(&other.sign);
295 }
296
297 match self.sign {
299 MySqlTimeSign::Positive => self.magnitude.cmp(&other.magnitude),
300 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 Display::fmt(&self.sign(), f)?;
317
318 write!(f, "{hours}:{minutes:02}:{seconds:02}")?;
319
320 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 while remainder > 0 && remaining_precision != Some(0) {
330 let digit = remainder / power_of_10;
331 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 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 if buf.is_empty() {
366 return Err("empty buffer".into());
367 }
368
369 let length = buf.get_u8();
370
371 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 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
452impl 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 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 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 pub fn is_positive(&self) -> bool {
518 matches!(self, Self::Positive)
519 }
520
521 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
553fn 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 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
611fn 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 let micros: u32 = micros.parse()?;
620 #[allow(clippy::cast_possible_truncation)]
622 Ok(micros * 10u32.pow((EXPECTED_DIGITS - len) as u32))
623 }
624 _ => 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 parse_microseconds("123456789").unwrap(),
709 123456,
710 );
711 }
712}