Skip to content

Commit b7cf548

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 b7cf548

File tree

6 files changed

+329
-2
lines changed

6 files changed

+329
-2
lines changed

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. FORMAT is the RowBinary by default
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

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/sql/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ impl SqlBuilder {
7474

7575
SqlBuilder::InProgress(parts, None)
7676
}
77+
pub(crate) fn raw(query: &str) -> Self {
78+
Self::InProgress(vec![Part::Text(query.to_string())], None)
79+
}
7780

7881
pub(crate) fn set_output_format(&mut self, format: impl Into<String>) {
7982
if let Self::InProgress(_, format_opt) = self {

tests/it/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ mod ip;
123123
mod mock;
124124
mod nested;
125125
mod query;
126+
mod query_raw;
126127
mod time;
127128
mod user_agent;
128129
mod uuid;

tests/it/query_raw.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
use clickhouse::Row;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Row, Deserialize, Debug)]
5+
struct PersonName<'a> {
6+
name: &'a str,
7+
}
8+
9+
#[derive(Row, Deserialize, Debug)]
10+
struct PersonInfo {
11+
name: String,
12+
age: u32,
13+
}
14+
15+
#[tokio::test]
16+
async fn verify_raw_query_basic_functionality() {
17+
let client = prepare_database!();
18+
19+
// The key test: verify that ? characters don't cause binding errors
20+
// Use fetch_bytes to avoid the RowBinary format issue
21+
let result = client
22+
.query_raw("SELECT 1 WHERE 'test?' = 'test?'")
23+
.fetch_bytes("TSV")
24+
.unwrap();
25+
26+
let mut data = Vec::new();
27+
let mut cursor = result;
28+
while let Some(chunk) = cursor.next().await.unwrap() {
29+
data.extend_from_slice(&chunk);
30+
}
31+
let response = String::from_utf8(data).unwrap();
32+
33+
// Should return "1\n" - proving the query executed successfully
34+
assert_eq!(response.trim(), "1");
35+
36+
// Contrast: regular query with ? should fail
37+
let regular_result = client
38+
.query("SELECT 1 WHERE 'test?' = 'test?'")
39+
.fetch_bytes("TSV");
40+
41+
// This should fail because ? is treated as a bind parameter
42+
assert!(regular_result.is_err());
43+
if let Err(error) = regular_result {
44+
let error_msg = error.to_string();
45+
assert!(error_msg.contains("unbound"));
46+
}
47+
}
48+
49+
#[tokio::test]
50+
async fn fetch_with_single_field_struct() {
51+
let client = prepare_database!();
52+
53+
// Create a test table
54+
client
55+
.query("CREATE TABLE test_users(name String) ENGINE = Memory")
56+
.execute()
57+
.await
58+
.unwrap();
59+
60+
// Insert test data
61+
client
62+
.query_raw("INSERT INTO test_users VALUES ('Alice?'), ('Bob??'), ('Charlie???')")
63+
.execute()
64+
.await
65+
.unwrap();
66+
67+
// Test raw query with struct fetching (FORMAT is RowBinary by default)
68+
let sql = "SELECT name FROM test_users ORDER BY name";
69+
70+
let mut cursor = client.query_raw(sql).fetch::<PersonName<'_>>().unwrap();
71+
72+
let mut names = Vec::new();
73+
while let Some(PersonName { name }) = cursor.next().await.unwrap() {
74+
names.push(name.to_string());
75+
}
76+
77+
assert_eq!(names, vec!["Alice?", "Bob??", "Charlie???"]);
78+
}
79+
80+
#[tokio::test]
81+
async fn fetch_with_multi_field_struct() {
82+
let client = prepare_database!();
83+
84+
// Create a test table
85+
client
86+
.query("CREATE TABLE test_persons(name String, age UInt32) ENGINE = Memory")
87+
.execute()
88+
.await
89+
.unwrap();
90+
91+
// Insert test data with question marks in names
92+
client
93+
.query_raw("INSERT INTO test_persons VALUES ('What?', 25), ('How??', 30), ('Why???', 35)")
94+
.execute()
95+
.await
96+
.unwrap();
97+
98+
// Test raw query with multi-field struct (FORMAT is RowBinary by default)
99+
let sql = "SELECT name, age FROM test_persons ORDER BY age";
100+
101+
let mut cursor = client.query_raw(sql).fetch::<PersonInfo>().unwrap();
102+
103+
let mut persons = Vec::new();
104+
while let Some(person) = cursor.next().await.unwrap() {
105+
persons.push((person.name.clone(), person.age));
106+
}
107+
108+
assert_eq!(
109+
persons,
110+
vec![
111+
("What?".to_string(), 25),
112+
("How??".to_string(), 30),
113+
("Why???".to_string(), 35)
114+
]
115+
);
116+
}
117+
118+
#[tokio::test]
119+
async fn compare_raw_vs_regular_query_with_structs() {
120+
let client = prepare_database!();
121+
122+
// Create a test table
123+
client
124+
.query("CREATE TABLE test_comparison(name String) ENGINE = Memory")
125+
.execute()
126+
.await
127+
.unwrap();
128+
129+
// Insert test data
130+
client
131+
.query_raw("INSERT INTO test_comparison VALUES ('Alice?')")
132+
.execute()
133+
.await
134+
.unwrap();
135+
136+
// Regular query with ? should fail due to unbound parameter
137+
let regular_result = client
138+
.query("SELECT name FROM test_comparison WHERE name = 'Alice?'")
139+
.fetch::<PersonName<'_>>();
140+
141+
assert!(regular_result.is_err());
142+
if let Err(error) = regular_result {
143+
let error_msg = error.to_string();
144+
assert!(error_msg.contains("unbound"));
145+
}
146+
147+
// Raw query with ? should succeed (FORMAT will be added automatically)
148+
let raw_result = client
149+
.query_raw("SELECT name FROM test_comparison WHERE name = 'Alice?'")
150+
.fetch::<PersonName<'_>>()
151+
.unwrap();
152+
153+
let mut names = Vec::new();
154+
let mut cursor = raw_result;
155+
while let Some(PersonName { name }) = cursor.next().await.unwrap() {
156+
names.push(name.to_string());
157+
}
158+
159+
assert_eq!(names, vec!["Alice?"]);
160+
}
161+
162+
#[tokio::test]
163+
async fn mixed_question_mark() {
164+
let client = prepare_database!();
165+
166+
// Test various question mark patterns with bytes fetch to avoid format issues
167+
let patterns = vec![
168+
("SELECT 1 WHERE 'test?' = 'test?'", "?"),
169+
("SELECT 2 WHERE 'test??' = 'test??'", "??"),
170+
("SELECT 3 WHERE 'test???' = 'test???'", "???"),
171+
(
172+
"SELECT 4 WHERE 'What? How?? Why???' = 'What? How?? Why???'",
173+
"mixed",
174+
),
175+
];
176+
177+
for (sql, pattern_type) in patterns {
178+
let result = client.query_raw(sql).fetch_bytes("TSV").unwrap();
179+
180+
let mut data = Vec::new();
181+
let mut cursor = result;
182+
while let Some(chunk) = cursor.next().await.unwrap() {
183+
data.extend_from_slice(&chunk);
184+
}
185+
let response = String::from_utf8(data).unwrap();
186+
187+
// Should return the expected number
188+
assert!(
189+
!response.trim().is_empty(),
190+
"Query should return data for pattern: {}",
191+
pattern_type
192+
);
193+
}
194+
}
195+
196+
#[tokio::test]
197+
async fn question_marks_in_comments() {
198+
let client = prepare_database!();
199+
200+
// Test question marks in SQL comments - should work without binding
201+
let result = client
202+
.query_raw("SELECT 1 /* What? How?? Why??? */ WHERE 1=1")
203+
.fetch_bytes("TSV")
204+
.unwrap();
205+
206+
let mut data = Vec::new();
207+
let mut cursor = result;
208+
while let Some(chunk) = cursor.next().await.unwrap() {
209+
data.extend_from_slice(&chunk);
210+
}
211+
let response = String::from_utf8(data).unwrap();
212+
213+
assert_eq!(response.trim(), "1");
214+
}
215+
216+
#[tokio::test]
217+
async fn contrast_with_regular_query() {
218+
let client = prepare_database!();
219+
220+
// This should fail with regular query because of unbound parameter
221+
let result = client
222+
.query("SELECT 1 WHERE 'test?' = 'test?'")
223+
.fetch_bytes("TSV");
224+
225+
// Regular query should fail due to unbound ?
226+
assert!(result.is_err());
227+
if let Err(error) = result {
228+
let error_msg = error.to_string();
229+
assert!(error_msg.contains("unbound"));
230+
}
231+
232+
// But raw query should succeed
233+
let raw_result = client
234+
.query_raw("SELECT 1 WHERE 'test?' = 'test?'")
235+
.fetch_bytes("TSV")
236+
.unwrap();
237+
238+
let mut data = Vec::new();
239+
let mut cursor = raw_result;
240+
while let Some(chunk) = cursor.next().await.unwrap() {
241+
data.extend_from_slice(&chunk);
242+
}
243+
let response = String::from_utf8(data).unwrap();
244+
245+
assert_eq!(response.trim(), "1");
246+
}
247+
248+
#[tokio::test]
249+
async fn complex_sql_with_question_marks() {
250+
use clickhouse::Row;
251+
use serde::{Deserialize, Serialize};
252+
253+
#[derive(Debug, Row, Serialize, Deserialize)]
254+
struct TestResult {
255+
question: String,
256+
confusion: String,
257+
bewilderment: String,
258+
answer: String,
259+
}
260+
261+
let client = prepare_database!();
262+
263+
// Test a more complex SQL query with question marks in various contexts
264+
let sql = r#"
265+
SELECT
266+
'What is this?' as question,
267+
'How does this work??' as confusion,
268+
'Why would you do this???' as bewilderment,
269+
CASE
270+
WHEN 1=1 THEN 'Yes?'
271+
ELSE 'No??'
272+
END as answer
273+
WHERE 'test?' LIKE '%?'
274+
"#;
275+
276+
let result = client.query_raw(sql).fetch_one::<TestResult>().await;
277+
278+
assert!(result.is_ok());
279+
let row = result.unwrap();
280+
assert_eq!(row.question, "What is this?");
281+
assert_eq!(row.confusion, "How does this work??");
282+
assert_eq!(row.bewilderment, "Why would you do this???");
283+
assert_eq!(row.answer, "Yes?");
284+
}
285+
286+
#[tokio::test]
287+
async fn query_raw_preserves_exact_sql() {
288+
let client = prepare_database!();
289+
290+
// Test that raw query preserves the exact SQL including whitespace and formatting
291+
let sql = "SELECT 1 WHERE 'test?' = 'test?' ";
292+
293+
let result = client.query_raw(sql).fetch_bytes("TSV").unwrap();
294+
295+
let mut data = Vec::new();
296+
let mut cursor = result;
297+
while let Some(chunk) = cursor.next().await.unwrap() {
298+
data.extend_from_slice(&chunk);
299+
}
300+
let response = String::from_utf8(data).unwrap();
301+
302+
assert_eq!(response.trim(), "1");
303+
}

0 commit comments

Comments
 (0)