1use std::collections::HashMap;
16use std::sync::Arc;
17use std::time::Duration;
18
19use anyhow::{Context, anyhow};
20use async_trait::async_trait;
21use iceberg::Catalog;
22use iceberg::arrow::schema_to_arrow_schema;
23use iceberg::spec::{DataFile, Operation, SerializedDataFile, TableMetadata};
24use iceberg::table::Table;
25use iceberg::transaction::{AddColumn, ApplyTransactionAction, FastAppendAction, Transaction};
26use itertools::Itertools;
27use risingwave_common::array::arrow::arrow_schema_iceberg::{
28 DataType as ArrowDataType, Field as ArrowField, Fields as ArrowFields,
29};
30use risingwave_common::array::arrow::{IcebergArrowConvert, IcebergCreateTableArrowConvert};
31use risingwave_common::bail;
32use risingwave_common::catalog::{Field, RISINGWAVE_ICEBERG_COMMIT_EPOCH};
33use risingwave_common::error::IcebergError;
34use risingwave_pb::connector_service::SinkMetadata;
35use risingwave_pb::connector_service::sink_metadata::Metadata::Serialized;
36use risingwave_pb::connector_service::sink_metadata::SerializedMetadata;
37use risingwave_pb::stream_plan::PbSinkSchemaChange;
38use serde::{Deserialize, Serialize};
39use serde_json::from_value;
40use thiserror_ext::AsReport;
41use tokio::sync::mpsc::UnboundedSender;
42use tracing::warn;
43
44use super::commit_retry::{self, CommitError, CommitRetryLogContext};
45use super::{GLOBAL_SINK_METRICS, IcebergConfig, SinkError, commit_branch, resolve_partition_type};
46use crate::connector_common::{IcebergCommittedSnapshot, IcebergSinkCompactionUpdate};
47use crate::sink::catalog::SinkId;
48use crate::sink::{Result, SinglePhaseCommitCoordinator, SinkParam, TwoPhaseCommitCoordinator};
49
50const SCHEMA_ID: &str = "schema_id";
51const PARTITION_SPEC_ID: &str = "partition_spec_id";
52const DATA_FILES: &str = "data_files";
53
54#[derive(Default, Clone)]
55pub struct IcebergCommitResult {
56 pub schema_id: i32,
57 pub partition_spec_id: i32,
58 pub data_files: Vec<SerializedDataFile>,
59}
60
61impl IcebergCommitResult {
62 pub fn try_from(value: &SinkMetadata) -> Result<Self> {
63 let Some(Serialized(value)) = &value.metadata else {
64 bail!("Can't create iceberg sink write result from empty data!");
65 };
66
67 Self::try_from_serialized_bytes(&value.metadata)
68 }
69
70 pub fn try_from_serialized_bytes(value: &[u8]) -> Result<Self> {
71 let mut values = if let serde_json::Value::Object(value) =
72 serde_json::from_slice::<serde_json::Value>(value)
73 .context("Can't parse iceberg sink metadata")?
74 {
75 value
76 } else {
77 bail!("iceberg sink metadata should be an object");
78 };
79
80 let schema_id;
81 if let Some(serde_json::Value::Number(value)) = values.remove(SCHEMA_ID) {
82 schema_id = value
83 .as_u64()
84 .ok_or_else(|| anyhow!("schema_id should be a u64"))?;
85 } else {
86 bail!("iceberg sink metadata should have schema_id");
87 }
88
89 let partition_spec_id;
90 if let Some(serde_json::Value::Number(value)) = values.remove(PARTITION_SPEC_ID) {
91 partition_spec_id = value
92 .as_u64()
93 .ok_or_else(|| anyhow!("partition_spec_id should be a u64"))?;
94 } else {
95 bail!("iceberg sink metadata should have partition_spec_id");
96 }
97
98 let data_files: Vec<SerializedDataFile>;
99 if let serde_json::Value::Array(values) = values
100 .remove(DATA_FILES)
101 .ok_or_else(|| anyhow!("iceberg sink metadata should have data_files object"))?
102 {
103 data_files = values
104 .into_iter()
105 .map(from_value::<SerializedDataFile>)
106 .collect::<std::result::Result<_, _>>()
107 .unwrap();
108 } else {
109 bail!("iceberg sink metadata should have data_files object");
110 }
111
112 Ok(Self {
113 schema_id: schema_id as i32,
114 partition_spec_id: partition_spec_id as i32,
115 data_files,
116 })
117 }
118}
119
120impl<'a> TryFrom<&'a IcebergCommitResult> for SinkMetadata {
121 type Error = SinkError;
122
123 fn try_from(value: &'a IcebergCommitResult) -> std::result::Result<SinkMetadata, Self::Error> {
124 let bytes = <Vec<u8>>::try_from(value)?;
125 Ok(SinkMetadata {
126 metadata: Some(Serialized(SerializedMetadata { metadata: bytes })),
127 })
128 }
129}
130
131impl<'a> TryFrom<&'a IcebergCommitResult> for Vec<u8> {
132 type Error = SinkError;
133
134 fn try_from(value: &'a IcebergCommitResult) -> std::result::Result<Vec<u8>, Self::Error> {
135 let json_data_files = serde_json::Value::Array(
136 value
137 .data_files
138 .iter()
139 .map(serde_json::to_value)
140 .collect::<std::result::Result<Vec<serde_json::Value>, _>>()
141 .context("Can't serialize data files to json")?,
142 );
143 let json_value = serde_json::Value::Object(
144 vec![
145 (
146 SCHEMA_ID.to_owned(),
147 serde_json::Value::Number(value.schema_id.into()),
148 ),
149 (
150 PARTITION_SPEC_ID.to_owned(),
151 serde_json::Value::Number(value.partition_spec_id.into()),
152 ),
153 (DATA_FILES.to_owned(), json_data_files),
154 ]
155 .into_iter()
156 .collect(),
157 );
158 Ok(serde_json::to_vec(&json_value).context("Can't serialize iceberg sink metadata")?)
159 }
160}
161
162#[derive(Default, Clone, Serialize, Deserialize)]
163pub struct IcebergPositionDeleteCommitResult {
164 pub schema_id: i32,
165 pub partition_spec_id: i32,
166 pub delete_files: Vec<SerializedDataFile>,
167 pub overwrite_files: Vec<SerializedDataFile>,
168}
169
170impl<'a> TryFrom<&'a SinkMetadata> for IcebergPositionDeleteCommitResult {
171 type Error = SinkError;
172
173 fn try_from(value: &'a SinkMetadata) -> Result<Self> {
174 let Some(Serialized(value)) = &value.metadata else {
175 bail!("Can't create iceberg dv merger commit result from empty data!");
176 };
177 let value = serde_json::from_slice(&value.metadata)
178 .context("Can't deserialize iceberg dv merger commit result from metadata")?;
179 Ok(value)
180 }
181}
182
183impl<'a> TryFrom<&'a IcebergPositionDeleteCommitResult> for SinkMetadata {
184 type Error = SinkError;
185
186 fn try_from(value: &'a IcebergPositionDeleteCommitResult) -> Result<SinkMetadata> {
187 let bytes = serde_json::to_vec(value)
188 .context("Can't serialize iceberg dv merger commit result to metadata")?;
189 Ok(SinkMetadata {
190 metadata: Some(Serialized(SerializedMetadata { metadata: bytes })),
191 })
192 }
193}
194
195fn arrow_data_type_compatible(current: &ArrowDataType, expected: &ArrowDataType) -> bool {
196 use ArrowDataType::*;
197
198 match (current, expected) {
199 (Decimal128(_, _), Decimal128(_, _)) => true,
205 (Binary, LargeBinary) | (LargeBinary, Binary) => true,
206 (List(current_field), List(expected_field)) => {
207 arrow_data_type_compatible(current_field.data_type(), expected_field.data_type())
208 }
209 (Map(current_field, current_sorted), Map(expected_field, expected_sorted)) => {
210 current_sorted == expected_sorted
211 && arrow_data_type_compatible(current_field.data_type(), expected_field.data_type())
212 }
213 (Struct(current_fields), Struct(expected_fields)) => {
214 let expected_fields = expected_fields
215 .iter()
216 .map(|field| field.as_ref().clone())
217 .collect_vec();
218 schema_contains_same_fields(current_fields, &expected_fields)
219 }
220 _ => current == expected,
221 }
222}
223
224fn schema_contains_same_fields(current: &ArrowFields, expected: &[ArrowField]) -> bool {
225 if current.len() != expected.len() {
226 return false;
227 }
228
229 let mut unmatched_current = current.iter().collect_vec();
230 expected.iter().all(|expected_field| {
231 let Some(pos) = unmatched_current.iter().position(|current_field| {
232 current_field.name() == expected_field.name()
233 && arrow_data_type_compatible(current_field.data_type(), expected_field.data_type())
234 }) else {
235 return false;
236 };
237 unmatched_current.swap_remove(pos);
238 true
239 })
240}
241
242pub struct IcebergSinkCommitter {
243 pub(super) catalog: Arc<dyn Catalog>,
244 pub(super) table: Table,
245 pub last_commit_epoch: u64,
246 pub(crate) sink_id: SinkId,
247 pub(crate) config: IcebergConfig,
248 pub(crate) param: SinkParam,
249 pub(super) commit_retry_num: u32,
250 pub(crate) iceberg_compact_stat_sender: Option<UnboundedSender<IcebergSinkCompactionUpdate>>,
251}
252
253impl IcebergSinkCommitter {
254 fn latest_observed_snapshot(&self) -> Option<IcebergCommittedSnapshot> {
255 let branch = commit_branch(self.config.r#type.as_str(), self.config.write_mode);
256 IcebergCommittedSnapshot::from_branch_head(self.table.metadata(), &branch)
257 }
258
259 fn notify_iceberg_compaction_scheduler(&self, force_compaction: bool) {
260 let Some(iceberg_compact_stat_sender) = &self.iceberg_compact_stat_sender else {
261 return;
262 };
263 let branch = commit_branch(self.config.r#type.as_str(), self.config.write_mode);
264
265 let Some(observed_snapshot) = self.latest_observed_snapshot() else {
266 warn!(
267 iceberg_component = "sink_committer",
268 iceberg_operation = "notify_compaction",
269 sink_id = %self.sink_id,
270 sink_name = %self.param.sink_name,
271 table = %self.table.identifier(),
272 branch = %branch,
273 force_compaction,
274 "iceberg_sink_compaction_update_skipped",
275 );
276 return;
277 };
278
279 let observed_snapshot_id = observed_snapshot.snapshot_id;
280 let observed_snapshot_timestamp_ms = observed_snapshot.timestamp_ms;
281 let observed_snapshot_branch = observed_snapshot.branch.clone();
282
283 if iceberg_compact_stat_sender
284 .send(IcebergSinkCompactionUpdate {
285 sink_id: self.sink_id,
286 force_compaction,
287 has_new_data: true,
288 observed_snapshot,
289 })
290 .is_err()
291 {
292 warn!(
293 iceberg_component = "sink_committer",
294 iceberg_operation = "notify_compaction",
295 sink_id = %self.sink_id,
296 sink_name = %self.param.sink_name,
297 table = %self.table.identifier(),
298 force_compaction,
299 observed_snapshot_id,
300 observed_snapshot_timestamp_ms,
301 observed_snapshot_branch = %observed_snapshot_branch,
302 "iceberg_sink_compaction_update_send_failed",
303 );
304 }
305 }
306}
307
308#[async_trait]
309impl SinglePhaseCommitCoordinator for IcebergSinkCommitter {
310 async fn init(&mut self) -> Result<()> {
311 tracing::info!(
312 iceberg_component = "sink_committer",
313 iceberg_operation = "init",
314 sink_id = %self.param.sink_id,
315 sink_name = %self.param.sink_name,
316 table = %self.table.identifier(),
317 "iceberg_sink_committer_initialized",
318 );
319
320 Ok(())
321 }
322
323 async fn commit_data(&mut self, epoch: u64, metadata: Vec<SinkMetadata>) -> Result<()> {
324 tracing::debug!(
325 iceberg_component = "sink_committer",
326 iceberg_operation = "direct_commit",
327 sink_id = %self.sink_id,
328 sink_name = %self.param.sink_name,
329 table = %self.table.identifier(),
330 epoch,
331 metadata_count = metadata.len(),
332 "iceberg_sink_commit_started",
333 );
334
335 if metadata.is_empty() {
336 tracing::debug!(
337 iceberg_component = "sink_committer",
338 iceberg_operation = "direct_commit",
339 sink_id = %self.sink_id,
340 table = %self.table.identifier(),
341 epoch,
342 "iceberg_sink_commit_skipped_empty_metadata",
343 );
344 return Ok(());
345 }
346
347 if let Some((write_results, snapshot_id)) = self.pre_commit_inner(epoch, metadata)? {
349 self.commit_data_impl(epoch, write_results, snapshot_id)
350 .await?;
351 }
352
353 Ok(())
354 }
355
356 async fn commit_schema_change(
357 &mut self,
358 epoch: u64,
359 schema_change: PbSinkSchemaChange,
360 ) -> Result<()> {
361 tracing::info!(
362 iceberg_component = "sink_committer",
363 iceberg_operation = "schema_change",
364 sink_id = %self.sink_id,
365 sink_name = %self.param.sink_name,
366 table = %self.table.identifier(),
367 epoch,
368 schema_change = ?schema_change,
369 "iceberg_sink_schema_change_commit_started",
370 );
371 self.commit_schema_change_impl(schema_change).await?;
372 tracing::info!(
373 iceberg_component = "sink_committer",
374 iceberg_operation = "schema_change",
375 sink_id = %self.sink_id,
376 table = %self.table.identifier(),
377 epoch,
378 "iceberg_sink_schema_change_commit_succeeded",
379 );
380
381 Ok(())
382 }
383}
384
385#[async_trait]
386impl TwoPhaseCommitCoordinator for IcebergSinkCommitter {
387 async fn init(&mut self) -> Result<()> {
388 tracing::info!(
389 iceberg_component = "sink_committer",
390 iceberg_operation = "init",
391 sink_id = %self.param.sink_id,
392 sink_name = %self.param.sink_name,
393 table = %self.table.identifier(),
394 "iceberg_sink_committer_initialized",
395 );
396
397 Ok(())
398 }
399
400 async fn pre_commit(
401 &mut self,
402 epoch: u64,
403 metadata: Vec<SinkMetadata>,
404 _schema_change: Option<PbSinkSchemaChange>,
405 ) -> Result<Option<Vec<u8>>> {
406 tracing::debug!(
407 iceberg_component = "sink_committer",
408 iceberg_operation = "pre_commit",
409 sink_id = %self.sink_id,
410 sink_name = %self.param.sink_name,
411 table = %self.table.identifier(),
412 epoch,
413 metadata_count = metadata.len(),
414 "iceberg_sink_pre_commit_started",
415 );
416
417 let (write_results, snapshot_id) = match self.pre_commit_inner(epoch, metadata)? {
418 Some((write_results, snapshot_id)) => (write_results, snapshot_id),
419 None => {
420 tracing::debug!(
421 iceberg_component = "sink_committer",
422 iceberg_operation = "pre_commit",
423 sink_id = %self.sink_id,
424 table = %self.table.identifier(),
425 epoch,
426 "iceberg_sink_pre_commit_skipped_no_data",
427 );
428 return Ok(None);
429 }
430 };
431
432 let mut write_results_bytes = Vec::new();
433 for each_parallelism_write_result in write_results {
434 let each_parallelism_write_result_bytes =
435 <Vec<u8>>::try_from(&each_parallelism_write_result)?;
436 write_results_bytes.push(each_parallelism_write_result_bytes);
437 }
438
439 let snapshot_id_bytes: Vec<u8> = snapshot_id.to_le_bytes().to_vec();
440 write_results_bytes.push(snapshot_id_bytes);
441
442 let pre_commit_metadata_bytes: Vec<u8> = serialize_metadata(write_results_bytes);
443 tracing::debug!(
444 iceberg_component = "sink_committer",
445 iceberg_operation = "pre_commit",
446 sink_id = %self.sink_id,
447 table = %self.table.identifier(),
448 epoch,
449 snapshot_id,
450 pre_commit_metadata_bytes = pre_commit_metadata_bytes.len(),
451 "iceberg_sink_pre_commit_metadata_encoded",
452 );
453 Ok(Some(pre_commit_metadata_bytes))
454 }
455
456 async fn commit_data(&mut self, epoch: u64, commit_metadata: Vec<u8>) -> Result<()> {
457 tracing::debug!(
458 iceberg_component = "sink_committer",
459 iceberg_operation = "commit",
460 sink_id = %self.sink_id,
461 sink_name = %self.param.sink_name,
462 table = %self.table.identifier(),
463 epoch,
464 commit_metadata_bytes = commit_metadata.len(),
465 "iceberg_sink_commit_started",
466 );
467
468 if commit_metadata.is_empty() {
469 tracing::debug!(
470 iceberg_component = "sink_committer",
471 iceberg_operation = "commit",
472 sink_id = %self.sink_id,
473 table = %self.table.identifier(),
474 epoch,
475 "iceberg_sink_commit_skipped_empty_metadata",
476 );
477 return Ok(());
478 }
479
480 let mut payload = deserialize_metadata(commit_metadata);
482 if payload.is_empty() {
483 return Err(SinkError::Iceberg(anyhow!(
484 "Invalid commit metadata: empty payload"
485 )));
486 }
487
488 let snapshot_id_bytes = payload.pop().ok_or_else(|| {
490 SinkError::Iceberg(anyhow!("Invalid commit metadata: missing snapshot_id"))
491 })?;
492 let snapshot_id = i64::from_le_bytes(
493 snapshot_id_bytes
494 .try_into()
495 .map_err(|_| SinkError::Iceberg(anyhow!("Invalid snapshot id bytes")))?,
496 );
497
498 let write_results = payload
500 .into_iter()
501 .map(|p| IcebergCommitResult::try_from_serialized_bytes(&p))
502 .collect::<Result<Vec<_>>>()?;
503
504 let snapshot_committed = self.is_snapshot_id_in_iceberg(snapshot_id).await?;
505
506 if snapshot_committed {
507 tracing::info!(
508 iceberg_component = "sink_committer",
509 iceberg_operation = "commit",
510 sink_id = %self.sink_id,
511 sink_name = %self.param.sink_name,
512 table = %self.table.identifier(),
513 epoch,
514 snapshot_id,
515 "iceberg_sink_commit_skipped_snapshot_already_committed",
516 );
517 return Ok(());
518 }
519
520 self.commit_data_impl(epoch, write_results, snapshot_id)
521 .await
522 }
523
524 async fn commit_schema_change(
525 &mut self,
526 epoch: u64,
527 schema_change: PbSinkSchemaChange,
528 ) -> Result<()> {
529 let schema_updated = self.check_schema_change_applied(&schema_change)?;
530 if schema_updated {
531 tracing::info!(
532 iceberg_component = "sink_committer",
533 iceberg_operation = "schema_change",
534 sink_id = %self.sink_id,
535 table = %self.table.identifier(),
536 epoch,
537 "iceberg_sink_schema_change_skipped_already_applied",
538 );
539 return Ok(());
540 }
541
542 tracing::info!(
543 iceberg_component = "sink_committer",
544 iceberg_operation = "schema_change",
545 sink_id = %self.sink_id,
546 sink_name = %self.param.sink_name,
547 table = %self.table.identifier(),
548 epoch,
549 schema_change = ?schema_change,
550 "iceberg_sink_schema_change_commit_started",
551 );
552 self.commit_schema_change_impl(schema_change).await?;
553 tracing::info!(
554 iceberg_component = "sink_committer",
555 iceberg_operation = "schema_change",
556 sink_id = %self.sink_id,
557 table = %self.table.identifier(),
558 epoch,
559 "iceberg_sink_schema_change_commit_succeeded",
560 );
561
562 Ok(())
563 }
564
565 async fn abort(&mut self, epoch: u64, _commit_metadata: Vec<u8>) {
566 tracing::debug!(
568 iceberg_component = "sink_committer",
569 iceberg_operation = "abort",
570 sink_id = %self.sink_id,
571 table = %self.table.identifier(),
572 epoch,
573 "iceberg_sink_commit_abort_unimplemented",
574 );
575 }
576}
577
578impl IcebergSinkCommitter {
580 fn pre_commit_inner(
581 &mut self,
582 epoch: u64,
583 metadata: Vec<SinkMetadata>,
584 ) -> Result<Option<(Vec<IcebergCommitResult>, i64)>> {
585 let write_results: Vec<IcebergCommitResult> = metadata
586 .iter()
587 .map(IcebergCommitResult::try_from)
588 .collect::<Result<Vec<IcebergCommitResult>>>()?;
589 let data_file_count: usize = write_results.iter().map(|r| r.data_files.len()).sum();
590 tracing::debug!(
591 iceberg_component = "sink_committer",
592 iceberg_operation = "pre_commit",
593 sink_id = %self.sink_id,
594 table = %self.table.identifier(),
595 epoch,
596 writer_result_count = write_results.len(),
597 data_file_count,
598 "iceberg_sink_pre_commit_metadata_decoded",
599 );
600
601 if write_results.is_empty() || write_results.iter().all(|r| r.data_files.is_empty()) {
603 return Ok(None);
604 }
605
606 let expect_schema_id = write_results[0].schema_id;
607 let expect_partition_spec_id = write_results[0].partition_spec_id;
608
609 if write_results
611 .iter()
612 .any(|r| r.schema_id != expect_schema_id)
613 || write_results
614 .iter()
615 .any(|r| r.partition_spec_id != expect_partition_spec_id)
616 {
617 return Err(SinkError::Iceberg(anyhow!(
618 "schema_id and partition_spec_id should be the same in all write results"
619 )));
620 }
621
622 let snapshot_id = FastAppendAction::generate_snapshot_id(&self.table);
623 tracing::debug!(
624 iceberg_component = "sink_committer",
625 iceberg_operation = "pre_commit",
626 sink_id = %self.sink_id,
627 table = %self.table.identifier(),
628 epoch,
629 snapshot_id,
630 schema_id = expect_schema_id,
631 partition_spec_id = expect_partition_spec_id,
632 data_file_count,
633 "iceberg_sink_pre_commit_snapshot_assigned",
634 );
635
636 Ok(Some((write_results, snapshot_id)))
637 }
638
639 async fn commit_data_impl(
640 &mut self,
641 epoch: u64,
642 write_results: Vec<IcebergCommitResult>,
643 snapshot_id: i64,
644 ) -> Result<()> {
645 assert!(
647 !write_results.is_empty() && !write_results.iter().all(|r| r.data_files.is_empty())
648 );
649
650 self.wait_for_snapshot_limit().await?;
652
653 let expect_schema_id = write_results[0].schema_id;
654 let expect_partition_spec_id = write_results[0].partition_spec_id;
655 let data_file_count: usize = write_results.iter().map(|r| r.data_files.len()).sum();
656 let target_branch = commit_branch(self.config.r#type.as_str(), self.config.write_mode);
657 tracing::info!(
658 iceberg_component = "sink_committer",
659 iceberg_operation = "commit",
660 sink_id = %self.sink_id,
661 sink_name = %self.param.sink_name,
662 table = %self.table.identifier(),
663 branch = %target_branch,
664 epoch,
665 snapshot_id,
666 schema_id = expect_schema_id,
667 partition_spec_id = expect_partition_spec_id,
668 data_file_count,
669 retry_num = self.commit_retry_num,
670 "iceberg_sink_commit_applying",
671 );
672
673 self.table = commit_retry::reload_table(
675 self.catalog.as_ref(),
676 self.table.identifier(),
677 expect_schema_id,
678 expect_partition_spec_id,
679 None,
680 )
681 .await
682 .map_err(SinkError::Iceberg)?;
683
684 let Some(schema) = self.table.metadata().schema_by_id(expect_schema_id) else {
685 return Err(SinkError::Iceberg(anyhow!(
686 "Can't find schema by id {}",
687 expect_schema_id
688 )));
689 };
690 let partition_type = resolve_partition_type(&self.table, expect_partition_spec_id, schema)?;
691
692 let data_files = write_results
693 .into_iter()
694 .flat_map(|r| {
695 r.data_files.into_iter().map(|f| {
696 f.try_into(expect_partition_spec_id, &partition_type, schema)
697 .map_err(|err| SinkError::Iceberg(anyhow!(err)))
698 })
699 })
700 .collect::<Result<Vec<DataFile>>>()?;
701
702 let catalog = self.catalog.clone();
707 let sink_id = self.sink_id;
708 let table_ident = self.table.identifier().clone();
709 let table_name = table_ident.to_string();
710 let retry_log_context = CommitRetryLogContext::new(
711 "sink_committer",
712 "commit",
713 table_name.clone(),
714 target_branch.clone(),
715 )
716 .with_sink_id(sink_id)
717 .with_epoch(epoch)
718 .with_snapshot_id(snapshot_id);
719
720 let table = commit_retry::run_with_retry(
721 catalog.clone(),
722 table_ident.clone(),
723 expect_schema_id,
724 expect_partition_spec_id,
725 None,
726 self.commit_retry_num as usize,
727 retry_log_context,
728 |table| {
729 let table_name = table_name.clone();
730 let target_branch = target_branch.clone();
731 let data_files = data_files.clone();
732 let catalog = catalog.clone();
733 async move {
734 let txn = Transaction::new(&table);
735 let append_action = txn
736 .fast_append()
737 .set_snapshot_id(snapshot_id)
738 .set_target_branch(target_branch.clone())
739 .set_snapshot_properties(HashMap::from([(
740 RISINGWAVE_ICEBERG_COMMIT_EPOCH.to_owned(),
741 epoch.to_string(),
742 )]))
743 .add_data_files(data_files);
744
745 let tx = append_action.apply(txn).map_err(|err| {
746 let err: IcebergError = err.into();
747 tracing::error!(
748 iceberg_component = "sink_committer",
749 iceberg_operation = "commit",
750 sink_id = %sink_id,
751 table = %table_name,
752 epoch,
753 snapshot_id,
754 branch = %target_branch,
755 data_file_count,
756 error = %err.as_report(),
757 "iceberg_sink_commit_fast_append_apply_failed",
758 );
759 CommitError::Commit(anyhow!(err).context("apply iceberg fast_append"))
760 })?;
761
762 let table = tx.commit(catalog.as_ref()).await.map_err(|err| {
763 let err: IcebergError = err.into();
764 tracing::error!(
765 iceberg_component = "sink_committer",
766 iceberg_operation = "commit",
767 sink_id = %sink_id,
768 table = %table_name,
769 epoch,
770 snapshot_id,
771 branch = %target_branch,
772 data_file_count,
773 error = %err.as_report(),
774 "iceberg_sink_commit_transaction_failed",
775 );
776 CommitError::Commit(anyhow!(err).context("commit iceberg transaction"))
777 })?;
778 Ok(table)
779 }
780 },
781 )
782 .await
783 .map_err(SinkError::Iceberg)?;
784 self.table = table;
785
786 let snapshot_num = self.table.metadata().snapshots().count();
787 let catalog_name = self.config.common.catalog_name();
788 let table_name = self.table.identifier().to_string();
789 let metrics_labels = [&self.param.sink_name, &catalog_name, &table_name];
790 GLOBAL_SINK_METRICS
791 .iceberg_snapshot_num
792 .with_guarded_label_values(&metrics_labels)
793 .set(snapshot_num as i64);
794
795 tracing::debug!(
796 iceberg_component = "sink_committer",
797 iceberg_operation = "commit",
798 sink_id = %self.sink_id,
799 sink_name = %self.param.sink_name,
800 table = %self.table.identifier(),
801 branch = %target_branch,
802 epoch,
803 snapshot_id,
804 snapshot_num,
805 data_file_count,
806 "iceberg_sink_commit_succeeded",
807 );
808
809 self.notify_iceberg_compaction_scheduler(false);
810
811 Ok(())
812 }
813
814 async fn is_snapshot_id_in_iceberg(&self, snapshot_id: i64) -> Result<bool> {
818 let table = self
819 .catalog
820 .load_table(self.table.identifier())
821 .await
822 .map_err(|err| SinkError::Iceberg(anyhow!(err).context("reload iceberg table")))?;
823 if table.metadata().snapshot_by_id(snapshot_id).is_some() {
824 Ok(true)
825 } else {
826 Ok(false)
827 }
828 }
829
830 fn check_schema_change_applied(&self, schema_change: &PbSinkSchemaChange) -> Result<bool> {
833 let current_schema = self.table.metadata().current_schema();
834 let current_arrow_schema = schema_to_arrow_schema(current_schema.as_ref())
835 .context("Failed to convert schema")
836 .map_err(SinkError::Iceberg)?;
837
838 let iceberg_arrow_convert = IcebergArrowConvert;
839
840 let schema_matches = |expected: &[ArrowField]| {
841 schema_contains_same_fields(current_arrow_schema.fields(), expected)
842 };
843
844 let original_arrow_fields: Vec<ArrowField> = schema_change
845 .original_schema
846 .iter()
847 .map(|pb_field| {
848 let field = Field::from(pb_field);
849 iceberg_arrow_convert
850 .to_arrow_field(&field.name, &field.data_type)
851 .context("Failed to convert field to arrow")
852 .map_err(SinkError::Iceberg)
853 })
854 .collect::<Result<_>>()?;
855
856 if schema_matches(&original_arrow_fields) {
858 tracing::debug!(
859 "Current iceberg schema matches original_schema ({} columns); schema change not applied",
860 original_arrow_fields.len()
861 );
862 return Ok(false);
863 }
864
865 let expected_after_change = match schema_change.op.as_ref() {
866 Some(risingwave_pb::stream_plan::sink_schema_change::Op::AddColumns(
867 add_columns_op,
868 )) => {
869 let add_arrow_fields: Vec<ArrowField> = add_columns_op
870 .fields
871 .iter()
872 .map(|pb_field| {
873 let field = Field::from(pb_field);
874 iceberg_arrow_convert
875 .to_arrow_field(&field.name, &field.data_type)
876 .context("Failed to convert field to arrow")
877 .map_err(SinkError::Iceberg)
878 })
879 .collect::<Result<_>>()?;
880
881 let mut expected_after_change = original_arrow_fields;
882 expected_after_change.extend(add_arrow_fields);
883 expected_after_change
884 }
885 Some(risingwave_pb::stream_plan::sink_schema_change::Op::DropColumns(
886 drop_columns_op,
887 )) => original_arrow_fields
888 .into_iter()
889 .filter(|field| {
890 !drop_columns_op
891 .column_names
892 .iter()
893 .any(|name| name == field.name())
894 })
895 .collect_vec(),
896 _ => {
897 return Err(SinkError::Iceberg(anyhow!(
898 "Unsupported sink schema change op in iceberg sink: {:?}",
899 schema_change.op
900 )));
901 }
902 };
903
904 if schema_matches(&expected_after_change) {
906 tracing::debug!(
907 "Current iceberg schema matches changed schema ({} columns); schema change already applied",
908 expected_after_change.len()
909 );
910 return Ok(true);
911 }
912
913 Err(SinkError::Iceberg(anyhow!(
914 "Current iceberg schema does not match either original_schema ({} cols) or changed schema; cannot determine whether schema change is applied",
915 schema_change.original_schema.len()
916 )))
917 }
918
919 async fn commit_schema_change_impl(&mut self, schema_change: PbSinkSchemaChange) -> Result<()> {
923 let iceberg_create_table_arrow_convert = IcebergCreateTableArrowConvert::default();
925 let mut new_fields = Vec::new();
926
927 let mut drop_column_names = Vec::new();
928 match schema_change.op.as_ref() {
929 Some(risingwave_pb::stream_plan::sink_schema_change::Op::AddColumns(
930 add_columns_op,
931 )) => {
932 let add_columns = add_columns_op.fields.iter().map(Field::from).collect_vec();
933 for field in &add_columns {
934 let arrow_field = iceberg_create_table_arrow_convert
936 .to_arrow_field(&field.name, &field.data_type)
937 .with_context(|| {
938 format!("Failed to convert field '{}' to arrow", field.name)
939 })
940 .map_err(SinkError::Iceberg)?;
941
942 let iceberg_type = iceberg::arrow::arrow_type_to_type(arrow_field.data_type())
944 .map_err(|err| {
945 SinkError::Iceberg(
946 anyhow!(err)
947 .context("Failed to convert Arrow type to Iceberg type"),
948 )
949 })?;
950
951 new_fields.push(AddColumn::optional(&field.name, iceberg_type));
952 tracing::info!("Prepared field '{}' for schema change", field.name);
953 }
954 }
955 Some(risingwave_pb::stream_plan::sink_schema_change::Op::DropColumns(
956 drop_columns_op,
957 )) => {
958 drop_column_names = drop_columns_op.column_names.clone();
959 }
960 _ => {
961 return Err(SinkError::Iceberg(anyhow!(
962 "Unsupported sink schema change op in iceberg sink: {:?}",
963 schema_change.op
964 )));
965 }
966 }
967
968 tracing::info!(
970 "Committing schema change to catalog for table {}",
971 self.table.identifier()
972 );
973
974 let txn = Transaction::new(&self.table);
975 let action_fields_added = new_fields.len();
976 let mut action = txn.update_schema();
977 for field in new_fields {
978 action = action.add_column(field);
979 }
980 for column_name in &drop_column_names {
981 action = action.delete_column(column_name);
982 }
983
984 let updated_table = action
985 .apply(txn)
986 .context("Failed to apply schema update action")
987 .map_err(SinkError::Iceberg)?
988 .commit(self.catalog.as_ref())
989 .await
990 .context("Failed to commit table schema change")
991 .map_err(SinkError::Iceberg)?;
992
993 self.table = updated_table;
994
995 tracing::info!(
996 "Successfully committed schema change, added {} columns and dropped {} columns from iceberg table",
997 action_fields_added,
998 drop_column_names.len()
999 );
1000
1001 Ok(())
1002 }
1003
1004 fn count_snapshots_since_rewrite_in_metadata(metadata: &TableMetadata, branch: &str) -> usize {
1007 let mut snapshot_id = metadata
1009 .snapshot_for_ref(branch)
1010 .map(|snapshot| snapshot.snapshot_id());
1011 let mut count = 0;
1012
1013 while let Some(current_snapshot_id) = snapshot_id {
1015 let Some(snapshot) = metadata.snapshot_by_id(current_snapshot_id) else {
1016 break;
1017 };
1018
1019 if snapshot.summary().operation == Operation::Replace {
1021 break;
1023 }
1024
1025 count += 1;
1027 snapshot_id = snapshot.parent_snapshot_id();
1028 }
1029
1030 count
1031 }
1032
1033 fn count_snapshots_since_rewrite(&self) -> usize {
1035 let branch = commit_branch(self.config.r#type.as_str(), self.config.write_mode);
1036 Self::count_snapshots_since_rewrite_in_metadata(self.table.metadata(), branch.as_str())
1037 }
1038
1039 async fn wait_for_snapshot_limit(&mut self) -> Result<()> {
1041 if let Some(max_snapshots) = self.config.max_snapshots_num_before_compaction {
1042 loop {
1043 let current_count = self.count_snapshots_since_rewrite();
1044
1045 if current_count < max_snapshots {
1046 tracing::info!(
1047 "Snapshot count check passed: {} < {}",
1048 current_count,
1049 max_snapshots
1050 );
1051 break;
1052 }
1053
1054 tracing::info!(
1055 "Snapshot count {} exceeds limit {}, waiting...",
1056 current_count,
1057 max_snapshots
1058 );
1059
1060 self.notify_iceberg_compaction_scheduler(true);
1061
1062 tokio::time::sleep(Duration::from_secs(30)).await;
1064
1065 let table_ident = self.table.identifier().clone();
1067 self.table = self.catalog.load_table(&table_ident).await.map_err(|err| {
1068 SinkError::Iceberg(anyhow!(err).context("reload iceberg table"))
1069 })?;
1070 }
1071 }
1072 Ok(())
1073 }
1074}
1075
1076fn serialize_metadata(metadata: Vec<Vec<u8>>) -> Vec<u8> {
1077 serde_json::to_vec(&metadata).unwrap()
1078}
1079
1080fn deserialize_metadata(bytes: Vec<u8>) -> Vec<Vec<u8>> {
1081 serde_json::from_slice(&bytes).unwrap()
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use std::collections::HashMap;
1087
1088 use iceberg::spec::{
1089 FormatVersion, MAIN_BRANCH, NestedField, PrimitiveType, Schema, Snapshot,
1090 SnapshotReference, SnapshotRetention, SortOrder, Summary, TableMetadataBuilder, Type,
1091 UnboundPartitionSpec,
1092 };
1093 use risingwave_common::array::arrow::arrow_schema_iceberg::{
1094 DataType as ArrowDataType, Field as ArrowField, FieldRef as ArrowFieldRef,
1095 Fields as ArrowFields, Schema as ArrowSchema,
1096 };
1097
1098 use super::*;
1099
1100 #[test]
1101 fn test_schema_contains_same_fields_allows_binary_large_binary() {
1102 let current_schema = ArrowSchema::new(vec![
1103 ArrowField::new("k", ArrowDataType::Int32, true),
1104 ArrowField::new("v", ArrowDataType::LargeBinary, true),
1105 ]);
1106 let expected_fields = vec![
1107 ArrowField::new("k", ArrowDataType::Int32, true),
1108 ArrowField::new("v", ArrowDataType::Binary, true),
1109 ];
1110
1111 assert!(schema_contains_same_fields(
1112 current_schema.fields(),
1113 &expected_fields
1114 ));
1115 }
1116
1117 #[test]
1118 fn test_schema_contains_same_fields_allows_nested_binary_large_binary() {
1119 let current_schema = ArrowSchema::new(vec![
1120 ArrowField::new(
1121 "s",
1122 ArrowDataType::Struct(ArrowFields::from(vec![ArrowField::new(
1123 "payload",
1124 ArrowDataType::LargeBinary,
1125 true,
1126 )])),
1127 true,
1128 ),
1129 ArrowField::new(
1130 "l",
1131 ArrowDataType::List(ArrowFieldRef::new(ArrowField::new_list_field(
1132 ArrowDataType::LargeBinary,
1133 true,
1134 ))),
1135 true,
1136 ),
1137 ArrowField::new_map(
1138 "m",
1139 "entries",
1140 ArrowFieldRef::new(ArrowField::new("key", ArrowDataType::Utf8, false)),
1141 ArrowFieldRef::new(ArrowField::new("value", ArrowDataType::LargeBinary, true)),
1142 false,
1143 true,
1144 ),
1145 ]);
1146 let expected_fields = vec![
1147 ArrowField::new(
1148 "s",
1149 ArrowDataType::Struct(ArrowFields::from(vec![ArrowField::new(
1150 "payload",
1151 ArrowDataType::Binary,
1152 true,
1153 )])),
1154 true,
1155 ),
1156 ArrowField::new(
1157 "l",
1158 ArrowDataType::List(ArrowFieldRef::new(ArrowField::new_list_field(
1159 ArrowDataType::Binary,
1160 true,
1161 ))),
1162 true,
1163 ),
1164 ArrowField::new_map(
1165 "m",
1166 "entries",
1167 ArrowFieldRef::new(ArrowField::new("key", ArrowDataType::Utf8, false)),
1168 ArrowFieldRef::new(ArrowField::new("value", ArrowDataType::Binary, true)),
1169 false,
1170 true,
1171 ),
1172 ];
1173
1174 assert!(schema_contains_same_fields(
1175 current_schema.fields(),
1176 &expected_fields
1177 ));
1178 }
1179
1180 #[test]
1181 fn test_schema_contains_same_fields_allows_decimal_precision_delta() {
1182 let current_schema = ArrowSchema::new(vec![ArrowField::new(
1183 "d",
1184 ArrowDataType::Decimal128(28, 10),
1185 true,
1186 )]);
1187 let expected_fields = vec![ArrowField::new(
1188 "d",
1189 ArrowDataType::Decimal128(38, 10),
1190 true,
1191 )];
1192
1193 assert!(schema_contains_same_fields(
1194 current_schema.fields(),
1195 &expected_fields
1196 ));
1197 }
1198
1199 #[test]
1200 fn test_schema_contains_same_fields_allows_decimal_precision_and_scale_delta() {
1201 let current_schema = ArrowSchema::new(vec![ArrowField::new(
1202 "d",
1203 ArrowDataType::Decimal128(38, 2),
1204 true,
1205 )]);
1206 let expected_fields = vec![ArrowField::new(
1207 "d",
1208 ArrowDataType::Decimal128(38, 10),
1209 true,
1210 )];
1211
1212 assert!(schema_contains_same_fields(
1213 current_schema.fields(),
1214 &expected_fields
1215 ));
1216 }
1217
1218 #[test]
1219 fn test_schema_contains_same_fields_rejects_length_mismatch() {
1220 let current_schema =
1221 ArrowSchema::new(vec![ArrowField::new("k", ArrowDataType::Int32, true)]);
1222 let expected_fields = vec![
1223 ArrowField::new("k", ArrowDataType::Int32, true),
1224 ArrowField::new("v", ArrowDataType::Utf8, true),
1225 ];
1226
1227 assert!(!schema_contains_same_fields(
1228 current_schema.fields(),
1229 &expected_fields
1230 ));
1231 }
1232
1233 #[test]
1234 fn test_schema_contains_same_fields_rejects_type_mismatch() {
1235 let current_schema =
1236 ArrowSchema::new(vec![ArrowField::new("v", ArrowDataType::Utf8, true)]);
1237 let expected_fields = vec![ArrowField::new("v", ArrowDataType::Int32, true)];
1238
1239 assert!(!schema_contains_same_fields(
1240 current_schema.fields(),
1241 &expected_fields
1242 ));
1243 }
1244
1245 #[test]
1246 fn test_schema_contains_same_fields_rejects_reused_duplicate_match() {
1247 let current_schema = ArrowSchema::new(vec![
1248 ArrowField::new("v", ArrowDataType::Int32, true),
1249 ArrowField::new("v", ArrowDataType::Utf8, true),
1250 ]);
1251 let expected_fields = vec![
1252 ArrowField::new("v", ArrowDataType::Int32, true),
1253 ArrowField::new("v", ArrowDataType::Int32, true),
1254 ];
1255
1256 assert!(!schema_contains_same_fields(
1257 current_schema.fields(),
1258 &expected_fields
1259 ));
1260 }
1261
1262 #[test]
1263 fn test_schema_contains_same_fields_rejects_nested_type_mismatch() {
1264 let current_schema = ArrowSchema::new(vec![ArrowField::new(
1265 "s",
1266 ArrowDataType::Struct(ArrowFields::from(vec![ArrowField::new(
1267 "payload",
1268 ArrowDataType::Utf8,
1269 true,
1270 )])),
1271 true,
1272 )]);
1273 let expected_fields = vec![ArrowField::new(
1274 "s",
1275 ArrowDataType::Struct(ArrowFields::from(vec![ArrowField::new(
1276 "payload",
1277 ArrowDataType::Int32,
1278 true,
1279 )])),
1280 true,
1281 )];
1282
1283 assert!(!schema_contains_same_fields(
1284 current_schema.fields(),
1285 &expected_fields
1286 ));
1287 }
1288
1289 #[test]
1290 fn test_count_snapshots_since_rewrite_in_metadata_ignores_other_branches() {
1291 let mut builder = TableMetadataBuilder::new(
1292 Schema::builder()
1293 .with_fields(vec![
1294 NestedField::new(1, "id", Type::Primitive(PrimitiveType::Long), false).into(),
1295 ])
1296 .build()
1297 .unwrap(),
1298 UnboundPartitionSpec::builder().build(),
1299 SortOrder::unsorted_order(),
1300 "s3://warehouse/db/table".to_owned(),
1301 FormatVersion::V2,
1302 HashMap::new(),
1303 )
1304 .unwrap();
1305
1306 for (snapshot_id, parent_snapshot_id, operation) in [
1307 (1, None, Operation::Append),
1308 (2, Some(1), Operation::Append),
1309 (3, Some(2), Operation::Replace),
1310 (4, Some(3), Operation::Append),
1311 (5, None, Operation::Overwrite),
1314 ] {
1315 builder = builder
1316 .add_snapshot(snapshot(snapshot_id, parent_snapshot_id, operation))
1317 .unwrap();
1318 }
1319
1320 let metadata = builder
1321 .set_ref(super::super::ICEBERG_COW_BRANCH, snapshot_ref(4))
1322 .unwrap()
1323 .set_ref(MAIN_BRANCH, snapshot_ref(5))
1324 .unwrap()
1325 .build()
1326 .unwrap()
1327 .metadata;
1328
1329 let count = IcebergSinkCommitter::count_snapshots_since_rewrite_in_metadata(
1330 &metadata,
1331 super::super::ICEBERG_COW_BRANCH,
1332 );
1333
1334 assert_eq!(count, 1);
1335 }
1336
1337 fn snapshot(
1338 snapshot_id: i64,
1339 parent_snapshot_id: Option<i64>,
1340 operation: Operation,
1341 ) -> Snapshot {
1342 Snapshot::builder()
1343 .with_snapshot_id(snapshot_id)
1344 .with_parent_snapshot_id(parent_snapshot_id)
1345 .with_sequence_number(snapshot_id)
1346 .with_timestamp_ms(snapshot_id)
1347 .with_manifest_list(format!("/snap-{snapshot_id}.avro"))
1348 .with_summary(Summary {
1349 operation,
1350 additional_properties: HashMap::new(),
1351 })
1352 .with_schema_id(0)
1353 .build()
1354 }
1355
1356 fn snapshot_ref(snapshot_id: i64) -> SnapshotReference {
1357 SnapshotReference::new(snapshot_id, SnapshotRetention::branch(None, None, None))
1358 }
1359}