Skip to main content

testcontainers_modules/clickhouse/
mod.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use testcontainers::{
4    core::{wait::HttpWaitStrategy, ContainerPort, WaitFor},
5    Image,
6};
7
8const DEFAULT_IMAGE_NAME: &str = "clickhouse/clickhouse-server";
9const DEFAULT_IMAGE_TAG: &str = "23.3.8.21-alpine";
10
11/// Port that the [`ClickHouse`] container has internally
12/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
13///
14/// [`ClickHouse`]: https://clickhouse.com/
15pub const CLICKHOUSE_PORT: ContainerPort = ContainerPort::Tcp(8123);
16
17/// Module to work with [`ClickHouse`] inside of tests.
18///
19/// This module is based on the official [`ClickHouse docker image`].
20///
21/// # Example
22/// ```
23/// use testcontainers_modules::{clickhouse, testcontainers::runners::SyncRunner};
24///
25/// let clickhouse = clickhouse::ClickHouse::default().start().unwrap();
26/// let http_port = clickhouse.get_host_port_ipv4(8123).unwrap();
27///
28/// // do something with the started clickhouse instance..
29/// ```
30///
31/// [`ClickHouse`]: https://clickhouse.com/
32/// [`Clickhouse docker image`]: https://hub.docker.com/r/clickhouse/clickhouse-server
33#[derive(Debug, Default, Clone)]
34pub struct ClickHouse {
35    env_vars: BTreeMap<String, String>,
36}
37
38impl Image for ClickHouse {
39    fn name(&self) -> &str {
40        DEFAULT_IMAGE_NAME
41    }
42
43    fn tag(&self) -> &str {
44        DEFAULT_IMAGE_TAG
45    }
46
47    fn ready_conditions(&self) -> Vec<WaitFor> {
48        vec![WaitFor::http(
49            HttpWaitStrategy::new("/")
50                .with_port(CLICKHOUSE_PORT)
51                .with_expected_status_code(200_u16),
52        )]
53    }
54
55    fn env_vars(
56        &self,
57    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
58        &self.env_vars
59    }
60
61    fn expose_ports(&self) -> &[ContainerPort] {
62        &[CLICKHOUSE_PORT]
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use clickhouse::Row;
69    use reqwest::Client;
70    use serde::Deserialize;
71
72    use crate::{clickhouse::ClickHouse as ClickhouseImage, testcontainers::runners::AsyncRunner};
73
74    #[tokio::test]
75    async fn clickhouse_db() -> Result<(), Box<dyn std::error::Error + 'static>> {
76        let clickhouse = ClickhouseImage::default();
77        let node = clickhouse.start().await?;
78
79        let host = node.get_host().await?;
80        let port = node.get_host_port_ipv4(8123).await?;
81        let url = format!("http://{}:{}", host, port);
82
83        // testing http endpoint
84        // curl http://localhost:8123/ping and check if the response is "Ok."
85        let response = Client::new().get(format!("{}/ping", url)).send().await?;
86        assert_eq!(response.status(), 200);
87
88        // create table
89        let query = "CREATE TABLE t (a UInt8) ENGINE = Memory";
90        let response = Client::new().post(url.clone()).body(query).send().await?;
91        assert_eq!(response.status(), 200);
92
93        // insert data
94        let query = "INSERT INTO t VALUES (1),(2),(3)";
95        let response = Client::new().post(url.clone()).body(query).send().await?;
96        assert_eq!(response.status(), 200);
97
98        // query data
99        let query = "SELECT * FROM t";
100        let response = Client::new().post(url.clone()).body(query).send().await?;
101        assert_eq!(response.status(), 200);
102
103        let client = clickhouse::Client::default().with_url(format!("http://{host}:{port}"));
104        #[derive(Row, Deserialize)]
105        struct MyRow {
106            #[serde(rename = "a")] // we don't read the field, so it's a dead-code in tests
107            _a: u8,
108        }
109        let rows = client.query("SELECT * FROM t").fetch_all::<MyRow>().await?;
110        assert_eq!(rows.len(), 3);
111
112        Ok(())
113    }
114}