Skip to main content

risingwave_frontend/stream_fragmenter/
mod.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod 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/// The mutable state when building fragment graph.
56#[derive(Educe)]
57#[educe(Default)]
58pub struct BuildFragmentGraphState {
59    /// fragment graph field, transformed from input streaming plan.
60    fragment_graph: StreamFragmentGraph,
61    /// local fragment id
62    next_local_fragment_id: FragmentId,
63
64    /// Next local table id to be allocated. It equals to total table ids cnt when finish stream
65    /// node traversing.
66    next_table_id: u32,
67
68    /// rewrite will produce new operators, and we need to track next operator id
69    #[educe(Default(expression = u32::MAX - 1))]
70    next_operator_id: u32,
71
72    /// dependent streaming job ids.
73    dependent_table_ids: HashSet<TableId>,
74
75    /// operator id to `LocalFragmentId` mapping used by share operator.
76    share_mapping: HashMap<StreamNodeLocalOperatorId, LocalFragmentId>,
77    /// operator id to `StreamNode` mapping used by share operator.
78    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    /// Create a new stream fragment with given node with generating a fragment id.
88    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    /// Generate an operator id
95    fn gen_operator_id(&mut self) -> StreamNodeLocalOperatorId {
96        self.next_operator_id -= 1;
97        LocalOperatorId::new(self.next_operator_id).into()
98    }
99
100    /// Generate an table id
101    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    /// Generate an table id
108    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    /// Generate a new stream node with `NoOp` body and the given `input`. The properties of the
129    /// stream node will also be copied from the `input` node.
130    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            // Take input's properties.
137            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// The type of streaming job. It is used to determine the parallelism of the job during `build_graph`.
147#[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    reject_variant_in_internal_storage_key(&mut stream_node)?;
186    generate_fragment_graph(&mut state, stream_node)?;
187    if state.has_source_backfill && state.has_snapshot_backfill {
188        return Err(RwError::from(NotSupported(
189            "Snapshot backfill with shared source backfill is not supported".to_owned(),
190            "`SET streaming_use_shared_source = false` to disable shared source backfill, or \
191                    `SET streaming_use_snapshot_backfill = false` to disable snapshot backfill"
192                .to_owned(),
193        )));
194    }
195    if state.has_cross_db_snapshot_backfill
196        && let Some(ref backfill_order) = backfill_order
197        && !backfill_order.order.is_empty()
198    {
199        return Err(RwError::from(NotSupported(
200            "Backfill order control with cross-db snapshot backfill is not supported".to_owned(),
201            "Please remove backfill order specification from your query".to_owned(),
202        )));
203    }
204
205    let (
206        normal_parallelism,
207        backfill_parallelism,
208        adaptive_parallelism_strategy,
209        backfill_adaptive_parallelism_strategy,
210        max_parallelism,
211    ) = {
212        let config = ctx.session_ctx().config();
213        let streaming_parallelism = config.streaming_parallelism();
214        let job_parallelism = job_type.map(|t| t.to_parallelism(config.deref()));
215        let normal_parallelism =
216            derive_parallelism(job_type, job_parallelism, streaming_parallelism);
217        let backfill_parallelism = if state.has_any_backfill {
218            derive_backfill_parallelism(config.streaming_parallelism_for_backfill())
219        } else {
220            ResolvedParallelism {
221                parallelism: None,
222                adaptive_strategy: None,
223            }
224        };
225        (
226            normal_parallelism.parallelism,
227            backfill_parallelism.parallelism,
228            normal_parallelism
229                .adaptive_strategy
230                .as_ref()
231                .map(AdaptiveParallelismStrategy::to_string)
232                .unwrap_or_default(),
233            backfill_parallelism
234                .adaptive_strategy
235                .as_ref()
236                .map(AdaptiveParallelismStrategy::to_string)
237                .unwrap_or_default(),
238            config.streaming_max_parallelism() as _,
239        )
240    };
241
242    let config_override = ctx
243        .session_ctx()
244        .config()
245        .to_initial_streaming_config_override()
246        .context("invalid initial streaming config override")?;
247    let fragments = state
248        .fragment_graph
249        .fragments
250        .into_iter()
251        .map(|(k, v)| (k, v.to_protobuf()))
252        .collect();
253    let edges = state.fragment_graph.edges.into_values().collect();
254
255    Ok(StreamFragmentGraphProto {
256        fragments,
257        edges,
258        dependent_table_ids: state.dependent_table_ids.into_iter().collect(),
259        table_ids_cnt: state.next_table_id,
260        ctx: Some(StreamContext {
261            timezone: ctx.get_session_timezone(),
262            config_override,
263        }),
264        parallelism: normal_parallelism,
265        backfill_parallelism,
266        adaptive_parallelism_strategy,
267        backfill_adaptive_parallelism_strategy,
268        max_parallelism,
269        backfill_order,
270    })
271}
272
273/// Rejects `VARIANT` (including nested) in the storage pk of any internal state table. Internal
274/// tables only materialize when the plan is lowered to protobuf, so this is the single point that
275/// backstops operators whose state keys no logical checker visits.
276fn reject_variant_in_internal_storage_key(stream_node: &mut StreamNode) -> Result<()> {
277    let mut err = None;
278    visit_stream_node_internal_tables(stream_node, |table, table_name| {
279        if err.is_some() {
280            return;
281        }
282        for order in &table.pk {
283            let column = &table.columns[order.column_index as usize];
284            let column_desc = column.column_desc.as_ref().unwrap();
285            let data_type: DataType = column_desc.column_type.as_ref().unwrap().into();
286            if data_type.contains_variant() {
287                err = Some(variant_key_error(format!(
288                    "VARIANT column \"{}\" is part of the storage primary key of the internal \
289                    state table of `{}`",
290                    column_desc.name, table_name,
291                )));
292                return;
293            }
294        }
295    });
296    match err {
297        Some(err) => Err(err),
298        None => Ok(()),
299    }
300}
301
302#[cfg(any())]
303fn is_stateful_executor(stream_node: &StreamNode) -> bool {
304    matches!(
305        stream_node.get_node_body().unwrap(),
306        NodeBody::HashAgg(_)
307            | NodeBody::HashJoin(_)
308            | NodeBody::DeltaIndexJoin(_)
309            | NodeBody::StreamScan(_)
310            | NodeBody::StreamCdcScan(_)
311            | NodeBody::DynamicFilter(_)
312    )
313}
314
315/// Do some dirty rewrites before building the fragments.
316/// Currently, it will split the fragment with multiple stateful operators (those have high I/O
317/// throughput) into multiple fragments, which may help improve the I/O concurrency.
318/// Known as "no-shuffle exchange" or "1v1 exchange".
319#[cfg(any())]
320fn rewrite_stream_node(
321    state: &mut BuildFragmentGraphState,
322    stream_node: StreamNode,
323    insert_exchange_flag: bool,
324) -> Result<StreamNode> {
325    let f = |child| {
326        // For stateful operators, set `exchange_flag = true`. If it's already true,
327        // force add an exchange.
328        if is_stateful_executor(&child) {
329            if insert_exchange_flag {
330                let child_node = rewrite_stream_node(state, child, true)?;
331
332                let strategy = DispatchStrategy {
333                    r#type: DispatcherType::NoShuffle.into(),
334                    dist_key_indices: vec![], // TODO: use distribution key
335                    output_indices: (0..(child_node.fields.len() as u32)).collect(),
336                };
337                Ok(StreamNode {
338                    stream_key: child_node.stream_key.clone(),
339                    fields: child_node.fields.clone(),
340                    node_body: Some(NodeBody::Exchange(ExchangeNode {
341                        strategy: Some(strategy),
342                    })),
343                    operator_id: state.gen_operator_id(),
344                    append_only: child_node.append_only,
345                    input: vec![child_node],
346                    identity: "Exchange (NoShuffle)".to_string(),
347                })
348            } else {
349                rewrite_stream_node(state, child, true)
350            }
351        } else {
352            match child.get_node_body()? {
353                // For exchanges, reset the flag.
354                NodeBody::Exchange(_) => rewrite_stream_node(state, child, false),
355                // Otherwise, recursively visit the children.
356                _ => rewrite_stream_node(state, child, insert_exchange_flag),
357            }
358        }
359    };
360    Ok(StreamNode {
361        input: stream_node
362            .input
363            .into_iter()
364            .map(f)
365            .collect::<Result<_>>()?,
366        ..stream_node
367    })
368}
369
370/// Generate fragment DAG from input streaming plan by their dependency.
371fn generate_fragment_graph(
372    state: &mut BuildFragmentGraphState,
373    stream_node: StreamNode,
374) -> Result<()> {
375    // TODO: the 1v1 exchange is disabled for now, as it breaks the assumption of independent
376    // scaling of fragments. We may introduce further optimization transparently to the fragmenter.
377    // #4614
378    #[cfg(any())]
379    let stream_node = rewrite_stream_node(state, stream_node, is_stateful_executor(&stream_node))?;
380
381    build_and_add_fragment(state, stream_node)?;
382    Ok(())
383}
384
385/// Use the given `stream_node` to create a fragment and add it to graph.
386fn build_and_add_fragment(
387    state: &mut BuildFragmentGraphState,
388    stream_node: StreamNode,
389) -> Result<Rc<StreamFragment>> {
390    let operator_id = stream_node.operator_id;
391    match state.share_mapping.get(&operator_id) {
392        None => {
393            let mut fragment = state.new_stream_fragment();
394            let node = build_fragment(state, &mut fragment, stream_node)?;
395
396            // It's possible that the stream node is rewritten while building the fragment, for
397            // example, empty fragment to no-op fragment. We get the operator id again instead of
398            // using the original one.
399            let operator_id = node.operator_id;
400
401            assert!(fragment.node.is_none());
402            fragment.node = Some(Box::new(node));
403            let fragment_ref = Rc::new(fragment);
404
405            state.fragment_graph.add_fragment(fragment_ref.clone());
406            state
407                .share_mapping
408                .insert(operator_id, fragment_ref.fragment_id);
409            Ok(fragment_ref)
410        }
411        Some(fragment_id) => Ok(state
412            .fragment_graph
413            .get_fragment(fragment_id)
414            .unwrap()
415            .clone()),
416    }
417}
418
419/// Build new fragment and link dependencies by visiting children recursively, update
420/// `requires_singleton` and `fragment_type` properties for current fragment.
421fn build_fragment(
422    state: &mut BuildFragmentGraphState,
423    current_fragment: &mut StreamFragment,
424    mut stream_node: StreamNode,
425) -> Result<StreamNode> {
426    recursive::tracker!().recurse(|_t| {
427        // Update current fragment based on the node we're visiting.
428        match stream_node.get_node_body()? {
429            NodeBody::BarrierRecv(_) => current_fragment
430                .fragment_type_mask
431                .add(FragmentTypeFlag::BarrierRecv),
432
433            NodeBody::Source(node) => {
434                current_fragment
435                    .fragment_type_mask
436                    .add(FragmentTypeFlag::Source);
437
438                if let Some(source) = node.source_inner.as_ref()
439                    && let Some(source_info) = source.info.as_ref()
440                    && ((source_info.is_shared() && !source_info.is_distributed)
441                        || source.with_properties.requires_singleton())
442                {
443                    current_fragment.requires_singleton = true;
444                }
445            }
446
447            NodeBody::Dml(_) => {
448                current_fragment
449                    .fragment_type_mask
450                    .add(FragmentTypeFlag::Dml);
451            }
452
453            NodeBody::Materialize(_) => {
454                current_fragment
455                    .fragment_type_mask
456                    .add(FragmentTypeFlag::Mview);
457            }
458
459            NodeBody::Sink(_) => current_fragment
460                .fragment_type_mask
461                .add(FragmentTypeFlag::Sink),
462
463            NodeBody::TopN(_) => current_fragment.requires_singleton = true,
464
465            NodeBody::StreamScan(node) => {
466                current_fragment
467                    .fragment_type_mask
468                    .add(FragmentTypeFlag::StreamScan);
469                #[expect(deprecated)]
470                match node.stream_scan_type() {
471                    StreamScanType::SnapshotBackfill => {
472                        current_fragment
473                            .fragment_type_mask
474                            .add(FragmentTypeFlag::SnapshotBackfillStreamScan);
475                        state.has_snapshot_backfill = true;
476                        state.has_any_backfill = true;
477                    }
478                    StreamScanType::Backfill | StreamScanType::ArrangementBackfill => {
479                        state.has_any_backfill = true;
480                    }
481                    StreamScanType::CrossDbSnapshotBackfill => {
482                        current_fragment
483                            .fragment_type_mask
484                            .add(FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan);
485                        state.has_cross_db_snapshot_backfill = true;
486                        state.has_any_backfill = true;
487                    }
488                    StreamScanType::Unspecified
489                    | StreamScanType::Chain
490                    | StreamScanType::Rearrange
491                    | StreamScanType::UpstreamOnly => {}
492                }
493                // memorize table id for later use
494                // The table id could be a upstream CDC source
495                state.dependent_table_ids.insert(node.table_id);
496            }
497
498            NodeBody::StreamCdcScan(node) => {
499                if let Some(o) = node.options
500                    && CdcScanOptions::from_proto(&o).is_parallelized_backfill()
501                {
502                    // Use parallel CDC backfill.
503                    current_fragment
504                        .fragment_type_mask
505                        .add(FragmentTypeFlag::StreamCdcScan);
506                } else {
507                    current_fragment
508                        .fragment_type_mask
509                        .add(FragmentTypeFlag::StreamScan);
510                    // the backfill algorithm is not parallel safe
511                    current_fragment.requires_singleton = true;
512                }
513                state.has_source_backfill = true;
514                state.has_any_backfill = true;
515            }
516
517            NodeBody::CdcFilter(node) => {
518                current_fragment
519                    .fragment_type_mask
520                    .add(FragmentTypeFlag::CdcFilter);
521                // memorize upstream source id for later use
522                state
523                    .dependent_table_ids
524                    .insert(node.upstream_source_id.as_cdc_table_id());
525            }
526            NodeBody::SourceBackfill(node) => {
527                current_fragment
528                    .fragment_type_mask
529                    .add(FragmentTypeFlag::SourceScan);
530                // memorize upstream source id for later use
531                let source_id = node.upstream_source_id;
532                state
533                    .dependent_table_ids
534                    .insert(source_id.as_cdc_table_id());
535                state.has_source_backfill = true;
536                state.has_any_backfill = true;
537            }
538
539            NodeBody::Now(_) => {
540                // TODO: Remove this and insert a `BarrierRecv` instead.
541                current_fragment
542                    .fragment_type_mask
543                    .add(FragmentTypeFlag::Now);
544                current_fragment.requires_singleton = true;
545            }
546
547            NodeBody::Values(_) => {
548                current_fragment
549                    .fragment_type_mask
550                    .add(FragmentTypeFlag::Values);
551                current_fragment.requires_singleton = true;
552            }
553
554            NodeBody::StreamFsFetch(_) => {
555                current_fragment
556                    .fragment_type_mask
557                    .add(FragmentTypeFlag::FsFetch);
558            }
559
560            NodeBody::VectorIndexWrite(_) => {
561                current_fragment
562                    .fragment_type_mask
563                    .add(FragmentTypeFlag::VectorIndexWrite);
564            }
565
566            NodeBody::UpstreamSinkUnion(_) => {
567                current_fragment
568                    .fragment_type_mask
569                    .add(FragmentTypeFlag::UpstreamSinkUnion);
570            }
571
572            NodeBody::LocalityProvider(_) => {
573                current_fragment
574                    .fragment_type_mask
575                    .add(FragmentTypeFlag::LocalityProvider);
576            }
577
578            _ => {}
579        };
580
581        // handle join logic
582        if let NodeBody::DeltaIndexJoin(delta_index_join) = stream_node.node_body.as_mut().unwrap()
583        {
584            if delta_index_join.get_join_type()? == JoinType::Inner
585                && delta_index_join.condition.is_none()
586            {
587                return build_delta_join_without_arrange(state, current_fragment, stream_node);
588            } else {
589                panic!("only inner join without non-equal condition is supported for delta joins");
590            }
591        }
592
593        // Usually we do not expect exchange node to be visited here, which should be handled by the
594        // following logic of "visit children" instead. If it does happen (for example, `Share` will be
595        // transformed to an `Exchange`), it means we have an empty fragment and we need to add a no-op
596        // node to it, so that the meta service can handle it correctly.
597        if let NodeBody::Exchange(_) = stream_node.node_body.as_ref().unwrap() {
598            stream_node = state.gen_no_op_stream_node(stream_node);
599        }
600
601        // Visit plan children.
602        stream_node.input = stream_node
603            .input
604            .into_iter()
605            .map(|mut child_node| {
606                match child_node.get_node_body()? {
607                    // When exchange node is generated when doing rewrites, it could be having
608                    // zero input. In this case, we won't recursively visit its children.
609                    NodeBody::Exchange(_) if child_node.input.is_empty() => Ok(child_node),
610                    // Exchange node indicates a new child fragment.
611                    NodeBody::Exchange(exchange_node) => {
612                        let exchange_node_strategy = exchange_node.get_strategy()?.clone();
613
614                        // Exchange node should have only one input.
615                        let [input]: [_; 1] =
616                            std::mem::take(&mut child_node.input).try_into().unwrap();
617                        let child_fragment = build_and_add_fragment(state, input)?;
618
619                        let result = state.fragment_graph.try_add_edge(
620                            child_fragment.fragment_id,
621                            current_fragment.fragment_id,
622                            StreamFragmentEdge {
623                                dispatch_strategy: exchange_node_strategy.clone(),
624                                // Always use the exchange operator id as the link id.
625                                link_id: child_node.operator_id.as_raw_id(),
626                            },
627                        );
628
629                        // It's possible that there're multiple edges between two fragments, while the
630                        // meta service and the compute node does not expect this. In this case, we
631                        // manually insert a fragment of `NoOp` between the two fragments.
632                        if result.is_err() {
633                            // Assign a new operator id for the `Exchange`, so we can distinguish it
634                            // from duplicate edges and break the sharing.
635                            child_node.operator_id = state.gen_operator_id();
636
637                            // Take the upstream plan node as the reference for properties of `NoOp`.
638                            let ref_fragment_node = child_fragment.node.as_ref().unwrap();
639                            let no_shuffle_strategy = DispatchStrategy {
640                                r#type: DispatcherType::NoShuffle as i32,
641                                dist_key_indices: vec![],
642                                output_mapping: PbDispatchOutputMapping::identical(
643                                    ref_fragment_node.fields.len(),
644                                )
645                                .into(),
646                            };
647
648                            let no_shuffle_exchange_operator_id = state.gen_operator_id();
649
650                            let no_op_fragment = {
651                                let node = state.gen_no_op_stream_node(StreamNode {
652                                    operator_id: no_shuffle_exchange_operator_id,
653                                    identity: "StreamNoShuffleExchange".into(),
654                                    node_body: Some(NodeBody::Exchange(Box::new(ExchangeNode {
655                                        strategy: Some(no_shuffle_strategy.clone()),
656                                    }))),
657                                    input: vec![],
658
659                                    // Take reference's properties.
660                                    stream_key: ref_fragment_node.stream_key.clone(),
661                                    stream_kind: ref_fragment_node.stream_kind,
662                                    fields: ref_fragment_node.fields.clone(),
663                                });
664
665                                let mut fragment = state.new_stream_fragment();
666                                fragment.node = Some(node.into());
667                                Rc::new(fragment)
668                            };
669
670                            state.fragment_graph.add_fragment(no_op_fragment.clone());
671
672                            state.fragment_graph.add_edge(
673                                child_fragment.fragment_id,
674                                no_op_fragment.fragment_id,
675                                StreamFragmentEdge {
676                                    // Use `NoShuffle` exhcnage strategy for upstream edge.
677                                    dispatch_strategy: no_shuffle_strategy,
678                                    link_id: no_shuffle_exchange_operator_id.as_raw_id(),
679                                },
680                            );
681                            state.fragment_graph.add_edge(
682                                no_op_fragment.fragment_id,
683                                current_fragment.fragment_id,
684                                StreamFragmentEdge {
685                                    // Use the original exchange strategy for downstream edge.
686                                    dispatch_strategy: exchange_node_strategy,
687                                    link_id: child_node.operator_id.as_raw_id(),
688                                },
689                            );
690                        }
691
692                        Ok(child_node)
693                    }
694
695                    // For other children, visit recursively.
696                    _ => build_fragment(state, current_fragment, child_node),
697                }
698            })
699            .collect::<Result<_>>()?;
700        Ok(stream_node)
701    })
702}
703
704#[cfg(test)]
705mod tests {
706    use risingwave_common::types::StructType;
707    use risingwave_pb::catalog::PbTable;
708    use risingwave_pb::common::PbColumnOrder;
709    use risingwave_pb::plan_common::{PbColumnCatalog, PbColumnDesc};
710    use risingwave_pb::stream_plan::TopNNode;
711
712    use super::*;
713
714    /// A `TopN` node is the simplest body carrying exactly one internal table.
715    fn top_n_with_pk_column(data_type: DataType) -> StreamNode {
716        let table = PbTable {
717            columns: vec![PbColumnCatalog {
718                column_desc: Some(PbColumnDesc {
719                    name: "v".to_owned(),
720                    column_type: Some(data_type.to_protobuf()),
721                    ..Default::default()
722                }),
723                ..Default::default()
724            }],
725            pk: vec![PbColumnOrder {
726                column_index: 0,
727                order_type: None,
728            }],
729            ..Default::default()
730        };
731        StreamNode {
732            node_body: Some(NodeBody::TopN(Box::new(TopNNode {
733                table: Some(table),
734                ..Default::default()
735            }))),
736            ..Default::default()
737        }
738    }
739
740    #[test]
741    fn rejects_variant_in_internal_state_table_pk() {
742        for data_type in [
743            DataType::Variant,
744            DataType::list(DataType::Variant),
745            DataType::Struct(StructType::new(vec![("v", DataType::Variant)])),
746        ] {
747            let mut node = top_n_with_pk_column(data_type.clone());
748            let err = reject_variant_in_internal_storage_key(&mut node).unwrap_err();
749            assert!(
750                err.to_string().contains("internal state table of `TopN`"),
751                "{data_type:?}: {err}"
752            );
753        }
754
755        let mut node = top_n_with_pk_column(DataType::Jsonb);
756        reject_variant_in_internal_storage_key(&mut node).unwrap();
757    }
758}