1use std::sync::Arc;
16
17use anyhow::anyhow;
18use async_trait::async_trait;
19use await_tree::InstrumentAwait;
20use iceberg::arrow::{
21 RecordBatchPartitionSplitter, arrow_schema_to_schema, schema_to_arrow_schema,
22};
23use iceberg::spec::{
24 DataFile, FormatVersion, PartitionSpecRef, Schema, SchemaRef as IcebergSchemaRef,
25 SerializedDataFile, StructType,
26};
27use iceberg::table::Table;
28use iceberg::writer::base_writer::data_file_writer::DataFileWriterBuilder;
29use iceberg::writer::base_writer::deletion_vector_writer::{
30 DeletionVectorWriter, DeletionVectorWriterBuilder,
31};
32use iceberg::writer::base_writer::equality_delete_writer::{
33 EqualityDeleteFileWriterBuilder, EqualityDeleteWriterConfig,
34};
35use iceberg::writer::base_writer::position_delete_file_writer::{
36 POSITION_DELETE_SCHEMA, PositionDeleteFileWriter, PositionDeleteFileWriterBuilder,
37 PositionDeleteInput,
38};
39use iceberg::writer::delta_writer::{DELETE_OP, DeltaWriterBuilder, INSERT_OP};
40use iceberg::writer::file_writer::ParquetWriterBuilder;
41use iceberg::writer::file_writer::location_generator::{
42 DefaultFileNameGenerator, DefaultLocationGenerator,
43};
44use iceberg::writer::file_writer::rolling_writer::RollingFileWriterBuilder;
45use iceberg::writer::task_writer::TaskWriter;
46use iceberg::writer::{IcebergWriter, IcebergWriterBuilder};
47use itertools::Itertools;
48use parquet::file::properties::WriterProperties;
49use risingwave_common::array::arrow::IcebergArrowConvert;
50use risingwave_common::array::arrow::arrow_array_iceberg::{Int32Array, RecordBatch};
51use risingwave_common::array::arrow::arrow_schema_iceberg::{
52 DataType as ArrowDataType, Field as ArrowField, Schema as ArrowSchema, SchemaRef,
53};
54use risingwave_common::array::{Op, StreamChunk};
55use risingwave_common::bitmap::Bitmap;
56use risingwave_common::metrics::{LabelGuardedHistogram, LabelGuardedIntCounter};
57use risingwave_common_estimate_size::EstimateSize;
58use risingwave_pb::connector_service::SinkMetadata;
59use thiserror_ext::AsReport;
60use uuid::Uuid;
61
62use super::prometheus::monitored_general_writer::MonitoredGeneralWriterBuilder;
63use super::{
64 GLOBAL_SINK_METRICS, IcebergCommitResult, IcebergConfig, PARQUET_CREATED_BY, SinkError,
65 SinkWriterParam, create_and_validate_table_impl,
66};
67use crate::sink::writer::SinkWriter;
68use crate::sink::{Result, SinkParam};
69
70enum ProjectIdxVec {
78 None,
79 Prepare(usize),
80 Done(Vec<usize>),
81}
82
83type DataFileWriterBuilderType =
84 DataFileWriterBuilder<ParquetWriterBuilder, DefaultLocationGenerator, DefaultFileNameGenerator>;
85pub type PositionDeleteFileWriterBuilderType = PositionDeleteFileWriterBuilder<
88 ParquetWriterBuilder,
89 DefaultLocationGenerator,
90 DefaultFileNameGenerator,
91>;
92type PositionDeleteFileWriterType = PositionDeleteFileWriter<
93 ParquetWriterBuilder,
94 DefaultLocationGenerator,
95 DefaultFileNameGenerator,
96>;
97type DeletionVectorWriterBuilderType =
98 DeletionVectorWriterBuilder<DefaultLocationGenerator, DefaultFileNameGenerator>;
99type DeletionVectorWriterType =
100 DeletionVectorWriter<DefaultLocationGenerator, DefaultFileNameGenerator>;
101type EqualityDeleteFileWriterBuilderType = EqualityDeleteFileWriterBuilder<
102 ParquetWriterBuilder,
103 DefaultLocationGenerator,
104 DefaultFileNameGenerator,
105>;
106
107#[derive(Clone)]
108enum PositionDeleteWriterBuilderType {
109 PositionDelete(PositionDeleteFileWriterBuilderType),
110 DeletionVector(DeletionVectorWriterBuilderType),
111}
112
113enum PositionDeleteWriterType {
114 PositionDelete(PositionDeleteFileWriterType),
115 DeletionVector(DeletionVectorWriterType),
116}
117
118#[async_trait]
119impl IcebergWriterBuilder<Vec<PositionDeleteInput>> for PositionDeleteWriterBuilderType {
120 type R = PositionDeleteWriterType;
121
122 async fn build(
123 &self,
124 partition_key: Option<iceberg::spec::PartitionKey>,
125 ) -> iceberg::Result<Self::R> {
126 match self {
127 PositionDeleteWriterBuilderType::PositionDelete(builder) => Ok(
128 PositionDeleteWriterType::PositionDelete(builder.build(partition_key).await?),
129 ),
130 PositionDeleteWriterBuilderType::DeletionVector(builder) => Ok(
131 PositionDeleteWriterType::DeletionVector(builder.build(partition_key).await?),
132 ),
133 }
134 }
135}
136
137#[async_trait]
138impl IcebergWriter<Vec<PositionDeleteInput>> for PositionDeleteWriterType {
139 async fn write(&mut self, input: Vec<PositionDeleteInput>) -> iceberg::Result<()> {
140 match self {
141 PositionDeleteWriterType::PositionDelete(writer) => writer.write(input).await,
142 PositionDeleteWriterType::DeletionVector(writer) => writer.write(input).await,
143 }
144 }
145
146 async fn close(&mut self) -> iceberg::Result<Vec<DataFile>> {
147 match self {
148 PositionDeleteWriterType::PositionDelete(writer) => writer.close().await,
149 PositionDeleteWriterType::DeletionVector(writer) => writer.close().await,
150 }
151 }
152}
153
154#[derive(Clone)]
155struct SharedIcebergWriterBuilder<B>(Arc<B>);
156
157#[async_trait]
158impl<B: IcebergWriterBuilder> IcebergWriterBuilder for SharedIcebergWriterBuilder<B> {
159 type R = B::R;
160
161 async fn build(
162 &self,
163 partition_key: Option<iceberg::spec::PartitionKey>,
164 ) -> iceberg::Result<Self::R> {
165 self.0.build(partition_key).await
166 }
167}
168
169#[derive(Clone)]
170struct TaskWriterBuilderWrapper<B: IcebergWriterBuilder> {
171 inner: Arc<B>,
172 fanout_enabled: bool,
173 schema: IcebergSchemaRef,
174 partition_spec: PartitionSpecRef,
175 compute_partition: bool,
176}
177
178impl<B: IcebergWriterBuilder> TaskWriterBuilderWrapper<B> {
179 fn new(
180 inner: B,
181 fanout_enabled: bool,
182 schema: IcebergSchemaRef,
183 partition_spec: PartitionSpecRef,
184 compute_partition: bool,
185 ) -> Self {
186 Self {
187 inner: Arc::new(inner),
188 fanout_enabled,
189 schema,
190 partition_spec,
191 compute_partition,
192 }
193 }
194
195 fn build(&self) -> iceberg::Result<TaskWriter<SharedIcebergWriterBuilder<B>>> {
196 let partition_splitter = match (
197 self.partition_spec.is_unpartitioned(),
198 self.compute_partition,
199 ) {
200 (true, _) => None,
201 (false, true) => Some(RecordBatchPartitionSplitter::try_new_with_computed_values(
202 self.schema.clone(),
203 self.partition_spec.clone(),
204 )?),
205 (false, false) => Some(
206 RecordBatchPartitionSplitter::try_new_with_precomputed_values(
207 self.schema.clone(),
208 self.partition_spec.clone(),
209 )?,
210 ),
211 };
212
213 Ok(TaskWriter::new_with_partition_splitter(
214 SharedIcebergWriterBuilder(self.inner.clone()),
215 self.fanout_enabled,
216 self.schema.clone(),
217 self.partition_spec.clone(),
218 partition_splitter,
219 ))
220 }
221}
222
223pub enum IcebergSinkWriter {
224 Created(IcebergSinkWriterArgs),
225 Initialized(IcebergSinkWriterInner),
226}
227
228pub struct IcebergSinkWriterArgs {
229 config: IcebergConfig,
230 sink_param: SinkParam,
231 writer_param: SinkWriterParam,
232 upsert_primary_key_column_names: Option<Vec<String>>,
233}
234
235pub struct IcebergSinkWriterInner {
236 writer: IcebergWriterDispatch,
237 arrow_schema: SchemaRef,
238 metrics: IcebergWriterMetrics,
240 table: Table,
242 actor_id: String,
243 sink_id: String,
244 sink_name: String,
245 table_name: String,
246 writer_mode: &'static str,
247 project_idx_vec: ProjectIdxVec,
250}
251
252enum IcebergWriterDispatch {
253 Append {
254 writer: Option<Box<dyn IcebergWriter>>,
255 writer_builder:
256 TaskWriterBuilderWrapper<MonitoredGeneralWriterBuilder<DataFileWriterBuilderType>>,
257 },
258 Upsert {
259 writer: Option<Box<dyn IcebergWriter>>,
260 writer_builder: TaskWriterBuilderWrapper<
261 MonitoredGeneralWriterBuilder<
262 DeltaWriterBuilder<
263 DataFileWriterBuilderType,
264 PositionDeleteWriterBuilderType,
265 EqualityDeleteFileWriterBuilderType,
266 >,
267 >,
268 >,
269 arrow_schema_with_op_column: SchemaRef,
270 },
271}
272
273impl IcebergWriterDispatch {
274 pub fn get_writer(&mut self) -> Option<&mut Box<dyn IcebergWriter>> {
275 match self {
276 IcebergWriterDispatch::Append { writer, .. }
277 | IcebergWriterDispatch::Upsert { writer, .. } => writer.as_mut(),
278 }
279 }
280}
281
282pub struct IcebergWriterMetrics {
283 _write_qps: LabelGuardedIntCounter,
288 _write_latency: LabelGuardedHistogram,
289 write_bytes: LabelGuardedIntCounter,
290}
291
292impl IcebergSinkWriter {
293 pub fn new(
294 config: IcebergConfig,
295 sink_param: SinkParam,
296 writer_param: SinkWriterParam,
297 upsert_primary_key_column_names: Option<Vec<String>>,
298 ) -> Self {
299 Self::Created(IcebergSinkWriterArgs {
300 config,
301 sink_param,
302 writer_param,
303 upsert_primary_key_column_names,
304 })
305 }
306}
307
308pub(super) fn resolve_equality_delete_field_ids(
309 primary_key_column_names: &[String],
310 iceberg_schema: &iceberg::spec::Schema,
311) -> Result<Vec<i32>> {
312 primary_key_column_names
313 .iter()
314 .map(|column_name| {
315 iceberg_schema.field_id_by_name(column_name).ok_or_else(|| {
316 SinkError::Config(anyhow!(
317 "Primary key column {} not found in Iceberg schema",
318 column_name
319 ))
320 })
321 })
322 .collect()
323}
324
325impl IcebergSinkWriterInner {
326 pub fn build_append_only(
327 config: &IcebergConfig,
328 table: Table,
329 writer_param: &SinkWriterParam,
330 ) -> Result<Self> {
331 let SinkWriterParam {
332 extra_partition_col_idx,
333 actor_id,
334 sink_id,
335 sink_name,
336 ..
337 } = writer_param;
338 let metrics_labels = [
339 &actor_id.to_string(),
340 &sink_id.to_string(),
341 sink_name.as_str(),
342 ];
343
344 let write_qps = GLOBAL_SINK_METRICS
346 .iceberg_write_qps
347 .with_guarded_label_values(&metrics_labels);
348 let write_latency = GLOBAL_SINK_METRICS
349 .iceberg_write_latency
350 .with_guarded_label_values(&metrics_labels);
351 let _rolling_unflushed_data_file = GLOBAL_SINK_METRICS
354 .iceberg_rolling_unflushed_data_file
355 .with_guarded_label_values(&metrics_labels);
356 let write_bytes = GLOBAL_SINK_METRICS
357 .iceberg_write_bytes
358 .with_guarded_label_values(&metrics_labels);
359
360 let schema = table.metadata().current_schema();
361 let partition_spec = table.metadata().default_partition_spec();
362 let fanout_enabled = !partition_spec.fields().is_empty();
363 let table_name = table.identifier().to_string();
364 let unique_uuid_suffix = Uuid::now_v7();
366 tracing::info!(
367 iceberg_component = "sink_writer",
368 iceberg_operation = "build_writer",
369 actor_id = %actor_id,
370 sink_id = %sink_id,
371 sink_name = %sink_name,
372 table = %table_name,
373 writer_mode = "append_only",
374 schema_id = table.metadata().current_schema_id(),
375 partition_spec_id = table.metadata().default_partition_spec_id(),
376 format_version = ?table.metadata().format_version(),
377 fanout_enabled,
378 target_file_size_mb = config.target_file_size_mb(),
379 parquet_compression = ?config.get_parquet_compression(),
380 "iceberg_sink_writer_initialized",
381 );
382
383 let parquet_writer_properties = WriterProperties::builder()
384 .set_compression(config.get_parquet_compression())
385 .set_max_row_group_bytes(config.write_parquet_max_row_group_bytes())
386 .set_created_by(PARQUET_CREATED_BY.to_owned())
387 .build();
388
389 let parquet_writer_builder =
390 ParquetWriterBuilder::new(parquet_writer_properties, schema.clone());
391 let rolling_builder = RollingFileWriterBuilder::new(
392 parquet_writer_builder,
393 (config.target_file_size_mb() * 1024 * 1024) as usize,
394 table.file_io().clone(),
395 DefaultLocationGenerator::new(table.metadata().clone())
396 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
397 DefaultFileNameGenerator::new(
398 writer_param.actor_id.to_string(),
399 Some(unique_uuid_suffix.to_string()),
400 iceberg::spec::DataFileFormat::Parquet,
401 ),
402 );
403 let data_file_builder = DataFileWriterBuilder::new(rolling_builder);
404 let monitored_builder = MonitoredGeneralWriterBuilder::new(
405 data_file_builder,
406 write_qps.clone(),
407 write_latency.clone(),
408 );
409 let writer_builder = TaskWriterBuilderWrapper::new(
410 monitored_builder,
411 fanout_enabled,
412 schema.clone(),
413 partition_spec.clone(),
414 true,
415 );
416 let inner_writer = Some(Box::new(
417 writer_builder
418 .build()
419 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
420 ) as Box<dyn IcebergWriter>);
421 Ok(Self {
422 arrow_schema: Arc::new(
423 schema_to_arrow_schema(table.metadata().current_schema())
424 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
425 ),
426 metrics: IcebergWriterMetrics {
427 _write_qps: write_qps,
428 _write_latency: write_latency,
429 write_bytes,
430 },
431 writer: IcebergWriterDispatch::Append {
432 writer: inner_writer,
433 writer_builder,
434 },
435 table,
436 actor_id: actor_id.to_string(),
437 sink_id: sink_id.to_string(),
438 sink_name: sink_name.clone(),
439 table_name,
440 writer_mode: "append_only",
441 project_idx_vec: {
442 if let Some(extra_partition_col_idx) = extra_partition_col_idx {
443 ProjectIdxVec::Prepare(*extra_partition_col_idx)
444 } else {
445 ProjectIdxVec::None
446 }
447 },
448 })
449 }
450
451 pub fn build_upsert(
452 config: &IcebergConfig,
453 table: Table,
454 primary_key_column_names: Vec<String>,
455 writer_param: &SinkWriterParam,
456 ) -> Result<Self> {
457 let SinkWriterParam {
458 extra_partition_col_idx,
459 actor_id,
460 sink_id,
461 sink_name,
462 ..
463 } = writer_param;
464 let metrics_labels = [
465 &actor_id.to_string(),
466 &sink_id.to_string(),
467 sink_name.as_str(),
468 ];
469
470 let write_qps = GLOBAL_SINK_METRICS
472 .iceberg_write_qps
473 .with_guarded_label_values(&metrics_labels);
474 let write_latency = GLOBAL_SINK_METRICS
475 .iceberg_write_latency
476 .with_guarded_label_values(&metrics_labels);
477 let _rolling_unflushed_data_file = GLOBAL_SINK_METRICS
480 .iceberg_rolling_unflushed_data_file
481 .with_guarded_label_values(&metrics_labels);
482 let write_bytes = GLOBAL_SINK_METRICS
483 .iceberg_write_bytes
484 .with_guarded_label_values(&metrics_labels);
485
486 let schema = table.metadata().current_schema();
488 let unique_column_ids =
489 resolve_equality_delete_field_ids(&primary_key_column_names, schema)?;
490 let partition_spec = table.metadata().default_partition_spec();
491 let fanout_enabled = !partition_spec.fields().is_empty();
492 let use_deletion_vectors = table.metadata().format_version() >= FormatVersion::V3;
493 let table_name = table.identifier().to_string();
494
495 let unique_uuid_suffix = Uuid::now_v7();
497 tracing::info!(
498 iceberg_component = "sink_writer",
499 iceberg_operation = "build_writer",
500 actor_id = %actor_id,
501 sink_id = %sink_id,
502 sink_name = %sink_name,
503 table = %table_name,
504 writer_mode = "upsert",
505 schema_id = table.metadata().current_schema_id(),
506 partition_spec_id = table.metadata().default_partition_spec_id(),
507 format_version = ?table.metadata().format_version(),
508 primary_key_columns = ?primary_key_column_names,
509 equality_delete_field_ids = ?unique_column_ids,
510 use_deletion_vectors,
511 fanout_enabled,
512 target_file_size_mb = config.target_file_size_mb(),
513 parquet_compression = ?config.get_parquet_compression(),
514 "iceberg_sink_writer_initialized",
515 );
516
517 let parquet_writer_properties = WriterProperties::builder()
518 .set_compression(config.get_parquet_compression())
519 .set_max_row_group_bytes(config.write_parquet_max_row_group_bytes())
520 .set_created_by(PARQUET_CREATED_BY.to_owned())
521 .build();
522
523 let data_file_builder = {
524 let parquet_writer_builder =
525 ParquetWriterBuilder::new(parquet_writer_properties.clone(), schema.clone());
526 let rolling_writer_builder = RollingFileWriterBuilder::new(
527 parquet_writer_builder,
528 (config.target_file_size_mb() * 1024 * 1024) as usize,
529 table.file_io().clone(),
530 DefaultLocationGenerator::new(table.metadata().clone())
531 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
532 DefaultFileNameGenerator::new(
533 writer_param.actor_id.to_string(),
534 Some(unique_uuid_suffix.to_string()),
535 iceberg::spec::DataFileFormat::Parquet,
536 ),
537 );
538 DataFileWriterBuilder::new(rolling_writer_builder)
539 };
540 let position_delete_builder = if use_deletion_vectors {
541 let location_generator = DefaultLocationGenerator::new(table.metadata().clone())
542 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
543 PositionDeleteWriterBuilderType::DeletionVector(DeletionVectorWriterBuilder::new(
544 table.file_io().clone(),
545 location_generator,
546 DefaultFileNameGenerator::new(
547 writer_param.actor_id.to_string(),
548 Some(format!("delvec-{}", unique_uuid_suffix)),
549 iceberg::spec::DataFileFormat::Puffin,
550 ),
551 ))
552 } else {
553 let parquet_writer_builder = ParquetWriterBuilder::new(
554 parquet_writer_properties.clone(),
555 POSITION_DELETE_SCHEMA.clone().into(),
556 );
557 let rolling_writer_builder = RollingFileWriterBuilder::new(
558 parquet_writer_builder,
559 (config.target_file_size_mb() * 1024 * 1024) as usize,
560 table.file_io().clone(),
561 DefaultLocationGenerator::new(table.metadata().clone())
562 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
563 DefaultFileNameGenerator::new(
564 writer_param.actor_id.to_string(),
565 Some(format!("pos-del-{}", unique_uuid_suffix)),
566 iceberg::spec::DataFileFormat::Parquet,
567 ),
568 );
569 PositionDeleteWriterBuilderType::PositionDelete(PositionDeleteFileWriterBuilder::new(
570 rolling_writer_builder,
571 ))
572 };
573 let equality_delete_builder = {
574 let eq_del_config = EqualityDeleteWriterConfig::new(
575 unique_column_ids.clone(),
576 table.metadata().current_schema().clone(),
577 )
578 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
579 let parquet_writer_builder = ParquetWriterBuilder::new(
580 parquet_writer_properties,
581 Arc::new(
582 arrow_schema_to_schema(eq_del_config.projected_arrow_schema_ref())
583 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
584 ),
585 );
586 let rolling_writer_builder = RollingFileWriterBuilder::new(
587 parquet_writer_builder,
588 (config.target_file_size_mb() * 1024 * 1024) as usize,
589 table.file_io().clone(),
590 DefaultLocationGenerator::new(table.metadata().clone())
591 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
592 DefaultFileNameGenerator::new(
593 writer_param.actor_id.to_string(),
594 Some(format!("eq-del-{}", unique_uuid_suffix)),
595 iceberg::spec::DataFileFormat::Parquet,
596 ),
597 );
598
599 EqualityDeleteFileWriterBuilder::new(rolling_writer_builder, eq_del_config)
600 };
601 let delta_builder = DeltaWriterBuilder::new(
602 data_file_builder,
603 position_delete_builder,
604 equality_delete_builder,
605 unique_column_ids,
606 schema.clone(),
607 );
608 let original_arrow_schema = Arc::new(
609 schema_to_arrow_schema(table.metadata().current_schema())
610 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
611 );
612 let schema_with_extra_op_column = {
613 let mut new_fields = original_arrow_schema.fields().iter().cloned().collect_vec();
614 new_fields.push(Arc::new(ArrowField::new(
615 "op".to_owned(),
616 ArrowDataType::Int32,
617 false,
618 )));
619 Arc::new(ArrowSchema::new(new_fields))
620 };
621 let writer_builder = TaskWriterBuilderWrapper::new(
622 MonitoredGeneralWriterBuilder::new(
623 delta_builder,
624 write_qps.clone(),
625 write_latency.clone(),
626 ),
627 fanout_enabled,
628 schema.clone(),
629 partition_spec.clone(),
630 true,
631 );
632 let inner_writer = Some(Box::new(
633 writer_builder
634 .build()
635 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
636 ) as Box<dyn IcebergWriter>);
637 Ok(Self {
638 arrow_schema: original_arrow_schema,
639 metrics: IcebergWriterMetrics {
640 _write_qps: write_qps,
641 _write_latency: write_latency,
642 write_bytes,
643 },
644 table,
645 writer: IcebergWriterDispatch::Upsert {
646 writer: inner_writer,
647 writer_builder,
648 arrow_schema_with_op_column: schema_with_extra_op_column,
649 },
650 project_idx_vec: {
651 if let Some(extra_partition_col_idx) = extra_partition_col_idx {
652 ProjectIdxVec::Prepare(*extra_partition_col_idx)
653 } else {
654 ProjectIdxVec::None
655 }
656 },
657 actor_id: actor_id.to_string(),
658 sink_id: sink_id.to_string(),
659 sink_name: sink_name.clone(),
660 table_name,
661 writer_mode: "upsert",
662 })
663 }
664
665 fn prepare_writer(&mut self) -> Result<()> {
666 match &mut self.writer {
667 IcebergWriterDispatch::Append {
668 writer,
669 writer_builder,
670 } => {
671 if writer.is_none() {
672 *writer = Some(Box::new(
673 writer_builder
674 .build()
675 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
676 ));
677 }
678 }
679 IcebergWriterDispatch::Upsert {
680 writer,
681 writer_builder,
682 ..
683 } => {
684 if writer.is_none() {
685 *writer = Some(Box::new(
686 writer_builder
687 .build()
688 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?,
689 ));
690 }
691 }
692 };
693 Ok(())
694 }
695
696 fn process_chunk(&mut self, chunk: StreamChunk) -> Result<Option<(RecordBatch, usize)>> {
697 let (mut chunk, ops) = chunk.compact_vis().into_parts();
698 match &mut self.project_idx_vec {
699 ProjectIdxVec::None => {}
700 ProjectIdxVec::Prepare(idx) => {
701 if *idx >= chunk.columns().len() {
702 return Err(SinkError::Iceberg(anyhow!(
703 "invalid extra partition column index {}",
704 idx
705 )));
706 }
707 let project_idx_vec = (0..*idx)
708 .chain(*idx + 1..chunk.columns().len())
709 .collect_vec();
710 chunk = chunk.project(&project_idx_vec);
711 self.project_idx_vec = ProjectIdxVec::Done(project_idx_vec);
712 }
713 ProjectIdxVec::Done(idx_vec) => {
714 chunk = chunk.project(idx_vec);
715 }
716 }
717 if ops.is_empty() {
718 return Ok(None);
719 }
720 let write_batch_size = chunk.estimated_heap_size();
721 let batch = match &self.writer {
722 IcebergWriterDispatch::Append { .. } => {
723 let filters =
725 chunk.visibility() & ops.iter().map(|op| *op == Op::Insert).collect::<Bitmap>();
726 chunk.set_visibility(filters);
727 IcebergArrowConvert
728 .to_record_batch(self.arrow_schema.clone(), &chunk.compact_vis())
729 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?
730 }
731 IcebergWriterDispatch::Upsert {
732 arrow_schema_with_op_column,
733 ..
734 } => {
735 let chunk = IcebergArrowConvert
736 .to_record_batch(self.arrow_schema.clone(), &chunk)
737 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?;
738 let ops = Arc::new(Int32Array::from(
739 ops.iter()
740 .map(|op| match op {
741 Op::UpdateInsert | Op::Insert => INSERT_OP,
742 Op::UpdateDelete | Op::Delete => DELETE_OP,
743 })
744 .collect_vec(),
745 ));
746 let mut columns = chunk.columns().to_vec();
747 columns.push(ops);
748 RecordBatch::try_new(arrow_schema_with_op_column.clone(), columns)
749 .map_err(|err| SinkError::Iceberg(anyhow!(err)))?
750 }
751 };
752 Ok(Some((batch, write_batch_size)))
753 }
754
755 pub async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()> {
756 self.prepare_writer()?;
757 let Some((batch, write_batch_size)) = self.process_chunk(chunk)? else {
758 return Ok(());
759 };
760
761 let writer = self.writer.get_writer().unwrap();
762 let batch_rows = batch.num_rows();
763 let batch_columns = batch.num_columns();
764 writer
765 .write(batch)
766 .instrument_await("iceberg_write")
767 .await
768 .inspect_err(|err| {
769 tracing::error!(
770 iceberg_component = "sink_writer",
771 iceberg_operation = "write_batch",
772 actor_id = %self.actor_id,
773 sink_id = %self.sink_id,
774 sink_name = %self.sink_name,
775 table = %self.table_name,
776 writer_mode = self.writer_mode,
777 batch_rows,
778 batch_columns,
779 write_bytes = write_batch_size,
780 error = %err.as_report(),
781 "iceberg_sink_writer_write_failed",
782 );
783 })?;
784 self.metrics.write_bytes.inc_by(write_batch_size as _);
785 Ok(())
786 }
787
788 pub async fn write_batch_with_position(
789 &mut self,
790 chunk: StreamChunk,
791 ) -> Result<Vec<PositionDeleteInput>> {
792 debug_assert!(
793 matches!(self.writer, IcebergWriterDispatch::Append { .. }),
794 "write_batch_with_position should only be called for append-only writer"
795 );
796
797 self.prepare_writer()?;
798 let Some((batch, write_batch_size)) = self.process_chunk(chunk)? else {
799 return Ok(vec![]);
800 };
801
802 let writer = self.writer.get_writer().unwrap();
803 let batch_rows = batch.num_rows();
804 let batch_columns = batch.num_columns();
805 let positions = writer
806 .write_with_position(batch)
807 .instrument_await("iceberg_write")
808 .await
809 .inspect_err(|err| {
810 tracing::error!(
811 iceberg_component = "sink_writer",
812 iceberg_operation = "write_batch_with_position",
813 actor_id = %self.actor_id,
814 sink_id = %self.sink_id,
815 sink_name = %self.sink_name,
816 table = %self.table_name,
817 writer_mode = self.writer_mode,
818 batch_rows,
819 batch_columns,
820 write_bytes = write_batch_size,
821 error = %err.as_report(),
822 "iceberg_sink_writer_write_with_position_failed",
823 );
824 })?;
825 self.metrics.write_bytes.inc_by(write_batch_size as _);
826 Ok(positions)
827 }
828
829 pub async fn close(&mut self) -> Result<Option<Vec<DataFile>>> {
830 let res = match &mut self.writer {
831 IcebergWriterDispatch::Append {
832 writer,
833 writer_builder,
834 } => {
835 let close_result = match writer.take() {
836 Some(mut writer) => {
837 let data_files = writer.close().instrument_await("iceberg_close").await?;
838 Some(data_files)
839 }
840 _ => None,
841 };
842 match writer_builder.build() {
843 Ok(new_writer) => {
844 *writer = Some(Box::new(new_writer));
845 }
846 Err(err) => {
847 tracing::warn!(
850 iceberg_component = "sink_writer",
851 iceberg_operation = "rebuild_writer",
852 actor_id = %self.actor_id,
853 sink_id = %self.sink_id,
854 sink_name = %self.sink_name,
855 table = %self.table_name,
856 writer_mode = self.writer_mode,
857 error = %err.as_report(),
858 "iceberg_sink_writer_rebuild_failed_after_close",
859 );
860 }
861 }
862 close_result
863 }
864 IcebergWriterDispatch::Upsert {
865 writer,
866 writer_builder,
867 ..
868 } => {
869 let close_result = match writer.take() {
870 Some(mut writer) => {
871 let data_files = writer.close().instrument_await("iceberg_close").await?;
872 Some(data_files)
873 }
874 _ => None,
875 };
876 match writer_builder.build() {
877 Ok(new_writer) => {
878 *writer = Some(Box::new(new_writer));
879 }
880 Err(err) => {
881 tracing::warn!(
884 iceberg_component = "sink_writer",
885 iceberg_operation = "rebuild_writer",
886 actor_id = %self.actor_id,
887 sink_id = %self.sink_id,
888 sink_name = %self.sink_name,
889 table = %self.table_name,
890 writer_mode = self.writer_mode,
891 error = %err.as_report(),
892 "iceberg_sink_writer_rebuild_failed_after_close",
893 );
894 }
895 }
896 close_result
897 }
898 };
899 Ok(res)
900 }
901
902 pub fn generate_commit_metadata(&self, data_files: Vec<DataFile>) -> Result<SinkMetadata> {
903 let serialized_data_files = serialize_data_files_default_spec(&self.table, data_files)?;
904 SinkMetadata::try_from(&IcebergCommitResult {
905 data_files: serialized_data_files,
906 schema_id: self.table.metadata().current_schema_id(),
907 partition_spec_id: self.table.metadata().default_partition_spec_id(),
908 })
909 }
910
911 pub fn partition_type(&self) -> &iceberg::spec::StructType {
912 self.table.metadata().default_partition_type()
913 }
914}
915
916#[async_trait]
917impl SinkWriter for IcebergSinkWriter {
918 type CommitMetadata = Option<SinkMetadata>;
919
920 async fn begin_epoch(&mut self, _epoch: u64) -> Result<()> {
922 let Self::Created(args) = self else {
923 return Ok(());
924 };
925
926 let table = create_and_validate_table_impl(&args.config, &args.sink_param).await?;
927 let inner = match &args.upsert_primary_key_column_names {
928 Some(primary_key_column_names) => IcebergSinkWriterInner::build_upsert(
929 &args.config,
930 table,
931 primary_key_column_names.clone(),
932 &args.writer_param,
933 )?,
934 None => {
935 IcebergSinkWriterInner::build_append_only(&args.config, table, &args.writer_param)?
936 }
937 };
938
939 *self = IcebergSinkWriter::Initialized(inner);
940 Ok(())
941 }
942
943 async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()> {
945 let Self::Initialized(inner) = self else {
946 unreachable!("IcebergSinkWriter should be initialized before barrier");
947 };
948 inner.write_batch(chunk).await
949 }
950
951 async fn barrier(&mut self, is_checkpoint: bool) -> Result<Option<SinkMetadata>> {
954 let Self::Initialized(inner) = self else {
955 unreachable!("IcebergSinkWriter should be initialized before barrier");
956 };
957
958 if !is_checkpoint {
960 return Ok(None);
961 }
962
963 let data_files = inner
964 .close()
965 .await?
966 .ok_or_else(|| anyhow!("No writer to close"))?;
967 let res = inner.generate_commit_metadata(data_files)?;
968 Ok(Some(res))
969 }
970}
971
972const MAX_COLUMN_STAT_SIZE: usize = 10240; pub fn serialize_data_files_default_spec(
992 table: &Table,
993 files: Vec<DataFile>,
994) -> Result<Vec<SerializedDataFile>> {
995 let format_version = table.metadata().format_version();
996 let partition_type = table.metadata().default_partition_type();
997 files
998 .into_iter()
999 .map(|f| {
1000 let truncated = truncate_datafile(f);
1002 Ok(SerializedDataFile::try_from(
1003 truncated,
1004 partition_type,
1005 format_version,
1006 )?)
1007 })
1008 .collect()
1009}
1010
1011pub fn resolve_partition_spec(table: &Table, spec_id: i32) -> Result<PartitionSpecRef> {
1015 table
1016 .metadata()
1017 .partition_spec_by_id(spec_id)
1018 .cloned()
1019 .ok_or_else(|| SinkError::Iceberg(anyhow!("partition spec {} not found", spec_id)))
1020}
1021
1022pub fn resolve_partition_type(table: &Table, spec_id: i32, schema: &Schema) -> Result<StructType> {
1028 resolve_partition_spec(table, spec_id)?
1029 .partition_type(schema)
1030 .map_err(|e| SinkError::Iceberg(anyhow!(e)))
1031}
1032
1033pub fn truncate_datafile(mut data_file: DataFile) -> DataFile {
1044 data_file.lower_bounds.retain(|field_id, datum| {
1046 let size = match datum.to_bytes() {
1048 Ok(bytes) => bytes.len(),
1049 Err(_) => 0,
1050 };
1051
1052 if size > MAX_COLUMN_STAT_SIZE {
1053 tracing::debug!(
1054 field_id = field_id,
1055 size = size,
1056 "Truncating large lower_bound statistic"
1057 );
1058 return false;
1059 }
1060 true
1061 });
1062
1063 data_file.upper_bounds.retain(|field_id, datum| {
1065 let size = match datum.to_bytes() {
1067 Ok(bytes) => bytes.len(),
1068 Err(_) => 0,
1069 };
1070
1071 if size > MAX_COLUMN_STAT_SIZE {
1072 tracing::debug!(
1073 field_id = field_id,
1074 size = size,
1075 "Truncating large upper_bound statistic"
1076 );
1077 return false;
1078 }
1079 true
1080 });
1081
1082 data_file
1083}