clickhouse_arrow/query.rs
1use std::fmt;
2
3use uuid::Uuid;
4
5use crate::Result;
6use crate::io::ClickHouseWrite;
7use crate::prelude::SettingValue;
8use crate::settings::SETTING_FLAG_CUSTOM;
9
10/// An internal representation of a query id, meant to reduce costs when tracing, passing around,
11/// and converting to strings.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct Qid(Uuid);
14
15impl Default for Qid {
16 fn default() -> Self { Self::new() }
17}
18
19impl Qid {
20 /// Generate a new `v4` [`Uuid`]
21 pub fn new() -> Self { Self(Uuid::new_v4()) }
22
23 /// Take the inner [`Uuid`]
24 pub fn into_inner(self) -> Uuid { self.0 }
25
26 // Convert to 32-char hex string, no heap allocation
27 pub(crate) async fn write_id<W: ClickHouseWrite>(&self, writer: &mut W) -> Result<()> {
28 let mut buffer = [0u8; 32];
29 let hex = self.0.as_simple().encode_lower(&mut buffer);
30 writer.write_string(hex).await
31 }
32
33 // Helper to calculate a determinstic hash from a qid
34 #[cfg_attr(not(feature = "inner_pool"), expect(unused))]
35 pub(crate) fn key(self) -> usize {
36 self.into_inner().as_bytes().iter().copied().map(usize::from).sum::<usize>()
37 }
38}
39
40impl<T: Into<Qid>> From<Option<T>> for Qid {
41 fn from(value: Option<T>) -> Self {
42 match value {
43 Some(v) => v.into(),
44 None => Qid::default(),
45 }
46 }
47}
48
49impl From<Uuid> for Qid {
50 fn from(id: Uuid) -> Self { Self(id) }
51}
52
53impl fmt::Display for Qid {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 // Use as_simple() for 32-char hex, no heap allocation
56 write!(f, "{}", self.0.as_simple())
57 }
58}
59
60/// Type alias to help distinguish settings from params
61pub type ParamValue = SettingValue;
62
63/// Represent parameters that can be passed to bind values during queries.
64///
65/// `ClickHouse` has very specific syntax for how it manages query parameters. Refer to their docs
66/// for more information.
67///
68/// See:
69/// [Queries with parameters](https://clickhouse.com/docs/interfaces/cli#cli-queries-with-parameters)
70#[derive(Debug, Clone, Default, PartialEq)]
71pub struct QueryParams(pub Vec<(String, ParamValue)>);
72
73impl<T, K, S> From<T> for QueryParams
74where
75 T: IntoIterator<Item = (K, S)>,
76 K: Into<String>,
77 ParamValue: From<S>,
78{
79 fn from(value: T) -> Self {
80 Self(value.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
81 }
82}
83
84impl<K, S> FromIterator<(K, S)> for QueryParams
85where
86 K: Into<String>,
87 ParamValue: From<S>,
88{
89 fn from_iter<T>(iter: T) -> Self
90 where
91 T: IntoIterator<Item = (K, S)>,
92 {
93 iter.into_iter().collect()
94 }
95}
96
97impl QueryParams {
98 /// Returns the number of query parameters.
99 pub(crate) fn len(&self) -> usize { self.0.len() }
100
101 /// Encodes query parameters to the `ClickHouse` native protocol.
102 ///
103 /// Parameters are encoded using the Settings wire format with custom flag:
104 /// - key (string)
105 /// - flags (varuint) - 0x02 (`settingFlagCustom`) for params
106 /// - value (string) - encoded as "field dump" format
107 ///
108 /// Field dump format follows `ClickHouse's` `Field::restoreFromDump`:
109 /// - Strings: `'value'` with escaped single quotes
110 /// - Numbers: raw numeric string (e.g., "42", "3.14")
111 /// - Booleans: "true" or "false"
112 ///
113 /// See: <https://github.com/ClickHouse/ClickHouse/blob/master/src/Core/Field.cpp#L312>
114 ///
115 /// # Errors
116 /// Returns an error if writing to the stream fails.
117 pub(crate) async fn encode<W: ClickHouseWrite>(
118 &self,
119 writer: &mut W,
120 _revision: u64,
121 ) -> Result<()> {
122 // Encode each parameter using Settings wire format
123 for (key, value) in &self.0 {
124 writer.write_string(key).await?;
125 writer.write_var_uint(SETTING_FLAG_CUSTOM).await?;
126
127 // Encode value as field dump
128 let field_dump = encode_field_dump(value);
129 writer.write_string(&field_dump).await?;
130 }
131 Ok(())
132 }
133}
134
135/// Encodes a `SettingValue` as a `ClickHouse` field dump string for query parameters.
136///
137/// **IMPORTANT**: `ClickHouse's` native protocol only supports **string** parameters!
138/// Non-string values are converted to their string representation.
139///
140/// This is because `ClickHouse` calls `Settings::toNameToNameMap()` on received parameters,
141/// which requires all values to be parseable as quoted strings. The `{param:Type}` syntax
142/// in queries tells `ClickHouse` how to cast the string parameter to the desired type.
143///
144/// Field dump format for parameters:
145/// - All values are encoded as quoted strings: `'value'`
146/// - Single quotes within strings are escaped: `'` -> `\'`
147///
148/// # Examples
149/// ```rust,ignore
150/// encode_field_dump(&SettingValue::String("hello")) // "'hello'"
151/// encode_field_dump(&SettingValue::String("it's")) // "'it\\'s'"
152/// encode_field_dump(&SettingValue::Int(42)) // "'42'"
153/// encode_field_dump(&SettingValue::Float(3.14)) // "'3.14'"
154/// encode_field_dump(&SettingValue::Bool(true)) // "'true'"
155/// ```
156///
157/// See: <https://github.com/ClickHouse/ClickHouse/blob/master/src/Server/TCPHandler.cpp>
158fn encode_field_dump(value: &SettingValue) -> String {
159 // All parameter values must be strings for toNameToNameMap() to work
160 match value {
161 SettingValue::String(s) => format!("'{}'", s.replace('\'', "\\'")),
162 SettingValue::Int(i) => format!("'{i}'"),
163 SettingValue::Float(f) => format!("'{f}'"),
164 SettingValue::Bool(b) => format!("'{b}'"),
165 }
166}
167
168/// Represents a parsed query.
169///
170/// In the future this will enable better validation of queries, possibly
171/// saving a roundtrip to the database.
172#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct ParsedQuery(pub(crate) String);
175
176impl std::ops::Deref for ParsedQuery {
177 type Target = String;
178
179 fn deref(&self) -> &Self::Target { &self.0 }
180}
181
182impl fmt::Display for ParsedQuery {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) }
184}
185
186impl From<String> for ParsedQuery {
187 fn from(q: String) -> ParsedQuery { ParsedQuery(q.trim().to_string()) }
188}
189
190impl From<&str> for ParsedQuery {
191 fn from(q: &str) -> ParsedQuery { ParsedQuery(q.trim().to_string()) }
192}
193
194impl From<&String> for ParsedQuery {
195 fn from(q: &String) -> ParsedQuery { ParsedQuery(q.trim().to_string()) }
196}