@@ -2,13 +2,14 @@ use bytes::Buf;
22use chrono:: {
33 DateTime , Datelike , Local , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Utc ,
44} ;
5+ use sqlx_core:: database:: Database ;
56
67use crate :: decode:: Decode ;
78use crate :: encode:: { Encode , IsNull } ;
89use crate :: error:: { BoxDynError , UnexpectedNullError } ;
910use crate :: protocol:: text:: ColumnType ;
1011use crate :: type_info:: MySqlTypeInfo ;
11- use crate :: types:: Type ;
12+ use crate :: types:: { MySqlTime , MySqlTimeSign , Type } ;
1213use crate :: { MySql , MySqlValueFormat , MySqlValueRef } ;
1314
1415impl Type < MySql > for DateTime < Utc > {
@@ -63,7 +64,7 @@ impl<'r> Decode<'r, MySql> for DateTime<Local> {
6364
6465impl Type < MySql > for NaiveTime {
6566 fn type_info ( ) -> MySqlTypeInfo {
66- MySqlTypeInfo :: binary ( ColumnType :: Time )
67+ MySqlTime :: type_info ( )
6768 }
6869}
6970
@@ -75,7 +76,7 @@ impl Encode<'_, MySql> for NaiveTime {
7576 // NaiveTime is not negative
7677 buf. push ( 0 ) ;
7778
78- // "date on 4 bytes little-endian format" (?)
79+ // Number of days in the interval; always 0 for time-of-day values.
7980 // https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
8081 buf. extend_from_slice ( & [ 0_u8 ; 4 ] ) ;
8182
@@ -95,34 +96,18 @@ impl Encode<'_, MySql> for NaiveTime {
9596 }
9697}
9798
99+ /// Decode from a `TIME` value.
100+ ///
101+ /// ### Errors
102+ /// Returns an error if the `TIME` value is negative or exceeds `23:59:59.999999`.
98103impl < ' r > Decode < ' r , MySql > for NaiveTime {
99104 fn decode ( value : MySqlValueRef < ' r > ) -> Result < Self , BoxDynError > {
100105 match value. format ( ) {
101106 MySqlValueFormat :: Binary => {
102- let mut buf = value. as_bytes ( ) ?;
103-
104- // data length, expecting 8 or 12 (fractional seconds)
105- let len = buf. get_u8 ( ) ;
106-
107- // MySQL specifies that if all of hours, minutes, seconds, microseconds
108- // are 0 then the length is 0 and no further data is send
109- // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
110- if len == 0 {
111- return Ok ( NaiveTime :: from_hms_micro_opt ( 0 , 0 , 0 , 0 )
112- . expect ( "expected NaiveTime to construct from all zeroes" ) ) ;
113- }
114-
115- // is negative : int<1>
116- let is_negative = buf. get_u8 ( ) ;
117- debug_assert_eq ! ( is_negative, 0 , "Negative dates/times are not supported" ) ;
118-
119- // "date on 4 bytes little-endian format" (?)
120- // https://mariadb.com/kb/en/resultset-row/#timestamp-binary-encoding
121- buf. advance ( 4 ) ;
122-
123- decode_time ( len - 5 , buf)
107+ // Covers most possible failure modes.
108+ MySqlTime :: decode ( value) ?. try_into ( )
124109 }
125-
110+ // Retaining this parsing for now as it allows us to cross-check our impl.
126111 MySqlValueFormat :: Text => {
127112 let s = value. as_str ( ) ?;
128113 NaiveTime :: parse_from_str ( s, "%H:%M:%S%.f" ) . map_err ( Into :: into)
@@ -131,6 +116,57 @@ impl<'r> Decode<'r, MySql> for NaiveTime {
131116 }
132117}
133118
119+ impl TryFrom < MySqlTime > for NaiveTime {
120+ type Error = BoxDynError ;
121+
122+ fn try_from ( time : MySqlTime ) -> Result < Self , Self :: Error > {
123+ NaiveTime :: from_hms_micro_opt (
124+ time. hours ( ) ,
125+ time. minutes ( ) as u32 ,
126+ time. seconds ( ) as u32 ,
127+ time. microseconds ( ) ,
128+ )
129+ . ok_or_else ( || format ! ( "Cannot convert `MySqlTime` value to `NaiveTime`: {time}" ) . into ( ) )
130+ }
131+ }
132+
133+ impl From < MySqlTime > for chrono:: TimeDelta {
134+ fn from ( time : MySqlTime ) -> Self {
135+ chrono:: TimeDelta :: new ( time. whole_seconds_signed ( ) , time. subsec_nanos ( ) )
136+ . expect ( "BUG: chrono::TimeDelta should have a greater range than MySqlTime" )
137+ }
138+ }
139+
140+ impl TryFrom < chrono:: TimeDelta > for MySqlTime {
141+ type Error = BoxDynError ;
142+
143+ fn try_from ( value : chrono:: TimeDelta ) -> Result < Self , Self :: Error > {
144+ let sign = if value < chrono:: TimeDelta :: zero ( ) {
145+ MySqlTimeSign :: Negative
146+ } else {
147+ MySqlTimeSign :: Positive
148+ } ;
149+
150+ Ok (
151+ // `std::time::Duration` has a greater positive range than `TimeDelta`
152+ // which makes it a great intermediate if you ignore the sign.
153+ MySqlTime :: try_from ( value. abs ( ) . to_std ( ) ?) ?. with_sign ( sign) ,
154+ )
155+ }
156+ }
157+
158+ impl Type < MySql > for chrono:: TimeDelta {
159+ fn type_info ( ) -> MySqlTypeInfo {
160+ MySqlTime :: type_info ( )
161+ }
162+ }
163+
164+ impl < ' r > Decode < ' r , MySql > for chrono:: TimeDelta {
165+ fn decode ( value : <MySql as Database >:: ValueRef < ' r > ) -> Result < Self , BoxDynError > {
166+ Ok ( MySqlTime :: decode ( value) ?. into ( ) )
167+ }
168+ }
169+
134170impl Type < MySql > for NaiveDate {
135171 fn type_info ( ) -> MySqlTypeInfo {
136172 MySqlTypeInfo :: binary ( ColumnType :: Date )
@@ -155,7 +191,14 @@ impl<'r> Decode<'r, MySql> for NaiveDate {
155191 fn decode ( value : MySqlValueRef < ' r > ) -> Result < Self , BoxDynError > {
156192 match value. format ( ) {
157193 MySqlValueFormat :: Binary => {
158- decode_date ( & value. as_bytes ( ) ?[ 1 ..] ) ?. ok_or_else ( || UnexpectedNullError . into ( ) )
194+ let buf = value. as_bytes ( ) ?;
195+
196+ // Row decoding should have left the length prefix.
197+ if buf. is_empty ( ) {
198+ return Err ( "empty buffer" . into ( ) ) ;
199+ }
200+
201+ decode_date ( & buf[ 1 ..] ) ?. ok_or_else ( || UnexpectedNullError . into ( ) )
159202 }
160203
161204 MySqlValueFormat :: Text => {
@@ -214,6 +257,10 @@ impl<'r> Decode<'r, MySql> for NaiveDateTime {
214257 MySqlValueFormat :: Binary => {
215258 let buf = value. as_bytes ( ) ?;
216259
260+ if buf. is_empty ( ) {
261+ return Err ( "empty buffer" . into ( ) ) ;
262+ }
263+
217264 let len = buf[ 0 ] ;
218265 let date = decode_date ( & buf[ 1 ..] ) ?. ok_or ( UnexpectedNullError ) ?;
219266
0 commit comments