Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ If something is missing, or you found a mistake in one of these examples, please
### General usage

- [usage.rs](usage.rs) - creating tables, executing other DDLs, inserting the data, and selecting it back. Optional cargo features: `inserter`.
- [query_flags.rs](query_flags.rs) - Supports query interpolation flags to finely control how SQL templates are processed.
- [mock.rs](mock.rs) - writing tests with `mock` feature. Cargo features: requires `test-util`.
- [inserter.rs](inserter.rs) - using the client-side batching via the `inserter` feature. Cargo features: requires `inserter`.
- [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
Expand Down
87 changes: 86 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,40 @@ impl Client {
inserter::Inserter::new(self, table)
}

/// Starts a new SELECT/DDL query.
/// Starts a new SELECT/DDL query with default interpolation flags.
///
/// This method uses [`query::QI::DEFAULT`] flags which enable only:
/// - `?fields` substitution with struct field names
///
/// Parameter binding (`?`) is not enabled by default.
/// For explicit control over interpolation features, use [`Client::query_with_flags`].
pub fn query(&self, query: &str) -> query::Query {
query::Query::new(self, query)
}

/// Starts a new SELECT/DDL query with explicit interpolation flags
/// to specify exactly which interpolation features should be enabled.
///
/// ## Example with both features enabled
/// ```rust,ignore
/// use clickhouse::query::QI;
///
/// let rows = client
/// .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM users WHERE age > ?")
/// .bind(18)
/// .fetch::<User>()
/// .await?;
/// ```
///
/// # Comparison with [`Client::query`]
///
/// - [`Client::query`] uses default flags ([`query::QI::DEFAULT`]) enabling only `?fields`
/// - [`Client::query_with_flags`] allows explicit control over interpolation features
///
pub fn query_with_flags<const FLAGS: u8>(&self, query: &str) -> query::Query<FLAGS> {
query::Query::<FLAGS>::new(self, query)
}

/// Enables or disables [`Row`] data types validation against the database schema
/// at the cost of performance. Validation is enabled by default, and in this mode,
/// the client will use `RowBinaryWithNamesAndTypes` format.
Expand Down Expand Up @@ -737,3 +766,59 @@ mod client_tests {
assert_ne!(client.options, client_clone.options,);
}
}

#[cfg(test)]
mod query_flags_tests {
use super::*;
use crate::query::QI;

#[test]
fn test_query_with_flags() {
let client = Client::default();

// Test query with BIND flag
let query_bind = client.query_with_flags::<{ QI::BIND }>("SELECT * FROM test WHERE id = ?");
assert_eq!(
format!("{}", query_bind.sql_display()),
"SELECT * FROM test WHERE id = ?"
);

// Test query with FIELDS flag
let query_fields = client.query_with_flags::<{ QI::FIELDS }>("SELECT ?fields FROM test");
assert_eq!(
format!("{}", query_fields.sql_display()),
"SELECT ?fields FROM test"
);

// Test query with combined flags
let query_combined = client
.query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM test WHERE id = ?");
assert_eq!(
format!("{}", query_combined.sql_display()),
"SELECT ?fields FROM test WHERE id = ?"
);
}

#[test]
fn test_binding_behavior_with_flags() {
let client = Client::default();

// Test with BIND flag - should work normally
let mut query_with_bind =
client.query_with_flags::<{ QI::BIND }>("SELECT * FROM test WHERE id = ?");
query_with_bind = query_with_bind.bind(42);
assert_eq!(
format!("{}", query_with_bind.sql_display()),
"SELECT * FROM test WHERE id = 42"
);

// Test without BIND flag - should skip binding
let mut query_without_bind =
client.query_with_flags::<{ QI::NONE }>("SELECT * FROM test WHERE id = ?");
query_without_bind = query_without_bind.bind(42); // This should be skipped
assert_eq!(
format!("{}", query_without_bind.sql_display()),
"SELECT * FROM test WHERE id = ?"
);
}
}
11 changes: 11 additions & 0 deletions src/query/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Suppress Clippy warning due to identical names for the folder and file (`query`)
#![allow(clippy::module_inception)]

// Declare the submodules
mod query;
mod query_flags;

