1use anyhow::Context;
16use iceberg::writer::PositionDeleteInput;
17use risingwave_common::array::DataChunk;
18use risingwave_common::array::stream_record::Record;
19use risingwave_common::id::SinkId;
20use risingwave_common::row::{Project, RowExt};
21use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
22use risingwave_common::util::iter_util::ZipEqFast;
23use risingwave_pb::connector_service::SinkMetadata;
24use risingwave_pb::stream_service::PbIcebergPkIndexSinkRole;
25use risingwave_storage::StateStore;
26
27use crate::common::change_buffer::output_kind;
28use crate::common::compact_chunk::{InconsistencyBehavior, compact_chunk_inline};
29use crate::executor::prelude::*;
30use crate::task::LocalBarrierManager;
31
32type PkRow<'a> = Project<'a, RowRef<'a>>;
33
34fn new_chunk_builder(chunk_size: usize) -> DataChunkBuilder {
35 DataChunkBuilder::new(vec![DataType::Varchar, DataType::Int64], chunk_size)
36}
37
38fn append_row(builder: &mut DataChunkBuilder, file_path: &str, position: i64) -> Option<DataChunk> {
39 builder.append_one_row([
40 Some(ScalarRefImpl::Utf8(file_path)),
41 Some(ScalarRefImpl::Int64(position)),
42 ])
43}
44
45#[async_trait::async_trait]
50pub trait IcebergWriter: Send + 'static {
51 async fn write_chunk(
53 &mut self,
54 chunk: DataChunk,
55 ) -> StreamExecutorResult<Vec<PositionDeleteInput>>;
56
57 async fn flush(&mut self) -> StreamExecutorResult<Option<SinkMetadata>>;
60}
61
62pub struct WriterExecutor<S, W>
76where
77 S: StateStore,
78 W: IcebergWriter,
79{
80 ctx: ActorContextRef,
81 input: Option<Executor>,
82 pk_indices: Vec<usize>,
84 pk_index_state_table: StateTable<S>,
87 writer: W,
89 delete_position_buffer: Option<DataChunkBuilder>,
91 chunk_size: usize,
92 sink_id: SinkId,
93 local_barrier_manager: LocalBarrierManager,
94}
95
96impl<S, W> WriterExecutor<S, W>
97where
98 S: StateStore,
99 W: IcebergWriter,
100{
101 #[allow(clippy::too_many_arguments)]
102 pub fn new(
103 ctx: ActorContextRef,
104 input: Executor,
105 pk_indices: Vec<usize>,
106 pk_index_state_table: StateTable<S>,
107 writer: W,
108 chunk_size: usize,
109 sink_id: SinkId,
110 local_barrier_manager: LocalBarrierManager,
111 ) -> Self {
112 Self {
113 ctx,
114 input: Some(input),
115 pk_indices,
116 pk_index_state_table,
117 writer,
118 delete_position_buffer: None,
119 chunk_size,
120 sink_id,
121 local_barrier_manager,
122 }
123 }
124
125 async fn delete_existing_row(
126 &mut self,
127 pk_row: PkRow<'_>,
128 delete_position_buffer: &mut DataChunkBuilder,
129 ) -> StreamExecutorResult<Option<DataChunk>> {
130 let Some(index_row) = self.pk_index_state_table.get_row(pk_row).await? else {
131 return Ok(None);
132 };
133
134 let num_cols = index_row.len();
135 let file_path = index_row
136 .datum_at(num_cols - 2)
137 .context("file_path should not be null")?
138 .into_utf8();
139 let position = index_row
140 .datum_at(num_cols - 1)
141 .context("position should not be null")?
142 .into_int64();
143 let chunk = append_row(delete_position_buffer, file_path, position);
144 self.pk_index_state_table.delete(index_row);
145 Ok(chunk)
146 }
147
148 #[try_stream(ok = DataChunk, error = StreamExecutorError)]
163 async fn process_chunk(&mut self, chunk: StreamChunk) {
164 let chunk = compact_chunk_inline::<{ output_kind::RETRACT }>(
165 chunk,
166 &self.pk_indices,
167 InconsistencyBehavior::Panic,
168 );
169
170 let mut delete_position_buffer = self
171 .delete_position_buffer
172 .take()
173 .unwrap_or_else(|| new_chunk_builder(self.chunk_size));
174 let pk_indices = self.pk_indices.clone();
175
176 let mut insert_chunk =
184 DataChunkBuilder::new(chunk.data_chunk().data_types(), chunk.capacity() + 1);
185 let mut insert_pks: Vec<PkRow<'_>> = Vec::new();
186
187 for record in chunk.records() {
188 match record {
189 Record::Insert { new_row } => {
190 let overflow = insert_chunk.append_one_row(new_row);
191 debug_assert!(overflow.is_none(), "insert chunk exceeds capacity");
192 insert_pks.push(new_row.project(&pk_indices));
193 }
194 Record::Delete { old_row } => {
195 let pk_row = old_row.project(&pk_indices);
196 if let Some(chunk) = self
197 .delete_existing_row(pk_row, &mut delete_position_buffer)
198 .await?
199 {
200 yield chunk;
201 }
202 }
203 Record::Update { new_row, .. } => {
204 let pk_row = new_row.project(&pk_indices);
206 if let Some(chunk) = self
207 .delete_existing_row(pk_row, &mut delete_position_buffer)
208 .await?
209 {
210 yield chunk;
211 }
212 let overflow = insert_chunk.append_one_row(new_row);
213 debug_assert!(overflow.is_none(), "insert chunk exceeds capacity");
214 insert_pks.push(pk_row);
215 }
216 }
217 }
218
219 if !insert_pks.is_empty() {
220 let write_chunk = insert_chunk.finish();
221 let positions = self.writer.write_chunk(write_chunk).await?;
222
223 for (pk, pos) in insert_pks.into_iter().zip_eq_fast(positions) {
224 let mut index_row_data = Vec::with_capacity(pk_indices.len() + 2);
225 for datum in pk.iter() {
226 index_row_data.push(datum);
227 }
228 index_row_data.push(Some(ScalarRefImpl::Utf8(&pos.path)));
229 index_row_data.push(Some(ScalarRefImpl::Int64(pos.pos)));
230 self.pk_index_state_table.insert(index_row_data.as_slice());
231 }
232 }
233
234 self.delete_position_buffer = Some(delete_position_buffer);
235 self.pk_index_state_table.try_flush().await?;
236 }
237
238 #[try_stream(ok = Message, error = StreamExecutorError)]
239 async fn execute_inner(mut self) {
240 let mut input = self.input.take().unwrap().execute();
241
242 let barrier = expect_first_barrier(&mut input).await?;
244 let first_epoch = barrier.epoch;
245
246 yield Message::Barrier(barrier);
247 self.pk_index_state_table.init_epoch(first_epoch).await?;
248
249 #[for_await]
250 for msg in input {
251 match msg? {
252 Message::Chunk(chunk) =>
253 {
254 #[for_await]
255 for data_chunk in self.process_chunk(chunk) {
256 yield Message::Chunk(data_chunk?.into());
257 }
258 }
259 Message::Barrier(barrier) => {
260 let mut metadata = None;
261 if barrier.is_checkpoint() {
262 if let Some(chunk) = self
263 .delete_position_buffer
264 .take()
265 .and_then(|mut b| b.consume_all())
266 {
267 yield Message::Chunk(chunk.into());
268 }
269 metadata = self.writer.flush().await?;
270 }
271
272 let epoch = barrier.epoch;
273 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(self.ctx.id);
274 let post_commit = self.pk_index_state_table.commit(epoch).await?;
275
276 if let Some(metadata) = metadata
277 && metadata.metadata.is_some()
278 {
279 self.local_barrier_manager
280 .report_iceberg_pk_index_sink_metadata(
281 epoch,
282 self.sink_id,
283 self.ctx.id,
284 PbIcebergPkIndexSinkRole::Writer,
285 Some(metadata),
286 );
287 }
288
289 yield Message::Barrier(barrier);
290
291 post_commit.post_yield_barrier(update_vnode_bitmap).await?;
292 }
293 Message::Watermark(w) => {
294 yield Message::Watermark(w);
295 }
296 }
297 }
298 }
299}
300
301impl<S, W> Execute for WriterExecutor<S, W>
302where
303 S: StateStore,
304 W: IcebergWriter,
305{
306 fn execute(self: Box<Self>) -> BoxedMessageStream {
307 self.execute_inner().boxed()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use std::sync::{Arc, Mutex};
314
315 use iceberg::writer::PositionDeleteInput;
316 use risingwave_common::array::Op;
317 use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema, TableId};
318 use risingwave_common::id::SinkId;
319 use risingwave_common::test_prelude::StreamChunkTestExt;
320 use risingwave_common::types::DataType;
321 use risingwave_common::util::epoch::test_epoch;
322 use risingwave_common::util::sort_util::OrderType;
323 use risingwave_storage::memory::MemoryStateStore;
324
325 use super::*;
326 use crate::common::table::test_utils::gen_pbtable;
327 use crate::executor::test_utils::{MessageSender, MockSource, StreamExecutorTestExt};
328 use crate::task::LocalBarrierManager;
329
330 const CHUNK_SIZE: usize = 1024;
331 const TEST_FILE_PATH: &str = "file1.parquet";
332
333 struct IcebergWriterMock {
334 file_path: String,
335 next_offset: i64,
336 written_chunks: Arc<Mutex<Vec<StreamChunk>>>,
337 }
338
339 impl IcebergWriterMock {
340 fn new(file_path: &str) -> Self {
341 Self {
342 file_path: file_path.to_owned(),
343 next_offset: 0,
344 written_chunks: Arc::new(Mutex::new(Vec::new())),
345 }
346 }
347
348 fn written_chunks(&self) -> Arc<Mutex<Vec<StreamChunk>>> {
349 self.written_chunks.clone()
350 }
351 }
352
353 #[async_trait::async_trait]
354 impl IcebergWriter for IcebergWriterMock {
355 async fn write_chunk(
356 &mut self,
357 chunk: DataChunk,
358 ) -> StreamExecutorResult<Vec<PositionDeleteInput>> {
359 let row_count = chunk.cardinality();
360 let mut positions = Vec::with_capacity(row_count);
361 for _ in 0..row_count {
362 positions.push(PositionDeleteInput::new(
363 Arc::<str>::from(self.file_path.as_str()),
364 self.next_offset,
365 ));
366 self.next_offset += 1;
367 }
368 self.written_chunks.lock().unwrap().push(chunk.into());
369 Ok(positions)
370 }
371
372 async fn flush(&mut self) -> StreamExecutorResult<Option<SinkMetadata>> {
373 Ok(None)
374 }
375 }
376
377 async fn create_pk_index_state_table(
378 store: MemoryStateStore,
379 table_id: TableId,
380 ) -> StateTable<MemoryStateStore> {
381 let column_descs = vec![
382 ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
383 ColumnDesc::unnamed(ColumnId::new(1), DataType::Varchar),
384 ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
385 ];
386 let order_types = vec![OrderType::ascending()];
387 let pk_indices = vec![0];
388
389 StateTable::from_table_catalog(
390 &gen_pbtable(table_id, column_descs, order_types, pk_indices, 0),
391 store,
392 None,
393 )
394 .await
395 }
396
397 fn input_schema() -> Schema {
398 Schema::new(vec![
399 Field::unnamed(DataType::Int64),
400 Field::unnamed(DataType::Int64),
401 ])
402 }
403
404 fn decode_chunk(chunk: StreamChunk) -> Vec<(String, i64)> {
405 chunk
406 .rows()
407 .map(|(op, row)| {
408 assert_eq!(op, Op::Insert);
409 let file_path = row.datum_at(0).unwrap().into_utf8().to_owned();
410 let position = row.datum_at(1).unwrap().into_int64();
411 (file_path, position)
412 })
413 .collect()
414 }
415
416 fn test_file_position(position: i64) -> (String, i64) {
417 (TEST_FILE_PATH.to_owned(), position)
418 }
419
420 struct WriterTestHarness {
421 tx: MessageSender,
422 executor: BoxedMessageStream,
423 written_chunks: Arc<Mutex<Vec<StreamChunk>>>,
424 }
425
426 impl WriterTestHarness {
427 async fn new() -> Self {
428 Self::with_schema(input_schema()).await
429 }
430
431 async fn with_schema(input_schema: Schema) -> Self {
434 let store = MemoryStateStore::new();
435 let state_table = create_pk_index_state_table(store, TableId::new(1)).await;
436 let writer = IcebergWriterMock::new(TEST_FILE_PATH);
437 let written_chunks = writer.written_chunks();
438
439 let (tx, source) = MockSource::channel();
440 let source = source.into_executor(input_schema, vec![0]);
441 let lbm = LocalBarrierManager::for_test();
442 let executor = WriterExecutor::new(
443 ActorContext::for_test(123),
444 source,
445 vec![0],
446 state_table,
447 writer,
448 CHUNK_SIZE,
449 SinkId::new(0),
450 lbm,
451 )
452 .boxed()
453 .execute();
454
455 Self {
456 tx,
457 executor,
458 written_chunks,
459 }
460 }
461
462 async fn init(&mut self) {
463 self.tx.push_barrier(test_epoch(1), false);
464 self.executor.expect_barrier().await;
465 }
466
467 fn push_chunk(&mut self, chunk: StreamChunk) {
468 self.tx.push_chunk(chunk);
469 }
470
471 fn push_pretty_chunk(&mut self, pretty: &str) {
472 self.push_chunk(StreamChunk::from_pretty(pretty));
473 }
474
475 fn push_barrier(&mut self, epoch: u64) {
476 self.tx.push_barrier(test_epoch(epoch), false);
477 }
478
479 async fn expect_barrier(&mut self) {
480 self.executor.expect_barrier().await;
481 }
482
483 async fn expect_position_chunk(&mut self, expected: Vec<(String, i64)>) {
484 assert_eq!(decode_chunk(self.executor.expect_chunk().await), expected);
485 }
486
487 fn written_chunks(&self) -> Vec<StreamChunk> {
488 self.written_chunks.lock().unwrap().clone()
489 }
490 }
491
492 #[tokio::test]
493 async fn test_writer_executor_insert_only() {
494 let mut harness = WriterTestHarness::new().await;
495 harness.init().await;
496
497 harness.push_pretty_chunk(
498 " I I
499 + 1 10
500 + 2 20
501 + 3 30",
502 );
503 harness.push_barrier(2);
504
505 harness.expect_barrier().await;
506 assert_eq!(
507 harness.written_chunks(),
508 vec![StreamChunk::from_pretty(
509 " I I
510 + 1 10
511 + 2 20
512 + 3 30",
513 )]
514 );
515 }
516
517 #[tokio::test]
518 async fn test_writer_executor_insert_then_delete() {
519 let mut harness = WriterTestHarness::new().await;
520 harness.init().await;
521
522 harness.push_pretty_chunk(
523 " I I
524 + 1 10
525 + 2 20
526 + 3 30",
527 );
528 harness.push_barrier(2);
529 harness.expect_barrier().await;
530
531 harness.push_pretty_chunk(
532 " I I
533 - 2 20",
534 );
535 harness.push_barrier(3);
536
537 harness
538 .expect_position_chunk(vec![test_file_position(1)])
539 .await;
540 harness.expect_barrier().await;
541 }
542
543 #[tokio::test]
544 async fn test_writer_executor_update_rewrites_position() {
545 let mut harness = WriterTestHarness::new().await;
546 harness.init().await;
547
548 harness.push_pretty_chunk(
549 " I I
550 + 1 10",
551 );
552 harness.push_barrier(2);
553 harness.expect_barrier().await;
554
555 harness.push_pretty_chunk(
556 " I I
557 U- 1 10
558 U+ 1 99",
559 );
560 harness.push_barrier(3);
561
562 harness
563 .expect_position_chunk(vec![test_file_position(0)])
564 .await;
565 harness.expect_barrier().await;
566
567 harness.push_pretty_chunk(
568 " I I
569 - 1 99",
570 );
571 harness.push_barrier(4);
572
573 harness
574 .expect_position_chunk(vec![test_file_position(1)])
575 .await;
576 harness.expect_barrier().await;
577
578 assert_eq!(
579 harness.written_chunks(),
580 vec![
581 StreamChunk::from_pretty(
582 " I I
583 + 1 10",
584 ),
585 StreamChunk::from_pretty(
586 " I I
587 + 1 99",
588 ),
589 ]
590 );
591 }
592
593 #[tokio::test]
594 async fn test_writer_executor_delete_then_insert_without_existing_row_is_fresh_insert() {
595 let mut harness = WriterTestHarness::new().await;
596 harness.init().await;
597
598 harness.push_pretty_chunk(
599 " I I
600 - 1 10
601 + 1 99",
602 );
603 harness.push_barrier(2);
604
605 harness.expect_barrier().await;
606 assert_eq!(
607 harness.written_chunks(),
608 vec![StreamChunk::from_pretty(
609 " I I
610 + 1 99",
611 )]
612 );
613 }
614
615 #[tokio::test]
616 async fn test_writer_executor_delete_then_insert_rewrites_existing_row() {
617 let mut harness = WriterTestHarness::new().await;
618 harness.init().await;
619
620 harness.push_pretty_chunk(
621 " I I
622 + 1 10",
623 );
624 harness.push_barrier(2);
625 harness.expect_barrier().await;
626
627 harness.push_pretty_chunk(
628 " I I
629 - 1 10
630 + 1 99",
631 );
632 harness.push_barrier(3);
633
634 harness
635 .expect_position_chunk(vec![test_file_position(0)])
636 .await;
637 harness.expect_barrier().await;
638
639 assert_eq!(
640 harness.written_chunks(),
641 vec![
642 StreamChunk::from_pretty(
643 " I I
644 + 1 10",
645 ),
646 StreamChunk::from_pretty(
647 " I I
648 + 1 99",
649 ),
650 ]
651 );
652 }
653
654 #[tokio::test]
658 #[should_panic(expected = "inconsistency happened")]
659 async fn test_writer_executor_duplicate_delete_in_same_chunk_panics() {
660 let mut harness = WriterTestHarness::new().await;
661 harness.init().await;
662
663 harness.push_pretty_chunk(
664 " I I
665 + 1 10",
666 );
667 harness.push_barrier(2);
668 harness.expect_barrier().await;
669
670 harness.push_pretty_chunk(
671 " I I
672 - 1 10
673 - 1 10",
674 );
675 harness.push_barrier(3);
676
677 harness.expect_barrier().await;
679 }
680
681 #[tokio::test]
682 async fn test_writer_executor_insert_then_delete_in_different_chunks_same_checkpoint() {
683 let mut harness = WriterTestHarness::new().await;
684 harness.init().await;
685
686 harness.push_pretty_chunk(
687 " I I
688 + 1 10",
689 );
690 harness.push_pretty_chunk(
691 " I I
692 - 1 10",
693 );
694 harness.push_barrier(2);
695
696 harness
697 .expect_position_chunk(vec![test_file_position(0)])
698 .await;
699 harness.expect_barrier().await;
700 assert_eq!(
701 harness.written_chunks(),
702 vec![StreamChunk::from_pretty(
703 " I I
704 + 1 10",
705 )]
706 );
707 }
708
709 #[tokio::test]
712 #[should_panic(expected = "inconsistency happened")]
713 async fn test_writer_executor_duplicate_insert_in_same_chunk_panics() {
714 let mut harness = WriterTestHarness::new().await;
715 harness.init().await;
716
717 harness.push_pretty_chunk(
718 " I I
719 + 1 10
720 + 1 99",
721 );
722 harness.push_barrier(2);
723
724 harness.expect_barrier().await;
726 }
727
728 #[tokio::test]
729 async fn test_writer_executor_insert_then_delete_in_same_chunk_is_cancelled() {
730 let mut harness = WriterTestHarness::new().await;
731 harness.init().await;
732
733 harness.push_pretty_chunk(
734 " I I
735 + 1 10
736 - 1 10",
737 );
738 harness.push_barrier(2);
739
740 harness.expect_barrier().await;
741 assert!(harness.written_chunks().is_empty());
742 }
743}