Skip to content

Commit 805acf0

Browse files
committed
Complete rawQuery implementation with comprehensive tests
- Add Query::raw() method for executing SQL queries with literal question marks - Implement comprehensive test suite covering various question mark scenarios - Fix fetch() method to handle raw queries without field binding - Support struct-based fetching with raw queries - Add error handling for bind function
1 parent 54a80a0 commit 805acf0

File tree

9 files changed

+650
-2
lines changed

9 files changed

+650
-2
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,40 @@ client.query("DROP TABLE IF EXISTS some").execute().await?;
188188
<details>
189189
<summary>
190190

191+
### Raw queries (without parameter binding)
192+
193+
</summary>
194+
195+
```rust,ignore
196+
use serde::Deserialize;
197+
use clickhouse::Row;
198+
199+
#[derive(Row, Deserialize)]
200+
struct MyRow {
201+
no: u32,
202+
name: String,
203+
}
204+
205+
// Raw queries don't support parameter binding but handle literal question marks
206+
let rows = client
207+
.query_raw("SELECT no, name FROM some WHERE name LIKE 'test%?' ORDER BY no")
208+
.fetch_all::<MyRow>()
209+
.await?;
210+
```
211+
212+
* Raw queries use an **odd/even question mark strategy**:
213+
- Consecutive odd number of `?` (1, 3, 5, ...) → add one more `?`
214+
- Consecutive even number of `?` (2, 4, 6, ...) → keep as is
215+
* Single `?` becomes `??` (literal `?` in ClickHouse)
216+
* Double `??` stays `??` (already escaped)
217+
* **No parameter binding**: `bind()` and `param()` methods will panic
218+
* Useful for pre-built SQL strings, legacy code migration, or when SQL contains literal question marks
219+
* ⚠️ **Security warning**: Since no parameter binding is available, ensure user input is properly sanitized
220+
221+
</details>
222+
<details>
223+
<summary>
224+
191225
### Live views
192226

193227
</summary>

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ If something is missing, or you found a mistake in one of these examples, please
99
### General usage
1010

1111
- [usage.rs](usage.rs) - creating tables, executing other DDLs, inserting the data, and selecting it back. Additionally, it covers `WATCH` queries. Optional cargo features: `inserter`, `watch`.
12+
- [query_raw.rs](query_raw.rs) - raw queries without parameter binding, with question mark escaping.
1213
- [mock.rs](mock.rs) - writing tests with `mock` feature. Cargo features: requires `test-util`.
1314
- [inserter.rs](inserter.rs) - using the client-side batching via the `inserter` feature. Cargo features: requires `inserter`.
1415
- [async_insert.rs](async_insert.rs) - using the server-side batching via the [asynchronous inserts](https://clickhouse.com/docs/en/optimize/asynchronous-inserts) ClickHouse feature

examples/query_raw.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Raw queries without parameter binding, with question mark escaping.
2+
3+
use clickhouse::{Client, Row};
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Row, Debug, Serialize, Deserialize)]
7+
struct TestData {
8+
id: u32,
9+
pattern: String,
10+
}
11+
12+
#[tokio::main]
13+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
14+
let client = Client::default()
15+
.with_url("http://localhost:8123")
16+
.with_database("default");
17+
18+
// Create test table
19+
client
20+
.query_raw("CREATE TABLE IF NOT EXISTS query_raw_demo (id UInt32, pattern String) ENGINE = MergeTree ORDER BY id")
21+
.execute()
22+
.await?;
23+
24+
// Insert data with question marks (they will be escaped)
25+
client
26+
.query_raw(
27+
"INSERT INTO query_raw_demo VALUES (1, 'single?'), (2, 'double??'), (3, 'triple???')",
28+
)
29+
.execute()
30+
.await?;
31+
32+
// Query data back
33+
let rows = client
34+
.query_raw("SELECT id, pattern FROM query_raw_demo ORDER BY id")
35+
.fetch_all::<TestData>()
36+
.await?;
37+
38+
println!("Data retrieved:");
39+
for row in &rows {
40+
println!(" ID {}: '{}'", row.id, row.pattern);
41+
}
42+
43+
// Show SQL transformation
44+
let demo_query = client.query_raw("SELECT * WHERE pattern = 'test?'");
45+
println!("\nSQL transformation:");
46+
println!(" Original: SELECT * WHERE pattern = 'test?'");
47+
println!(" Sent to server: {}", demo_query.sql_display());
48+
49+
// Note: Parameter binding would panic
50+
// client.query_raw("SELECT * WHERE id = ?").bind(42); // ❌ Panics!
51+
52+
// Clean up
53+
client
54+
.query_raw("DROP TABLE query_raw_demo")
55+
.execute()
56+
.await?;
57+
58+
println!("\n✅ Example completed!");
59+
60+
Ok(())
61+
}

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ impl Client {
313313
query::Query::new(self, query)
314314
}
315315

