Skip to content

Commit 1465896

Browse files
authored
Merge branch 'main' into feat/qualified-identifier
2 parents c9636c3 + 92926fd commit 1465896

File tree

28 files changed

+1948
-108
lines changed

28 files changed

+1948
-108
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
- run: cargo update -p zerofrom --precise 0.1.5 # 0.1.6 requires rustc 1.81
4242
- run: cargo update -p lz4_flex --precise 0.11.3 # 0.11.4 requires rustc 1.81
4343
- run: cargo update -p url --precise 2.5.0 # 2.5.4 requires rustc 1.82
44+
- run: cargo update -p time --precise 0.3.41 # 0.3.43 requires rustc 1.81
45+
- run: cargo update -p time-core --precise 0.1.4 # 0.1.6 requires rustc 1.81
46+
- run: cargo update -p deranged --precise 0.4.0 # 0.5.x requires rustc 1.81
4447
- run: cargo build
4548
- run: cargo build --no-default-features
4649
- run: cargo build --features uuid,time,chrono
@@ -109,7 +112,23 @@ jobs:
109112
- name: Run tests with all features
110113
run: cargo llvm-cov test --workspace --no-report --all-features
111114

115+
- name: Check access to GitHub secrets
116+
id: check-secrets-access
117+
run: |
118+
if [[ "${{ github.actor }}" == "loyd" ]]; then
119+
echo "has-access=true" >> $GITHUB_OUTPUT
120+
echo "Paul Loyd is our VIP"
121+
elif gh api orgs/ClickHouse/members/${{ github.actor }} --silent; then
122+
echo "has-access=true" >> $GITHUB_OUTPUT
123+
else
124+
echo "has-access=false" >> $GITHUB_OUTPUT
125+
fi
126+
env:
127+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128+
continue-on-error: true
129+
112130
- name: Run tests with ClickHouse Cloud
131+
if: steps.check-secrets-access.outputs.has-access == 'true'
113132
env:
114133
CLICKHOUSE_TEST_ENVIRONMENT: cloud
115134
CLICKHOUSE_CLOUD_HOST: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_HOST_SMT }}
@@ -122,9 +141,12 @@ jobs:
122141
cargo llvm-cov test https_errors --no-report -- --nocapture
123142
124143
- name: Generate code coverage
144+
if: steps.check-secrets-access.outputs.has-access == 'true'
125145
run: cargo llvm-cov report --codecov --output-path codecov.json
146+
126147
- name: Upload coverage to Codecov
127148
uses: codecov/codecov-action@v5
149+
if: steps.check-secrets-access.outputs.has-access == 'true'
128150
with:
129151
files: codecov.json
130152
token: ${{ secrets.CODECOV_TOKEN }}

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ required-features = ["time", "uuid", "chrono"]
7878
name = "data_types_variant"
7979
required-features = ["time"]
8080

81+
[[example]]
82+
name = "time_types_example"
83+
required-features = ["time", "chrono"]
84+
8185
[profile.release]
8286
debug = true
8387

