Skip to main content

tds_driver/tds/time/
time.rs

1//! Mappings between TDS and and time crate types (with `time` feature flag
2//! enabled).
3//!
4//! The time library offers better ergonomy and are highly recommended if
5//! needing to modify and deal with date and time in SQL Server.
6
7#[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    // Use the signed `time::Duration` so that negative day offsets (dates
16    // before `start_year`, e.g. `datetime` values prior to 1900) do not
17    // overflow. Casting a negative day count into an unsigned type and
18    // multiplying it out panics with "multiply with overflow".
19    //
20    // `days` ultimately comes from untrusted server bytes, so a malformed value
21    // can land outside the range `time::Date` can represent. Use `checked_add`
22    // and clamp to the type's bounds instead of panicking with "resulting value
23    // is out of range".
24    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/// Convert a server-supplied fractional-seconds `increments` at the given
30/// `scale` into nanoseconds without panicking. `scale` and `increments` are
31/// untrusted; a `scale > 9` would otherwise underflow `9 - scale`, and a large
32/// `increments` would overflow the multiply.
33#[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/// 1/300 s since midnight, rounded to the nearest tick as the server rounds
90/// a `datetime` literal (00:00:00.999 is 00:00:01.000). The last tick of a
91/// day rounds up to 300 * 86,400, which `to_datetime` carries into the next
92/// day.
93#[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    // 3 * ns / 10^7, half up; a day's nanoseconds fit u64.
102    ((nanos * 3 + 5_000_000) / 10_000_000) as u32
103}
104
105/// `datetime`: days since 1900-01-01 and ticks since midnight.
106#[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            // A malformed server offset outside ±14h is not representable by
137            // `UtcOffset`; fall back to UTC rather than panicking.
138            let offset = UtcOffset::from_whole_seconds(dto.offset as i32 * 60)
139                .unwrap_or(UtcOffset::UTC);
140
141            // Nor past `Date::MAX` locally, where a malformed date is clamped
142            // (`to_offset` panics there).
143            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            // datetimeoffset stores whole minutes: one with seconds is
172            // rejected instead of cut. Within ±25:59:59, so i16 holds it.
173            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            // From the timestamp: `to_offset(UTC)` panics when the UTC date
180            // passes year 9999.
181            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    // Regression test for #316: a `datetime` value with a date before 1900 has
202    // a negative day offset from the 1900 base date. This must round-trip
203    // without a "multiply with overflow" panic.
204    #[test]
205    fn from_days_handles_negative_offsets() {
206        // 1899-12-31 is one day before the 1900 base date.
207        assert_eq!(
208            from_days(-1, 1900),
209            Date::from_calendar_date(1899, Month::December, 31).unwrap()
210        );
211
212        // A date well before 1900, at the lower edge of the `datetime` range.
213        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        // Rebuilding from the (negative) day offset must not overflow.
221        assert_eq!(from_days(days, 1900), expected);
222    }
223
224    // Exercise the full decode path (`DateTime` -> `PrimitiveDateTime`) for a
225    // pre-1900 value, matching what happens when reading a `datetime` column.
226    #[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        // A day offset far outside the representable `time::Date` range forces
240        // the `checked_add` fallback. Positive overflow clamps to MAX, negative
241        // to MIN. Pins the `days < 0` sign test (vs `==` / `>`).
242        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        // Used to wrap silently to 00:00.
277        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        // One second == 300 sec-fragments (1/300 s units).
285        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        // 299.7 ticks rounds up to the next second; 299.49 stays.
303        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        // 12:34:56 as 100ns increments since midnight, scale 7.
316        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        // Round trip back through ToSql.
330        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        // SmallDateTime path.
395        let sdt = crate::tds::time::SmallDateTime::new(1, 30); // 30 minutes past midnight on day 1 (1900-01-02)
396        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        // DateTime path.
402        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    /// A malformed date is clamped to `Date::MAX`, past which a positive
409    /// offset made `to_offset` panic.
410    #[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); // +1h offset
446
447        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        // In range locally, out of range in UTC, which is what datetimeoffset
490        // stores. The upper one used to panic in `to_offset(UTC)`.
491        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        // Either side of the Unix epoch, with a negative and a positive offset.
512        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    /// Cut to whole minutes, +01:00:30 was stored as +01:00, 30 s off.
541    #[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    // `ColumnData::Time`/`Date`/`DateTimeOffset` and their `time`-crate
564    // `FromSql` impls only exist with the `tds73` feature.
565    #[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}