diff --git a/CHANGELOG.md b/CHANGELOG.md index 062dc5ce..1c228685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +### Added + +- sql: added `QualifiedIdentifier` type to represent qualified identifiers (e.g., `table.column`) safely as client-side bind values + ## [0.14.0] - 2025-10-08 ### Removed @@ -28,13 +32,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mock server, so it properly handles the response format and automatically disables parsing `RowBinaryWithNamesAndTypes` header parsing and validation. Additionally, it is not required to call `with_url` explicitly. See the [updated example](./examples/mock.rs). -- **BREAKING** query: `Query::fetch_bytes()` now expects `impl AsRef` for `format` instead of `Into`. +- **BREAKING** query: `Query::fetch_bytes()` now expects `impl AsRef` for `format` instead of `Into`. Most usages should not be affected, however, unless passing a custom type that implements the latter but not the former. ([#311]) - query: due to `RowBinaryWithNamesAndTypes` format usage, there might be an impact on fetch performance, which largely depends on how the dataset is defined. If you notice decreased performance, consider disabling validation by using `Client::with_validation(false)`. -- serde: it is now possible to deserialize Map ClickHouse type into `HashMap` (or `BTreeMap`, `IndexMap`, +- serde: it is now possible to deserialize Map ClickHouse type into `HashMap` (or `BTreeMap`, `IndexMap`, `DashMap`, etc.). - tls: improved error messages in case of missing TLS features when using HTTPS ([#229]). - crate: MSRV is now 1.79 due to borrowed rows support redesign in [#247]. @@ -55,7 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- client: extract the exception code from `X-ClickHouse-Exception-Code` in case of incorrect 200 OK response +- client: extract the exception code from `X-ClickHouse-Exception-Code` in case of incorrect 200 OK response that could occur with ClickHouse server up to versions 24.x ([#256]). - query: pass format as `?default_format` URL parameter instead of using `FORMAT` clause, allowing queries to have trailing comments and/or semicolons ([#267], [#269], [#311]). diff --git a/src/sql/bind.rs b/src/sql/bind.rs index 4783621c..2e84c063 100644 --- a/src/sql/bind.rs +++ b/src/sql/bind.rs @@ -31,3 +31,55 @@ impl Bind for Identifier<'_> { escape::identifier(self.0, dst).map_err(|err| err.to_string()) } } + +/// A variant on `Identifier` which supports qualifying an identifier. For example, +/// `QualifiedIdentifier("foo", "bar")` will emit the SQL `\`foo\`.\`bar\``. +#[derive(Clone, Copy)] +pub struct QualifiedIdentifier<'a>(pub &'a str, pub &'a str); + +#[sealed] +impl Bind for QualifiedIdentifier<'_> { + #[inline] + fn write(&self, dst: &mut impl fmt::Write) -> Result<(), String> { + if !self.0.is_empty() { + escape::identifier(self.0, dst).map_err(|err| err.to_string())?; + dst.write_char('.').map_err(|err| err.to_string())?; + } + escape::identifier(self.1, dst).map_err(|err| err.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::{Bind, QualifiedIdentifier}; + + fn bind_to_string(b: impl Bind) -> String { + let mut s = String::new(); + b.write(&mut s).expect("bind should succeed"); + s + } + + #[test] + fn test_qualified_identifier() { + assert_eq!( + bind_to_string(QualifiedIdentifier("foo", "bar baz")), + "`foo`.`bar baz`" + ); + assert_eq!( + bind_to_string(QualifiedIdentifier("", "bar baz")), + "`bar baz`" + ); + + assert_eq!( + bind_to_string(QualifiedIdentifier("`'.", ".................````")), + "`\\`\\'.`.`.................\\`\\`\\`\\``" + ); + + assert_eq!( + bind_to_string(QualifiedIdentifier("クリック", "ハウス")), + "`クリック`.`ハウス`" + ); + + assert_eq!(bind_to_string(QualifiedIdentifier(" ", " ")), "` `.` `"); + } +} diff --git a/src/sql/mod.rs b/src/sql/mod.rs index 560fe5dd..d09914f2 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -5,7 +5,7 @@ use crate::{ row::{self, Row}, }; -pub use bind::{Bind, Identifier}; +pub use bind::{Bind, Identifier, QualifiedIdentifier}; mod bind; pub(crate) mod escape;