cdbc_mysql/types/
time.rs

1use std::borrow::Cow;
2use std::convert::TryFrom;
3
4use byteorder::{ByteOrder, LittleEndian};
5use bytes::Buf;
6use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
7
8use cdbc::decode::Decode;
9use cdbc::encode::{Encode, IsNull};
10use cdbc::error::{BoxDynError, UnexpectedNullError};
11use crate::protocol::text::ColumnType;
12use crate::type_info::MySqlTypeInfo;
13use crate::{MySql, MySqlValueFormat, MySqlValueRef};
14use cdbc::types::Type;
15
16impl Type<MySql> for OffsetDateTime {
17    fn type_info() -> MySqlTypeInfo {
18        MySqlTypeInfo::binary(ColumnType::Timestamp)
19    }
20
21    fn compatible(ty: &MySqlTypeInfo) -> bool {
22        matches!(ty.r#type, ColumnType::Datetime | ColumnType::Timestamp)
23    }
24}
25
26impl Encode<'_, MySql> for OffsetDateTime {
27    fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
28        let utc_dt = self.to_offset(UtcOffset::UTC);
29        let primitive_dt = PrimitiveDateTime::new(utc_dt.date(), utc_dt.time());
30
31        Encode::<MySql>::encode(&primitive_dt, buf)
32    }
33}
34
35impl<'r> Decode<'r, MySql> for OffsetDateTime {
36    fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
37        let primitive: PrimitiveDateTime = Decode::<MySql>::decode(value)?;
38
39        Ok(primitive.assume_utc())
40    }
41}
42
43impl Type<MySql> for Time {
44    fn type_info() -> MySqlTypeInfo {
45        MySqlTypeInfo::binary(ColumnType::Time)
46    }
47}
48
49impl Encode<'_, MySql> for Time {
50    fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
51        let len = Encode::<MySql>::size_hint(self) - 1;
52        buf.push(len as u8);
53
54        // Time is not negative
55        buf.push(0);
56
57        // "date on 4 bytes little-endian format" (?)
58        // https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
59        buf.extend_from_slice(&[0_u8; 4]);
60
61        encode_time(self, len > 9, buf);
62
63        IsNull::No
64    }
65
66    fn size_hint(&self) -> usize {
67        if self.nanosecond() == 0 {
68            // if micro_seconds is 0, length is 8 and micro_seconds is not sent
69            9
70        } else {
71            // otherwise length is 12
72            13
73        }
74    }
75}
76
77impl<'r> Decode<'r, MySql> for Time {
78    fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
79        match value.format() {
80            MySqlValueFormat::Binary => {
81                let mut buf = value.as_bytes()?;
82
83                // data length, expecting 8 or 12 (fractional seconds)
84                let len = buf.get_u8();
85
86                // MySQL specifies that if all of hours, minutes, seconds, microseconds
87                // are 0 then the length is 0 and no further data is send
88                // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
89                if len == 0 {
90                    return Ok(Time::from_hms(0,0,0).unwrap());
91                }
92
93                // is negative : int<1>
94                let is_negative = buf.get_u8();
95                assert_eq!(is_negative, 0, "Negative dates/times are not supported");
96
97                // "date on 4 bytes little-endian format" (?)
98                // https://mariadb.com/kb/en/resultset-row/#timestamp-binary-encoding
99                buf.advance(4);
100
101                decode_time(len - 5, buf)
102            }
103
104            MySqlValueFormat::Text => {
105                let s = value.as_str()?;
106
107                // If there are less than 9 digits after the decimal point
108                // We need to zero-pad
109                // TODO: Ask [time] to add a parse % for less-than-fixed-9 nanos
110
111                let s = if s.len() < 20 {
112                    Cow::Owned(format!("{:0<19}", s))
113                } else {
114                    Cow::Borrowed(s)
115                };
116                let format = time::format_description::parse("[hour]:[minute]:[second]")?;
117                Time::parse(&*s as &str, &format).map_err(Into::into)
118            }
119        }
120    }
121}
122
123impl Type<MySql> for Date {
124    fn type_info() -> MySqlTypeInfo {
125        MySqlTypeInfo::binary(ColumnType::Date)
126    }
127}
128
129impl Encode<'_, MySql> for Date {
130    fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
131        buf.push(4);
132
133        encode_date(self, buf);
134
135        IsNull::No
136    }
137
138    fn size_hint(&self) -> usize {
139        5
140    }
141}
142
143impl<'r> Decode<'r, MySql> for Date {
144    fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
145        match value.format() {
146            MySqlValueFormat::Binary => {
147                Ok(decode_date(&value.as_bytes()?[1..])?.ok_or(UnexpectedNullError)?)
148            }
149            MySqlValueFormat::Text => {
150                let s = value.as_str()?;
151                let format = time::format_description::parse("[year]-[month]-[day]")?;
152                Date::parse(s, &format).map_err(Into::into)
153            }
154        }
155    }
156}
157
158impl Type<MySql> for PrimitiveDateTime {
159    fn type_info() -> MySqlTypeInfo {
160        MySqlTypeInfo::binary(ColumnType::Datetime)
161    }
162}
163
164impl Encode<'_, MySql> for PrimitiveDateTime {
165    fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
166        let len = Encode::<MySql>::size_hint(self) - 1;
167        buf.push(len as u8);
168
169        encode_date(&self.date(), buf);
170
171        if len > 4 {
172            encode_time(&self.time(), len > 8, buf);
173        }
174
175        IsNull::No
176    }
177
178    fn size_hint(&self) -> usize {
179        // to save space the packet can be compressed:
180        match (self.hour(), self.minute(), self.second(), self.nanosecond()) {
181            // if hour, minutes, seconds and micro_seconds are all 0,
182            // length is 4 and no other field is sent
183            (0, 0, 0, 0) => 5,
184
185            // if micro_seconds is 0, length is 7
186            // and micro_seconds is not sent
187            (_, _, _, 0) => 8,
188
189            // otherwise length is 11
190            (_, _, _, _) => 12,
191        }
192    }
193}
194
195impl<'r> Decode<'r, MySql> for PrimitiveDateTime {
196    fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
197        match value.format() {
198            MySqlValueFormat::Binary => {
199                let buf = value.as_bytes()?;
200                let len = buf[0];
201                let date = decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?;
202
203                let dt = if len > 4 {
204                    date.with_time(decode_time(len - 4, &buf[5..])?)
205                } else {
206                    date.midnight()
207                };
208
209                Ok(dt)
210            }
211
212            MySqlValueFormat::Text => {
213                let s = value.as_str()?;
214
215                // If there are less than 9 digits after the decimal point
216                // We need to zero-pad
217                // TODO: Ask [time] to add a parse % for less-than-fixed-9 nanos
218
219                let s = if s.len() < 31 {
220                    if s.contains('.') {
221                        Cow::Owned(format!("{:0<30}", s))
222                    } else {
223                        Cow::Owned(format!("{}.000000000", s))
224                    }
225                } else {
226                    Cow::Borrowed(s)
227                };
228                let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]")?;
229                PrimitiveDateTime::parse(&*s, &format).map_err(Into::into)
230            }
231        }
232    }
233}
234
235fn encode_date(date: &Date, buf: &mut Vec<u8>) {
236    // MySQL supports years from 1000 - 9999
237    let year = u16::try_from(date.year())
238        .unwrap_or_else(|_| panic!("Date out of range for Mysql: {}", date));
239
240    buf.extend_from_slice(&year.to_le_bytes());
241    buf.push(date.month() as i32 as u8);
242    buf.push(date.day());
243}
244
245fn decode_date(buf: &[u8]) -> Result<Option<Date>, BoxDynError> {
246    if buf.is_empty() {
247        // zero buffer means a zero date (null)
248        return Ok(None);
249    }
250
251    Date::from_calendar_date(
252        LittleEndian::read_u16(buf) as i32,
253         time::Month::try_from(buf[2] as u8).unwrap(),
254        buf[3] as u8,
255    )
256    .map_err(Into::into)
257    .map(Some)
258}
259
260fn encode_time(time: &Time, include_micros: bool, buf: &mut Vec<u8>) {
261    buf.push(time.hour());
262    buf.push(time.minute());
263    buf.push(time.second());
264
265    if include_micros {
266        buf.extend(&((time.nanosecond() / 1000) as u32).to_le_bytes());
267    }
268}
269
270fn decode_time(len: u8, mut buf: &[u8]) -> Result<Time, BoxDynError> {
271    let hour = buf.get_u8();
272    let minute = buf.get_u8();
273    let seconds = buf.get_u8();
274
275    let micros = if len > 3 {
276        // microseconds : int<EOF>
277        buf.get_uint_le(buf.len())
278    } else {
279        0
280    };
281
282    Time::from_hms_micro(hour, minute, seconds, micros as u32)
283        .map_err(|e| format!("Time out of range for MySQL: {}", e).into())
284}
285
286
287impl Type<MySql> for mco::std::time::time::Time{
288    fn type_info() -> MySqlTypeInfo {
289        MySqlTypeInfo::binary(ColumnType::Timestamp)
290    }
291
292    fn compatible(ty: &MySqlTypeInfo) -> bool {
293        matches!(ty.r#type, ColumnType::Datetime | ColumnType::Timestamp)
294    }
295}
296
297impl Encode<'_, MySql> for mco::std::time::time::Time {
298    fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
299        self.inner.encode_by_ref(buf)
300    }
301}
302
303impl<'r> Decode<'r, MySql> for mco::std::time::time::Time {
304    fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
305        Ok(mco::std::time::time::Time{ inner: OffsetDateTime::decode(value)? })
306    }
307}