1use 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
59const MAX_ENCODABLE_DECIMAL_PRECISION: u8 = 38;
62
63const 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 #[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" => {
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" => {
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" => {
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" => {
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" => {
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" => {
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() .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, #[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 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 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 fn check_and_correct_column_type(
474 fields_type: &DataType,
475 ck_column: &SystemColumn,
476 ) -> Result<()> {
477 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 | 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 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 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 column_correct_map: HashMap<String, ClickHouseSchemaFeature>,
648 rw_fields_name_after_calibration: Vec<String>,
649 clickhouse_engine: ClickHouseEngine,
650 inserter: Option<InsertFormatted>,
651 buf: BytesMut,
653}
654#[derive(Debug)]
655struct ClickHouseSchemaFeature {
656 can_null: bool,
657 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 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 fn build_column_correct_map(
721 ck_column: &SystemColumn,
722 ) -> Result<(String, ClickHouseSchemaFeature)> {
723 let can_null = ck_column.r#type.contains("Nullable");
724 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
848fn 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#[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#[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 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 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
1175pub 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 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 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}