Skip to content

Commit ec60b1d

Browse files
committed
fix: replace use of deprecated chrono APIs
1 parent 0f6c377 commit ec60b1d

File tree

5 files changed

+65
-34
lines changed

5 files changed

+65
-34
lines changed

sqlx-mysql/src/types/chrono.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ impl<'r> Decode<'r, MySql> for NaiveTime {
106106
// are 0 then the length is 0 and no further data is send
107107
// https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
108108
if len == 0 {
109-
return Ok(NaiveTime::from_hms_micro(0, 0, 0, 0));
109+
return Ok(NaiveTime::from_hms_micro_opt(0, 0, 0, 0)
110+
.expect("expected NaiveTime to construct from all zeroes"));
110111
}
111112

112113
// is negative : int<1>
@@ -117,7 +118,7 @@ impl<'r> Decode<'r, MySql> for NaiveTime {
117118
// https://mariadb.com/kb/en/resultset-row/#timestamp-binary-encoding
118119
buf.advance(4);
119120

120-
Ok(decode_time(len - 5, buf))
121+
decode_time(len - 5, buf)
121122
}
122123

123124
MySqlValueFormat::Text => {
@@ -152,7 +153,7 @@ impl<'r> Decode<'r, MySql> for NaiveDate {
152153
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
153154
match value.format() {
154155
MySqlValueFormat::Binary => {
155-
decode_date(&value.as_bytes()?[1..]).ok_or_else(|| UnexpectedNullError.into())
156+
decode_date(&value.as_bytes()?[1..])?.ok_or_else(|| UnexpectedNullError.into())
156157
}
157158

158159
MySqlValueFormat::Text => {
@@ -212,12 +213,13 @@ impl<'r> Decode<'r, MySql> for NaiveDateTime {
212213
let buf = value.as_bytes()?;
213214

214215
let len = buf[0];
215-
let date = decode_date(&buf[1..]).ok_or(UnexpectedNullError)?;
216+
let date = decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?;
216217

217218
let dt = if len > 4 {
218-
date.and_time(decode_time(len - 4, &buf[5..]))
219+
date.and_time(decode_time(len - 4, &buf[5..])?)
219220
} else {
220-
date.and_hms(0, 0, 0)
221+
date.and_hms_opt(0, 0, 0)
222+
.expect("expected `NaiveDate::and_hms_opt(0, 0, 0)` to be valid")
221223
};
222224

223225
Ok(dt)
@@ -241,17 +243,21 @@ fn encode_date(date: &NaiveDate, buf: &mut Vec<u8>) {
241243
buf.push(date.day() as u8);
242244
}
243245

244-
fn decode_date(mut buf: &[u8]) -> Option<NaiveDate> {
245-
if buf.len() == 0 {
246+
fn decode_date(mut buf: &[u8]) -> Result<Option<NaiveDate>, BoxDynError> {
247+
match buf.len() {
246248
// MySQL specifies that if there are no bytes, this is all zeros
247-
None
248-
} else {
249-
let year = buf.get_u16_le();
250-
Some(NaiveDate::from_ymd(
251-
year as i32,
252-
buf[0] as u32,
253-
buf[1] as u32,
254-
))
249+
0 => Ok(None),
250+
4.. => {
251+
let year = buf.get_u16_le() as i32;
252+
let month = buf[0] as u32;
253+
let day = buf[1] as u32;
254+
255+
let date = NaiveDate::from_ymd_opt(year, month, day)
256+
.ok_or_else(|| format!("server returned invalid date: {year}/{month}/{day}"))?;
257+
258+
Ok(Some(date))
259+
}
260+
len => Err(format!("expected at least 4 bytes for date, got {len}").into()),
255261
}
256262
}
257263

@@ -265,7 +271,7 @@ fn encode_time(time: &NaiveTime, include_micros: bool, buf: &mut Vec<u8>) {
265271
}
266272
}
267273

268-
fn decode_time(len: u8, mut buf: &[u8]) -> NaiveTime {
274+
fn decode_time(len: u8, mut buf: &[u8]) -> Result<NaiveTime, BoxDynError> {
269275
let hour = buf.get_u8();
270276
let minute = buf.get_u8();
271277
let seconds = buf.get_u8();
@@ -277,5 +283,6 @@ fn decode_time(len: u8, mut buf: &[u8]) -> NaiveTime {
277283
0
278284
};
279285

280-
NaiveTime::from_hms_micro(hour as u32, minute as u32, seconds as u32, micros as u32)
286+
NaiveTime::from_hms_micro_opt(hour as u32, minute as u32, seconds as u32, micros as u32)
287+
.ok_or_else(|| format!("server returned invalid time: {hour:02}:{minute:02}:{seconds:02}; micros: {micros}").into())
281288
}

sqlx-postgres/src/types/chrono/date.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl PgHasArrayType for NaiveDate {
2121
impl Encode<'_, Postgres> for NaiveDate {
2222
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
2323
// DATE is encoded as the days since epoch
24-
let days = (*self - NaiveDate::from_ymd(2000, 1, 1)).num_days() as i32;
24+
let days = (*self - postgres_epoch_date()).num_days() as i32;
2525
Encode::<Postgres>::encode(&days, buf)
2626
}
2727

@@ -36,10 +36,15 @@ impl<'r> Decode<'r, Postgres> for NaiveDate {
3636
PgValueFormat::Binary => {
3737
// DATE is encoded as the days since epoch
3838
let days: i32 = Decode::<Postgres>::decode(value)?;
39-
NaiveDate::from_ymd(2000, 1, 1) + Duration::days(days.into())
39+
postgres_epoch_date() + Duration::days(days.into())
4040
}
4141

4242
PgValueFormat::Text => NaiveDate::parse_from_str(value.as_str()?, "%Y-%m-%d")?,
4343
})
4444
}
4545
}
46+
47+
#[inline]
48+
fn postgres_epoch_date() -> NaiveDate {
49+
NaiveDate::from_ymd_opt(2000, 1, 1).expect("expected 2000-01-01 to be a valid NaiveDate")
50+
}

sqlx-postgres/src/types/chrono/datetime.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ impl Encode<'_, Postgres> for NaiveDateTime {
3636
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
3737
// FIXME: We should *really* be returning an error, Encode needs to be fallible
3838
// TIMESTAMP is encoded as the microseconds since the epoch
39-
let epoch = NaiveDate::from_ymd(2000, 1, 1).and_hms(0, 0, 0);
40-
let us = (*self - epoch)
39+
let us = (*self - postgres_epoch_datetime())
4140
.num_microseconds()
4241
.unwrap_or_else(|| panic!("NaiveDateTime out of range for Postgres: {:?}", self));
4342

@@ -54,9 +53,8 @@ impl<'r> Decode<'r, Postgres> for NaiveDateTime {
5453
Ok(match value.format() {
5554
PgValueFormat::Binary => {
5655
// TIMESTAMP is encoded as the microseconds since the epoch
57-
let epoch = NaiveDate::from_ymd(2000, 1, 1).and_hms(0, 0, 0);
5856
let us = Decode::<Postgres>::decode(value)?;
59-
epoch + Duration::microseconds(us)
57+
postgres_epoch_datetime() + Duration::microseconds(us)
6058
}
6159

6260
PgValueFormat::Text => {
@@ -107,3 +105,11 @@ impl<'r> Decode<'r, Postgres> for DateTime<FixedOffset> {
107105
Ok(Utc.fix().from_utc_datetime(&naive))
108106
}
109107
}
108+
109+
#[inline]
110+
fn postgres_epoch_datetime() -> NaiveDateTime {
111+
NaiveDate::from_ymd_opt(2000, 1, 1)
112+
.expect("expected 2000-01-01 to be a valid NaiveDate")
113+
.and_hms_opt(0, 0, 0)
114+
.expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime")
115+
}

sqlx-postgres/src/types/chrono/time.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ impl Encode<'_, Postgres> for NaiveTime {
2222
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
2323
// TIME is encoded as the microseconds since midnight
2424
// NOTE: panic! is on overflow and 1 day does not have enough micros to overflow
25-
let us = (*self - NaiveTime::from_hms(0, 0, 0))
26-
.num_microseconds()
27-
.unwrap();
25+
let us = (*self - NaiveTime::default()).num_microseconds().unwrap();
2826

2927
Encode::<Postgres>::encode(&us, buf)
3028
}
@@ -40,10 +38,20 @@ impl<'r> Decode<'r, Postgres> for NaiveTime {
4038
PgValueFormat::Binary => {
4139
// TIME is encoded as the microseconds since midnight
4240
let us: i64 = Decode::<Postgres>::decode(value)?;
43-
NaiveTime::from_hms(0, 0, 0) + Duration::microseconds(us)
41+
NaiveTime::default() + Duration::microseconds(us)
4442
}
4543

4644
PgValueFormat::Text => NaiveTime::parse_from_str(value.as_str()?, "%H:%M:%S%.f")?,
4745
})
4846
}
4947
}
48+
49+
#[test]
50+
fn check_naive_time_default_is_midnight() {
51+
// Just a canary in case this changes.
52+
assert_eq!(
53+
NaiveTime::from_hms_opt(0, 0, 0),
54+
Some(NaiveTime::default()),
55+
"implementation assumes `NaiveTime::default()` equals midnight"
56+
);
57+
}

sqlx-postgres/src/types/time_tz.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,20 @@ mod chrono {
7171

7272
// TIME is encoded as the microseconds since midnight
7373
let us = buf.read_i64::<BigEndian>()?;
74-
let time = NaiveTime::from_hms(0, 0, 0) + Duration::microseconds(us);
74+
// default is midnight, there is a canary test for this
75+
// in `sqlx-postgres/src/types/chrono/time.rs`
76+
let time = NaiveTime::default() + Duration::microseconds(us);
7577

7678
// OFFSET is encoded as seconds from UTC
77-
let seconds = buf.read_i32::<BigEndian>()?;
79+
let offset_seconds = buf.read_i32::<BigEndian>()?;
7880

79-
Ok(PgTimeTz {
80-
time,
81-
offset: FixedOffset::west(seconds),
82-
})
81+
let offset = FixedOffset::west_opt(offset_seconds).ok_or_else(|| {
82+
format!(
83+
"server returned out-of-range offset for `TIMETZ`: {offset_seconds} seconds"
84+
)
85+
})?;
86+
87+
Ok(PgTimeTz { time, offset })
8388
}
8489

8590
PgValueFormat::Text => {

0 commit comments

Comments
 (0)