1#[cfg(not(feature = "tds73"))]
9use super::DateTime as DateTime1;
10#[cfg(feature = "tds73")]
11use super::{Date, DateTime2, DateTimeOffset, Time};
12use crate::tds::codec::ColumnData;
13#[cfg(feature = "tds73")]
14#[cfg_attr(docsrs, doc(cfg(feature = "tds73")))]
15pub use chrono::offset::{FixedOffset, Utc};
16pub use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
17
18#[inline]
19fn from_days(days: i64, start_year: i32) -> NaiveDate {
20 let base = NaiveDate::from_ymd_opt(start_year, 1, 1).unwrap();
23 base.checked_add_signed(chrono::Duration::days(days))
24 .unwrap_or(if days < 0 {
25 NaiveDate::MIN
26 } else {
27 NaiveDate::MAX
28 })
29}
30
31#[inline]
35#[cfg(feature = "tds73")]
36fn nanos_from_increments(increments: u64, scale: u8) -> i64 {
37 let pow = 9u32.saturating_sub(scale as u32);
38 increments
39 .saturating_mul(10u64.saturating_pow(pow))
40 .min(i64::MAX as u64) as i64
41}
42
43fn from_millis(ms: u32) -> crate::Result<NaiveTime> {
44 NaiveTime::from_num_seconds_from_midnight_opt(ms / 1000, ms % 1000 * 1_000_000)
45 .ok_or_else(|| crate::Error::Conversion(format!("{ms} ms is past midnight").into()))
46}
47
48fn from_datetime(dt: super::DateTime) -> crate::Result<NaiveDateTime> {
49 let time = from_millis(dt.millis_of_day()?)?;
50 Ok(NaiveDateTime::new(from_days(dt.days() as i64, 1900), time))
51}
52
53#[cfg(feature = "tds73")]
54fn from_smalldatetime(dt: super::SmallDateTime) -> crate::Result<NaiveDateTime> {
55 let time = from_millis(dt.minutes_of_day()? * 60_000)?;
56 Ok(NaiveDateTime::new(from_days(dt.days() as i64, 1900), time))
57}
58
59impl<'a> crate::FromSql<'a> for NaiveDateTime {
60 fn from_sql(data: &'a ColumnData<'static>) -> crate::Result<Option<Self>> {
61 match data {
62 ColumnData::DateTime(dt) => dt.map(from_datetime).transpose(),
63 #[cfg(feature = "tds73")]
64 ColumnData::SmallDateTime(dt) => dt.map(from_smalldatetime).transpose(),
65 #[cfg(feature = "tds73")]
66 ColumnData::DateTime2(dt) => Ok(dt.map(|dt| {
67 let ns = nanos_from_increments(dt.time.increments, dt.time.scale);
68 NaiveDateTime::new(
69 from_days(dt.date.days() as i64, 1),
70 NaiveTime::MIN + chrono::Duration::nanoseconds(ns),
71 )
72 })),
73 _ => Err(crate::Error::Conversion(
74 format!("cannot interpret {:?} as an NaiveDateTime value", data).into(),
75 )),
76 }
77 }
78}
79
80impl crate::FromSqlOwned for NaiveDateTime {
81 fn from_sql_owned(data: ColumnData<'static>) -> crate::Result<Option<Self>> {
82 <Self as crate::FromSql>::from_sql(&data)
83 }
84}
85
86#[inline]
87fn to_days(date: NaiveDate, start_year: i32) -> i64 {
88 date.signed_duration_since(NaiveDate::from_ymd_opt(start_year, 1, 1).unwrap())
89 .num_days()
90}
91
92#[inline]
98#[cfg(not(feature = "tds73"))]
99fn to_sec_fragments(time: NaiveTime) -> u32 {
100 use chrono::Timelike;
101
102 match time.nanosecond() {
103 n if n >= 1_000_000_000 => DateTime1::LEAP_SECOND,
104 n => {
105 let ns = u64::from(time.num_seconds_from_midnight()) * 1_000_000_000 + u64::from(n);
107 ((ns * 3 + 5_000_000) / 10_000_000) as u32
108 }
109 }
110}
111
112#[cfg(not(feature = "tds73"))]
114fn to_datetime(dt: NaiveDateTime) -> DateTime1 {
115 let mut days = to_days(dt.date(), 1900) as i32;
116 let mut ticks = to_sec_fragments(dt.time());
117
118 if ticks == 300 * 86_400 {
119 days += 1;
120 ticks = 0;
121 }
122
123 DateTime1::new(days, ticks)
124}
125
126#[cfg(feature = "tds73")]
127from_sql!(
128 NaiveTime:
129 ColumnData::Time(ref time) => time.map(|time| {
130 let ns = nanos_from_increments(time.increments, time.scale);
131 NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns)
132 });
133 NaiveDate:
134 ColumnData::Date(ref date) => date.map(|date| from_days(date.days() as i64, 1));
135 chrono::DateTime<Utc>:
136 ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
138 let date = from_days(dto.datetime2.date.days() as i64, 1);
139 let ns = nanos_from_increments(dto.datetime2.time.increments, dto.datetime2.time.scale);
140 let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
141
142 chrono::DateTime::from_naive_utc_and_offset(NaiveDateTime::new(date, time), Utc)
143 }),
144 ColumnData::DateTime2(ref dt2) => dt2.map(|dt2| {
145 let date = from_days(dt2.date.days() as i64, 1);
146 let ns = nanos_from_increments(dt2.time.increments, dt2.time.scale);
147 let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
148 let naive = NaiveDateTime::new(date, time);
149
150 chrono::DateTime::from_naive_utc_and_offset(naive, Utc)
151 });
152 chrono::DateTime<FixedOffset>: ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
153 let date = from_days(dto.datetime2.date.days() as i64, 1);
154 let ns = nanos_from_increments(dto.datetime2.time.increments, dto.datetime2.time.scale);
155 let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
156
157 let offset = FixedOffset::east_opt((dto.offset as i32) * 60).unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
158 let naive = NaiveDateTime::new(date, time);
159
160 chrono::DateTime::from_naive_utc_and_offset(naive, offset)
161 })
162);
163
164#[cfg(feature = "tds73")]
168fn to_time(time: NaiveTime) -> Time {
169 use chrono::Timelike;
170
171 let increments = match time.nanosecond() {
172 n if n >= 1_000_000_000 => Time::LEAP_SECOND,
173 n => (u64::from(time.num_seconds_from_midnight()) * 1_000_000_000 + u64::from(n)) / 100,
174 };
175
176 Time {
177 increments,
178 scale: 7,
179 }
180}
181
182#[cfg(feature = "tds73")]
183fn to_datetime2(naive: NaiveDateTime) -> DateTime2 {
184 DateTime2::new(
185 Date::from_signed_days(to_days(naive.date(), 1)),
186 to_time(naive.time()),
187 )
188}
189
190#[cfg(feature = "tds73")]
193fn to_datetimeoffset(dt: &chrono::DateTime<FixedOffset>) -> DateTimeOffset {
194 let secs = dt.offset().local_minus_utc();
195 let offset = match secs % 60 {
196 0 => (secs / 60) as i16,
198 _ => DateTimeOffset::OFFSET_WITH_SECONDS,
199 };
200
201 DateTimeOffset::new(to_datetime2(dt.naive_utc()), offset)
202}
203
204#[cfg(feature = "tds73")]
205to_sql!(self_,
206 NaiveDate: (ColumnData::Date, Date::from_signed_days(to_days(*self_, 1)));
207 NaiveTime: (ColumnData::Time, to_time(*self_));
208 NaiveDateTime: (ColumnData::DateTime2, to_datetime2(*self_));
209 chrono::DateTime<Utc>: (ColumnData::DateTime2, to_datetime2(self_.naive_utc()));
210 chrono::DateTime<FixedOffset>: (ColumnData::DateTimeOffset, to_datetimeoffset(self_));
211);
212
213#[cfg(feature = "tds73")]
214into_sql!(self_,
215 NaiveDate: (ColumnData::Date, Date::from_signed_days(to_days(self_, 1)));
216 NaiveTime: (ColumnData::Time, to_time(self_));
217 NaiveDateTime: (ColumnData::DateTime2, to_datetime2(self_));
218 chrono::DateTime<Utc>: (ColumnData::DateTime2, to_datetime2(self_.naive_utc()));
219 chrono::DateTime<FixedOffset>: (ColumnData::DateTimeOffset, to_datetimeoffset(&self_));
220);
221
222#[cfg(not(feature = "tds73"))]
223to_sql!(self_,
224 NaiveDateTime: (ColumnData::DateTime, to_datetime(*self_));
225);
226
227#[cfg(not(feature = "tds73"))]
228into_sql!(self_,
229 NaiveDateTime: (ColumnData::DateTime, to_datetime(self_));
230);
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::{FromSql, IntoSql};
236
237 #[test]
238 fn from_days_clamps_on_overflow() {
239 assert_eq!(from_days(200_000_000, 1), NaiveDate::MAX);
244 assert_eq!(from_days(-200_000_000, 1), NaiveDate::MIN);
245 }
246
247 #[test]
248 fn datetime_rounds_to_the_millisecond() {
249 let get = |ticks| {
250 let cd = ColumnData::DateTime(Some(crate::tds::time::DateTime::new(0, ticks)));
251 NaiveDateTime::from_sql(&cd).unwrap().unwrap().time()
252 };
253 assert_eq!(get(300), NaiveTime::from_hms_opt(0, 0, 1).unwrap());
254 assert_eq!(
255 get(299),
256 NaiveTime::from_hms_milli_opt(0, 0, 0, 997).unwrap()
257 );
258 assert_eq!(get(2), NaiveTime::from_hms_milli_opt(0, 0, 0, 7).unwrap());
259 assert_eq!(
260 get(25919999),
261 NaiveTime::from_hms_milli_opt(23, 59, 59, 997).unwrap()
262 );
263 }
264
265 #[test]
266 fn out_of_range_times_are_conversion_errors() {
267 use crate::tds::time::{DateTime, SmallDateTime};
268 use crate::FromSqlOwned;
269
270 let dt = ColumnData::DateTime(Some(DateTime::new(0, 300 * 86_400)));
271 assert!(matches!(
272 NaiveDateTime::from_sql(&dt),
273 Err(crate::Error::Conversion(_))
274 ));
275 assert!(NaiveDateTime::from_sql_owned(dt).is_err());
276
277 let sdt = ColumnData::SmallDateTime(Some(SmallDateTime::new(0, 1440)));
278 #[cfg(feature = "tds73")]
279 assert!(matches!(
280 NaiveDateTime::from_sql(&sdt),
281 Err(crate::Error::Conversion(_))
282 ));
283 #[cfg(not(feature = "tds73"))]
284 assert!(NaiveDateTime::from_sql(&sdt).is_err());
285 }
286
287 #[cfg(feature = "tds73")]
288 #[test]
289 fn smalldatetime_converts() {
290 let cd = ColumnData::SmallDateTime(Some(crate::tds::time::SmallDateTime::new(1, 1439)));
291 assert_eq!(
292 NaiveDateTime::from_sql(&cd).unwrap(),
293 Some(NaiveDateTime::new(
294 NaiveDate::from_ymd_opt(1900, 1, 2).unwrap(),
295 NaiveTime::from_hms_opt(23, 59, 0).unwrap()
296 ))
297 );
298 }
299
300 #[cfg(not(feature = "tds73"))]
301 #[test]
302 fn to_sec_fragments_converts() {
303 assert_eq!(
305 to_sec_fragments(NaiveTime::from_hms_opt(0, 0, 1).unwrap()),
306 300
307 );
308 }
309
310 #[cfg(not(feature = "tds73"))]
311 #[test]
312 fn to_datetime_rounds_to_the_nearest_tick_and_carries_the_day() {
313 let at = |h, m, s, nano| {
314 NaiveDate::from_ymd_opt(1900, 1, 1)
315 .unwrap()
316 .and_hms_nano_opt(h, m, s, nano)
317 .unwrap()
318 };
319 let ticks = |dt: NaiveDateTime| {
320 let dt = to_datetime(dt);
321 (dt.days(), dt.seconds_fragments())
322 };
323
324 assert_eq!(ticks(at(0, 0, 0, 999_000_000)), (0, 300));
326 assert_eq!(ticks(at(0, 0, 0, 998_300_000)), (0, 299));
327 assert_eq!(ticks(at(0, 0, 0, 998_500_000)), (0, 300));
328 assert_eq!(ticks(at(23, 59, 59, 999_000_000)), (1, 0));
329 assert_eq!(ticks(at(23, 59, 59, 998_000_000)), (0, 25_919_999));
330 }
331
332 #[cfg(feature = "tds73")]
333 #[test]
334 fn naive_date_round_trip() {
335 let date = NaiveDate::from_ymd_opt(2021, 6, 15).unwrap();
336 let cd: ColumnData<'static> = date.into_sql();
337 assert!(matches!(cd, ColumnData::Date(Some(_))));
338 assert_eq!(NaiveDate::from_sql(&cd).unwrap(), Some(date));
339 }
340
341 #[cfg(feature = "tds73")]
342 #[test]
343 fn naive_time_round_trip() {
344 let time = NaiveTime::from_hms_opt(13, 37, 42).unwrap();
345 let cd: ColumnData<'static> = time.into_sql();
346 assert!(matches!(cd, ColumnData::Time(Some(_))));
347 assert_eq!(NaiveTime::from_sql(&cd).unwrap(), Some(time));
348 }
349
350 #[cfg(feature = "tds73")]
351 #[test]
352 fn naive_datetime_round_trip() {
353 let dt = NaiveDateTime::new(
354 NaiveDate::from_ymd_opt(2000, 12, 31).unwrap(),
355 NaiveTime::from_hms_opt(23, 59, 58).unwrap(),
356 );
357 let cd: ColumnData<'static> = dt.into_sql();
358 assert!(matches!(cd, ColumnData::DateTime2(Some(_))));
359 assert_eq!(NaiveDateTime::from_sql(&cd).unwrap(), Some(dt));
360 }
361
362 #[cfg(feature = "tds73")]
363 #[test]
364 fn datetime_utc_round_trip() {
365 let naive = NaiveDateTime::new(
366 NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
367 NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
368 );
369 let dt = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
370 let cd: ColumnData<'static> = dt.into_sql();
371 assert!(matches!(cd, ColumnData::DateTime2(Some(_))));
372 assert_eq!(chrono::DateTime::<Utc>::from_sql(&cd).unwrap(), Some(dt));
373 }
374
375 #[cfg(feature = "tds73")]
376 #[test]
377 fn datetime_fixed_offset_round_trip() {
378 let offset = FixedOffset::east_opt(2 * 3600).unwrap();
379 let naive = NaiveDateTime::new(
380 NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
381 NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
382 );
383 let dt = chrono::DateTime::from_naive_utc_and_offset(naive, offset);
384 let cd: ColumnData<'static> = dt.into_sql();
385 assert!(matches!(cd, ColumnData::DateTimeOffset(Some(_))));
386 assert_eq!(
387 chrono::DateTime::<FixedOffset>::from_sql(&cd).unwrap(),
388 Some(dt)
389 );
390 }
391
392 #[cfg(feature = "tds73")]
396 #[test]
397 fn naive_datetime_from_legacy_datetime_column() {
398 let cd = ColumnData::DateTime(Some(crate::tds::time::DateTime::new(0, 0)));
399 let dt = NaiveDateTime::from_sql(&cd).unwrap().unwrap();
400 assert_eq!(
401 dt,
402 NaiveDateTime::new(
403 NaiveDate::from_ymd_opt(1900, 1, 1).unwrap(),
404 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
405 )
406 );
407 }
408
409 #[cfg(feature = "tds73")]
412 #[test]
413 fn datetime_offset_reads_as_utc() {
414 let offset = FixedOffset::east_opt(2 * 3600).unwrap();
415 let naive = NaiveDateTime::new(
416 NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
417 NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
418 );
419 let dt: chrono::DateTime<FixedOffset> =
420 chrono::DateTime::from_naive_utc_and_offset(naive, offset);
421 let cd: ColumnData<'static> = dt.into_sql();
422 assert!(matches!(cd, ColumnData::DateTimeOffset(Some(_))));
423
424 let utc = chrono::DateTime::<Utc>::from_sql(&cd).unwrap().unwrap();
426 assert_eq!(utc.naive_utc(), naive);
427 assert_eq!(utc, dt);
428 }
429
430 fn encode(data: ColumnData<'_>) -> crate::Result<()> {
431 use crate::tds::codec::Encode;
432
433 let mut buf = bytes::BytesMut::new();
434 data.encode(&mut crate::BytesMutWithTypeInfo::new(&mut buf))
435 }
436
437 #[cfg(feature = "tds73")]
438 #[test]
439 fn dates_outside_year_1_to_9999_are_encode_errors() {
440 use crate::ToSql;
441 use chrono::TimeZone;
442
443 let plus_one = FixedOffset::east_opt(3600).unwrap();
444
445 for (y, m, d) in [(0, 12, 31), (-5, 1, 1), (10000, 1, 1), (50000, 1, 1)] {
446 let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
447 let naive = date.and_hms_opt(12, 0, 0).unwrap();
448 let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
449 let fixed = chrono::DateTime::<FixedOffset>::from_naive_utc_and_offset(naive, plus_one);
450
451 for data in [
452 date.to_sql(),
453 naive.to_sql(),
454 utc.to_sql(),
455 fixed.to_sql(),
456 date.into_sql(),
457 naive.into_sql(),
458 utc.into_sql(),
459 fixed.into_sql(),
460 ] {
461 let err = encode(data).unwrap_err();
462 assert!(
463 matches!(err, crate::Error::Conversion(_)),
464 "{date}: {err:?}"
465 );
466 }
467 }
468
469 let local = NaiveDate::from_ymd_opt(1, 1, 1)
471 .unwrap()
472 .and_hms_opt(0, 30, 0)
473 .unwrap();
474 let dto = plus_one.from_local_datetime(&local).unwrap();
475 assert!(matches!(
476 encode(dto.to_sql()),
477 Err(crate::Error::Conversion(_))
478 ));
479 }
480
481 #[cfg(feature = "tds73")]
482 #[test]
483 fn year_1_and_9999_encode() {
484 use crate::ToSql;
485
486 let first = NaiveDate::from_ymd_opt(1, 1, 1).unwrap();
487 let last = NaiveDate::from_ymd_opt(9999, 12, 31).unwrap();
488
489 for date in [first, last] {
490 let naive = date.and_hms_nano_opt(23, 59, 59, 999_999_900).unwrap();
491 let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
492 for data in [date.to_sql(), naive.to_sql(), utc.to_sql()] {
493 encode(data).unwrap();
494 }
495 }
496 }
497
498 #[cfg(feature = "tds73")]
499 #[test]
500 fn leap_seconds_are_encode_errors() {
501 use crate::ToSql;
502
503 for (h, m) in [(23, 59), (12, 34)] {
505 let time = NaiveTime::from_hms_nano_opt(h, m, 59, 1_500_000_000).unwrap();
506 let naive = NaiveDate::from_ymd_opt(2016, 12, 31)
507 .unwrap()
508 .and_time(time);
509 let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
510 let fixed = utc.fixed_offset();
511
512 for data in [
513 time.to_sql(),
514 naive.to_sql(),
515 utc.to_sql(),
516 fixed.to_sql(),
517 time.into_sql(),
518 naive.into_sql(),
519 utc.into_sql(),
520 fixed.into_sql(),
521 ] {
522 let err = encode(data).unwrap_err();
523 assert!(
524 matches!(&err, crate::Error::Conversion(m) if m.contains("leap")),
525 "{time}: {err:?}"
526 );
527 }
528 }
529
530 let last = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap();
531 encode(last.to_sql()).unwrap();
532 }
533
534 #[cfg(feature = "tds73")]
535 #[test]
536 fn offsets_with_seconds_are_encode_errors() {
537 use crate::ToSql;
538 use chrono::TimeZone;
539
540 let naive = NaiveDate::from_ymd_opt(2024, 5, 6)
541 .unwrap()
542 .and_hms_opt(1, 2, 3)
543 .unwrap();
544
545 for secs in [30, -30, 3600 + 1] {
546 let dto = FixedOffset::east_opt(secs)
547 .unwrap()
548 .from_local_datetime(&naive)
549 .unwrap();
550 for data in [dto.to_sql(), dto.into_sql()] {
551 let err = encode(data).unwrap_err();
552 assert!(matches!(err, crate::Error::Conversion(_)), "{dto}");
553 }
554 }
555
556 for secs in [-14 * 3600, -90 * 60, 14 * 3600] {
557 let dto = FixedOffset::east_opt(secs)
558 .unwrap()
559 .from_local_datetime(&naive)
560 .unwrap();
561 encode(dto.to_sql()).unwrap();
562 }
563 }
564
565 #[cfg(feature = "tds73")]
566 #[test]
567 fn null_maps_to_none() {
568 assert_eq!(NaiveDate::from_sql(&ColumnData::Date(None)).unwrap(), None);
569 assert_eq!(NaiveTime::from_sql(&ColumnData::Time(None)).unwrap(), None);
570 }
571
572 #[cfg(not(feature = "tds73"))]
573 #[test]
574 fn naive_datetime_round_trip_legacy() {
575 let dt = NaiveDateTime::new(
576 NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
577 NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
578 );
579 let cd: ColumnData<'static> = dt.into_sql();
580 assert!(matches!(cd, ColumnData::DateTime(Some(_))));
581 assert_eq!(NaiveDateTime::from_sql(&cd).unwrap(), Some(dt));
582 }
583
584 #[cfg(not(feature = "tds73"))]
585 #[test]
586 fn leap_seconds_are_datetime_encode_errors() {
587 use crate::ToSql;
588
589 for (h, m) in [(23, 59), (12, 34)] {
592 let time = NaiveTime::from_hms_nano_opt(h, m, 59, 1_500_000_000).unwrap();
593 let naive = NaiveDate::from_ymd_opt(2016, 12, 31)
594 .unwrap()
595 .and_time(time);
596
597 for data in [naive.to_sql(), naive.into_sql()] {
598 let err = encode(data).unwrap_err();
599 assert!(
600 matches!(&err, crate::Error::Conversion(m) if m.contains("leap")),
601 "{time}: {err:?}"
602 );
603 }
604 }
605
606 let last = NaiveDate::from_ymd_opt(2016, 12, 31)
607 .unwrap()
608 .and_hms_nano_opt(23, 59, 59, 999_999_999)
609 .unwrap();
610 encode(last.to_sql()).unwrap();
611 }
612}