Skip to content

Commit

Permalink
feat(mssql): support DateTime2 and DateTimeOffset types via chrono
Browse files Browse the repository at this point in the history
  • Loading branch information
milgner committed Feb 26, 2022
1 parent 99d3220 commit 6b44b23
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ all-types = [
"decimal",
"json",
"time",
"chrono",
"with_chrono",
"ipnetwork",
"mac_address",
"uuid",
Expand Down Expand Up @@ -121,7 +121,7 @@ mssql = ["sqlx-core/mssql", "sqlx-macros/mssql"]
# types
bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros/bigdecimal"]
decimal = ["sqlx-core/decimal", "sqlx-macros/decimal"]
chrono = ["sqlx-core/chrono", "sqlx-macros/chrono"]
with_chrono = ["sqlx-core/chrono", "sqlx-macros/chrono"]
ipnetwork = ["sqlx-core/ipnetwork", "sqlx-macros/ipnetwork"]
mac_address = ["sqlx-core/mac_address", "sqlx-macros/mac_address"]
uuid = ["sqlx-core/uuid", "sqlx-macros/uuid"]
Expand Down Expand Up @@ -153,6 +153,7 @@ url = "2.2.2"
rand = "0.8.4"
rand_xoshiro = "0.6.0"
hex = "0.4.3"
chrono = "0.4.19"
#
# Any
#
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ sqlx = { version = "0.5", features = [ "runtime-async-std-native-tls" ] }

- `uuid`: Add support for UUID (in Postgres).

- `chrono`: Add support for date and time types from `chrono`.
- `with_chrono`: Add support for date and time types from `chrono`.

- `time`: Add support for date and time types from `time` crate (alternative to `chrono`, which is preferred by `query!` macro, if both enabled)

Expand Down
15 changes: 15 additions & 0 deletions sqlx-core/src/mssql/protocol/type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ impl TypeInfo {
DataType::BigChar => "BIGCHAR",
DataType::NChar => "NCHAR",

DataType::DateTime2N => "DATETIME2",
DataType::DateTimeOffsetN => "DATETIMEOFFSET",

_ => unimplemented!("name: unsupported data type {:?}", self.ty),
}
}
Expand Down Expand Up @@ -578,6 +581,18 @@ impl TypeInfo {
s.push_str("bit");
}

DataType::DateTime2N => {
s.push_str("datetime2(");
s.push_str(itoa::Buffer::new().format(self.scale));
s.push_str(")");
}

DataType::DateTimeOffsetN => {
s.push_str("datetimeoffset(");
s.push_str(itoa::Buffer::new().format(self.scale));
s.push_str(")");
}

_ => unimplemented!("fmt: unsupported data type {:?}", self.ty),
}
}
Expand Down
152 changes: 152 additions & 0 deletions sqlx-core/src/mssql/types/chrono.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Datelike, FixedOffset, NaiveDateTime, Offset, Timelike};

use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
use crate::error::BoxDynError;
use crate::mssql::protocol::type_info::{DataType, TypeInfo};
use crate::mssql::{Mssql, MssqlTypeInfo, MssqlValueRef};
use crate::types::Type;

/// Provides conversion of chrono::DateTime (UTC) to MS SQL DateTime2N
///
/// Note that MS SQL has a number of DateTime-related types and conversion
/// might not work.
/// During encoding, values are always encoded with the best possible
/// precision, which uses 7 digits for nanoseconds.
impl Type<Mssql> for NaiveDateTime {
fn type_info() -> MssqlTypeInfo {
MssqlTypeInfo(TypeInfo {
scale: 7,
ty: DataType::DateTime2N,
size: 8,
collation: None,
precision: 0,
})
}

fn compatible(ty: &MssqlTypeInfo) -> bool {
matches!(ty.0.ty, DataType::DateTime2N)
}
}

impl<T> Type<Mssql> for DateTime<T>
where
T: chrono::TimeZone,
{
fn type_info() -> MssqlTypeInfo {
MssqlTypeInfo(TypeInfo {
scale: 7,
ty: DataType::DateTimeOffsetN,
size: 8,
collation: None,
precision: 34,
})
}

fn compatible(ty: &MssqlTypeInfo) -> bool {
matches!(ty.0.ty, DataType::DateTimeOffsetN)
}
}

/// Split the time into days from Gregorian calendar, seconds and nanoseconds
/// as required for DateTime2
fn split_time(date_time: &NaiveDateTime) -> (i32, u32, u32) {
let mut days = date_time.num_days_from_ce() - 1;
let mut seconds = date_time.num_seconds_from_midnight();
let mut ns = date_time.nanosecond();

// this date format cannot encode anything outside of 0000-01-01 to 9999-12-31
// so it's best to do some bounds-checking
if days < 0 {
days = 0;
seconds = 0;
ns = 0;
} else if days > 3652058 {
// corresponds to 9999-12-31, the highest plausible value for YYYY-MM-DD
days = 3652058;
seconds = 59 + 59 * 60 + 23 * 3600;
ns = 999999900
}
(days, seconds, ns)
}

