Skip to content

Commit d76582f

Browse files
committed
feat: flag for time/chrono crate support
Introduces a new --temporal option to choose temporal type mapping: - numeric (default): primitive types for backward compatibility - time: uses time crate (time::Date, time::OffsetDateTime, time::Duration) - chrono: uses chrono crate (chrono::NaiveDate, chrono::DateTime, etc.) - Adds support for ClickHouse Time and Time64(precision) types. Updated all snapshots.
1 parent 0402177 commit d76582f

32 files changed

+2729
-85
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
<!-- next-header -->
88

99
## [Unreleased] - ReleaseDate
10+
### Added
11+
- Support for ClickHouse `Time` and `Time64(precision)` types.
12+
- New `--temporal` option to choose temporal type mapping mode: `numeric` (default), `time`, or `chrono`.
13+
- **numeric** mode (default): maintains backward compatibility, uses primitive Rust types (u16, i32, u32, i64).
14+
- **time** mode: uses `time` crate types (`time::Date`, `time::OffsetDateTime`, `time::Duration`).
15+
- **chrono** mode: uses `chrono` crate types (`chrono::NaiveDate`, `chrono::DateTime<Utc>`, `chrono::Duration`).
16+
- Comprehensive serde support for temporal types in all modes with appropriate precision handling.
1017

1118
## [0.1.8] - 2024-09-27
1219
### Added

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ native-tls = ["clickhouse/native-tls"]
2020

