@@ -2,13 +2,14 @@ use bytes::Buf;
2
2
use chrono:: {
3
3
DateTime , Datelike , Local , NaiveDate , NaiveDateTime , NaiveTime , TimeZone , Timelike , Utc ,
4
4
} ;
5
+ use sqlx_core:: database:: Database ;
5
6
6
7
use crate :: decode:: Decode ;
7
8
use crate :: encode:: { Encode , IsNull } ;
8
9
use crate :: error:: { BoxDynError , UnexpectedNullError } ;
9
10
use crate :: protocol:: text:: ColumnType ;
10
11
use crate :: type_info:: MySqlTypeInfo ;
11
- use crate :: types:: Type ;
12
+ use crate :: types:: { MySqlTime , MySqlTimeSign , Type } ;
12
13
use crate :: { MySql , MySqlValueFormat , MySqlValueRef } ;
13
14
14
15
impl Type < MySql > for DateTime < Utc > {
@@ -63,7 +64,7 @@ impl<'r> Decode<'r, MySql> for DateTime<Local> {
63
64
64
65
impl Type < MySql > for NaiveTime {
65
66
fn type_info ( ) -> MySqlTypeInfo {
66
- MySqlTypeInfo :: binary ( ColumnType :: Time )
67
+ MySqlTime :: type_info ( )
67
68
}
68
69
}
69
70
@@ -75,7 +76,7 @@ impl Encode<'_, MySql> for NaiveTime {
75
76
// NaiveTime is not negative
76
77
buf. push ( 0 ) ;
77
78
78
- // "date on 4 bytes little-endian format" (?)
79
+ // Number of days in the interval; always 0 for time-of-day values.
79
80
// https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
80
81
buf. extend_from_slice ( & [ 0_u8 ; 4 ] ) ;
81
82
@@ -95,34 +96,18 @@ impl Encode<'_, MySql> for NaiveTime {
95
96
}
96
97
}
97
98
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`.
98
103
impl < ' r > Decode < ' r , MySql > for NaiveTime {
99
104
fn decode ( value : MySqlValueRef < ' r > ) -> Result < Self , BoxDynError > {
100
105
match value. format ( ) {
101
106
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 ( )
124
109
}
125
-
110
+ // Retaining this parsing for now as it allows us to cross-check our impl.
126
111
MySqlValueFormat :: Text => {
127
112
let s = value. as_str ( ) ?;
128
113
NaiveTime :: parse_from_str ( s, "%H:%M:%S%.f" ) . map_err ( Into :: into)
@@ -131,6 +116,57 @@ impl<'r> Decode<'r, MySql> for NaiveTime {
131
116
}
132
117
}
133
118
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
+
134
170
impl Type < MySql > for NaiveDate {
135
171
fn type_info ( ) -> MySqlTypeInfo {
136
172
MySqlTypeInfo :: binary ( ColumnType :: Date )
@@ -155,7 +191,14 @@ impl<'r> Decode<'r, MySql> for NaiveDate {
155
191
fn decode ( value : MySqlValueRef < ' r > ) -> Result < Self , BoxDynError > {
156
192
match value. format ( ) {
157
193
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 ( ) )
159
202
}
160
203
161
204
MySqlValueFormat :: Text => {
@@ -214,6 +257,10 @@ impl<'r> Decode<'r, MySql> for NaiveDateTime {
214
257
MySqlValueFormat :: Binary => {
215
258
let buf = value. as_bytes ( ) ?;
216
259
260
+ if buf. is_empty ( ) {
261
+ return Err ( "empty buffer" . into ( ) ) ;
262
+ }
263
+
217
264
let len = buf[ 0 ] ;
218
265
let date = decode_date ( & buf[ 1 ..] ) ?. ok_or ( UnexpectedNullError ) ?;
219
266
0 commit comments