diff --git a/Cargo.lock b/Cargo.lock index 2f0ddab8d8..a90288e544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2349,6 +2349,7 @@ version = "0.5.10" dependencies = [ "anyhow", "async-std", + "chrono", "dotenv", "env_logger 0.8.4", "futures", diff --git a/Cargo.toml b/Cargo.toml index 7a2ea8ae28..d0dbdbb781 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ all-types = [ "decimal", "json", "time", - "chrono", + "with_chrono", "ipnetwork", "mac_address", "uuid", @@ -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"] @@ -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 # diff --git a/README.md b/README.md index 09d5b38631..c110316863 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/sqlx-core/src/mssql/protocol/type_info.rs b/sqlx-core/src/mssql/protocol/type_info.rs index 99a22438c2..a9d41f8a51 100644 --- a/sqlx-core/src/mssql/protocol/type_info.rs +++ b/sqlx-core/src/mssql/protocol/type_info.rs @@ -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), } } diff --git a/sqlx-core/src/mssql/types/chrono.rs b/sqlx-core/src/mssql/types/chrono.rs new file mode 100644 index 0000000000..aa9278596d --- /dev/null +++ b/sqlx-core/src/mssql/types/chrono.rs @@ -0,0 +1,132 @@ +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 7 digits for nanoseconds. +impl Type for chrono::DateTime { + 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) -> (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 { + fn encode_by_ref(&self, buf: &mut Vec) -> IsNull { + let (days, seconds, ns) = split_time(self); + + // 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); + 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; + } + 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) +} + +/// Decodes DateTime2N values received from the server +impl Decode<'_, Mssql> for chrono::DateTime { + fn decode(value: MssqlValueRef<'_>) -> Result { + 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 = Vec::new(); + assert!(matches!(now.encode_by_ref(&mut buf), IsNull::No)); + + let decoded: DateTime = 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); + } +} diff --git a/sqlx-core/src/mssql/types/mod.rs b/sqlx-core/src/mssql/types/mod.rs index 148eaeae85..af80bf2160 100644 --- a/sqlx-core/src/mssql/types/mod.rs +++ b/sqlx-core/src/mssql/types/mod.rs @@ -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 { fn encode(self, buf: &mut Vec) -> IsNull { diff --git a/tests/mssql/types.rs b/tests/mssql/types.rs index c3bb1076a5..bb76b93341 100644 --- a/tests/mssql/types.rs +++ b/tests/mssql/types.rs @@ -1,5 +1,6 @@ use sqlx::mssql::Mssql; use sqlx_test::test_type; +use chrono::{DateTime,NaiveDateTime,Utc}; test_type!(null>(Mssql, "CAST(NULL as INT)" == None:: @@ -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::::from_utc(NaiveDateTime::from_timestamp(1477226737, 123456700), Utc) +)); diff --git a/tests/x.py b/tests/x.py index 33e5ffce9e..d97f3b521d 100755 --- a/tests/x.py +++ b/tests/x.py @@ -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}",