Skip to content

Commit 39e36b9

Browse files
committed
Add query_raw method for raw SQL execution
- Update documentation and tests
1 parent 466b873 commit 39e36b9

File tree

6 files changed

+340
-2
lines changed

6 files changed

+340
-2
lines changed

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ If something is missing, or you found a mistake in one of these examples, please
88

99
### General usage
1010

11-
- [usage.rs](usage.rs) - creating tables, executing other DDLs, inserting the data, and selecting it back. Optional cargo features: `inserter`.
11+
- [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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,24 @@ impl Client {
319319
query::Query::new(self, query)
320320
}
321321

322+
/// Starts a new SELECT/DDL query that will be used as-is without any processing.
323+
///
324+
/// # Key Differences from `query()`
325+
///
326+
/// - **No parameter binding**: Question marks are treated as literal characters
327+
/// - **Raw SQL execution**: The query is sent to ClickHouse exactly as written
328+
/// - **No SQL injection protection**: Since no parameter binding occurs, ensure
329+
/// your SQL is safe and doesn't contain user input
330+
///
331+
/// # Parameters
332+
///
333+
/// - **Input**: `&str` - the raw SQL query to be executed
334+
/// - **Output**: [`Query`] - the query builder that executes the query
335+
///
336+
pub fn query_raw(&self, query: &str) -> query::Query {
337+
query::Query::raw(self, query)
338+
}
339+
322340
/// Enables or disables [`Row`] data types validation against the database schema
323341
/// at the cost of performance. Validation is enabled by default, and in this mode,
324342
/// the client will use `RowBinaryWithNamesAndTypes` format.

src/query.rs

Lines changed: 17 additions & 1 deletion
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,7 +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+
// skip binding if raw query
101+
if !self.raw {
102+
self.sql.bind_fields::<T>();
103+
}
88104

89105
let validation = self.client.get_validation();
90106
if validation {

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
@@ -177,6 +177,7 @@ mod ip;
177177
mod mock;
178178
mod nested;
179179
mod query;
180+
mod query_raw;
180181
mod rbwnat;
181182
mod time;
182183
mod user_agent;

tests/it/query_raw.rs

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

0 commit comments

Comments
 (0)