2121
[dependencies]
2222
anyhow = "1.0.40"
23-
clickhouse = "0.13.0"
23+
clickhouse = "0.14.0"
2424
heck = "0.5.0"
2525
serde = { version = "1.0.126", features = ["derive"] }
2626
structopt = "0.3.21"
@@ -32,4 +32,6 @@ serde_repr = "0.1.7"
3232
serde_bytes = "0.11.5"
3333
trybuild = "1.0.42"
3434
uuid = "1.2.1"
35-
clickhouse = { version = "0.13.0", features = ["uuid"] }
35+
clickhouse = { version = "0.14.0", features = ["uuid", "time", "chrono"] }
36+
time = { version = "0.3.44", features = ["serde"] }
37+
chrono = { version = "0.4.42", features = ["serde"] }

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ An auxiliary utility for generating Rust structures from ClickHouse DB schemas f
88
cargo install ch2rs
99
```
1010

11-
The crate enables `rustls-tls` [client](https://github.com/ClickHouse/clickhouse-rs/blob/main/Cargo.toml) feature by default, which allows to work with HTTPS URLs.
11+
The crate enables `rustls-tls` [client](https://github.com/ClickHouse/clickhouse-rs/blob/main/Cargo.toml) feature by default, which allows to work with HTTPS URLs.
1212
If `rustls-tls` does not work in your use case, you can install the crate with `native-tls` instead:
1313

1414
```sh
@@ -43,12 +43,36 @@ OPTIONS:
4343
--derive <trait>... Add `#[derive(<trait>)]` to the generated types
4444
-T <types>... Override the type, e.g. 'Decimal(18, 9)=fixnum::FixedPoint<i64, typenum::U9>'
4545
-U <url> ClickHouse server's URL [default: localhost:8123]
46+
--temporal <mode> Temporal mapping: numeric|time|chrono [default: numeric]
4647
-u <user>
4748
4849
ARGS:
4950
<table> The table's name
5051
```
5152

53+
### Temporal modes
54+
55+
- **numeric** (default): uses primitive Rust types:
56+
- Date → `u16`, Date32 → `i32`
57+
- DateTime → `u32`, DateTime64(_) → `i64`
58+
- Time → `i32`, Time64(_) → `i64`
59+
60+
- **time**: uses the `time` crate types
61+
- Date/Date32: `time::Date`
62+
- DateTime/DateTime64: `time::OffsetDateTime`
63+
- Time/Time64: `time::Duration`
64+
- Requires in your project: `time` dependency and `clickhouse` feature `time`
65+
- `clickhouse = { version = "…", features = ["time"] }`
66+
67+
- **chrono**: uses the `chrono` crate types
68+
- Date/Date32: `chrono::NaiveDate`
69+
- DateTime/DateTime64: `chrono::DateTime<chrono::Utc>`
70+
- Time/Time64: `chrono::Duration`
71+
- Requires in your project: `chrono` dependency and `clickhouse` feature `chrono`
72+
- `clickhouse = { version = "…", features = ["chrono"] }`
73+
74+
Select mode via `--temporal {numeric|time|chrono}`.
75+
5276
## Examples
5377

5478
See [snapshots](tests/snapshots).

src/codegen.rs

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{bail, Context, Result};
44
use heck::{ToSnakeCase, ToUpperCamelCase};
55

66
use crate::{
7-
options::Options,
7+
options::{Options, Temporal},
88
schema::{Column, SqlType, Table},
99
};
1010

@@ -82,18 +82,46 @@ fn make_attribute(column: &Column, options: &Options) -> Option<String> {
8282
return None;
8383
}
8484

85-
let attr = match column.type_ {
86-
SqlType::UUID => r#" #[serde(with = "::clickhouse::serde::uuid")]"#,
87-
SqlType::IPv4 => r#" #[serde(with = "::clickhouse::serde::ipv4")]"#,
88-
SqlType::Nullable(ref inner) => match inner.as_ref() {
89-
SqlType::UUID => r#" #[serde(with = "::clickhouse::serde::uuid::option")]"#,
90-
SqlType::IPv4 => r#" #[serde(with = "::clickhouse::serde::ipv4::option")]"#,
91-
_ => return None,
92-
},
93-
_ => return None,
85+
let (inner, is_option) = match &column.type_ {
86+
SqlType::Nullable(inner) => (inner.as_ref(), true),
87+
_ => (&column.type_, false),
9488
};
9589

96-
Some(attr.into())
90+
let base = match inner {
91+
SqlType::UUID => Some("::clickhouse::serde::uuid".into()),
92+
SqlType::IPv4 => Some("::clickhouse::serde::ipv4".into()),
93+
SqlType::Date => temporal_path(options.temporal, "date", None),
94+
SqlType::Date32 => temporal_path(options.temporal, "date32", None),
95+
SqlType::DateTime(_) => temporal_path(options.temporal, "datetime", None),
96+
SqlType::DateTime64(prec, _) => temporal_path(options.temporal, "datetime64", Some(prec)),
97+
SqlType::Time => temporal_path(options.temporal, "time", None),
98+
SqlType::Time64(prec) => temporal_path(options.temporal, "time64", Some(prec)),
99+
_ => None,
100+
}?;
101+
102+
Some(format!(
103+
" #[serde(with = \"{}{}\")]",
104+
base,
105+
if is_option { "::option" } else { "" }
106+
))
107+
}
108+
109+
fn temporal_path(temporal_mode: Temporal, ty: &str, prec: Option<&u32>) -> Option<String> {
110+
let temporal_base = match temporal_mode {
111+
Temporal::Time => Some("::clickhouse::serde::time::"),
112+
Temporal::Chrono => Some("::clickhouse::serde::chrono::"),
113+
Temporal::Numeric => None,
114+
}?;
115+
let prec = match prec {
116+
None => Some(""),
117+
Some(&0) => Some("::secs"),
118+
Some(&3) => Some("::millis"),
119+
Some(&6) => Some("::micros"),
120+
Some(&9) => Some("::nanos"),
121+
Some(_) => None,
122+
}?;
123+
124+
Some(format!("{}{}{}", temporal_base, ty, prec))
97125
}
98126

99127
fn make_type(column: &Column, options: &Options) -> Result<String> {
@@ -105,6 +133,20 @@ fn do_make_type(name: &str, sql_type: &SqlType, options: &Options) -> Result<Str
105133
return Ok(type_.into());
106134
}
107135

136+
// validate precision for chrono and time modes.
137+
if matches!(options.temporal, Temporal::Time | Temporal::Chrono) {
138+
if let SqlType::DateTime64(p, _) | SqlType::Time64(p) = sql_type {
139+
if !matches!(p, 0 | 3 | 6 | 9) {
140+
bail!(
141+
"Unsupported precision {} for {:?} in {:?} mode; supported: 0, 3, 6, 9",
142+
p,
143+
sql_type,
144+
options.temporal
145+
);
146+
}
147+
}
148+
}
149+
108150
Ok(match sql_type {
109151
SqlType::UInt8 => "u8".into(),
110152
SqlType::UInt16 => "u16".into(),
@@ -122,9 +164,38 @@ fn do_make_type(name: &str, sql_type: &SqlType, options: &Options) -> Result<Str
122164
// SqlType::FixedString(size) => todo!(),
123165
SqlType::Float32 => "f32".into(),
124166
SqlType::Float64 => "f64".into(),
125-
// SqlType::Date => todo!(),
126-
// SqlType::DateTime(_) => todo!(),
127-
// SqlType::DateTime64(_, _) => todo!(),
167+
SqlType::Date
168+
| SqlType::Date32
169+
| SqlType::DateTime(_)
170+
| SqlType::DateTime64(_, _)
171+
| SqlType::Time
172+
| SqlType::Time64(_) => match options.temporal {
173+
Temporal::Numeric => match sql_type {
174+
SqlType::Date => "u16".into(),
175+
SqlType::Date32 => "i32".into(),
176+
SqlType::DateTime(_) => "u32".into(),
177+
SqlType::DateTime64(_, _) => "i64".into(),
178+
SqlType::Time => "i32".into(),
179+
SqlType::Time64(_) => "i64".into(),
180+
_ => unreachable!(),
181+
},
182+
Temporal::Time => match sql_type {
183+
SqlType::Date | SqlType::Date32 => "::time::Date".into(),
184+
SqlType::DateTime(_) => "::time::OffsetDateTime".into(),
185+
SqlType::DateTime64(..) => "::time::OffsetDateTime".into(),
186+
SqlType::Time => "::time::Duration".into(),
187+
SqlType::Time64(_) => "::time::Duration".into(),
188+
_ => unreachable!(),
189+
},
190+
Temporal::Chrono => match sql_type {
191+
SqlType::Date | SqlType::Date32 => "::chrono::NaiveDate".into(),
192+
SqlType::DateTime(_) => "::chrono::DateTime<::chrono::Utc>".into(),
193+
SqlType::DateTime64(..) => "::chrono::DateTime<::chrono::Utc>".into(),
194+
SqlType::Time => "::chrono::Duration".into(),
195+
SqlType::Time64(_) => "::chrono::Duration".into(),
196+
_ => unreachable!(),
197+
},
198+
},
128199
SqlType::IPv4 => "::std::net::Ipv4Addr".into(),
129200
SqlType::IPv6 => "::std::net::Ipv6Addr".into(),
130201
SqlType::UUID => "::uuid::Uuid".into(),

src/miner.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ pub fn parse_type(raw: &str) -> Result<SqlType> {
112112
"Float32" => SqlType::Float32,
113113
"Float64" => SqlType::Float64,
114114
"Date" => SqlType::Date,
115+
"Date32" => SqlType::Date32,
115116
"DateTime" => SqlType::DateTime(None),
117+
"Time" => SqlType::Time,
116118
"IPv4" => SqlType::IPv4,
117119
"IPv6" => SqlType::IPv6,
118120
"UUID" => SqlType::UUID,
@@ -134,6 +136,11 @@ pub fn parse_type(raw: &str) -> Result<SqlType> {
134136
let prec = prec.trim().parse().context("invalid precision")?;
135137
SqlType::DateTime64(prec, tz.map(str::trim).map(Into::into))
136138
}
139+
// Time64(prec)
140+
else if let Some(inner) = extract_inner(raw, "Time64") {
141+
let prec = inner.trim().parse().context("invalid precision")?;
142+
SqlType::Time64(prec)
143+
}
137144
// Enum8('K' = v, 'K2' = v2)
138145
else if let Some(inner) = extract_inner(raw, "Enum8") {
139146
SqlType::Enum8(parse_kv_list(inner).context("invalid enum")?)

src/options.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ pub struct Options {
4646
/// Add `#[derive(<trait>)]` to the generated types.
4747
#[structopt(long = "derive", number_of_values = 1, name = "trait")]
4848
pub derives: Vec<String>,
49+
50+
/// Temporal mapping mode
51+
#[structopt(long = "temporal", default_value = "numeric", possible_values=&["numeric", "time", "chrono"])]
52+
pub temporal: Temporal,
4953
}
5054

