diff --git a/examples/README.md b/examples/README.md index 2cf709c6..d9a641af 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 97fc795b..625fc26b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::() + /// .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(&self, query: &str) -> query::Query { + query::Query::::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. @@ -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 = ?" + ); + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 00000000..fa248d6f --- /dev/null +++ b/src/query/mod.rs @@ -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; diff --git a/src/query.rs b/src/query/query.rs similarity index 88% rename from src/query.rs rename to src/query/query.rs index b9904bc0..32f5208a 100644 --- a/src/query.rs +++ b/src/query/query.rs @@ -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 { client: Client, sql: SqlBuilder, + interp_flags: u8, } -impl Query { +impl Query { pub(crate) fn new(client: &Client, template: &str) -> Self { Self { client: client.clone(), sql: SqlBuilder::new(template), + interp_flags: INTERPFLAGS, } } @@ -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 } @@ -84,7 +97,9 @@ impl Query { /// # Ok(()) } /// ``` pub fn fetch(mut self) -> Result> { - self.sql.bind_fields::(); + if QI::has_fields(self.interp_flags) { + self.sql.bind_fields::(); + } let validation = self.client.get_validation(); if validation { @@ -150,7 +165,7 @@ impl Query { } pub(crate) fn do_execute(self, read_only: bool) -> Result { - 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)))?; diff --git a/src/query/query_flags.rs b/src/query/query_flags.rs new file mode 100644 index 00000000..9414b25f --- /dev/null +++ b/src/query/query_flags.rs @@ -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 + } +} diff --git a/src/sql/mod.rs b/src/sql/mod.rs index 1987c6a7..1f5684e5 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Display, Write}; use crate::{ error::{Error, Result}, + query::QI, row::{self, Row}, }; @@ -113,7 +114,7 @@ impl SqlBuilder { } } - pub(crate) fn finish(mut self) -> Result { + pub(crate) fn finish(mut self, interp_flags: u8) -> Result { let mut sql = String::new(); if let Self::InProgress(parts, _) = &self { @@ -121,12 +122,31 @@ impl SqlBuilder { 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 + } } } } @@ -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::(); - assert_eq!(sql.finish().unwrap(), expected); + assert_eq!(sql.finish(QI::BIND).unwrap(), expected); } const ARGS: &[&str] = &["bar", "baz", "foobar"]; @@ -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?'" ); } @@ -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'" ); } @@ -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::); - 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::(); - 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")); } } diff --git a/tests/it/insert.rs b/tests/it/insert.rs index 4f43e7c4..62655444 100644 --- a/tests/it/insert.rs +++ b/tests/it/insert.rs @@ -1,5 +1,9 @@ use crate::{SimpleRow, create_simple_table, fetch_rows, flush_query_log}; -use clickhouse::{Row, sql::Identifier}; +use clickhouse::{Row, query::QI, sql::Identifier}; + +//new changes +// use crate::{SimpleRow, create_simple_table, fetch_rows, flush_query_log}; +// use clickhouse::{Row, sql::Identifier}; use serde::{Deserialize, Serialize}; use std::panic::AssertUnwindSafe; @@ -28,7 +32,7 @@ async fn keeps_client_options() { flush_query_log(&client).await; let (has_insert_setting, has_client_setting) = client - .query(&format!( + .query_with_flags::<{ QI::BIND }>(&format!( " SELECT Settings['{insert_setting_name}'] = '{insert_setting_value}', @@ -87,7 +91,7 @@ async fn overrides_client_options() { flush_query_log(&client).await; let has_setting_override = client - .query(&format!( + .query_with_flags::<{ QI::BIND }>(&format!( " SELECT Settings['{setting_name}'] = '{override_value}' FROM system.query_log @@ -149,7 +153,7 @@ async fn rename_insert() { let client = prepare_database!(); client - .query( + .query_with_flags::<{ QI::BIND }>( " CREATE TABLE ?( fixId UInt64, @@ -273,7 +277,7 @@ async fn cache_row_metadata() { println!("row_insert_metadata_query: {row_insert_metadata_query:?}"); let initial_count: u64 = client - .query(count_query) + .query_with_flags::<{ QI::BIND }>(count_query) .bind(&row_insert_metadata_query) .fetch_one() .await @@ -295,7 +299,7 @@ async fn cache_row_metadata() { flush_query_log(&client).await; let after_insert: u64 = client - .query(count_query) + .query_with_flags::<{ QI::BIND }>(count_query) .bind(&row_insert_metadata_query) .fetch_one() .await @@ -321,7 +325,7 @@ async fn cache_row_metadata() { flush_query_log(&client).await; let final_count: u64 = client - .query(count_query) + .query_with_flags::<{ QI::BIND }>(count_query) .bind(&row_insert_metadata_query) .fetch_one() .await diff --git a/tests/it/int128.rs b/tests/it/int128.rs index c273c3a8..61ae5a40 100644 --- a/tests/it/int128.rs +++ b/tests/it/int128.rs @@ -1,7 +1,7 @@ use rand::random; use serde::{Deserialize, Serialize}; -use clickhouse::Row; +use clickhouse::{Row, query::QI}; #[tokio::test] async fn u128() { @@ -51,7 +51,9 @@ async fn u128() { insert.end().await.unwrap(); let rows = client - .query("SELECT ?fields FROM test WHERE id IN ? ORDER BY value") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>( + "SELECT ?fields FROM test WHERE id IN ? ORDER BY value", + ) .bind(vec![id0, id2]) .fetch_all::() .await @@ -110,7 +112,9 @@ async fn i128() { insert.end().await.unwrap(); let rows = client - .query("SELECT ?fields FROM test WHERE id IN ? ORDER BY value") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>( + "SELECT ?fields FROM test WHERE id IN ? ORDER BY value", + ) .bind(vec![id0, id2]) .fetch_all::() .await diff --git a/tests/it/main.rs b/tests/it/main.rs index 2d2a78ee..78e3a4ad 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -25,7 +25,9 @@ //! the "cloud" environment, it appends the current timestamp to allow //! clean up outdated databases based on its creation time. -use clickhouse::{Client, Row, RowOwned, RowRead, RowWrite, sql::Identifier}; +use clickhouse::{Client, Row, RowOwned, RowRead, RowWrite, query::QI, sql::Identifier}; +//new changes +// use clickhouse::{Client, Row, RowOwned, RowRead, RowWrite, sql::Identifier}; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -149,7 +151,9 @@ impl SimpleRow { pub(crate) async fn create_simple_table(client: &Client, table_name: &str) { client - .query("CREATE TABLE ?(id UInt64, data String) ENGINE = MergeTree ORDER BY id") + .query_with_flags::<{ QI::BIND }>( + "CREATE TABLE ?(id UInt64, data String) ENGINE = MergeTree ORDER BY id", + ) .with_option("wait_end_of_query", "1") .bind(Identifier(table_name)) .execute() @@ -162,7 +166,7 @@ where T: RowOwned + RowRead, { client - .query("SELECT ?fields FROM ?") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ?") .bind(Identifier(table_name)) .fetch_all::() .await @@ -204,7 +208,7 @@ where insert.end().await.unwrap(); client - .query("SELECT ?fields FROM ? ORDER BY () ASC") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ? ORDER BY () ASC") .bind(Identifier(table_name)) .fetch_all::() .await @@ -285,7 +289,7 @@ mod _priv { let client = get_client(); client - .query("DROP DATABASE IF EXISTS ?") + .query_with_flags::<{ QI::BIND }>("DROP DATABASE IF EXISTS ?") .with_option("wait_end_of_query", "1") .bind(Identifier(db_name)) .execute() @@ -293,7 +297,7 @@ mod _priv { .unwrap_or_else(|err| panic!("cannot drop db {db_name}, cause: {err}")); client - .query("CREATE DATABASE ?") + .query_with_flags::<{ QI::BIND }>("CREATE DATABASE ?") .with_option("wait_end_of_query", "1") .bind(Identifier(db_name)) .execute() diff --git a/tests/it/query.rs b/tests/it/query.rs index 8e282d22..89ef11b2 100644 --- a/tests/it/query.rs +++ b/tests/it/query.rs @@ -1,7 +1,8 @@ use serde::{Deserialize, Serialize}; -use clickhouse::{Row, error::Error}; - +use clickhouse::{Row, error::Error, query::QI, sql::Identifier}; +//new changes +// use clickhouse::{Row, error::Error}; #[tokio::test] async fn smoke() { let client = prepare_database!(); @@ -35,7 +36,9 @@ async fn smoke() { // Read from the table. let mut cursor = client - .query("SELECT ?fields FROM test WHERE name = ? AND no BETWEEN ? AND ?.2") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>( + "SELECT ?fields FROM test WHERE name = ? AND no BETWEEN ? AND ?.2", + ) .bind("foo") .bind(500) .bind((42, 504)) @@ -137,7 +140,7 @@ async fn long_query() { let long_string = "A".repeat(100_000); let got_string = client - .query("select ?") + .query_with_flags::<{ QI::BIND }>("select ?") .bind(&long_string) .fetch_one::() .await @@ -223,7 +226,9 @@ async fn keeps_client_options() { let client = prepare_database!().with_option(client_setting_name, client_setting_value); let value = client - .query("SELECT value FROM system.settings WHERE name = ? OR name = ? ORDER BY name") + .query_with_flags::<{ QI::BIND }>( + "SELECT value FROM system.settings WHERE name = ? OR name = ? ORDER BY name", + ) .bind(query_setting_name) .bind(client_setting_name) .with_option(query_setting_name, query_setting_value) @@ -242,7 +247,7 @@ async fn overrides_client_options() { let client = prepare_database!().with_option(setting_name, setting_value); let value = client - .query("SELECT value FROM system.settings WHERE name = ?") + .query_with_flags::<{ QI::BIND }>("SELECT value FROM system.settings WHERE name = ?") .bind(setting_name) .with_option(setting_name, override_value) .fetch_one::() @@ -263,3 +268,162 @@ async fn prints_query() { "SELECT ?fields FROM test WHERE a = ? AND b < ?" ); } + +#[tokio::test] +async fn query_flags_normal_query_with_bind_should_error() { + let client = prepare_database!(); + + // Test that using .bind() with regular .query() should skip binding and produce warning + let result = client + .query("SELECT ? as value") + .bind(42) + .fetch_one::() + .await; + + // Should fail because ? becomes NULL and we can't cast NULL to i32 + assert!(result.is_err()); +} + +#[tokio::test] +async fn query_flags_fields_without_fields_flag_should_error() { + let client = prepare_database!(); + + #[derive(Debug, Row, Deserialize, PartialEq)] + struct TestRow { + value: u8, + } + + // Test that using ?fields with only BIND flag should fail when ?fields is not substituted + // This tests the SQL generation without fetch auto-binding + let result = client + .query_with_flags::<{ QI::BIND }>("INSERT INTO test (?fields) VALUES (42)") + .execute() + .await; + + // Should fail because ?fields is skipped and we get "INSERT INTO test () VALUES (42)" which is invalid SQL + assert!(result.is_err()); +} + +#[tokio::test] +async fn query_flags_happy_case_bind_only() { + let client = prepare_database!(); + + // Test successful binding with BIND flag + let result = client + .query_with_flags::<{ QI::BIND }>("SELECT ? as value") + .bind(42u8) + .fetch_one::() + .await + .unwrap(); + + assert_eq!(result, 42); +} + +#[tokio::test] +async fn query_flags_happy_case_fields_only() { + let client = prepare_database!(); + + #[derive(Debug, Row, Deserialize, PartialEq)] + struct TestRow { + value: u8, + } + + // Test successful fields substitution with FIELDS flag + let result = client + .query_with_flags::<{ QI::FIELDS }>("SELECT ?fields FROM (SELECT 42 as value)") + .fetch_one::() + .await + .unwrap(); + + assert_eq!(result, TestRow { value: 42 }); +} + +#[tokio::test] +async fn query_flags_happy_case_both_flags() { + let client = prepare_database!(); + + #[derive(Debug, Row, Deserialize, PartialEq)] + struct TestRow { + value: u8, + } + + // Test successful combination of both ?fields and ? with both flags + let result = client + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM (SELECT ? as value)") + .bind(42u8) + .fetch_one::() + .await + .unwrap(); + + assert_eq!(result, TestRow { value: 42 }); +} + +#[tokio::test] +async fn query_flags_multiple_binds_with_flag() { + let client = prepare_database!(); + + // Test multiple bind parameters with BIND flag + let result = client + .query_with_flags::<{ QI::BIND }>("SELECT ? + ? as sum") + .bind(10u8) + .bind(32u8) + .fetch_one::() + .await + .unwrap(); + + assert_eq!(result, 42); +} + +#[tokio::test] +async fn query_flags_bind_with_identifier() { + let client = prepare_database!(); + let table_name = "test_table"; + + // Create a test table + client + .query_with_flags::<{ QI::BIND }>( + "CREATE TABLE ? (id UInt32, value String) ENGINE = Memory", + ) + .bind(Identifier(table_name)) + .execute() + .await + .unwrap(); + + // Insert test data + client + .query_with_flags::<{ QI::BIND }>("INSERT INTO ? VALUES (1, 'test')") + .bind(Identifier(table_name)) + .execute() + .await + .unwrap(); + + // Query with identifier binding + let count = client + .query_with_flags::<{ QI::BIND }>("SELECT count() FROM ?") + .bind(Identifier(table_name)) + .fetch_one::() + .await + .unwrap(); + + assert_eq!(count, 1); +} + +#[tokio::test] +async fn query_flags_none_with_fields_and_bind() { + let client = prepare_database!(); + + #[derive(Debug, Row, Deserialize, PartialEq)] + struct TestRow { + value: u8, + } + + // Test that QI::NONE skips both ?fields and ? - this should fail because ?fields becomes empty + let result = client + .query_with_flags::<{ QI::NONE }>("SELECT ?fields, ? as extra FROM (SELECT 42 as value)") + .bind(999) // This should be ignored + .execute() // Use execute to avoid fetch auto-binding fields + .await; + + // Should fail because ?fields is skipped making "SELECT , NULL as extra FROM ..." which is invalid SQL + assert!(result.is_err()); +} diff --git a/tests/it/rbwnat_smoke.rs b/tests/it/rbwnat_smoke.rs index 77ae3005..5537fc94 100644 --- a/tests/it/rbwnat_smoke.rs +++ b/tests/it/rbwnat_smoke.rs @@ -2,6 +2,7 @@ use crate::decimals::*; use crate::geo_types::{LineString, MultiLineString, MultiPolygon, Point, Polygon, Ring}; use crate::{SimpleRow, create_simple_table, execute_statements, get_client, insert_and_select}; use clickhouse::Row; +use clickhouse::query::QI; use clickhouse::sql::Identifier; use fxhash::FxHashMap; use indexmap::IndexMap; @@ -463,7 +464,7 @@ async fn enums() { let client = prepare_database!(); client - .query( + .query_with_flags::<{ QI::BIND }>( " CREATE OR REPLACE TABLE ? ( @@ -1433,7 +1434,7 @@ async fn ephemeral_columns() { let client = prepare_database!(); client - .query( + .query_with_flags::<{ QI::BIND }>( " CREATE OR REPLACE TABLE ? ( @@ -1468,7 +1469,7 @@ async fn ephemeral_columns() { insert.end().await.unwrap(); let rows = client - .query("SELECT ?fields FROM ? ORDER BY () ASC") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ? ORDER BY () ASC") .bind(Identifier(table_name)) .fetch_all::() .await @@ -1524,7 +1525,7 @@ async fn materialized_columns() { .await; let rows = client - .query("SELECT ?fields FROM ? ORDER BY x ASC") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ? ORDER BY x ASC") .bind(Identifier(table_name)) .fetch_all::() .await @@ -1559,7 +1560,7 @@ async fn materialized_columns() { insert.end().await.unwrap(); let rows_after_insert = client - .query("SELECT ?fields FROM ? ORDER BY x ASC") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ? ORDER BY x ASC") .bind(Identifier(table_name)) .fetch_all::() .await @@ -1615,7 +1616,7 @@ async fn alias_columns() { .await; let rows = client - .query("SELECT ?fields FROM ?") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ?") .bind(Identifier(table_name)) .fetch_all::() .await @@ -1647,7 +1648,7 @@ async fn alias_columns() { insert.end().await.unwrap(); let rows_after_insert = client - .query("SELECT ?fields FROM ? ORDER BY id ASC") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ? ORDER BY id ASC") .bind(Identifier(table_name)) .fetch_all::() .await diff --git a/tests/it/user_agent.rs b/tests/it/user_agent.rs index afe565ac..cbacbbd7 100644 --- a/tests/it/user_agent.rs +++ b/tests/it/user_agent.rs @@ -1,7 +1,10 @@ use crate::{SimpleRow, create_simple_table, flush_query_log}; -use clickhouse::Client; -use clickhouse::sql::Identifier; +use clickhouse::{Client, query::QI, sql::Identifier}; +//new changes +// use crate::{SimpleRow, create_simple_table, flush_query_log}; +// use clickhouse::Client; +// use clickhouse::sql::Identifier; const PKG_VER: &str = env!("CARGO_PKG_VERSION"); const RUST_VER: &str = env!("CARGO_PKG_RUST_VERSION"); const OS: &str = std::env::consts::OS; @@ -45,7 +48,7 @@ async fn assert_queries_user_agents(client: &Client, table_name: &str, expected_ insert.end().await.unwrap(); let rows = client - .query("SELECT ?fields FROM ?") + .query_with_flags::<{ QI::FIELDS | QI::BIND }>("SELECT ?fields FROM ?") .bind(Identifier(table_name)) .fetch_all::() .await