diff --git a/Cargo.toml b/Cargo.toml index d242c7f9..f6afb972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ required-features = ["rustls-tls"] [[example]] name = "data_types_derive_simple" -required-features = ["time", "uuid", "chrono"] +required-features = ["time", "uuid", "chrono", "jiff"] [[example]] name = "data_types_variant" @@ -80,7 +80,7 @@ required-features = ["time"] [[example]] name = "time_types_example" -required-features = ["time", "chrono"] +required-features = ["time", "chrono", "jiff"] [profile.release] debug = true @@ -94,6 +94,7 @@ uuid = ["dep:uuid"] time = ["dep:time"] lz4 = ["dep:lz4_flex", "dep:cityhash-rs"] chrono = ["dep:chrono"] +jiff = ["dep:jiff"] futures03 = [] ## TLS @@ -148,6 +149,7 @@ cityhash-rs = { version = "=1.0.1", optional = true } # exact version for safety uuid = { version = "1", optional = true } time = { version = "0.3", optional = true } chrono = { version = "0.4", optional = true, features = ["serde"] } +jiff = { version = "0.2", optional = true } bstr = { version = "1.11.0", default-features = false } quanta = { version = "0.12", optional = true } replace_with = { version = "0.1.7" } diff --git a/README.md b/README.md index 2ef75591..8268c7f7 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ client.query("DROP TABLE IF EXISTS some").execute().await?; * `uuid` — adds `serde::uuid` to work with [uuid](https://docs.rs/uuid) crate. * `time` — adds `serde::time` to work with [time](https://docs.rs/time) crate. * `chrono` — adds `serde::chrono` to work with [chrono](https://docs.rs/chrono) crate. +* `jiff` — adds `serde::jiff` to work with [jiff](https://docs.rs/jiff) crate. ### TLS By default, TLS is disabled and one or more following features must be enabled to use HTTPS urls: @@ -316,6 +317,7 @@ How to choose between all these features? Here are some considerations: * `Date` maps to/from `u16` or a newtype around it and represents a number of days elapsed since `1970-01-01`. The following external types are supported: * [`time::Date`](https://docs.rs/time/latest/time/struct.Date.html) is supported by using `serde::time::date`, requiring the `time` feature. * [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html) is supported by using `serde::chrono::date`, requiring the `chrono` feature. + * [`jiff::civil::Date`](https://docs.rs/jiff/latest/jiff/civil/struct.Date.html) is supported by using `serde::jiff::date`, requiring the `jiff` feature.
Example @@ -324,10 +326,13 @@ How to choose between all these features? Here are some considerations: struct MyRow { days: u16, #[serde(with = "clickhouse::serde::time::date")] - date: Date, + date: time::Date, // if you prefer using chrono: #[serde(with = "clickhouse::serde::chrono::date")] date_chrono: NaiveDate, + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::date")] + date_jiff: jiff::civil::Date, } ``` @@ -335,6 +340,7 @@ How to choose between all these features? Here are some considerations: * `Date32` maps to/from `i32` or a newtype around it and represents a number of days elapsed since `1970-01-01`. The following external types are supported: * [`time::Date`](https://docs.rs/time/latest/time/struct.Date.html) is supported by using `serde::time::date32`, requiring the `time` feature. * [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html) is supported by using `serde::chrono::date32`, requiring the `chrono` feature. + * [`jiff::civil::Date`](https://docs.rs/jiff/latest/jiff/civil/struct.Date.html) is supported by using `serde::jiff::date32`, requiring the `jiff` feature.
Example @@ -343,11 +349,13 @@ How to choose between all these features? Here are some considerations: struct MyRow { days: i32, #[serde(with = "clickhouse::serde::time::date32")] - date: Date, + date: time::Date, // if you prefer using chrono: #[serde(with = "clickhouse::serde::chrono::date32")] date_chrono: NaiveDate, - + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::date32")] + date_jiff: jiff::civil::Date, } ``` @@ -355,6 +363,7 @@ How to choose between all these features? Here are some considerations: * `DateTime` maps to/from `u32` or a newtype around it and represents a number of seconds elapsed since UNIX epoch. The following external types are supported: * [`time::OffsetDateTime`](https://docs.rs/time/latest/time/struct.OffsetDateTime.html) is supported by using `serde::time::datetime`, requiring the `time` feature. * [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) is supported by using `serde::chrono::datetime`, requiring the `chrono` feature. + * [`jiff::Timestamp`](https://docs.rs/jiff/latest/jiff/struct.Timestamp.html) is supported by using `serde::jiff::datetime`, requiring the `jiff` feature.
Example @@ -366,13 +375,17 @@ How to choose between all these features? Here are some considerations: dt: OffsetDateTime, // if you prefer using chrono: #[serde(with = "clickhouse::serde::chrono::datetime")] - dt_chrono: DateTime, + dt_chrono: DateTime, + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::datetime")] + dt_jiff: Timestamp, } ```
* `DateTime64(_)` maps to/from `i64` or a newtype around it and represents a time elapsed since UNIX epoch. The following external types are supported: * [`time::OffsetDateTime`](https://docs.rs/time/latest/time/struct.OffsetDateTime.html) is supported by using `serde::time::datetime64::*`, requiring the `time` feature. * [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) is supported by using `serde::chrono::datetime64::*`, requiring the `chrono` feature. + * [`jiff::Timestamp`](https://docs.rs/jiff/latest/jiff/struct.Timestamp.html) is supported by using `serde::jiff::datetime64::*`, requiring the `jiff` feature.
Example @@ -397,39 +410,78 @@ How to choose between all these features? Here are some considerations: dt64us_chrono: DateTime, // `DateTime64(6)` #[serde(with = "clickhouse::serde::chrono::datetime64::nanos")] dt64ns_chrono: DateTime, // `DateTime64(9)` + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::datetime64::secs")] + dt64s_jiff: Timestamp, // `DateTime64(0)` + #[serde(with = "clickhouse::serde::jiff::datetime64::millis")] + dt64ms_jiff: Timestamp, // `DateTime64(3)` + #[serde(with = "clickhouse::serde::jiff::datetime64::micros")] + dt64us_jiff: Timestamp, // `DateTime64(6)` + #[serde(with = "clickhouse::serde::jiff::datetime64::nanos")] + dt64ns_jiff: Timestamp, // `DateTime64(9)` } ```
* `Time` maps to/from i32 or a newtype around it. The Time data type is used to store a time value independent of any calendar date. It is ideal for representing daily schedules, event times, or any situation where only the time component (hours, minutes, seconds) is important. - * [`time:Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is is supported by using `serde::time::*`, requiring the `time` feature. - * [`chrono::Duration`](https://docs.rs/chrono/latest/chrono/type.Duration.html) is supported by using `serde::chrono::*`, which is an alias to `TimeDelta`, requiring the `chrono` feature + * [`time::Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is supported by using `serde::time::time`, requiring the `time` feature. + * [`chrono::Duration`](https://docs.rs/chrono/latest/chrono/type.Duration.html) is supported by using `serde::chrono::time`, which is an alias to `TimeDelta`, requiring the `chrono` feature + * [`jiff::SignedDuration`](https://docs.rs/jiff/latest/jiff/struct.SignedDuration.html) is supported by using `serde::jiff::time`, requiring the `jiff` feature.
Example ```rust,ignore #[derive(Row, Serialize, Deserialize)] struct MyRow { - #[serde(with = "clickhouse::serde::chrono::time64::secs")] - t0: chrono::Duration, - #[serde(with = "clickhouse::serde::chrono::time64::secs::option")] - t0_opt: Option, + #[serde(with = "clickhouse::serde::time::time")] + t: time::Duration, + // if you prefer using chrono: + #[serde(with = "clickhouse::serde::chrono::time")] + t_chrono: chrono::Duration, + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::time")] + t_jiff: jiff::SignedDuration, } ```
* `Time64(_)` maps to/from i64 or a newtype around it. The Time data type is used to store a time value independent of any calendar date. It is ideal for representing daily schedules, event times, or any situation where only the time component (hours, minutes, seconds) is important. - * [`time:Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is is supported by using `serde::time::*`, requiring the `time` feature. - * [`chrono::Duration`](https://docs.rs/chrono/latest/chrono/type.Duration.html) is supported by using `serde::chrono::*`, requiring the `chrono` feature + * [`time::Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is supported by using `serde::time::time64::*`, requiring the `time` feature. + * [`chrono::Duration`](https://docs.rs/chrono/latest/chrono/type.Duration.html) is supported by using `serde::chrono::time64::*`, requiring the `chrono` feature + * [`jiff::SignedDuration`](https://docs.rs/jiff/latest/jiff/struct.SignedDuration.html) is supported by using `serde::jiff::time64::*`, requiring the `jiff` feature.
Example ```rust,ignore #[derive(Row, Serialize, Deserialize)] struct MyRow { - #[serde(with = "clickhouse::serde::time::time")] - t0: Time, + #[serde(with = "clickhouse::serde::time::time64::secs")] + t64s: time::Duration, + #[serde(with = "clickhouse::serde::time::time64::millis")] + t64ms: time::Duration, + #[serde(with = "clickhouse::serde::time::time64::micros")] + t64us: time::Duration, + #[serde(with = "clickhouse::serde::time::time64::nanos")] + t64ns: time::Duration, + // if you prefer using chrono: + #[serde(with = "clickhouse::serde::chrono::time64::secs")] + t64s_chrono: chrono::Duration, + #[serde(with = "clickhouse::serde::chrono::time64::millis")] + t64ms_chrono: chrono::Duration, + #[serde(with = "clickhouse::serde::chrono::time64::micros")] + t64us_chrono: chrono::Duration, + #[serde(with = "clickhouse::serde::chrono::time64::nanos")] + t64ns_chrono: chrono::Duration, + // if you prefer using jiff: + #[serde(with = "clickhouse::serde::jiff::time64::secs")] + t64s_jiff: jiff::SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::millis")] + t64ms_jiff: jiff::SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::micros")] + t64us_jiff: jiff::SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::nanos")] + t64ns_jiff: jiff::SignedDuration, } ``` diff --git a/examples/data_types_derive_simple.rs b/examples/data_types_derive_simple.rs index bc97b6f9..78f6a769 100644 --- a/examples/data_types_derive_simple.rs +++ b/examples/data_types_derive_simple.rs @@ -3,6 +3,7 @@ use fixnum::{ typenum::{U12, U4, U8}, FixedPoint, }; +use jiff::{civil::Date as JiffDate, Timestamp}; use rand::prelude::IndexedRandom; use rand::{distr::Alphanumeric, Rng}; use std::str::FromStr; @@ -73,6 +74,16 @@ async fn main() -> Result<()> { chrono_datetime64_6 DateTime64(6), chrono_datetime64_9 DateTime64(9), chrono_datetime64_9_tz DateTime64(9, 'UTC'), + + jiff_date Date, + jiff_date32 Date32, + jiff_datetime DateTime, + jiff_datetime_tz DateTime('UTC'), + jiff_datetime64_0 DateTime64(0), + jiff_datetime64_3 DateTime64(3), + jiff_datetime64_6 DateTime64(6), + jiff_datetime64_9 DateTime64(9), + jiff_datetime64_9_tz DateTime64(9, 'UTC'), ) ENGINE MergeTree ORDER BY (); ", ) @@ -168,6 +179,25 @@ pub struct Row { pub chrono_datetime64_9: DateTime, #[serde(with = "clickhouse::serde::chrono::datetime64::nanos")] pub chrono_datetime64_9_tz: DateTime, + + #[serde(with = "clickhouse::serde::jiff::date")] + pub jiff_date: JiffDate, + #[serde(with = "clickhouse::serde::jiff::date32")] + pub jiff_date32: JiffDate, + #[serde(with = "clickhouse::serde::jiff::datetime")] + pub jiff_datetime: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime")] + pub jiff_datetime_tz: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::secs")] + pub jiff_datetime64_0: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::millis")] + pub jiff_datetime64_3: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::micros")] + pub jiff_datetime64_6: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::nanos")] + pub jiff_datetime64_9: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::nanos")] + pub jiff_datetime64_9_tz: Timestamp, } // See ClickHouse decimal sizes: https://clickhouse.com/docs/en/sql-reference/data-types/decimal @@ -255,6 +285,16 @@ impl Row { chrono_datetime64_6: Utc::now(), chrono_datetime64_9: Utc::now(), chrono_datetime64_9_tz: Utc::now(), + + jiff_date: JiffDate::constant(2149, 6, 6), + jiff_date32: JiffDate::constant(2299, 12, 31), + jiff_datetime: Timestamp::now(), + jiff_datetime_tz: Timestamp::now(), + jiff_datetime64_0: Timestamp::now(), + jiff_datetime64_3: Timestamp::now(), + jiff_datetime64_6: Timestamp::now(), + jiff_datetime64_9: Timestamp::now(), + jiff_datetime64_9_tz: Timestamp::now(), } } } diff --git a/examples/time_types_example.rs b/examples/time_types_example.rs index dfac616a..b416b49a 100644 --- a/examples/time_types_example.rs +++ b/examples/time_types_example.rs @@ -1,5 +1,6 @@ use chrono::Duration; use clickhouse::Client; +use jiff::SignedDuration; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, clickhouse::Row)] @@ -44,9 +45,30 @@ struct TimeExampleChrono { time64_nanos: Duration, } +#[derive(Debug, Serialize, Deserialize, clickhouse::Row)] +struct TimeExampleJiff { + #[serde(with = "clickhouse::serde::jiff::time")] + time_field: SignedDuration, + + #[serde(with = "clickhouse::serde::jiff::time::option")] + time_optional: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::secs")] + time64_seconds: SignedDuration, + + #[serde(with = "clickhouse::serde::jiff::time64::millis")] + time64_millis: SignedDuration, + + #[serde(with = "clickhouse::serde::jiff::time64::micros")] + time64_micros: SignedDuration, + + #[serde(with = "clickhouse::serde::jiff::time64::nanos")] + time64_nanos: SignedDuration, +} + #[tokio::main] async fn main() -> Result<(), Box> { - let client = Client::default(); + let client = Client::default().with_url("http://localhost:8123"); let create_table_sql = r#" CREATE TABLE IF NOT EXISTS time_example ( @@ -58,6 +80,7 @@ async fn main() -> Result<(), Box> { time64_nanos Time64(9) ) ENGINE = MergeTree() ORDER BY time_field + SETTINGS enable_time_time64_type = 1 "#; client.query(create_table_sql).execute().await?; @@ -116,6 +139,20 @@ async fn main() -> Result<(), Box> { println!("Inserted edge case #{i}: {edge:?}"); } + // Insert data using jiff crate + let time_example = TimeExampleJiff { + time_field: SignedDuration::new(23 * 3600 + 56 * 60, 0), + time_optional: Some(SignedDuration::new(3600 + 2 * 60 + 2, 0)), + time64_seconds: SignedDuration::new(3 * 3600 + 4 * 60 + 5, 0), + time64_millis: SignedDuration::new(6 * 3600 + 7 * 60 + 8, 123_000_000), + time64_micros: SignedDuration::new(9 * 3600 + 10 * 60 + 11, 456_789_000), + time64_nanos: SignedDuration::new(12 * 3600 + 13 * 60 + 14, 123_456_789), + }; + + let mut insert = client.insert::("time_example").await?; + insert.write(&time_example).await?; + insert.end().await?; + // Query the data let rows: Vec = client .query("SELECT * FROM time_example ORDER BY time_field") diff --git a/src/serde.rs b/src/serde.rs index e0e4d178..d527c0ac 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -112,6 +112,7 @@ pub mod chrono { use ::chrono::{DateTime, Utc}; use serde::{de::Error as _, ser::Error as _}; + /// Ser/de `DateTime` to/from `DateTime`. pub mod datetime { use super::*; @@ -262,7 +263,7 @@ pub mod chrono { } } - /// Ser/de `time::Date` to/from `Date`. + /// Ser/de `chrono::NaiveDate` to/from `Date`. pub mod date { use super::*; use ::chrono::{Duration, NaiveDate}; @@ -301,7 +302,7 @@ pub mod chrono { } } - /// Ser/de `time::Date` to/from `Date32`. + /// Ser/de `chrono::NaiveDate` to/from `Date32`. pub mod date32 { use ::chrono::{Duration, NaiveDate}; @@ -491,6 +492,390 @@ pub mod chrono { } } +#[cfg(feature = "jiff")] +pub mod jiff { + use super::*; + use ::jiff::Timestamp; + use serde::{de::Error as _, ser::Error as _}; + + /// Ser/de `jiff::Timestamp` to/from `DateTime`. + pub mod datetime { + use super::*; + + option!( + Timestamp, + "Ser/de `Option` to/from `Nullable(DateTime)`." + ); + + pub fn serialize(dt: &Timestamp, serializer: S) -> Result + where + S: Serializer, + { + let ts = dt.as_second(); + + u32::try_from(ts) + .map_err(|_| S::Error::custom(format!("{dt} cannot be represented as DateTime")))? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ts: u32 = Deserialize::deserialize(deserializer)?; + Timestamp::from_second(i64::from(ts)) + .map_err(|_| D::Error::custom(format!("{ts} cannot be converted to Timestamp"))) + } + } + + /// Contains modules to ser/de `jiff::Timestamp` to/from `DateTime64(_)`. + pub mod datetime64 { + use super::*; + + /// Ser/de `Timestamp` to/from `DateTime64(0)` (seconds). + pub mod secs { + use super::*; + + option!( + Timestamp, + "Ser/de `Option` to/from `Nullable(DateTime64(0))`." + ); + + pub fn serialize(dt: &Timestamp, serializer: S) -> Result + where + S: Serializer, + { + let ts = dt.as_second(); + ts.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ts: i64 = Deserialize::deserialize(deserializer)?; + Timestamp::from_second(ts) + .map_err(|_| D::Error::custom(format!("Can't create Timestamp from {ts}"))) + } + } + + /// Ser/de `Timestamp` to/from `DateTime64(3)` (milliseconds). + pub mod millis { + use super::*; + + option!( + Timestamp, + "Ser/de `Option` to/from `Nullable(DateTime64(3))`." + ); + + pub fn serialize(dt: &Timestamp, serializer: S) -> Result + where + S: Serializer, + { + let ts = dt.as_millisecond(); + ts.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ts: i64 = Deserialize::deserialize(deserializer)?; + Timestamp::from_millisecond(ts) + .map_err(|_| D::Error::custom(format!("Can't create Timestamp from {ts}"))) + } + } + + /// Ser/de `Timestamp` to/from `DateTime64(6)` (microseconds). + pub mod micros { + use super::*; + + option!( + Timestamp, + "Ser/de `Option` to/from `Nullable(DateTime64(6))`." + ); + + pub fn serialize(dt: &Timestamp, serializer: S) -> Result + where + S: Serializer, + { + let ts = dt.as_microsecond(); + ts.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ts: i64 = Deserialize::deserialize(deserializer)?; + Timestamp::from_microsecond(ts) + .map_err(|_| D::Error::custom(format!("Can't create Timestamp from {ts}"))) + } + } + + /// Ser/de `Timestamp` to/from `DateTime64(9)` (nanoseconds). + pub mod nanos { + use super::*; + + option!( + Timestamp, + "Ser/de `Option` to/from `Nullable(DateTime64(9))`." + ); + + pub fn serialize(dt: &Timestamp, serializer: S) -> Result + where + S: Serializer, + { + let ts = i64::try_from(dt.as_nanosecond()).map_err(|_| { + S::Error::custom(format!("{dt} cannot be represented as DateTime64")) + })?; + ts.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ts: i64 = Deserialize::deserialize(deserializer)?; + Timestamp::from_nanosecond(i128::from(ts)) + .map_err(|_| D::Error::custom(format!("Can't create Timestamp from {ts}"))) + } + } + } + + /// Ser/de `jiff::civil::Date` to/from `Date`. + pub mod date { + use super::*; + use ::jiff::{civil::Date, SignedDuration}; + + option!(Date, "Ser/de `Option` to/from `Nullable(Date)`."); + + const ORIGIN: Date = Date::constant(1970, 1, 1); + + pub fn serialize(date: &Date, serializer: S) -> Result + where + S: Serializer, + { + if *date < ORIGIN { + let msg = format!("{date} cannot be represented as Date"); + return Err(S::Error::custom(msg)); + } + + let elapsed = date.duration_since(ORIGIN); // cannot underflow: checked above + let days = elapsed.as_hours() / 24; + + u16::try_from(days) + .map_err(|_| S::Error::custom(format!("{date} cannot be represented as Date")))? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let days: u16 = Deserialize::deserialize(deserializer)?; + Ok(ORIGIN + SignedDuration::from_hours(i64::from(days) * 24)) // cannot overflow: always < `Date::MAX` + } + } + + /// Ser/de `jiff::civil::Date` to/from `Date32`. + pub mod date32 { + use super::*; + use ::jiff::{civil::Date, SignedDuration}; + + option!(Date, "Ser/de `Option` to/from `Nullable(Date32)`."); + + const ORIGIN: Date = Date::constant(1970, 1, 1); + + // NOTE: actually, it's 1925 and 2283 with a tail for versions before 22.8-lts. + const MIN: Date = Date::constant(1900, 1, 1); + const MAX: Date = Date::constant(2299, 12, 31); + + pub fn serialize(date: &Date, serializer: S) -> Result + where + S: Serializer, + { + if *date < MIN || *date > MAX { + let msg = format!("{date} cannot be represented as Date32"); + return Err(S::Error::custom(msg)); + } + + let elapsed = date.duration_since(ORIGIN); // cannot underflow: checked above + let days = elapsed.as_hours() / 24; + + i32::try_from(days) + .map_err(|_| S::Error::custom(format!("{date} cannot be represented as Date32")))? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let days: i32 = Deserialize::deserialize(deserializer)?; + + // It shouldn't overflow, because clamped by CH and < `Date::MAX`. + // TODO: ensure CH clamps when an invalid value is inserted in binary format. + Ok(ORIGIN + SignedDuration::from_hours(i64::from(days) * 24)) + } + } + + /// Ser/de `jiff::SignedDuration` to/from `Time`. + pub mod time { + use super::*; + use ::jiff::SignedDuration; + + option!( + SignedDuration, + "Ser/de `Option` to/from `Nullable(Time)`." + ); + + pub fn serialize(time: &SignedDuration, serializer: S) -> Result + where + S: Serializer, + { + let seconds = time.as_secs(); + + i32::try_from(seconds) + .map_err(|_| S::Error::custom(format!("{time} cannot be represented as Time")))? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds: i32 = Deserialize::deserialize(deserializer)?; + Ok(SignedDuration::from_secs(seconds as i64)) + } + } + + /// Contains modules to ser/de `jiff::SignedDuration` to/from `Time64(_)`. + pub mod time64 { + use super::*; + use ::jiff::SignedDuration; + + /// Ser/de `SignedDuration` to/from `Time64(0)` (seconds). + pub mod secs { + use super::*; + + option!( + SignedDuration, + "Ser/de `Option` to/from `Nullable(Time64(0))`." + ); + + pub fn serialize(time: &SignedDuration, serializer: S) -> Result + where + S: Serializer, + { + let seconds = time.as_secs(); + seconds.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds: i64 = Deserialize::deserialize(deserializer)?; + Ok(SignedDuration::from_secs(seconds)) + } + } + + /// Ser/de `SignedDuration` to/from `Time64(3)` (milliseconds). + pub mod millis { + use super::*; + + option!( + SignedDuration, + "Ser/de `Option` to/from `Nullable(Time64(3))`." + ); + + pub fn serialize(time: &SignedDuration, serializer: S) -> Result + where + S: Serializer, + { + let millis = time.as_millis(); + + i64::try_from(millis) + .map_err(|_| { + S::Error::custom(format!("{time} cannot be represented as Time64")) + })? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let millis: i64 = Deserialize::deserialize(deserializer)?; + Ok(SignedDuration::from_millis(millis)) + } + } + + /// Ser/de `SignedDuration` to/from `Time64(6)` (microseconds). + pub mod micros { + use super::*; + + option!( + SignedDuration, + "Ser/de `Option` to/from `Nullable(Time64(6))`." + ); + + pub fn serialize(time: &SignedDuration, serializer: S) -> Result + where + S: Serializer, + { + let micros = time.as_micros(); + + i64::try_from(micros) + .map_err(|_| { + S::Error::custom(format!("{time} cannot be represented as Time64")) + })? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let micros: i64 = Deserialize::deserialize(deserializer)?; + Ok(SignedDuration::from_micros(micros)) + } + } + + /// Ser/de `SignedDuration` to/from `Time64(9)` (nanoseconds). + pub mod nanos { + use super::*; + + option!( + SignedDuration, + "Ser/de `Option` to/from `Nullable(Time64(9))`." + ); + + pub fn serialize(time: &SignedDuration, serializer: S) -> Result + where + S: Serializer, + { + let nanos = time.as_nanos(); + + i64::try_from(nanos) + .map_err(|_| { + S::Error::custom(format!("{time} cannot be represented as Time64")) + })? + .serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let nanos: i64 = Deserialize::deserialize(deserializer)?; + Ok(SignedDuration::from_nanos(nanos)) + } + } + } +} + /// Ser/de [`::time::OffsetDateTime`] and [`::time::Date`]. #[cfg(feature = "time")] pub mod time { diff --git a/tests/it/jiff.rs b/tests/it/jiff.rs new file mode 100644 index 00000000..6bdb4e4a --- /dev/null +++ b/tests/it/jiff.rs @@ -0,0 +1,552 @@ +#![cfg(feature = "jiff")] + +use std::ops::RangeBounds; + +use jiff::{ + civil::{Date, DateTime, Time}, + tz::TimeZone, + SignedDuration, Timestamp, +}; +use rand::{ + distr::{Distribution, StandardUniform}, + Rng, +}; +use serde::{Deserialize, Serialize}; + +use clickhouse::Row; + +#[tokio::test] +async fn datetime() { + let client = prepare_database!(); + + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Row)] + struct MyRow { + #[serde(with = "clickhouse::serde::jiff::datetime")] + dt: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime::option")] + dt_opt: Option, + #[serde(with = "clickhouse::serde::jiff::datetime64::secs")] + dt64s: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::secs::option")] + dt64s_opt: Option, + #[serde(with = "clickhouse::serde::jiff::datetime64::millis")] + dt64ms: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::millis::option")] + dt64ms_opt: Option, + #[serde(with = "clickhouse::serde::jiff::datetime64::micros")] + dt64us: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::micros::option")] + dt64us_opt: Option, + #[serde(with = "clickhouse::serde::jiff::datetime64::nanos")] + dt64ns: Timestamp, + #[serde(with = "clickhouse::serde::jiff::datetime64::nanos::option")] + dt64ns_opt: Option, + } + + #[derive(Debug, Deserialize, Row)] + struct MyRowStr { + dt: String, + dt64s: String, + dt64ms: String, + dt64us: String, + dt64ns: String, + } + + client + .query( + " + CREATE TABLE test( + dt DateTime, + dt_opt Nullable(DateTime), + dt64s DateTime64(0), + dt64s_opt Nullable(DateTime64(0)), + dt64ms DateTime64(3), + dt64ms_opt Nullable(DateTime64(3)), + dt64us DateTime64(6), + dt64us_opt Nullable(DateTime64(6)), + dt64ns DateTime64(9), + dt64ns_opt Nullable(DateTime64(9)) + ) + ENGINE = MergeTree ORDER BY dt + ", + ) + .execute() + .await + .unwrap(); + let d = DateTime::constant(2022, 11, 13, 0, 0, 0, 0) + .to_zoned(TimeZone::UTC) + .unwrap(); + let dt_s = d.with().hour(15).minute(27).second(42).build().unwrap(); + let dt_ms = dt_s.with().millisecond(123).build().unwrap(); + let dt_us = dt_ms.with().microsecond(456).build().unwrap(); + let dt_ns = dt_us.with().nanosecond(789).build().unwrap(); + + let dt_s = dt_s.timestamp(); + let dt_ms = dt_ms.timestamp(); + let dt_us = dt_us.timestamp(); + let dt_ns = dt_ns.timestamp(); + + let original_row = MyRow { + dt: dt_s, + dt_opt: Some(dt_s), + dt64s: dt_s, + dt64s_opt: Some(dt_s), + dt64ms: dt_ms, + dt64ms_opt: Some(dt_ms), + dt64us: dt_us, + dt64us_opt: Some(dt_us), + dt64ns: dt_ns, + dt64ns_opt: Some(dt_ns), + }; + + let mut insert = client.insert::("test").await.unwrap(); + insert.write(&original_row).await.unwrap(); + insert.end().await.unwrap(); + + let row = client + .query("SELECT ?fields FROM test") + .fetch_one::() + .await + .unwrap(); + + let row_str = client + .query( + " + SELECT toString(dt) AS dt, + toString(dt64s) AS dt64s, + toString(dt64ms) AS dt64ms, + toString(dt64us) AS dt64us, + toString(dt64ns) AS dt64ns + FROM test + ", + ) + .fetch_one::() + .await + .unwrap(); + + assert_eq!(row, original_row); + assert_eq!(row_str.dt, original_row.dt.strftime("%F %T").to_string()); + assert_eq!( + row_str.dt64s, + original_row.dt64s.strftime("%F %T").to_string() + ); + assert_eq!( + row_str.dt64ms, + original_row.dt64ms.strftime("%F %T%.f").to_string() + ); + assert_eq!( + row_str.dt64us, + original_row.dt64us.strftime("%F %T%.f").to_string() + ); + assert_eq!( + row_str.dt64ns, + original_row.dt64ns.strftime("%F %T%.f").to_string() + ); +} + +#[tokio::test] +async fn date() { + let client = prepare_database!(); + + #[derive(Debug, Serialize, Deserialize, Row)] + struct MyRow { + #[serde(with = "clickhouse::serde::jiff::date")] + date: Date, + #[serde(with = "clickhouse::serde::jiff::date::option")] + date_opt: Option, + } + + client + .query( + " + CREATE TABLE test( + date Date, + date_opt Nullable(Date) + ) ENGINE = MergeTree ORDER BY date + ", + ) + .execute() + .await + .unwrap(); + + let mut insert = client.insert::("test").await.unwrap(); + + let dates = generate_dates(1970..2149, 100); + for &date in &dates { + let original_row = MyRow { + date, + date_opt: Some(date), + }; + + insert.write(&original_row).await.unwrap(); + } + insert.end().await.unwrap(); + + let actual = client + .query("SELECT ?fields, toString(date) FROM test ORDER BY date") + .fetch_all::<(MyRow, String)>() + .await + .unwrap(); + + assert_eq!(actual.len(), dates.len()); + + for ((row, date_str), expected) in actual.iter().zip(dates) { + assert_eq!(row.date, expected); + assert_eq!(row.date_opt, Some(expected)); + assert_eq!(date_str, &expected.to_string()); + } +} + +#[tokio::test] +async fn date32() { + let client = prepare_database!(); + + #[derive(Debug, Serialize, Deserialize, Row)] + struct MyRow { + #[serde(with = "clickhouse::serde::jiff::date32")] + date: Date, + #[serde(with = "clickhouse::serde::jiff::date32::option")] + date_opt: Option, + } + + client + .query( + " + CREATE TABLE test( + date Date32, + date_opt Nullable(Date32) + ) ENGINE = MergeTree ORDER BY date + ", + ) + .execute() + .await + .unwrap(); + + let mut insert = client.insert::("test").await.unwrap(); + + let dates = generate_dates(1925..2283, 100); // TODO: 1900..=2299 for newer versions. + for &date in &dates { + let original_row = MyRow { + date, + date_opt: Some(date), + }; + + insert.write(&original_row).await.unwrap(); + } + insert.end().await.unwrap(); + + let actual = client + .query("SELECT ?fields, toString(date) FROM test ORDER BY date") + .fetch_all::<(MyRow, String)>() + .await + .unwrap(); + + assert_eq!(actual.len(), dates.len()); + + for ((row, date_str), expected) in actual.iter().zip(dates) { + assert_eq!(row.date, expected); + assert_eq!(row.date_opt, Some(expected)); + assert_eq!(date_str, &expected.to_string()); + } +} + +// Distribution isn't implemented for `jiff` types, but we can lift the implementation from the `time` crate: https://docs.rs/crate/time/0.3.44/source/src/rand09.rs#17-27 +struct DateWrapper(Date); + +impl Distribution for StandardUniform { + fn sample(&self, rng: &mut R) -> DateWrapper { + // For some reason adding to a Date a SignedDuration that is larger than + // 2932896 days causes an overflow. + // + // Causing this: + // ``` + // Date::MIN + Date::MAX.duration_since(Date::MIN) + // ``` + // to fail. + // + // Let's just limit ourselves to years 1900 - 2299 (roughly 3506304 hours). + DateWrapper( + Date::constant(1900, 1, 1) + SignedDuration::from_hours(rng.random_range(0..=3506304)), + ) + } +} + +fn generate_dates(years: impl RangeBounds, count: usize) -> Vec { + let mut rng = rand::rng(); + let mut dates: Vec<_> = (&mut rng) + .sample_iter(StandardUniform) + .filter_map(|date: DateWrapper| { + if years.contains(&date.0.year()) { + Some(date.0) + } else { + None + } + }) + .take(count) + .collect(); + + dates.sort_unstable(); + dates +} + +#[tokio::test] +async fn time_roundtrip() { + let client = prepare_database!(); + + client + .query( + r#" + CREATE TABLE test_time ( + t0 Time, + t1 Nullable(Time) + ) ENGINE = MergeTree ORDER BY tuple() + SETTINGS enable_time_time64_type = 1; + "#, + ) + .execute() + .await + .unwrap(); + + #[derive(Debug, PartialEq, Serialize, Deserialize, Row)] + struct TimeRow { + #[serde(with = "clickhouse::serde::jiff::time")] + t0: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time::option")] + t1: Option, + } + + let time = Time::constant(12, 34, 56, 0); + let duration = time.duration_since(Time::midnight()); + + let row = TimeRow { + t0: duration, + t1: Some(duration), + }; + + let mut insert = client.insert::("test_time").await.unwrap(); + insert.write(&row).await.unwrap(); + insert.end().await.unwrap(); + + let fetched = client + .query("SELECT ?fields FROM test_time") + .fetch_one::() + .await + .unwrap(); + + assert_eq!(fetched, row); +} + +#[tokio::test] +async fn time_negative_roundtrip() { + let client = prepare_database!(); + + client + .query( + r#" + CREATE TABLE test_time_chrono_negative ( + t0 Time, + t1 Nullable(Time) + ) ENGINE = MergeTree ORDER BY tuple() + SETTINGS enable_time_time64_type = 1; + "#, + ) + .execute() + .await + .unwrap(); + + #[derive(Debug, PartialEq, Serialize, Deserialize, Row)] + struct TimeRow { + #[serde(with = "clickhouse::serde::jiff::time")] + t0: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time::option")] + t1: Option, + } + + // Create negative duration directly + let negative_duration = SignedDuration::from_secs(-2 * 3600 - 15 * 60 - 30); // -02:15:30 + + let row = TimeRow { + t0: negative_duration, + t1: Some(negative_duration), + }; + + let mut insert = client + .insert::("test_time_chrono_negative") + .await + .unwrap(); + insert.write(&row).await.unwrap(); + insert.end().await.unwrap(); + + let fetched = client + .query("SELECT ?fields FROM test_time_chrono_negative") + .fetch_one::() + .await + .unwrap(); + + assert_eq!(fetched, row); +} + +#[tokio::test] +async fn time64_roundtrip() { + let client = prepare_database!(); + + client + .query( + r#" + CREATE TABLE test_time64 ( + t0 Time64(0), + t0_opt Nullable(Time64(0)), + t3 Time64(3), + t3_opt Nullable(Time64(3)), + t6 Time64(6), + t6_opt Nullable(Time64(6)), + t9 Time64(9), + t9_opt Nullable(Time64(9)) + ) ENGINE = MergeTree + ORDER BY tuple() + SETTINGS enable_time_time64_type = 1; + "#, + ) + .execute() + .await + .unwrap(); + + #[derive(Debug, PartialEq, Serialize, Deserialize, Row)] + struct MyRow { + #[serde(with = "clickhouse::serde::jiff::time64::secs")] + t0: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::secs::option")] + t0_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::millis")] + t3: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::millis::option")] + t3_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::micros")] + t6: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::micros::option")] + t6_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::nanos")] + t9: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::nanos::option")] + t9_opt: Option, + } + + let time_s = Time::constant(12, 34, 56, 0); + let time_ms = Time::constant(12, 34, 56, 789_000_000); + let time_us = Time::constant(12, 34, 56, 789_123_000); + let time_ns = Time::constant(12, 34, 56, 789_123_456); + + let dur_s = time_s.duration_since(Time::midnight()); + let dur_ms = time_ms.duration_since(Time::midnight()); + let dur_us = time_us.duration_since(Time::midnight()); + let dur_ns = time_ns.duration_since(Time::midnight()); + + let original_row = MyRow { + t0: dur_s, + t0_opt: Some(dur_s), + t3: dur_ms, + t3_opt: Some(dur_ms), + t6: dur_us, + t6_opt: Some(dur_us), + t9: dur_ns, + t9_opt: Some(dur_ns), + }; + + let mut insert = client.insert::("test_time64").await.unwrap(); + insert.write(&original_row).await.unwrap(); + insert.end().await.unwrap(); + + let fetched = client + .query("SELECT ?fields FROM test_time64") + .fetch_one::() + .await + .unwrap(); + + assert_eq!(fetched, original_row); +} + +#[tokio::test] +async fn time64_negative_roundtrip() { + let client = prepare_database!(); + + client + .query( + r#" + CREATE TABLE test_time64_negative ( + t0 Time64(0), + t0_opt Nullable(Time64(0)), + t3 Time64(3), + t3_opt Nullable(Time64(3)), + t6 Time64(6), + t6_opt Nullable(Time64(6)), + t9 Time64(9), + t9_opt Nullable(Time64(9)) + ) ENGINE = MergeTree + ORDER BY tuple() + SETTINGS enable_time_time64_type = 1; + "#, + ) + .execute() + .await + .unwrap(); + + #[derive(Debug, PartialEq, Serialize, Deserialize, Row)] + struct MyRow { + #[serde(with = "clickhouse::serde::jiff::time64::secs")] + t0: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::secs::option")] + t0_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::millis")] + t3: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::millis::option")] + t3_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::micros")] + t6: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::micros::option")] + t6_opt: Option, + + #[serde(with = "clickhouse::serde::jiff::time64::nanos")] + t9: SignedDuration, + #[serde(with = "clickhouse::serde::jiff::time64::nanos::option")] + t9_opt: Option, + } + + // Create negative durations directly + let neg_base_seconds = -5 * 3600 - 15 * 60 - 30; // -18930 seconds (-05:15:30) + + let dur_s = SignedDuration::from_secs(neg_base_seconds); + let dur_ms = dur_s - SignedDuration::from_millis(123); + let dur_us = dur_s - SignedDuration::from_micros(123_456); + let dur_ns = dur_s - SignedDuration::from_nanos(123_456_789); + + let negative_row = MyRow { + t0: dur_s, + t0_opt: Some(dur_s), + t3: dur_ms, + t3_opt: Some(dur_ms), + t6: dur_us, + t6_opt: Some(dur_us), + t9: dur_ns, + t9_opt: Some(dur_ns), + }; + + let mut insert = client + .insert::("test_time64_negative") + .await + .unwrap(); + insert.write(&negative_row).await.unwrap(); + insert.end().await.unwrap(); + + let fetched = client + .query("SELECT ?fields FROM test_time64_negative") + .fetch_one::() + .await + .unwrap(); + + assert_eq!(fetched, negative_row); +} diff --git a/tests/it/main.rs b/tests/it/main.rs index 48167acf..4519d318 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -235,6 +235,7 @@ mod insert; mod inserter; mod int128; mod ip; +mod jiff; mod mock; mod nested; mod query;