5155
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -76,6 +80,35 @@ fn parse_override(s: &str) -> Result<Override> {
7680
})
7781
}
7882

83+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84+
pub enum Temporal {
85+
Numeric,
86+
Time,
87+
Chrono,
88+
}
89+
90+
impl std::str::FromStr for Temporal {
91+
type Err = String;
92+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
93+
match s {
94+
"numeric" => Ok(Temporal::Numeric),
95+
"time" => Ok(Temporal::Time),
96+
"chrono" => Ok(Temporal::Chrono),
97+
_ => Err("invalid temporal".into()),
98+
}
99+
}
100+
}
101+
102+
impl std::fmt::Display for Temporal {
103+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104+
match self {
105+
Temporal::Numeric => f.write_str("numeric"),
106+
Temporal::Time => f.write_str("time"),
107+
Temporal::Chrono => f.write_str("chrono"),
108+
}
109+
}
110+
}
111+
79112
impl Options {
80113
pub fn format(&self) -> String {
81114
let mut s = String::new();
@@ -140,6 +173,10 @@ impl Options {
140173
let _ = writeln!(&mut s, " -I '{}' \\", i);
141174
}
142175

176+
if self.temporal != Temporal::Numeric {
177+
let _ = write!(&mut s, " --temporal {}", self.temporal);
178+
}
179+
143180
s.trim_end_matches(|c| ['\\', ' ', '\n'].contains(&c))
144181
.into()
145182
}

src/schema.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ pub enum SqlType {
3131
Float32,
3232
Float64,
3333
Date,
34+
Date32,
3435
DateTime(Option<String>),
3536
DateTime64(u32, Option<String>),
37+
Time,
38+
Time64(u32),
3639
IPv4,
3740
IPv6,
3841
UUID,

0 commit comments

Comments
 (0)