Skip to content

Commit

Permalink
feat(mssql): support DateTime2 type via chrono
Browse files Browse the repository at this point in the history
  • Loading branch information
milgner committed Feb 6, 2022
1 parent 2182925 commit dfe3a12
Show file tree
Hide file tree
Showing 8 changed files with 153 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
6 changes: 6 additions & 0 deletions sqlx-core/src/mssql/protocol/type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,12 @@ impl TypeInfo {
s.push_str("bit");
}

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

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

use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
use crate::error::BoxDynError;
use crate::mssql::{Mssql, MssqlTypeInfo, MssqlValueRef};
use crate::mssql::protocol::type_info::{DataType, TypeInfo};
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 5 digits for nanoseconds.
impl Type<Mssql> for chrono::DateTime<chrono::Utc> {
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)
}
}

/// Split the time into days from Gregorian calendar, seconds and nanoseconds
/// as required for DateTime2
fn split_time(date_time: &chrono::DateTime<chrono::Utc>) -> (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)
}

/// Encodes DateTime objects for transfer over the wire
impl Encode<'_, Mssql> for chrono::DateTime<chrono::Utc> {
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> IsNull {
let (days, seconds, ns) = split_time(self);

// always use full scale=7, using 5 bytes
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);
buf.extend_from_slice(&date);
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;
}
for _i in 0..(7 - scale) {
acc *= 10;
}
let nsbig = acc * 100;
let seconds = nsbig / 1_000_000_000;
let ns = nsbig % 1_000_000_000;
(seconds as u32, ns as u32)
}

/// Decodes DateTime2N values received from the server
impl Decode<'_, Mssql> for chrono::DateTime<chrono::Utc> {
fn decode(value: MssqlValueRef<'_>) -> Result<Self, BoxDynError> {
let bytes = value.as_bytes()?;
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(value.type_info.0.scale, &bytes[0..timesize]);
let time = NaiveTime::from_num_seconds_from_midnight(seconds, nanoseconds);

Ok(chrono::DateTime::from_utc(day.and_time(time), chrono::Utc))
}
}

#[cfg(test)]
mod tests {
use chrono::{DateTime, Timelike};

use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
use crate::mssql::{MssqlTypeInfo, MssqlValueRef};
use crate::mssql::protocol::type_info::{DataType, TypeInfo};

#[test]
fn test_decode_from_encode() {
let now = chrono::Utc::now();
let mut buf: Vec<u8> = Vec::new();
assert!(matches!(now.encode_by_ref(&mut buf), IsNull::No));

let decoded: DateTime<chrono::Utc> = chrono::DateTime::decode(MssqlValueRef {
data: Some(&bytes::Bytes::from(buf)),
type_info: MssqlTypeInfo(TypeInfo {
ty: DataType::DateTime2N,
size: 8,
precision: 0,
scale: 7,
collation: None})
}).unwrap();

let now_truncated_ns = now.with_nanosecond((now.nanosecond()/ 10000) * 10000).unwrap();
assert_eq!(now_truncated_ns, decoded);
}
}
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 @@ -6,6 +6,8 @@ mod bool;
mod float;
mod int;
mod str;
#[cfg(feature = "chrono")]
mod chrono;

impl<'q, T: 'q + Encode<'q, Mssql>> Encode<'q, Mssql> for Option<T> {
fn encode(self, buf: &mut Vec<u8>) -> IsNull {
Expand Down
6 changes: 6 additions & 0 deletions tests/mssql/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use sqlx::mssql::Mssql;
use sqlx_test::test_type;
use chrono::{DateTime,NaiveDateTime,Utc};

test_type!(null<Option<i32>>(Mssql,
"CAST(NULL as INT)" == None::<i32>
Expand Down Expand Up @@ -41,3 +42,8 @@ test_type!(bool(
"CAST(1 as BIT)" == true,
"CAST(0 as BIT)" == false
));

test_type!(DateTime<_>(
Mssql,
"CAST('2016-10-23 12:45:37.1234567' as DateTime2)" == DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(1477226737, 123456700), Utc)
));
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 dfe3a12

Please sign in to comment.