fn encode_date_time2(datetime: &NaiveDateTime) -> [u8; 8] {
let (days, seconds, ns) = split_time(datetime);

// always use full scale, 7 digits for nanoseconds,
// requiring 5 bytes for seconds + nanoseconds combined
let mut date = [0u8; 8];
let ns_total = (seconds as i64) * 1_000_000_000 + ns as i64;
let t = ns_total / 100;
for i in 0..5 {
date[i] = (t >> i * 8) as u8;
}
LittleEndian::write_i24(&mut date[5..8], days);
date
}

/// Encodes DateTime objects for transfer over the wire
impl Encode<'_, Mssql> for NaiveDateTime {
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
let encoded = encode_date_time2(self);
buf.extend_from_slice(&encoded);
IsNull::No
}
}

impl<T> Encode<'_, Mssql> for DateTime<T>
where
T: chrono::TimeZone,
{
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
buf.extend_from_slice(&encode_date_time2(&self.naive_utc()));
let from_utc = self.offset().fix().local_minus_utc();
let mut encoded_offset: [u8; 2] = [0, 0];
LittleEndian::write_i16(&mut encoded_offset, (from_utc / 60) as i16);
buf.extend_from_slice(&encoded_offset);
IsNull::No
}
}

/// Determines seconds since midnight and nanoseconds since the last second
fn decode_time(scale: u8, data: &[u8]) -> (u32, u32) {
let mut acc = 0u64;
for i in (0..data.len()).rev() {
acc <<= 8;
acc |= data[i] as u64;
}
acc *= 10u64.pow(9u32 - scale as u32);
let seconds = acc / 1_000_000_000;
let ns = acc % 1_000_000_000;
(seconds as u32, ns as u32)
}

fn decode_datetime2(scale: u8, bytes: &[u8]) -> NaiveDateTime {
let timesize = bytes.len() - 3;

let days_from_ce = LittleEndian::read_i24(&bytes[timesize..]);
let day = chrono::NaiveDate::from_num_days_from_ce(days_from_ce + 1);

let (seconds, nanoseconds) = decode_time(scale, &bytes[0..timesize]);
let time = chrono::NaiveTime::from_num_seconds_from_midnight(seconds, nanoseconds);

day.and_time(time)
}

/// Decodes DateTime2N values received from the server
impl Decode<'_, Mssql> for NaiveDateTime {
fn decode(value: MssqlValueRef<'_>) -> Result<Self, BoxDynError> {
let bytes = value.as_bytes()?;
Ok(decode_datetime2(value.type_info.0.scale, bytes))
}
}

impl Decode<'_, Mssql> for DateTime<FixedOffset> {
fn decode(value: MssqlValueRef<'_>) -> Result<Self, BoxDynError> {
let bytes = value.as_bytes()?;
let naive = decode_datetime2(value.type_info.0.scale, &bytes[..bytes.len() - 2]);
let offset = LittleEndian::read_i16(&bytes[bytes.len() - 2..]);
Ok(DateTime::from_utc(naive, FixedOffset::east(offset as i32)))
}
}
2 changes: 2 additions & 0 deletions sqlx-core/src/mssql/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::mssql::protocol::type_info::{DataType, TypeInfo};
use crate::mssql::{Mssql, MssqlTypeInfo};

mod bool;
#[cfg(feature = "chrono")]
mod chrono;
mod float;
mod int;
mod str;
Expand Down
12 changes: 12 additions & 0 deletions tests/mssql/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::{DateTime, NaiveDateTime};
use sqlx::mssql::Mssql;
use sqlx_test::test_type;

Expand Down Expand Up @@ -41,3 +42,14 @@ test_type!(bool(
"CAST(1 as BIT)" == true,
"CAST(0 as BIT)" == false
));

test_type!(NaiveDateTime(
Mssql,
"CAST('2016-10-23 12:45:37.1234567' as DateTime2)"
== NaiveDateTime::from_timestamp(1477226737, 123456700)
));

test_type!(DateTime<_>(
Mssql,
"CAST('2016-10-23 12:45:37.1234567 +02:00' as datetimeoffset(7))" == DateTime::parse_from_rfc3339("2016-10-23T12:45:37.1234567+02:00").unwrap()
));
2 changes: 1 addition & 1 deletion tests/x.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None, data

for version in ["2019", "2017"]:
run(
f"cargo test --no-default-features --features macros,offline,any,all-types,mssql,runtime-{runtime}-{tls}",
f"cargo test --no-default-features --features macros,offline,any,all-types,with_chrono,mssql,runtime-{runtime}-{tls}",
comment=f"test mssql {version}",
service=f"mssql_{version}",
tag=f"mssql_{version}" if runtime == "async-std" else f"mssql_{version}_{runtime}",
Expand Down

0 comments on commit 6b44b23

Please sign in to comment.