Skip to main content

tds_driver/tds/time/
chrono.rs

1//! Mappings between TDS and and Chrono types (with `chrono` feature flag
2//! enabled).
3//!
4//! The chrono library offers better ergonomy, but is known to hold certain
5//! security vulnerabilities. The code here is for legacy purposes, please use
6//! `time` crate for greenfield projects.
7
8#[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    // `days` derives from untrusted server bytes; clamp to the representable
21    // range instead of panicking on overflow.
22    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/// Convert a server-supplied fractional-seconds `increments` at the given
32/// `scale` into nanoseconds without panicking (`scale > 9` would underflow
33/// `9 - scale`; a large `increments` would overflow the multiply).
34#[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/// 1/300 s since midnight, rounded to the nearest tick as the server rounds
93/// a `datetime` literal (00:00:00.999 is 00:00:01.000). The last tick of a
94/// day rounds up to 300 * 86,400, which `to_datetime` carries into the next
95/// day. A leap second (nanosecond >= 1e9) becomes `DateTime::LEAP_SECOND`:
96/// counted on, it would read as the next second, or past 24:00.
97#[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            // 3 * ns / 10^7, half up; a day's nanoseconds fit u64.
106            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/// `datetime`: days since 1900-01-01 and ticks since midnight.
113#[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        // The datetime2 part of a datetimeoffset is already UTC.
137        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/// 10^-7 s since midnight. A leap second (nanosecond >= 1e9) becomes
165/// `Time::LEAP_SECOND`: counted on, it would read as the next second, or
166/// past 24:00.
167#[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/// datetimeoffset stores whole minutes: an offset with seconds becomes
191/// `DateTimeOffset::OFFSET_WITH_SECONDS` instead of being truncated.
192#[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        // A FixedOffset is under 24 hours: at most 1439 minutes.
197        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        // A day offset far outside the representable `NaiveDate` range forces
240        // the `checked_add_signed` fallback. Positive overflow must clamp to
241        // MAX, negative overflow to MIN. This pins the `days < 0` sign test
242        // (distinguishing it from `==` and `>`).
243        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        // One second == 300 sec-fragments (1/300 s units).
304        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        // 299.7 ticks rounds up to the next second; 299.49 stays.
325        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    // The tds73 `from_sql` NaiveDateTime path has a dedicated arm for the legacy
393    // `ColumnData::DateTime` wire type; exercise it directly (round-trips produce
394    // `DateTime2`, never `DateTime`, so this arm is otherwise unreachable).
395    #[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    // The `DateTimeOffset -> DateTime<Utc>` conversion arm (distinct from the
410    // `DateTime<FixedOffset>` arm exercised by the round-trip test).
411    #[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        // Same instant: the wire time is UTC, the offset only labels it.
425        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        // Year 1 locally, year 0 in UTC, which is what datetimeoffset stores.
470        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        // At 23:59 it went past 24:00; at 12:34 the server read 12:35:00.5.
504        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        // At 23:59 it went past midnight (8023); at 12:34 it was the next
590        // second, 12:35:00.5.
591        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}