diff --git a/sqlx-mysql/src/types/chrono.rs b/sqlx-mysql/src/types/chrono.rs index 4059b1ef..656c5bce 100644 --- a/sqlx-mysql/src/types/chrono.rs +++ b/sqlx-mysql/src/types/chrono.rs @@ -106,7 +106,8 @@ impl<'r> Decode<'r, MySql> for NaiveTime { // are 0 then the length is 0 and no further data is send // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html if len == 0 { - return Ok(NaiveTime::from_hms_micro(0, 0, 0, 0)); + return Ok(NaiveTime::from_hms_micro_opt(0, 0, 0, 0) + .expect("expected NaiveTime to construct from all zeroes")); } // is negative : int<1> @@ -117,7 +118,7 @@ impl<'r> Decode<'r, MySql> for NaiveTime { // https://mariadb.com/kb/en/resultset-row/#timestamp-binary-encoding buf.advance(4); - Ok(decode_time(len - 5, buf)) + decode_time(len - 5, buf) } MySqlValueFormat::Text => { @@ -152,7 +153,7 @@ impl<'r> Decode<'r, MySql> for NaiveDate { fn decode(value: MySqlValueRef<'r>) -> Result { match value.format() { MySqlValueFormat::Binary => { - decode_date(&value.as_bytes()?[1..]).ok_or_else(|| UnexpectedNullError.into()) + decode_date(&value.as_bytes()?[1..])?.ok_or_else(|| UnexpectedNullError.into()) } MySqlValueFormat::Text => { @@ -212,12 +213,13 @@ impl<'r> Decode<'r, MySql> for NaiveDateTime { let buf = value.as_bytes()?; let len = buf[0]; - let date = decode_date(&buf[1..]).ok_or(UnexpectedNullError)?; + let date = decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?; let dt = if len > 4 { - date.and_time(decode_time(len - 4, &buf[5..])) + date.and_time(decode_time(len - 4, &buf[5..])?) } else { - date.and_hms(0, 0, 0) + date.and_hms_opt(0, 0, 0) + .expect("expected `NaiveDate::and_hms_opt(0, 0, 0)` to be valid") }; Ok(dt) @@ -241,17 +243,21 @@ fn encode_date(date: &NaiveDate, buf: &mut Vec) { buf.push(date.day() as u8); } -fn decode_date(mut buf: &[u8]) -> Option { - if buf.len() == 0 { +fn decode_date(mut buf: &[u8]) -> Result, BoxDynError> { + match buf.len() { // MySQL specifies that if there are no bytes, this is all zeros - None - } else { - let year = buf.get_u16_le(); - Some(NaiveDate::from_ymd( - year as i32, - buf[0] as u32, - buf[1] as u32, - )) + 0 => Ok(None), + 4.. => { + let year = buf.get_u16_le() as i32; + let month = buf[0] as u32; + let day = buf[1] as u32; + + let date = NaiveDate::from_ymd_opt(year, month, day) + .ok_or_else(|| format!("server returned invalid date: {year}/{month}/{day}"))?; + + Ok(Some(date)) + } + len => Err(format!("expected at least 4 bytes for date, got {len}").into()), } } @@ -265,7 +271,7 @@ fn encode_time(time: &NaiveTime, include_micros: bool, buf: &mut Vec) { } } -fn decode_time(len: u8, mut buf: &[u8]) -> NaiveTime { +fn decode_time(len: u8, mut buf: &[u8]) -> Result { let hour = buf.get_u8(); let minute = buf.get_u8(); let seconds = buf.get_u8(); @@ -277,5 +283,6 @@ fn decode_time(len: u8, mut buf: &[u8]) -> NaiveTime { 0 }; - NaiveTime::from_hms_micro(hour as u32, minute as u32, seconds as u32, micros as u32) + NaiveTime::from_hms_micro_opt(hour as u32, minute as u32, seconds as u32, micros as u32) + .ok_or_else(|| format!("server returned invalid time: {hour:02}:{minute:02}:{seconds:02}; micros: {micros}").into()) } diff --git a/sqlx-postgres/src/types/chrono/date.rs b/sqlx-postgres/src/types/chrono/date.rs index c02d6b85..4e968e10 100644 --- a/sqlx-postgres/src/types/chrono/date.rs +++ b/sqlx-postgres/src/types/chrono/date.rs @@ -21,7 +21,7 @@ impl PgHasArrayType for NaiveDate { impl Encode<'_, Postgres> for NaiveDate { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { // DATE is encoded as the days since epoch - let days = (*self - NaiveDate::from_ymd(2000, 1, 1)).num_days() as i32; + let days = (*self - postgres_epoch_date()).num_days() as i32; Encode::::encode(&days, buf) } @@ -36,10 +36,15 @@ impl<'r> Decode<'r, Postgres> for NaiveDate { PgValueFormat::Binary => { // DATE is encoded as the days since epoch let days: i32 = Decode::::decode(value)?; - NaiveDate::from_ymd(2000, 1, 1) + Duration::days(days.into()) + postgres_epoch_date() + Duration::days(days.into()) } PgValueFormat::Text => NaiveDate::parse_from_str(value.as_str()?, "%Y-%m-%d")?, }) } } + +#[inline] +fn postgres_epoch_date() -> NaiveDate { + NaiveDate::from_ymd_opt(2000, 1, 1).expect("expected 2000-01-01 to be a valid NaiveDate") +} diff --git a/sqlx-postgres/src/types/chrono/datetime.rs b/sqlx-postgres/src/types/chrono/datetime.rs index a8740bdb..1860fbfe 100644 --- a/sqlx-postgres/src/types/chrono/datetime.rs +++ b/sqlx-postgres/src/types/chrono/datetime.rs @@ -36,8 +36,7 @@ impl Encode<'_, Postgres> for NaiveDateTime { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { // FIXME: We should *really* be returning an error, Encode needs to be fallible // TIMESTAMP is encoded as the microseconds since the epoch - let epoch = NaiveDate::from_ymd(2000, 1, 1).and_hms(0, 0, 0); - let us = (*self - epoch) + let us = (*self - postgres_epoch_datetime()) .num_microseconds() .unwrap_or_else(|| panic!("NaiveDateTime out of range for Postgres: {:?}", self)); @@ -54,9 +53,8 @@ impl<'r> Decode<'r, Postgres> for NaiveDateTime { Ok(match value.format() { PgValueFormat::Binary => { // TIMESTAMP is encoded as the microseconds since the epoch - let epoch = NaiveDate::from_ymd(2000, 1, 1).and_hms(0, 0, 0); let us = Decode::::decode(value)?; - epoch + Duration::microseconds(us) + postgres_epoch_datetime() + Duration::microseconds(us) } PgValueFormat::Text => { @@ -107,3 +105,11 @@ impl<'r> Decode<'r, Postgres> for DateTime { Ok(Utc.fix().from_utc_datetime(&naive)) } } + +#[inline] +fn postgres_epoch_datetime() -> NaiveDateTime { + NaiveDate::from_ymd_opt(2000, 1, 1) + .expect("expected 2000-01-01 to be a valid NaiveDate") + .and_hms_opt(0, 0, 0) + .expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime") +} diff --git a/sqlx-postgres/src/types/chrono/time.rs b/sqlx-postgres/src/types/chrono/time.rs index b354b8d9..3bdd65ee 100644 --- a/sqlx-postgres/src/types/chrono/time.rs +++ b/sqlx-postgres/src/types/chrono/time.rs @@ -22,9 +22,7 @@ impl Encode<'_, Postgres> for NaiveTime { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { // TIME is encoded as the microseconds since midnight // NOTE: panic! is on overflow and 1 day does not have enough micros to overflow - let us = (*self - NaiveTime::from_hms(0, 0, 0)) - .num_microseconds() - .unwrap(); + let us = (*self - NaiveTime::default()).num_microseconds().unwrap(); Encode::::encode(&us, buf) } @@ -40,10 +38,20 @@ impl<'r> Decode<'r, Postgres> for NaiveTime { PgValueFormat::Binary => { // TIME is encoded as the microseconds since midnight let us: i64 = Decode::::decode(value)?; - NaiveTime::from_hms(0, 0, 0) + Duration::microseconds(us) + NaiveTime::default() + Duration::microseconds(us) } PgValueFormat::Text => NaiveTime::parse_from_str(value.as_str()?, "%H:%M:%S%.f")?, }) } } + +#[test] +fn check_naive_time_default_is_midnight() { + // Just a canary in case this changes. + assert_eq!( + NaiveTime::from_hms_opt(0, 0, 0), + Some(NaiveTime::default()), + "implementation assumes `NaiveTime::default()` equals midnight" + ); +} diff --git a/sqlx-postgres/src/types/time_tz.rs b/sqlx-postgres/src/types/time_tz.rs index b3acf724..5855a0b7 100644 --- a/sqlx-postgres/src/types/time_tz.rs +++ b/sqlx-postgres/src/types/time_tz.rs @@ -71,15 +71,20 @@ mod chrono { // TIME is encoded as the microseconds since midnight let us = buf.read_i64::()?; - let time = NaiveTime::from_hms(0, 0, 0) + Duration::microseconds(us); + // default is midnight, there is a canary test for this + // in `sqlx-postgres/src/types/chrono/time.rs` + let time = NaiveTime::default() + Duration::microseconds(us); // OFFSET is encoded as seconds from UTC - let seconds = buf.read_i32::()?; + let offset_seconds = buf.read_i32::()?; - Ok(PgTimeTz { - time, - offset: FixedOffset::west(seconds), - }) + let offset = FixedOffset::west_opt(offset_seconds).ok_or_else(|| { + format!( + "server returned out-of-range offset for `TIMETZ`: {offset_seconds} seconds" + ) + })?; + + Ok(PgTimeTz { time, offset }) } PgValueFormat::Text => {