diesel/mysql_like/types/date_and_time/
time.rs1use std::os::raw as libc;
2use time::{
3 Date as NaiveDate, Month, OffsetDateTime, PrimitiveDateTime, Time as NaiveTime, UtcOffset,
4};
5
6use crate::deserialize::{self, FromSql};
7use crate::mysql_like::{MysqlLikeBackend, MysqlValue};
8use crate::serialize::{self, Output, ToSql};
9use crate::sql_types::{Date, Datetime, Time, Timestamp};
10
11use super::{MysqlTime, MysqlTimestampType};
12
13fn to_time(dt: MysqlTime) -> Result<NaiveTime, Box<dyn core::error::Error>> {
14 for (name, field) in [
15 ("year", dt.year),
16 ("month", dt.month),
17 ("day", dt.day),
18 ("offset", dt.time_zone_displacement.try_into()?),
19 ] {
20 if field != 0 {
21 return Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Unable to convert {0:?} to time: {1} must be 0",
dt, name))
})format!("Unable to convert {dt:?} to time: {name} must be 0").into());
22 }
23 }
24
25 let hour: u8 = dt.hour.try_into()?;
26 let minute: u8 = dt.minute.try_into()?;
27 let second: u8 = dt.second.try_into()?;
28 let microsecond: u32 = dt.second_part.try_into()?;
29
30 Ok(NaiveTime::from_hms_micro(
31 hour,
32 minute,
33 second,
34 microsecond,
35 )?)
36}
37
38fn to_datetime(dt: MysqlTime) -> Result<OffsetDateTime, Box<dyn core::error::Error>> {
39 let year: i32 = dt.year.try_into()?;
40 let month: u8 = dt.month.try_into()?;
41 let month: Month = month.try_into()?;
42 let day: u8 = dt.day.try_into()?;
43 let hour: u8 = dt.hour.try_into()?;
44 let minute: u8 = dt.minute.try_into()?;
45 let second: u8 = dt.second.try_into()?;
46 let microsecond: u32 = dt.second_part.try_into()?;
47 let offset = UtcOffset::from_whole_seconds(dt.time_zone_displacement)?;
48
49 Ok(PrimitiveDateTime::new(
50 NaiveDate::from_calendar_date(year, month, day)?,
51 NaiveTime::from_hms_micro(hour, minute, second, microsecond)?,
52 )
53 .assume_offset(offset))
54}
55
56fn to_primitive_datetime(dt: OffsetDateTime) -> PrimitiveDateTime {
57 let dt = dt.to_offset(UtcOffset::UTC);
58 PrimitiveDateTime::new(dt.date(), dt.time())
59}
60
61#[cfg(feature = "time")]
63impl<DB: MysqlLikeBackend> ToSql<Datetime, DB> for PrimitiveDateTime {
64 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
65 let mysql_time = MysqlTime {
66 year: self.year().try_into()?,
67 month: self.month() as libc::c_uint,
68 day: self.day() as libc::c_uint,
69 hour: self.hour() as libc::c_uint,
70 minute: self.minute() as libc::c_uint,
71 second: self.second() as libc::c_uint,
72 second_part: libc::c_ulong::from(self.microsecond()),
73 neg: false,
74 time_type: MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME,
75 time_zone_displacement: 0,
76 };
77
78 <MysqlTime as ToSql<Timestamp, DB>>::to_sql(&mysql_time, &mut out.reborrow())
79 }
80}
81
82#[cfg(feature = "time")]
83impl<DB: MysqlLikeBackend> FromSql<Datetime, DB> for PrimitiveDateTime {
84 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
85 let mysql_time = <MysqlTime as FromSql<Timestamp, DB>>::from_sql(bytes)?;
86
87 to_datetime(mysql_time)
88 .map(to_primitive_datetime)
89 .map_err(|err| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Cannot parse this date: {0:?}: {1}",
mysql_time, err))
})format!("Cannot parse this date: {mysql_time:?}: {err}").into())
90 }
91}
92
93#[cfg(feature = "time")]
95impl<DB: MysqlLikeBackend> ToSql<Timestamp, DB> for PrimitiveDateTime {
96 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
97 <PrimitiveDateTime as ToSql<Datetime, DB>>::to_sql(self, out)
98 }
99}
100
101#[cfg(feature = "time")]
102impl<DB: MysqlLikeBackend> FromSql<Timestamp, DB> for PrimitiveDateTime {
103 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
104 <PrimitiveDateTime as FromSql<Datetime, DB>>::from_sql(bytes)
105 }
106}
107
108#[cfg(feature = "time")]
110impl<DB: MysqlLikeBackend> ToSql<Datetime, DB> for OffsetDateTime {
111 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
112 let prim = to_primitive_datetime(*self);
113 <PrimitiveDateTime as ToSql<Datetime, DB>>::to_sql(&prim, &mut out.reborrow())
114 }
115}
116
117#[cfg(feature = "time")]
118impl<DB: MysqlLikeBackend> FromSql<Datetime, DB> for OffsetDateTime {
119 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
120 let prim = <PrimitiveDateTime as FromSql<Datetime, DB>>::from_sql(bytes)?;
121 Ok(prim.assume_offset(UtcOffset::UTC))
122 }
123}
124
125#[cfg(feature = "time")]
127impl<DB: MysqlLikeBackend> ToSql<Timestamp, DB> for OffsetDateTime {
128 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
129 <OffsetDateTime as ToSql<Datetime, DB>>::to_sql(self, out)
130 }
131}
132
133#[cfg(feature = "time")]
134impl<DB: MysqlLikeBackend> FromSql<Timestamp, DB> for OffsetDateTime {
135 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
136 <OffsetDateTime as FromSql<Datetime, DB>>::from_sql(bytes)
137 }
138}
139
140#[cfg(feature = "time")]
141impl<DB: MysqlLikeBackend> ToSql<Time, DB> for NaiveTime {
142 fn to_sql<'b>(&'b self, out: &mut serialize::Output<'b, '_, DB>) -> serialize::Result {
143 let mysql_time = MysqlTime {
144 hour: self.hour() as libc::c_uint,
145 minute: self.minute() as libc::c_uint,
146 second: self.second() as libc::c_uint,
147 day: 0,
148 month: 0,
149 second_part: 0,
150 year: 0,
151 neg: false,
152 time_type: MysqlTimestampType::MYSQL_TIMESTAMP_TIME,
153 time_zone_displacement: 0,
154 };
155
156 <MysqlTime as ToSql<Time, DB>>::to_sql(&mysql_time, &mut out.reborrow())
157 }
158}
159
160#[cfg(feature = "time")]
161impl<DB: MysqlLikeBackend> FromSql<Time, DB> for NaiveTime {
162 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
163 let mysql_time = <MysqlTime as FromSql<Time, DB>>::from_sql(bytes)?;
164
165 to_time(mysql_time)
166 .map_err(|err| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Unable to convert {0:?} to time: {1}",
mysql_time, err))
})format!("Unable to convert {mysql_time:?} to time: {err}").into())
167 }
168}
169
170#[cfg(feature = "time")]
171impl<DB: MysqlLikeBackend> ToSql<Date, DB> for NaiveDate {
172 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
173 let mysql_time = MysqlTime {
174 year: self.year().try_into()?,
175 month: self.month() as libc::c_uint,
176 day: self.day() as libc::c_uint,
177 hour: 0,
178 minute: 0,
179 second: 0,
180 second_part: 0,
181 neg: false,
182 time_type: MysqlTimestampType::MYSQL_TIMESTAMP_DATE,
183 time_zone_displacement: 0,
184 };
185
186 <MysqlTime as ToSql<Date, DB>>::to_sql(&mysql_time, &mut out.reborrow())
187 }
188}
189
190#[cfg(feature = "time")]
191impl<DB: MysqlLikeBackend> FromSql<Date, DB> for NaiveDate {
192 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
193 let mysql_time = <MysqlTime as FromSql<Date, DB>>::from_sql(bytes)?;
194
195 to_datetime(mysql_time)
196 .map_err(|err| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Unable to convert {0:?} to time: {1}",
mysql_time, err))
})format!("Unable to convert {mysql_time:?} to time: {err}").into())
197 .and_then(|dt| {
198 let prim = to_primitive_datetime(dt);
199 if prim.time() == NaiveTime::MIDNIGHT {
200 Ok(prim.date())
201 } else {
202 Err(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Unable to convert {0:?} to date: non-0 time part",
prim))
})format!("Unable to convert {prim:?} to date: non-0 time part").into())
203 }
204 })
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 extern crate dotenvy;
211 extern crate time;
212
213 use time::macros::{date, datetime, time};
214 use time::{Date as NaiveDate, Duration, OffsetDateTime, Time as NaiveTime};
215
216 use super::to_primitive_datetime;
217
218 use crate::dsl::{now, sql};
219 use crate::prelude::*;
220 use crate::select;
221 use crate::sql_types::{Date, Datetime, Time, Timestamp};
222 use crate::test_helpers::connection;
223
224 #[diesel_test_helper::test]
225 fn unix_epoch_encodes_correctly() {
226 let connection = &mut connection();
227 let time = datetime!(1970-1-1 0:0:0);
228 let query = select(sql::<Timestamp>("CAST('1970-01-01' AS DATETIME)").eq(time));
229 assert!(query.get_result::<bool>(connection).unwrap());
230 let query = select(sql::<Datetime>("CAST('1970-01-01' AS DATETIME)").eq(time));
231 assert!(query.get_result::<bool>(connection).unwrap());
232 }
233
234 #[diesel_test_helper::test]
235 fn unix_epoch_decodes_correctly() {
236 let connection = &mut connection();
237 let time = datetime!(1970-1-1 0:0:0);
238 let epoch_from_sql =
239 select(sql::<Timestamp>("CAST('1970-01-01' AS DATETIME)")).get_result(connection);
240 assert_eq!(Ok(time), epoch_from_sql);
241 let epoch_from_sql =
242 select(sql::<Datetime>("CAST('1970-01-01' AS DATETIME)")).get_result(connection);
243 assert_eq!(Ok(time), epoch_from_sql);
244 }
245
246 #[diesel_test_helper::test]
247 fn times_relative_to_now_encode_correctly() {
248 let connection = &mut connection();
249 let time = to_primitive_datetime(OffsetDateTime::now_utc()) + Duration::days(1);
250 let query = select(now.lt(time));
251 assert!(query.get_result::<bool>(connection).unwrap());
252
253 let time = to_primitive_datetime(OffsetDateTime::now_utc()) - Duration::days(1);
254 let query = select(now.gt(time));
255 assert!(query.get_result::<bool>(connection).unwrap());
256 }
257
258 #[diesel_test_helper::test]
259 fn times_of_day_encode_correctly() {
260 let connection = &mut connection();
261
262 let midnight = time!(0:0:0);
263 let query = select(sql::<Time>("CAST('00:00:00' AS TIME)").eq(midnight));
264 assert!(query.get_result::<bool>(connection).unwrap());
265
266 let noon = time!(12:0:0);
267 let query = select(sql::<Time>("CAST('12:00:00' AS TIME)").eq(noon));
268 assert!(query.get_result::<bool>(connection).unwrap());
269
270 let roughly_half_past_eleven = time!(23:37:4);
271 let query = select(sql::<Time>("CAST('23:37:04' AS TIME)").eq(roughly_half_past_eleven));
272 assert!(query.get_result::<bool>(connection).unwrap());
273 }
274
275 #[diesel_test_helper::test]
276 fn times_of_day_decode_correctly() {
277 let connection = &mut connection();
278 let midnight = time!(0:0:0);
279 let query = select(sql::<Time>("CAST('00:00:00' AS TIME)"));
280 assert_eq!(Ok(midnight), query.get_result::<NaiveTime>(connection));
281
282 let noon = time!(12:0:0);
283 let query = select(sql::<Time>("CAST('12:00:00' AS TIME)"));
284 assert_eq!(Ok(noon), query.get_result::<NaiveTime>(connection));
285
286 let roughly_half_past_eleven = time!(23:37:4);
287 let query = select(sql::<Time>("CAST('23:37:04' AS TIME)"));
288 assert_eq!(
289 Ok(roughly_half_past_eleven),
290 query.get_result::<NaiveTime>(connection)
291 );
292 }
293
294 #[diesel_test_helper::test]
295 fn dates_encode_correctly() {
296 let connection = &mut connection();
297 let january_first_2000 = date!(2000 - 1 - 1);
298 let query = select(sql::<Date>("CAST('2000-1-1' AS DATE)").eq(january_first_2000));
299 assert!(query.get_result::<bool>(connection).unwrap());
300
301 let january_first_2018 = date!(2018 - 1 - 1);
302 let query = select(sql::<Date>("CAST('2018-1-1' AS DATE)").eq(january_first_2018));
303 assert!(query.get_result::<bool>(connection).unwrap());
304 }
305
306 #[diesel_test_helper::test]
307 fn dates_decode_correctly() {
308 let connection = &mut connection();
309 let january_first_2000 = date!(2000 - 1 - 1);
310 let query = select(sql::<Date>("CAST('2000-1-1' AS DATE)"));
311 assert_eq!(
312 Ok(january_first_2000),
313 query.get_result::<NaiveDate>(connection)
314 );
315
316 let january_first_2018 = date!(2018 - 1 - 1);
317 let query = select(sql::<Date>("CAST('2018-1-1' AS DATE)"));
318 assert_eq!(
319 Ok(january_first_2018),
320 query.get_result::<NaiveDate>(connection)
321 );
322
323 crate::sql_query("SET sql_mode = (SELECT REPLACE(@@sql_mode, 'NO_ZERO_DATE,', ''))")
324 .execute(connection)
325 .unwrap();
326 let query = select(sql::<Date>("CAST('0000-00-00' AS DATE)"));
327 assert!(query.get_result::<NaiveDate>(connection).is_err());
328 }
329}