Skip to main content

risingwave_connector/sink/
clickhouse.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use core::fmt::Debug;
16use core::num::NonZeroU64;
17use std::collections::{BTreeMap, HashMap, HashSet};
18
19use anyhow::anyhow;
20use bytes::{BufMut, BytesMut};
21use clickhouse::insert_formatted::InsertFormatted;
22use clickhouse::{Client as ClickHouseClient, Row as ClickHouseRow};
23use clickhouse_types::put_leb128;
24use itertools::Itertools;
25use phf::{Set, phf_set};
26use risingwave_common::array::{Op, StreamChunk};
27use risingwave_common::catalog::{FieldLike, Schema};
28use risingwave_common::row::Row;
29use risingwave_common::types::{DataType, Decimal, ScalarRefImpl, Serial};
30use risingwave_common::util::iter_util::ZipEqDebug;
31use serde::Deserialize;
32use serde_with::{DisplayFromStr, serde_as};
33use thiserror_ext::AsReport;
34use tonic::async_trait;
35use tracing::warn;
36use with_options::WithOptions;
37
38use super::decouple_checkpoint_log_sink::{
39    DecoupleCheckpointLogSinkerOf, default_commit_checkpoint_interval,
40};
41use super::writer::SinkWriter;
42use super::{SinkWriterMetrics, SinkWriterParam};
43use crate::enforce_secret::EnforceSecret;
44use crate::error::ConnectorResult;
45use crate::sink::{
46    Result, SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, Sink, SinkError, SinkParam,
47};
48
49const QUERY_ENGINE: &str =
50    "select distinct ?fields from system.tables where database = ? and name = ?";
51const QUERY_COLUMN: &str =
52    "select distinct ?fields from system.columns where database = ? and table = ? order by ?";
53pub const CLICKHOUSE_SINK: &str = "clickhouse";
54
55const ALLOW_EXPERIMENTAL_JSON_TYPE: &str = "allow_experimental_json_type";
56const INPUT_FORMAT_BINARY_READ_JSON_AS_STRING: &str = "input_format_binary_read_json_as_string";
57const OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING: &str = "output_format_binary_write_json_as_string";
58
59/// `ClickHouse` backs `Decimal(P, S)` with `Decimal256` once `P > 38`, which takes 32 bytes on the
60/// wire, while [`ClickHouseDecimal`] never writes more than 16.
61const MAX_ENCODABLE_DECIMAL_PRECISION: u8 = 38;
62
63/// Encoded rows are sent once they reach this size, each send compressed as one block.
64const INSERT_CHUNK_SIZE: usize = 256 * 1024;
65
66#[serde_as]
67#[derive(Deserialize, Debug, Clone, WithOptions)]
68pub struct ClickHouseCommon {
69    #[serde(rename = "clickhouse.url")]
70    pub url: String,
71    #[serde(rename = "clickhouse.user")]
72    pub user: String,
73    #[serde(rename = "clickhouse.password")]
74    pub password: String,
75    #[serde(rename = "clickhouse.database")]
76    pub database: String,
77    #[serde(rename = "clickhouse.table")]
78    pub table: String,
79    #[serde(rename = "clickhouse.delete.column")]
80    pub delete_column: Option<String>,
81    /// Commit every n(>0) checkpoints, default is 10.
82    #[serde(default = "default_commit_checkpoint_interval")]
83    #[serde_as(as = "DisplayFromStr")]
84    #[with_option(allow_alter_on_fly)]
85    pub commit_checkpoint_interval: u64,
86}
87
88impl EnforceSecret for ClickHouseCommon {
89    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
90        "clickhouse.password", "clickhouse.user"
91    };
92}
93
94#[derive(Debug)]
95enum ClickHouseEngine {
96    MergeTree,
97    ReplacingMergeTree(Option<String>),
98    SummingMergeTree,
99    AggregatingMergeTree,
100    CollapsingMergeTree(String),
101    VersionedCollapsingMergeTree(String),
102    GraphiteMergeTree,
103    ReplicatedMergeTree,
104    ReplicatedReplacingMergeTree(Option<String>),
105    ReplicatedSummingMergeTree,
106    ReplicatedAggregatingMergeTree,
107    ReplicatedCollapsingMergeTree(String),
108    ReplicatedVersionedCollapsingMergeTree(String),
109    ReplicatedGraphiteMergeTree,
110    SharedMergeTree,
111    SharedReplacingMergeTree(Option<String>),
112    SharedSummingMergeTree,
113    SharedAggregatingMergeTree,
114    SharedCollapsingMergeTree(String),
115    SharedVersionedCollapsingMergeTree(String),
116    SharedGraphiteMergeTree,
117    Null,
118}
119impl ClickHouseEngine {
120    pub fn is_collapsing_engine(&self) -> bool {
121        matches!(
122            self,
123            ClickHouseEngine::CollapsingMergeTree(_)
124                | ClickHouseEngine::VersionedCollapsingMergeTree(_)
125                | ClickHouseEngine::ReplicatedCollapsingMergeTree(_)
126                | ClickHouseEngine::ReplicatedVersionedCollapsingMergeTree(_)
127                | ClickHouseEngine::SharedCollapsingMergeTree(_)
128                | ClickHouseEngine::SharedVersionedCollapsingMergeTree(_)
129        )
130    }
131
132    pub fn is_delete_replacing_engine(&self) -> bool {
133        match self {
134            ClickHouseEngine::ReplacingMergeTree(delete_col) => delete_col.is_some(),
135            ClickHouseEngine::ReplicatedReplacingMergeTree(delete_col) => delete_col.is_some(),
136            ClickHouseEngine::SharedReplacingMergeTree(delete_col) => delete_col.is_some(),
137            _ => false,
138        }
139    }
140
141    pub fn get_delete_col(&self) -> Option<String> {
142        match self {
143            ClickHouseEngine::ReplacingMergeTree(Some(delete_col)) => Some(delete_col.clone()),
144            ClickHouseEngine::ReplicatedReplacingMergeTree(Some(delete_col)) => {
145                Some(delete_col.clone())
146            }
147            ClickHouseEngine::SharedReplacingMergeTree(Some(delete_col)) => {
148                Some(delete_col.clone())
149            }
150            _ => None,
151        }
152    }
153
154    pub fn get_sign_name(&self) -> Option<String> {
155        match self {
156            ClickHouseEngine::CollapsingMergeTree(sign_name) => Some(sign_name.clone()),
157            ClickHouseEngine::VersionedCollapsingMergeTree(sign_name) => Some(sign_name.clone()),
158            ClickHouseEngine::ReplicatedCollapsingMergeTree(sign_name) => Some(sign_name.clone()),
159            ClickHouseEngine::ReplicatedVersionedCollapsingMergeTree(sign_name) => {
160                Some(sign_name.clone())
161            }
162            ClickHouseEngine::SharedCollapsingMergeTree(sign_name) => Some(sign_name.clone()),
163            ClickHouseEngine::SharedVersionedCollapsingMergeTree(sign_name) => {
164                Some(sign_name.clone())
165            }
166            _ => None,
167        }
168    }
169
170    pub fn is_shared_tree(&self) -> bool {
171        matches!(
172            self,
173            ClickHouseEngine::SharedMergeTree
174                | ClickHouseEngine::SharedReplacingMergeTree(_)
175                | ClickHouseEngine::SharedSummingMergeTree
176                | ClickHouseEngine::SharedAggregatingMergeTree
177                | ClickHouseEngine::SharedCollapsingMergeTree(_)
178                | ClickHouseEngine::SharedVersionedCollapsingMergeTree(_)
179                | ClickHouseEngine::SharedGraphiteMergeTree
180        )
181    }
182
183    pub fn from_query_engine(
184        engine_name: &ClickhouseQueryEngine,
185        config: &ClickHouseConfig,
186    ) -> Result<Self> {
187        match engine_name.engine.as_str() {
188            "MergeTree" => Ok(ClickHouseEngine::MergeTree),
189            "Null" => Ok(ClickHouseEngine::Null),
190            "ReplacingMergeTree" => {
191                let delete_column = config.common.delete_column.clone();
192                Ok(ClickHouseEngine::ReplacingMergeTree(delete_column))
193            }
194            "SummingMergeTree" => Ok(ClickHouseEngine::SummingMergeTree),
195            "AggregatingMergeTree" => Ok(ClickHouseEngine::AggregatingMergeTree),
196            // VersionedCollapsingMergeTree(sign_name,"a")
197            "VersionedCollapsingMergeTree" => {
198                let sign_name = engine_name
199                    .create_table_query
200                    .split("VersionedCollapsingMergeTree(")
201                    .last()
202                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
203                    .split(',')
204                    .next()
205                    .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
206                    .trim()
207                    .to_owned();
208                Ok(ClickHouseEngine::VersionedCollapsingMergeTree(sign_name))
209            }
210            // CollapsingMergeTree(sign_name)
211            "CollapsingMergeTree" => {
212                let sign_name = engine_name
213                    .create_table_query
214                    .split("CollapsingMergeTree(")
215                    .last()
216                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
217                    .split(')')
218                    .next()
219                    .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
220                    .trim()
221                    .to_owned();
222                Ok(ClickHouseEngine::CollapsingMergeTree(sign_name))
223            }
224            "GraphiteMergeTree" => Ok(ClickHouseEngine::GraphiteMergeTree),
225            "ReplicatedMergeTree" => Ok(ClickHouseEngine::ReplicatedMergeTree),
226            "ReplicatedReplacingMergeTree" => {
227                let delete_column = config.common.delete_column.clone();
228                Ok(ClickHouseEngine::ReplicatedReplacingMergeTree(
229                    delete_column,
230                ))
231            }
232            "ReplicatedSummingMergeTree" => Ok(ClickHouseEngine::ReplicatedSummingMergeTree),
233            "ReplicatedAggregatingMergeTree" => {
234                Ok(ClickHouseEngine::ReplicatedAggregatingMergeTree)
235            }
236            // ReplicatedVersionedCollapsingMergeTree("a","b",sign_name,"c")
237            "ReplicatedVersionedCollapsingMergeTree" => {
238                let sign_name = engine_name
239                    .create_table_query
240                    .split("ReplicatedVersionedCollapsingMergeTree(")
241                    .last()
242                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
243                    .split(',')
244                    .rev()
245                    .nth(1)
246                    .ok_or_else(|| SinkError::ClickHouse("must have index 1".to_owned()))?
247                    .trim()
248                    .to_owned();
249                Ok(ClickHouseEngine::ReplicatedVersionedCollapsingMergeTree(
250                    sign_name,
251                ))
252            }
253            // ReplicatedCollapsingMergeTree("a","b",sign_name)
254            "ReplicatedCollapsingMergeTree" => {
255                let sign_name = engine_name
256                    .create_table_query
257                    .split("ReplicatedCollapsingMergeTree(")
258                    .last()
259                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
260                    .split(')')
261                    .next()
262                    .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
263                    .split(',')
264                    .next_back()
265                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
266                    .trim()
267                    .to_owned();
268                Ok(ClickHouseEngine::ReplicatedCollapsingMergeTree(sign_name))
269            }
270            "ReplicatedGraphiteMergeTree" => Ok(ClickHouseEngine::ReplicatedGraphiteMergeTree),
271            "SharedMergeTree" => Ok(ClickHouseEngine::SharedMergeTree),
272            "SharedReplacingMergeTree" => {
273                let delete_column = config.common.delete_column.clone();
274                Ok(ClickHouseEngine::SharedReplacingMergeTree(delete_column))
275            }
276            "SharedSummingMergeTree" => Ok(ClickHouseEngine::SharedSummingMergeTree),
277            "SharedAggregatingMergeTree" => Ok(ClickHouseEngine::SharedAggregatingMergeTree),
278            // SharedVersionedCollapsingMergeTree("a","b",sign_name,"c")
279            "SharedVersionedCollapsingMergeTree" => {
280                let sign_name = engine_name
281                    .create_table_query
282                    .split("SharedVersionedCollapsingMergeTree(")
283                    .last()
284                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
285                    .split(',')
286                    .rev()
287                    .nth(1)
288                    .ok_or_else(|| SinkError::ClickHouse("must have index 1".to_owned()))?
289                    .trim()
290                    .to_owned();
291                Ok(ClickHouseEngine::SharedVersionedCollapsingMergeTree(
292                    sign_name,
293                ))
294            }
295            // SharedCollapsingMergeTree("a","b",sign_name)
296            "SharedCollapsingMergeTree" => {
297                let sign_name = engine_name
298                    .create_table_query
299                    .split("SharedCollapsingMergeTree(")
300                    .last()
301                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
302                    .split(')')
303                    .next()
304                    .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
305                    .split(',')
306                    .next_back()
307                    .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
308                    .trim()
309                    .to_owned();
310                Ok(ClickHouseEngine::SharedCollapsingMergeTree(sign_name))
311            }
312            "SharedGraphiteMergeTree" => Ok(ClickHouseEngine::SharedGraphiteMergeTree),
313            _ => Err(SinkError::ClickHouse(format!(
314                "Cannot find clickhouse engine {:?}",
315                engine_name.engine
316            ))),
317        }
318    }
319}
320
321impl ClickHouseCommon {
322    pub(crate) fn build_client(&self) -> ConnectorResult<ClickHouseClient> {
323        let client = ClickHouseClient::default() // hyper(0.14) client inside
324            .with_url(&self.url)
325            .with_user(&self.user)
326            .with_password(&self.password)
327            .with_database(&self.database)
328            .with_setting(ALLOW_EXPERIMENTAL_JSON_TYPE, "1")
329            .with_setting(INPUT_FORMAT_BINARY_READ_JSON_AS_STRING, "1")
330            .with_setting(OUTPUT_FORMAT_BINARY_WRITE_JSON_AS_STRING, "1");
331        Ok(client)
332    }
333}
334
335#[serde_as]
336#[derive(Clone, Debug, Deserialize, WithOptions)]
337pub struct ClickHouseConfig {
338    #[serde(flatten)]
339    pub common: ClickHouseCommon,
340
341    pub r#type: String, // accept "append-only" or "upsert"
342
343    #[serde(flatten)]
344    pub unknown_fields: std::collections::HashMap<String, String>,
345}
346
347crate::impl_sink_unknown_fields!(ClickHouseConfig);
348
349impl EnforceSecret for ClickHouseConfig {
350    fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
351        ClickHouseCommon::enforce_one(prop)
352    }
353
354    fn enforce_secret<'a>(
355        prop_iter: impl Iterator<Item = &'a str>,
356    ) -> crate::error::ConnectorResult<()> {
357        for prop in prop_iter {
358            ClickHouseCommon::enforce_one(prop)?;
359        }
360        Ok(())
361    }
362}
363
364#[derive(Clone, Debug)]
365pub struct ClickHouseSink {
366    pub config: ClickHouseConfig,
367    schema: Schema,
368    pk_indices: Vec<usize>,
369    is_append_only: bool,
370}
371
372impl EnforceSecret for ClickHouseSink {
373    fn enforce_secret<'a>(
374        prop_iter: impl Iterator<Item = &'a str>,
375    ) -> crate::error::ConnectorResult<()> {
376        for prop in prop_iter {
377            ClickHouseConfig::enforce_one(prop)?;
378        }
379        Ok(())
380    }
381}
382
383impl ClickHouseConfig {
384    pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
385        let config =
386            serde_json::from_value::<ClickHouseConfig>(serde_json::to_value(properties).unwrap())
387                .map_err(|e| SinkError::Config(anyhow!(e)))?;
388        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
389            return Err(SinkError::Config(anyhow!(
390                "`{}` must be {}, or {}",
391                SINK_TYPE_OPTION,
392                SINK_TYPE_APPEND_ONLY,
393                SINK_TYPE_UPSERT
394            )));
395        }
396        Ok(config)
397    }
398}
399
400impl TryFrom<SinkParam> for ClickHouseSink {
401    type Error = SinkError;
402
403    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
404        let schema = param.schema();
405        let pk_indices = param.downstream_pk_or_empty();
406        let config = ClickHouseConfig::from_btreemap(param.properties)?;
407        Ok(Self {
408            config,
409            schema,
410            pk_indices,
411            is_append_only: param.sink_type.is_append_only(),
412        })
413    }
414}
415
416impl ClickHouseSink {
417    /// Check that the column names and types of risingwave and clickhouse are identical
418    fn check_column_name_and_type(&self, clickhouse_columns_desc: &[SystemColumn]) -> Result<()> {
419        let rw_fields_name = build_fields_name_type_from_schema(&self.schema)?;
420        let clickhouse_columns_desc: HashMap<String, SystemColumn> = clickhouse_columns_desc
421            .iter()
422            .map(|s| (s.name.clone(), s.clone()))
423            .collect();
424
425        if rw_fields_name.len().gt(&clickhouse_columns_desc.len()) {
426            return Err(SinkError::ClickHouse("The columns of the sink must be equal to or a superset of the target table's columns.".to_owned()));
427        }
428
429        for i in rw_fields_name {
430            let value = clickhouse_columns_desc.get(&i.0).ok_or_else(|| {
431                SinkError::ClickHouse(format!(
432                    "Column name don't find in clickhouse, risingwave is {:?} ",
433                    i.0
434                ))
435            })?;
436
437            Self::check_and_correct_column_type(&i.1, value)?;
438        }
439        Ok(())
440    }
441
442    /// Check that the column names and types of risingwave and clickhouse are identical
443    fn check_pk_match(&self, clickhouse_columns_desc: &[SystemColumn]) -> Result<()> {
444        let mut clickhouse_pks: HashSet<String> = clickhouse_columns_desc
445            .iter()
446            .filter(|s| s.is_in_primary_key == 1)
447            .map(|s| s.name.clone())
448            .collect();
449
450        for (_, field) in self
451            .schema
452            .fields()
453            .iter()
454            .enumerate()
455            .filter(|(index, _)| self.pk_indices.contains(index))
456        {
457            if !clickhouse_pks.remove(&field.name) {
458                return Err(SinkError::ClickHouse(
459                    "Clicklhouse and RisingWave pk is not match".to_owned(),
460                ));
461            }
462        }
463
464        if !clickhouse_pks.is_empty() {
465            return Err(SinkError::ClickHouse(
466                "Clicklhouse and RisingWave pk is not match".to_owned(),
467            ));
468        }
469        Ok(())
470    }
471
472    /// Check that the column types of risingwave and clickhouse are identical
473    fn check_and_correct_column_type(
474        fields_type: &DataType,
475        ck_column: &SystemColumn,
476    ) -> Result<()> {
477        // FIXME: the "contains" based implementation is wrong
478        let is_match = match fields_type {
479            risingwave_common::types::DataType::Boolean => Ok(ck_column.r#type.contains("Bool")),
480            risingwave_common::types::DataType::Int16 => Ok(ck_column.r#type.contains("UInt16")
481                | ck_column.r#type.contains("Int16")
482                // Allow Int16 to be pushed to Enum16, they share an encoding and value range
483                // No special care is taken to ensure values are valid.
484                | ck_column.r#type.contains("Enum16")),
485            risingwave_common::types::DataType::Int32 => {
486                Ok(ck_column.r#type.contains("UInt32") | ck_column.r#type.contains("Int32"))
487            }
488            risingwave_common::types::DataType::Int64 => {
489                Ok(ck_column.r#type.contains("UInt64") | ck_column.r#type.contains("Int64"))
490            }
491            risingwave_common::types::DataType::Float32 => Ok(ck_column.r#type.contains("Float32")),
492            risingwave_common::types::DataType::Float64 => Ok(ck_column.r#type.contains("Float64")),
493            risingwave_common::types::DataType::Decimal => {
494                match parse_decimal_accuracy(&ck_column.r#type)? {
495                    Some((precision, _)) if precision > MAX_ENCODABLE_DECIMAL_PRECISION => {
496                        Err(SinkError::ClickHouse(format!(
497                            "column {:?} has ClickHouse type `{}`: precision {} is backed by Decimal256, \
498                             but this sink can only encode decimals up to precision {}. Lower the column \
499                             precision (recreating the table if it is a key column), or leave the column \
500                             out of the sink.",
501                            ck_column.name,
502                            ck_column.r#type,
503                            precision,
504                            MAX_ENCODABLE_DECIMAL_PRECISION,
505                        )))
506                    }
507                    accuracy => Ok(accuracy.is_some()),
508                }
509            }
510            risingwave_common::types::DataType::Date => Ok(ck_column.r#type.contains("Date32")),
511            risingwave_common::types::DataType::Varchar => Ok(ck_column.r#type.contains("String")),
512            risingwave_common::types::DataType::Time => Err(SinkError::ClickHouse(
513                "TIME is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
514            )),
515            risingwave_common::types::DataType::Timestamp => Err(SinkError::ClickHouse(
516                "clickhouse does not have a type corresponding to naive timestamp".to_owned(),
517            )),
518            risingwave_common::types::DataType::Timestamptz => {
519                Ok(ck_column.r#type.contains("DateTime64"))
520            }
521            risingwave_common::types::DataType::Interval => Err(SinkError::ClickHouse(
522                "INTERVAL is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
523            )),
524            risingwave_common::types::DataType::Struct(_) => Err(SinkError::ClickHouse(
525                "struct needs to be converted into a list".to_owned(),
526            )),
527            risingwave_common::types::DataType::List(list) => {
528                Self::check_and_correct_column_type(list.elem(), ck_column)?;
529                Ok(ck_column.r#type.contains("Array"))
530            }
531            risingwave_common::types::DataType::Bytea => Err(SinkError::ClickHouse(
532                "BYTEA is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
533            )),
534            risingwave_common::types::DataType::Jsonb => Ok(ck_column.r#type.contains("JSON")),
535            risingwave_common::types::DataType::Variant => Err(SinkError::ClickHouse(
536                "VARIANT is not supported for ClickHouse sink.".to_owned(),
537            )),
538            risingwave_common::types::DataType::Serial => {
539                Ok(ck_column.r#type.contains("UInt64") | ck_column.r#type.contains("Int64"))
540            }
541            risingwave_common::types::DataType::Int256 => Err(SinkError::ClickHouse(
542                "INT256 is not supported for ClickHouse sink.".to_owned(),
543            )),
544            risingwave_common::types::DataType::Map(_) => Err(SinkError::ClickHouse(
545                "MAP is not supported for ClickHouse sink.".to_owned(),
546            )),
547            DataType::Vector(_) => Err(SinkError::ClickHouse(
548                "VECTOR is not supported for ClickHouse sink.".to_owned(),
549            )),
550        };
551        if !is_match? {
552            return Err(SinkError::ClickHouse(format!(
553                "Column type mismatch for column {:?}: RisingWave type is {:?}, ClickHouse type is {:?}",
554                ck_column.name, fields_type, ck_column.r#type
555            )));
556        }
557
558        Ok(())
559    }
560}
561
562impl Sink for ClickHouseSink {
563    type LogSinker = DecoupleCheckpointLogSinkerOf<ClickHouseSinkWriter>;
564
565    const SINK_NAME: &'static str = CLICKHOUSE_SINK;
566
567    crate::impl_validate_sink_unknown_fields!();
568
569    async fn validate(&self) -> Result<()> {
570        // For upsert clickhouse sink, the primary key must be defined.
571        if !self.is_append_only && self.pk_indices.is_empty() {
572            return Err(SinkError::Config(anyhow!(
573                "Primary key not defined for upsert clickhouse sink (please define in `primary_key` field)"
574            )));
575        }
576
577        // check reachability
578        let client = self.config.common.build_client()?;
579
580        let (clickhouse_column, clickhouse_engine) =
581            query_column_engine_from_ck(client, &self.config).await?;
582        if clickhouse_engine.is_shared_tree() {
583            risingwave_common::license::Feature::ClickHouseSharedEngine
584                .check_available()
585                .map_err(|e| anyhow::anyhow!(e))?;
586        }
587
588        if !self.is_append_only
589            && !clickhouse_engine.is_collapsing_engine()
590            && !clickhouse_engine.is_delete_replacing_engine()
591        {
592            return match clickhouse_engine {
593                ClickHouseEngine::ReplicatedReplacingMergeTree(None) | ClickHouseEngine::ReplacingMergeTree(None) | ClickHouseEngine::SharedReplacingMergeTree(None) =>  {
594                    Err(SinkError::ClickHouse("To enable upsert with a `ReplacingMergeTree`, you must set a `clickhouse.delete.column` to the UInt8 column in ClickHouse used to signify deletes. See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree#is_deleted for more information".to_owned()))
595                }
596                _ => Err(SinkError::ClickHouse("If you want to use upsert, please use either `VersionedCollapsingMergeTree`, `CollapsingMergeTree` or the `ReplacingMergeTree` in ClickHouse".to_owned()))
597            };
598        }
599
600        self.check_column_name_and_type(&clickhouse_column)?;
601        if !self.is_append_only {
602            self.check_pk_match(&clickhouse_column)?;
603        }
604
605        if self.config.common.commit_checkpoint_interval == 0 {
606            return Err(SinkError::Config(anyhow!(
607                "`commit_checkpoint_interval` must be greater than 0"
608            )));
609        }
610        Ok(())
611    }
612
613    fn validate_alter_config(config: &BTreeMap<String, String>) -> Result<()> {
614        ClickHouseConfig::from_btreemap(config.clone())?;
615        Ok(())
616    }
617
618    async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
619        let writer = ClickHouseSinkWriter::new(
620            self.config.clone(),
621            self.schema.clone(),
622            self.pk_indices.clone(),
623            self.is_append_only,
624        )
625        .await?;
626        let commit_checkpoint_interval =
627    NonZeroU64::new(self.config.common.commit_checkpoint_interval).expect(
628        "commit_checkpoint_interval should be greater than 0, and it should be checked in config validation",
629    );
630
631        Ok(DecoupleCheckpointLogSinkerOf::new(
632            writer,
633            SinkWriterMetrics::new(&writer_param),
634            commit_checkpoint_interval,
635        ))
636    }
637}
638pub struct ClickHouseSinkWriter {
639    pub config: ClickHouseConfig,
640    schema: Schema,
641    #[expect(dead_code)]
642    pk_indices: Vec<usize>,
643    client: ClickHouseClient,
644    #[expect(dead_code)]
645    is_append_only: bool,
646    // Save some features of the clickhouse column type
647    column_correct_map: HashMap<String, ClickHouseSchemaFeature>,
648    rw_fields_name_after_calibration: Vec<String>,
649    clickhouse_engine: ClickHouseEngine,
650    inserter: Option<InsertFormatted>,
651    /// `RowBinary` rows not sent to `inserter` yet.
652    buf: BytesMut,
653}
654#[derive(Debug)]
655struct ClickHouseSchemaFeature {
656    can_null: bool,
657    // Time accuracy in clickhouse for rw and ck conversions
658    accuracy_time: u8,
659
660    accuracy_decimal: (u8, u8),
661}
662
663impl ClickHouseSinkWriter {
664    pub async fn new(
665        config: ClickHouseConfig,
666        schema: Schema,
667        pk_indices: Vec<usize>,
668        is_append_only: bool,
669    ) -> Result<Self> {
670        let client = config.common.build_client()?;
671
672        let (clickhouse_column, clickhouse_engine) =
673            query_column_engine_from_ck(client.clone(), &config).await?;
674
675        let column_correct_map: Result<HashMap<String, ClickHouseSchemaFeature>> =
676            clickhouse_column
677                .iter()
678                .map(Self::build_column_correct_map)
679                .collect();
680        let sink_fields = build_fields_name_type_from_schema(&schema)?;
681
682        // The table may have been altered since `CREATE SINK` validated it, and an under-sized
683        // decimal encoding would desync every following column of the same RowBinary row. Only
684        // columns the writer encodes as decimals are held to the decimal rule.
685        let ck_columns: HashMap<&str, &SystemColumn> = clickhouse_column
686            .iter()
687            .map(|c| (c.name.as_str(), c))
688            .collect();
689        for (name, data_type) in sink_fields.iter().filter(|(_, t)| contains_decimal(t)) {
690            if let Some(ck_column) = ck_columns.get(name.as_str()) {
691                ClickHouseSink::check_and_correct_column_type(data_type, ck_column)?;
692            }
693        }
694
695        let mut rw_fields_name_after_calibration =
696            sink_fields.iter().map(|(a, _)| a.clone()).collect_vec();
697
698        if let Some(sign) = clickhouse_engine.get_sign_name() {
699            rw_fields_name_after_calibration.push(sign);
700        }
701        if let Some(delete_col) = clickhouse_engine.get_delete_col() {
702            rw_fields_name_after_calibration.push(delete_col);
703        }
704        Ok(Self {
705            config,
706            schema,
707            pk_indices,
708            client,
709            is_append_only,
710            column_correct_map: column_correct_map?,
711            rw_fields_name_after_calibration,
712            clickhouse_engine,
713            inserter: None,
714            buf: BytesMut::new(),
715        })
716    }
717
718    /// Check if clickhouse's column is 'Nullable', valid bits of `DateTime64`. And save it in
719    /// `column_correct_map`
720    fn build_column_correct_map(
721        ck_column: &SystemColumn,
722    ) -> Result<(String, ClickHouseSchemaFeature)> {
723        let can_null = ck_column.r#type.contains("Nullable");
724        // `DateTime64` without precision is already displayed as `DateTime(3)` in `system.columns`.
725        let accuracy_time = if ck_column.r#type.contains("DateTime64(") {
726            ck_column
727                .r#type
728                .split("DateTime64(")
729                .last()
730                .ok_or_else(|| SinkError::ClickHouse("must have last".to_owned()))?
731                .split(')')
732                .next()
733                .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
734                .split(',')
735                .next()
736                .ok_or_else(|| SinkError::ClickHouse("must have next".to_owned()))?
737                .parse::<u8>()
738                .map_err(|e| SinkError::ClickHouse(e.to_report_string()))?
739        } else {
740            0_u8
741        };
742        let accuracy_decimal = parse_decimal_accuracy(&ck_column.r#type)?.unwrap_or((0, 0));
743        Ok((
744            ck_column.name.clone(),
745            ClickHouseSchemaFeature {
746                can_null,
747                accuracy_time,
748                accuracy_decimal,
749            },
750        ))
751    }
752
753    async fn write(&mut self, chunk: StreamChunk) -> Result<()> {
754        let inserter = self.inserter.get_or_insert_with(|| {
755            let sql = format!(
756                "INSERT INTO {}({}) FORMAT RowBinary",
757                self.config.common.table,
758                self.rw_fields_name_after_calibration.join(",")
759            );
760            self.client.insert_formatted_with(sql)
761        });
762        for (op, row) in chunk.rows() {
763            let mut clickhouse_filed_vec = vec![];
764            for (data, field) in row.iter().zip_eq_debug(self.schema.fields()) {
765                clickhouse_filed_vec.extend(ClickHouseFieldWithNull::from_scalar_ref(
766                    data,
767                    &self.column_correct_map,
768                    field.name(),
769                    &field.data_type(),
770                )?);
771            }
772            match op {
773                Op::Insert | Op::UpdateInsert => {
774                    if self.clickhouse_engine.is_collapsing_engine() {
775                        clickhouse_filed_vec.push(ClickHouseFieldWithNull::WithoutSome(
776                            ClickHouseField::Int8(1),
777                        ));
778                    }
779                    if self.clickhouse_engine.is_delete_replacing_engine() {
780                        clickhouse_filed_vec.push(ClickHouseFieldWithNull::WithoutSome(
781                            ClickHouseField::Int8(0),
782                        ))
783                    }
784                }
785                Op::Delete | Op::UpdateDelete => {
786                    if !self.clickhouse_engine.is_collapsing_engine()
787                        && !self.clickhouse_engine.is_delete_replacing_engine()
788                    {
789                        return Err(SinkError::ClickHouse(
790                            "Clickhouse engine don't support upsert".to_owned(),
791                        ));
792                    }
793                    if self.clickhouse_engine.is_collapsing_engine() {
794                        clickhouse_filed_vec.push(ClickHouseFieldWithNull::WithoutSome(
795                            ClickHouseField::Int8(-1),
796                        ));
797                    }
798                    if self.clickhouse_engine.is_delete_replacing_engine() {
799                        clickhouse_filed_vec.push(ClickHouseFieldWithNull::WithoutSome(
800                            ClickHouseField::Int8(1),
801                        ))
802                    }
803                }
804            }
805            for field in &clickhouse_filed_vec {
806                field.write_row_binary(&mut self.buf);
807            }
808            if self.buf.len() >= INSERT_CHUNK_SIZE {
809                inserter.send(self.buf.split().freeze()).await?;
810            }
811        }
812        Ok(())
813    }
814}
815
816#[async_trait]
817impl SinkWriter for ClickHouseSinkWriter {
818    async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()> {
819        self.write(chunk).await
820    }
821
822    async fn begin_epoch(&mut self, _epoch: u64) -> Result<()> {
823        Ok(())
824    }
825
826    async fn abort(&mut self) -> Result<()> {
827        Ok(())
828    }
829
830    async fn barrier(&mut self, is_checkpoint: bool) -> Result<()> {
831        if is_checkpoint && let Some(mut inserter) = self.inserter.take() {
832            if !self.buf.is_empty() {
833                inserter.send(self.buf.split().freeze()).await?;
834            }
835            inserter.end().await?;
836        }
837        Ok(())
838    }
839}
840
841#[derive(ClickHouseRow, Deserialize, Clone)]
842struct SystemColumn {
843    name: String,
844    r#type: String,
845    is_in_primary_key: u8,
846}
847
848/// Parse `(precision, scale)` out of a decimal type, or `None` if it is not a decimal.
849/// `system.columns` renders decimals canonically, so `Decimal128(10)` arrives as `Decimal(38, 10)`.
850fn parse_decimal_accuracy(ck_type: &str) -> Result<Option<(u8, u8)>> {
851    let Some((_, rest)) = ck_type.split_once("Decimal(") else {
852        return Ok(None);
853    };
854    let (args, _) = rest
855        .split_once(')')
856        .ok_or_else(|| SinkError::ClickHouse(format!("unterminated decimal type {ck_type:?}")))?;
857    let (precision, scale) = args
858        .split_once(',')
859        .ok_or_else(|| SinkError::ClickHouse(format!("decimal type {ck_type:?} has no scale")))?;
860    let parse = |field: &str| {
861        field.trim().parse::<u8>().map_err(|e| {
862            SinkError::ClickHouse(format!(
863                "cannot parse decimal type {:?}: {}",
864                ck_type,
865                e.to_report_string()
866            ))
867        })
868    };
869    Ok(Some((parse(precision)?, parse(scale)?)))
870}
871
872fn contains_decimal(data_type: &DataType) -> bool {
873    match data_type {
874        DataType::Decimal => true,
875        DataType::List(list) => contains_decimal(list.elem()),
876        _ => false,
877    }
878}
879
880#[derive(ClickHouseRow, Deserialize)]
881struct ClickhouseQueryEngine {
882    #[expect(dead_code)]
883    name: String,
884    engine: String,
885    create_table_query: String,
886}
887
888async fn query_column_engine_from_ck(
889    client: ClickHouseClient,
890    config: &ClickHouseConfig,
891) -> Result<(Vec<SystemColumn>, ClickHouseEngine)> {
892    let query_engine = QUERY_ENGINE;
893    let query_column = QUERY_COLUMN;
894
895    let clickhouse_engine = client
896        .query(query_engine)
897        .bind(config.common.database.clone())
898        .bind(config.common.table.clone())
899        .fetch_all::<ClickhouseQueryEngine>()
900        .await?;
901    let mut clickhouse_column = client
902        .query(query_column)
903        .bind(config.common.database.clone())
904        .bind(config.common.table.clone())
905        .bind("position")
906        .fetch_all::<SystemColumn>()
907        .await?;
908    if clickhouse_engine.is_empty() || clickhouse_column.is_empty() {
909        return Err(SinkError::ClickHouse(format!(
910            "table {:?}.{:?} is not find in clickhouse",
911            config.common.database, config.common.table
912        )));
913    }
914
915    let clickhouse_engine =
916        ClickHouseEngine::from_query_engine(clickhouse_engine.first().unwrap(), config)?;
917
918    if let Some(sign) = &clickhouse_engine.get_sign_name() {
919        clickhouse_column.retain(|a| sign.ne(&a.name))
920    }
921
922    if let Some(delete_col) = &clickhouse_engine.get_delete_col() {
923        clickhouse_column.retain(|a| delete_col.ne(&a.name))
924    }
925
926    Ok((clickhouse_column, clickhouse_engine))
927}
928
929/// Basic data types for use with the clickhouse interface
930#[derive(Debug)]
931enum ClickHouseField {
932    Int16(i16),
933    Int32(i32),
934    Int64(i64),
935    Serial(Serial),
936    Float32(f32),
937    Float64(f64),
938    String(String),
939    Bool(bool),
940    List(Vec<ClickHouseFieldWithNull>),
941    Int8(i8),
942    Decimal(ClickHouseDecimal),
943}
944#[derive(Debug)]
945enum ClickHouseDecimal {
946    Decimal32(i32),
947    Decimal64(i64),
948    Decimal128(i128),
949}
950
951/// Enum that support clickhouse nullable
952#[derive(Debug)]
953enum ClickHouseFieldWithNull {
954    WithSome(ClickHouseField),
955    WithoutSome(ClickHouseField),
956    None,
957}
958
959impl ClickHouseFieldWithNull {
960    pub fn from_scalar_ref(
961        data: Option<ScalarRefImpl<'_>>,
962        clickhouse_schema_feature_map: &HashMap<String, ClickHouseSchemaFeature>,
963        rw_name: &str,
964        rw_type: &DataType,
965    ) -> Result<Vec<ClickHouseFieldWithNull>> {
966        let clickhouse_schema_feature = if !matches!(rw_type, DataType::Struct(_)) {
967            Some(clickhouse_schema_feature_map.get(rw_name).ok_or_else(|| {
968                SinkError::ClickHouse(format!(
969                    "No column found from clickhouse table schema, name is {rw_name}"
970                ))
971            })?)
972        } else {
973            None
974        };
975
976        let Some(data) = data else {
977            let Some(clickhouse_schema_feature) = clickhouse_schema_feature else {
978                return Err(SinkError::ClickHouse(
979                    "clickhouse's nested can not insert null".to_owned(),
980                ));
981            };
982            if !clickhouse_schema_feature.can_null {
983                return Err(SinkError::ClickHouse(
984                    "Cannot insert null value into non-nullable ClickHouse column".to_owned(),
985                ));
986            } else {
987                return Ok(vec![ClickHouseFieldWithNull::None]);
988            }
989        };
990        let data = match data {
991            ScalarRefImpl::Int16(v) => ClickHouseField::Int16(v),
992            ScalarRefImpl::Int32(v) => ClickHouseField::Int32(v),
993            ScalarRefImpl::Int64(v) => ClickHouseField::Int64(v),
994            ScalarRefImpl::Int256(_) => {
995                return Err(SinkError::ClickHouse(
996                    "INT256 is not supported for ClickHouse sink.".to_owned(),
997                ));
998            }
999            ScalarRefImpl::Serial(v) => ClickHouseField::Serial(v),
1000            ScalarRefImpl::Float32(v) => ClickHouseField::Float32(v.into_inner()),
1001            ScalarRefImpl::Float64(v) => ClickHouseField::Float64(v.into_inner()),
1002            ScalarRefImpl::Utf8(v) => ClickHouseField::String(v.to_owned()),
1003            ScalarRefImpl::Bool(v) => ClickHouseField::Bool(v),
1004            ScalarRefImpl::Decimal(d) => {
1005                let d = if let Decimal::Normalized(d) = d {
1006                    let scale = clickhouse_schema_feature.unwrap().accuracy_decimal.1 as i32
1007                        - d.scale() as i32;
1008                    if scale < 0 {
1009                        d.mantissa() / 10_i128.pow(scale.unsigned_abs())
1010                    } else {
1011                        d.mantissa() * 10_i128.pow(scale as u32)
1012                    }
1013                } else if clickhouse_schema_feature.unwrap().can_null {
1014                    warn!("Inf, -Inf, Nan in RW decimal is converted into clickhouse null!");
1015                    return Ok(vec![ClickHouseFieldWithNull::None]);
1016                } else {
1017                    warn!("Inf, -Inf, Nan in RW decimal is converted into clickhouse 0!");
1018                    0_i128
1019                };
1020                if clickhouse_schema_feature.unwrap().accuracy_decimal.0 <= 9 {
1021                    ClickHouseField::Decimal(ClickHouseDecimal::Decimal32(d as i32))
1022                } else if clickhouse_schema_feature.unwrap().accuracy_decimal.0 <= 18 {
1023                    ClickHouseField::Decimal(ClickHouseDecimal::Decimal64(d as i64))
1024                } else {
1025                    ClickHouseField::Decimal(ClickHouseDecimal::Decimal128(d))
1026                }
1027            }
1028            ScalarRefImpl::Interval(_) => {
1029                return Err(SinkError::ClickHouse(
1030                    "INTERVAL is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
1031                ));
1032            }
1033            ScalarRefImpl::Date(v) => {
1034                let days = v.get_nums_days_unix_epoch();
1035                ClickHouseField::Int32(days)
1036            }
1037            ScalarRefImpl::Time(_) => {
1038                return Err(SinkError::ClickHouse(
1039                    "TIME is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
1040                ));
1041            }
1042            ScalarRefImpl::Timestamp(_) => {
1043                return Err(SinkError::ClickHouse(
1044                    "clickhouse does not have a type corresponding to naive timestamp".to_owned(),
1045                ));
1046            }
1047            ScalarRefImpl::Timestamptz(v) => {
1048                let micros = v.timestamp_micros();
1049                let ticks = match clickhouse_schema_feature.unwrap().accuracy_time <= 6 {
1050                    true => {
1051                        micros
1052                            / 10_i64
1053                                .pow((6 - clickhouse_schema_feature.unwrap().accuracy_time).into())
1054                    }
1055                    false => micros
1056                        .checked_mul(
1057                            10_i64
1058                                .pow((clickhouse_schema_feature.unwrap().accuracy_time - 6).into()),
1059                        )
1060                        .ok_or_else(|| SinkError::ClickHouse("DateTime64 overflow".to_owned()))?,
1061                };
1062                ClickHouseField::Int64(ticks)
1063            }
1064            ScalarRefImpl::Jsonb(v) => {
1065                let json_str = v.to_string();
1066                ClickHouseField::String(json_str)
1067            }
1068            ScalarRefImpl::Variant(_) => {
1069                return Err(SinkError::ClickHouse(
1070                    "VARIANT is not supported for ClickHouse sink.".to_owned(),
1071                ));
1072            }
1073            ScalarRefImpl::Struct(v) => {
1074                let mut struct_vec = vec![];
1075                for (field, (struct_field_name, struct_field_type)) in
1076                    v.iter_fields_ref().zip_eq_debug(rw_type.as_struct().iter())
1077                {
1078                    let name = format!("{}.{}", rw_name, struct_field_name);
1079                    let a = Self::from_scalar_ref(
1080                        field,
1081                        clickhouse_schema_feature_map,
1082                        &name,
1083                        struct_field_type,
1084                    )?;
1085                    struct_vec.push(ClickHouseFieldWithNull::WithoutSome(ClickHouseField::List(
1086                        a,
1087                    )));
1088                }
1089                return Ok(struct_vec);
1090            }
1091            ScalarRefImpl::List(v) => {
1092                let mut vec = vec![];
1093                for i in v.iter() {
1094                    vec.extend(Self::from_scalar_ref(
1095                        i,
1096                        clickhouse_schema_feature_map,
1097                        rw_name,
1098                        rw_type,
1099                    )?)
1100                }
1101                return Ok(vec![ClickHouseFieldWithNull::WithoutSome(
1102                    ClickHouseField::List(vec),
1103                )]);
1104            }
1105            ScalarRefImpl::Bytea(_) => {
1106                return Err(SinkError::ClickHouse(
1107                    "BYTEA is not supported for ClickHouse sink. Please convert to VARCHAR or other supported types.".to_owned(),
1108                ));
1109            }
1110            ScalarRefImpl::Map(_) => {
1111                return Err(SinkError::ClickHouse(
1112                    "MAP is not supported for ClickHouse sink.".to_owned(),
1113                ));
1114            }
1115            ScalarRefImpl::Vector(_) => {
1116                return Err(SinkError::ClickHouse(
1117                    "VECTOR is not supported for ClickHouse sink.".to_owned(),
1118                ));
1119            }
1120        };
1121        let data = if let Some(clickhouse_schema_feature) = clickhouse_schema_feature
1122            && clickhouse_schema_feature.can_null
1123        {
1124            vec![ClickHouseFieldWithNull::WithSome(data)]
1125        } else {
1126            vec![ClickHouseFieldWithNull::WithoutSome(data)]
1127        };
1128        Ok(data)
1129    }
1130}
1131
1132impl ClickHouseField {
1133    /// Appends the value in the `RowBinary` format.
1134    fn write_row_binary(&self, buf: &mut BytesMut) {
1135        match self {
1136            ClickHouseField::Int8(v) => buf.put_i8(*v),
1137            ClickHouseField::Int16(v) => buf.put_i16_le(*v),
1138            ClickHouseField::Int32(v) => buf.put_i32_le(*v),
1139            ClickHouseField::Int64(v) => buf.put_i64_le(*v),
1140            ClickHouseField::Serial(v) => buf.put_i64_le((*v).into()),
1141            ClickHouseField::Float32(v) => buf.put_f32_le(*v),
1142            ClickHouseField::Float64(v) => buf.put_f64_le(*v),
1143            ClickHouseField::String(v) => {
1144                put_leb128(&mut *buf, v.len() as u64);
1145                buf.put_slice(v.as_bytes());
1146            }
1147            ClickHouseField::Bool(v) => buf.put_u8(*v as u8),
1148            ClickHouseField::List(v) => {
1149                put_leb128(&mut *buf, v.len() as u64);
1150                for element in v {
1151                    element.write_row_binary(buf);
1152                }
1153            }
1154            ClickHouseField::Decimal(ClickHouseDecimal::Decimal32(v)) => buf.put_i32_le(*v),
1155            ClickHouseField::Decimal(ClickHouseDecimal::Decimal64(v)) => buf.put_i64_le(*v),
1156            ClickHouseField::Decimal(ClickHouseDecimal::Decimal128(v)) => buf.put_i128_le(*v),
1157        }
1158    }
1159}
1160
1161impl ClickHouseFieldWithNull {
1162    /// Appends the value in the `RowBinary` format, with the `Nullable` marker when it has one.
1163    fn write_row_binary(&self, buf: &mut BytesMut) {
1164        match self {
1165            ClickHouseFieldWithNull::WithSome(v) => {
1166                buf.put_u8(0);
1167                v.write_row_binary(buf);
1168            }
1169            ClickHouseFieldWithNull::WithoutSome(v) => v.write_row_binary(buf),
1170            ClickHouseFieldWithNull::None => buf.put_u8(1),
1171        }
1172    }
1173}
1174
1175/// 'Struct'(clickhouse type name is nested) will be converted into some arrays by clickhouse. So we
1176/// need to make some conversions
1177pub fn build_fields_name_type_from_schema(schema: &Schema) -> Result<Vec<(String, DataType)>> {
1178    let mut vec = vec![];
1179    for field in schema.fields() {
1180        if let DataType::Struct(st) = &field.data_type {
1181            for (name, data_type) in st.iter() {
1182                if matches!(data_type, DataType::Struct(_)) {
1183                    return Err(SinkError::ClickHouse(
1184                        "Only one level of nesting is supported for struct".to_owned(),
1185                    ));
1186                } else {
1187                    vec.push((
1188                        format!("{}.{}", field.name, name),
1189                        DataType::list(data_type.clone()),
1190                    ))
1191                }
1192            }
1193        } else {
1194            vec.push((field.name.clone(), field.data_type()));
1195        }
1196    }
1197    Ok(vec)
1198}
1199
1200impl From<::clickhouse::error::Error> for SinkError {
1201    fn from(value: ::clickhouse::error::Error) -> Self {
1202        SinkError::ClickHouse(value.to_report_string())
1203    }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208    use super::*;
1209
1210    fn system_column(ck_type: &str) -> SystemColumn {
1211        SystemColumn {
1212            name: "amount".to_owned(),
1213            r#type: ck_type.to_owned(),
1214            is_in_primary_key: 0,
1215        }
1216    }
1217
1218    #[test]
1219    fn test_parse_decimal_accuracy() {
1220        let parse = |ck_type: &str| parse_decimal_accuracy(ck_type).unwrap();
1221
1222        assert_eq!(parse("Decimal(38, 10)"), Some((38, 10)));
1223        assert_eq!(parse("Decimal(38,10)"), Some((38, 10)));
1224        assert_eq!(parse("Nullable(Decimal(76, 4))"), Some((76, 4)));
1225        assert_eq!(parse("DateTime64(3)"), None);
1226
1227        assert!(parse_decimal_accuracy("Decimal(38)").is_err());
1228    }
1229
1230    #[test]
1231    fn test_decimal_column_check() {
1232        let check = |rw_type: &DataType, ck_type: &str| {
1233            ClickHouseSink::check_and_correct_column_type(rw_type, &system_column(ck_type))
1234        };
1235        let decimal_list = DataType::list(DataType::Decimal);
1236
1237        check(&DataType::Decimal, "Nullable(Decimal(38, 10))").unwrap();
1238        check(&decimal_list, "Array(Decimal(9, 2))").unwrap();
1239        // Only columns encoded as decimals are held to the precision rule.
1240        check(&DataType::Jsonb, "JSON(a Decimal(76, 4), b Decimal(9, 2))").unwrap();
1241
1242        check(&DataType::Decimal, "String").unwrap_err();
1243        let err = check(&DataType::Decimal, "Decimal(76, 4)")
1244            .unwrap_err()
1245            .to_string();
1246        assert!(err.contains("amount"), "{err}");
1247        assert!(err.contains("Decimal(76, 4)"), "{err}");
1248        assert!(err.contains("precision 76"), "{err}");
1249    }
1250
1251    #[test]
1252    fn test_row_binary_encoding() {
1253        // 200 bytes, so the length prefix takes two bytes.
1254        let long = "é".repeat(100);
1255        let fields = [
1256            ClickHouseFieldWithNull::WithSome(ClickHouseField::Int32(-2)),
1257            ClickHouseFieldWithNull::None,
1258            ClickHouseFieldWithNull::WithoutSome(ClickHouseField::String(long.clone())),
1259            ClickHouseFieldWithNull::WithoutSome(ClickHouseField::List(vec![
1260                ClickHouseFieldWithNull::WithSome(ClickHouseField::Int8(1)),
1261                ClickHouseFieldWithNull::None,
1262            ])),
1263            ClickHouseFieldWithNull::WithoutSome(ClickHouseField::Decimal(
1264                ClickHouseDecimal::Decimal128(-1),
1265            )),
1266        ];
1267        let mut buf = BytesMut::new();
1268        for field in &fields {
1269            field.write_row_binary(&mut buf);
1270        }
1271
1272        let mut expected = vec![0, 0xfe, 0xff, 0xff, 0xff, 1, 0xc8, 0x01];
1273        expected.extend(long.as_bytes());
1274        expected.extend([2, 0, 1, 1]);
1275        expected.extend([0xff; 16]);
1276        assert_eq!(&buf[..], expected.as_slice());
1277    }
1278}