// Re-export public items
pub use crate::cursors::{BytesCursor, RowCursor};
pub use query::Query;
pub use query_flags::QI;
27 changes: 21 additions & 6 deletions src/query.rs → src/query/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,25 @@ use crate::{

const MAX_QUERY_LEN_TO_USE_GET: usize = 8192;

pub use crate::cursors::{BytesCursor, RowCursor};
pub(super) use crate::cursors::{BytesCursor, RowCursor};
use crate::headers::with_authentication;

use crate::query::query_flags::QI;

#[must_use]
#[derive(Clone)]
pub struct Query {
pub struct Query<const INTERPFLAGS: u8 = { QI::DEFAULT }> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing a public type should be a major version bump.

Can this be avoided, by the way? What is the purpose of having INTERPFLAGS here instead of a regular setter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comment. I think discouraging unconscious use of bind or fields function should be query wise. Otherwise, it would appear less intuitive and introduce unnecessary complexity, since these functions are commonly used with both execute and fetch.
Therefore, handling interpolation only in fetch would not be sufficient. If fetch were the only place where interpolation was needed, I would simply enable all flags (including bind and fields) and update the interpolation state directly via the fetch function template argument.

client: Client,
sql: SqlBuilder,
interp_flags: u8,
}

impl Query {
impl<const INTERPFLAGS: u8> Query<INTERPFLAGS> {
pub(crate) fn new(client: &Client, template: &str) -> Self {
Self {
client: client.clone(),
sql: SqlBuilder::new(template),
interp_flags: INTERPFLAGS,
}
}

Expand All @@ -53,7 +57,16 @@ impl Query {
/// [`Identifier`]: crate::sql::Identifier
#[track_caller]
pub fn bind(mut self, value: impl Bind) -> Self {
self.sql.bind_arg(value);
if QI::has_bind(self.interp_flags) {
// Only bind if the BIND flag is set
self.sql.bind_arg(value);
} else {
// Warn if QI::BIND flag is not set - binding will be skipped
eprintln!(
"warning: .bind() used without QI::BIND flag; use client.query_with_flags::<{{QI::BIND}}>(...) for binding: {}",
self.sql
);
}
self
}

Expand Down Expand Up @@ -84,7 +97,9 @@ impl Query {
/// # Ok(()) }
/// ```
pub fn fetch<T: Row>(mut self) -> Result<RowCursor<T>> {
self.sql.bind_fields::<T>();
if QI::has_fields(self.interp_flags) {
self.sql.bind_fields::<T>();
}

let validation = self.client.get_validation();
if validation {
Expand Down Expand Up @@ -150,7 +165,7 @@ impl Query {
}

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

let mut url =
Url::parse(&self.client.url).map_err(|err| Error::InvalidParams(Box::new(err)))?;
Expand Down
37 changes: 37 additions & 0 deletions src/query/query_flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// Query interpolation flags for controlling SQL template processing.
///
/// This struct provides compile-time constants that control how SQL templates
/// are processed, specifically for `?` parameter binding and `?fields` substitution.
#[derive(Clone)]
pub struct QI;

impl QI {
/// No interpolation features enabled. `?` becomes `NULL`, `?fields` is skipped. Implemented only for test purposes
pub const NONE: u8 = 0;

/// Enable `?fields` substitution with struct field names.
pub const FIELDS: u8 = 0b0001;

/// Enable `?` parameter binding with `.bind()` method.
pub const BIND: u8 = 0b0010;

/// Default flags used by `.query()` method.
///
/// By default, only `?fields` substitution is enabled. Parameter binding (`?`)
/// is opt-in via `Client::query_with_flags`.
pub const DEFAULT: u8 = QI::FIELDS;

/// All interpolation features enabled.
pub const ALL: u8 = QI::FIELDS | QI::BIND;

/// Compile-time flag checking functions
#[inline(always)]
pub const fn has_fields(flags: u8) -> bool {
(flags & Self::FIELDS) != 0
}

#[inline(always)]
pub const fn has_bind(flags: u8) -> bool {
(flags & Self::BIND) != 0
}
}
79 changes: 64 additions & 15 deletions src/sql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::fmt::{self, Display, Write};

use crate::{
error::{Error, Result},
query::QI,
row::{self, Row},
};

Expand Down Expand Up @@ -113,20 +114,39 @@ impl SqlBuilder {
}
}

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

if let Self::InProgress(parts, _) = &self {
for part in parts {
match part {
Part::Text(text) => sql.push_str(text),
Part::Arg => {
self.error("unbound query argument");
break;
if QI::has_bind(interp_flags) {
// Error on unbound arguments when BIND flag is set
self.error("unbound query argument");
break;
} else {
// Push NULL as placeholder when BIND flag is not set
eprintln!(
"warning: bind() called but QI::BIND flag not set, using NULL for query: {}",
self
);
sql.push_str("NULL");
}
}
Part::Fields => {
self.error("unbound query argument ?fields");
break;
if QI::has_fields(interp_flags) {
self.error("unbound query argument ?fields");
break;
} else {
// Skip ?fields binding entirely when FIELDS flag is not set
eprintln!(
"warning: use QI::FIELDS template flag, ?fields skipped for query: {}",
self
);
// Don't push anything - just skip this part
}
}
}
}
Expand Down Expand Up @@ -194,18 +214,35 @@ mod tests {
);

assert_eq!(
sql.finish().unwrap(),
sql.finish(QI::BIND).unwrap(),
r"SELECT `a`,`b` FROM test WHERE a = 'foo' AND b < 42"
);
}

#[test]
fn skipped_fields() {
// Test that ?fields is skipped when FIELDS flag is not set
let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE id = ?");
sql.bind_arg(42);

// Without QI::FIELDS flag, ?fields should be skipped entirely
// The bound ? becomes 42 as expected
let result = sql.finish(QI::NONE).unwrap();
assert_eq!(result, "SELECT FROM test WHERE id = 42");

// Test case with unbound ? - should become NULL when BIND flag not set
let sql2 = SqlBuilder::new("SELECT ?fields FROM test WHERE id = ?");
let result2 = sql2.finish(QI::NONE).unwrap();
assert_eq!(result2, "SELECT FROM test WHERE id = NULL");
}

#[test]
fn in_clause() {
fn t(arg: &[&str], expected: &str) {
let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE a IN ?");
sql.bind_arg(arg);
sql.bind_fields::<Row>();
assert_eq!(sql.finish().unwrap(), expected);
assert_eq!(sql.finish(QI::BIND).unwrap(), expected);
}

const ARGS: &[&str] = &["bar", "baz", "foobar"];
Expand All @@ -228,7 +265,7 @@ mod tests {
sql.bind_arg(&["a?b", "c?"][..]);
sql.bind_arg("a?");
assert_eq!(
sql.finish().unwrap(),
sql.finish(QI::BIND).unwrap(),
r"SELECT 1 FROM test WHERE a IN ['a?b','c?'] AND b = 'a?'"
);
}
Expand All @@ -237,7 +274,7 @@ mod tests {
fn question_escape() {
let sql = SqlBuilder::new("SELECT 1 FROM test WHERE a IN 'a??b'");
assert_eq!(
sql.finish().unwrap(),
sql.finish(QI::BIND).unwrap(),
r"SELECT 1 FROM test WHERE a IN 'a?b'"
);
}
Expand All @@ -246,39 +283,51 @@ mod tests {
fn option_as_null() {
let mut sql = SqlBuilder::new("SELECT 1 FROM test WHERE a = ?");
sql.bind_arg(None::<u32>);
assert_eq!(sql.finish().unwrap(), r"SELECT 1 FROM test WHERE a = NULL");
assert_eq!(
sql.finish(QI::BIND).unwrap(),
r"SELECT 1 FROM test WHERE a = NULL"
);
}

#[test]
fn option_as_value() {
let mut sql = SqlBuilder::new("SELECT 1 FROM test WHERE a = ?");
sql.bind_arg(Some(1u32));
assert_eq!(sql.finish().unwrap(), r"SELECT 1 FROM test WHERE a = 1");
assert_eq!(
sql.finish(QI::BIND).unwrap(),
r"SELECT 1 FROM test WHERE a = 1"
);
}

#[test]
fn failures() {
let mut sql = SqlBuilder::new("SELECT 1");
sql.bind_arg(42);
let err = sql.finish().unwrap_err();
let err = sql.finish(QI::BIND).unwrap_err();
assert!(err.to_string().contains("all arguments are already bound"));

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

//new changes ->
// let err = sql.finish().unwrap_err();
// assert!(
// err.to_string()
// .contains("argument ?fields cannot be used with non-struct row types")
// );
let mut sql = SqlBuilder::new("SELECT a FROM test WHERE b = ? AND c = ?");
sql.bind_arg(42);
let err = sql.finish().unwrap_err();
let err = sql.finish(QI::BIND).unwrap_err();
assert!(err.to_string().contains("unbound query argument"));

let mut sql = SqlBuilder::new("SELECT ?fields FROM test WHERE b = ?");
sql.bind_arg(42);
let err = sql.finish().unwrap_err();
let err = sql.finish(QI::BIND | QI::FIELDS).unwrap_err();
assert!(err.to_string().contains("unbound query argument ?fields"));
}
}
Loading