316+
/// Starts a new SELECT/DDL query that will be used as-is without any processing.
317+
pub fn query_raw(&self, query: &str) -> query::Query {
318+
query::Query::raw(self, query)
319+
}
320+
316321
/// Starts a new WATCH query.
317322
///
318323
/// The `query` can be either the table name or a SELECT query.

src/query.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ use crate::headers::with_authentication;
2323
pub struct Query {
2424
client: Client,
2525
sql: SqlBuilder,
26+
raw: bool,
2627
}
2728

2829
impl Query {
2930
pub(crate) fn new(client: &Client, template: &str) -> Self {
3031
Self {
3132
client: client.clone(),
3233
sql: SqlBuilder::new(template),
34+
raw: false,
35+
}
36+
}
37+
/// Creates a new query that will be used as-is without any processing.
38+
pub(crate) fn raw(client: &Client, query: &str) -> Self {
39+
Self {
40+
client: client.clone(),
41+
sql: SqlBuilder::raw(query),
42+
raw: true,
3343
}
3444
}
3545

@@ -53,6 +63,9 @@ impl Query {
5363
/// [`Identifier`]: crate::sql::Identifier
5464
#[track_caller]
5565
pub fn bind(mut self, value: impl Bind) -> Self {
66+
if self.raw {
67+
panic!("bind() cannot be used with raw queries");
68+
}
5669
self.sql.bind_arg(value);
5770
self
5871
}
@@ -84,9 +97,10 @@ impl Query {
8497
/// # Ok(()) }
8598
/// ```
8699
pub fn fetch<T: Row>(mut self) -> Result<RowCursor<T>> {
87-
self.sql.bind_fields::<T>();
100+
if !self.raw {
101+
self.sql.bind_fields::<T>();
102+
}
88103
self.sql.set_output_format("RowBinary");
89-
90104
let response = self.do_execute(true)?;
91105
Ok(RowCursor::new(response))
92106
}

src/query_raw.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//! Raw SQL queries without parameter binding.
2+
//!
3+
//! [`QueryRaw`] executes SQL as-is without parameter binding support.
4+
//! Question marks are escaped using an odd/even strategy:
5+
//! - `?` → `??` (odd count, add one more)
6+
//! - `??` → `??` (even count, unchanged)
7+
use crate::{error::Result, row::Row, sql::Bind, Client};
8+
use serde::Serialize;
9+
10+
pub use crate::cursors::{BytesCursor, RowCursor};
11+
use crate::query::Query;
12+
13+
#[must_use]
14+
#[derive(Clone)]
15+
pub struct QueryRaw {
16+
query: Query,
17+
}
18+
19+
impl QueryRaw {
20+
pub(crate) fn new(client: &Client, template: &str) -> Self {
21+
// Raw queries apply odd/even question mark strategy:
22+
// - Consecutive odd number of ? (1,3,5...) -> add one more ?
23+
// - Consecutive even number of ? (2,4,6...) -> keep as is
24+
Self {
25+
query: Query::new_raw(client, template),
26+
}
27+
}
28+
29+
/// Display the SQL query with question mark escaping applied.
30+
pub fn sql_display(&self) -> &impl std::fmt::Display {
31+
self.query.sql_display()
32+
}
33+
34+
/// Attempts to bind a value to the query.
35+
///
36+
/// # Panics
37+
/// Always panics - raw queries don't support parameter binding.
38+
#[track_caller]
39+
pub fn bind(self, _value: impl Bind) -> Self {
40+
panic!("cannot bind parameters to raw query - use regular query() for parameter binding");
41+
}
42+
43+
/// Attempts to set server-side parameters for the query.
44+
///
45+
/// # Panics
46+
/// Always panics - raw queries don't support parameter binding.
47+
#[track_caller]
48+
pub fn param(self, _name: &str, _value: impl Serialize) -> Self {
49+
panic!("cannot set parameters on raw query - use regular query() for parameter binding");
50+
}
51+
52+
/// Executes the query.
53+
pub async fn execute(self) -> Result<()> {
54+
self.query.execute().await
55+
}
56+
57+
/// Executes the query, returning a [`RowCursor`] to obtain results.
58+
pub fn fetch<T: Row>(self) -> Result<RowCursor<T>> {
59+
self.query.fetch()
60+
}
61+
62+
/// Executes the query and returns just a single row.
63+
pub async fn fetch_one<T>(self) -> Result<T>
64+
where
65+
T: Row + for<'b> serde::Deserialize<'b>,
66+
{
67+
self.query.fetch_one().await
68+
}
69+
70+
/// Executes the query and returns at most one row.
71+
pub async fn fetch_optional<T>(self) -> Result<Option<T>>
72+
where
73+
T: Row + for<'b> serde::Deserialize<'b>,
74+
{
75+
self.query.fetch_optional().await
76+
}
77+
78+
/// Executes the query and returns all the generated results,
79+
/// collected into a Vec.
80+
pub async fn fetch_all<T>(self) -> Result<Vec<T>>
81+
where
82+
T: Row + for<'b> serde::Deserialize<'b>,
83+
{
84+
self.query.fetch_all().await
85+
}
86+
87+
/// Executes the query, returning a [`BytesCursor`] to obtain results as raw
88+
/// bytes containing data in the [provided format].
89+
pub fn fetch_bytes(self, format: impl Into<String>) -> Result<BytesCursor> {
90+
self.query.fetch_bytes(format)
91+
}
92+
93+
/// Similar to [`Client::with_option`], but for this particular query only.
94+
pub fn with_option(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
95+
self.query = self.query.with_option(name, value);
96+
self
97+
}
98+
}
99+
100+
impl std::fmt::Display for QueryRaw {
101+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102+
write!(f, "{}", self.query.sql_display())
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use crate::Client;
109+
use serde::{Deserialize, Serialize};
110+
111+
// XXX: need for `derive(Row)`. Provide `row(crate = ..)` instead.
112+
use crate as clickhouse;
113+
use clickhouse_derive::Row;
114+
115+
#[derive(Row, Debug, Serialize, Deserialize)]
116+
struct TestRow {
117+
id: u32,
118+
name: String,
119+
}
120+
121+
#[test]
122+
fn test_question_mark_escaping() {
123+
let client = Client::default();
124+
let query = client.query_raw("SELECT * FROM test WHERE name LIKE 'test%?'");
125+
// Single ? (odd) becomes ??
126+
assert_eq!(
127+
query.sql_display().to_string(),
128+
"SELECT * FROM test WHERE name LIKE 'test%??'"
129+
);
130+
}
131+
132+
#[test]
133+
fn test_multiple_question_marks() {
134+
let client = Client::default();
135+
let query = client.query_raw("SELECT * FROM test WHERE a = ? AND b = ?");
136+
// Each single ? (odd) becomes ??
137+
assert_eq!(
138+
query.sql_display().to_string(),
139+
"SELECT * FROM test WHERE a = ?? AND b = ??"
140+
);
141+
}
142+
143+
#[test]
144+
fn test_already_escaped_question_marks() {
145+
let client = Client::default();
146+
let query = client.query_raw("SELECT * FROM test WHERE name LIKE 'test%??'");
147+
// Double ?? (even) stays ??
148+
assert_eq!(
149+
query.sql_display().to_string(),
150+
"SELECT * FROM test WHERE name LIKE 'test%??'"
151+
);
152+
}
153+
154+
#[test]
155+
#[should_panic(expected = "cannot bind parameters to raw query")]
156+
fn test_bind_panics() {
157+
let client = Client::default();
158+
let query = client.query_raw("SELECT * FROM test WHERE id = ?");
159+
let _ = query.bind(42); // This should panic
160+
}
161+
162+
#[test]
163+
#[should_panic(expected = "cannot set parameters on raw query")]
164+
fn test_param_panics() {
165+
let client = Client::default();
166+
let query = client.query_raw("SELECT * FROM test WHERE id = {val: Int32}");
167+
let _ = query.param("val", 42); // This should panic
168+
}
169+
170+
#[test]
171+
fn test_with_option() {
172+
let client = Client::default();
173+
let query = client
174+
.query_raw("SELECT * FROM test")
175+
.with_option("max_execution_time", "60");
176+
assert_eq!(query.sql_display().to_string(), "SELECT * FROM test");
177+
}
178+
179+
#[test]
180+
fn test_display_implementation() {
181+
let client = Client::default();
182+
let query = client.query_raw("SELECT * FROM test WHERE name LIKE 'test%?'");
183+
let display = format!("{}", query);
184+
assert_eq!(display, "SELECT * FROM test WHERE name LIKE 'test%??'");
185+
}
186+
187+
#[test]
188+
fn test_complex_escaping() {
189+
let client = Client::default();
190+
let query = client.query_raw(
191+
"SELECT '?', '??', '???', 'a?b', 'a??b', 'a???b' FROM test WHERE pattern LIKE '%?%'",
192+
);
193+
// ? -> ??, ?? -> ??, ??? -> ????, a?b -> a??b, a??b -> a??b, a???b -> a????b, %?% -> %??%
194+
let expected = "SELECT '??', '??', '????', 'a??b', 'a??b', 'a????b' FROM test WHERE pattern LIKE '%??%'";
195+
assert_eq!(query.sql_display().to_string(), expected);
196+
}
197+
198+
#[test]
199+
fn test_mixed_patterns() {
200+
let client = Client::default();
201+
// Test odd/even strategy: ? -> ??, ?? -> ??, ??? -> ????, ???? -> ????
202+
let query = client.query_raw("SELECT '?', '??', '???', '????' FROM test");
203+
let expected = "SELECT '??', '??', '????', '????' FROM test";
204+
assert_eq!(query.sql_display().to_string(), expected);
205+
}
206+
207+
#[test]
208+
fn test_consecutive_question_marks() {
209+
let client = Client::default();
210+
// Test various consecutive patterns
211+
let query = client.query_raw("SELECT '?????', '??????', '???????' FROM test");
212+
// ????? (5, odd) -> ??????, ?????? (6, even) -> ??????, ??????? (7, odd) -> ????????
213+
let expected = "SELECT '??????', '??????', '????????' FROM test";
214+
assert_eq!(query.sql_display().to_string(), expected);
215+
}
216+
217+
#[test]
218+
fn test_no_question_marks() {
219+
let client = Client::default();
220+
let query = client.query_raw("SELECT * FROM test WHERE id = 42");
221+
assert_eq!(
222+
query.sql_display().to_string(),
223+
"SELECT * FROM test WHERE id = 42"
224+
);
225+
}
226+
}

0 commit comments

Comments
 (0)