#[cfg(not(feature = "tds73"))]
use super::DateTime as DateTime1;
#[cfg(feature = "tds73")]
use super::{Date, DateTime2, DateTimeOffset, Time};
use crate::tds::codec::ColumnData;
#[cfg(feature = "tds73")]
#[cfg_attr(docsrs, doc(cfg(feature = "tds73")))]
pub use chrono::offset::{FixedOffset, Utc};
pub use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
#[inline]
fn from_days(days: i64, start_year: i32) -> NaiveDate {
let base = NaiveDate::from_ymd_opt(start_year, 1, 1).unwrap();
base.checked_add_signed(chrono::Duration::days(days))
.unwrap_or(if days < 0 {
NaiveDate::MIN
} else {
NaiveDate::MAX
})
}
#[inline]
#[cfg(feature = "tds73")]
fn nanos_from_increments(increments: u64, scale: u8) -> i64 {
let pow = 9u32.saturating_sub(scale as u32);
increments
.saturating_mul(10u64.saturating_pow(pow))
.min(i64::MAX as u64) as i64
}
fn from_millis(ms: u32) -> crate::Result<NaiveTime> {
NaiveTime::from_num_seconds_from_midnight_opt(ms / 1000, ms % 1000 * 1_000_000)
.ok_or_else(|| crate::Error::Conversion(format!("{ms} ms is past midnight").into()))
}
fn from_datetime(dt: super::DateTime) -> crate::Result<NaiveDateTime> {
let time = from_millis(dt.millis_of_day()?)?;
Ok(NaiveDateTime::new(from_days(dt.days() as i64, 1900), time))
}
#[cfg(feature = "tds73")]
fn from_smalldatetime(dt: super::SmallDateTime) -> crate::Result<NaiveDateTime> {
let time = from_millis(dt.minutes_of_day()? * 60_000)?;
Ok(NaiveDateTime::new(from_days(dt.days() as i64, 1900), time))
}
impl<'a> crate::FromSql<'a> for NaiveDateTime {
fn from_sql(data: &'a ColumnData<'static>) -> crate::Result<Option<Self>> {
match data {
ColumnData::DateTime(dt) => dt.map(from_datetime).transpose(),
#[cfg(feature = "tds73")]
ColumnData::SmallDateTime(dt) => dt.map(from_smalldatetime).transpose(),
#[cfg(feature = "tds73")]
ColumnData::DateTime2(dt) => Ok(dt.map(|dt| {
let ns = nanos_from_increments(dt.time.increments, dt.time.scale);
NaiveDateTime::new(
from_days(dt.date.days() as i64, 1),
NaiveTime::MIN + chrono::Duration::nanoseconds(ns),
)
})),
_ => Err(crate::Error::Conversion(
format!("cannot interpret {:?} as an NaiveDateTime value", data).into(),
)),
}
}
}
impl crate::FromSqlOwned for NaiveDateTime {
fn from_sql_owned(data: ColumnData<'static>) -> crate::Result<Option<Self>> {
<Self as crate::FromSql>::from_sql(&data)
}
}
#[inline]
fn to_days(date: NaiveDate, start_year: i32) -> i64 {
date.signed_duration_since(NaiveDate::from_ymd_opt(start_year, 1, 1).unwrap())
.num_days()
}
#[inline]
#[cfg(not(feature = "tds73"))]
fn to_sec_fragments(time: NaiveTime) -> u32 {
use chrono::Timelike;
match time.nanosecond() {
n if n >= 1_000_000_000 => DateTime1::LEAP_SECOND,
n => {
let ns = u64::from(time.num_seconds_from_midnight()) * 1_000_000_000 + u64::from(n);
((ns * 3 + 5_000_000) / 10_000_000) as u32
}
}
}
#[cfg(not(feature = "tds73"))]
fn to_datetime(dt: NaiveDateTime) -> DateTime1 {
let mut days = to_days(dt.date(), 1900) as i32;
let mut ticks = to_sec_fragments(dt.time());
if ticks == 300 * 86_400 {
days += 1;
ticks = 0;
}
DateTime1::new(days, ticks)
}
#[cfg(feature = "tds73")]
from_sql!(
NaiveTime:
ColumnData::Time(ref time) => time.map(|time| {
let ns = nanos_from_increments(time.increments, time.scale);
NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns)
});
NaiveDate:
ColumnData::Date(ref date) => date.map(|date| from_days(date.days() as i64, 1));
chrono::DateTime<Utc>:
ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
let date = from_days(dto.datetime2.date.days() as i64, 1);
let ns = nanos_from_increments(dto.datetime2.time.increments, dto.datetime2.time.scale);
let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
chrono::DateTime::from_naive_utc_and_offset(NaiveDateTime::new(date, time), Utc)
}),
ColumnData::DateTime2(ref dt2) => dt2.map(|dt2| {
let date = from_days(dt2.date.days() as i64, 1);
let ns = nanos_from_increments(dt2.time.increments, dt2.time.scale);
let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
let naive = NaiveDateTime::new(date, time);
chrono::DateTime::from_naive_utc_and_offset(naive, Utc)
});
chrono::DateTime<FixedOffset>: ColumnData::DateTimeOffset(ref dto) => dto.map(|dto| {
let date = from_days(dto.datetime2.date.days() as i64, 1);
let ns = nanos_from_increments(dto.datetime2.time.increments, dto.datetime2.time.scale);
let time = NaiveTime::from_hms_opt(0,0,0).unwrap() + chrono::Duration::nanoseconds(ns);
let offset = FixedOffset::east_opt((dto.offset as i32) * 60).unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
let naive = NaiveDateTime::new(date, time);
chrono::DateTime::from_naive_utc_and_offset(naive, offset)
})
);
#[cfg(feature = "tds73")]
fn to_time(time: NaiveTime) -> Time {
use chrono::Timelike;
let increments = match time.nanosecond() {
n if n >= 1_000_000_000 => Time::LEAP_SECOND,
n => (u64::from(time.num_seconds_from_midnight()) * 1_000_000_000 + u64::from(n)) / 100,
};
Time {
increments,
scale: 7,
}
}
#[cfg(feature = "tds73")]
fn to_datetime2(naive: NaiveDateTime) -> DateTime2 {
DateTime2::new(
Date::from_signed_days(to_days(naive.date(), 1)),
to_time(naive.time()),
)
}
#[cfg(feature = "tds73")]
fn to_datetimeoffset(dt: &chrono::DateTime<FixedOffset>) -> DateTimeOffset {
let secs = dt.offset().local_minus_utc();
let offset = match secs % 60 {
0 => (secs / 60) as i16,
_ => DateTimeOffset::OFFSET_WITH_SECONDS,
};
DateTimeOffset::new(to_datetime2(dt.naive_utc()), offset)
}
#[cfg(feature = "tds73")]
to_sql!(self_,
NaiveDate: (ColumnData::Date, Date::from_signed_days(to_days(*self_, 1)));
NaiveTime: (ColumnData::Time, to_time(*self_));
NaiveDateTime: (ColumnData::DateTime2, to_datetime2(*self_));
chrono::DateTime<Utc>: (ColumnData::DateTime2, to_datetime2(self_.naive_utc()));
chrono::DateTime<FixedOffset>: (ColumnData::DateTimeOffset, to_datetimeoffset(self_));
);
#[cfg(feature = "tds73")]
into_sql!(self_,
NaiveDate: (ColumnData::Date, Date::from_signed_days(to_days(self_, 1)));
NaiveTime: (ColumnData::Time, to_time(self_));
NaiveDateTime: (ColumnData::DateTime2, to_datetime2(self_));
chrono::DateTime<Utc>: (ColumnData::DateTime2, to_datetime2(self_.naive_utc()));
chrono::DateTime<FixedOffset>: (ColumnData::DateTimeOffset, to_datetimeoffset(&self_));
);
#[cfg(not(feature = "tds73"))]
to_sql!(self_,
NaiveDateTime: (ColumnData::DateTime, to_datetime(*self_));
);
#[cfg(not(feature = "tds73"))]
into_sql!(self_,
NaiveDateTime: (ColumnData::DateTime, to_datetime(self_));
);
#[cfg(test)]
mod tests {
use super::*;
use crate::{FromSql, IntoSql};
#[test]
fn from_days_clamps_on_overflow() {
assert_eq!(from_days(200_000_000, 1), NaiveDate::MAX);
assert_eq!(from_days(-200_000_000, 1), NaiveDate::MIN);
}
#[test]
fn datetime_rounds_to_the_millisecond() {
let get = |ticks| {
let cd = ColumnData::DateTime(Some(crate::tds::time::DateTime::new(0, ticks)));
NaiveDateTime::from_sql(&cd).unwrap().unwrap().time()
};
assert_eq!(get(300), NaiveTime::from_hms_opt(0, 0, 1).unwrap());
assert_eq!(
get(299),
NaiveTime::from_hms_milli_opt(0, 0, 0, 997).unwrap()
);
assert_eq!(get(2), NaiveTime::from_hms_milli_opt(0, 0, 0, 7).unwrap());
assert_eq!(
get(25919999),
NaiveTime::from_hms_milli_opt(23, 59, 59, 997).unwrap()
);
}
#[test]
fn out_of_range_times_are_conversion_errors() {
use crate::tds::time::{DateTime, SmallDateTime};
use crate::FromSqlOwned;
let dt = ColumnData::DateTime(Some(DateTime::new(0, 300 * 86_400)));
assert!(matches!(
NaiveDateTime::from_sql(&dt),
Err(crate::Error::Conversion(_))
));
assert!(NaiveDateTime::from_sql_owned(dt).is_err());
let sdt = ColumnData::SmallDateTime(Some(SmallDateTime::new(0, 1440)));
#[cfg(feature = "tds73")]
assert!(matches!(
NaiveDateTime::from_sql(&sdt),
Err(crate::Error::Conversion(_))
));
#[cfg(not(feature = "tds73"))]
assert!(NaiveDateTime::from_sql(&sdt).is_err());
}
#[cfg(feature = "tds73")]
#[test]
fn smalldatetime_converts() {
let cd = ColumnData::SmallDateTime(Some(crate::tds::time::SmallDateTime::new(1, 1439)));
assert_eq!(
NaiveDateTime::from_sql(&cd).unwrap(),
Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1900, 1, 2).unwrap(),
NaiveTime::from_hms_opt(23, 59, 0).unwrap()
))
);
}
#[cfg(not(feature = "tds73"))]
#[test]
fn to_sec_fragments_converts() {
assert_eq!(
to_sec_fragments(NaiveTime::from_hms_opt(0, 0, 1).unwrap()),
300
);
}
#[cfg(not(feature = "tds73"))]
#[test]
fn to_datetime_rounds_to_the_nearest_tick_and_carries_the_day() {
let at = |h, m, s, nano| {
NaiveDate::from_ymd_opt(1900, 1, 1)
.unwrap()
.and_hms_nano_opt(h, m, s, nano)
.unwrap()
};
let ticks = |dt: NaiveDateTime| {
let dt = to_datetime(dt);
(dt.days(), dt.seconds_fragments())
};
assert_eq!(ticks(at(0, 0, 0, 999_000_000)), (0, 300));
assert_eq!(ticks(at(0, 0, 0, 998_300_000)), (0, 299));
assert_eq!(ticks(at(0, 0, 0, 998_500_000)), (0, 300));
assert_eq!(ticks(at(23, 59, 59, 999_000_000)), (1, 0));
assert_eq!(ticks(at(23, 59, 59, 998_000_000)), (0, 25_919_999));
}
#[cfg(feature = "tds73")]
#[test]
fn naive_date_round_trip() {
let date = NaiveDate::from_ymd_opt(2021, 6, 15).unwrap();
let cd: ColumnData<'static> = date.into_sql();
assert!(matches!(cd, ColumnData::Date(Some(_))));
assert_eq!(NaiveDate::from_sql(&cd).unwrap(), Some(date));
}
#[cfg(feature = "tds73")]
#[test]
fn naive_time_round_trip() {
let time = NaiveTime::from_hms_opt(13, 37, 42).unwrap();
let cd: ColumnData<'static> = time.into_sql();
assert!(matches!(cd, ColumnData::Time(Some(_))));
assert_eq!(NaiveTime::from_sql(&cd).unwrap(), Some(time));
}
#[cfg(feature = "tds73")]
#[test]
fn naive_datetime_round_trip() {
let dt = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2000, 12, 31).unwrap(),
NaiveTime::from_hms_opt(23, 59, 58).unwrap(),
);
let cd: ColumnData<'static> = dt.into_sql();
assert!(matches!(cd, ColumnData::DateTime2(Some(_))));
assert_eq!(NaiveDateTime::from_sql(&cd).unwrap(), Some(dt));
}
#[cfg(feature = "tds73")]
#[test]
fn datetime_utc_round_trip() {
let naive = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
);
let dt = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
let cd: ColumnData<'static> = dt.into_sql();
assert!(matches!(cd, ColumnData::DateTime2(Some(_))));
assert_eq!(chrono::DateTime::<Utc>::from_sql(&cd).unwrap(), Some(dt));
}
#[cfg(feature = "tds73")]
#[test]
fn datetime_fixed_offset_round_trip() {
let offset = FixedOffset::east_opt(2 * 3600).unwrap();
let naive = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
);
let dt = chrono::DateTime::from_naive_utc_and_offset(naive, offset);
let cd: ColumnData<'static> = dt.into_sql();
assert!(matches!(cd, ColumnData::DateTimeOffset(Some(_))));
assert_eq!(
chrono::DateTime::<FixedOffset>::from_sql(&cd).unwrap(),
Some(dt)
);
}
#[cfg(feature = "tds73")]
#[test]
fn naive_datetime_from_legacy_datetime_column() {
let cd = ColumnData::DateTime(Some(crate::tds::time::DateTime::new(0, 0)));
let dt = NaiveDateTime::from_sql(&cd).unwrap().unwrap();
assert_eq!(
dt,
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1900, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
)
);
}
#[cfg(feature = "tds73")]
#[test]
fn datetime_offset_reads_as_utc() {
let offset = FixedOffset::east_opt(2 * 3600).unwrap();
let naive = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2015, 3, 4).unwrap(),
NaiveTime::from_hms_opt(1, 2, 3).unwrap(),
);
let dt: chrono::DateTime<FixedOffset> =
chrono::DateTime::from_naive_utc_and_offset(naive, offset);
let cd: ColumnData<'static> = dt.into_sql();
assert!(matches!(cd, ColumnData::DateTimeOffset(Some(_))));
let utc = chrono::DateTime::<Utc>::from_sql(&cd).unwrap().unwrap();
assert_eq!(utc.naive_utc(), naive);
assert_eq!(utc, dt);
}
fn encode(data: ColumnData<'_>) -> crate::Result<()> {
use crate::tds::codec::Encode;
let mut buf = bytes::BytesMut::new();
data.encode(&mut crate::BytesMutWithTypeInfo::new(&mut buf))
}
#[cfg(feature = "tds73")]
#[test]
fn dates_outside_year_1_to_9999_are_encode_errors() {
use crate::ToSql;
use chrono::TimeZone;
let plus_one = FixedOffset::east_opt(3600).unwrap();
for (y, m, d) in [(0, 12, 31), (-5, 1, 1), (10000, 1, 1), (50000, 1, 1)] {
let date = NaiveDate::from_ymd_opt(y, m, d).unwrap();
let naive = date.and_hms_opt(12, 0, 0).unwrap();
let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
let fixed = chrono::DateTime::<FixedOffset>::from_naive_utc_and_offset(naive, plus_one);
for data in [
date.to_sql(),
naive.to_sql(),
utc.to_sql(),
fixed.to_sql(),
date.into_sql(),
naive.into_sql(),
utc.into_sql(),
fixed.into_sql(),
] {
let err = encode(data).unwrap_err();
assert!(
matches!(err, crate::Error::Conversion(_)),
"{date}: {err:?}"
);
}
}
let local = NaiveDate::from_ymd_opt(1, 1, 1)
.unwrap()
.and_hms_opt(0, 30, 0)
.unwrap();
let dto = plus_one.from_local_datetime(&local).unwrap();
assert!(matches!(
encode(dto.to_sql()),
Err(crate::Error::Conversion(_))
));
}
#[cfg(feature = "tds73")]
#[test]
fn year_1_and_9999_encode() {
use crate::ToSql;
let first = NaiveDate::from_ymd_opt(1, 1, 1).unwrap();
let last = NaiveDate::from_ymd_opt(9999, 12, 31).unwrap();
for date in [first, last] {
let naive = date.and_hms_nano_opt(23, 59, 59, 999_999_900).unwrap();
let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
for data in [date.to_sql(), naive.to_sql(), utc.to_sql()] {
encode(data).unwrap();
}
}
}
#[cfg(feature = "tds73")]
#[test]
fn leap_seconds_are_encode_errors() {
use crate::ToSql;
for (h, m) in [(23, 59), (12, 34)] {
let time = NaiveTime::from_hms_nano_opt(h, m, 59, 1_500_000_000).unwrap();
let naive = NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_time(time);
let utc = chrono::DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
let fixed = utc.fixed_offset();
for data in [
time.to_sql(),
naive.to_sql(),
utc.to_sql(),
fixed.to_sql(),
time.into_sql(),
naive.into_sql(),
utc.into_sql(),
fixed.into_sql(),
] {
let err = encode(data).unwrap_err();
assert!(
matches!(&err, crate::Error::Conversion(m) if m.contains("leap")),
"{time}: {err:?}"
);
}
}
let last = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap();
encode(last.to_sql()).unwrap();
}
#[cfg(feature = "tds73")]
#[test]
fn offsets_with_seconds_are_encode_errors() {
use crate::ToSql;
use chrono::TimeZone;
let naive = NaiveDate::from_ymd_opt(2024, 5, 6)
.unwrap()
.and_hms_opt(1, 2, 3)
.unwrap();
for secs in [30, -30, 3600 + 1] {
let dto = FixedOffset::east_opt(secs)
.unwrap()
.from_local_datetime(&naive)
.unwrap();
for data in [dto.to_sql(), dto.into_sql()] {
let err = encode(data).unwrap_err();
assert!(matches!(err, crate::Error::Conversion(_)), "{dto}");
}
}
for secs in [-14 * 3600, -90 * 60, 14 * 3600] {
let dto = FixedOffset::east_opt(secs)
.unwrap()
.from_local_datetime(&naive)
.unwrap();
encode(dto.to_sql()).unwrap();
}
}
#[cfg(feature = "tds73")]
#[test]
fn null_maps_to_none() {
assert_eq!(NaiveDate::from_sql(&ColumnData::Date(None)).unwrap(), None);
assert_eq!(NaiveTime::from_sql(&ColumnData::Time(None)).unwrap(), None);
}
#[cfg(not(feature = "tds73"))]
#[test]
fn naive_datetime_round_trip_legacy() {
let dt = NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
);
let cd: ColumnData<'static> = dt.into_sql();
assert!(matches!(cd, ColumnData::DateTime(Some(_))));
assert_eq!(NaiveDateTime::from_sql(&cd).unwrap(), Some(dt));
}
#[cfg(not(feature = "tds73"))]
#[test]
fn leap_seconds_are_datetime_encode_errors() {
use crate::ToSql;
for (h, m) in [(23, 59), (12, 34)] {
let time = NaiveTime::from_hms_nano_opt(h, m, 59, 1_500_000_000).unwrap();
let naive = NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_time(time);
for data in [naive.to_sql(), naive.into_sql()] {
let err = encode(data).unwrap_err();
assert!(
matches!(&err, crate::Error::Conversion(m) if m.contains("leap")),
"{time}: {err:?}"
);
}
}
let last = NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_hms_nano_opt(23, 59, 59, 999_999_999)
.unwrap();
encode(last.to_sql()).unwrap();
}
}