Skip to content

Add support for raw queries without going through query builder (#234) #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ozgurcancal
Copy link

Summary

Implements raw SQL query support as requested in #234.

@CLAassistant
Copy link

CLAassistant commented Jun 18, 2025

CLA assistant check
All committers have signed the CLA.

src/lib.rs Outdated
/// Question marks are escaped using odd/even strategy:
/// - `?` (odd count) → `??`
/// - `??` (even count) → `??` (unchanged)
/// - `???` (odd count) → `????`
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder, if this is a raw query without support to binds on ?, why it is needed? For example, this is how the CLI client behaves:

SELECT
    '??',
    '?',
    '????'

   ┌─'??'─┬─'?'─┬─'????'─┐
1. │ ??   │ ?   │ ????   │
   └──────┴─────┴────────┘

i.e. no additional translation for ? symbols. Or is it because of some HTTP interface quirks?

Copy link
Author

@ozgurcancal ozgurcancal Jun 19, 2025

Choose a reason for hiding this comment

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

Thanks for your comment. My tests show that Literal ? in SQL is escaped as ?? for ClickHouse HTTP interface whereas CLI client sends SQL directly so it is an HTTP interface quirk as you sad

Copy link
Contributor

@slvrtrn slvrtrn Jun 19, 2025

Choose a reason for hiding this comment

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

I think that might be a URL encoding oddity, then. It is worth checking that it is performed correctly.

For example, using POST:

curl -XPOST "http://localhost:8123" --data-binary "select '?', '??', '???' FORMAT JSONEachRow"

{"'?'":"?","'??'":"??","'???'":"???"}

GET with proper URI encoding:

curl "http://localhost:8123?query=SELECT%20'%3F'%2C'%3F%3F'%2C'%3F%3F%3F'%20FORMAT%20JSONEachRow"

{"'?'":"?","'??'":"??","'???'":"???"}

Where the query is encoded as:

encodeURIComponent("SELECT '?','??','???' FORMAT JSONEachRow")
"SELECT%20'%3F'%2C'%3F%3F'%2C'%3F%3F%3F'%20FORMAT%20JSONEachRow"

All in all, that could be related to #230 and this bit in particular:

https://github.com/ClickHouse/clickhouse-rs/blob/main/src/query.rs#L158-L169

Copy link
Author

Choose a reason for hiding this comment

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

Thanks I think my last commit would fix the issue, I also compared the output with the CLI client.

src/lib.rs Outdated
/// // query.bind(value) ❌
/// // query.param("key", value) ❌
/// ```
pub fn query_raw(&self, query: &str) -> query_raw::QueryRaw {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am curious if we can just add a builder method to the main Query that disables query builder usage, as well as some bind checks.

Copy link
Author

@ozgurcancal ozgurcancal Jun 19, 2025

Choose a reason for hiding this comment

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

Thanks for the comment, I just wanted to implement a solution with zero change on the existing codebase to make further changes and tests easier. I could adjust builder mechanism using a bool flag too such as;

pub struct Query {
client: Client,
sql: SqlBuilder,
raw: bool
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@loyd @serprex, what is your opinion on this? Shall we just keep a single Query with a builder method and a few additional internal boolean checks, or keep a QueryRaw with (almost) similar API?

Copy link
Member

Choose a reason for hiding this comment

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

would have to compare the two choices to really decide, ideally you have Query/QueryRaw & implement Query on top of QueryRaw, but very likely that runs into some hurdle

src/query_raw.rs Outdated
#[track_caller]
pub fn param(self, _name: &str, _value: impl Serialize) -> Self {
panic!("cannot set parameters on raw query - use regular query() for parameter binding");
}
Copy link
Contributor

@slvrtrn slvrtrn Jun 19, 2025

Choose a reason for hiding this comment

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

if this is a separate struct/impl, why do we even need these methods that always panic?

Copy link
Author

@ozgurcancal ozgurcancal Jun 19, 2025

Choose a reason for hiding this comment

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

Thanks for the detailed review. I thought the developer should be notified about the misuse of the functionality to help prevent future bugs.

Copy link
Contributor

@slvrtrn slvrtrn left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution. I like that there is a lot of tests and docs. I have a few questions about the implementation details, though, see the comments.

@ozgurcancal ozgurcancal requested a review from slvrtrn June 20, 2025 17:45
@ozgurcancal ozgurcancal force-pushed the development branch 3 times, most recently from 805acf0 to 0c070fe Compare June 20, 2025 21:41
@ozgurcancal ozgurcancal reopened this Jun 20, 2025
async fn query_raw_preserves_exact_sql() {
let client = prepare_database!();

// Test that raw query preserves the exact SQL including whitespace and formatting
Copy link
Collaborator

Choose a reason for hiding this comment

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

It can check, for instance, system.query_log to ensure it, but currently it doesn't ensure this statement

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 the comment, it now compares with the system logs

@@ -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.

@@ -313,6 +313,11 @@ 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

loyd
loyd previously approved these changes Jun 21, 2025
@slvrtrn
Copy link
Contributor

slvrtrn commented Jun 23, 2025

@ozgurcancal, let's resolve the conflicts, add a bit more docs as @loyd requested and then we can merge.

slvrtrn
slvrtrn previously approved these changes Jun 23, 2025
@ozgurcancal ozgurcancal dismissed stale reviews from slvrtrn and loyd via d725f3c June 25, 2025 20:06
- Update documentation and tests
slvrtrn
slvrtrn previously approved these changes Jun 26, 2025
@@ -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 without parameter binding, with question mark escaping. FORMAT is the RowBinary by default
Copy link
Contributor

Choose a reason for hiding this comment

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

WATCH is removed as of #245, since it is deprecated feature, and its implementation was incompatible with CH 25+.

with question mark escaping

is it still true?

@@ -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
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.


// Test question marks in SQL comments - should work without binding
let result = client
.query_raw("SELECT 1 /* What? How?? Why??? */ WHERE 1=1")
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this test, very nice 👍

Copy link
Author

Choose a reason for hiding this comment

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

#243 (comment)
It would be great, but I think the new builder will share a lot with the current one, so we’ll probably need a common base trait for both, and that means a bunch of changes.

@slvrtrn
Copy link
Contributor

slvrtrn commented Jun 26, 2025

CH Cloud test failures can be ignored, cause it's secrets pull issue for now, need to re-do the setup to support PRs from external contributors.
The rest (rustfmt, docs) should be fixed before merging.

@ozgurcancal
Copy link
Author

ozgurcancal commented Jun 26, 2025

CH Cloud test failures can be ignored, cause it's secrets pull issue for now, need to re-do the setup to support PRs from external contributors. The rest (rustfmt, docs) should be fixed before merging.

I have addressed the suggested fixes. Thanks for the support, next time my PR will be much cleaner :)

@loyd
Copy link
Collaborator

loyd commented Jul 2, 2025

I think we shouldn't merge this PR until #157 (comment) is discussed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants