1#[cfg(feature = "tds73")]
8use std::time::Duration;
9pub use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
10
11use crate::tds::codec::ColumnData;
12
13#[inline]
14fn from_days(days: i64, start_year: i32) -> Date {
15 let base = Date::from_calendar_date(start_year, Month::January, 1).unwrap();
25 base.checked_add(time::Duration::days(days))
26 .unwrap_or(if days < 0 { Date::MIN } else { Date::MAX })
27}
28
29#[inline]
34#[cfg(feature = "tds73")]
35fn nanos_from_increments(increments: u64, scale: u8) -> u64 {
36 let pow = 9u32.saturating_sub(scale as u32);
37 increments.saturating_mul(10u64.saturating_pow(pow))
38}
39
40fn from_millis(ms: u32) -> crate::Result<Time> {
41 let err = || crate::Error::Conversion(format!("{ms} ms is past midnight").into());
42 let hour = u8::try_from(ms / 3_600_000).map_err(|_| err())?;
43 let (min, sec, milli) = (ms / 60_000 % 60, ms / 1000 % 60, ms % 1000);
44
45 Time::from_hms_milli(hour, min as u8, sec as u8, milli as u16).map_err(|_| err())
46}
47
48fn from_datetime(dt: super::DateTime) -> crate::Result<PrimitiveDateTime> {
49 let time = from_millis(dt.millis_of_day()?)?;
50 Ok(from_days(dt.days() as i64, 1900).with_time(time))
51}
52
53#[cfg(feature = "tds73")]
54fn from_smalldatetime(dt: super::SmallDateTime) -> crate::Result<PrimitiveDateTime> {
55 let time = from_millis(dt.minutes_of_day()? * 60_000)?;
56 Ok(from_days(dt.days() as i64, 1900).with_time(time))
57}
58
59impl<'a> crate::FromSql<'a> for PrimitiveDateTime {
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 from_days(dt.date.days() as i64, 1)
69 .with_time(Time::MIDNIGHT + Duration::from_nanos(ns))
70 })),
71 _ => Err(crate::Error::Conversion(
72 format!("cannot interpret {:?} as an PrimitiveDateTime value", data).into(),
73 )),
74 }
75 }
76}
77
78impl crate::FromSqlOwned for PrimitiveDateTime {
79 fn from_sql_owned(data: ColumnData<'static>) -> crate::Result<Option<Self>> {
80 <Self as crate::FromSql>::from_sql(&data)
81 }
82}
83
84#[inline]
85fn to_days(date: Date, start_year: i32) -> i64 {
86 (date - Date::from_calendar_date(start_year, Month::January, 1).unwrap()).whole_days()
87}
88
89#[inline]
94#[cfg(not(feature = "tds73"))]
95fn to_sec_fragments(from: Time) -> u32 {
96 let nanos: u64 = (from - Time::from_hms(0, 0, 0).unwrap())
97 .whole_nanoseconds()
98 .try_into()
99 .unwrap();
100
101 ((nanos * 3 + 5_000_000) / 10_000_000) as u32
103}
104
105#[cfg(not(feature = "tds73"))]
107fn to_datetime(dt: PrimitiveDateTime) -> super::DateTime {
108 let mut days = to_days(dt.date(), 1900) as i32;
109 let mut ticks = to_sec_fragments(dt.time());
110
111 if ticks == 300 * 86_400 {
112 days += 1;
113 ticks = 0;
114 }
115
116 super::DateTime::new(days, ticks)
117}
118
119#[cfg(feature = "tds73")]
120from_sql!(
121 Time:
122 ColumnData::Time(ref time) => time.map(|time| {
123 let ns = nanos_from_increments(time.increments, time.scale);
124 Time::from_hms(0,0,0).unwrap() + Duration::from_nanos(ns)
125 });
126 Date:
127 ColumnData::Date(ref date) => date.map(|date| from_days(date.days() as i64, 1));
128 OffsetDateTime:
129 ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
130 let date = from_days(dto.datetime2.date.days() as i64, 1);
131 let dt = dto.datetime2;
132
133 let time = Time::from_hms(0,0,0).unwrap()
134 + Duration::from_nanos(nanos_from_increments(dt.time.increments, dt.time.scale));
135
136 let offset = UtcOffset::from_whole_seconds(dto.offset as i32 * 60)
139 .unwrap_or(UtcOffset::UTC);
140
141 let utc = date.with_time(time);
144 match utc.checked_add(time::Duration::seconds(offset.whole_seconds().into())) {
145 Some(local) => local.assume_offset(offset),
146 None => utc.assume_utc(),
147 }
148 })
149);
150
151#[cfg(feature = "tds73")]
152to_sql!(self_,
153 Date: (ColumnData::Date, super::Date::from_signed_days(to_days(*self_, 1)));
154 Time: (ColumnData::Time, {
155 let nanos: u64 = (*self_ - Time::from_hms(0, 0, 0).unwrap()).whole_nanoseconds().try_into().unwrap();
156 let increments = nanos / 100;
157
158 super::Time {increments, scale: 7}
159 });
160 PrimitiveDateTime: (ColumnData::DateTime2, {
161 let time = self_.time();
162 let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap()).whole_nanoseconds().try_into().unwrap();
163 let increments = nanos / 100;
164
165 let date = super::Date::from_signed_days(to_days(self_.date(), 1));
166 let time = super::Time {increments, scale: 7};
167
168 super::DateTime2::new(date, time)
169 });
170 OffsetDateTime: (ColumnData::DateTimeOffset, {
171 let tz = self_.offset();
174 let offset = match tz.seconds_past_minute() {
175 0 => (tz.whole_seconds() / 60) as i16,
176 _ => super::DateTimeOffset::OFFSET_WITH_SECONDS,
177 };
178
179 const DAY_NANOS: i128 = 86_400_000_000_000;
182 const DAYS_TO_UNIX_EPOCH: i64 = 719_162;
183 let utc = self_.unix_timestamp_nanos();
184
185 let date = super::Date::from_signed_days(DAYS_TO_UNIX_EPOCH + utc.div_euclid(DAY_NANOS) as i64);
186 let time = super::Time { increments: (utc.rem_euclid(DAY_NANOS) / 100) as u64, scale: 7 };
187
188 super::DateTimeOffset::new(super::DateTime2::new(date, time), offset)
189 });
190);
191
192#[cfg(not(feature = "tds73"))]
193to_sql!(self_,
194 PrimitiveDateTime: (ColumnData::DateTime, to_datetime(*self_));
195);
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
205 fn from_days_handles_negative_offsets() {
206 assert_eq!(
208 from_days(-1, 1900),
209 Date::from_calendar_date(1899, Month::December, 31).unwrap()
210 );
211
212 let expected = Date::from_calendar_date(1850, Month::January, 1).unwrap();
214 let days = to_days(expected, 1900);
215 assert!(
216 days < 0,
217 "expected a negative day offset for pre-1900 dates"
218 );
219
220 assert_eq!(from_days(days, 1900), expected);
222 }
223
224 #[test]
227 fn datetime_before_1900_decodes() {
228 let expected_date = Date::from_calendar_date(1850, Month::January, 1).unwrap();
229 let days = to_days(expected_date, 1900) as i32;
230
231 let decoded = from_datetime(crate::tds::time::DateTime::new(days, 0)).unwrap();
232
233 assert_eq!(decoded.date(), expected_date);
234 assert_eq!(decoded.time(), Time::from_hms(0, 0, 0).unwrap());
235 }
236
237 #[test]
238 fn from_days_clamps_on_overflow() {
239 assert_eq!(from_days(10_000_000, 1), Date::MAX);
243 assert_eq!(from_days(-10_000_000, 1), Date::MIN);
244 }
245
246 #[test]
247 fn datetime_rounds_to_the_millisecond() {
248 let get = |ticks| {
249 from_datetime(crate::tds::time::DateTime::new(0, ticks))
250 .unwrap()
251 .time()
252 };
253 assert_eq!(get(300), Time::from_hms(0, 0, 1).unwrap());
254 assert_eq!(get(299), Time::from_hms_milli(0, 0, 0, 997).unwrap());
255 assert_eq!(get(2), Time::from_hms_milli(0, 0, 0, 7).unwrap());
256 assert_eq!(
257 get(25919999),
258 Time::from_hms_milli(23, 59, 59, 997).unwrap()
259 );
260 assert!(from_millis(86_400_000).is_err());
261 assert!(from_millis(u32::MAX).is_err());
262 }
263
264 #[test]
265 fn out_of_range_times_are_conversion_errors() {
266 use crate::tds::time::{DateTime, SmallDateTime};
267 use crate::{FromSql, FromSqlOwned};
268
269 let dt = ColumnData::DateTime(Some(DateTime::new(0, 300 * 86_400)));
270 assert!(matches!(
271 PrimitiveDateTime::from_sql(&dt),
272 Err(crate::Error::Conversion(_))
273 ));
274 assert!(PrimitiveDateTime::from_sql_owned(dt).is_err());
275
276 let sdt = ColumnData::SmallDateTime(Some(SmallDateTime::new(0, 1440)));
278 assert!(PrimitiveDateTime::from_sql(&sdt).is_err());
279 }
280
281 #[cfg(not(feature = "tds73"))]
282 #[test]
283 fn to_sec_fragments_converts() {
284 assert_eq!(to_sec_fragments(Time::from_hms(0, 0, 1).unwrap()), 300);
286 }
287
288 #[cfg(not(feature = "tds73"))]
289 #[test]
290 fn to_datetime_rounds_to_the_nearest_tick_and_carries_the_day() {
291 let at = |h, m, s, nano| {
292 Date::from_calendar_date(1900, Month::January, 1)
293 .unwrap()
294 .with_hms_nano(h, m, s, nano)
295 .unwrap()
296 };
297 let ticks = |dt: PrimitiveDateTime| {
298 let dt = to_datetime(dt);
299 (dt.days(), dt.seconds_fragments())
300 };
301
302 assert_eq!(ticks(at(0, 0, 0, 999_000_000)), (0, 300));
304 assert_eq!(ticks(at(0, 0, 0, 998_300_000)), (0, 299));
305 assert_eq!(ticks(at(0, 0, 0, 998_500_000)), (0, 300));
306 assert_eq!(ticks(at(23, 59, 59, 999_000_000)), (1, 0));
307 assert_eq!(ticks(at(23, 59, 59, 998_000_000)), (0, 25_919_999));
308 }
309
310 #[cfg(feature = "tds73")]
311 #[test]
312 fn time_from_sql_and_back() {
313 use crate::FromSql;
314
315 let expected = Time::from_hms(12, 34, 56).unwrap();
317 let nanos: u64 = (expected - Time::from_hms(0, 0, 0).unwrap())
318 .whole_nanoseconds()
319 .try_into()
320 .unwrap();
321 let increments: u64 = nanos / 100;
322
323 let tds_time = super::super::Time::new(increments, 7);
324 let data = ColumnData::Time(Some(tds_time));
325
326 let decoded = Time::from_sql(&data).unwrap().unwrap();
327 assert_eq!(decoded, expected);
328
329 use crate::ToSql;
331 let round_tripped = decoded.to_sql();
332 match round_tripped {
333 ColumnData::Time(Some(t)) => assert_eq!(t.increments, increments),
334 other => panic!("unexpected: {:?}", other),
335 }
336 }
337
338 #[cfg(feature = "tds73")]
339 #[test]
340 fn date_from_sql_and_back() {
341 use crate::{FromSql, ToSql};
342
343 let expected = Date::from_calendar_date(2020, Month::June, 15).unwrap();
344 let days = to_days(expected, 1) as u32;
345
346 let data = ColumnData::Date(Some(super::super::Date::new(days)));
347 let decoded = Date::from_sql(&data).unwrap().unwrap();
348 assert_eq!(decoded, expected);
349
350 match decoded.to_sql() {
351 ColumnData::Date(Some(d)) => assert_eq!(d.days(), days),
352 other => panic!("unexpected: {:?}", other),
353 }
354 }
355
356 #[cfg(feature = "tds73")]
357 #[test]
358 fn primitive_datetime_from_datetime2_and_back() {
359 use crate::{FromSql, ToSql};
360
361 let date = Date::from_calendar_date(2020, Month::June, 15).unwrap();
362 let time = Time::from_hms(1, 2, 3).unwrap();
363 let expected = PrimitiveDateTime::new(date, time);
364
365 let days = to_days(date, 1) as u32;
366 let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap())
367 .whole_nanoseconds()
368 .try_into()
369 .unwrap();
370 let increments = nanos / 100;
371
372 let dt2 = super::super::DateTime2::new(
373 super::super::Date::new(days),
374 super::super::Time::new(increments, 7),
375 );
376 let data = ColumnData::DateTime2(Some(dt2));
377
378 let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
379 assert_eq!(decoded, expected);
380
381 match decoded.to_sql() {
382 ColumnData::DateTime2(Some(dt)) => {
383 assert_eq!(dt.date.days(), days);
384 }
385 other => panic!("unexpected: {:?}", other),
386 }
387 }
388
389 #[cfg(feature = "tds73")]
390 #[test]
391 fn primitive_datetime_from_smalldatetime_and_datetime() {
392 use crate::FromSql;
393
394 let sdt = crate::tds::time::SmallDateTime::new(1, 30); let data = ColumnData::SmallDateTime(Some(sdt));
397 let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
398 assert_eq!(decoded.date(), from_days(1, 1900));
399 assert_eq!(decoded.time(), Time::from_hms(0, 30, 0).unwrap());
400
401 let dt = crate::tds::time::DateTime::new(1, 0);
403 let data = ColumnData::DateTime(Some(dt));
404 let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
405 assert_eq!(decoded.date(), from_days(1, 1900));
406 }
407
408 #[cfg(feature = "tds73")]
411 #[test]
412 fn offset_date_time_past_the_last_local_date_keeps_utc() {
413 use crate::FromSql;
414
415 let dt2 = super::super::DateTime2::new(
416 super::super::Date::new(0xff_ffff),
417 super::super::Time::new(23 * 3600 * 10_000_000, 7),
418 );
419 let dto = super::super::DateTimeOffset::new(dt2, 14 * 60);
420 let data = ColumnData::DateTimeOffset(Some(dto));
421 let decoded = OffsetDateTime::from_sql(&data).unwrap().unwrap();
422
423 let utc = from_days(0xff_ffff, 1).with_time(Time::from_hms(23, 0, 0).unwrap());
424 assert_eq!(decoded, utc.assume_utc());
425 }
426
427 #[cfg(feature = "tds73")]
428 #[test]
429 fn offset_date_time_from_sql_and_back() {
430 use crate::{FromSql, ToSql};
431
432 let date = Date::from_calendar_date(2020, Month::June, 15).unwrap();
433 let time = Time::from_hms(1, 2, 3).unwrap();
434 let days = to_days(date, 1) as u32;
435 let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap())
436 .whole_nanoseconds()
437 .try_into()
438 .unwrap();
439 let increments = nanos / 100;
440
441 let dt2 = super::super::DateTime2::new(
442 super::super::Date::new(days),
443 super::super::Time::new(increments, 7),
444 );
445 let dto = super::super::DateTimeOffset::new(dt2, 60); let data = ColumnData::DateTimeOffset(Some(dto));
448 let decoded = OffsetDateTime::from_sql(&data).unwrap().unwrap();
449
450 assert_eq!(
451 decoded.offset(),
452 UtcOffset::from_whole_seconds(3600).unwrap()
453 );
454
455 match decoded.to_sql() {
456 ColumnData::DateTimeOffset(Some(round_tripped)) => {
457 assert_eq!(round_tripped.offset, 60);
458 }
459 other => panic!("unexpected: {:?}", other),
460 }
461 }
462
463 #[cfg(feature = "tds73")]
464 fn encode(data: ColumnData<'_>) -> crate::Result<()> {
465 use crate::tds::codec::Encode;
466
467 let mut buf = bytes::BytesMut::new();
468 data.encode(&mut crate::BytesMutWithTypeInfo::new(&mut buf))
469 }
470
471 #[cfg(feature = "tds73")]
472 #[test]
473 fn dates_outside_year_1_to_9999_are_encode_errors() {
474 use crate::ToSql;
475
476 let noon = Time::from_hms(12, 0, 0).unwrap();
477 for year in [0, -5, -9999] {
478 let date = Date::from_calendar_date(year, Month::January, 1).unwrap();
479 let dt = PrimitiveDateTime::new(date, noon);
480 for data in [date.to_sql(), dt.to_sql(), dt.assume_utc().to_sql()] {
481 let err = encode(data).unwrap_err();
482 assert!(
483 matches!(err, crate::Error::Conversion(_)),
484 "{year}: {err:?}"
485 );
486 }
487 }
488
489 let edges = [
492 (1, Month::January, 1, 0, 30, 1),
493 (9999, Month::December, 31, 23, 30, -1),
494 ];
495 for (y, m, d, h, min, offset_h) in edges {
496 let local = PrimitiveDateTime::new(
497 Date::from_calendar_date(y, m, d).unwrap(),
498 Time::from_hms(h, min, 0).unwrap(),
499 );
500 let dto = local.assume_offset(UtcOffset::from_hms(offset_h, 0, 0).unwrap());
501 let err = encode(dto.to_sql()).unwrap_err();
502 assert!(matches!(err, crate::Error::Conversion(_)), "{dto}: {err:?}");
503 }
504 }
505
506 #[cfg(feature = "tds73")]
507 #[test]
508 fn offset_date_time_to_sql_stores_the_utc_date_and_time() {
509 use crate::ToSql;
510
511 let cases = [
513 ((2024, Month::March, 4), (5, 6, 7, 123_456_700), (-5, -45)),
514 ((1900, Month::January, 1), (0, 0, 0, 500_000_000), (14, 0)),
515 ];
516 for ((y, mo, d), (h, mi, s, ns), (oh, om)) in cases {
517 let local = PrimitiveDateTime::new(
518 Date::from_calendar_date(y, mo, d).unwrap(),
519 Time::from_hms_nano(h, mi, s, ns).unwrap(),
520 );
521 let offset = UtcOffset::from_hms(oh, om, 0).unwrap();
522 let dto = local.assume_offset(offset);
523 let utc = dto.to_offset(UtcOffset::UTC);
524 let utc_nanos: u64 = (utc.time() - Time::MIDNIGHT)
525 .whole_nanoseconds()
526 .try_into()
527 .unwrap();
528
529 match dto.to_sql() {
530 ColumnData::DateTimeOffset(Some(v)) => {
531 assert_eq!(v.datetime2.date.days() as i64, to_days(utc.date(), 1));
532 assert_eq!(v.datetime2.time.increments, utc_nanos / 100);
533 assert_eq!(v.offset as i32, offset.whole_seconds() / 60);
534 }
535 other => panic!("unexpected: {:?}", other),
536 }
537 }
538 }
539
540 #[cfg(feature = "tds73")]
542 #[test]
543 fn offsets_with_seconds_are_encode_errors() {
544 use crate::ToSql;
545
546 let local = PrimitiveDateTime::new(
547 Date::from_calendar_date(2024, Month::May, 6).unwrap(),
548 Time::from_hms(1, 2, 3).unwrap(),
549 );
550
551 for (h, m, s) in [(0, 0, 30), (0, 0, -30), (1, 0, 30), (-1, -30, -1)] {
552 let dto = local.assume_offset(UtcOffset::from_hms(h, m, s).unwrap());
553 let err = encode(dto.to_sql()).unwrap_err();
554 assert!(matches!(err, crate::Error::Conversion(_)), "{dto}: {err:?}");
555 }
556
557 for (h, m) in [(-14, 0), (-1, -30), (14, 0)] {
558 let dto = local.assume_offset(UtcOffset::from_hms(h, m, 0).unwrap());
559 encode(dto.to_sql()).unwrap();
560 }
561 }
562
563 #[cfg(feature = "tds73")]
566 #[test]
567 fn from_sql_null_variants_return_none_tds73() {
568 use crate::FromSql;
569
570 assert_eq!(Time::from_sql(&ColumnData::Time(None)).unwrap(), None);
571 assert_eq!(Date::from_sql(&ColumnData::Date(None)).unwrap(), None);
572 assert_eq!(
573 OffsetDateTime::from_sql(&ColumnData::DateTimeOffset(None)).unwrap(),
574 None
575 );
576 }
577
578 #[test]
579 fn primitive_datetime_from_sql_null_returns_none() {
580 use crate::FromSql;
581
582 assert_eq!(
583 PrimitiveDateTime::from_sql(&ColumnData::DateTime(None)).unwrap(),
584 None
585 );
586 }
587}