Skip to content

Commit 9b8aa48

Browse files
committed
Implement query with interpolation flags
1 parent 56c5dd3 commit 9b8aa48

File tree

13 files changed

+399
-46
lines changed

13 files changed

+399
-46
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. Optional cargo features: `inserter`.
12+
- [query_flags.rs](query_flags.rs) - Supports query interpolation flags to finely control how SQL templates are processed.
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: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,39 @@ impl Client {
317317
inserter::Inserter::new(self, table)
318318
}
319319

320-
/// Starts a new SELECT/DDL query.
320+
/// Starts a new SELECT/DDL query with default interpolation flags.
321+
///
322+
/// This method uses [`queries::QI::DEFAULT`] flags which enable:
323+
/// - `?fields` substitution with struct field names
324+
/// - Parameter substitution (reserved for future use)
325+
///
326+
/// For explicit control over interpolation features, use [`Client::query_with_flags`].
321327
pub fn query(&self, query: &str) -> query::Query {
322328
query::Query::new(self, query)
323329
}
324330

331+
/// Starts a new SELECT/DDL query with explicit interpolation flags
332+
/// to specify exactly which interpolation features should be enabled..
333+
///
334+
/// ## Query with both features enabled
335+
/// ```rust,ignore
336+
/// use clickhouse::queries::QI;
337+
///
338+
/// let rows = client
339+
/// .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM users WHERE age > ?")
340+
/// .bind(18)
341+
/// .fetch::<User>()
342+
/// .await?;
343+
/// ```
344+
/// # Comparison with [`Client::query`]
345+
///
346+
/// - [`Client::query`] uses default flags ([`query::QI::DEFAULT`])
347+
/// - [`Client::query_with_flags`] allows explicit control over interpolation features
348+
///
349+
pub fn query_with_flags<const FLAGS: u8>(&self, query: &str) -> query::Query<FLAGS> {
350+
query::Query::<FLAGS>::new(self, query)
351+
}
352+
325353
/// Enables or disables [`Row`] data types validation against the database schema
326354
/// at the cost of performance. Validation is enabled by default, and in this mode,
327355
/// the client will use `RowBinaryWithNamesAndTypes` format.
@@ -519,3 +547,59 @@ mod client_tests {
519547
assert!(client.validation);
520548
}
521549
}
550+
551+
#[cfg(test)]
552+
mod query_flags_tests {
553+
use super::*;
554+
use crate::query::QI;
555+
556+
#[test]
557+
fn test_query_with_flags() {
558+
let client = Client::default();
559+
560+
// Test query with BIND flag
561+
let query_bind = client.query_with_flags::<{ QI::BIND }>("SELECT * FROM test WHERE id = ?");
562+
assert_eq!(
563+
format!("{}", query_bind.sql_display()),
564+
"SELECT * FROM test WHERE id = ?"
565+
);
566+
567+
// Test query with FIELDS flag
568+
let query_fields = client.query_with_flags::<{ QI::FIELDS }>("SELECT ?fields FROM test");
569+
assert_eq!(
570+
format!("{}", query_fields.sql_display()),
571+
"SELECT ?fields FROM test"
572+
);
573+
574+
// Test query with combined flags
575+
let query_combined = client
576+
.query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM test WHERE id = ?");
577+
assert_eq!(
578+
format!("{}", query_combined.sql_display()),
579+
"SELECT ?fields FROM test WHERE id = ?"
580+
);
581+
}
582+
583+
#[test]
584+
fn test_binding_behavior_with_flags() {
585+
let client = Client::default();
586+
587+
// Test with BIND flag - should work normally
588+
let mut query_with_bind =
589+
client.query_with_flags::<{ QI::BIND }>("SELECT * FROM test WHERE id = ?");
590+
query_with_bind = query_with_bind.bind(42);
591+
assert_eq!(
592+
format!("{}", query_with_bind.sql_display()),
593+
"SELECT * FROM test WHERE id = 42"
594+
);
595+
596+
// Test without BIND flag - should skip binding
597+
let mut query_without_bind =
598+
client.query_with_flags::<{ QI::NONE }>("SELECT * FROM test WHERE id = ?");
599+
query_without_bind = query_without_bind.bind(42); // This should be skipped
600+
assert_eq!(
601+
format!("{}", query_without_bind.sql_display()),
602+
"SELECT * FROM test WHERE id = ?"
603+
);
604+
}
605+
}

