diff --git a/docs/source/data-types/data-types.md b/docs/source/data-types/data-types.md index ec5cac41f3..274b49dd6e 100644 --- a/docs/source/data-types/data-types.md +++ b/docs/source/data-types/data-types.md @@ -21,9 +21,9 @@ Database types and their Rust equivalents: * `Blob` <----> `Vec` * `Inet` <----> `std::net::IpAddr` * `Uuid`, `Timeuuid` <----> `uuid::Uuid` -* `Date` <----> `chrono::NaiveDate`, `u32` -* `Time` <----> `chrono::Duration` -* `Timestamp` <----> `chrono::Duration` +* `Date` <----> `u32`, `chrono::NaiveDate`, `time::Date` +* `Time` <----> `i64`, `chrono::NaiveTime`, `time::Time` +* `Timestamp` <----> `i64`, `chrono::DateTime`, `time::OffsetDateTime` * `Duration` <----> `value::CqlDuration` * `Decimal` <----> `bigdecimal::Decimal` * `Varint` <----> `num_bigint::BigInt` @@ -55,4 +55,4 @@ Database types and their Rust equivalents: tuple udt -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/date.md b/docs/source/data-types/date.md index 6d3384c6af..6e59457f2e 100644 --- a/docs/source/data-types/date.md +++ b/docs/source/data-types/date.md @@ -1,29 +1,77 @@ # Date -For most use cases `Date` can be represented as -[`chrono::NaiveDate`](https://docs.rs/chrono/0.4.19/chrono/naive/struct.NaiveDate.html).\ -`NaiveDate` supports dates from -262145-1-1 to 262143-12-31. +Depending on feature flags, three different types can be used to interact with date. -For dates outside of this range you can use the raw `u32` representation. +Internally [date](https://docs.scylladb.com/stable/cql/types.html#dates) is represented as number of days since +-5877641-06-23 i.e. 2^31 days before unix epoch. + +## CqlDate + +Without any extra features enabled, only `frame::value::CqlDate` is available. It's an +[`u32`](https://doc.rust-lang.org/std/primitive.u32.html) wrapper and it matches the internal date representation. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. -### Using `chrono::NaiveDate`: ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlDate; +use scylla::IntoTypedRows; + +// 1970-01-08 +let to_insert = CqlDate((1 << 31) + 7); + +// Insert date into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read raw Date from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlDate,)>() { + let (date_value,): (CqlDate,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::NaiveDate + +If full range is not required and `chrono` feature is enabled, +[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) can be used. +[`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) supports dates from +-262145-01-01 to 262143-12-31. + +```rust # extern crate chrono; +# extern crate scylla; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::NaiveDate; use scylla::IntoTypedRows; -use chrono::naive::NaiveDate; -// Insert some date into the table -let to_insert: NaiveDate = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap(); +// 2021-03-24 +let to_insert = NaiveDate::from_ymd_opt(2021, 3, 24).unwrap(); + +// Insert date into the table session .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; // Read NaiveDate from the table -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ for row in rows.into_typed::<(NaiveDate,)>() { let (date_value,): (NaiveDate,) = row?; } @@ -32,32 +80,40 @@ if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.ro # } ``` -### Using raw `u32` representation -Internally `Date` is represented as number of days since -5877641-06-23 i.e. 2^31 days before unix epoch. +## time::Date + +Alternatively, `time` feature can be used to enable support of +[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html). +[`time::Date`](https://docs.rs/time/0.3/time/struct.Date.html)'s value range depends on feature flags, see its +documentation to get more info. ```rust # extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { -use scylla::frame::value::Date; -use scylla::frame::response::result::CqlValue; +use scylla::IntoTypedRows; +use time::{Date, Month}; -// Insert date using raw u32 representation -let to_insert: Date = Date(2_u32.pow(31)); // 1970-01-01 +// 2021-03-24 +let to_insert = Date::from_calendar_date(2021, Month::March, 24).unwrap(); + +// Insert date into the table session .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read raw Date from the table -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows { - let date_value: u32 = match row.columns[0] { - Some(CqlValue::Date(date_value)) => date_value, - _ => panic!("Should be a date!") - }; +// Read Date from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(Date,)>() { + let (date_value,): (Date,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/time.md b/docs/source/data-types/time.md index 6f46f9dae1..3faddaf7a3 100644 --- a/docs/source/data-types/time.md +++ b/docs/source/data-types/time.md @@ -1,33 +1,117 @@ # Time -`Time` is represented as [`chrono::Duration`](https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html) -Internally `Time` is represented as number of nanoseconds since midnight. -It can't be negative or exceed `86399999999999` (24 hours). +Depending on feature flags used, three different types can be used to interact with time. -When sending in a query it needs to be wrapped in `value::Time` to differentiate from [`Timestamp`](timestamp.md) +Internally [time](https://docs.scylladb.com/stable/cql/types.html#times) is represented as number of nanoseconds since +midnight. It can't be negative or exceed `86399999999999` (23:59:59.999999999). + +## CqlTime + +Without any extra features enabled, only `frame::value::CqlTime` is available. It's an +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlTime; +use scylla::IntoTypedRows; + +// 64 seconds since midnight +let to_insert = CqlTime(64 * 1_000_000_000); + +// Insert time into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlTime,)>() { + let (time_value,): (CqlTime,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::NaiveTime + +If `chrono` feature is enabled, [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) +can be used to interact with the database. Although chrono can represent leap seconds, they are not supported. +Attempts to convert [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html) with leap +second to `CqlTime` or write it to the database will return an error. + +```rust # extern crate chrono; +# extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::NaiveTime; +use scylla::IntoTypedRows; + +// 01:02:03.456,789,012 +let to_insert = NaiveTime::from_hms_nano_opt(1, 2, 3, 456_789_012); + +// Insert time into the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(NaiveTime,)>() { + let (time_value,): (NaiveTime,) = row?; + } +} +# Ok(()) +# } +``` + +## time::Time + +If `time` feature is enabled, [`time::Time`](https://docs.rs/time/0.3/time/struct.Time.html) can be used to interact +with the database. + +```rust +# extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { use scylla::IntoTypedRows; -use scylla::frame::value::Time; -use chrono::Duration; +use time::Time; + +// 01:02:03.456,789,012 +let to_insert = Time::from_hms_nano(1, 2, 3, 456_789_012).unwrap(); -// Insert some time into the table -let to_insert: Duration = Duration::seconds(64); +// Insert time into the table session - .query("INSERT INTO keyspace.table (a) VALUES(?)", (Time(to_insert),)) + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read time from the table, no need for a wrapper here -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (time_value,): (Duration,) = row?; +// Read time from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(Time,)>() { + let (time_value,): (Time,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/docs/source/data-types/timestamp.md b/docs/source/data-types/timestamp.md index d61aec2aec..1be843754d 100644 --- a/docs/source/data-types/timestamp.md +++ b/docs/source/data-types/timestamp.md @@ -1,33 +1,129 @@ # Timestamp -`Timestamp` is represented as [`chrono::Duration`](https://docs.rs/chrono/0.4.19/chrono/struct.Duration.html) -Internally `Timestamp` is represented as `i64` describing number of milliseconds since unix epoch. -Driver converts this to `chrono::Duration` +Depending on feature flags, three different types can be used to interact with timestamps. -When sending in a query it needs to be wrapped in `value::Timestamp` to differentiate from [`Time`](time.md) +Internally [timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) is represented as +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) describing number of milliseconds since unix epoch. + +## CqlTimestamp + +Without any extra features enabled, only `frame::value::CqlTimestamp` is available. It's an +[`i64`](https://doc.rust-lang.org/std/primitive.i64.html) wrapper and it matches the internal time representation. It's +the only type that supports full range of values that database accepts. + +However, for most use cases other types are more practical. See following sections for `chrono` and `time`. ```rust # extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use scylla::frame::value::CqlTimestamp; +use scylla::IntoTypedRows; + +// 64 seconds since unix epoch, 1970-01-01 00:01:04 +let to_insert = CqlTimestamp(64 * 1000); + +// Write timestamp to the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(CqlTimestamp,)>() { + let (timestamp_value,): (CqlTimestamp,) = row?; + } +} +# Ok(()) +# } +``` + +## chrono::DateTime + +If full value range is not required, `chrono` feature can be used to enable support of +[`chrono::DateTime`](https://docs.rs/chrono/0.4/chrono/struct.DateTime.html). All values are expected to be converted +to UTC timezone explicitly, as [timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) doesn't store +timezone information. Any precision finer than 1ms will be lost. + +```rust # extern crate chrono; +# extern crate scylla; +# use scylla::Session; +# use std::error::Error; +# async fn check_only_compiles(session: &Session) -> Result<(), Box> { +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; +use scylla::IntoTypedRows; + +// 64.123 seconds since unix epoch, 1970-01-01 00:01:04.123 +let to_insert = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(), + NaiveTime::from_hms_milli_opt(0, 1, 4, 123).unwrap(), +) +.and_utc(); + +// Write timestamp to the table +session + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) + .await?; + +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(DateTime,)>() { + let (timestamp_value,): (DateTime,) = row?; + } +} +# Ok(()) +# } +``` + +## time::OffsetDateTime + +Alternatively, `time` feature can be used to enable support of +[`time::OffsetDateTime`](https://docs.rs/time/0.3/time/struct.OffsetDateTime.html). As +[timestamp](https://docs.scylladb.com/stable/cql/types.html#timestamps) doesn't support timezone information, time will +be corrected to UTC and timezone info will be erased on write. On read, UTC timestamp is returned. Any precision finer +than 1ms will also be lost. + +```rust +# extern crate scylla; +# extern crate time; # use scylla::Session; # use std::error::Error; # async fn check_only_compiles(session: &Session) -> Result<(), Box> { use scylla::IntoTypedRows; -use scylla::frame::value::Timestamp; -use chrono::Duration; +use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; + +// 64.123 seconds since unix epoch, 1970-01-01 00:01:04.123 +let to_insert = PrimitiveDateTime::new( + Date::from_calendar_date(1970, Month::January, 1).unwrap(), + Time::from_hms_milli(0, 1, 4, 123).unwrap(), +) +.assume_utc(); -// Insert some timestamp into the table -let to_insert: Duration = Duration::seconds(64); +// Write timestamp to the table session - .query("INSERT INTO keyspace.table (a) VALUES(?)", (Timestamp(to_insert),)) + .query("INSERT INTO keyspace.table (a) VALUES(?)", (to_insert,)) .await?; -// Read timestamp from the table, no need for a wrapper here -if let Some(rows) = session.query("SELECT a FROM keyspace.table", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (timestamp_value,): (Duration,) = row?; +// Read timestamp from the table +if let Some(rows) = session + .query("SELECT a FROM keyspace.table", &[]) + .await? + .rows +{ + for row in rows.into_typed::<(OffsetDateTime,)>() { + let (timestamp_value,): (OffsetDateTime,) = row?; } } # Ok(()) # } -``` \ No newline at end of file +``` diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 84ac516db9..88a7dbda62 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,11 +10,12 @@ futures = "0.3.6" openssl = "0.10.32" rustyline = "9" rustyline-derive = "0.6" -scylla = {path = "../scylla", features = ["ssl", "cloud"]} +scylla = {path = "../scylla", features = ["ssl", "cloud", "chrono", "time"]} tokio = {version = "1.1.0", features = ["full"]} tracing = "0.1.25" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } chrono = { version = "0.4", default-features = false } +time = { version = "0.3.22" } uuid = "1.0" tower = "0.4" stats_alloc = "0.1" diff --git a/examples/cql-time-types.rs b/examples/cql-time-types.rs index ac4da4831e..72f151c050 100644 --- a/examples/cql-time-types.rs +++ b/examples/cql-time-types.rs @@ -2,9 +2,9 @@ // Date, Time, Timestamp use anyhow::Result; -use chrono::{Duration, NaiveDate}; +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use scylla::frame::response::result::CqlValue; -use scylla::frame::value::{Date, Time, Timestamp}; +use scylla::frame::value::{CqlDate, CqlTime, CqlTimestamp}; use scylla::transport::session::{IntoTypedRows, Session}; use scylla::SessionBuilder; use std::env; @@ -29,11 +29,12 @@ async fn main() -> Result<()> { ) .await?; - // Dates in the range -262145-1-1 to 262143-12-31 can be represented using chrono::NaiveDate - let example_date: NaiveDate = NaiveDate::from_ymd_opt(2020, 2, 20).unwrap(); + // If 'chrono' feature is enabled, dates in the range -262145-1-1 to 262143-12-31 can be represented using + // chrono::NaiveDate + let chrono_date = NaiveDate::from_ymd_opt(2020, 2, 20).unwrap(); session - .query("INSERT INTO ks.dates (d) VALUES (?)", (example_date,)) + .query("INSERT INTO ks.dates (d) VALUES (?)", (chrono_date,)) .await?; if let Some(rows) = session.query("SELECT d from ks.dates", &[]).await?.rows { @@ -43,12 +44,32 @@ async fn main() -> Result<()> { Err(_) => continue, // We might read a date that does not fit in NaiveDate, skip it }; - println!("Read a date: {:?}", read_date); + println!("Parsed a date into chrono::NaiveDate: {:?}", read_date); + } + } + + // Alternatively, you can enable 'time' feature and use `time::Date` to represent date. `time::Date` only allows + // dates in range -9999-1-1 to 9999-12-31. Or, if you have 'time/large-dates' feature enabled, this range changes + // to -999999-1-1 to 999999-12-31 + let time_date = time::Date::from_calendar_date(2020, time::Month::March, 21).unwrap(); + + session + .query("INSERT INTO ks.dates (d) VALUES (?)", (time_date,)) + .await?; + + if let Some(rows) = session.query("SELECT d from ks.dates", &[]).await?.rows { + for row in rows.into_typed::<(time::Date,)>() { + let (read_date,) = match row { + Ok(read_date) => read_date, + Err(_) => continue, // We might read a date that does not fit in time::Date, skip it + }; + + println!("Parsed a date into time::Date: {:?}", read_date); } } // Dates outside this range must be represented in the raw form - an u32 describing days since -5877641-06-23 - let example_big_date: Date = Date(u32::MAX); + let example_big_date: CqlDate = CqlDate(u32::MAX); session .query("INSERT INTO ks.dates (d) VALUES (?)", (example_big_date,)) .await?; @@ -64,8 +85,8 @@ async fn main() -> Result<()> { } } - // Time - nanoseconds since midnight in range 0..=86399999999999 - let example_time: Duration = Duration::hours(1); + // Time + // Time is represented as nanosecond count since midnight in range 0..=86399999999999 session .query( @@ -74,21 +95,56 @@ async fn main() -> Result<()> { ) .await?; - // Time as bound value must be wrapped in value::Time to differentiate from Timestamp + // Time can be represented using 3 different types, chrono::NaiveTime, time::Time and CqlTime. All types support + // full value range + + // chrono::NaiveTime + let chrono_time = NaiveTime::from_hms_nano_opt(1, 2, 3, 456_789_012).unwrap(); + + session + .query("INSERT INTO ks.times (t) VALUES (?)", (chrono_time,)) + .await?; + + if let Some(rows) = session.query("SELECT t from ks.times", &[]).await?.rows { + for row in rows.into_typed::<(NaiveTime,)>() { + let (read_time,) = row?; + + println!("Parsed a time into chrono::NaiveTime: {:?}", read_time); + } + } + + // time::Time + let time_time = time::Time::from_hms_nano(2, 3, 4, 567_890_123).unwrap(); + session - .query("INSERT INTO ks.times (t) VALUES (?)", (Time(example_time),)) + .query("INSERT INTO ks.times (t) VALUES (?)", (time_time,)) .await?; if let Some(rows) = session.query("SELECT t from ks.times", &[]).await?.rows { - for row in rows.into_typed::<(Duration,)>() { - let (read_time,): (Duration,) = row?; + for row in rows.into_typed::<(time::Time,)>() { + let (read_time,) = row?; - println!("Read a time: {:?}", read_time); + println!("Parsed a time into time::Time: {:?}", read_time); } } - // Timestamp - milliseconds since unix epoch - 1970-01-01 - let example_timestamp: Duration = Duration::hours(1); // This will describe 1970-01-01 01:00:00 + // CqlTime + let time_time = CqlTime(((3 * 60 + 4) * 60 + 5) * 1_000_000_000 + 678_901_234); + + session + .query("INSERT INTO ks.times (t) VALUES (?)", (time_time,)) + .await?; + + if let Some(rows) = session.query("SELECT t from ks.times", &[]).await?.rows { + for row in rows.into_typed::<(CqlTime,)>() { + let (read_time,) = row?; + + println!("Read a time as raw nanos: {:?}", read_time); + } + } + + // Timestamp + // Timestamp is represented as milliseconds since unix epoch - 1970-01-01. Negative values are also possible session .query( @@ -97,11 +153,16 @@ async fn main() -> Result<()> { ) .await?; - // Timestamp as bound value must be wrapped in value::Timestamp to differentiate from Time + // Timestamp can also be represented using 3 different types, chrono::DateTime, time::OffsetDateTime and + // CqlTimestamp. Only CqlTimestamp allows full range. + + // chrono::DateTime + let chrono_datetime = Utc::now(); + session .query( "INSERT INTO ks.timestamps (t) VALUES (?)", - (Timestamp(example_timestamp),), + (chrono_datetime,), ) .await?; @@ -110,10 +171,54 @@ async fn main() -> Result<()> { .await? .rows { - for row in rows.into_typed::<(Duration,)>() { - let (read_time,): (Duration,) = row?; + for row in rows.into_typed::<(DateTime,)>() { + let (read_time,) = row?; + + println!( + "Parsed a timestamp into chrono::DateTime: {:?}", + read_time + ); + } + } + + // time::OffsetDateTime + let time_datetime = time::OffsetDateTime::now_utc(); + + session + .query("INSERT INTO ks.timestamps (t) VALUES (?)", (time_datetime,)) + .await?; + + if let Some(rows) = session + .query("SELECT t from ks.timestamps", &[]) + .await? + .rows + { + for row in rows.into_typed::<(time::OffsetDateTime,)>() { + let (read_time,) = row?; + + println!( + "Parsed a timestamp into time::OffsetDateTime: {:?}", + read_time + ); + } + } + + // CqlTimestamp + let cql_datetime = CqlTimestamp(1 << 31); + + session + .query("INSERT INTO ks.timestamps (t) VALUES (?)", (cql_datetime,)) + .await?; + + if let Some(rows) = session + .query("SELECT t from ks.timestamps", &[]) + .await? + .rows + { + for row in rows.into_typed::<(CqlTimestamp,)>() { + let (read_time,) = row?; - println!("Read a timestamp: {:?}", read_time); + println!("Read a timestamp as raw millis: {:?}", read_time); } } diff --git a/scylla-cql/Cargo.toml b/scylla-cql/Cargo.toml index 5d5133d91b..44bb427b74 100644 --- a/scylla-cql/Cargo.toml +++ b/scylla-cql/Cargo.toml @@ -21,13 +21,16 @@ uuid = "1.0" thiserror = "1.0" bigdecimal = "0.2.0" num-bigint = "0.3" -chrono = { version = "0.4", default-features = false } +chrono = { version = "0.4", default-features = false, optional = true } lz4_flex = { version = "0.11.1" } async-trait = "0.1.57" serde = { version = "1.0", features = ["derive"], optional = true } +time = { version = "0.3", optional = true } [dev-dependencies] criterion = "0.5" +# Use large-dates feature to test potential edge cases +time = { version = "0.3.21", features = ["large-dates"] } [[bench]] name = "benchmark" @@ -35,3 +38,5 @@ harness = false [features] secret = ["secrecy"] +time = ["dep:time"] +chrono = ["dep:chrono"] diff --git a/scylla-cql/src/frame/response/cql_to_rust.rs b/scylla-cql/src/frame/response/cql_to_rust.rs index 50ba7a9386..e5924054ca 100644 --- a/scylla-cql/src/frame/response/cql_to_rust.rs +++ b/scylla-cql/src/frame/response/cql_to_rust.rs @@ -1,7 +1,6 @@ use super::result::{CqlValue, Row}; -use crate::frame::value::{Counter, CqlDuration}; +use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use bigdecimal::BigDecimal; -use chrono::{DateTime, Duration, NaiveDate, TimeZone, Utc}; use num_bigint::BigInt; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::Hash; @@ -9,6 +8,9 @@ use std::net::IpAddr; use thiserror::Error; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; + #[cfg(feature = "secret")] use secrecy::{Secret, Zeroize}; @@ -125,7 +127,6 @@ impl_from_cql_value_from_method!(Counter, as_counter); // Counter::from_cql impl_from_cql_value_from_method!(BigInt, into_varint); // BigInt::from_cql impl_from_cql_value_from_method!(i8, as_tinyint); // i8::from_cql -impl_from_cql_value_from_method!(NaiveDate, as_date); // NaiveDate::from_cql impl_from_cql_value_from_method!(f32, as_float); // f32::from_cql impl_from_cql_value_from_method!(f64, as_double); // f64::from_cql impl_from_cql_value_from_method!(bool, as_boolean); // bool::from_cql @@ -134,7 +135,6 @@ impl_from_cql_value_from_method!(Vec, into_blob); // Vec::from_cql impl_from_cql_value_from_method!(Uuid, as_uuid); // Uuid::from_cql impl_from_cql_value_from_method!(BigDecimal, into_decimal); // BigDecimal::from_cql -impl_from_cql_value_from_method!(Duration, as_duration); // Duration::from_cql impl_from_cql_value_from_method!(CqlDuration, as_cql_duration); // CqlDuration::from_cql impl FromCqlVal for [u8; N] { @@ -144,16 +144,40 @@ impl FromCqlVal for [u8; N] { } } -impl FromCqlVal for crate::frame::value::Date { +impl FromCqlVal for CqlDate { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Date(d) => Ok(CqlDate(d)), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + +#[cfg(feature = "chrono")] +impl FromCqlVal for NaiveDate { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Date(date_days) => CqlDate(date_days) + .try_into() + .map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + +#[cfg(feature = "time")] +impl FromCqlVal for time::Date { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { - CqlValue::Date(d) => Ok(crate::frame::value::Date(d)), + CqlValue::Date(date_days) => CqlDate(date_days) + .try_into() + .map_err(|_| FromCqlValError::BadVal), _ => Err(FromCqlValError::BadCqlType), } } } -impl FromCqlVal for crate::frame::value::Time { +impl FromCqlVal for CqlTime { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { CqlValue::Time(d) => Ok(Self(d)), @@ -162,7 +186,27 @@ impl FromCqlVal for crate::frame::value::Time { } } -impl FromCqlVal for crate::frame::value::Timestamp { +#[cfg(feature = "chrono")] +impl FromCqlVal for NaiveTime { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Time(d) => CqlTime(d).try_into().map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + +#[cfg(feature = "time")] +impl FromCqlVal for time::Time { + fn from_cql(cql_val: CqlValue) -> Result { + match cql_val { + CqlValue::Time(d) => CqlTime(d).try_into().map_err(|_| FromCqlValError::BadVal), + _ => Err(FromCqlValError::BadCqlType), + } + } +} + +impl FromCqlVal for CqlTimestamp { fn from_cql(cql_val: CqlValue) -> Result { match cql_val { CqlValue::Timestamp(d) => Ok(Self(d)), @@ -171,13 +215,25 @@ impl FromCqlVal for crate::frame::value::Timestamp { } } +#[cfg(feature = "chrono")] impl FromCqlVal for DateTime { fn from_cql(cql_val: CqlValue) -> Result { - let timestamp = cql_val.as_bigint().ok_or(FromCqlValError::BadCqlType)?; - match Utc.timestamp_millis_opt(timestamp) { - chrono::LocalResult::Single(datetime) => Ok(datetime), - _ => Err(FromCqlValError::BadVal), - } + cql_val + .as_cql_timestamp() + .ok_or(FromCqlValError::BadCqlType)? + .try_into() + .map_err(|_| FromCqlValError::BadVal) + } +} + +#[cfg(feature = "time")] +impl FromCqlVal for time::OffsetDateTime { + fn from_cql(cql_val: CqlValue) -> Result { + cql_val + .as_cql_timestamp() + .ok_or(FromCqlValError::BadCqlType)? + .try_into() + .map_err(|_| FromCqlValError::BadVal) } } @@ -360,10 +416,9 @@ impl_tuple_from_cql!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 mod tests { use super::{CqlValue, FromCqlVal, FromCqlValError, FromRow, FromRowError, Row}; use crate as scylla; - use crate::frame::value::Counter; + use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use crate::macros::FromRow; use bigdecimal::BigDecimal; - use chrono::{Duration, NaiveDate}; use num_bigint::{BigInt, ToBigInt}; use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr}; @@ -452,8 +507,11 @@ mod tests { assert_eq!(Ok(counter), Counter::from_cql(CqlValue::Counter(counter))); } + #[cfg(feature = "chrono")] #[test] fn naive_date_from_cql() { + use chrono::NaiveDate; + let unix_epoch: CqlValue = CqlValue::Date(2_u32.pow(31)); assert_eq!( Ok(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), @@ -480,43 +538,59 @@ mod tests { } #[test] - fn date_from_cql() { - use crate::frame::value::Date; - + fn cql_date_from_cql() { let unix_epoch: CqlValue = CqlValue::Date(2_u32.pow(31)); - assert_eq!(Ok(Date(2_u32.pow(31))), Date::from_cql(unix_epoch)); + assert_eq!(Ok(CqlDate(2_u32.pow(31))), CqlDate::from_cql(unix_epoch)); let min_date: CqlValue = CqlValue::Date(0); - assert_eq!(Ok(Date(0)), Date::from_cql(min_date)); + assert_eq!(Ok(CqlDate(0)), CqlDate::from_cql(min_date)); let max_date: CqlValue = CqlValue::Date(u32::MAX); - assert_eq!(Ok(Date(u32::MAX)), Date::from_cql(max_date)); + assert_eq!(Ok(CqlDate(u32::MAX)), CqlDate::from_cql(max_date)); } + #[cfg(feature = "time")] #[test] - fn duration_from_cql() { - let time_duration = Duration::nanoseconds(86399999999999); + fn date_from_cql() { + // UNIX epoch + let unix_epoch = CqlValue::Date(1 << 31); + assert_eq!( + Ok(time::Date::from_ordinal_date(1970, 1).unwrap()), + time::Date::from_cql(unix_epoch) + ); + + // 7 days after UNIX epoch + let after_epoch = CqlValue::Date((1 << 31) + 7); assert_eq!( - time_duration, - Duration::from_cql(CqlValue::Time(time_duration)).unwrap(), + Ok(time::Date::from_ordinal_date(1970, 8).unwrap()), + time::Date::from_cql(after_epoch) ); - let timestamp_duration = Duration::milliseconds(i64::MIN); + // 3 days before UNIX epoch + let before_epoch = CqlValue::Date((1 << 31) - 3); assert_eq!( - timestamp_duration, - Duration::from_cql(CqlValue::Timestamp(timestamp_duration)).unwrap(), + Ok(time::Date::from_ordinal_date(1969, 363).unwrap()), + time::Date::from_cql(before_epoch) ); - let timestamp_i64 = 997; + // Min possible stored date. Since value is out of `time::Date` range, it should return `BadVal` error + let min_date = CqlValue::Date(u32::MIN); + assert_eq!(Err(FromCqlValError::BadVal), time::Date::from_cql(min_date)); + + // Max possible stored date. Since value is out of `time::Date` range, it should return `BadVal` error + let max_date = CqlValue::Date(u32::MAX); + assert_eq!(Err(FromCqlValError::BadVal), time::Date::from_cql(max_date)); + + // Different CQL type. Since value can't be casted, it should return `BadCqlType` error + let bad_type = CqlValue::Double(0.5); assert_eq!( - timestamp_i64, - i64::from_cql(CqlValue::Timestamp(Duration::milliseconds(timestamp_i64))).unwrap() - ) + Err(FromCqlValError::BadCqlType), + time::Date::from_cql(bad_type) + ); } #[test] fn cql_duration_from_cql() { - use crate::frame::value::CqlDuration; let cql_duration = CqlDuration { months: 3, days: 2, @@ -528,31 +602,108 @@ mod tests { ); } + #[test] + fn cql_time_from_cql() { + let time_ns = 86399999999999; + assert_eq!( + time_ns, + CqlTime::from_cql(CqlValue::Time(time_ns)).unwrap().0, + ); + } + + #[cfg(feature = "chrono")] + #[test] + fn naive_time_from_cql() { + use chrono::NaiveTime; + + // Midnight + let midnight = CqlValue::Time(0); + assert_eq!(Ok(NaiveTime::MIN), NaiveTime::from_cql(midnight)); + + // 7:15:21.123456789 + let morning = CqlValue::Time((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((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::NaiveTime` range, it should return `BadVal` error + let bad_time1 = CqlValue::Time(-1); + assert_eq!(Err(FromCqlValError::BadVal), NaiveTime::from_cql(bad_time1)); + let bad_time2 = CqlValue::Time(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 = "time")] #[test] fn time_from_cql() { - use crate::frame::value::Time; - let time_duration = Duration::nanoseconds(86399999999999); + // Midnight + let midnight = CqlValue::Time(0); + assert_eq!(Ok(time::Time::MIDNIGHT), time::Time::from_cql(midnight)); + + // 7:15:21.123456789 + let morning = CqlValue::Time((7 * 3600 + 15 * 60 + 21) * 1_000_000_000 + 123_456_789); + assert_eq!( + Ok(time::Time::from_hms_nano(7, 15, 21, 123_456_789).unwrap()), + time::Time::from_cql(morning) + ); + + // 23:59:59.999999999 + let late_night = CqlValue::Time((23 * 3600 + 59 * 60 + 59) * 1_000_000_000 + 999_999_999); + assert_eq!( + Ok(time::Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap()), + time::Time::from_cql(late_night) + ); + + // Bad values. Since value is out of `time::Time` range, it should return `BadVal` error + let bad_time1 = CqlValue::Time(-1); + assert_eq!( + Err(FromCqlValError::BadVal), + time::Time::from_cql(bad_time1) + ); + let bad_time2 = CqlValue::Time(i64::MAX); assert_eq!( - time_duration, - Time::from_cql(CqlValue::Time(time_duration)).unwrap().0, + Err(FromCqlValError::BadVal), + time::Time::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), + time::Time::from_cql(bad_type) ); } #[test] - fn timestamp_from_cql() { - use crate::frame::value::Timestamp; - let timestamp_duration = Duration::milliseconds(86399999999999); + fn cql_timestamp_from_cql() { + let timestamp_ms = 86399999999999; assert_eq!( - timestamp_duration, - Timestamp::from_cql(CqlValue::Timestamp(timestamp_duration)) + timestamp_ms, + CqlTimestamp::from_cql(CqlValue::Timestamp(timestamp_ms)) .unwrap() .0, ); } + #[cfg(feature = "chrono")] #[test] fn datetime_from_cql() { - use chrono::{DateTime, Duration, Utc}; + use chrono::{DateTime, NaiveDate, Utc}; let naivedatetime_utc = NaiveDate::from_ymd_opt(2022, 12, 31) .unwrap() .and_hms_opt(2, 0, 0) @@ -561,10 +712,59 @@ mod tests { assert_eq!( datetime_utc, - DateTime::::from_cql(CqlValue::Timestamp(Duration::milliseconds( - datetime_utc.timestamp_millis() - ))) - .unwrap() + DateTime::::from_cql(CqlValue::Timestamp(datetime_utc.timestamp_millis())) + .unwrap() + ); + } + + #[cfg(feature = "time")] + #[test] + fn offset_datetime_from_cql() { + // UNIX epoch + let unix_epoch = CqlValue::Timestamp(0); + assert_eq!( + Ok(time::OffsetDateTime::UNIX_EPOCH), + time::OffsetDateTime::from_cql(unix_epoch) + ); + + // 1 day 2 hours 3 minutes 4 seconds and 5 nanoseconds before UNIX epoch + let before_epoch = CqlValue::Timestamp(-(26 * 3600 + 3 * 60 + 4) * 1000 - 5); + assert_eq!( + Ok(time::OffsetDateTime::UNIX_EPOCH + - time::Duration::new(26 * 3600 + 3 * 60 + 4, 5 * 1_000_000)), + time::OffsetDateTime::from_cql(before_epoch) + ); + + // 6 days 7 hours 8 minutes 9 seconds and 10 nanoseconds after UNIX epoch + let after_epoch = CqlValue::Timestamp(((6 * 24 + 7) * 3600 + 8 * 60 + 9) * 1000 + 10); + assert_eq!( + Ok(time::PrimitiveDateTime::new( + time::Date::from_ordinal_date(1970, 7).unwrap(), + time::Time::from_hms_milli(7, 8, 9, 10).unwrap() + ) + .assume_utc()), + time::OffsetDateTime::from_cql(after_epoch) + ); + + // Min possible stored timestamp. Since value is out of `time::OffsetDateTime` range, it should return `BadVal` error + let min_timestamp = CqlValue::Timestamp(i64::MIN); + assert_eq!( + Err(FromCqlValError::BadVal), + time::OffsetDateTime::from_cql(min_timestamp) + ); + + // Max possible stored timestamp. Since value is out of `time::OffsetDateTime` range, it should return `BadVal` error + let max_timestamp = CqlValue::Timestamp(i64::MAX); + assert_eq!( + Err(FromCqlValError::BadVal), + time::OffsetDateTime::from_cql(max_timestamp) + ); + + // 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), + time::OffsetDateTime::from_cql(bad_type) ); } diff --git a/scylla-cql/src/frame/response/result.rs b/scylla-cql/src/frame/response/result.rs index 288baf91eb..5d29458374 100644 --- a/scylla-cql/src/frame/response/result.rs +++ b/scylla-cql/src/frame/response/result.rs @@ -1,13 +1,11 @@ use crate::cql_to_rust::{FromRow, FromRowError}; use crate::frame::response::event::SchemaChangeEvent; use crate::frame::types::vint_decode; -use crate::frame::value::{Counter, CqlDuration}; +use crate::frame::value::{Counter, CqlDate, CqlDuration, CqlTime, CqlTimestamp}; use crate::frame::{frame_errors::ParseError, types}; use bigdecimal::BigDecimal; use byteorder::{BigEndian, ReadBytesExt}; use bytes::{Buf, Bytes}; -use chrono; -use chrono::prelude::*; use num_bigint::BigInt; use std::{ convert::{TryFrom, TryInto}, @@ -17,6 +15,9 @@ use std::{ }; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, Utc}; + #[derive(Debug)] pub struct SetKeyspace { pub keyspace_name: String, @@ -92,7 +93,7 @@ pub enum CqlValue { BigInt(i64), Text(String), /// Milliseconds since unix epoch - Timestamp(chrono::Duration), + Timestamp(i64), Inet(IpAddr), List(Vec), Map(Vec<(CqlValue, CqlValue)>), @@ -108,7 +109,7 @@ pub enum CqlValue { SmallInt(i16), TinyInt(i8), /// Nanoseconds since midnight - Time(chrono::Duration), + Time(i64), Timeuuid(Uuid), Tuple(Vec>), Uuid(Uuid), @@ -123,31 +124,57 @@ impl CqlValue { } } - pub fn as_date(&self) -> Option { - // Days since -5877641-06-23 i.e. 2^31 days before unix epoch - let date_days: u32 = match self { - CqlValue::Date(days) => *days, - _ => return None, - }; + pub fn as_cql_date(&self) -> Option { + match self { + Self::Date(d) => Some(CqlDate(*d)), + _ => None, + } + } + + #[cfg(feature = "chrono")] + pub fn as_naive_date(&self) -> Option { + self.as_cql_date().and_then(|date| date.try_into().ok()) + } + + #[cfg(feature = "time")] + pub fn as_date(&self) -> Option { + self.as_cql_date().and_then(|date| date.try_into().ok()) + } - // date_days is u32 then converted to i64 - // then we substract 2^31 - this can't panic - let days_since_epoch = - chrono::Duration::days(date_days.into()) - chrono::Duration::days(1 << 31); + pub fn as_cql_timestamp(&self) -> Option { + match self { + Self::Timestamp(i) => Some(CqlTimestamp(*i)), + _ => None, + } + } - NaiveDate::from_ymd_opt(1970, 1, 1) - .unwrap() - .checked_add_signed(days_since_epoch) + #[cfg(feature = "chrono")] + pub fn as_datetime(&self) -> Option> { + self.as_cql_timestamp().and_then(|ts| ts.try_into().ok()) } - pub fn as_duration(&self) -> Option { + #[cfg(feature = "time")] + pub fn as_offset_date_time(&self) -> Option { + self.as_cql_timestamp().and_then(|ts| ts.try_into().ok()) + } + + pub fn as_cql_time(&self) -> Option { match self { - Self::Timestamp(i) => Some(*i), - Self::Time(i) => Some(*i), + Self::Time(i) => Some(CqlTime(*i)), _ => None, } } + #[cfg(feature = "chrono")] + pub fn as_naive_time(&self) -> Option { + self.as_cql_time().and_then(|ts| ts.try_into().ok()) + } + + #[cfg(feature = "time")] + pub fn as_time(&self) -> Option { + self.as_cql_time().and_then(|ts| ts.try_into().ok()) + } + pub fn as_cql_duration(&self) -> Option { match self { Self::Duration(i) => Some(*i), @@ -201,7 +228,7 @@ impl CqlValue { pub fn as_bigint(&self) -> Option { match self { Self::BigInt(i) => Some(*i), - Self::Timestamp(d) => Some(d.num_milliseconds()), + Self::Timestamp(d) => Some(*d), _ => None, } } @@ -691,7 +718,7 @@ pub fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult()?; - CqlValue::Timestamp(chrono::Duration::milliseconds(millis)) + CqlValue::Timestamp(millis) } Time => { if buf.len() != 8 { @@ -709,7 +736,7 @@ pub fn deser_cql_value(typ: &ColumnType, buf: &mut &[u8]) -> StdResult { if buf.len() != 16 { @@ -915,10 +942,8 @@ pub fn deserialize(buf: &mut &[u8]) -> StdResult { #[cfg(test)] mod tests { use crate as scylla; - use crate::frame::value::{Counter, CqlDuration}; + use crate::frame::value::{Counter, CqlDate, CqlDuration}; use bigdecimal::BigDecimal; - use chrono::Duration; - use chrono::NaiveDate; use num_bigint::BigInt; use num_bigint::ToBigInt; use scylla::frame::response::result::{ColumnType, CqlValue}; @@ -1212,7 +1237,7 @@ mod tests { } #[test] - fn date_deserialize() { + fn test_deserialize_date() { // Date is correctly parsed from a 4 byte array let four_bytes: [u8; 4] = [12, 23, 34, 45]; let date: CqlValue = @@ -1230,55 +1255,147 @@ mod tests { super::deser_cql_value(&ColumnType::Date, &mut [1, 2, 3].as_ref()).unwrap_err(); super::deser_cql_value(&ColumnType::Date, &mut [1, 2, 3, 4, 5].as_ref()).unwrap_err(); + // Deserialize unix epoch + let unix_epoch_bytes = 2_u32.pow(31).to_be_bytes(); + + let date = + super::deser_cql_value(&ColumnType::Date, &mut unix_epoch_bytes.as_ref()).unwrap(); + assert_eq!(date.as_cql_date(), Some(CqlDate(1 << 31))); + + // 2^31 - 30 when converted to NaiveDate is 1969-12-02 + let before_epoch = CqlDate((1 << 31) - 30); + let date: CqlValue = super::deser_cql_value( + &ColumnType::Date, + &mut ((1_u32 << 31) - 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_cql_date(), Some(before_epoch)); + + // 2^31 + 30 when converted to NaiveDate is 1970-01-31 + let after_epoch = CqlDate((1 << 31) + 30); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1_u32 << 31) + 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_cql_date(), Some(after_epoch)); + + // Min date + let min_date = CqlDate(u32::MIN); + let date = super::deser_cql_value(&ColumnType::Date, &mut u32::MIN.to_be_bytes().as_ref()) + .unwrap(); + assert_eq!(date.as_cql_date(), Some(min_date)); + + // Max date + let max_date = CqlDate(u32::MAX); + let date = super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) + .unwrap(); + assert_eq!(date.as_cql_date(), Some(max_date)); + } + + #[cfg(feature = "chrono")] + #[test] + fn test_naive_date_from_cql() { + use chrono::NaiveDate; + // 2^31 when converted to NaiveDate is 1970-01-01 - let unix_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); - let date: CqlValue = - super::deser_cql_value(&ColumnType::Date, &mut 2_u32.pow(31).to_be_bytes().as_ref()) + 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_date().unwrap(), unix_epoch); + assert_eq!(date.as_naive_date(), Some(unix_epoch)); // 2^31 - 30 when converted to NaiveDate is 1969-12-02 - let before_epoch: NaiveDate = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); - let date: CqlValue = super::deser_cql_value( + let before_epoch = NaiveDate::from_ymd_opt(1969, 12, 2).unwrap(); + let date = super::deser_cql_value( &ColumnType::Date, - &mut (2_u32.pow(31) - 30).to_be_bytes().as_ref(), + &mut ((1u32 << 31) - 30).to_be_bytes().as_ref(), ) .unwrap(); - assert_eq!(date.as_date().unwrap(), before_epoch); + assert_eq!(date.as_naive_date(), Some(before_epoch)); // 2^31 + 30 when converted to NaiveDate is 1970-01-31 - let after_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); - let date: CqlValue = super::deser_cql_value( + let after_epoch = NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(); + let date = super::deser_cql_value( &ColumnType::Date, - &mut (2_u32.pow(31) + 30).to_be_bytes().as_ref(), + &mut ((1u32 << 31) + 30).to_be_bytes().as_ref(), ) .unwrap(); - assert_eq!(date.as_date().unwrap(), after_epoch); + assert_eq!(date.as_naive_date(), Some(after_epoch)); - // 0 and u32::MAX is out of NaiveDate range, fails with an error, not panics - assert!( + // 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_date() - .is_none() + .as_naive_date(), + None ); - assert!( + assert_eq!( super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) .unwrap() - .as_date() - .is_none() + .as_naive_date(), + None ); + } + + #[cfg(feature = "time")] + #[test] + fn test_date_from_cql() { + use time::Date; + use time::Month::*; + + // 2^31 when converted to time::Date is 1970-01-01 + let unix_epoch = Date::from_calendar_date(1970, January, 1).unwrap(); + let date = + super::deser_cql_value(&ColumnType::Date, &mut (1u32 << 31).to_be_bytes().as_ref()) + .unwrap(); - // It's hard to test NaiveDate more because it involves calculating days between calendar dates - // There are more tests using database queries that should cover it + assert_eq!(date.as_date(), Some(unix_epoch)); + + // 2^31 - 30 when converted to time::Date is 1969-12-02 + let before_epoch = Date::from_calendar_date(1969, December, 2).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) - 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_date(), Some(before_epoch)); + + // 2^31 + 30 when converted to time::Date is 1970-01-31 + let after_epoch = Date::from_calendar_date(1970, January, 31).unwrap(); + let date = super::deser_cql_value( + &ColumnType::Date, + &mut ((1u32 << 31) + 30).to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_date(), 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_date(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Date, &mut u32::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_date(), + None + ); } #[test] - fn test_time_deserialize() { + fn test_deserialize_time() { // Time is an i64 - nanoseconds since midnight // in range 0..=86399999999999 @@ -1290,7 +1407,7 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); let cql_value: CqlValue = super::deser_cql_value(&ColumnType::Time, &mut &bytes[..]).unwrap(); - assert_eq!(cql_value, CqlValue::Time(Duration::nanoseconds(*test_val))); + assert_eq!(cql_value, CqlValue::Time(*test_val)); } // Negative values cause an error @@ -1299,19 +1416,85 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); super::deser_cql_value(&ColumnType::Time, &mut &bytes[..]).unwrap_err(); } + } - // chrono::Duration has enough precision to represent nanoseconds accurately - assert_eq!(Duration::nanoseconds(1).num_nanoseconds().unwrap(), 1); - assert_eq!( - Duration::nanoseconds(7364737473).num_nanoseconds().unwrap(), - 7364737473 - ); - assert_eq!( - Duration::nanoseconds(86399999999999) - .num_nanoseconds() - .unwrap(), - 86399999999999 - ); + #[cfg(feature = "chrono")] + #[test] + fn test_naive_time_from_cql() { + use chrono::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(), 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(), 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(), Some(midnight)); + } + + #[cfg(feature = "time")] + #[test] + fn test_primitive_time_from_cql() { + use time::Time; + + // 0 when converted to NaiveTime is 0:0:0.0 + let midnight = Time::from_hms_nano(0, 0, 0, 0).unwrap(); + let time = + super::deser_cql_value(&ColumnType::Time, &mut (0i64).to_be_bytes().as_ref()).unwrap(); + + dbg!(&time); + assert_eq!(time.as_time(), Some(midnight)); + + // 10:10:30.500,000,001 + let (h, m, s, n) = (10, 10, 30, 500_000_001); + let midnight = Time::from_hms_nano(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_time(), Some(midnight)); + + // 23:59:59.999,999,999 + let (h, m, s, n) = (23, 59, 59, 999_999_999); + let midnight = Time::from_hms_nano(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_time(), Some(midnight)); } #[test] @@ -1323,19 +1506,126 @@ mod tests { let bytes: [u8; 8] = test_val.to_be_bytes(); let cql_value: CqlValue = super::deser_cql_value(&ColumnType::Timestamp, &mut &bytes[..]).unwrap(); - assert_eq!( - cql_value, - CqlValue::Timestamp(Duration::milliseconds(*test_val)) - ); - - // Check that Duration converted back to i64 is correct - assert_eq!( - Duration::milliseconds(*test_val).num_milliseconds(), - *test_val - ); + assert_eq!(cql_value, CqlValue::Timestamp(*test_val)); } } + #[cfg(feature = "chrono")] + #[test] + fn test_datetime_from_cql() { + use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + + // 0 when converted to DateTime is 1970-01-01 0:00:00.00 + let unix_epoch = NaiveDateTime::from_timestamp_opt(0, 0).unwrap().and_utc(); + let date = super::deser_cql_value(&ColumnType::Timestamp, &mut 0i64.to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_datetime(), 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 = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1969, 12, 1).unwrap(), + NaiveTime::from_hms_milli_opt(11, 29, 29, 500).unwrap(), + ) + .and_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime(), 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 = NaiveDateTime::new( + NaiveDate::from_ymd_opt(1970, 1, 31).unwrap(), + NaiveTime::from_hms_milli_opt(12, 30, 30, 500).unwrap(), + ) + .and_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_datetime(), 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(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_datetime(), + None + ); + } + + #[cfg(feature = "time")] + #[test] + fn test_offset_datetime_from_cql() { + use time::{Date, Month::*, OffsetDateTime, PrimitiveDateTime, Time}; + + // 0 when converted to OffsetDateTime is 1970-01-01 0:00:00.00 + let unix_epoch = OffsetDateTime::from_unix_timestamp(0).unwrap(); + let date = super::deser_cql_value(&ColumnType::Timestamp, &mut 0i64.to_be_bytes().as_ref()) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), 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 = PrimitiveDateTime::new( + Date::from_calendar_date(1969, December, 1).unwrap(), + Time::from_hms_milli(11, 29, 29, 500).unwrap(), + ) + .assume_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), 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 = PrimitiveDateTime::new( + Date::from_calendar_date(1970, January, 31).unwrap(), + Time::from_hms_milli(12, 30, 30, 500).unwrap(), + ) + .assume_utc(); + let date = super::deser_cql_value( + &ColumnType::Timestamp, + &mut timestamp.to_be_bytes().as_ref(), + ) + .unwrap(); + + assert_eq!(date.as_offset_date_time(), 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_offset_date_time(), + None + ); + + assert_eq!( + super::deser_cql_value(&ColumnType::Timestamp, &mut i64::MAX.to_be_bytes().as_ref()) + .unwrap() + .as_offset_date_time(), + None + ); + } + #[test] fn test_serialize_empty() { use crate::frame::value::Value; diff --git a/scylla-cql/src/frame/value.rs b/scylla-cql/src/frame/value.rs index 9e927be3a3..811b08cf9f 100644 --- a/scylla-cql/src/frame/value.rs +++ b/scylla-cql/src/frame/value.rs @@ -2,8 +2,6 @@ use crate::frame::frame_errors::ParseError; use crate::frame::types; use bigdecimal::BigDecimal; use bytes::BufMut; -use chrono::prelude::*; -use chrono::Duration; use num_bigint::BigInt; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; @@ -12,6 +10,9 @@ use std::net::IpAddr; use thiserror::Error; use uuid::Uuid; +#[cfg(feature = "chrono")] +use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc}; + use super::response::result::CqlValue; use super::types::vint_encode; @@ -42,20 +43,209 @@ pub enum MaybeUnset { Set(V), } -/// Wrapper that allows to send dates outside of NaiveDate range (-262145-1-1 to 262143-12-31) -/// Days since -5877641-06-23 i.e. 2^31 days before unix epoch +/// Native CQL date representation that allows for a bigger range of dates (-262145-1-1 to 262143-12-31). +/// +/// Represented as number of days since -5877641-06-23 i.e. 2^31 days before unix epoch. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Date(pub u32); +pub struct CqlDate(pub u32); -/// Wrapper used to differentiate between Time and Timestamp as sending values -/// Milliseconds since unix epoch +/// Native CQL timestamp representation that allows full supported timestamp range. +/// +/// Represented as signed milliseconds since unix epoch. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Timestamp(pub Duration); +pub struct CqlTimestamp(pub i64); -/// Wrapper used to differentiate between Time and Timestamp as sending values -/// Nanoseconds since midnight +/// Native CQL time representation. +/// +/// Represented as nanoseconds since midnight. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Time(pub Duration); +pub struct CqlTime(pub i64); + +#[cfg(feature = "chrono")] +impl From for CqlDate { + fn from(value: NaiveDate) -> Self { + let unix_epoch = 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")] +impl TryInto for CqlDate { + type Error = ValueTooBig; + + 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::Duration::days(days_since_unix_epoch); + + NaiveDate::from_yo_opt(1970, 1) + .unwrap() + .checked_add_signed(duration_since_unix_epoch) + .ok_or(ValueTooBig) + } +} + +#[cfg(feature = "chrono")] +impl From> for CqlTimestamp { + fn from(value: DateTime) -> Self { + Self(value.timestamp_millis()) + } +} + +#[cfg(feature = "chrono")] +impl TryInto> for CqlTimestamp { + type Error = ValueTooBig; + + fn try_into(self) -> Result, Self::Error> { + match Utc.timestamp_millis_opt(self.0) { + chrono::LocalResult::Single(datetime) => Ok(datetime), + _ => Err(ValueTooBig), + } + } +} + +#[cfg(feature = "chrono")] +impl TryFrom for CqlTime { + type Error = ValueTooBig; + + fn try_from(value: NaiveTime) -> Result { + let nanos = value + .signed_duration_since(chrono::NaiveTime::MIN) + .num_nanoseconds() + .unwrap(); + + // Value can exceed max CQL time in case of leap second + if nanos <= 86399999999999 { + Ok(Self(nanos)) + } else { + Err(ValueTooBig) + } + } +} + +#[cfg(feature = "chrono")] +impl TryInto for CqlTime { + type Error = ValueTooBig; + + fn try_into(self) -> Result { + let secs = (self.0 / 1_000_000_000) + .try_into() + .map_err(|_| ValueTooBig)?; + let nanos = (self.0 % 1_000_000_000) + .try_into() + .map_err(|_| ValueTooBig)?; + NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos).ok_or(ValueTooBig) + } +} + +#[cfg(feature = "time")] +impl From for CqlDate { + fn from(value: time::Date) -> Self { + const JULIAN_DAY_OFFSET: i64 = + (1 << 31) - time::OffsetDateTime::UNIX_EPOCH.date().to_julian_day() as i64; + + // Statically assert that no possible value will ever overflow + const _: () = + assert!(time::Date::MAX.to_julian_day() as i64 + JULIAN_DAY_OFFSET < u32::MAX as i64); + const _: () = + assert!(time::Date::MIN.to_julian_day() as i64 + JULIAN_DAY_OFFSET > u32::MIN as i64); + + let days = value.to_julian_day() as i64 + JULIAN_DAY_OFFSET; + + Self(days as u32) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlDate { + type Error = ValueTooBig; + + fn try_into(self) -> Result { + const JULIAN_DAY_OFFSET: i64 = + (1 << 31) - time::OffsetDateTime::UNIX_EPOCH.date().to_julian_day() as i64; + + let julian_days = (self.0 as i64 - JULIAN_DAY_OFFSET) + .try_into() + .map_err(|_| ValueTooBig)?; + + time::Date::from_julian_day(julian_days).map_err(|_| ValueTooBig) + } +} + +#[cfg(feature = "time")] +impl From for CqlTimestamp { + fn from(value: time::OffsetDateTime) -> Self { + // Statically assert that no possible value will ever overflow. OffsetDateTime doesn't allow offset to overflow + // the UTC PrimitiveDateTime value value + const _: () = assert!( + time::PrimitiveDateTime::MAX + .assume_utc() + .unix_timestamp_nanos() + // Nanos to millis + / 1_000_000 + < i64::MAX as i128 + ); + const _: () = assert!( + time::PrimitiveDateTime::MIN + .assume_utc() + .unix_timestamp_nanos() + / 1_000_000 + > i64::MIN as i128 + ); + + // Edge cases were statically asserted above, checked math is not required + Self(value.unix_timestamp() * 1000 + value.millisecond() as i64) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlTimestamp { + type Error = ValueTooBig; + + fn try_into(self) -> Result { + time::OffsetDateTime::from_unix_timestamp_nanos(self.0 as i128 * 1_000_000) + .map_err(|_| ValueTooBig) + } +} + +#[cfg(feature = "time")] +impl From for CqlTime { + fn from(value: time::Time) -> Self { + let (h, m, s, n) = value.as_hms_nano(); + + // no need for checked arithmetic as all these types are guaranteed to fit in i64 without overflow + let nanos = (h as i64 * 3600 + m as i64 * 60 + s as i64) * 1_000_000_000 + n as i64; + + Self(nanos) + } +} + +#[cfg(feature = "time")] +impl TryInto for CqlTime { + type Error = ValueTooBig; + + fn try_into(self) -> Result { + let h = self.0 / 3_600_000_000_000; + let m = self.0 / 60_000_000_000 % 60; + let s = self.0 / 1_000_000_000 % 60; + let n = self.0 % 1_000_000_000; + + time::Time::from_hms_nano( + h.try_into().map_err(|_| ValueTooBig)?, + m as u8, + s as u8, + n as u32, + ) + .map_err(|_| ValueTooBig) + } +} /// Keeps a buffer with serialized Values /// Allows adding new Values and iterating over serialized ones @@ -384,24 +574,14 @@ impl Value for BigDecimal { } } +#[cfg(feature = "chrono")] impl Value for NaiveDate { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { - buf.put_i32(4); - let unix_epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); - - let days: u32 = self - .signed_duration_since(unix_epoch) - .num_days() - .checked_add(1 << 31) - .and_then(|days| days.try_into().ok()) // convert to u32 - .ok_or(ValueTooBig)?; - - buf.put_u32(days); - Ok(()) + CqlDate::from(*self).serialize(buf) } } -impl Value for Date { +impl Value for CqlDate { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(4); buf.put_u32(self.0); @@ -409,27 +589,54 @@ impl Value for Date { } } -impl Value for Timestamp { +#[cfg(feature = "time")] +impl Value for time::Date { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlDate::from(*self).serialize(buf) + } +} + +impl Value for CqlTimestamp { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(8); - buf.put_i64(self.0.num_milliseconds()); + buf.put_i64(self.0); Ok(()) } } -impl Value for Time { +impl Value for CqlTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { buf.put_i32(8); - buf.put_i64(self.0.num_nanoseconds().ok_or(ValueTooBig)?); + buf.put_i64(self.0); Ok(()) } } +#[cfg(feature = "chrono")] impl Value for DateTime { fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { - buf.put_i32(8); - buf.put_i64(self.timestamp_millis()); - Ok(()) + CqlTimestamp::from(*self).serialize(buf) + } +} + +#[cfg(feature = "time")] +impl Value for time::OffsetDateTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTimestamp::from(*self).serialize(buf) + } +} + +#[cfg(feature = "chrono")] +impl Value for NaiveTime { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTime::try_from(*self)?.serialize(buf) + } +} + +#[cfg(feature = "time")] +impl Value for time::Time { + fn serialize(&self, buf: &mut Vec) -> Result<(), ValueTooBig> { + CqlTime::from(*self).serialize(buf) } } @@ -733,10 +940,10 @@ impl Value for CqlValue { serialize_tuple(fields.iter().map(|(_, value)| value), buf) } - CqlValue::Date(d) => Date(*d).serialize(buf), + CqlValue::Date(d) => CqlDate(*d).serialize(buf), CqlValue::Duration(d) => d.serialize(buf), - CqlValue::Timestamp(t) => Timestamp(*t).serialize(buf), - CqlValue::Time(t) => Time(*t).serialize(buf), + CqlValue::Timestamp(t) => CqlTimestamp(*t).serialize(buf), + CqlValue::Time(t) => CqlTime(*t).serialize(buf), CqlValue::Ascii(s) | CqlValue::Text(s) => s.serialize(buf), CqlValue::List(v) | CqlValue::Set(v) => v.serialize(buf), diff --git a/scylla-cql/src/frame/value_tests.rs b/scylla-cql/src/frame/value_tests.rs index f7e480362f..88de8dcbed 100644 --- a/scylla-cql/src/frame/value_tests.rs +++ b/scylla-cql/src/frame/value_tests.rs @@ -1,11 +1,10 @@ use crate::frame::value::BatchValuesIterator; use super::value::{ - BatchValues, Date, MaybeUnset, SerializeValuesError, SerializedValues, Time, Timestamp, Unset, - Value, ValueList, ValueTooBig, + BatchValues, CqlDate, CqlTime, CqlTimestamp, MaybeUnset, SerializeValuesError, + SerializedValues, Unset, Value, ValueList, ValueTooBig, }; use bytes::BufMut; -use chrono::{Duration, NaiveDate}; use std::{borrow::Cow, convert::TryInto}; use uuid::Uuid; @@ -41,8 +40,19 @@ fn u8_slice_serialization() { assert_eq!(serialized(val.as_slice()), vec![0, 0, 0, 4, 1, 1, 1, 1]); } +#[test] +fn cql_date_serialization() { + assert_eq!(serialized(CqlDate(0)), vec![0, 0, 0, 4, 0, 0, 0, 0]); + assert_eq!( + serialized(CqlDate(u32::MAX)), + vec![0, 0, 0, 4, 255, 255, 255, 255] + ); +} + +#[cfg(feature = "chrono")] #[test] fn naive_date_serialization() { + use chrono::NaiveDate; // 1970-01-31 is 2^31 let unix_epoch: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); assert_eq!(serialized(unix_epoch), vec![0, 0, 0, 4, 128, 0, 0, 0]); @@ -62,18 +72,55 @@ fn naive_date_serialization() { assert_eq!((2_u32.pow(31) + 30).to_be_bytes(), [128, 0, 0, 30]); } +#[cfg(feature = "time")] #[test] fn date_serialization() { - assert_eq!(serialized(Date(0)), vec![0, 0, 0, 4, 0, 0, 0, 0]); + // 1970-01-31 is 2^31 + let unix_epoch = time::Date::from_ordinal_date(1970, 1).unwrap(); + assert_eq!(serialized(unix_epoch), 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 = time::Date::from_calendar_date(1969, time::Month::December, 2).unwrap(); assert_eq!( - serialized(Date(u32::MAX)), - vec![0, 0, 0, 4, 255, 255, 255, 255] + serialized(before_epoch), + 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 = time::Date::from_calendar_date(1970, time::Month::January, 31).unwrap(); + assert_eq!(serialized(after_epoch), vec![0, 0, 0, 4, 128, 0, 0, 30]); + assert_eq!((2_u32.pow(31) + 30).to_be_bytes(), [128, 0, 0, 30]); + + // Min date represented by time::Date (without large-dates feature) + let long_before_epoch = time::Date::from_calendar_date(-9999, time::Month::January, 1).unwrap(); + let days_till_epoch = (unix_epoch - long_before_epoch).whole_days(); + assert_eq!( + (2_u32.pow(31) - days_till_epoch as u32).to_be_bytes(), + [127, 189, 75, 125] + ); + assert_eq!( + serialized(long_before_epoch), + vec![0, 0, 0, 4, 127, 189, 75, 125] + ); + + // Max date represented by time::Date (without large-dates feature) + let long_after_epoch = time::Date::from_calendar_date(9999, time::Month::December, 31).unwrap(); + let days_since_epoch = (long_after_epoch - unix_epoch).whole_days(); + assert_eq!( + (2_u32.pow(31) + days_since_epoch as u32).to_be_bytes(), + [128, 44, 192, 160] + ); + assert_eq!( + serialized(long_after_epoch), + vec![0, 0, 0, 4, 128, 44, 192, 160] ); } #[test] -fn time_serialization() { - // Time is an i64 - nanoseconds since midnight +fn cql_time_serialization() { + // CqlTime is an i64 - nanoseconds since midnight // in range 0..=86399999999999 let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; @@ -81,8 +128,8 @@ fn time_serialization() { // Check that basic values are serialized correctly // Invalid values are also serialized correctly - database will respond with an error - for test_val in [0, 1, 15, 18463, max_time, -1, -324234, max_time + 16].iter() { - let test_time: Time = Time(Duration::nanoseconds(*test_val)); + for test_val in [0, 1, 15, 18463, max_time, -1, -324234, max_time + 16].into_iter() { + let test_time: CqlTime = CqlTime(test_val); let bytes: Vec = serialized(test_time); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; @@ -91,18 +138,75 @@ fn time_serialization() { assert_eq!(bytes, expected_bytes); assert_eq!(expected_bytes.len(), 12); } +} + +#[cfg(feature = "chrono")] +#[test] +fn naive_time_serialization() { + use chrono::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::MIN, 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); + + 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!(leap_second.serialize(&mut buffer), Err(ValueTooBig)) +} + +#[cfg(feature = "time")] +#[test] +fn time_serialization() { + 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 = [ + (time::Time::MIDNIGHT, midnight_time.to_be_bytes()), + ( + time::Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap(), + max_time.to_be_bytes(), + ), + ( + time::Time::from_hms_nano(1, 2, 3, 4).unwrap(), + any_time.to_be_bytes(), + ), + ]; + for (time, expected) in test_cases { + let bytes = serialized(time); + + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); - // Durations so long that nanoseconds don't fit in i64 cause an error - let long_time = Time(Duration::milliseconds(i64::MAX)); - assert_eq!(long_time.serialize(&mut Vec::new()), Err(ValueTooBig)); + assert_eq!(bytes, expected_bytes) + } } #[test] -fn timestamp_serialization() { - // Timestamp is milliseconds since unix epoch represented as i64 +fn cql_timestamp_serialization() { + // CqlTimestamp is milliseconds since unix epoch represented as i64 for test_val in &[0, -1, 1, -45345346, 453451, i64::MIN, i64::MAX] { - let test_timestamp: Timestamp = Timestamp(Duration::milliseconds(*test_val)); + let test_timestamp: CqlTimestamp = CqlTimestamp(*test_val); let bytes: Vec = serialized(test_timestamp); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; @@ -113,23 +217,115 @@ fn timestamp_serialization() { } } +#[cfg(feature = "chrono")] #[test] -fn datetime_serialization() { - use chrono::{DateTime, NaiveDateTime, Utc}; - // Datetime is milliseconds since unix epoch represented as i64 - let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; - - for test_val in &[0, 1, 15, 18463, max_time, max_time + 16] { - let native_datetime = NaiveDateTime::from_timestamp_opt( - *test_val / 1000, - ((*test_val % 1000) as i32 * 1_000_000) as u32, - ) - .expect("invalid or out-of-range datetime"); - let test_datetime = DateTime::::from_utc(native_datetime, Utc); +fn naive_date_time_serialization() { + use chrono::NaiveDateTime; + let test_cases = [ + ( + // Max time serialized without error + NaiveDateTime::MAX, + NaiveDateTime::MAX.timestamp_millis().to_be_bytes(), + ), + ( + // Min time serialized without error + NaiveDateTime::MIN, + NaiveDateTime::MIN.timestamp_millis().to_be_bytes(), + ), + ( + // UNIX epoch baseline + NaiveDateTime::from_timestamp_opt(0, 0).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // One second since UNIX epoch + NaiveDateTime::from_timestamp_opt(1, 0).unwrap(), + 1000i64.to_be_bytes(), + ), + ( + // 1 nanosecond since UNIX epoch, lost during serialization + NaiveDateTime::from_timestamp_opt(0, 1).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // 1 millisecond since UNIX epoch + NaiveDateTime::from_timestamp_opt(0, 1_000_000).unwrap(), + 1i64.to_be_bytes(), + ), + ( + // 2 days before UNIX epoch + NaiveDateTime::from_timestamp_opt(-2 * 24 * 60 * 60, 0).unwrap(), + (-2 * 24i64 * 60 * 60 * 1000).to_be_bytes(), + ), + ]; + for (datetime, expected) in test_cases { + let test_datetime = datetime.and_utc(); let bytes: Vec = serialized(test_datetime); let mut expected_bytes: Vec = vec![0, 0, 0, 8]; - expected_bytes.extend_from_slice(&test_val.to_be_bytes()); + expected_bytes.extend_from_slice(&expected); + + assert_eq!(bytes, expected_bytes); + assert_eq!(expected_bytes.len(), 12); + } +} + +#[cfg(feature = "time")] +#[test] +fn offset_date_time_serialization() { + use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; + let offset_max = + PrimitiveDateTime::MAX.assume_offset(time::UtcOffset::from_hms(-23, -59, -59).unwrap()); + let offset_min = + PrimitiveDateTime::MIN.assume_offset(time::UtcOffset::from_hms(23, 59, 59).unwrap()); + let test_cases = [ + ( + // Max time serialized without error + offset_max, + (offset_max.unix_timestamp() * 1000 + offset_max.nanosecond() as i64 / 1_000_000) + .to_be_bytes(), + ), + ( + // Min time serialized without error + offset_min, + (offset_min.unix_timestamp() * 1000 + offset_min.nanosecond() as i64 / 1_000_000) + .to_be_bytes(), + ), + ( + // UNIX epoch baseline + OffsetDateTime::from_unix_timestamp(0).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // One second since UNIX epoch + OffsetDateTime::from_unix_timestamp(1).unwrap(), + 1000i64.to_be_bytes(), + ), + ( + // 1 nanosecond since UNIX epoch, lost during serialization + OffsetDateTime::from_unix_timestamp_nanos(1).unwrap(), + 0i64.to_be_bytes(), + ), + ( + // 1 millisecond since UNIX epoch + OffsetDateTime::from_unix_timestamp_nanos(1_000_000).unwrap(), + 1i64.to_be_bytes(), + ), + ( + // 2 days before UNIX epoch + PrimitiveDateTime::new( + Date::from_calendar_date(1969, Month::December, 30).unwrap(), + Time::MIDNIGHT, + ) + .assume_utc(), + (-2 * 24i64 * 60 * 60 * 1000).to_be_bytes(), + ), + ]; + for (datetime, expected) in test_cases { + let bytes: Vec = serialized(datetime); + + let mut expected_bytes: Vec = vec![0, 0, 0, 8]; + expected_bytes.extend_from_slice(&expected); assert_eq!(bytes, expected_bytes); assert_eq!(expected_bytes.len(), 12); diff --git a/scylla/Cargo.toml b/scylla/Cargo.toml index f83bdbd240..963fef6aef 100644 --- a/scylla/Cargo.toml +++ b/scylla/Cargo.toml @@ -18,6 +18,8 @@ default = [] ssl = ["dep:tokio-openssl", "dep:openssl"] cloud = ["ssl", "scylla-cql/serde", "dep:serde_yaml", "dep:serde", "dep:url", "dep:base64"] secret = ["scylla-cql/secret"] +chrono = ["scylla-cql/chrono"] +time = ["scylla-cql/time"] [dependencies] scylla-macros = { version = "0.2.0", path = "../scylla-macros" } @@ -60,6 +62,7 @@ criterion = "0.5" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } assert_matches = "1.5.0" rand_chacha = "0.3.1" +time = "0.3" [[bench]] name = "benchmark" diff --git a/scylla/src/tracing.rs b/scylla/src/tracing.rs index 3d40e36431..7753e4bd91 100644 --- a/scylla/src/tracing.rs +++ b/scylla/src/tracing.rs @@ -5,6 +5,7 @@ use uuid::Uuid; use crate::cql_to_rust::{FromRow, FromRowError}; use crate::frame::response::result::Row; +use crate::frame::value::CqlTimestamp; /// Tracing info retrieved from `system_traces.sessions` /// with all events from `system_traces.events` @@ -17,7 +18,7 @@ pub struct TracingInfo { pub parameters: Option>, pub request: Option, /// started_at is a timestamp - time since unix epoch - pub started_at: Option, + pub started_at: Option, pub events: Vec, } @@ -64,7 +65,7 @@ impl FromRow for TracingInfo { Option, Option>, Option, - Option, + Option, )>::from_row(row)?; Ok(TracingInfo { diff --git a/scylla/src/transport/cql_types_test.rs b/scylla/src/transport/cql_types_test.rs index dbc4cf2537..2a72b25958 100644 --- a/scylla/src/transport/cql_types_test.rs +++ b/scylla/src/transport/cql_types_test.rs @@ -1,16 +1,13 @@ use crate as scylla; use crate::cql_to_rust::FromCqlVal; use crate::frame::response::result::CqlValue; -use crate::frame::value::Counter; -use crate::frame::value::Value; -use crate::frame::value::{Date, Time, Timestamp}; +use crate::frame::value::{Counter, CqlDate, CqlTime, CqlTimestamp, Value}; use crate::macros::{FromUserType, IntoUserType}; use crate::test_utils::create_new_session_builder; use crate::transport::session::IntoTypedRows; use crate::transport::session::Session; use crate::utils::test_utils::unique_keyspace_name; use bigdecimal::BigDecimal; -use chrono::{Duration, NaiveDate}; use num_bigint::BigInt; use std::cmp::PartialEq; use std::fmt::Debug; @@ -194,9 +191,10 @@ async fn test_counter() { } } +#[cfg(features = "chrono")] #[tokio::test] async fn test_naive_date() { - let session: Session = init_test("naive_date", "date").await; + let session: Session = init_test("chrono_naive_date_tests", "date").await; let min_naive_date: NaiveDate = NaiveDate::MIN; assert_eq!( @@ -214,7 +212,7 @@ async fn test_naive_date() { // Basic test values ( "0000-01-01", - Some(NaiveDate::from_ymd_opt(0000, 1, 1).unwrap()), + Some(NaiveDate::from_ymd_opt(0, 1, 1).unwrap()), ), ( "1970-01-01", @@ -250,7 +248,7 @@ async fn test_naive_date() { session .query( format!( - "INSERT INTO naive_date (id, val) VALUES (0, '{}')", + "INSERT INTO chrono_naive_date_tests (id, val) VALUES (0, '{}')", date_text ), &[], @@ -259,7 +257,7 @@ async fn test_naive_date() { .unwrap(); let read_date: Option = session - .query("SELECT val from naive_date", &[]) + .query("SELECT val from chrono_naive_date_tests", &[]) .await .unwrap() .rows @@ -276,14 +274,14 @@ async fn test_naive_date() { if let Some(naive_date) = date { session .query( - "INSERT INTO naive_date (id, val) VALUES (0, ?)", + "INSERT INTO chrono_naive_date_tests (id, val) VALUES (0, ?)", (naive_date,), ) .await .unwrap(); let (read_date,): (NaiveDate,) = session - .query("SELECT val from naive_date", &[]) + .query("SELECT val from chrono_naive_date_tests", &[]) .await .unwrap() .rows @@ -295,11 +293,53 @@ async fn test_naive_date() { assert_eq!(read_date, *naive_date); } } +} + +#[tokio::test] +async fn test_cql_date() { + // Tests value::Date which allows to insert dates outside NaiveDate range + + let session: Session = init_test("cql_date_tests", "date").await; + + let tests = [ + ("1970-01-01", CqlDate(2_u32.pow(31))), + ("1969-12-02", CqlDate(2_u32.pow(31) - 30)), + ("1970-01-31", CqlDate(2_u32.pow(31) + 30)), + ("-5877641-06-23", CqlDate(0)), + // NOTICE: dropped for Cassandra 4 compatibility + //("5881580-07-11", Date(u32::MAX)), + ]; + + for (date_text, date) in &tests { + session + .query( + format!( + "INSERT INTO cql_date_tests (id, val) VALUES (0, '{}')", + date_text + ), + &[], + ) + .await + .unwrap(); + + let read_date: CqlDate = session + .query("SELECT val from cql_date_tests", &[]) + .await + .unwrap() + .rows + .unwrap()[0] + .columns[0] + .as_ref() + .map(|cql_val| cql_val.as_cql_date().unwrap()) + .unwrap(); + + assert_eq!(read_date, *date); + } // 1 less/more than min/max values allowed by the database should cause error session .query( - "INSERT INTO naive_date (id, val) VALUES (0, '-5877641-06-22')", + "INSERT INTO cql_date_tests (id, val) VALUES (0, '-5877641-06-22')", &[], ) .await @@ -307,33 +347,60 @@ async fn test_naive_date() { session .query( - "INSERT INTO naive_date (id, val) VALUES (0, '5881580-07-12')", + "INSERT INTO cql_date_tests (id, val) VALUES (0, '5881580-07-12')", &[], ) .await .unwrap_err(); } +#[cfg(features = "time")] #[tokio::test] async fn test_date() { - // Tests value::Date which allows to insert dates outside NaiveDate range + use time::{Date, Month::*}; - let session: Session = init_test("date_tests", "date").await; + let session: Session = init_test("time_date_tests", "date").await; let tests = [ - ("1970-01-01", Date(2_u32.pow(31))), - ("1969-12-02", Date(2_u32.pow(31) - 30)), - ("1970-01-31", Date(2_u32.pow(31) + 30)), - ("-5877641-06-23", Date(0)), - // NOTICE: dropped for Cassandra 4 compatibility - //("5881580-07-11", Date(u32::MAX)), + // Basic test values + ( + "0000-01-01", + Some(Date::from_calendar_date(0, January, 1).unwrap()), + ), + ( + "1970-01-01", + Some(Date::from_calendar_date(1970, January, 1).unwrap()), + ), + ( + "2020-03-07", + Some(Date::from_calendar_date(2020, March, 7).unwrap()), + ), + ( + "1337-04-05", + Some(Date::from_calendar_date(1337, April, 5).unwrap()), + ), + ( + "-0001-12-31", + Some(Date::from_calendar_date(-1, December, 31).unwrap()), + ), + // min/max values allowed by time::Date depend on feature flags, but following values must always be allowed + ( + "9999-12-31", + Some(Date::from_calendar_date(9999, December, 31).unwrap()), + ), + ( + "-9999-01-01", + Some(Date::from_calendar_date(-9999, January, 1).unwrap()), + ), + // min value allowed by the database + ("-5877641-06-23", None), ]; - for (date_text, date) in &tests { + for (date_text, date) in tests.iter() { session .query( format!( - "INSERT INTO date_tests (id, val) VALUES (0, '{}')", + "INSERT INTO time_date_tests (id, val) VALUES (0, '{}')", date_text ), &[], @@ -341,40 +408,52 @@ async fn test_date() { .await .unwrap(); - let read_date: Date = session - .query("SELECT val from date_tests", &[]) + let (read_date,) = session + .query("SELECT val from time_date_tests", &[]) .await .unwrap() - .rows - .unwrap()[0] - .columns[0] - .as_ref() - .map(|cql_val| match cql_val { - CqlValue::Date(days) => Date(*days), - _ => panic!(), - }) + .first_row_typed::<(Date,)>() .unwrap(); assert_eq!(read_date, *date); + + // If date is representable by time::Date try inserting it and reading again + if let Some(date) = date { + session + .query( + "INSERT INTO time_date_tests (id, val) VALUES (0, ?)", + (date,), + ) + .await + .unwrap(); + + let (read_date,) = session + .query("SELECT val from time_date_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Date,)>() + .unwrap(); + assert_eq!(read_date, *date); + } } } #[tokio::test] -async fn test_time() { - // Time is an i64 - nanoseconds since midnight +async fn test_cql_time() { + // CqlTime is an i64 - nanoseconds since midnight // in range 0..=86399999999999 - let session: Session = init_test("time_tests", "time").await; + let session: Session = init_test("cql_time_tests", "time").await; let max_time: i64 = 24 * 60 * 60 * 1_000_000_000 - 1; assert_eq!(max_time, 86399999999999); let tests = [ - ("00:00:00", Duration::nanoseconds(0)), - ("01:01:01", Duration::seconds(60 * 60 + 60 + 1)), - ("00:00:00.000000000", Duration::nanoseconds(0)), - ("00:00:00.000000001", Duration::nanoseconds(1)), - ("23:59:59.999999999", Duration::nanoseconds(max_time)), + ("00:00:00", CqlTime(0)), + ("01:01:01", CqlTime((60 * 60 + 60 + 1) * 1_000_000_000)), + ("00:00:00.000000000", CqlTime(0)), + ("00:00:00.000000001", CqlTime(1)), + ("23:59:59.999999999", CqlTime(max_time)), ]; for (time_str, time_duration) in &tests { @@ -382,7 +461,7 @@ async fn test_time() { session .query( format!( - "INSERT INTO time_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_time_tests (id, val) VALUES (0, '{}')", time_str ), &[], @@ -390,35 +469,35 @@ async fn test_time() { .await .unwrap(); - let (read_time,): (Duration,) = session - .query("SELECT val from time_tests", &[]) + let (read_time,) = session + .query("SELECT val from cql_time_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTime,)>() .next() .unwrap() .unwrap(); assert_eq!(read_time, *time_duration); - // Insert time as a bound Time value and verify that it matches + // Insert time as a bound CqlTime value and verify that it matches session .query( - "INSERT INTO time_tests (id, val) VALUES (0, ?)", - (Time(*time_duration),), + "INSERT INTO cql_time_tests (id, val) VALUES (0, ?)", + (*time_duration,), ) .await .unwrap(); - let (read_time,): (Duration,) = session - .query("SELECT val from time_tests", &[]) + let (read_time,) = session + .query("SELECT val from cql_time_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTime,)>() .next() .unwrap() .unwrap(); @@ -442,7 +521,7 @@ async fn test_time() { session .query( format!( - "INSERT INTO time_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_time_tests (id, val) VALUES (0, '{}')", time_str ), &[], @@ -452,9 +531,157 @@ async fn test_time() { } } +#[cfg(feature = "chrono")] +#[tokio::test] +async fn test_naive_time() { + use chrono::NaiveTime; + + let session = init_test("chrono_time_tests", "time").await; + + let tests = [ + ("00:00:00", NaiveTime::MIN), + ("01:01:01", NaiveTime::from_hms_opt(1, 1, 1).unwrap()), + ( + "00:00:00.000000000", + NaiveTime::from_hms_nano_opt(0, 0, 0, 0).unwrap(), + ), + ( + "00:00:00.000000001", + NaiveTime::from_hms_nano_opt(0, 0, 0, 1).unwrap(), + ), + ( + "12:34:56.789012345", + NaiveTime::from_hms_nano_opt(12, 34, 56, 789_012_345).unwrap(), + ), + ( + "23:59:59.999999999", + NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap(), + ), + ]; + + for (time_text, time) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO chrono_time_tests (id, val) VALUES (0, '{}')", + time_text + ), + &[], + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from chrono_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(NaiveTime,)>() + .unwrap(); + + assert_eq!(read_time, *time); + + // Insert as type and read it again + session + .query( + "INSERT INTO chrono_time_tests (id, val) VALUES (0, ?)", + (time,), + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from chrono_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(NaiveTime,)>() + .unwrap(); + assert_eq!(read_time, *time); + } + + // chrono can represent leap seconds, this should not panic + let leap_second = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_500_000_000); + session + .query( + "INSERT INTO cql_time_tests (id, val) VALUES (0, ?)", + (leap_second,), + ) + .await + .unwrap_err(); +} + +#[cfg(feature = "time")] +#[tokio::test] +async fn test_time() { + use time::Time; + + let session = init_test("time_time_tests", "time").await; + + let tests = [ + ("00:00:00", Time::MIDNIGHT), + ("01:01:01", Time::from_hms(1, 1, 1).unwrap()), + ( + "00:00:00.000000000", + Time::from_hms_nano(0, 0, 0, 0).unwrap(), + ), + ( + "00:00:00.000000001", + Time::from_hms_nano(0, 0, 0, 1).unwrap(), + ), + ( + "12:34:56.789012345", + Time::from_hms_nano(12, 34, 56, 789_012_345).unwrap(), + ), + ( + "23:59:59.999999999", + Time::from_hms_nano(23, 59, 59, 999_999_999).unwrap(), + ), + ]; + + for (time_text, time) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO time_time_tests (id, val) VALUES (0, '{}')", + time_text + ), + &[], + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from time_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Time,)>() + .unwrap(); + + assert_eq!(read_time, *time); + + // Insert as type and read it again + session + .query( + "INSERT INTO time_time_tests (id, val) VALUES (0, ?)", + (time,), + ) + .await + .unwrap(); + + let (read_time,) = session + .query("SELECT val from time_time_tests", &[]) + .await + .unwrap() + .first_row_typed::<(Time,)>() + .unwrap(); + assert_eq!(read_time, *time); + } +} + #[tokio::test] -async fn test_timestamp() { - let session: Session = init_test("timestamp_tests", "timestamp").await; +async fn test_cql_timestamp() { + let session: Session = init_test("cql_timestamp_tests", "timestamp").await; //let epoch_date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(); @@ -465,9 +692,9 @@ async fn test_timestamp() { //let after_epoch_offset = after_epoch.signed_duration_since(epoch_date); let tests = [ - ("0", Duration::milliseconds(0)), - ("9223372036854775807", Duration::milliseconds(i64::MAX)), - ("-9223372036854775808", Duration::milliseconds(i64::MIN)), + ("0", CqlTimestamp(0)), + ("9223372036854775807", CqlTimestamp(i64::MAX)), + ("-9223372036854775808", CqlTimestamp(i64::MIN)), // NOTICE: dropped for Cassandra 4 compatibility //("1970-01-01", Duration::milliseconds(0)), //("2020-03-08", after_epoch_offset), @@ -486,7 +713,7 @@ async fn test_timestamp() { session .query( format!( - "INSERT INTO timestamp_tests (id, val) VALUES (0, '{}')", + "INSERT INTO cql_timestamp_tests (id, val) VALUES (0, '{}')", timestamp_str ), &[], @@ -494,35 +721,35 @@ async fn test_timestamp() { .await .unwrap(); - let (read_timestamp,): (Duration,) = session - .query("SELECT val from timestamp_tests", &[]) + let (read_timestamp,) = session + .query("SELECT val from cql_timestamp_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTimestamp,)>() .next() .unwrap() .unwrap(); assert_eq!(read_timestamp, *timestamp_duration); - // Insert timestamp as a bound Timestamp value and verify that it matches + // Insert timestamp as a bound CqlTimestamp value and verify that it matches session .query( - "INSERT INTO timestamp_tests (id, val) VALUES (0, ?)", - (Timestamp(*timestamp_duration),), + "INSERT INTO cql_timestamp_tests (id, val) VALUES (0, ?)", + (*timestamp_duration,), ) .await .unwrap(); - let (read_timestamp,): (Duration,) = session - .query("SELECT val from timestamp_tests", &[]) + let (read_timestamp,) = session + .query("SELECT val from cql_timestamp_tests", &[]) .await .unwrap() .rows .unwrap() - .into_typed::<(Duration,)>() + .into_typed::<(CqlTimestamp,)>() .next() .unwrap() .unwrap(); @@ -531,6 +758,315 @@ async fn test_timestamp() { } } +#[cfg(feature = "chrono")] +#[tokio::test] +async fn test_date_time() { + use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; + + let session = init_test("chrono_datetime_tests", "timestamp").await; + + let tests = [ + ( + "0", + NaiveDateTime::from_timestamp_opt(0, 0).unwrap().and_utc(), + ), + ( + "2001-02-03T04:05:06.789+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2001, 2, 3).unwrap(), + NaiveTime::from_hms_milli_opt(4, 5, 6, 789).unwrap(), + ) + .and_utc(), + ), + ( + "2011-02-03T04:05:00.000+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2011, 2, 3).unwrap(), + NaiveTime::from_hms_milli_opt(4, 5, 0, 0).unwrap(), + ) + .and_utc(), + ), + // New Zealand timezone, converted to GMT + ( + "2011-02-03T04:05:06.987+1245", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2011, 2, 2).unwrap(), + NaiveTime::from_hms_milli_opt(15, 20, 6, 987).unwrap(), + ) + .and_utc(), + ), + ( + "9999-12-31T23:59:59.999+0000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap(), + ) + .and_utc(), + ), + ( + "-377705116800000", + NaiveDateTime::new( + NaiveDate::from_ymd_opt(-9999, 1, 1).unwrap(), + NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(), + ) + .and_utc(), + ), + ]; + + for (datetime_text, datetime) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, '{}')", + datetime_text + ), + &[], + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + + assert_eq!(read_datetime, *datetime); + + // Insert as type and read it again + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (datetime,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, *datetime); + } + + // chrono datetime has higher precision, round excessive submillisecond time down + let nanosecond_precision_1st_half = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_nano_opt(23, 59, 59, 123_123_456).unwrap(), + ) + .and_utc(); + let nanosecond_precision_1st_half_rounded = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 123).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_1st_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_1st_half_rounded); + + let nanosecond_precision_2nd_half = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_nano_opt(23, 59, 59, 123_987_654).unwrap(), + ) + .and_utc(); + let nanosecond_precision_2nd_half_rounded = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 123).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO chrono_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_2nd_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from chrono_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(DateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_2nd_half_rounded); + + // chrono can represent leap seconds, this should not panic + let leap_second = NaiveDateTime::new( + NaiveDate::from_ymd_opt(2015, 6, 30).unwrap(), + NaiveTime::from_hms_milli_opt(23, 59, 59, 1500).unwrap(), + ) + .and_utc(); + session + .query( + "INSERT INTO cql_datetime_tests (id, val) VALUES (0, ?)", + (leap_second,), + ) + .await + .unwrap_err(); +} + +#[cfg(feature = "time")] +#[tokio::test] +async fn test_offset_date_time() { + use time::{Date, Month::*, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; + + let session = init_test("time_datetime_tests", "timestamp").await; + + let tests = [ + ("0", OffsetDateTime::UNIX_EPOCH), + ( + "2001-02-03T04:05:06.789+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(2001, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 6, 789).unwrap(), + ) + .assume_utc(), + ), + ( + "2011-02-03T04:05:00.000+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(2011, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 0, 0).unwrap(), + ) + .assume_utc(), + ), + // New Zealand timezone, converted to GMT + ( + "2011-02-03T04:05:06.987+1245", + PrimitiveDateTime::new( + Date::from_calendar_date(2011, February, 3).unwrap(), + Time::from_hms_milli(4, 5, 6, 987).unwrap(), + ) + .assume_offset(UtcOffset::from_hms(12, 45, 0).unwrap()), + ), + ( + "9999-12-31T23:59:59.999+0000", + PrimitiveDateTime::new( + Date::from_calendar_date(9999, December, 31).unwrap(), + Time::from_hms_milli(23, 59, 59, 999).unwrap(), + ) + .assume_utc(), + ), + ( + "-377705116800000", + PrimitiveDateTime::new( + Date::from_calendar_date(-9999, January, 1).unwrap(), + Time::from_hms_milli(0, 0, 0, 0).unwrap(), + ) + .assume_utc(), + ), + ]; + + for (datetime_text, datetime) in tests.iter() { + // Insert as string and read it again + session + .query( + format!( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, '{}')", + datetime_text + ), + &[], + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + + assert_eq!(read_datetime, *datetime); + + // Insert as type and read it again + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (datetime,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, *datetime); + } + + // time datetime has higher precision, round excessive submillisecond time down + let nanosecond_precision_1st_half = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_nano(23, 59, 59, 123_123_456).unwrap(), + ) + .assume_utc(); + let nanosecond_precision_1st_half_rounded = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_milli(23, 59, 59, 123).unwrap(), + ) + .assume_utc(); + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_1st_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_1st_half_rounded); + + let nanosecond_precision_2nd_half = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_nano(23, 59, 59, 123_987_654).unwrap(), + ) + .assume_utc(); + let nanosecond_precision_2nd_half_rounded = PrimitiveDateTime::new( + Date::from_calendar_date(2015, June, 30).unwrap(), + Time::from_hms_milli(23, 59, 59, 123).unwrap(), + ) + .assume_utc(); + session + .query( + "INSERT INTO time_datetime_tests (id, val) VALUES (0, ?)", + (nanosecond_precision_2nd_half,), + ) + .await + .unwrap(); + + let (read_datetime,) = session + .query("SELECT val from time_datetime_tests", &[]) + .await + .unwrap() + .first_row_typed::<(OffsetDateTime,)>() + .unwrap(); + assert_eq!(read_datetime, nanosecond_precision_2nd_half_rounded); +} + #[tokio::test] async fn test_timeuuid() { let session: Session = init_test("timeuuid_tests", "timeuuid").await; diff --git a/scylla/src/utils/pretty.rs b/scylla/src/utils/pretty.rs index 37851e0883..3b99d6cc6a 100644 --- a/scylla/src/utils/pretty.rs +++ b/scylla/src/utils/pretty.rs @@ -68,14 +68,14 @@ where write!( f, "'{:02}:{:02}:{:02}.{:09}'", - t.num_hours() % 24, - t.num_minutes() % 60, - t.num_seconds() % 60, - t.num_nanoseconds().unwrap_or(0) % 1_000_000_000, + t / 3_600_000_000_000, + t / 60_000_000_000 % 60, + t / 1_000_000_000 % 60, + t % 1_000_000_000, )?; } CqlValue::Timestamp(t) => { - match Utc.timestamp_millis_opt(t.num_milliseconds()) { + match Utc.timestamp_millis_opt(*t) { LocalResult::Ambiguous(_, _) => unreachable!(), // not supposed to happen with timestamp_millis_opt LocalResult::Single(d) => { write!(f, "{}", d.format("'%Y-%m-%d %H:%M:%S%.3f%z'"))? @@ -205,7 +205,6 @@ where #[cfg(test)] mod tests { use std::str::FromStr; - use std::time::Duration; use bigdecimal::BigDecimal; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; @@ -261,15 +260,12 @@ mod tests { ), "1mo2d3ns" ); - let t = Duration::from_nanos(123) - + Duration::from_secs(4) - + Duration::from_secs(5 * 60) - + Duration::from_secs(6 * 60 * 60); + let t = chrono::NaiveTime::from_hms_nano_opt(6, 5, 4, 123) + .unwrap() + .signed_duration_since(chrono::NaiveTime::MIN); + let t = t.num_nanoseconds().unwrap(); assert_eq!( - format!( - "{}", - CqlValueDisplayer(CqlValue::Time(chrono::Duration::from_std(t).unwrap())) - ), + format!("{}", CqlValueDisplayer(CqlValue::Time(t))), "'06:05:04.000000123'" ); @@ -281,6 +277,7 @@ mod tests { "{}", CqlValueDisplayer(CqlValue::Timestamp( t.signed_duration_since(NaiveDateTime::default()) + .num_milliseconds() )) ), "'2005-04-02 19:37:42.000+0000'"