@@ -133,8 +137,8 @@ hyper-rustls = { version = "0.27.3", default-features = false, features = [
133137
"tls12",
134138
], optional = true }
135139
url = "2.1.1"
136-
futures = "0.3.5"
137-
futures-channel = "0.3.30"
140+
futures-util = { version = "0.3.5", default-features = false, features = ["sink", "io"] }
141+
futures-channel = { version = "0.3.30", features = ["sink"] }
138142
static_assertions = "1.1"
139143
sealed = "0.6"
140144
lz4_flex = { version = "0.11.3", default-features = false, features = [

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,38 @@ How to choose between all these features? Here are some considerations:
400400
}
401401
402402
403+
```
404+
</details>
405+
* `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.
406+
* [`time:Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is is supported by using `serde::time::*`, requiring the `time` feature.
407+
* [`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
408+
<details>
409+
<summary>Example</summary>
410+
411+
```rust,ignore
412+
#[derive(Row, Serialize, Deserialize)]
413+
struct MyRow {
414+
#[serde(with = "clickhouse::serde::chrono::time64::secs")]
415+
t0: chrono::Duration,
416+
#[serde(with = "clickhouse::serde::chrono::time64::secs::option")]
417+
t0_opt: Option<chrono::Duration>,
418+
}
419+
420+
```
421+
</details>
422+
* `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.
423+
* [`time:Duration`](https://docs.rs/time/latest/time/struct.Duration.html) is is supported by using `serde::time::*`, requiring the `time` feature.
424+
* [`chrono::Duration`](https://docs.rs/chrono/latest/chrono/type.Duration.html) is supported by using `serde::chrono::*`, requiring the `chrono` feature
425+
<details>
426+
<summary>Example</summary>
427+
428+
```rust,ignore
429+
#[derive(Row, Serialize, Deserialize)]
430+
struct MyRow {
431+
#[serde(with = "clickhouse::serde::time::time")]
432+
t0: Time,
433+
}
434+
403435
```
404436
</details>
405437
* `Tuple(A, B, ...)` maps to/from `(A, B, ...)` or a newtype around it.

benches/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{
1212

1313
use bytes::Bytes;
1414
use clickhouse::error::Result;
15-
use futures::stream::StreamExt;
15+
use futures_util::stream::StreamExt;
1616
use http_body_util::BodyExt;
1717
use hyper::{
1818
body::{Body, Incoming},

benches/mocked_select.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clickhouse::{
55
};
66
use clickhouse_types::{Column, DataTypeNode};
77
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
8-
use futures::stream::{self, StreamExt as _};
8+
use futures_util::stream::{self, StreamExt as _};
99
use http_body_util::StreamBody;
1010
use hyper::{
1111
body::{Body, Frame, Incoming},

benches/select_nyc_taxi_data.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::common_select::{
44
do_select_bench, print_header, print_results, BenchmarkRow, WithAccessType, WithId,
55
};
6-
use clickhouse::{Compression, Row};
6+
use clickhouse::{Client, Compression, Row};
77
use serde::Deserialize;
88
use serde_repr::Deserialize_repr;
99
use time::OffsetDateTime;
@@ -74,6 +74,86 @@ struct TripSmallMapAccess {
7474
impl_benchmark_row!(TripSmallSeqAccess, trip_id, "seq");
7575
impl_benchmark_row!(TripSmallMapAccess, trip_id, "map");
7676

77+
// See https://clickhouse.com/docs/getting-started/example-datasets/nyc-taxi
78+
async fn prepare_data() {
79+
let client = Client::default().with_url("http://localhost:8123");
80+
81+
client
82+
.query("CREATE DATABASE IF NOT EXISTS nyc_taxi")
83+
.execute()
84+
.await
85+
.unwrap();
86+
client
87+
.query(
88+
r#"
89+
CREATE TABLE IF NOT EXISTS nyc_taxi.trips_small (
90+
trip_id UInt32,
91+
pickup_datetime DateTime,
92+
dropoff_datetime DateTime,
93+
pickup_longitude Nullable(Float64),
94+
pickup_latitude Nullable(Float64),
95+
dropoff_longitude Nullable(Float64),
96+
dropoff_latitude Nullable(Float64),
97+
passenger_count UInt8,
98+
trip_distance Float32,
99+
fare_amount Float32,
100+
extra Float32,
101+
tip_amount Float32,
102+
tolls_amount Float32,
103+
total_amount Float32,
104+
payment_type Enum('CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4, 'UNK' = 5),
105+
pickup_ntaname LowCardinality(String),
106+
dropoff_ntaname LowCardinality(String)
107+
)
108+
ENGINE = MergeTree
109+
PRIMARY KEY (pickup_datetime, dropoff_datetime)
110+
"#,
111+
)
112+
.execute()
113+
.await
114+
.unwrap();
115+
116+
let len = client
117+
.query("SELECT count() FROM nyc_taxi.trips_small")
118+
.fetch_one::<usize>()
119+
.await
120+
.unwrap();
121+
122+
if len == 0 {
123+
client
124+
.query(
125+
"
126+
INSERT INTO nyc_taxi.trips_small
127+
SELECT
128+
trip_id,
129+
pickup_datetime,
130+
dropoff_datetime,
131+
pickup_longitude,
132+
pickup_latitude,
133+
dropoff_longitude,
134+
dropoff_latitude,
135+
passenger_count,
136+
trip_distance,
137+
fare_amount,
138+
extra,
139+
tip_amount,
140+
tolls_amount,
141+
total_amount,
142+
payment_type,
143+
pickup_ntaname,
144+
dropoff_ntaname
145+
FROM gcs(
146+
'https://storage.googleapis.com/clickhouse-public-datasets/nyc-taxi/trips_{0..2}.gz',
147+
'TabSeparatedWithNames'
148+
);
149+
",
150+
)
151+
.execute()
152+
.await
153+
.unwrap();
154+
}
155+
}
156+
77157
async fn bench<T: BenchmarkRow>(compression: Compression, validation: bool) {
78158
let stats = do_select_bench::<T>(
79159
"SELECT * FROM nyc_taxi.trips_small ORDER BY trip_id DESC",
@@ -87,6 +167,7 @@ async fn bench<T: BenchmarkRow>(compression: Compression, validation: bool) {
87167

88168
#[tokio::main]
89169
async fn main() {
170+
prepare_data().await;
90171
print_header(Some(" access"));
91172
bench::<TripSmallSeqAccess>(Compression::None, false).await;
92173
bench::<TripSmallSeqAccess>(Compression::None, true).await;

codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
coverage:
2+
range: 60..90
3+
round: down
4+
precision: 2

examples/mock.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ async fn main() {
5454
make_insert(&client, &list).await.unwrap();
5555
let rows: Vec<SomeRow> = recording.collect().await;
5656
assert_eq!(rows, list);
57+
58+
// How to test unsuccessful INSERT.
59+
mock.add(test::handlers::exception(209));
60+
let reason = make_insert(&client, &list).await;
61+
assert_eq!(format!("{reason:?}"), r#"Err(BadResponse("Code: 209"))"#);
5762
}

examples/stream_into_file.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ async fn cursor_next(filename: &str) {
4545
}
4646
}
4747

48-
// Pattern 3: use the `futures::(Try)StreamExt` traits.
48+
// Pattern 3: use the `futures_util::(Try)StreamExt` traits.
4949
#[cfg(feature = "futures03")]
5050
async fn futures03_stream(filename: &str) {
51-
use futures::TryStreamExt;
51+
use futures_util::TryStreamExt;
5252

5353
let mut cursor = query(NUMBERS);
5454
let mut file = File::create(filename).await.unwrap();

examples/time_types_example.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use chrono::Duration;
2+
use clickhouse::Client;
3+
use serde::{Deserialize, Serialize};
4+
5+
#[derive(Debug, Serialize, Deserialize, clickhouse::Row)]
6+
struct TimeExample {
7+
#[serde(with = "clickhouse::serde::time::time")]
8+
time_field: time::Duration,
9+
10+
#[serde(with = "clickhouse::serde::time::time::option")]
11+
time_optional: Option<time::Duration>,
12+
13+
#[serde(with = "clickhouse::serde::time::time64::secs")]
14+
time64_seconds: time::Duration,
15+
16+
#[serde(with = "clickhouse::serde::time::time64::millis")]
17+
time64_millis: time::Duration,
18+
19+
#[serde(with = "clickhouse::serde::time::time64::micros")]
20+
time64_micros: time::Duration,
21+
22+
#[serde(with = "clickhouse::serde::time::time64::nanos")]
23+
time64_nanos: time::Duration,
24+
}
25+
26+
#[derive(Debug, Serialize, Deserialize, clickhouse::Row)]
27+
struct TimeExampleChrono {
28+
#[serde(with = "clickhouse::serde::chrono::time")]
29+
time_field: Duration,
30+
31+
#[serde(with = "clickhouse::serde::chrono::time::option")]
32+
time_optional: Option<Duration>,
33+
34+
#[serde(with = "clickhouse::serde::chrono::time64::secs")]
35+
time64_seconds: Duration,
36+
37+
#[serde(with = "clickhouse::serde::chrono::time64::millis")]
38+
time64_millis: Duration,
39+
40+
#[serde(with = "clickhouse::serde::chrono::time64::micros")]
41+
time64_micros: Duration,
42+
43+
#[serde(with = "clickhouse::serde::chrono::time64::nanos")]
44+
time64_nanos: Duration,
45+
}
46+
47+
#[tokio::main]
48+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
49+
let client = Client::default();
50+
51+
let create_table_sql = r#"
52+
CREATE TABLE IF NOT EXISTS time_example (
53+
time_field Time,
54+
time_optional Nullable(Time),
55+
time64_seconds Time64(0),
56+
time64_millis Time64(3),
57+
time64_micros Time64(6),
58+
time64_nanos Time64(9)
59+
) ENGINE = MergeTree()
60+
ORDER BY time_field
61+
"#;
62+
63+
client.query(create_table_sql).execute().await?;
64+
65+
// Insert data using time crate
66+
let time_example = TimeExample {
67+
time_field: time::Duration::seconds(12 * 3600 + 34 * 60 + 56),
68+
time_optional: Some(time::Duration::seconds(23 * 3600 + 59 * 60 + 59)),
69+
time64_seconds: time::Duration::seconds(3600 + 2 * 60 + 3),
70+
time64_millis: time::Duration::seconds(4 * 3600 + 5 * 60 + 6)
71+
+ time::Duration::milliseconds(123),
72+
time64_micros: time::Duration::seconds(7 * 3600 + 8 * 60 + 9)
73+
+ time::Duration::microseconds(456_789),
74+
time64_nanos: time::Duration::seconds(10 * 3600 + 11 * 60 + 12)
75+
+ time::Duration::nanoseconds(123_456_789),
76+
};
77+
78+
let mut insert = client.insert::<TimeExample>("time_example")?;
79+
insert.write(&time_example).await?;
80+
insert.end().await?;
81+
82+
// Insert data using chrono crate
83+
let time_example_chrono = TimeExampleChrono {
84+
time_field: Duration::seconds(13 * 3600 + 45 * 60),
85+
time_optional: Some(Duration::seconds(1)),
86+
time64_seconds: Duration::seconds(2 * 3600 + 3 * 60 + 4),
87+
time64_millis: Duration::seconds(5 * 3600 + 6 * 60 + 7) + Duration::milliseconds(456),
88+
time64_micros: Duration::seconds(8 * 3600 + 9 * 60 + 10) + Duration::microseconds(789_012),
89+
time64_nanos: Duration::seconds(11 * 3600 + 12 * 60 + 13)
90+
+ Duration::nanoseconds(987_654_321),
91+
};
92+
93+
let mut insert = client.insert::<TimeExampleChrono>("time_example")?;
94+
insert.write(&time_example_chrono).await?;
95+
insert.end().await?;
96+
97+
// Insert chrono edge cases
98+
let edge_cases = vec![
99+
Duration::seconds(-999 * 3600 - 59 * 60 - 59), // Min
100+
Duration::zero(), // Midnight
101+
Duration::seconds(999 * 3600 + 59 * 60 + 59), // Max
102+
];
103+
104+
for (i, edge) in edge_cases.into_iter().enumerate() {
105+
let data = TimeExampleChrono {
106+
time_field: edge,
107+
time_optional: Some(edge),
108+
time64_seconds: edge,
109+
time64_millis: edge,
110+
time64_micros: edge,
111+
time64_nanos: edge,
112+
};
113+
let mut insert = client.insert::<TimeExampleChrono>("time_example")?;
114+
insert.write(&data).await?;
115+
insert.end().await?;
116+
println!("Inserted edge case #{i}: {edge:?}");
117+
}
118+
119+
// Query the data
120+
let rows: Vec<TimeExample> = client
121+
.query("SELECT * FROM time_example ORDER BY time_field")
122+
.fetch_all()
123+
.await?;
124+
for time_example in rows {
125+
println!("Time example: {time_example:?}");
126+
}
127+
128+
println!("Time and Time64 types example completed successfully!");
129+
130+
Ok(())
131+
}

0 commit comments

Comments
 (0)