src/query/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Suppress Clippy warning due to identical names for the folder and file (`query`)
2+
#![allow(clippy::module_inception)]
3+
4+
// Declare the submodules
5+
mod query;
6+
mod query_flags;
7+
8+
// Re-export public items
9+
pub use crate::cursors::{BytesCursor, RowCursor};
10+
pub use query::Query;
11+
pub use query_flags::QI;

src/query.rs renamed to src/query/query.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::fmt::Display;
44
use url::Url;
55

66
use crate::{
7+
cursors::{BytesCursor, RowCursor},
78
error::{Error, Result},
89
headers::with_request_headers,
910
request_body::RequestBody,
@@ -15,21 +16,24 @@ use crate::{
1516

1617
const MAX_QUERY_LEN_TO_USE_GET: usize = 8192;
1718

18-
pub use crate::cursors::{BytesCursor, RowCursor};
1919
use crate::headers::with_authentication;
2020

21+
use crate::query::query_flags::QI;
22+
2123
#[must_use]
2224
#[derive(Clone)]
23-
pub struct Query {
25+
pub struct Query<const INTERPFLAGS: u8 = { QI::DEFAULT }> {
2426
client: Client,
2527
sql: SqlBuilder,
28+
interp_flags: u8,
2629
}
2730

28-
impl Query {
31+
impl<const INTERPFLAGS: u8> Query<INTERPFLAGS> {
2932
pub(crate) fn new(client: &Client, template: &str) -> Self {
3033
Self {
3134
client: client.clone(),
3235
sql: SqlBuilder::new(template),
36+
interp_flags: INTERPFLAGS,
3337
}
3438
}
3539

@@ -53,7 +57,9 @@ impl Query {
5357
/// [`Identifier`]: crate::sql::Identifier
5458
#[track_caller]
5559
pub fn bind(mut self, value: impl Bind) -> Self {
56-
self.sql.bind_arg(value);
60+
if QI::has_bind(self.interp_flags) {
61+
self.sql.bind_arg(value);
62+
}
5763
self
5864
}
5965

@@ -84,7 +90,9 @@ impl Query {
8490
/// # Ok(()) }
8591
/// ```
8692
pub fn fetch<T: Row>(mut self) -> Result<RowCursor<T>> {
87-
self.sql.bind_fields::<T>();
93+
if QI::has_fields(self.interp_flags) {
94+
self.sql.bind_fields::<T>();
95+
}
8896

8997
let validation = self.client.get_validation();
9098
if validation {
@@ -150,7 +158,7 @@ impl Query {
150158
}
151159

152160
pub(crate) fn do_execute(self, read_only: bool) -> Result<Response> {
153-
let query = self.sql.finish()?;
161+
let query = self.sql.finish(self.interp_flags)?;
154162

155163
let mut url =
156164
Url::parse(&self.client.url).map_err(|err| Error::InvalidParams(Box::new(err)))?;

src/query/query_flags.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// Query interpolation flags for controlling SQL template processing.
2+
///
3+
/// This struct provides compile-time constants that control how SQL templates
4+
/// are processed, specifically for `?` parameter binding and `?fields` substitution.
5+
#[derive(Clone)]
6+
pub struct QI;
7+
8+
impl QI {
9+
/// No interpolation features enabled. `?` becomes `NULL`, `?fields` is skipped. Implemented only for test purposes
10+
pub const NONE: u8 = 0;
11+
12+
/// Enable `?fields` substitution with struct field names.
13+
pub const FIELDS: u8 = 0b0001;
14+
15+
/// Enable parameter substitution (reserved for future use).
16+
pub const PARAMS: u8 = 0b0010;
17+
18+
/// Enable `?` parameter binding with `.bind()` method.
19+
pub const BIND: u8 = 0b0100;
20+
21+
/// Default flags used by `.query()` method.
22+
pub const DEFAULT: u8 = QI::FIELDS | QI::PARAMS;
23+
24+
/// All interpolation features enabled.
25+
pub const ALL: u8 = QI::FIELDS | QI::PARAMS | QI::BIND;
26+
27+
/// Compile-time flag checking functions
28+
#[inline(always)]
29+
pub const fn has_fields(flags: u8) -> bool {
30+
(flags & Self::FIELDS) != 0
31+
}
32+
33+
#[inline(always)]
34+
pub const fn has_params(flags: u8) -> bool {
35+
(flags & Self::PARAMS) != 0
36+
}
37+
38+
#[inline(always)]
39+
pub const fn has_bind(flags: u8) -> bool {
40+
(flags & Self::BIND) != 0
41+
}
42+
}

src/rowbinary/validation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ pub(crate) enum VariantValidationState {
239239
Identifier(u8),
240240
}
241241

242-
impl<'cursor, R: Row> SchemaValidator<R> for Option<InnerDataTypeValidator<'_, 'cursor, R>> {
242+
impl<R: Row> SchemaValidator<R> for Option<InnerDataTypeValidator<'_, '_, R>> {
243243
type Inner<'de>
244244
= Self
245245
where

src/sql/mod.rs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fmt::{self, Display, Write};
22

33
use crate::{
44
error::{Error, Result},
5+
query::QI,
56
row::{self, Row},
67
};
78

@@ -113,20 +114,35 @@ impl SqlBuilder {
113114
}
114115
}
115116

116-
pub(crate) fn finish(mut self) -> Result<String> {
117+
pub(crate) fn finish(mut self, interp_flags: u8) -> Result<String> {
117118
let mut sql = String::new();
118119

119120
if let Self::InProgress(parts, _) = &self {
120121
for part in parts {
121122
match part {
122123
Part::Text(text) => sql.push_str(text),
123124
Part::Arg => {
124-
self.error("unbound query argument");
125-
break;
125+
if QI::has_bind(interp_flags) {
126+
self.error("unbound query argument");
127+
break;
128+
} else {
129+
// Push NULL as placeholder to prevent ClickHouse server error
130+
eprintln!(
131+
"warning use QI::BIND template flag,bind() skipped for query: {}",
132+
self
133+
);
134+
sql.push_str("NULL");
135+
}
126136
}
127137
Part::Fields => {
128-
self.error("unbound query argument ?fields");
129-
break;
138+
if QI::has_fields(interp_flags) {
139+
self.error("unbound query argument ?fields");
140+
break;
141+
} else {
142+
// Skip ?fields binding entirely when FIELDS flag is not set
143+
eprintln!("warning: use QI::FIELDS template flag, ?fields skipped for query: {}", self);
144+
// Don't push anything - just skip this part
145+
}
130146
}
131147
}
132148
}
@@ -194,18 +210,35 @@ mod tests {
194210
);
195211

196212
assert_eq!(
197-
sql.finish().unwrap(),
213+
sql.finish(QI::BIND).unwrap(),
198214
r"SELECT `a`,`b` FROM test WHERE a = 'foo' AND b < 42"
199215
);
200216
}
201217

218+
#[test]
219+
fn skipped_fields() {
220+
// Test that ?fields is skipped when FIELDS flag is not set
221+
let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE id = ?");
222+
sql.bind_arg(42);
223+
224+
// Without QI::FIELDS flag, ?fields should be skipped entirely
225+
// The bound ? becomes 42 as expected
226+
let result = sql.finish(QI::NONE).unwrap();
227+
assert_eq!(result, "SELECT FROM test WHERE id = 42");
228+
229+
// Test case with unbound ? - should become NULL when BIND flag not set
230+
let sql2 = SqlBuilder::new("SELECT ?fields FROM test WHERE id = ?");
231+
let result2 = sql2.finish(QI::NONE).unwrap();
232+
assert_eq!(result2, "SELECT FROM test WHERE id = NULL");
233+
}
234+
202235
#[test]
203236
fn in_clause() {
204237
fn t(arg: &[&str], expected: &str) {
205238
let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE a IN ?");
206239
sql.bind_arg(arg);
207240
sql.bind_fields::<Row>();
208-
assert_eq!(sql.finish().unwrap(), expected);
241+
assert_eq!(sql.finish(QI::BIND).unwrap(), expected);
209242
}
210243

211244
const ARGS: &[&str] = &["bar", "baz", "foobar"];
@@ -228,7 +261,7 @@ mod tests {
228261
sql.bind_arg(&["a?b", "c?"][..]);
229262
sql.bind_arg("a?");
230263
assert_eq!(
231-
sql.finish().unwrap(),
264+
sql.finish(QI::BIND).unwrap(),
232265
r"SELECT 1 FROM test WHERE a IN ['a?b','c?'] AND b = 'a?'"
233266
);
234267
}
@@ -237,7 +270,7 @@ mod tests {
237270
fn question_escape() {
238271
let sql = SqlBuilder::new("SELECT 1 FROM test WHERE a IN 'a??b'");
239272
assert_eq!(
240-
sql.finish().unwrap(),
273+
sql.finish(QI::BIND).unwrap(),
241274
r"SELECT 1 FROM test WHERE a IN 'a?b'"
242275
);
243276
}
@@ -246,38 +279,44 @@ mod tests {
246279
fn option_as_null() {
247280
let mut sql = SqlBuilder::new("SELECT 1 FROM test WHERE a = ?");
248281
sql.bind_arg(None::<u32>);
249-
assert_eq!(sql.finish().unwrap(), r"SELECT 1 FROM test WHERE a = NULL");
282+
assert_eq!(
283+
sql.finish(QI::BIND).unwrap(),
284+
r"SELECT 1 FROM test WHERE a = NULL"
285+
);
250286
}
251287

252288
#[test]
253289
fn option_as_value() {
254290
let mut sql = SqlBuilder::new("SELECT 1 FROM test WHERE a = ?");
255291
sql.bind_arg(Some(1u32));
256-
assert_eq!(sql.finish().unwrap(), r"SELECT 1 FROM test WHERE a = 1");
292+
assert_eq!(
293+
sql.finish(QI::BIND).unwrap(),
294+
r"SELECT 1 FROM test WHERE a = 1"
295+
);
257296
}
258297

259298
#[test]
260299
fn failures() {
261300
let mut sql = SqlBuilder::new("SELECT 1");
262301
sql.bind_arg(42);
263-
let err = sql.finish().unwrap_err();
302+
let err = sql.finish(QI::BIND).unwrap_err();
264303
assert!(err.to_string().contains("all arguments are already bound"));
265304

266305
let mut sql = SqlBuilder::new("SELECT ?fields");
267306
sql.bind_fields::<Unnamed>();
268-
let err = sql.finish().unwrap_err();
307+
let err = sql.finish(QI::BIND | QI::FIELDS).unwrap_err();
269308
assert!(err
270309
.to_string()
271310
.contains("argument ?fields cannot be used with non-struct row types"));
272311

273312
let mut sql = SqlBuilder::new("SELECT a FROM test WHERE b = ? AND c = ?");
274313
sql.bind_arg(42);
275-
let err = sql.finish().unwrap_err();
314+
let err = sql.finish(QI::BIND).unwrap_err();
276315
assert!(err.to_string().contains("unbound query argument"));
277316

278317
let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE b = ?");
279318
sql.bind_arg(42);
280-
let err = sql.finish().unwrap_err();
319+
let err = sql.finish(QI::BIND | QI::FIELDS).unwrap_err();
281320
assert!(err.to_string().contains("unbound query argument ?fields"));
282321
}
283322
}

0 commit comments

Comments
 (0)