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
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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`.
- [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`.
- [query_raw.rs](query_raw.rs) - raw queries executes SQL queries exactly as written, without parameter binding or preprocessing. This allows queries containing literal question marks that would otherwise be treated as bind parameters. FORMAT is the RowBinary by default
- [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
18 changes: 18 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,24 @@ impl Client {
query::Query::new(self, query)
}

/// Starts a new SELECT/DDL query that will be used as-is without any processing.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should provide more documentation on this difference to help newcomers choose the right constructor

///
/// # Key Differences from `query()`
///
/// - **No parameter binding**: Question marks are treated as literal characters
/// - **Raw SQL execution**: The query is sent to ClickHouse exactly as written
/// - **No SQL injection protection**: Since no parameter binding occurs, ensure
/// your SQL is safe and doesn't contain user input
///
/// # Parameters
///
/// - **Input**: `&str` - the raw SQL query to be executed
/// - **Output**: [`query::Query`] - the query builder that executes the query
///
pub fn query_raw(&self, query: &str) -> query::Query {
query::Query::raw(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
18 changes: 17 additions & 1 deletion src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,23 @@ use crate::headers::with_authentication;
pub struct Query {
client: Client,
sql: SqlBuilder,
raw: bool,
}

impl Query {
pub(crate) fn new(client: &Client, template: &str) -> Self {
Self {
client: client.clone(),
sql: SqlBuilder::new(template),
raw: false,
}
}
/// Creates a new query that will be used as-is without any processing.
pub(crate) fn raw(client: &Client, query: &str) -> Self {
Self {
client: client.clone(),
sql: SqlBuilder::raw(query),
raw: true,
}
}

Expand All @@ -53,6 +63,9 @@ impl Query {
/// [`Identifier`]: crate::sql::Identifier
#[track_caller]
pub fn bind(mut self, value: impl Bind) -> Self {
if self.raw {
Copy link
Collaborator

@loyd loyd Jun 21, 2025

Choose a reason for hiding this comment

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

In general, I'm fine with this implementation.

However, it's slightly strange to check in runtime properties that can easily be checked at compile time:

struct Query<M = DefaultMode> { .. }

impl Query<DefaultMode> {
    pub(crate) fn new(..) -> Self { .. }
    pub fn bind(..) { .. } // only in the default mode
}

impl Query<RawMode> {
    pub(crate) fn new(..) -> Self { .. }
}

and, obviously, a trait (sealed) to turn bind_fields on/off

anyway up to @slvrtrn

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, it's probably duplicate of #243 (comment)

Copy link
Contributor

@slvrtrn slvrtrn Jun 26, 2025

Choose a reason for hiding this comment

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

We could also try to explore an option of using QueryRaw as a building block for Query, as @serprex suggested. But perhaps current impl is good enough to start with, so let's not change it.

panic!("bind() cannot be used with raw queries");
}
self.sql.bind_arg(value);
self
}
Expand Down Expand Up @@ -84,7 +97,10 @@ impl Query {
/// # Ok(()) }
/// ```
pub fn fetch<T: Row>(mut self) -> Result<RowCursor<T>> {
self.sql.bind_fields::<T>();
// skip binding if raw query
if !self.raw {
self.sql.bind_fields::<T>();
}

let validation = self.client.get_validation();
if validation {
Expand Down
3 changes: 3 additions & 0 deletions src/sql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ impl SqlBuilder {

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

pub(crate) fn set_output_format(&mut self, format: impl Into<String>) {
if let Self::InProgress(_, format_opt) = self {
Expand Down
1 change: 1 addition & 0 deletions tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ mod ip;
mod mock;
mod nested;
mod query;
mod query_raw;
mod rbwnat;
mod time;
mod user_agent;
Expand Down
Loading