Skip to main content

tiberius/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
7use std::time::Duration;
8pub use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
9
10use crate::tds::codec::ColumnData;
11
12#[inline]
13fn from_days(days: i64, start_year: i32) -> Date {
14    // Use the signed `time::Duration` so that negative day offsets (dates
15    // before `start_year`, e.g. `datetime` values prior to 1900) do not
16    // overflow. Casting a negative day count into an unsigned type and
17    // multiplying it out panics with "multiply with overflow".
18    //
19    // `days` ultimately comes from untrusted server bytes, so a malformed value
20    // can land outside the range `time::Date` can represent. Use `checked_add`
21    // and clamp to the type's bounds instead of panicking with "resulting value
22    // is out of range".
23    let base = Date::from_calendar_date(start_year, Month::January, 1).unwrap();
24    base.checked_add(time::Duration::days(days))
25        .unwrap_or(if days < 0 { Date::MIN } else { Date::MAX })
26}
27
28/// Convert a server-supplied fractional-seconds `increments` at the given
29/// `scale` into nanoseconds without panicking. `scale` and `increments` are
30/// untrusted; a `scale > 9` would otherwise underflow `9 - scale`, and a large
31/// `increments` would overflow the multiply.
32#[inline]
33#[cfg(feature = "tds73")]
34fn nanos_from_increments(increments: u64, scale: u8) -> u64 {
35    let pow = 9u32.saturating_sub(scale as u32);
36    increments.saturating_mul(10u64.saturating_pow(pow))
37}
38
39#[inline]
40#[cfg(feature = "tds73")]
41fn from_secs(secs: u64) -> Time {
42    Time::from_hms(0, 0, 0).unwrap() + Duration::from_secs(secs)
43}
44
45#[inline]
46fn from_sec_fragments(sec_fragments: u64) -> Time {
47    Time::from_hms(0, 0, 0).unwrap() + Duration::from_nanos(sec_fragments * (1e9 as u64) / 300)
48}
49
50#[inline]
51fn to_days(date: Date, start_year: i32) -> i64 {
52    (date - Date::from_calendar_date(start_year, Month::January, 1).unwrap()).whole_days()
53}
54
55#[inline]
56#[cfg(not(feature = "tds73"))]
57fn to_sec_fragments(from: Time) -> i64 {
58    let nanos: i64 = (from - Time::from_hms(0, 0, 0).unwrap())
59        .whole_nanoseconds()
60        .try_into()
61        .unwrap();
62
63    nanos * 300 / (1e9 as i64)
64}
65
66#[cfg(feature = "tds73")]
67from_sql!(
68    PrimitiveDateTime:
69        ColumnData::SmallDateTime(ref dt) => dt.map(|dt| PrimitiveDateTime::new(
70            from_days(dt.days as i64, 1900),
71            from_secs(dt.seconds_fragments as u64 * 60),
72        )),
73        ColumnData::DateTime2(ref dt) => dt.map(|dt| PrimitiveDateTime::new(
74            from_days(dt.date.days() as i64, 1),
75            Time::from_hms(0,0,0).unwrap() + Duration::from_nanos(nanos_from_increments(dt.time.increments, dt.time.scale))
76        )),
77        ColumnData::DateTime(ref dt) => dt.map(|dt| PrimitiveDateTime::new(
78            from_days(dt.days as i64, 1900),
79            from_sec_fragments(dt.seconds_fragments as u64)
80        ));
81    Time:
82        ColumnData::Time(ref time) => time.map(|time| {
83            let ns = nanos_from_increments(time.increments, time.scale);
84            Time::from_hms(0,0,0).unwrap() + Duration::from_nanos(ns)
85        });
86    Date:
87        ColumnData::Date(ref date) => date.map(|date| from_days(date.days() as i64, 1));
88    OffsetDateTime:
89        ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
90            let date = from_days(dto.datetime2.date.days() as i64, 1);
91            let dt = dto.datetime2;
92
93            let time = Time::from_hms(0,0,0).unwrap()
94                + Duration::from_nanos(nanos_from_increments(dt.time.increments, dt.time.scale));
95
96            // A malformed server offset outside ±14h is not representable by
97            // `UtcOffset`; fall back to UTC rather than panicking.
98            let offset = UtcOffset::from_whole_seconds(dto.offset as i32 * 60)
99                .unwrap_or(UtcOffset::UTC);
100
101            date.with_time(time).assume_utc().to_offset(offset)
102        })
103);
104
105#[cfg(feature = "tds73")]
106to_sql!(self_,
107        Date: (ColumnData::Date, super::Date::new(to_days(*self_, 1) as u32));
108        Time: (ColumnData::Time, {
109            let nanos: u64 = (*self_ - Time::from_hms(0, 0, 0).unwrap()).whole_nanoseconds().try_into().unwrap();
110            let increments = nanos / 100;
111
112            super::Time {increments, scale: 7}
113        });
114        PrimitiveDateTime: (ColumnData::DateTime2, {
115            let time = self_.time();
116            let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap()).whole_nanoseconds().try_into().unwrap();
117            let increments = nanos / 100;
118
119            let date = super::Date::new(to_days(self_.date(), 1) as u32);
120            let time = super::Time {increments, scale: 7};
121
122            super::DateTime2::new(date, time)
123        });
124        OffsetDateTime: (ColumnData::DateTimeOffset, {
125            let tz = self_.offset();
126            let offset = (tz.whole_seconds() / 60) as i16;
127
128            let utc_date = self_.to_offset(UtcOffset::UTC);
129
130            let nanos: u64 = (utc_date.time() - Time::from_hms(0, 0, 0).unwrap()).whole_nanoseconds().try_into().unwrap();
131
132            let date = super::Date::new(to_days(utc_date.date(), 1) as u32);
133            let time = super::Time { increments: nanos / 100, scale: 7 };
134
135            super::DateTimeOffset::new(super::DateTime2::new(date, time), offset)
136        });
137);
138
139#[cfg(not(feature = "tds73"))]
140to_sql!(self_,
141        PrimitiveDateTime: (ColumnData::DateTime, {
142            let date = self_.date();
143            let time = self_.time();
144
145            let days = to_days(date, 1900) as i32;
146            let seconds_fragments = to_sec_fragments(time);
147
148            super::DateTime::new(days, seconds_fragments as u32)
149        });
150);
151
152#[cfg(not(feature = "tds73"))]
153from_sql!(
154    PrimitiveDateTime:
155    ColumnData::DateTime(ref dt) => dt.map(|dt| {
156        from_days(dt.days as i64, 1900).with_time(from_sec_fragments(dt.seconds_fragments as u64))
157    })
158);
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    // Regression test for #316: a `datetime` value with a date before 1900 has
165    // a negative day offset from the 1900 base date. This must round-trip
166    // without a "multiply with overflow" panic.
167    #[test]
168    fn from_days_handles_negative_offsets() {
169        // 1899-12-31 is one day before the 1900 base date.
170        assert_eq!(
171            from_days(-1, 1900),
172            Date::from_calendar_date(1899, Month::December, 31).unwrap()
173        );
174
175        // A date well before 1900, at the lower edge of the `datetime` range.
176        let expected = Date::from_calendar_date(1850, Month::January, 1).unwrap();
177        let days = to_days(expected, 1900);
178        assert!(
179            days < 0,
180            "expected a negative day offset for pre-1900 dates"
181        );
182
183        // Rebuilding from the (negative) day offset must not overflow.
184        assert_eq!(from_days(days, 1900), expected);
185    }
186
187    // Exercise the full decode path (`DateTime` -> `PrimitiveDateTime`) for a
188    // pre-1900 value, matching what happens when reading a `datetime` column.
189    #[test]
190    fn datetime_before_1900_decodes() {
191        let expected_date = Date::from_calendar_date(1850, Month::January, 1).unwrap();
192        let days = to_days(expected_date, 1900) as i32;
193
194        // Reconstruct the way the `from_sql!` mapping does for `ColumnData::DateTime`.
195        let dt = crate::tds::time::DateTime::new(days, 0);
196        let decoded = from_days(dt.days() as i64, 1900)
197            .with_time(from_sec_fragments(dt.seconds_fragments() as u64));
198
199        assert_eq!(decoded.date(), expected_date);
200        assert_eq!(decoded.time(), Time::from_hms(0, 0, 0).unwrap());
201    }
202
203    #[test]
204    fn from_days_clamps_on_overflow() {
205        // A day offset far outside the representable `time::Date` range forces
206        // the `checked_add` fallback. Positive overflow clamps to MAX, negative
207        // to MIN. Pins the `days < 0` sign test (vs `==` / `>`).
208        assert_eq!(from_days(10_000_000, 1), Date::MAX);
209        assert_eq!(from_days(-10_000_000, 1), Date::MIN);
210    }
211
212    #[cfg(feature = "tds73")]
213    #[test]
214    fn from_secs_converts() {
215        // 3600 s past midnight == 01:00:00 (pins the `+` in `from_secs`).
216        assert_eq!(from_secs(3600), Time::from_hms(1, 0, 0).unwrap());
217    }
218
219    #[test]
220    fn from_sec_fragments_converts() {
221        // 300 sec-fragments (1/300 s units) == exactly one second.
222        assert_eq!(from_sec_fragments(300), Time::from_hms(0, 0, 1).unwrap());
223    }
224
225    #[cfg(not(feature = "tds73"))]
226    #[test]
227    fn to_sec_fragments_converts() {
228        // One second == 300 sec-fragments (1/300 s units).
229        assert_eq!(to_sec_fragments(Time::from_hms(0, 0, 1).unwrap()), 300);
230    }
231
232    #[cfg(feature = "tds73")]
233    #[test]
234    fn time_from_sql_and_back() {
235        use crate::FromSql;
236
237        // 12:34:56 as 100ns increments since midnight, scale 7.
238        let expected = Time::from_hms(12, 34, 56).unwrap();
239        let nanos: u64 = (expected - Time::from_hms(0, 0, 0).unwrap())
240            .whole_nanoseconds()
241            .try_into()
242            .unwrap();
243        let increments: u64 = nanos / 100;
244
245        let tds_time = super::super::Time::new(increments, 7);
246        let data = ColumnData::Time(Some(tds_time));
247
248        let decoded = Time::from_sql(&data).unwrap().unwrap();
249        assert_eq!(decoded, expected);
250
251        // Round trip back through ToSql.
252        use crate::ToSql;
253        let round_tripped = decoded.to_sql();
254        match round_tripped {
255            ColumnData::Time(Some(t)) => assert_eq!(t.increments, increments),
256            other => panic!("unexpected: {:?}", other),
257        }
258    }
259
260    #[cfg(feature = "tds73")]
261    #[test]
262    fn date_from_sql_and_back() {
263        use crate::{FromSql, ToSql};
264
265        let expected = Date::from_calendar_date(2020, Month::June, 15).unwrap();
266        let days = to_days(expected, 1) as u32;
267
268        let data = ColumnData::Date(Some(super::super::Date::new(days)));
269        let decoded = Date::from_sql(&data).unwrap().unwrap();
270        assert_eq!(decoded, expected);
271
272        match decoded.to_sql() {
273            ColumnData::Date(Some(d)) => assert_eq!(d.days(), days),
274            other => panic!("unexpected: {:?}", other),
275        }
276    }
277
278    #[cfg(feature = "tds73")]
279    #[test]
280    fn primitive_datetime_from_datetime2_and_back() {
281        use crate::{FromSql, ToSql};
282
283        let date = Date::from_calendar_date(2020, Month::June, 15).unwrap();
284        let time = Time::from_hms(1, 2, 3).unwrap();
285        let expected = PrimitiveDateTime::new(date, time);
286
287        let days = to_days(date, 1) as u32;
288        let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap())
289            .whole_nanoseconds()
290            .try_into()
291            .unwrap();
292        let increments = nanos / 100;
293
294        let dt2 = super::super::DateTime2::new(
295            super::super::Date::new(days),
296            super::super::Time::new(increments, 7),
297        );
298        let data = ColumnData::DateTime2(Some(dt2));
299
300        let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
301        assert_eq!(decoded, expected);
302
303        match decoded.to_sql() {
304            ColumnData::DateTime2(Some(dt)) => {
305                assert_eq!(dt.date.days(), days);
306            }
307            other => panic!("unexpected: {:?}", other),
308        }
309    }
310
311    #[cfg(feature = "tds73")]
312    #[test]
313    fn primitive_datetime_from_smalldatetime_and_datetime() {
314        use crate::FromSql;
315
316        // SmallDateTime path.
317        let sdt = crate::tds::time::SmallDateTime::new(1, 30); // 30 minutes past midnight on day 1 (1900-01-02)
318        let data = ColumnData::SmallDateTime(Some(sdt));
319        let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
320        assert_eq!(decoded.date(), from_days(1, 1900));
321
322        // DateTime path.
323        let dt = crate::tds::time::DateTime::new(1, 0);
324        let data = ColumnData::DateTime(Some(dt));
325        let decoded = PrimitiveDateTime::from_sql(&data).unwrap().unwrap();
326        assert_eq!(decoded.date(), from_days(1, 1900));
327    }
328
329    #[cfg(feature = "tds73")]
330    #[test]
331    fn offset_date_time_from_sql_and_back() {
332        use crate::{FromSql, ToSql};
333
334        let date = Date::from_calendar_date(2020, Month::June, 15).unwrap();
335        let time = Time::from_hms(1, 2, 3).unwrap();
336        let days = to_days(date, 1) as u32;
337        let nanos: u64 = (time - Time::from_hms(0, 0, 0).unwrap())
338            .whole_nanoseconds()
339            .try_into()
340            .unwrap();
341        let increments = nanos / 100;
342
343        let dt2 = super::super::DateTime2::new(
344            super::super::Date::new(days),
345            super::super::Time::new(increments, 7),
346        );
347        let dto = super::super::DateTimeOffset::new(dt2, 60); // +1h offset
348
349        let data = ColumnData::DateTimeOffset(Some(dto));
350        let decoded = OffsetDateTime::from_sql(&data).unwrap().unwrap();
351
352        assert_eq!(
353            decoded.offset(),
354            UtcOffset::from_whole_seconds(3600).unwrap()
355        );
356
357        match decoded.to_sql() {
358            ColumnData::DateTimeOffset(Some(round_tripped)) => {
359                assert_eq!(round_tripped.offset, 60);
360            }
361            other => panic!("unexpected: {:?}", other),
362        }
363    }
364
365    // `ColumnData::Time`/`Date`/`DateTimeOffset` and their `time`-crate
366    // `FromSql` impls only exist with the `tds73` feature.
367    #[cfg(feature = "tds73")]
368    #[test]
369    fn from_sql_null_variants_return_none_tds73() {
370        use crate::FromSql;
371
372        assert_eq!(Time::from_sql(&ColumnData::Time(None)).unwrap(), None);
373        assert_eq!(Date::from_sql(&ColumnData::Date(None)).unwrap(), None);
374        assert_eq!(
375            OffsetDateTime::from_sql(&ColumnData::DateTimeOffset(None)).unwrap(),
376            None
377        );
378    }
379
380    #[test]
381    fn primitive_datetime_from_sql_null_returns_none() {
382        use crate::FromSql;
383
384        assert_eq!(
385            PrimitiveDateTime::from_sql(&ColumnData::DateTime(None)).unwrap(),
386            None
387        );
388    }
389}