diff --git a/Cargo.lock.msrv b/Cargo.lock.msrv index 7a4c76d87..6dc950db1 100644 --- a/Cargo.lock.msrv +++ b/Cargo.lock.msrv @@ -178,6 +178,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158b0bd7d75cbb6bf9c25967a48a2e9f77da95876b858eadfabaa99cd069de6e" +dependencies = [ + "num", + "time 0.1.45", +] + [[package]] name = "chrono" version = "0.4.35" @@ -475,7 +485,7 @@ name = "examples" version = "0.0.0" dependencies = [ "anyhow", - "chrono", + "chrono 0.4.35", "clap", "futures", "openssl", @@ -484,7 +494,7 @@ dependencies = [ "rustyline-derive", "scylla", "stats_alloc", - "time", + "time 0.3.23", "tokio", "tower", "tracing", @@ -630,7 +640,7 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -874,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -943,6 +953,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-integer", + "num-iter", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.3.3" @@ -975,6 +996,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1427,7 +1459,7 @@ dependencies = [ "bigdecimal", "byteorder", "bytes", - "chrono", + "chrono 0.4.35", "criterion", "dashmap", "futures", @@ -1450,7 +1482,7 @@ dependencies = [ "snap", "socket2", "thiserror", - "time", + "time 0.3.23", "tokio", "tokio-openssl", "tracing", @@ -1467,7 +1499,8 @@ dependencies = [ "bigdecimal", "byteorder", "bytes", - "chrono", + "chrono 0.3.0", + "chrono 0.4.35", "criterion", "lz4_flex", "num-bigint 0.3.3", @@ -1477,7 +1510,7 @@ dependencies = [ "serde", "snap", "thiserror", - "time", + "time 0.3.23", "tokio", "uuid", ] @@ -1500,7 +1533,7 @@ dependencies = [ "bigdecimal", "byteorder", "bytes", - "chrono", + "chrono 0.4.35", "futures", "ntest", "num-bigint 0.3.3", @@ -1706,6 +1739,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.23" @@ -2005,6 +2049,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/scylla-cql/Cargo.toml b/scylla-cql/Cargo.toml index 88312fac2..292b8e9b9 100644 --- a/scylla-cql/Cargo.toml +++ b/scylla-cql/Cargo.toml @@ -21,6 +21,7 @@ thiserror = "1.0" num-bigint-03 = { package = "num-bigint", version = "0.3", optional = true } num-bigint-04 = { package = "num-bigint", version = "0.4", optional = true } bigdecimal-04 = { package = "bigdecimal", version = "0.4", optional = true } +chrono-03 = { package = "chrono", version = "0.3", default-features = false, optional = true } chrono-04 = { package = "chrono", version = "0.4.32", default-features = false, optional = true } lz4_flex = { version = "0.11.1" } async-trait = "0.1.57" @@ -39,11 +40,12 @@ harness = false [features] secrecy-08 = ["dep:secrecy-08"] time-03 = ["dep:time-03"] +chrono-03 = ["dep:chrono-03"] chrono-04 = ["dep:chrono-04"] num-bigint-03 = ["dep:num-bigint-03"] num-bigint-04 = ["dep:num-bigint-04"] bigdecimal-04 = ["dep:bigdecimal-04"] -full-serialization = ["chrono-04", "time-03", "secrecy-08", "num-bigint-03", "num-bigint-04", "bigdecimal-04"] +full-serialization = ["chrono-03", "chrono-04", "time-03", "secrecy-08", "num-bigint-03", "num-bigint-04", "bigdecimal-04"] [lints.rust] unreachable_pub = "warn" diff --git a/scylla-cql/src/frame/response/cql_to_rust.rs b/scylla-cql/src/frame/response/cql_to_rust.rs index 8c7c40e99..c9a76871d 100644 --- a/scylla-cql/src/frame/response/cql_to_rust.rs +++ b/scylla-cql/src/frame/response/cql_to_rust.rs @@ -175,6 +175,16 @@ impl FromCqlVal for bigdecimal_04::BigDecimal { } } +#[cfg(feature = "chrono-03")] +impl FromCqlVal for chrono_03::NaiveDate { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Date(cql_date) => cql_date.try_into().map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + #[cfg(feature = "chrono-04")] impl FromCqlVal for chrono_04::NaiveDate { fn from_cql(cql_val: CqlValue) -> Result { @@ -195,6 +205,16 @@ impl FromCqlVal for time_03::Date { } } +#[cfg(feature = "chrono-03")] +impl FromCqlVal for chrono_03::NaiveTime { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Time(cql_time) => cql_time.try_into().map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + #[cfg(feature = "chrono-04")] impl FromCqlVal for chrono_04::NaiveTime { fn from_cql(cql_val: CqlValue) -> Result { @@ -215,6 +235,17 @@ impl FromCqlVal for time_03::Time { } } +#[cfg(feature = "chrono-03")] +impl FromCqlVal for chrono_03::DateTime { + fn from_cql(cql_val: CqlValue) -> Result { + cql_val + .as_cql_timestamp() + .ok_or(FromCqlValError::BadCqlType)? + .try_into() + .map_err(|_| FromCqlValError::BadVal) + } +} + #[cfg(feature = "chrono-04")] impl FromCqlVal for chrono_04::DateTime { fn from_cql(cql_val: CqlValue) -> Result { @@ -523,6 +554,36 @@ mod tests { assert_eq!(Ok(counter), Counter::from_cql(CqlValue::Counter(counter))); } + #[cfg(feature = "chrono-03")] + #[test] + fn naive_date_03_from_cql() { + use chrono_03::NaiveDate; + + let unix_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31))); + assert_eq!( + Ok(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), + NaiveDate::from_cql(unix_epoch) + ); + + let before_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31) - 30)); + assert_eq!( + Ok(NaiveDate::from_ymd_opt(1969, 12, 2).unwrap()), + NaiveDate::from_cql(before_epoch) + ); + + let after_epoch: CqlValue = CqlValue::Date(CqlDate(2_u32.pow(31) + 30)); + assert_eq!( + Ok(NaiveDate::from_ymd_opt(1970, 1, 31).unwrap()), + NaiveDate::from_cql(after_epoch) + ); + + let min_date: CqlValue = CqlValue::Date(CqlDate(0)); + assert!(NaiveDate::from_cql(min_date).is_err()); + + let max_date: CqlValue = CqlValue::Date(CqlDate(u32::MAX)); + assert!(NaiveDate::from_cql(max_date).is_err()); + } + #[cfg(feature = "chrono-04")] #[test] fn naive_date_04_from_cql() { @@ -631,6 +692,50 @@ mod tests { assert_eq!(time_ns, CqlTime::from_cql(cql_value).unwrap().0); } + #[cfg(feature = "chrono-03")] + #[test] + fn naive_time_03_from_cql() { + use chrono_03::NaiveTime; + + // Midnight + let midnight = CqlValue::Time(CqlTime(0)); + assert_eq!( + Ok(NaiveTime::from_hms(0, 0, 0)), + NaiveTime::from_cql(midnight) + ); + + // 7:15:21.123456789 + let morning = CqlValue::Time(CqlTime( + (7 * 3600 + 15 * 60 + 21) * 1_000_000_000 + 123_456_789, + )); + assert_eq!( + Ok(NaiveTime::from_hms_nano_opt(7, 15, 21, 123_456_789).unwrap()), + NaiveTime::from_cql(morning) + ); + + // 23:59:59.999999999 + let late_night = CqlValue::Time(CqlTime( + (23 * 3600 + 59 * 60 + 59) * 1_000_000_000 + 999_999_999, + )); + assert_eq!( + Ok(NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap()), + NaiveTime::from_cql(late_night) + ); + + // Bad values. Since value is out of `chrono_04::NaiveTime` range, it should return `BadVal` error + let bad_time1 = CqlValue::Time(CqlTime(-1)); + assert_eq!(Err(FromCqlValError::BadVal), NaiveTime::from_cql(bad_time1)); + let bad_time2 = CqlValue::Time(CqlTime(i64::MAX)); + assert_eq!(Err(FromCqlValError::BadVal), NaiveTime::from_cql(bad_time2)); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); + assert_eq!( + Err(FromCqlValError::BadCqlType), + NaiveTime::from_cql(bad_type) + ); + } + #[cfg(feature = "chrono-04")] #[test] fn naive_time_04_from_cql() { @@ -731,6 +836,25 @@ mod tests { ); } + #[cfg(feature = "chrono-03")] + #[test] + fn datetime_03_from_cql() { + use chrono_03::{DateTime, NaiveDate, UTC}; + let naivedatetime_utc = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(2, 0, 0) + .unwrap(); + let datetime_utc = DateTime::::from_utc(naivedatetime_utc, UTC); + + assert_eq!( + datetime_utc, + DateTime::::from_cql(CqlValue::Timestamp(CqlTimestamp( + datetime_utc.timestamp() * 1000 + datetime_utc.timestamp_subsec_millis() as i64 + ))) + .unwrap() + ); + } + #[cfg(feature = "chrono-04")] #[test] fn datetime_04_from_cql() { diff --git a/scylla-cql/src/frame/response/result.rs b/scylla-cql/src/frame/response/result.rs index 219e97336..d7c7e8456 100644 --- a/scylla-cql/src/frame/response/result.rs +++ b/scylla-cql/src/frame/response/result.rs @@ -153,6 +153,12 @@ impl CqlValue { } } + #[cfg(test)] + #[cfg(feature = "chrono-03")] + fn as_naive_date_03(&self) -> Option { + self.as_cql_date().and_then(|date| date.try_into().ok()) + } + #[cfg(test)] #[cfg(feature = "chrono-04")] fn as_naive_date_04(&self) -> Option { @@ -171,6 +177,12 @@ impl CqlValue { } } + #[cfg(test)] + #[cfg(feature = "chrono-03")] + fn as_datetime_03(&self) -> Option> { + self.as_cql_timestamp().and_then(|ts| ts.try_into().ok()) + } + #[cfg(test)] #[cfg(feature = "chrono-04")] fn as_datetime_04(&self) -> Option> { @@ -189,6 +201,12 @@ impl CqlValue { } } + #[cfg(test)] + #[cfg(feature = "chrono-03")] + fn as_naive_time_03(&self) -> Option { + self.as_cql_time().and_then(|ts| ts.try_into().ok()) + } + #[cfg(test)] #[cfg(feature = "chrono-04")] fn as_naive_time_04(&self) -> Option { @@ -1362,6 +1380,55 @@ mod tests { assert_eq!(date.as_cql_date(), Some(max_date)); } + #[cfg(feature = "chrono-03")] + #[test] + fn test_naive_date_03_from_cql() { + use chrono_03::NaiveDate; + + // 2^31 when converted to NaiveDate is 1970-01-01 + let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + let date = + super::deser_cql_value(&ColumnType::Date, &mut (1u32 << 31).to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_naive_date_03(), Some(unix_epoch)); + + // 2^31 - 30 when converted to NaiveDate is 1969-12-02 + let before_epoch = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) - 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_naive_date_03(), Some(before_epoch)); + + // 2^31 + 30 when converted to NaiveDate is 1970-01-31 + let after_epoch = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) + 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_naive_date_03(), Some(after_epoch)); + + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( + super::deser_cql_value(&ColumnType::Date, &mut 0_u32.to_be_bytes().as_ref()) + .unwrap() + .as_naive_date_04(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_naive_date_04(), + None + ); + } + #[cfg(feature = "chrono-04")] #[test] fn test_naive_date_04_from_cql() { @@ -1485,6 +1552,45 @@ mod tests { } } + #[cfg(feature = "chrono-03")] + #[test] + fn test_naive_time_03_from_cql() { + use chrono_03::NaiveTime; + + // 0 when converted to NaiveTime is 0:0:0.0 + let midnight = NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(); + let time = + super::deser_cql_value(&ColumnType::Time, &mut (0i64).to_be_bytes().as_ref()).unwrap(); + + assert_eq!(time.as_naive_time_03(), Some(midnight)); + + // 10:10:30.500,000,001 + let (h, m, s, n) = (10, 10, 30, 500_000_001); + let midnight = NaiveTime::from_hms_nano_opt(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_naive_time_03(), Some(midnight)); + + // 23:59:59.999,999,999 + let (h, m, s, n) = (23, 59, 59, 999_999_999); + let midnight = NaiveTime::from_hms_nano_opt(h, m, s, n).unwrap(); + let time = super::deser_cql_value( + &ColumnType::Time, + &mut ((h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64) + .to_be_bytes() + .as_ref(), + ) + .unwrap(); + + assert_eq!(time.as_naive_time_03(), Some(midnight)); + } + #[cfg(feature = "chrono-04")] #[test] fn test_naive_time_04_from_cql() { @@ -1577,6 +1683,68 @@ mod tests { } } + #[cfg(feature = "chrono-03")] + #[test] + fn test_datetime_03_from_cql() { + use chrono_03::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, UTC}; + + // 0 when converted to DateTime is 1970-01-01 0:00:00.00 + let unix_epoch = DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), UTC); + let date = super::deser_cql_value(&ColumnType::Timestamp, &mut 0i64.to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_datetime_03(), Some(unix_epoch)); + + // When converted to NaiveDateTime, this is 1969-12-01 11:29:29.5 + let timestamp: i64 = -((((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500); + let before_epoch = DateTime::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(1969, 12, 1).unwrap(), + NaiveTime::from_hms_milli_opt(11, 29, 29, 500).unwrap(), + ), + UTC, + ); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime_03(), Some(before_epoch)); + + // when converted to NaiveDateTime, this is is 1970-01-31 12:30:30.5 + let timestamp: i64 = (((30 * 24 + 12) * 60 + 30) * 60 + 30) * 1000 + 500; + let after_epoch = DateTime::from_utc( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(), + NaiveTime::from_hms_milli_opt(12, 30, 30, 500).unwrap(), + ), + UTC, + ); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime_03(), Some(after_epoch)); + + // 0 and u32::MAX are out of NaiveDate range, fails with an error, not panics + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MIN.to_be_bytes().as_ref()) + .unwrap() + .as_datetime_04(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_datetime_04(), + None + ); + } + #[cfg(feature = "chrono-04")] #[test] fn test_datetime_04_from_cql() { diff --git a/scylla-cql/src/frame/value.rs b/scylla-cql/src/frame/value.rs index f1e853c07..1dc986057 100644 --- a/scylla-cql/src/frame/value.rs +++ b/scylla-cql/src/frame/value.rs @@ -464,6 +464,19 @@ pub struct CqlTimestamp(pub i64); #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct CqlTime(pub i64); +#[cfg(feature = "chrono-03")] +impl From for CqlDate { + fn from(value: chrono_03::NaiveDate) -> Self { + let unix_epoch = chrono_03::NaiveDate::from_yo_opt(1970, 1).unwrap(); + + // `NaiveDate` range is -262145-01-01 to 262143-12-31 + // Both values are well within supported range + let days = ((1 << 31) + value.signed_duration_since(unix_epoch).num_days()) as u32; + + Self(days) + } +} + #[cfg(feature = "chrono-04")] impl From for CqlDate { fn from(value: chrono_04::NaiveDate) -> Self { @@ -477,6 +490,24 @@ impl From for CqlDate { } } +#[cfg(feature = "chrono-03")] +impl TryInto for CqlDate { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + let days_since_unix_epoch = self.0 as i64 - (1 << 31); + + // date_days is u32 then converted to i64 then we subtract 2^31; + // Max value is 2^31, min value is -2^31. Both values can safely fit in chrono::Duration, this call won't panic + let duration_since_unix_epoch = chrono_03::Duration::days(days_since_unix_epoch); + + chrono_03::NaiveDate::from_yo_opt(1970, 1) + .unwrap() + .checked_add_signed(duration_since_unix_epoch) + .ok_or(ValueOverflow) + } +} + #[cfg(feature = "chrono-04")] impl TryInto for CqlDate { type Error = ValueOverflow; @@ -496,6 +527,14 @@ impl TryInto for CqlDate { } } +#[cfg(feature = "chrono-03")] +impl From> for CqlTimestamp { + fn from(value: chrono_03::DateTime) -> Self { + // timestamp() method returns number of seconds since epoch. + Self(value.timestamp() * 1000 + value.timestamp_subsec_millis() as i64) + } +} + #[cfg(feature = "chrono-04")] impl From> for CqlTimestamp { fn from(value: chrono_04::DateTime) -> Self { @@ -503,6 +542,21 @@ impl From> for CqlTimestamp { } } +#[cfg(feature = "chrono-03")] +impl TryInto> for CqlTimestamp { + type Error = ValueOverflow; + + fn try_into(self) -> Result, Self::Error> { + use chrono_03::TimeZone; + let secs = self.0.div_euclid(1000); + let nsecs = self.0.rem_euclid(1000) as u32 * 1_000_000; + match chrono_03::UTC.timestamp_opt(secs, nsecs) { + chrono_03::LocalResult::Single(datetime) => Ok(datetime), + _ => Err(ValueOverflow), + } + } +} + #[cfg(feature = "chrono-04")] impl TryInto> for CqlTimestamp { type Error = ValueOverflow; @@ -516,6 +570,25 @@ impl TryInto> for CqlTimestamp { } } +#[cfg(feature = "chrono-03")] +impl TryFrom for CqlTime { + type Error = ValueOverflow; + + fn try_from(value: chrono_03::NaiveTime) -> Result { + let nanos = value + .signed_duration_since(chrono_03::NaiveTime::from_hms(0, 0, 0)) + .num_nanoseconds() + .unwrap(); + + // Value can exceed max CQL time in case of leap second + if nanos <= 86399999999999 { + Ok(Self(nanos)) + } else { + Err(ValueOverflow) + } + } +} + #[cfg(feature = "chrono-04")] impl TryFrom for CqlTime { type Error = ValueOverflow; @@ -535,6 +608,21 @@ impl TryFrom for CqlTime { } } +#[cfg(feature = "chrono-03")] +impl TryInto for CqlTime { + type Error = ValueOverflow; + + fn try_into(self) -> Result { + let secs = (self.0 / 1_000_000_000) + .try_into() + .map_err(|_| ValueOverflow)?; + let nanos = (self.0 % 1_000_000_000) + .try_into() + .map_err(|_| ValueOverflow)?; + chrono_03::NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or(ValueOverflow) + } +} + #[cfg(feature = "chrono-04")] impl TryInto for CqlTime { type Error = ValueOverflow; @@ -1003,6 +1091,13 @@ impl Value for bigdecimal_04::BigDecimal { } } +#[cfg(feature = "chrono-03")] +impl Value for chrono_03::NaiveDate { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlDate::from(*self).serialize(buf) + } +} + #[cfg(feature = "chrono-04")] impl Value for chrono_04::NaiveDate { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { @@ -1041,6 +1136,13 @@ impl Value for CqlTime { } } +#[cfg(feature = "chrono-03")] +impl Value for chrono_03::DateTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTimestamp::from(*self).serialize(buf) + } +} + #[cfg(feature = "chrono-04")] impl Value for chrono_04::DateTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { @@ -1055,6 +1157,15 @@ impl Value for time_03::OffsetDateTime { } } +#[cfg(feature = "chrono-03")] +impl Value for chrono_03::NaiveTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTime::try_from(*self) + .map_err(|_| ValueTooBig)? + .serialize(buf) + } +} + #[cfg(feature = "chrono-04")] impl Value for chrono_04::NaiveTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { diff --git a/scylla-cql/src/frame/value_tests.rs b/scylla-cql/src/frame/value_tests.rs index b0d5d017b..f19ec3f9b 100644 --- a/scylla-cql/src/frame/value_tests.rs +++ b/scylla-cql/src/frame/value_tests.rs @@ -304,6 +304,35 @@ fn ipaddr_serialization() { ); } +#[cfg(feature = "chrono-03")] +#[test] +fn naive_date_03_serialization() { + use chrono_03::NaiveDate; + // 1970-01-31 is 2^31 + let unix_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); + assert_eq!( + serialized(unix_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 0] + ); + assert_eq!(2_u32.pow(31).to_be_bytes(), [128, 0, 0, 0]); + + // 1969-12-02 is 2^31 - 30 + let before_epoch: NaiveDate = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); + assert_eq!( + serialized(before_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 127, 255, 255, 226] + ); + assert_eq!((2_u32.pow(31) - 30).to_be_bytes(), [127, 255, 255, 226]); + + // 1970-01-31 is 2^31 + 30 + let after_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); + assert_eq!( + serialized(after_epoch, ColumnType::Date), + vec![0, 0, 0, 4, 128, 0, 0, 30] + ); + assert_eq!((2_u32.pow(31) + 30).to_be_bytes(), [128, 0, 0, 30]); +} + #[cfg(feature = "chrono-04")] #[test] fn naive_date_04_serialization() { @@ -410,6 +439,43 @@ fn cql_time_serialization() { } } +#[cfg(feature = "chrono-03")] +#[test] +fn naive_time_03_serialization() { + use chrono_03::NaiveTime; + + let midnight_time: i64 = 0; + let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; + let any_time: i64 = (3600 + 2 * 60 + 3) * 1_000_000_000 + 4; + let test_cases = [ + (NaiveTime::from_hms(0, 0, 0), midnight_time.to_be_bytes()), + ( + NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(), + max_time.to_be_bytes(), + ), + ( + NaiveTime::from_hms_nano_opt(1, 2, 3, 4).unwrap(), + any_time.to_be_bytes(), + ), + ]; + for (time, expected) in test_cases { + let bytes = serialized(time, ColumnType::Time); + + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); + + assert_eq!(bytes, expected_bytes) + } + + // Leap second must return error on serialize + let leap_second = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_500_000_000).unwrap(); + let mut buffer = Vec::new(); + assert_eq!( + <_ as Value>::serialize(&leap_second, &mut buffer), + Err(ValueTooBig) + ) +} + #[cfg(feature = "chrono-04")] #[test] fn naive_time_04_serialization() { diff --git a/scylla-cql/src/types/serialize/value.rs b/scylla-cql/src/types/serialize/value.rs index 6497d8665..da68ece50 100644 --- a/scylla-cql/src/types/serialize/value.rs +++ b/scylla-cql/src/types/serialize/value.rs @@ -19,7 +19,7 @@ use crate::frame::value::{ MaybeUnset, Unset, Value, }; -#[cfg(feature = "chrono-04")] +#[cfg(any(feature = "chrono-04", feature = "chrono-03"))] use crate::frame::value::ValueOverflow; use super::writers::WrittenCellProof; @@ -157,6 +157,13 @@ impl SerializeCql for CqlTime { writer.set_value(me.0.to_be_bytes().as_slice()).unwrap() }); } +#[cfg(feature = "chrono-03")] +impl SerializeCql for chrono_03::NaiveDate { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Date); + ::serialize(&(*me).into(), typ, writer)? + }); +} #[cfg(feature = "chrono-04")] impl SerializeCql for chrono_04::NaiveDate { impl_serialize_via_writer!(|me, typ, writer| { @@ -164,6 +171,13 @@ impl SerializeCql for chrono_04::NaiveDate { ::serialize(&(*me).into(), typ, writer)? }); } +#[cfg(feature = "chrono-03")] +impl SerializeCql for chrono_03::DateTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Timestamp); + ::serialize(&(*me).into(), typ, writer)? + }); +} #[cfg(feature = "chrono-04")] impl SerializeCql for chrono_04::DateTime { impl_serialize_via_writer!(|me, typ, writer| { @@ -171,6 +185,16 @@ impl SerializeCql for chrono_04::DateTime { ::serialize(&(*me).into(), typ, writer)? }); } +#[cfg(feature = "chrono-03")] +impl SerializeCql for chrono_03::NaiveTime { + impl_serialize_via_writer!(|me, typ, writer| { + exact_type_check!(typ, Time); + let cql_time = CqlTime::try_from(*me).map_err(|_: ValueOverflow| { + mk_ser_err::(typ, BuiltinSerializationErrorKind::ValueOverflow) + })?; + ::serialize(&cql_time, typ, writer)? + }); +} #[cfg(feature = "chrono-04")] impl SerializeCql for chrono_04::NaiveTime { impl_serialize_via_writer!(|me, typ, writer| { diff --git a/scylla/Cargo.toml b/scylla/Cargo.toml index ee1b9e6ce..840657256 100644 --- a/scylla/Cargo.toml +++ b/scylla/Cargo.toml @@ -18,12 +18,13 @@ default = [] ssl = ["dep:tokio-openssl", "dep:openssl"] cloud = ["ssl", "scylla-cql/serde", "dep:serde_yaml", "dep:serde", "dep:url", "dep:base64"] secrecy-08 = ["scylla-cql/secrecy-08"] +chrono-03 = ["scylla-cql/chrono-03"] chrono-04 = ["scylla-cql/chrono-04"] time-03 = ["scylla-cql/time-03"] num-bigint-03 = ["scylla-cql/num-bigint-03"] num-bigint-04 = ["scylla-cql/num-bigint-04"] bigdecimal-04 = ["scylla-cql/bigdecimal-04"] -full-serialization = ["chrono-04", "time-03", "secrecy-08", "num-bigint-03", "num-bigint-04", "bigdecimal-04"] +full-serialization = ["chrono-03", "chrono-04", "time-03", "secrecy-08", "num-bigint-03", "num-bigint-04", "bigdecimal-04"] [dependencies] scylla-macros = { version = "0.4.0", path = "../scylla-macros" } diff --git a/scylla/src/transport/cql_types_test.rs b/scylla/src/transport/cql_types_test.rs index c145a7ceb..513327bd7 100644 --- a/scylla/src/transport/cql_types_test.rs +++ b/scylla/src/transport/cql_types_test.rs @@ -293,6 +293,12 @@ async fn test_counter() { } } +#[cfg(feature = "chrono-03")] +#[tokio::test] +async fn test_naive_date_03() { + // TODO: chronov3 does not have constants such as NaiveDate::MIN/MAX...... +} + #[cfg(feature = "chrono-04")] #[tokio::test] async fn test_naive_date_04() {