1mod graph;
16use anyhow::Context;
17use graph::*;
18use risingwave_common::util::recursive::{self, Recurse as _};
19use risingwave_connector::WithPropertiesExt;
20use risingwave_pb::stream_plan::stream_node::NodeBody;
21mod parallelism;
22mod rewrite;
23
24use std::collections::{HashMap, HashSet};
25use std::ops::Deref;
26use std::rc::Rc;
27
28use educe::Educe;
29use risingwave_common::catalog::{FragmentTypeFlag, TableId};
30use risingwave_common::session_config::SessionConfig;
31use risingwave_common::session_config::parallelism::ConfigParallelism;
32use risingwave_common::system_param::AdaptiveParallelismStrategy;
33use risingwave_common::types::DataType;
34use risingwave_common::util::stream_graph_visitor::visit_stream_node_internal_tables;
35use risingwave_connector::source::cdc::CdcScanOptions;
36use risingwave_pb::id::{LocalOperatorId, StreamNodeLocalOperatorId};
37use risingwave_pb::plan_common::JoinType;
38use risingwave_pb::stream_plan::{
39 BackfillOrder, DispatchStrategy, DispatcherType, ExchangeNode, NoOpNode,
40 PbDispatchOutputMapping, StreamContext, StreamFragmentGraph as StreamFragmentGraphProto,
41 StreamNode, StreamScanType,
42};
43
44use self::rewrite::build_delta_join_without_arrange;
45use crate::catalog::FragmentId;
46use crate::error::ErrorCode::NotSupported;
47use crate::error::{Result, RwError};
48use crate::optimizer::plan_node::generic::GenericPlanRef;
49use crate::optimizer::plan_node::{StreamPlanRef as PlanRef, reorganize_elements_id};
50use crate::optimizer::variant_key::variant_key_error;
51use crate::stream_fragmenter::parallelism::{
52 ResolvedParallelism, derive_backfill_parallelism, derive_parallelism,
53};
54
55#[derive(Educe)]
57#[educe(Default)]
58pub struct BuildFragmentGraphState {
59 fragment_graph: StreamFragmentGraph,
61 next_local_fragment_id: FragmentId,
63
64 next_table_id: u32,
67
68 #[educe(Default(expression = u32::MAX - 1))]
70 next_operator_id: u32,
71
72 dependent_table_ids: HashSet<TableId>,
74
75 share_mapping: HashMap<StreamNodeLocalOperatorId, LocalFragmentId>,
77 share_stream_node_mapping: HashMap<StreamNodeLocalOperatorId, StreamNode>,
79
80 has_source_backfill: bool,
81 has_snapshot_backfill: bool,
82 has_cross_db_snapshot_backfill: bool,
83 has_any_backfill: bool,
84}
85
86impl BuildFragmentGraphState {
87 fn new_stream_fragment(&mut self) -> StreamFragment {
89 let fragment = StreamFragment::new(self.next_local_fragment_id);
90 self.next_local_fragment_id += 1;
91 fragment
92 }
93
94 pub(crate) fn gen_operator_id(&mut self) -> StreamNodeLocalOperatorId {
96 self.next_operator_id -= 1;
97 LocalOperatorId::new(self.next_operator_id).into()
98 }
99
100 pub fn gen_table_id(&mut self) -> u32 {
102 let ret = self.next_table_id;
103 self.next_table_id += 1;
104 ret
105 }
106
107 pub fn gen_table_id_wrapped(&mut self) -> TableId {
109 TableId::new(self.gen_table_id())
110 }
111
112 pub fn add_share_stream_node(
113 &mut self,
114 operator_id: StreamNodeLocalOperatorId,
115 stream_node: StreamNode,
116 ) {
117 self.share_stream_node_mapping
118 .insert(operator_id, stream_node);
119 }
120
121 pub fn get_share_stream_node(
122 &mut self,
123 operator_id: StreamNodeLocalOperatorId,
124 ) -> Option<&StreamNode> {
125 self.share_stream_node_mapping.get(&operator_id)
126 }
127
128 pub fn gen_no_op_stream_node(&mut self, input: StreamNode) -> StreamNode {
131 StreamNode {
132 operator_id: self.gen_operator_id(),
133 identity: "StreamNoOp".into(),
134 node_body: Some(NodeBody::NoOp(NoOpNode {})),
135
136 stream_key: input.stream_key.clone(),
138 stream_kind: input.stream_kind,
139 fields: input.fields.clone(),
140
141 input: vec![input],
142 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum GraphJobType {
149 Table,
150 MaterializedView,
151 Source,
152 Sink,
153 Index,
154}
155
156impl GraphJobType {
157 pub fn to_parallelism(self, config: &SessionConfig) -> ConfigParallelism {
158 match self {
159 GraphJobType::Table => config.streaming_parallelism_for_table(),
160 GraphJobType::MaterializedView => config.streaming_parallelism_for_materialized_view(),
161 GraphJobType::Source => config.streaming_parallelism_for_source(),
162 GraphJobType::Sink => config.streaming_parallelism_for_sink(),
163 GraphJobType::Index => config.streaming_parallelism_for_index(),
164 }
165 }
166}
167
168pub fn build_graph(
169 plan_node: PlanRef,
170 job_type: Option<GraphJobType>,
171) -> Result<StreamFragmentGraphProto> {
172 build_graph_with_strategy(plan_node, job_type, None)
173}
174
175pub fn build_graph_with_strategy(
176 plan_node: PlanRef,
177 job_type: Option<GraphJobType>,
178 backfill_order: Option<BackfillOrder>,
179) -> Result<StreamFragmentGraphProto> {
180 let ctx = plan_node.plan_base().ctx();
181 let plan_node = reorganize_elements_id(plan_node);
182
183 let mut state = BuildFragmentGraphState::default();
184 let mut stream_node = plan_node.to_stream_prost(&mut state)?;
185 wire_iceberg_update_lists(&mut stream_node)?;
186 reject_variant_in_internal_storage_key(&mut stream_node)?;
187 generate_fragment_graph(&mut state, stream_node)?;
188 if state.has_source_backfill && state.has_snapshot_backfill {
189 return Err(RwError::from(NotSupported(
190 "Snapshot backfill with shared source backfill is not supported".to_owned(),
191 "`SET streaming_use_shared_source = false` to disable shared source backfill, or \
192 `SET streaming_use_snapshot_backfill = false` to disable snapshot backfill"
193 .to_owned(),
194 )));
195 }
196 if state.has_cross_db_snapshot_backfill
197 && let Some(ref backfill_order) = backfill_order
198 && !backfill_order.order.is_empty()
199 {
200 return Err(RwError::from(NotSupported(
201 "Backfill order control with cross-db snapshot backfill is not supported".to_owned(),
202 "Please remove backfill order specification from your query".to_owned(),
203 )));
204 }
205
206 let (
207 normal_parallelism,
208 backfill_parallelism,
209 adaptive_parallelism_strategy,
210 backfill_adaptive_parallelism_strategy,
211 max_parallelism,
212 ) = {
213 let config = ctx.session_ctx().config();
214 let streaming_parallelism = config.streaming_parallelism();
215 let job_parallelism = job_type.map(|t| t.to_parallelism(config.deref()));
216 let normal_parallelism =
217 derive_parallelism(job_type, job_parallelism, streaming_parallelism);
218 let backfill_parallelism = if state.has_any_backfill {
219 derive_backfill_parallelism(config.streaming_parallelism_for_backfill())
220 } else {
221 ResolvedParallelism {
222 parallelism: None,
223 adaptive_strategy: None,
224 }
225 };
226 (
227 normal_parallelism.parallelism,
228 backfill_parallelism.parallelism,
229 normal_parallelism
230 .adaptive_strategy
231 .as_ref()
232 .map(AdaptiveParallelismStrategy::to_string)
233 .unwrap_or_default(),
234 backfill_parallelism
235 .adaptive_strategy
236 .as_ref()
237 .map(AdaptiveParallelismStrategy::to_string)
238 .unwrap_or_default(),
239 config.streaming_max_parallelism() as _,
240 )
241 };
242
243 let config_override = ctx
244 .session_ctx()
245 .config()
246 .to_initial_streaming_config_override()
247 .context("invalid initial streaming config override")?;
248 let fragments = state
249 .fragment_graph
250 .fragments
251 .into_iter()
252 .map(|(k, v)| (k, v.to_protobuf()))
253 .collect();
254 let edges = state.fragment_graph.edges.into_values().collect();
255
256 Ok(StreamFragmentGraphProto {
257 fragments,
258 edges,
259 dependent_table_ids: state.dependent_table_ids.into_iter().collect(),
260 table_ids_cnt: state.next_table_id,
261 ctx: Some(StreamContext {
262 timezone: ctx.get_session_timezone(),
263 config_override,
264 }),
265 parallelism: normal_parallelism,
266 backfill_parallelism,
267 adaptive_parallelism_strategy,
268 backfill_adaptive_parallelism_strategy,
269 max_parallelism,
270 backfill_order,
271 })
272}
273
274fn wire_iceberg_update_lists(node: &mut StreamNode) -> Result<()> {
277 for input in &mut node.input {
278 wire_iceberg_update_lists(input)?;
279 }
280 let Some(NodeBody::StreamFsFetch(fetch)) = &node.node_body else {
281 return Ok(());
282 };
283 let Some(fetch) = &fetch.node_inner else {
284 return Ok(());
285 };
286 if !fetch.with_properties.is_iceberg_update_source() {
287 return Ok(());
288 }
289 let table = fetch
290 .state_table
291 .as_ref()
292 .context("missing Iceberg Fetch state table")?;
293 let desc = risingwave_pb::plan_common::StorageTableDesc {
295 table_id: table.id,
296 ..Default::default()
297 };
298 let mut matched = 0;
299 for input in &mut node.input {
300 risingwave_common::util::stream_graph_visitor::visit_stream_node_mut(input, |body| {
301 if let NodeBody::Source(source) = body
302 && let Some(source) = &mut source.source_inner
303 && source.source_id == fetch.source_id
304 && source.with_properties.is_iceberg_update_source()
305 && source
306 .downstream_columns
307 .as_ref()
308 .is_some_and(|columns| columns.columns == fetch.columns)
309 && source.iceberg_fetch_state_table.is_none()
310 {
311 source.iceberg_fetch_state_table = Some(desc.clone());
312 matched += 1;
313 }
314 });
315 }
316 if matched != 1 {
317 return Err(NotSupported(
318 "Iceberg update Fetch requires exactly one dedicated matching List".to_owned(),
319 "Shared List state is not supported".to_owned(),
320 )
321 .into());
322 }
323 Ok(())
324}
325
326fn reject_variant_in_internal_storage_key(stream_node: &mut StreamNode) -> Result<()> {
330 let mut err = None;
331 visit_stream_node_internal_tables(stream_node, |table, table_name| {
332 if err.is_some() {
333 return;
334 }
335 for order in &table.pk {
336 let column = &table.columns[order.column_index as usize];
337 let column_desc = column.column_desc.as_ref().unwrap();
338 let data_type: DataType = column_desc.column_type.as_ref().unwrap().into();
339 if data_type.contains_variant() {
340 err = Some(variant_key_error(format!(
341 "VARIANT column \"{}\" is part of the storage primary key of the internal \
342 state table of `{}`",
343 column_desc.name, table_name,
344 )));
345 return;
346 }
347 }
348 });
349 match err {
350 Some(err) => Err(err),
351 None => Ok(()),
352 }
353}
354
355#[cfg(any())]
356fn is_stateful_executor(stream_node: &StreamNode) -> bool {
357 matches!(
358 stream_node.get_node_body().unwrap(),
359 NodeBody::HashAgg(_)
360 | NodeBody::HashJoin(_)
361 | NodeBody::DeltaIndexJoin(_)
362 | NodeBody::StreamScan(_)
363 | NodeBody::StreamCdcScan(_)
364 | NodeBody::DynamicFilter(_)
365 )
366}
367
368#[cfg(any())]
373fn rewrite_stream_node(
374 state: &mut BuildFragmentGraphState,
375 stream_node: StreamNode,
376 insert_exchange_flag: bool,
377) -> Result<StreamNode> {
378 let f = |child| {
379 if is_stateful_executor(&child) {
382 if insert_exchange_flag {
383 let child_node = rewrite_stream_node(state, child, true)?;
384
385 let strategy = DispatchStrategy {
386 r#type: DispatcherType::NoShuffle.into(),
387 dist_key_indices: vec![], output_indices: (0..(child_node.fields.len() as u32)).collect(),
389 };
390 Ok(StreamNode {
391 stream_key: child_node.stream_key.clone(),
392 fields: child_node.fields.clone(),
393 node_body: Some(NodeBody::Exchange(ExchangeNode {
394 strategy: Some(strategy),
395 })),
396 operator_id: state.gen_operator_id(),
397 append_only: child_node.append_only,
398 input: vec![child_node],
399 identity: "Exchange (NoShuffle)".to_string(),
400 })
401 } else {
402 rewrite_stream_node(state, child, true)
403 }
404 } else {
405 match child.get_node_body()? {
406 NodeBody::Exchange(_) => rewrite_stream_node(state, child, false),
408 _ => rewrite_stream_node(state, child, insert_exchange_flag),
410 }
411 }
412 };
413 Ok(StreamNode {
414 input: stream_node
415 .input
416 .into_iter()
417 .map(f)
418 .collect::<Result<_>>()?,
419 ..stream_node
420 })
421}
422
423fn generate_fragment_graph(
425 state: &mut BuildFragmentGraphState,
426 stream_node: StreamNode,
427) -> Result<()> {
428 #[cfg(any())]
432 let stream_node = rewrite_stream_node(state, stream_node, is_stateful_executor(&stream_node))?;
433
434 build_and_add_fragment(state, stream_node)?;
435 Ok(())
436}
437
438fn build_and_add_fragment(
440 state: &mut BuildFragmentGraphState,
441 stream_node: StreamNode,
442) -> Result<Rc<StreamFragment>> {
443 let operator_id = stream_node.operator_id;
444 match state.share_mapping.get(&operator_id) {
445 None => {
446 let mut fragment = state.new_stream_fragment();
447 let node = build_fragment(state, &mut fragment, stream_node)?;
448
449 let operator_id = node.operator_id;
453
454 assert!(fragment.node.is_none());
455 fragment.node = Some(Box::new(node));
456 let fragment_ref = Rc::new(fragment);
457
458 state.fragment_graph.add_fragment(fragment_ref.clone());
459 state
460 .share_mapping
461 .insert(operator_id, fragment_ref.fragment_id);
462 Ok(fragment_ref)
463 }
464 Some(fragment_id) => Ok(state
465 .fragment_graph
466 .get_fragment(fragment_id)
467 .unwrap()
468 .clone()),
469 }
470}
471
472fn build_fragment(
475 state: &mut BuildFragmentGraphState,
476 current_fragment: &mut StreamFragment,
477 mut stream_node: StreamNode,
478) -> Result<StreamNode> {
479 recursive::tracker!().recurse(|_t| {
480 match stream_node.get_node_body()? {
482 NodeBody::BarrierRecv(_) => current_fragment
483 .fragment_type_mask
484 .add(FragmentTypeFlag::BarrierRecv),
485
486 NodeBody::Source(node) => {
487 current_fragment
488 .fragment_type_mask
489 .add(FragmentTypeFlag::Source);
490
491 if let Some(source) = node.source_inner.as_ref()
492 && let Some(source_info) = source.info.as_ref()
493 && ((source_info.is_shared() && !source_info.is_distributed)
494 || source.with_properties.requires_singleton())
495 {
496 current_fragment.requires_singleton = true;
497 }
498 }
499
500 NodeBody::Dml(_) => {
501 current_fragment
502 .fragment_type_mask
503 .add(FragmentTypeFlag::Dml);
504 }
505
506 NodeBody::Materialize(_) => {
507 current_fragment
508 .fragment_type_mask
509 .add(FragmentTypeFlag::Mview);
510 }
511
512 NodeBody::Sink(_) => current_fragment
513 .fragment_type_mask
514 .add(FragmentTypeFlag::Sink),
515
516 NodeBody::TopN(_) => current_fragment.requires_singleton = true,
517
518 NodeBody::StreamScan(node) => {
519 current_fragment
520 .fragment_type_mask
521 .add(FragmentTypeFlag::StreamScan);
522 #[expect(deprecated)]
523 match node.stream_scan_type() {
524 StreamScanType::SnapshotBackfill => {
525 current_fragment
526 .fragment_type_mask
527 .add(FragmentTypeFlag::SnapshotBackfillStreamScan);
528 state.has_snapshot_backfill = true;
529 state.has_any_backfill = true;
530 }
531 StreamScanType::Backfill | StreamScanType::ArrangementBackfill => {
532 state.has_any_backfill = true;
533 }
534 StreamScanType::CrossDbSnapshotBackfill => {
535 current_fragment
536 .fragment_type_mask
537 .add(FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan);
538 state.has_cross_db_snapshot_backfill = true;
539 state.has_any_backfill = true;
540 }
541 StreamScanType::Unspecified
542 | StreamScanType::Chain
543 | StreamScanType::Rearrange
544 | StreamScanType::UpstreamOnly => {}
545 }
546 state.dependent_table_ids.insert(node.table_id);
549 }
550
551 NodeBody::StreamCdcScan(node) => {
552 current_fragment
553 .fragment_type_mask
554 .add(FragmentTypeFlag::StreamCdcScan);
555 let is_parallelized_backfill = node
556 .options
557 .as_ref()
558 .is_some_and(|o| CdcScanOptions::from_proto(o).is_parallelized_backfill());
559 if !is_parallelized_backfill {
560 current_fragment
561 .fragment_type_mask
562 .add(FragmentTypeFlag::StreamScan);
563 current_fragment.requires_singleton = true;
565 }
566 state.has_source_backfill = true;
567 state.has_any_backfill = true;
568 }
569
570 NodeBody::CdcFilter(node) => {
571 current_fragment
572 .fragment_type_mask
573 .add(FragmentTypeFlag::CdcFilter);
574 state
576 .dependent_table_ids
577 .insert(node.upstream_source_id.as_cdc_table_id());
578 }
579 NodeBody::SourceBackfill(node) => {
580 current_fragment
581 .fragment_type_mask
582 .add(FragmentTypeFlag::SourceScan);
583 let source_id = node.upstream_source_id;
585 state
586 .dependent_table_ids
587 .insert(source_id.as_cdc_table_id());
588 state.has_source_backfill = true;
589 state.has_any_backfill = true;
590 }
591
592 NodeBody::Now(_) => {
593 current_fragment
595 .fragment_type_mask
596 .add(FragmentTypeFlag::Now);
597 current_fragment.requires_singleton = true;
598 }
599
600 NodeBody::Values(_) => {
601 current_fragment
602 .fragment_type_mask
603 .add(FragmentTypeFlag::Values);
604 current_fragment.requires_singleton = true;
605 }
606
607 NodeBody::CompactionResolver(_) => {
608 current_fragment.requires_singleton = true;
609 }
610
611 NodeBody::StreamFsFetch(_) => {
612 current_fragment
613 .fragment_type_mask
614 .add(FragmentTypeFlag::FsFetch);
615 }
616
617 NodeBody::VectorIndexWrite(_) => {
618 current_fragment
619 .fragment_type_mask
620 .add(FragmentTypeFlag::VectorIndexWrite);
621 }
622
623 NodeBody::IcebergWithPkIndexWriter(_) => {
624 current_fragment
625 .fragment_type_mask
626 .add(FragmentTypeFlag::IcebergWithPkIndexWriter);
627 }
628
629 NodeBody::UpstreamSinkUnion(_) => {
630 current_fragment
631 .fragment_type_mask
632 .add(FragmentTypeFlag::UpstreamSinkUnion);
633 }
634
635 NodeBody::LocalityProvider(_) => {
636 current_fragment
637 .fragment_type_mask
638 .add(FragmentTypeFlag::LocalityProvider);
639 }
640
641 _ => {}
642 };
643
644 if let NodeBody::DeltaIndexJoin(delta_index_join) = stream_node.node_body.as_mut().unwrap()
646 {
647 if delta_index_join.get_join_type()? == JoinType::Inner
648 && delta_index_join.condition.is_none()
649 {
650 return build_delta_join_without_arrange(state, current_fragment, stream_node);
651 } else {
652 panic!("only inner join without non-equal condition is supported for delta joins");
653 }
654 }
655
656 if let NodeBody::Exchange(_) = stream_node.node_body.as_ref().unwrap() {
661 stream_node = state.gen_no_op_stream_node(stream_node);
662 }
663
664 stream_node.input = stream_node
666 .input
667 .into_iter()
668 .map(|mut child_node| {
669 match child_node.get_node_body()? {
670 NodeBody::Exchange(_) if child_node.input.is_empty() => Ok(child_node),
673 NodeBody::Exchange(exchange_node) => {
675 let exchange_node_strategy = exchange_node.get_strategy()?.clone();
676
677 let [input]: [_; 1] =
679 std::mem::take(&mut child_node.input).try_into().unwrap();
680 let child_fragment = build_and_add_fragment(state, input)?;
681
682 let result = state.fragment_graph.try_add_edge(
683 child_fragment.fragment_id,
684 current_fragment.fragment_id,
685 StreamFragmentEdge {
686 dispatch_strategy: exchange_node_strategy.clone(),
687 link_id: child_node.operator_id.as_raw_id(),
689 },
690 );
691
692 if result.is_err() {
696 child_node.operator_id = state.gen_operator_id();
699
700 let ref_fragment_node = child_fragment.node.as_ref().unwrap();
702 let no_shuffle_strategy = DispatchStrategy {
703 r#type: DispatcherType::NoShuffle as i32,
704 dist_key_indices: vec![],
705 output_mapping: PbDispatchOutputMapping::identical(
706 ref_fragment_node.fields.len(),
707 )
708 .into(),
709 };
710
711 let no_shuffle_exchange_operator_id = state.gen_operator_id();
712
713 let no_op_fragment = {
714 let node = state.gen_no_op_stream_node(StreamNode {
715 operator_id: no_shuffle_exchange_operator_id,
716 identity: "StreamNoShuffleExchange".into(),
717 node_body: Some(NodeBody::Exchange(Box::new(ExchangeNode {
718 strategy: Some(no_shuffle_strategy.clone()),
719 }))),
720 input: vec![],
721
722 stream_key: ref_fragment_node.stream_key.clone(),
724 stream_kind: ref_fragment_node.stream_kind,
725 fields: ref_fragment_node.fields.clone(),
726 });
727
728 let mut fragment = state.new_stream_fragment();
729 fragment.node = Some(node.into());
730 Rc::new(fragment)
731 };
732
733 state.fragment_graph.add_fragment(no_op_fragment.clone());
734
735 state.fragment_graph.add_edge(
736 child_fragment.fragment_id,
737 no_op_fragment.fragment_id,
738 StreamFragmentEdge {
739 dispatch_strategy: no_shuffle_strategy,
741 link_id: no_shuffle_exchange_operator_id.as_raw_id(),
742 },
743 );
744 state.fragment_graph.add_edge(
745 no_op_fragment.fragment_id,
746 current_fragment.fragment_id,
747 StreamFragmentEdge {
748 dispatch_strategy: exchange_node_strategy,
750 link_id: child_node.operator_id.as_raw_id(),
751 },
752 );
753 }
754
755 Ok(child_node)
756 }
757
758 _ => build_fragment(state, current_fragment, child_node),
760 }
761 })
762 .collect::<Result<_>>()?;
763 Ok(stream_node)
764 })
765}
766
767#[cfg(test)]
768mod tests {
769 use risingwave_common::types::StructType;
770 use risingwave_common::util::iter_util::ZipEqFast;
771 use risingwave_pb::catalog::PbTable;
772 use risingwave_pb::common::PbColumnOrder;
773 use risingwave_pb::plan_common::{PbColumnCatalog, PbColumnDesc};
774 use risingwave_pb::stream_plan::TopNNode;
775
776 use super::*;
777
778 #[test]
779 fn iceberg_update_lists_use_per_fetch_tables_not_source_ids() -> Result<()> {
780 use risingwave_pb::stream_plan::{
781 Columns, SourceNode, StreamFsFetch, StreamFsFetchNode, StreamSource,
782 };
783 let props = std::collections::BTreeMap::from([
784 ("connector".to_owned(), "iceberg".to_owned()),
785 ("streaming_updates".to_owned(), "true".to_owned()),
786 ]);
787 let pair = |table_id: u32| StreamNode {
788 node_body: Some(NodeBody::StreamFsFetch(Box::new(StreamFsFetchNode {
789 node_inner: Some(StreamFsFetch {
790 source_id: 9.into(),
791 with_properties: props.clone(),
792 state_table: Some(PbTable {
793 id: table_id.into(),
794 columns: vec![PbColumnCatalog {
795 column_desc: Some(PbColumnDesc {
796 name: "task".to_owned(),
797 column_type: Some(DataType::Varchar.to_protobuf()),
798 ..Default::default()
799 }),
800 ..Default::default()
801 }],
802 pk: vec![PbColumnOrder {
803 column_index: 0,
804 order_type: None,
805 }],
806 distribution_key: vec![0],
807 value_indices: vec![0],
808 ..Default::default()
809 }),
810 ..Default::default()
811 }),
812 }))),
813 input: vec![StreamNode {
814 node_body: Some(NodeBody::Source(Box::new(SourceNode {
815 source_inner: Some(StreamSource {
816 source_id: 9.into(),
817 with_properties: props.clone(),
818 downstream_columns: Some(Columns { columns: vec![] }),
819 ..Default::default()
820 }),
821 }))),
822 ..Default::default()
823 }],
824 ..Default::default()
825 };
826 let mut root = StreamNode {
827 input: vec![pair(10), pair(11)],
828 ..Default::default()
829 };
830 wire_iceberg_update_lists(&mut root)?;
831 for (node, id) in root.input.iter().zip_eq_fast([10u32, 11]) {
832 let Some(NodeBody::Source(source)) = &node.input[0].node_body else {
833 panic!("missing List")
834 };
835 assert_eq!(
837 source
838 .source_inner
839 .as_ref()
840 .unwrap()
841 .iceberg_fetch_state_table,
842 Some(risingwave_pb::plan_common::StorageTableDesc {
843 table_id: id.into(),
844 ..Default::default()
845 })
846 );
847 }
848 let mut no_list = pair(12);
849 no_list.input.clear();
850 assert!(wire_iceberg_update_lists(&mut no_list).is_err());
851 let mut shared_list = pair(13);
852 shared_list.input.push(shared_list.input[0].clone());
853 assert!(wire_iceberg_update_lists(&mut shared_list).is_err());
854 Ok(())
855 }
856
857 fn top_n_with_pk_column(data_type: DataType) -> StreamNode {
859 let table = PbTable {
860 columns: vec![PbColumnCatalog {
861 column_desc: Some(PbColumnDesc {
862 name: "v".to_owned(),
863 column_type: Some(data_type.to_protobuf()),
864 ..Default::default()
865 }),
866 ..Default::default()
867 }],
868 pk: vec![PbColumnOrder {
869 column_index: 0,
870 order_type: None,
871 }],
872 ..Default::default()
873 };
874 StreamNode {
875 node_body: Some(NodeBody::TopN(Box::new(TopNNode {
876 table: Some(table),
877 ..Default::default()
878 }))),
879 ..Default::default()
880 }
881 }
882
883 #[test]
884 fn rejects_variant_in_internal_state_table_pk() {
885 for data_type in [
886 DataType::Variant,
887 DataType::list(DataType::Variant),
888 DataType::Struct(StructType::new(vec![("v", DataType::Variant)])),
889 ] {
890 let mut node = top_n_with_pk_column(data_type.clone());
891 let err = reject_variant_in_internal_storage_key(&mut node).unwrap_err();
892 assert!(
893 err.to_string().contains("internal state table of `TopN`"),
894 "{data_type:?}: {err}"
895 );
896 }
897
898 let mut node = top_n_with_pk_column(DataType::Jsonb);
899 reject_variant_in_internal_storage_key(&mut node).unwrap();
900 }
901}