Skip to content

Commit 8aa11f3

Browse files
committed
mysql: Negative TIME values in prepared statements
In our outbound mysql serialization, we were intentionally failing to write out `TIME` values that may have been negative. Mysql does allow `TIME` calues to be in the range '-838:59:59' to '838:59:59'[0]. The prior code, at least in the serialization of data returned from a proxied request, assumed that `TIME` must be `Duration`, and `Duration` is always positive. This CL gets around that limitation by just ditching `Duration` and using the existin`MySqlTime` type, which already handles positive/negative `TIME` values. In practice, I'd suspect negative `TIME` values to be fairly rare, but fuzz testing stomps on this landmine oftern enough (and they are valid values). [0] https://dev.mysql.com/doc/refman/8.4/en/time.html Fixes: REA-5966 Release-Note-Core: Support negative `TIME` values for mysql. Change-Id: Ifde7e80177bd8b1bd31de532875a8ada14a8d9f2 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/10330 Tested-by: Buildkite CI Reviewed-by: Marcelo Altmann <marcelo@readyset.io>
1 parent e85ff4e commit 8aa11f3

File tree

1 file changed

+22
-88
lines changed

1 file changed

+22
-88
lines changed

mysql-srv/src/value/encode.rs

Lines changed: 22 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -656,61 +656,6 @@ impl ToMySqlValue for NaiveDateTime {
656656
}
657657
}
658658

659-
use std::time::Duration;
660-
661-
impl ToMySqlValue for Duration {
662-
fn to_mysql_text<W: Write>(&self, w: &mut W) -> io::Result<()> {
663-
let s = self.as_secs();
664-
//let d = s / (24 * 3600);
665-
// assert!(d <= 34);
666-
//let h = (s % (24 * 3600)) / 3600;
667-
let h = s / 3600;
668-
let m = (s % 3600) / 60;
669-
let s = s % 60;
670-
let us = self.subsec_micros();
671-
if us != 0 {
672-
w.write_lenenc_str(format!("{h:02}:{m:02}:{s:02}.{us:06}").as_bytes())
673-
.map(|_| ())
674-
} else {
675-
w.write_lenenc_str(format!("{h:02}:{m:02}:{s:02}").as_bytes())
676-
.map(|_| ())
677-
}
678-
}
679-
680-
#[allow(clippy::many_single_char_names)]
681-
fn to_mysql_bin<W: Write>(&self, w: &mut W, c: &Column) -> io::Result<()> {
682-
let s = self.as_secs();
683-
let d = s / (24 * 3600);
684-
assert!(d <= 34);
685-
let h = (s % (24 * 3600)) / 3600;
686-
let m = (s % 3600) / 60;
687-
let s = s % 60;
688-
let us = self.subsec_micros();
689-
690-
match c.coltype {
691-
ColumnType::MYSQL_TYPE_TIME => {
692-
if us != 0 {
693-
w.write_u8(12u8)?;
694-
} else {
695-
w.write_u8(8u8)?;
696-
}
697-
698-
w.write_u8(0u8)?; // positive only (for now)
699-
w.write_u32::<LittleEndian>(d as u32)?;
700-
w.write_u8(h as u8)?;
701-
w.write_u8(m as u8)?;
702-
w.write_u8(s as u8)?;
703-
704-
if us != 0 {
705-
w.write_u32::<LittleEndian>(us)?;
706-
}
707-
Ok(())
708-
}
709-
_ => Err(bad(self, c)),
710-
}
711-
}
712-
}
713-
714659
impl ToMySqlValue for myc::value::Value {
715660
#[allow(clippy::many_single_char_names)]
716661
fn to_mysql_text<W: Write>(&self, w: &mut W) -> io::Result<()> {
@@ -726,22 +671,14 @@ impl ToMySqlValue for myc::value::Value {
726671
.to_mysql_text(w)
727672
}
728673
myc::value::Value::Time(neg, d, h, m, s, us) => {
729-
if neg {
730-
return Err(other_error(OtherErrorKind::Unexpected {
731-
error: "negative times not yet supported".to_string(),
732-
}));
733-
}
734-
(chrono::Duration::days(i64::from(d))
735-
+ chrono::Duration::hours(i64::from(h))
736-
+ chrono::Duration::minutes(i64::from(m))
737-
+ chrono::Duration::seconds(i64::from(s))
738-
+ chrono::Duration::microseconds(i64::from(us)))
739-
.to_std()
740-
.map_err(|_| {
741-
other_error(OtherErrorKind::Unexpected {
742-
error: "negative times not yet supported".to_string(),
743-
})
744-
})?
674+
let hours = (d * 24) + h as u32;
675+
MySqlTime::from_hmsus(
676+
!neg,
677+
hours.try_into().expect("Hours are too large for TIME"),
678+
m,
679+
s,
680+
us.into(),
681+
)
745682
.to_mysql_text(w)
746683
}
747684
}
@@ -797,22 +734,14 @@ impl ToMySqlValue for myc::value::Value {
797734
w.write_all(buf.as_slice())
798735
}
799736
myc::value::Value::Time(neg, d, h, m, s, us) => {
800-
if neg {
801-
return Err(other_error(OtherErrorKind::Unexpected {
802-
error: "negative times not yet supported".to_string(),
803-
}));
804-
}
805-
(chrono::Duration::days(i64::from(d))
806-
+ chrono::Duration::hours(i64::from(h))
807-
+ chrono::Duration::minutes(i64::from(m))
808-
+ chrono::Duration::seconds(i64::from(s))
809-
+ chrono::Duration::microseconds(i64::from(us)))
810-
.to_std()
811-
.map_err(|_| {
812-
other_error(OtherErrorKind::Unexpected {
813-
error: "negative times not yet supported".to_string(),
814-
})
815-
})?
737+
let hours = (d * 24) + h as u32;
738+
MySqlTime::from_hmsus(
739+
!neg,
740+
hours.try_into().expect("Hours are too large for TIME"),
741+
m,
742+
s,
743+
us.into(),
744+
)
816745
.to_mysql_bin(w, c)
817746
}
818747
}
@@ -891,7 +820,6 @@ mod tests {
891820
.unwrap()
892821
.naive_utc()
893822
);
894-
rt!(dur, time::Duration, time::Duration::from_secs(1893));
895823
rt!(bytes, Vec<u8>, vec![0x42, 0x00, 0x1a]);
896824
rt!(string, String, "foobar".to_owned());
897825
}
@@ -1050,6 +978,12 @@ mod tests {
1050978
MySqlTime::from_hmsus(true, 20, 15, 14, 123_456),
1051979
ColumnType::MYSQL_TYPE_TIME
1052980
);
981+
rt!(
982+
time_neg,
983+
MySqlTime,
984+
MySqlTime::from_hmsus(false, 142, 15, 14, 123_456),
985+
ColumnType::MYSQL_TYPE_TIME
986+
);
1053987
rt!(
1054988
bytes,
1055989
Vec<u8>,

0 commit comments

Comments
 (0)