clickhouse/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4
5#[macro_use]
6extern crate static_assertions;
7
8use self::{error::Result, http_client::HttpClient};
9use std::{collections::HashMap, fmt::Display, sync::Arc};
10
11pub use self::{compression::Compression, row::Row};
12pub use clickhouse_derive::Row;
13
14pub mod error;
15pub mod insert;
16#[cfg(feature = "inserter")]
17pub mod inserter;
18pub mod query;
19pub mod serde;
20pub mod sql;
21#[cfg(feature = "test-util")]
22pub mod test;
23#[cfg(feature = "watch")]
24pub mod watch;
25
26mod bytes_ext;
27mod compression;
28mod cursors;
29mod headers;
30mod http_client;
31mod request_body;
32mod response;
33mod row;
34mod rowbinary;
35#[cfg(feature = "inserter")]
36mod ticks;
37
38/// A client containing HTTP pool.
39#[derive(Clone)]
40pub struct Client {
41    http: Arc<dyn HttpClient>,
42
43    url: String,
44    database: Option<String>,
45    authentication: Authentication,
46    compression: Compression,
47    options: HashMap<String, String>,
48    headers: HashMap<String, String>,
49    products_info: Vec<ProductInfo>,
50}
51
52#[derive(Clone)]
53struct ProductInfo {
54    name: String,
55    version: String,
56}
57
58impl Display for ProductInfo {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(f, "{}/{}", self.name, self.version)
61    }
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub(crate) enum Authentication {
66    Credentials {
67        user: Option<String>,
68        password: Option<String>,
69    },
70    Jwt {
71        access_token: String,
72    },
73}
74
75impl Default for Authentication {
76    fn default() -> Self {
77        Self::Credentials {
78            user: None,
79            password: None,
80        }
81    }
82}
83
84impl Default for Client {
85    fn default() -> Self {
86        Self::with_http_client(http_client::default())
87    }
88}
89
90impl Client {
91    /// Creates a new client with a specified underlying HTTP client.
92    ///
93    /// See `HttpClient` for details.
94    pub fn with_http_client(client: impl HttpClient) -> Self {
95        Self {
96            http: Arc::new(client),
97            url: String::new(),
98            database: None,
99            authentication: Authentication::default(),
100            compression: Compression::default(),
101            options: HashMap::new(),
102            headers: HashMap::new(),
103            products_info: Vec::default(),
104        }
105    }
106
107    /// Specifies ClickHouse's url. Should point to HTTP endpoint.
108    ///
109    /// # Examples
110    /// ```
111    /// # use clickhouse::Client;
112    /// let client = Client::default().with_url("http://localhost:8123");
113    /// ```
114    pub fn with_url(mut self, url: impl Into<String>) -> Self {
115        self.url = url.into();
116        self
117    }
118
119    /// Specifies a database name.
120    ///
121    /// # Examples
122    /// ```
123    /// # use clickhouse::Client;
124    /// let client = Client::default().with_database("test");
125    /// ```
126    pub fn with_database(mut self, database: impl Into<String>) -> Self {
127        self.database = Some(database.into());
128        self
129    }
130
131    /// Specifies a user.
132    ///
133    /// # Panics
134    /// If called after [`Client::with_access_token`].
135    ///
136    /// # Examples
137    /// ```
138    /// # use clickhouse::Client;
139    /// let client = Client::default().with_user("test");
140    /// ```
141    pub fn with_user(mut self, user: impl Into<String>) -> Self {
142        match self.authentication {
143            Authentication::Jwt { .. } => {
144                panic!("`user` cannot be set together with `access_token`");
145            }
146            Authentication::Credentials { password, .. } => {
147                self.authentication = Authentication::Credentials {
148                    user: Some(user.into()),
149                    password,
150                };
151            }
152        }
153        self
154    }
155
156    /// Specifies a password.
157    ///
158    /// # Panics
159    /// If called after [`Client::with_access_token`].
160    ///
161    /// # Examples
162    /// ```
163    /// # use clickhouse::Client;
164    /// let client = Client::default().with_password("secret");
165    /// ```
166    pub fn with_password(mut self, password: impl Into<String>) -> Self {
167        match self.authentication {
168            Authentication::Jwt { .. } => {
169                panic!("`password` cannot be set together with `access_token`");
170            }
171            Authentication::Credentials { user, .. } => {
172                self.authentication = Authentication::Credentials {
173                    user,
174                    password: Some(password.into()),
175                };
176            }
177        }
178        self
179    }
180
181    /// A JWT access token to authenticate with ClickHouse.
182    /// JWT token authentication is supported in ClickHouse Cloud only.
183    /// Should not be called after [`Client::with_user`] or [`Client::with_password`].
184    ///
185    /// # Panics
186    /// If called after [`Client::with_user`] or [`Client::with_password`].
187    ///
188    /// # Examples
189    /// ```
190    /// # use clickhouse::Client;
191    /// let client = Client::default().with_access_token("jwt");
192    /// ```
193    pub fn with_access_token(mut self, access_token: impl Into<String>) -> Self {
194        match self.authentication {
195            Authentication::Credentials { user, password }
196                if user.is_some() || password.is_some() =>
197            {
198                panic!("`access_token` cannot be set together with `user` or `password`");
199            }
200            _ => {
201                self.authentication = Authentication::Jwt {
202                    access_token: access_token.into(),
203                }
204            }
205        }
206        self
207    }
208
209    /// Specifies a compression mode. See [`Compression`] for details.
210    /// By default, `Lz4` is used.
211    ///
212    /// # Examples
213    /// ```
214    /// # use clickhouse::{Client, Compression};
215    /// # #[cfg(feature = "lz4")]
216    /// let client = Client::default().with_compression(Compression::Lz4);
217    /// ```
218    pub fn with_compression(mut self, compression: Compression) -> Self {
219        self.compression = compression;
220        self
221    }
222
223    /// Used to specify options that will be passed to all queries.
224    ///
225    /// # Example
226    /// ```
227    /// # use clickhouse::Client;
228    /// Client::default().with_option("allow_nondeterministic_mutations", "1");
229    /// ```
230    pub fn with_option(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
231        self.options.insert(name.into(), value.into());
232        self
233    }
234
235    /// Used to specify a header that will be passed to all queries.
236    ///
237    /// # Example
238    /// ```
239    /// # use clickhouse::Client;
240    /// Client::default().with_header("Cookie", "A=1");
241    /// ```
242    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
243        self.headers.insert(name.into(), value.into());
244        self
245    }
246
247    /// Specifies the product name and version that will be included
248    /// in the default User-Agent header. Multiple products are supported.
249    /// This could be useful for the applications built on top of this client.
250    ///
251    /// # Examples
252    ///
253    /// Sample default User-Agent header:
254    ///
255    /// ```plaintext
256    /// clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
257    /// ```
258    ///
259    /// Sample User-Agent with a single product information:
260    ///
261    /// ```
262    /// # use clickhouse::Client;
263    /// let client = Client::default().with_product_info("MyDataSource", "v1.0.0");
264    /// ```
265    ///
266    /// ```plaintext
267    /// MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
268    /// ```
269    ///
270    /// Sample User-Agent with multiple products information
271    /// (NB: the products are added in the reverse order of
272    /// [`Client::with_product_info`] calls, which could be useful to add
273    /// higher abstraction layers first):
274    ///
275    /// ```
276    /// # use clickhouse::Client;
277    /// let client = Client::default()
278    ///     .with_product_info("MyDataSource", "v1.0.0")
279    ///     .with_product_info("MyApp", "0.0.1");
280    /// ```
281    ///
282    /// ```plaintext
283    /// MyApp/0.0.1 MyDataSource/v1.0.0 clickhouse-rs/0.12.2 (lv:rust/1.67.0, os:macos)
284    /// ```
285    pub fn with_product_info(
286        mut self,
287        product_name: impl Into<String>,
288        product_version: impl Into<String>,
289    ) -> Self {
290        self.products_info.push(ProductInfo {
291            name: product_name.into(),
292            version: product_version.into(),
293        });
294        self
295    }
296
297    /// Starts a new INSERT statement.
298    ///
299    /// # Panics
300    /// If `T` has unnamed fields, e.g. tuples.
301    pub fn insert<T: Row>(&self, table: &str) -> Result<insert::Insert<T>> {
302        insert::Insert::new(self, table)
303    }
304
305    /// Creates an inserter to perform multiple INSERTs.
306    #[cfg(feature = "inserter")]
307    pub fn inserter<T: Row>(&self, table: &str) -> Result<inserter::Inserter<T>> {
308        inserter::Inserter::new(self, table)
309    }
310
311    /// Starts a new SELECT/DDL query.
312    pub fn query(&self, query: &str) -> query::Query {
313        query::Query::new(self, query)
314    }
315
316    /// Starts a new WATCH query.
317    ///
318    /// The `query` can be either the table name or a SELECT query.
319    /// In the second case, a new LV table is created.
320    #[cfg(feature = "watch")]
321    pub fn watch(&self, query: &str) -> watch::Watch {
322        watch::Watch::new(self, query)
323    }
324
325    /// Used internally to modify the options map of an _already cloned_
326    /// [`Client`] instance.
327    pub(crate) fn add_option(&mut self, name: impl Into<String>, value: impl Into<String>) {
328        self.options.insert(name.into(), value.into());
329    }
330}
331
332/// This is a private API exported only for internal purposes.
333/// Do not use it in your code directly, it doesn't follow semver.
334#[doc(hidden)]
335pub mod _priv {
336    #[cfg(feature = "lz4")]
337    pub fn lz4_compress(uncompressed: &[u8]) -> super::Result<bytes::Bytes> {
338        crate::compression::lz4::compress(uncompressed)
339    }
340}
341
342#[cfg(test)]
343mod client_tests {
344    use crate::{Authentication, Client};
345
346    #[test]
347    fn it_can_use_credentials_auth() {
348        assert_eq!(
349            Client::default()
350                .with_user("bob")
351                .with_password("secret")
352                .authentication,
353            Authentication::Credentials {
354                user: Some("bob".into()),
355                password: Some("secret".into()),
356            }
357        );
358    }
359
360    #[test]
361    fn it_can_use_credentials_auth_user_only() {
362        assert_eq!(
363            Client::default().with_user("alice").authentication,
364            Authentication::Credentials {
365                user: Some("alice".into()),
366                password: None,
367            }
368        );
369    }
370
371    #[test]
372    fn it_can_use_credentials_auth_password_only() {
373        assert_eq!(
374            Client::default().with_password("secret").authentication,
375            Authentication::Credentials {
376                user: None,
377                password: Some("secret".into()),
378            }
379        );
380    }
381
382    #[test]
383    fn it_can_override_credentials_auth() {
384        assert_eq!(
385            Client::default()
386                .with_user("bob")
387                .with_password("secret")
388                .with_user("alice")
389                .with_password("something_else")
390                .authentication,
391            Authentication::Credentials {
392                user: Some("alice".into()),
393                password: Some("something_else".into()),
394            }
395        );
396    }
397
398    #[test]
399    fn it_can_use_jwt_auth() {
400        assert_eq!(
401            Client::default().with_access_token("my_jwt").authentication,
402            Authentication::Jwt {
403                access_token: "my_jwt".into(),
404            }
405        );
406    }
407
408    #[test]
409    fn it_can_override_jwt_auth() {
410        assert_eq!(
411            Client::default()
412                .with_access_token("my_jwt")
413                .with_access_token("my_jwt_2")
414                .authentication,
415            Authentication::Jwt {
416                access_token: "my_jwt_2".into(),
417            }
418        );
419    }
420
421    #[test]
422    #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")]
423    fn it_cannot_use_jwt_after_with_user() {
424        let _ = Client::default()
425            .with_user("bob")
426            .with_access_token("my_jwt");
427    }
428
429    #[test]
430    #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")]
431    fn it_cannot_use_jwt_after_with_password() {
432        let _ = Client::default()
433            .with_password("secret")
434            .with_access_token("my_jwt");
435    }
436
437    #[test]
438    #[should_panic(expected = "`access_token` cannot be set together with `user` or `password`")]
439    fn it_cannot_use_jwt_after_both_with_user_and_with_password() {
440        let _ = Client::default()
441            .with_user("alice")
442            .with_password("secret")
443            .with_access_token("my_jwt");
444    }
445
446    #[test]
447    #[should_panic(expected = "`user` cannot be set together with `access_token`")]
448    fn it_cannot_use_with_user_after_jwt() {
449        let _ = Client::default()
450            .with_access_token("my_jwt")
451            .with_user("alice");
452    }
453
454    #[test]
455    #[should_panic(expected = "`password` cannot be set together with `access_token`")]
456    fn it_cannot_use_with_password_after_jwt() {
457        let _ = Client::default()
458            .with_access_token("my_jwt")
459            .with_password("secret");
460    }
461}