Skip to main content

risingwave_meta/stream/stream_graph/
fragment.rs

1// Copyright 2023 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
15use std::collections::{BTreeMap, HashMap, HashSet};
16use std::num::NonZeroUsize;
17use std::ops::{Deref, DerefMut};
18use std::sync::LazyLock;
19
20use anyhow::{Context, anyhow};
21use enum_as_inner::EnumAsInner;
22use itertools::Itertools;
23use risingwave_common::bail;
24use risingwave_common::catalog::{
25    CDC_SOURCE_COLUMN_NUM, ColumnCatalog, ColumnId, Field, FragmentTypeFlag, FragmentTypeMask,
26    TableId, generate_internal_table_name_with_type,
27};
28use risingwave_common::hash::VnodeCount;
29use risingwave_common::id::JobId;
30use risingwave_common::util::iter_util::ZipEqFast;
31use risingwave_common::util::stream_graph_visitor::{
32    self, visit_stream_node_cont, visit_stream_node_cont_mut,
33};
34use risingwave_connector::sink::catalog::SinkType;
35use risingwave_meta_model::streaming_job::BackfillOrders;
36use risingwave_pb::catalog::{PbSink, PbTable, Table};
37use risingwave_pb::ddl_service::TableJobType;
38use risingwave_pb::expr::{ExprNode as PbExprNode, expr_node};
39use risingwave_pb::id::{RelationId, StreamNodeLocalOperatorId};
40use risingwave_pb::plan_common::{PbColumnCatalog, PbColumnDesc};
41use risingwave_pb::stream_plan::dispatch_output_mapping::TypePair;
42use risingwave_pb::stream_plan::stream_fragment_graph::{
43    Parallelism, StreamFragment, StreamFragmentEdge as StreamFragmentEdgeProto,
44};
45use risingwave_pb::stream_plan::stream_node::{NodeBody, PbNodeBody};
46use risingwave_pb::stream_plan::{
47    BackfillOrder, DispatchOutputMapping, DispatchStrategy, DispatcherType, PbStreamNode,
48    PbStreamScanType, StreamFragmentGraph as StreamFragmentGraphProto, StreamNode, StreamScanNode,
49    StreamScanType,
50};
51
52use crate::barrier::SnapshotBackfillInfo;
53use crate::controller::id::IdGeneratorManager;
54use crate::manager::{MetaSrvEnv, StreamingJob, StreamingJobType};
55use crate::model::{Fragment, FragmentDownstreamRelation, FragmentId};
56use crate::stream::stream_graph::id::{GlobalFragmentId, GlobalFragmentIdGen, GlobalTableIdGen};
57use crate::stream::stream_graph::schedule::Distribution;
58use crate::{MetaError, MetaResult};
59
60/// The fragment in the building phase, including the [`StreamFragment`] from the frontend and
61/// several additional helper fields.
62#[derive(Debug, Clone)]
63pub(super) struct BuildingFragment {
64    /// The fragment structure from the frontend, with the global fragment ID.
65    inner: StreamFragment,
66
67    /// The ID of the job if it contains the streaming job node.
68    job_id: Option<JobId>,
69
70    /// The required column IDs of each upstream table.
71    /// Will be converted to indices when building the edge connected to the upstream.
72    ///
73    /// For shared CDC table on source, its `vec![]`, since the upstream source's output schema is fixed.
74    upstream_job_columns: HashMap<JobId, Vec<PbColumnDesc>>,
75}
76
77impl BuildingFragment {
78    /// Create a new [`BuildingFragment`] from a [`StreamFragment`]. The global fragment ID and
79    /// global table IDs will be correctly filled with the given `id` and `table_id_gen`.
80    fn new(
81        id: GlobalFragmentId,
82        fragment: StreamFragment,
83        job: &StreamingJob,
84        table_id_gen: GlobalTableIdGen,
85    ) -> Self {
86        let mut fragment = StreamFragment {
87            fragment_id: id.as_global_id(),
88            ..fragment
89        };
90
91        // Fill the information of the internal tables in the fragment.
92        Self::fill_internal_tables(&mut fragment, job, table_id_gen);
93
94        let job_id = Self::fill_job(&mut fragment, job).then(|| job.id());
95        let upstream_job_columns =
96            Self::extract_upstream_columns_except_cross_db_backfill(&fragment);
97
98        Self {
99            inner: fragment,
100            job_id,
101            upstream_job_columns,
102        }
103    }
104
105    /// Extract the internal tables from the fragment.
106    fn extract_internal_tables(&self) -> Vec<Table> {
107        let mut fragment = self.inner.clone();
108        let mut tables = Vec::new();
109        stream_graph_visitor::visit_internal_tables(&mut fragment, |table, _| {
110            tables.push(table.clone());
111        });
112        tables
113    }
114
115    /// Fill the information with the internal tables in the fragment.
116    fn fill_internal_tables(
117        fragment: &mut StreamFragment,
118        job: &StreamingJob,
119        table_id_gen: GlobalTableIdGen,
120    ) {
121        let fragment_id = fragment.fragment_id;
122        stream_graph_visitor::visit_internal_tables(fragment, |table, table_type_name| {
123            table.id = table_id_gen
124                .to_global_id(table.id.as_raw_id())
125                .as_global_id();
126            table.schema_id = job.schema_id();
127            table.database_id = job.database_id();
128            table.name = generate_internal_table_name_with_type(
129                &job.name(),
130                fragment_id,
131                table.id,
132                table_type_name,
133            );
134            table.fragment_id = fragment_id;
135            table.owner = job.owner();
136            table.job_id = Some(job.id());
137        });
138    }
139
140    /// Fill the information with the job in the fragment.
141    fn fill_job(fragment: &mut StreamFragment, job: &StreamingJob) -> bool {
142        let job_id = job.id();
143        let fragment_id = fragment.fragment_id;
144        let mut has_job = false;
145
146        stream_graph_visitor::visit_fragment_mut(fragment, |node_body| match node_body {
147            NodeBody::Materialize(materialize_node) => {
148                materialize_node.table_id = job_id.as_mv_table_id();
149
150                // Fill the table field of `MaterializeNode` from the job.
151                let table = materialize_node.table.insert(job.table().unwrap().clone());
152                table.fragment_id = fragment_id; // this will later be synced back to `job.table` with `set_info_from_graph`
153                // In production, do not include full definition in the table in plan node.
154                if cfg!(not(debug_assertions)) {
155                    table.definition = job.name();
156                }
157
158                has_job = true;
159            }
160            NodeBody::Sink(sink_node) => {
161                sink_node.sink_desc.as_mut().unwrap().id = job_id.as_sink_id();
162
163                has_job = true;
164            }
165            NodeBody::IcebergWithPkIndexWriter(writer_node) => {
166                writer_node.sink_desc.as_mut().unwrap().id = job_id.as_sink_id();
167
168                has_job = true;
169            }
170            NodeBody::IcebergWithPkIndexPositionDeleteMerger(merger_node) => {
171                merger_node.sink_desc.as_mut().unwrap().id = job_id.as_sink_id();
172
173                has_job = true;
174            }
175            NodeBody::Dml(dml_node) => {
176                dml_node.table_id = job_id.as_mv_table_id();
177                dml_node.table_version_id = job.table_version_id().unwrap();
178            }
179            NodeBody::StreamFsFetch(fs_fetch_node) => {
180                if let StreamingJob::Table(table_source, _, _) = job
181                    && let Some(node_inner) = fs_fetch_node.node_inner.as_mut()
182                    && let Some(source) = table_source
183                {
184                    node_inner.source_id = source.id;
185                    if let Some(id) = source.optional_associated_table_id {
186                        node_inner.associated_table_id = Some(id.into());
187                    }
188                }
189            }
190            NodeBody::Source(source_node) => {
191                match job {
192                    // Note: For table without connector, it has a dummy Source node.
193                    // Note: For table with connector, it's source node has a source id different with the table id (job id), assigned in create_job_catalog.
194                    StreamingJob::Table(source, _table, _table_job_type) => {
195                        if let Some(source_inner) = source_node.source_inner.as_mut()
196                            && let Some(source) = source
197                        {
198                            debug_assert_ne!(source.id, job_id.as_raw_id());
199                            source_inner.source_id = source.id;
200                            if let Some(id) = source.optional_associated_table_id {
201                                source_inner.associated_table_id = Some(id.into());
202                            }
203                        }
204                    }
205                    StreamingJob::Source(source) => {
206                        has_job = true;
207                        if let Some(source_inner) = source_node.source_inner.as_mut() {
208                            debug_assert_eq!(source.id, job_id.as_raw_id());
209                            source_inner.source_id = source.id;
210                            if let Some(id) = source.optional_associated_table_id {
211                                source_inner.associated_table_id = Some(id.into());
212                            }
213                        }
214                    }
215                    // For other job types, no need to fill the source id, since it refers to an existing source.
216                    _ => {}
217                }
218            }
219            NodeBody::StreamCdcScan(node) => {
220                if let Some(table_desc) = node.cdc_table_desc.as_mut() {
221                    table_desc.table_id = job_id.as_mv_table_id();
222                }
223            }
224            NodeBody::VectorIndexWrite(node) => {
225                let table = node.table.as_mut().unwrap();
226                table.id = job_id.as_mv_table_id();
227                table.database_id = job.database_id();
228                table.schema_id = job.schema_id();
229                table.fragment_id = fragment_id;
230                #[cfg(not(debug_assertions))]
231                {
232                    table.definition = job.name();
233                }
234
235                has_job = true;
236            }
237            _ => {}
238        });
239
240        has_job
241    }
242
243    /// Extract the required columns of each upstream table except for cross-db backfill.
244    fn extract_upstream_columns_except_cross_db_backfill(
245        fragment: &StreamFragment,
246    ) -> HashMap<JobId, Vec<PbColumnDesc>> {
247        let mut table_columns = HashMap::new();
248
249        stream_graph_visitor::visit_fragment(fragment, |node_body| {
250            let (table_id, column_ids) = match node_body {
251                NodeBody::StreamScan(stream_scan) => {
252                    if stream_scan.get_stream_scan_type().unwrap()
253                        == StreamScanType::CrossDbSnapshotBackfill
254                    {
255                        return;
256                    }
257                    (
258                        stream_scan.table_id.as_job_id(),
259                        stream_scan.upstream_columns(),
260                    )
261                }
262                NodeBody::CdcFilter(cdc_filter) => (
263                    cdc_filter.upstream_source_id.as_share_source_job_id(),
264                    vec![],
265                ),
266                NodeBody::SourceBackfill(backfill) => (
267                    backfill.upstream_source_id.as_share_source_job_id(),
268                    // FIXME: only pass required columns instead of all columns here
269                    backfill.column_descs(),
270                ),
271                _ => return,
272            };
273            table_columns
274                .try_insert(table_id, column_ids)
275                .expect("currently there should be no two same upstream tables in a fragment");
276        });
277
278        table_columns
279    }
280
281    pub fn has_shuffled_backfill(&self) -> bool {
282        let stream_node = match self.inner.node.as_ref() {
283            Some(node) => node,
284            _ => return false,
285        };
286        let mut has_shuffled_backfill = false;
287        let has_shuffled_backfill_mut_ref = &mut has_shuffled_backfill;
288        visit_stream_node_cont(stream_node, |node| {
289            let is_shuffled_backfill = if let Some(node) = &node.node_body
290                && let Some(node) = node.as_stream_scan()
291            {
292                node.stream_scan_type == StreamScanType::ArrangementBackfill as i32
293                    || node.stream_scan_type == StreamScanType::SnapshotBackfill as i32
294            } else {
295                false
296            };
297            if is_shuffled_backfill {
298                *has_shuffled_backfill_mut_ref = true;
299                false
300            } else {
301                true
302            }
303        });
304        has_shuffled_backfill
305    }
306}
307
308impl Deref for BuildingFragment {
309    type Target = StreamFragment;
310
311    fn deref(&self) -> &Self::Target {
312        &self.inner
313    }
314}
315
316impl DerefMut for BuildingFragment {
317    fn deref_mut(&mut self) -> &mut Self::Target {
318        &mut self.inner
319    }
320}
321
322/// The ID of an edge in the fragment graph. For different types of edges, the ID will be in
323/// different variants.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumAsInner)]
325pub(super) enum EdgeId {
326    /// The edge between two building (internal) fragments.
327    Internal {
328        /// The ID generated by the frontend, generally the operator ID of `Exchange`.
329        /// See [`StreamFragmentEdgeProto`].
330        link_id: u64,
331    },
332
333    /// The edge between an upstream external fragment and downstream building fragment. Used for
334    /// MV on MV.
335    UpstreamExternal {
336        /// The ID of the upstream table or materialized view.
337        upstream_job_id: JobId,
338        /// The ID of the downstream fragment.
339        downstream_fragment_id: GlobalFragmentId,
340    },
341
342    /// The edge between an upstream building fragment and downstream external fragment. Used for
343    /// schema change (replace table plan).
344    DownstreamExternal(DownstreamExternalEdgeId),
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
348pub(super) struct DownstreamExternalEdgeId {
349    /// The ID of the original upstream fragment (`Materialize`).
350    pub(super) original_upstream_fragment_id: GlobalFragmentId,
351    /// The ID of the downstream fragment.
352    pub(super) downstream_fragment_id: GlobalFragmentId,
353}
354
355/// The edge in the fragment graph.
356///
357/// The edge can be either internal or external. This is distinguished by the [`EdgeId`].
358#[derive(Debug, Clone)]
359pub(super) struct StreamFragmentEdge {
360    /// The ID of the edge.
361    pub id: EdgeId,
362
363    /// The strategy used for dispatching the data.
364    pub dispatch_strategy: DispatchStrategy,
365}
366
367impl StreamFragmentEdge {
368    fn from_protobuf(edge: &StreamFragmentEdgeProto) -> Self {
369        Self {
370            // By creating an edge from the protobuf, we know that the edge is from the frontend and
371            // is internal.
372            id: EdgeId::Internal {
373                link_id: edge.link_id,
374            },
375            dispatch_strategy: edge.get_dispatch_strategy().unwrap().clone(),
376        }
377    }
378}
379
380fn clone_fragment(fragment: &Fragment, id_generator_manager: &IdGeneratorManager) -> Fragment {
381    let fragment_id = GlobalFragmentIdGen::new(id_generator_manager, 1)
382        .to_global_id(0)
383        .as_global_id();
384    Fragment {
385        fragment_id,
386        fragment_type_mask: fragment.fragment_type_mask,
387        distribution_type: fragment.distribution_type,
388        state_table_ids: fragment.state_table_ids.clone(),
389        maybe_vnode_count: fragment.maybe_vnode_count,
390        nodes: fragment.nodes.clone(),
391    }
392}
393
394pub fn check_sink_fragments_support_refresh_schema(
395    fragments: &BTreeMap<FragmentId, Fragment>,
396) -> MetaResult<()> {
397    if fragments.len() != 1 {
398        return Err(anyhow!(
399            "sink with auto schema change should have only 1 fragment, but got {:?}",
400            fragments.len()
401        )
402        .into());
403    }
404    let (_, fragment) = fragments.first_key_value().expect("non-empty");
405    let sink_node = &fragment.nodes;
406    let PbNodeBody::Sink(_) = sink_node.node_body.as_ref().unwrap() else {
407        return Err(anyhow!("expect PbNodeBody::Sink but got: {:?}", sink_node.node_body).into());
408    };
409    let [stream_input_node] = sink_node.input.as_slice() else {
410        panic!("Sink has more than 1 input: {:?}", sink_node.input);
411    };
412    let stream_scan_node = match stream_input_node.node_body.as_ref().unwrap() {
413        PbNodeBody::StreamScan(_) => stream_input_node,
414        PbNodeBody::Project(_) => {
415            let [stream_scan_node] = stream_input_node.input.as_slice() else {
416                return Err(anyhow!(
417                    "Project node must have exactly 1 input for auto schema change, but got {:?}",
418                    stream_input_node.input.len()
419                )
420                .into());
421            };
422            stream_scan_node
423        }
424        _ => {
425            return Err(anyhow!(
426                "expect PbNodeBody::StreamScan or PbNodeBody::Project but got: {:?}",
427                stream_input_node.node_body
428            )
429            .into());
430        }
431    };
432    let PbNodeBody::StreamScan(scan) = stream_scan_node.node_body.as_ref().unwrap() else {
433        return Err(anyhow!(
434            "expect PbNodeBody::StreamScan but got: {:?}",
435            stream_scan_node.node_body
436        )
437        .into());
438    };
439    let stream_scan_type = PbStreamScanType::try_from(scan.stream_scan_type).unwrap();
440    if stream_scan_type != PbStreamScanType::ArrangementBackfill {
441        return Err(anyhow!(
442            "unsupported stream_scan_type for auto refresh schema: {:?}",
443            stream_scan_type
444        )
445        .into());
446    }
447    let [merge_node, _batch_plan_node] = stream_scan_node.input.as_slice() else {
448        panic!(
449            "the number of StreamScan inputs is not 2: {:?}",
450            stream_scan_node.input
451        );
452    };
453    let NodeBody::Merge(_) = merge_node.node_body.as_ref().unwrap() else {
454        return Err(anyhow!(
455            "expect PbNodeBody::Merge but got: {:?}",
456            merge_node.node_body
457        )
458        .into());
459    };
460    Ok(())
461}
462
463/// Output mapping info after rewriting a `StreamScan` node.
464struct ScanRewriteResult {
465    old_output_index_to_new_output_index: HashMap<u32, u32>,
466    new_output_index_by_column_id: HashMap<ColumnId, u32>,
467    output_fields: Vec<risingwave_pb::plan_common::Field>,
468}
469
470/// Append new columns to a sink/log-store column list with updated names/ids.
471fn extend_sink_columns(
472    sink_columns: &mut Vec<PbColumnCatalog>,
473    new_columns: &[ColumnCatalog],
474    get_column_name: impl Fn(&String) -> String,
475) {
476    let next_column_id = sink_columns
477        .iter()
478        .map(|col| col.column_desc.as_ref().unwrap().column_id + 1)
479        .max()
480        .unwrap_or(1);
481    sink_columns.extend(new_columns.iter().enumerate().map(|(i, col)| {
482        let mut col = col.to_protobuf();
483        let column_desc = col.column_desc.as_mut().unwrap();
484        column_desc.column_id = next_column_id + (i as i32);
485        column_desc.name = get_column_name(&column_desc.name);
486        col
487    }));
488}
489
490/// Build sink column list after removing and appending columns.
491fn build_new_sink_columns(
492    sink: &PbSink,
493    removed_column_names: &HashSet<String>,
494    newly_added_columns: &[ColumnCatalog],
495) -> Vec<PbColumnCatalog> {
496    let mut columns: Vec<PbColumnCatalog> = sink
497        .columns
498        .iter()
499        .filter(|col| {
500            let column_name = &col.column_desc.as_ref().unwrap().name;
501            !removed_column_names.contains(column_name)
502        })
503        .cloned()
504        .collect();
505    extend_sink_columns(&mut columns, newly_added_columns, |name| name.clone());
506    columns
507}
508
509/// Rewrite log store table columns for schema change.
510fn rewrite_log_store_table(
511    log_store_table: &mut PbTable,
512    removed_log_store_column_names: &HashSet<String>,
513    newly_added_columns: &[ColumnCatalog],
514    upstream_table_name: &str,
515) {
516    log_store_table.columns.retain(|col| {
517        !removed_log_store_column_names.contains(&col.column_desc.as_ref().unwrap().name)
518    });
519    extend_sink_columns(&mut log_store_table.columns, newly_added_columns, |name| {
520        format!("{}_{}", upstream_table_name, name)
521    });
522    log_store_table.value_indices = (0..log_store_table.columns.len() as i32).collect();
523}
524
525/// Rewrite `StreamScan` + Merge to match the new upstream schema.
526fn rewrite_stream_scan_and_merge(
527    stream_scan_node: &mut StreamNode,
528    removed_column_ids: &HashSet<ColumnId>,
529    newly_added_columns: &[ColumnCatalog],
530    upstream_table: &PbTable,
531    upstream_table_fragment_id: FragmentId,
532) -> MetaResult<ScanRewriteResult> {
533    let PbNodeBody::StreamScan(scan) = stream_scan_node.node_body.as_mut().unwrap() else {
534        return Err(anyhow!(
535            "expect PbNodeBody::StreamScan but got: {:?}",
536            stream_scan_node.node_body
537        )
538        .into());
539    };
540    let [merge_node, _batch_plan_node] = stream_scan_node.input.as_mut_slice() else {
541        panic!(
542            "the number of StreamScan inputs is not 2: {:?}",
543            stream_scan_node.input
544        );
545    };
546    let NodeBody::Merge(merge) = merge_node.node_body.as_mut().unwrap() else {
547        return Err(anyhow!(
548            "expect PbNodeBody::Merge but got: {:?}",
549            merge_node.node_body
550        )
551        .into());
552    };
553
554    let stream_scan_type = PbStreamScanType::try_from(scan.stream_scan_type).unwrap();
555    if stream_scan_type != PbStreamScanType::ArrangementBackfill {
556        return Err(anyhow!(
557            "unsupported stream_scan_type for auto refresh schema: {:?}",
558            stream_scan_type
559        )
560        .into());
561    }
562
563    let upstream_columns_by_id: HashMap<i32, PbColumnDesc> = upstream_table
564        .columns
565        .iter()
566        .map(|col| {
567            let desc = col.column_desc.as_ref().unwrap().clone();
568            (desc.column_id, desc)
569        })
570        .collect();
571
572    let old_upstream_column_ids = scan.upstream_column_ids.clone();
573    let old_output_indices = scan.output_indices.clone();
574    let mut old_upstream_index_to_new_upstream_index = HashMap::new();
575    let mut new_upstream_column_ids = Vec::new();
576    for (old_idx, &column_id) in old_upstream_column_ids.iter().enumerate() {
577        if !removed_column_ids.contains(&ColumnId::new(column_id as _)) {
578            let new_idx = new_upstream_column_ids.len() as u32;
579            old_upstream_index_to_new_upstream_index.insert(old_idx as u32, new_idx);
580            new_upstream_column_ids.push(column_id);
581        }
582    }
583    let mut new_output_indices = Vec::new();
584    for old_output_index in &old_output_indices {
585        if let Some(new_index) = old_upstream_index_to_new_upstream_index.get(old_output_index) {
586            new_output_indices.push(*new_index);
587        }
588    }
589    for col in newly_added_columns {
590        let new_index = new_upstream_column_ids.len() as u32;
591        new_upstream_column_ids.push(col.column_id().get_id());
592        new_output_indices.push(new_index);
593    }
594
595    let new_output_column_ids: Vec<i32> = new_output_indices
596        .iter()
597        .map(|&idx| new_upstream_column_ids[idx as usize])
598        .collect();
599    let mut new_output_index_by_column_id = HashMap::new();
600    for (pos, &column_id) in new_output_column_ids.iter().enumerate() {
601        new_output_index_by_column_id.insert(ColumnId::new(column_id as _), pos as u32);
602    }
603    let mut old_output_index_to_new_output_index = HashMap::new();
604    for (old_pos, old_output_index) in old_output_indices.iter().enumerate() {
605        let column_id = old_upstream_column_ids[*old_output_index as usize];
606        if let Some(new_pos) = new_output_index_by_column_id.get(&ColumnId::new(column_id as _)) {
607            old_output_index_to_new_output_index.insert(old_pos as u32, *new_pos);
608        }
609    }
610
611    scan.arrangement_table = Some(upstream_table.clone());
612    scan.upstream_column_ids = new_upstream_column_ids;
613    scan.output_indices = new_output_indices;
614    let table_desc = scan.table_desc.as_mut().unwrap();
615    table_desc.columns = scan
616        .upstream_column_ids
617        .iter()
618        .map(|column_id| {
619            upstream_columns_by_id
620                .get(column_id)
621                .unwrap_or_else(|| panic!("upstream column id not found: {}", column_id))
622                .clone()
623        })
624        .collect();
625
626    stream_scan_node.fields = new_output_column_ids
627        .iter()
628        .map(|column_id| {
629            let col_desc = upstream_columns_by_id
630                .get(column_id)
631                .unwrap_or_else(|| panic!("upstream column id not found: {}", column_id));
632            Field::new(
633                format!("{}.{}", upstream_table.name, col_desc.name),
634                col_desc.column_type.as_ref().unwrap().into(),
635            )
636            .to_prost()
637        })
638        .collect();
639    // following logic in <StreamTableScan as Explain>::distill
640    stream_scan_node.identity = {
641        let columns = stream_scan_node
642            .fields
643            .iter()
644            .map(|col| &col.name)
645            .join(", ");
646        format!("StreamTableScan {{ table: t, columns: [{columns}] }}")
647    };
648
649    // update merge node
650    merge_node.fields = scan
651        .upstream_column_ids
652        .iter()
653        .map(|&column_id| {
654            let col_desc = upstream_columns_by_id
655                .get(&column_id)
656                .unwrap_or_else(|| panic!("upstream column id not found: {}", column_id));
657            Field::new(
658                col_desc.name.clone(),
659                col_desc.column_type.as_ref().unwrap().into(),
660            )
661            .to_prost()
662        })
663        .collect();
664    merge.upstream_fragment_id = upstream_table_fragment_id;
665
666    Ok(ScanRewriteResult {
667        old_output_index_to_new_output_index,
668        new_output_index_by_column_id,
669        output_fields: stream_scan_node.fields.clone(),
670    })
671}
672
673/// Rewrite Project node input refs and extend with newly added columns.
674fn rewrite_project_node(
675    project_node: &mut StreamNode,
676    scan_rewrite: &ScanRewriteResult,
677    newly_added_columns: &[ColumnCatalog],
678    removed_column_ids: &HashSet<ColumnId>,
679    upstream_table_name: &str,
680) -> MetaResult<()> {
681    let PbNodeBody::Project(project_node_body) = project_node.node_body.as_mut().unwrap() else {
682        return Err(anyhow!(
683            "expect PbNodeBody::Project but got: {:?}",
684            project_node.node_body
685        )
686        .into());
687    };
688    let has_non_input_ref = project_node_body
689        .select_list
690        .iter()
691        .any(|expr| !matches!(expr.rex_node, Some(expr_node::RexNode::InputRef(_))));
692    if has_non_input_ref && !removed_column_ids.is_empty() {
693        return Err(anyhow!(
694            "auto schema change with drop column only supports Project with InputRef"
695        )
696        .into());
697    }
698
699    let mut new_select_list = Vec::with_capacity(project_node_body.select_list.len());
700    let mut new_project_fields = Vec::with_capacity(project_node.fields.len());
701    for (index, expr) in project_node_body.select_list.iter().enumerate() {
702        let mut new_expr = expr.clone();
703        if let Some(expr_node::RexNode::InputRef(old_index)) = new_expr.rex_node {
704            let Some(&new_index) = scan_rewrite
705                .old_output_index_to_new_output_index
706                .get(&old_index)
707            else {
708                continue;
709            };
710            new_expr.rex_node = Some(expr_node::RexNode::InputRef(new_index));
711        } else if !removed_column_ids.is_empty() {
712            return Err(anyhow!(
713                "auto schema change with drop column only supports Project with InputRef"
714            )
715            .into());
716        }
717        new_select_list.push(new_expr);
718        new_project_fields.push(project_node.fields[index].clone());
719    }
720
721    for col in newly_added_columns {
722        let Some(&new_index) = scan_rewrite
723            .new_output_index_by_column_id
724            .get(&col.column_id())
725        else {
726            return Err(anyhow!("new column id not found in scan output").into());
727        };
728        new_select_list.push(PbExprNode {
729            function_type: expr_node::Type::Unspecified as i32,
730            return_type: Some(col.data_type().to_protobuf()),
731            rex_node: Some(expr_node::RexNode::InputRef(new_index)),
732        });
733        new_project_fields.push(
734            Field::new(
735                format!("{}.{}", upstream_table_name, col.column_desc.name),
736                col.data_type().clone(),
737            )
738            .to_prost(),
739        );
740    }
741
742    project_node_body.select_list = new_select_list;
743    project_node.fields = new_project_fields;
744    Ok(())
745}
746
747pub fn rewrite_refresh_schema_sink_fragment(
748    original_sink_fragment: &Fragment,
749    sink: &PbSink,
750    newly_added_columns: &[ColumnCatalog],
751    removed_columns: &[ColumnCatalog],
752    upstream_table: &PbTable,
753    upstream_table_fragment_id: FragmentId,
754    id_generator_manager: &IdGeneratorManager,
755) -> MetaResult<(Fragment, Vec<PbColumnCatalog>, Option<PbTable>)> {
756    let removed_column_ids: HashSet<_> =
757        removed_columns.iter().map(|col| col.column_id()).collect();
758    let removed_log_store_column_names: HashSet<_> = removed_columns
759        .iter()
760        .map(|col| format!("{}_{}", upstream_table.name, col.column_desc.name))
761        .collect();
762    let removed_sink_column_names: HashSet<_> = removed_columns
763        .iter()
764        .map(|col| col.column_desc.name.clone())
765        .collect();
766    let new_sink_columns =
767        build_new_sink_columns(sink, &removed_sink_column_names, newly_added_columns);
768
769    let mut new_sink_fragment = clone_fragment(original_sink_fragment, id_generator_manager);
770    let sink_node = &mut new_sink_fragment.nodes;
771    let PbNodeBody::Sink(sink_node_body) = sink_node.node_body.as_mut().unwrap() else {
772        return Err(anyhow!("expect PbNodeBody::Sink but got: {:?}", sink_node.node_body).into());
773    };
774    let [stream_input_node] = sink_node.input.as_mut_slice() else {
775        panic!("Sink has more than 1 input: {:?}", sink_node.input);
776    };
777    let stream_input_body = stream_input_node.node_body.as_ref().unwrap();
778    let stream_input_is_project = matches!(stream_input_body, PbNodeBody::Project(_));
779    let stream_input_is_scan = matches!(stream_input_body, PbNodeBody::StreamScan(_));
780    if !stream_input_is_project && !stream_input_is_scan {
781        return Err(anyhow!(
782            "expect PbNodeBody::StreamScan or PbNodeBody::Project but got: {:?}",
783            stream_input_body
784        )
785        .into());
786    }
787
788    // update sink_node
789    // following logic in <StreamSink as Explain>::distill
790    sink_node.identity = {
791        let sink_type = SinkType::from_proto(sink.sink_type());
792        let sink_type_str = sink_type.type_str();
793        let column_names = new_sink_columns
794            .iter()
795            .map(|col| {
796                ColumnCatalog::from(col.clone())
797                    .name_with_hidden()
798                    .to_string()
799            })
800            .join(", ");
801        let downstream_pk = if !sink_type.is_append_only() {
802            let downstream_pk = sink
803                .downstream_pk
804                .iter()
805                .map(|i| &sink.columns[*i as usize].column_desc.as_ref().unwrap().name)
806                .collect_vec();
807            format!(", downstream_pk: {downstream_pk:?}")
808        } else {
809            "".to_owned()
810        };
811        format!("StreamSink {{ type: {sink_type_str}, columns: [{column_names}]{downstream_pk} }}")
812    };
813    let new_log_store_table = if let Some(log_store_table) = &mut sink_node_body.table {
814        rewrite_log_store_table(
815            log_store_table,
816            &removed_log_store_column_names,
817            newly_added_columns,
818            &upstream_table.name,
819        );
820        Some(log_store_table.clone())
821    } else {
822        None
823    };
824    sink_node_body.sink_desc.as_mut().unwrap().column_catalogs = new_sink_columns.clone();
825
826    let stream_scan_node = if stream_input_is_project {
827        let [stream_scan_node] = stream_input_node.input.as_mut_slice() else {
828            return Err(anyhow!(
829                "Project node must have exactly 1 input for auto schema change, but got {:?}",
830                stream_input_node.input.len()
831            )
832            .into());
833        };
834        stream_scan_node
835    } else {
836        stream_input_node
837    };
838    let scan_rewrite = rewrite_stream_scan_and_merge(
839        stream_scan_node,
840        &removed_column_ids,
841        newly_added_columns,
842        upstream_table,
843        upstream_table_fragment_id,
844    )?;
845
846    if stream_input_is_project {
847        let [project_node] = sink_node.input.as_mut_slice() else {
848            panic!("Sink has more than 1 input: {:?}", sink_node.input);
849        };
850        rewrite_project_node(
851            project_node,
852            &scan_rewrite,
853            newly_added_columns,
854            &removed_column_ids,
855            &upstream_table.name,
856        )?;
857        sink_node.fields = project_node.fields.clone();
858    } else {
859        sink_node.fields = scan_rewrite.output_fields;
860    }
861    Ok((new_sink_fragment, new_sink_columns, new_log_store_table))
862}
863
864/// Adjacency list (G) of backfill orders.
865/// `G[10] -> [1, 2, 11]`
866/// means for the backfill node in `fragment 10`
867/// should be backfilled before the backfill nodes in `fragment 1, 2 and 11`.
868#[derive(Clone, Debug, Default)]
869pub struct FragmentBackfillOrder<const EXTENDED: bool> {
870    inner: HashMap<FragmentId, Vec<FragmentId>>,
871}
872
873impl<const EXTENDED: bool> Deref for FragmentBackfillOrder<EXTENDED> {
874    type Target = HashMap<FragmentId, Vec<FragmentId>>;
875
876    fn deref(&self) -> &Self::Target {
877        &self.inner
878    }
879}
880
881impl UserDefinedFragmentBackfillOrder {
882    pub fn new(inner: HashMap<FragmentId, Vec<FragmentId>>) -> Self {
883        Self { inner }
884    }
885
886    pub fn merge(orders: impl Iterator<Item = Self>) -> Self {
887        Self {
888            inner: orders.flat_map(|order| order.inner).collect(),
889        }
890    }
891
892    pub fn to_meta_model(&self) -> BackfillOrders {
893        self.inner.clone().into()
894    }
895}
896
897pub type UserDefinedFragmentBackfillOrder = FragmentBackfillOrder<false>;
898pub type ExtendedFragmentBackfillOrder = FragmentBackfillOrder<true>;
899
900/// In-memory representation of a **Fragment** Graph, built from the [`StreamFragmentGraphProto`]
901/// from the frontend.
902///
903/// This only includes nodes and edges of the current job itself. It will be converted to [`CompleteStreamFragmentGraph`] later,
904/// that contains the additional information of pre-existing
905/// fragments, which are connected to the graph's top-most or bottom-most fragments.
906#[derive(Default, Debug)]
907pub struct StreamFragmentGraph {
908    /// stores all the fragments in the graph.
909    pub(super) fragments: HashMap<GlobalFragmentId, BuildingFragment>,
910
911    /// stores edges between fragments: upstream => downstream.
912    pub(super) downstreams:
913        HashMap<GlobalFragmentId, HashMap<GlobalFragmentId, StreamFragmentEdge>>,
914
915    /// stores edges between fragments: downstream -> upstream.
916    pub(super) upstreams: HashMap<GlobalFragmentId, HashMap<GlobalFragmentId, StreamFragmentEdge>>,
917
918    /// Dependent relations of this job.
919    dependent_table_ids: HashSet<TableId>,
920
921    /// The default parallelism of the job, specified by the `STREAMING_PARALLELISM` session
922    /// variable. If not specified, all active worker slots will be used.
923    specified_parallelism: Option<NonZeroUsize>,
924    /// The parallelism to use during backfill, specified by the `STREAMING_PARALLELISM_FOR_BACKFILL`
925    /// session variable. If not specified, falls back to `specified_parallelism`.
926    specified_backfill_parallelism: Option<NonZeroUsize>,
927
928    /// Specified max parallelism, i.e., expected vnode count for the graph.
929    ///
930    /// The scheduler on the meta service will use this as a hint to decide the vnode count
931    /// for each fragment.
932    ///
933    /// Note that the actual vnode count may be different from this value.
934    /// For example, a no-shuffle exchange between current fragment graph and an existing
935    /// upstream fragment graph requires two fragments to be in the same distribution,
936    /// thus the same vnode count.
937    max_parallelism: usize,
938
939    /// The backfill ordering strategy of the graph.
940    backfill_order: BackfillOrder,
941}
942
943impl StreamFragmentGraph {
944    /// Create a new [`StreamFragmentGraph`] from the given [`StreamFragmentGraphProto`], with all
945    /// global IDs correctly filled.
946    pub fn new(
947        env: &MetaSrvEnv,
948        proto: StreamFragmentGraphProto,
949        job: &StreamingJob,
950    ) -> MetaResult<Self> {
951        let fragment_id_gen =
952            GlobalFragmentIdGen::new(env.id_gen_manager(), proto.fragments.len() as u64);
953        // Note: in SQL backend, the ids generated here are fake and will be overwritten again
954        // with `refill_internal_table_ids` later.
955        // TODO: refactor the code to remove this step.
956        let table_id_gen = GlobalTableIdGen::new(env.id_gen_manager(), proto.table_ids_cnt as u64);
957
958        // Create nodes.
959        let fragments: HashMap<_, _> = proto
960            .fragments
961            .into_iter()
962            .map(|(id, fragment)| {
963                let id = fragment_id_gen.to_global_id(id.as_raw_id());
964                let fragment = BuildingFragment::new(id, fragment, job, table_id_gen);
965                (id, fragment)
966            })
967            .collect();
968
969        assert_eq!(
970            fragments
971                .values()
972                .map(|f| f.extract_internal_tables().len() as u32)
973                .sum::<u32>(),
974            proto.table_ids_cnt
975        );
976
977        // Create edges.
978        let mut downstreams = HashMap::new();
979        let mut upstreams = HashMap::new();
980
981        for edge in proto.edges {
982            let upstream_id = fragment_id_gen.to_global_id(edge.upstream_id.as_raw_id());
983            let downstream_id = fragment_id_gen.to_global_id(edge.downstream_id.as_raw_id());
984            let edge = StreamFragmentEdge::from_protobuf(&edge);
985
986            upstreams
987                .entry(downstream_id)
988                .or_insert_with(HashMap::new)
989                .try_insert(upstream_id, edge.clone())
990                .unwrap();
991            downstreams
992                .entry(upstream_id)
993                .or_insert_with(HashMap::new)
994                .try_insert(downstream_id, edge)
995                .unwrap();
996        }
997
998        // Note: Here we directly use the field `dependent_table_ids` in the proto (resolved in
999        // frontend), instead of visiting the graph ourselves.
1000        let dependent_table_ids = proto.dependent_table_ids.iter().copied().collect();
1001
1002        let specified_parallelism = if let Some(Parallelism { parallelism }) = proto.parallelism {
1003            Some(NonZeroUsize::new(parallelism as usize).context("parallelism should not be 0")?)
1004        } else {
1005            None
1006        };
1007        let specified_backfill_parallelism =
1008            if let Some(Parallelism { parallelism }) = proto.backfill_parallelism {
1009                Some(
1010                    NonZeroUsize::new(parallelism as usize)
1011                        .context("backfill parallelism should not be 0")?,
1012                )
1013            } else {
1014                None
1015            };
1016
1017        let max_parallelism = proto.max_parallelism as usize;
1018        let backfill_order = proto.backfill_order.unwrap_or(BackfillOrder {
1019            order: Default::default(),
1020        });
1021
1022        Ok(Self {
1023            fragments,
1024            downstreams,
1025            upstreams,
1026            dependent_table_ids,
1027            specified_parallelism,
1028            specified_backfill_parallelism,
1029            max_parallelism,
1030            backfill_order,
1031        })
1032    }
1033
1034    /// Retrieve the **incomplete** internal tables map of the whole graph.
1035    ///
1036    /// Note that some fields in the table catalogs are not filled during the current phase, e.g.,
1037    /// `fragment_id`, `vnode_count`. They will be all filled after a `TableFragments` is built.
1038    /// Be careful when using the returned values.
1039    pub fn incomplete_internal_tables(&self) -> BTreeMap<TableId, Table> {
1040        let mut tables = BTreeMap::new();
1041        for fragment in self.fragments.values() {
1042            for table in fragment.extract_internal_tables() {
1043                let table_id = table.id;
1044                tables
1045                    .try_insert(table_id, table)
1046                    .unwrap_or_else(|_| panic!("duplicated table id `{}`", table_id));
1047            }
1048        }
1049        tables
1050    }
1051
1052    /// Refill the internal tables' `table_id`s according to the given map, typically obtained from
1053    /// `create_internal_table_catalog`.
1054    pub fn refill_internal_table_ids(&mut self, table_id_map: HashMap<TableId, TableId>) {
1055        for fragment in self.fragments.values_mut() {
1056            stream_graph_visitor::visit_internal_tables(
1057                &mut fragment.inner,
1058                |table, _table_type_name| {
1059                    let target = table_id_map.get(&table.id).cloned().unwrap();
1060                    table.id = target;
1061                },
1062            );
1063        }
1064    }
1065
1066    /// Use a trivial algorithm to match the internal tables of the new graph for
1067    /// `ALTER TABLE` or `ALTER SOURCE`.
1068    pub fn fit_internal_tables_trivial(
1069        &mut self,
1070        mut old_internal_tables: Vec<Table>,
1071    ) -> MetaResult<()> {
1072        let mut new_internal_table_ids = Vec::new();
1073        for fragment in self.fragments.values() {
1074            for table in &fragment.extract_internal_tables() {
1075                new_internal_table_ids.push(table.id);
1076            }
1077        }
1078
1079        if new_internal_table_ids.len() != old_internal_tables.len() {
1080            bail!(
1081                "Different number of internal tables. New: {}, Old: {}",
1082                new_internal_table_ids.len(),
1083                old_internal_tables.len()
1084            );
1085        }
1086        old_internal_tables.sort_by_key(|t| t.id);
1087        new_internal_table_ids.sort();
1088
1089        let internal_table_id_map = new_internal_table_ids
1090            .into_iter()
1091            .zip_eq_fast(old_internal_tables.into_iter())
1092            .collect::<HashMap<_, _>>();
1093
1094        // TODO(alter-mv): unify this with `fit_internal_table_ids_with_mapping` after we
1095        // confirm the behavior is the same.
1096        for fragment in self.fragments.values_mut() {
1097            stream_graph_visitor::visit_internal_tables(
1098                &mut fragment.inner,
1099                |table, _table_type_name| {
1100                    // XXX: this replaces the entire table, instead of just the id!
1101                    let target = internal_table_id_map.get(&table.id).cloned().unwrap();
1102                    *table = target;
1103                },
1104            );
1105        }
1106
1107        Ok(())
1108    }
1109
1110    /// Fit the internal tables' `table_id`s according to the given mapping.
1111    pub fn fit_internal_table_ids_with_mapping(&mut self, mut matches: HashMap<TableId, Table>) {
1112        for fragment in self.fragments.values_mut() {
1113            stream_graph_visitor::visit_internal_tables(
1114                &mut fragment.inner,
1115                |table, _table_type_name| {
1116                    let target = matches.remove(&table.id).unwrap_or_else(|| {
1117                        panic!("no matching table for table {}({})", table.id, table.name)
1118                    });
1119                    table.id = target.id;
1120                    table.maybe_vnode_count = target.maybe_vnode_count;
1121                },
1122            );
1123        }
1124    }
1125
1126    pub fn fit_snapshot_backfill_epochs(
1127        &mut self,
1128        mut snapshot_backfill_epochs: HashMap<StreamNodeLocalOperatorId, u64>,
1129    ) {
1130        for fragment in self.fragments.values_mut() {
1131            visit_stream_node_cont_mut(fragment.node.as_mut().unwrap(), |node| {
1132                if let PbNodeBody::StreamScan(scan) = node.node_body.as_mut().unwrap()
1133                    && let StreamScanType::SnapshotBackfill
1134                    | StreamScanType::CrossDbSnapshotBackfill = scan.stream_scan_type()
1135                {
1136                    let Some(epoch) = snapshot_backfill_epochs.remove(&node.operator_id) else {
1137                        panic!("no snapshot epoch found for node {:?}", node)
1138                    };
1139                    scan.snapshot_backfill_epoch = Some(epoch);
1140                }
1141                true
1142            })
1143        }
1144    }
1145
1146    /// Returns the fragment id where the streaming job node located.
1147    pub fn table_fragment_id(&self) -> FragmentId {
1148        Itertools::exactly_one(
1149            self.fragments
1150                .values()
1151                .filter(|b| b.job_id.is_some())
1152                .map(|b| b.fragment_id),
1153        )
1154        .expect(
1155            "require exactly 1 materialize/sink/cdc source node when creating the streaming job",
1156        )
1157    }
1158
1159    /// Returns the fragment id where the table dml is received.
1160    pub fn dml_fragment_id(&self) -> Option<FragmentId> {
1161        self.fragments
1162            .values()
1163            .filter(|b| {
1164                FragmentTypeMask::from(b.fragment_type_mask).contains(FragmentTypeFlag::Dml)
1165            })
1166            .map(|b| b.fragment_id)
1167            .at_most_one()
1168            .expect("require at most 1 dml node when creating the streaming job")
1169    }
1170
1171    /// Get the dependent streaming job ids of this job.
1172    pub fn dependent_table_ids(&self) -> &HashSet<TableId> {
1173        &self.dependent_table_ids
1174    }
1175
1176    /// Get the parallelism of the job, if specified by the user.
1177    pub fn specified_parallelism(&self) -> Option<NonZeroUsize> {
1178        self.specified_parallelism
1179    }
1180
1181    /// Get the backfill parallelism of the job, if specified by the user.
1182    pub fn specified_backfill_parallelism(&self) -> Option<NonZeroUsize> {
1183        self.specified_backfill_parallelism
1184    }
1185
1186    /// Get the expected vnode count of the graph. See documentation of the field for more details.
1187    pub fn max_parallelism(&self) -> usize {
1188        self.max_parallelism
1189    }
1190
1191    /// Get downstreams of a fragment.
1192    fn get_downstreams(
1193        &self,
1194        fragment_id: GlobalFragmentId,
1195    ) -> &HashMap<GlobalFragmentId, StreamFragmentEdge> {
1196        self.downstreams.get(&fragment_id).unwrap_or(&EMPTY_HASHMAP)
1197    }
1198
1199    /// Get upstreams of a fragment.
1200    fn get_upstreams(
1201        &self,
1202        fragment_id: GlobalFragmentId,
1203    ) -> &HashMap<GlobalFragmentId, StreamFragmentEdge> {
1204        self.upstreams.get(&fragment_id).unwrap_or(&EMPTY_HASHMAP)
1205    }
1206
1207    pub fn collect_snapshot_backfill_info(
1208        &self,
1209    ) -> MetaResult<(Option<SnapshotBackfillInfo>, SnapshotBackfillInfo)> {
1210        Self::collect_snapshot_backfill_info_impl(self.fragments.values().map(|fragment| {
1211            (
1212                fragment.node.as_ref().unwrap(),
1213                fragment.fragment_type_mask.into(),
1214            )
1215        }))
1216    }
1217
1218    /// Returns `Ok((Some(``snapshot_backfill_info``), ``cross_db_snapshot_backfill_info``))`
1219    pub fn collect_snapshot_backfill_info_impl(
1220        fragments: impl IntoIterator<Item = (&PbStreamNode, FragmentTypeMask)>,
1221    ) -> MetaResult<(Option<SnapshotBackfillInfo>, SnapshotBackfillInfo)> {
1222        let mut prev_stream_scan: Option<(Option<SnapshotBackfillInfo>, StreamScanNode)> = None;
1223        let mut cross_db_info = SnapshotBackfillInfo {
1224            upstream_mv_table_id_to_backfill_epoch: Default::default(),
1225        };
1226        let mut result = Ok(());
1227        for (node, fragment_type_mask) in fragments {
1228            visit_stream_node_cont(node, |node| {
1229                if let Some(NodeBody::StreamScan(stream_scan)) = node.node_body.as_ref() {
1230                    let stream_scan_type = StreamScanType::try_from(stream_scan.stream_scan_type)
1231                        .expect("invalid stream_scan_type");
1232                    let is_snapshot_backfill = match stream_scan_type {
1233                        StreamScanType::SnapshotBackfill => {
1234                            assert!(
1235                                fragment_type_mask
1236                                    .contains(FragmentTypeFlag::SnapshotBackfillStreamScan)
1237                            );
1238                            true
1239                        }
1240                        StreamScanType::CrossDbSnapshotBackfill => {
1241                            assert!(
1242                                fragment_type_mask
1243                                    .contains(FragmentTypeFlag::CrossDbSnapshotBackfillStreamScan)
1244                            );
1245                            cross_db_info
1246                                .upstream_mv_table_id_to_backfill_epoch
1247                                .insert(stream_scan.table_id, stream_scan.snapshot_backfill_epoch);
1248
1249                            return true;
1250                        }
1251                        _ => false,
1252                    };
1253
1254                    match &mut prev_stream_scan {
1255                        Some((prev_snapshot_backfill_info, prev_stream_scan)) => {
1256                            match (prev_snapshot_backfill_info, is_snapshot_backfill) {
1257                                (Some(prev_snapshot_backfill_info), true) => {
1258                                    prev_snapshot_backfill_info
1259                                        .upstream_mv_table_id_to_backfill_epoch
1260                                        .insert(
1261                                            stream_scan.table_id,
1262                                            stream_scan.snapshot_backfill_epoch,
1263                                        );
1264                                    true
1265                                }
1266                                (None, false) => true,
1267                                (_, _) => {
1268                                    result = Err(anyhow!("must be either all snapshot_backfill or no snapshot_backfill. Curr: {stream_scan:?} Prev: {prev_stream_scan:?}").into());
1269                                    false
1270                                }
1271                            }
1272                        }
1273                        None => {
1274                            prev_stream_scan = Some((
1275                                if is_snapshot_backfill {
1276                                    Some(SnapshotBackfillInfo {
1277                                        upstream_mv_table_id_to_backfill_epoch: HashMap::from_iter(
1278                                            [(
1279                                                stream_scan.table_id,
1280                                                stream_scan.snapshot_backfill_epoch,
1281                                            )],
1282                                        ),
1283                                    })
1284                                } else {
1285                                    None
1286                                },
1287                                *stream_scan.clone(),
1288                            ));
1289                            true
1290                        }
1291                    }
1292                } else {
1293                    true
1294                }
1295            })
1296        }
1297        result.map(|_| {
1298            (
1299                prev_stream_scan
1300                    .map(|(snapshot_backfill_info, _)| snapshot_backfill_info)
1301                    .unwrap_or(None),
1302                cross_db_info,
1303            )
1304        })
1305    }
1306
1307    /// Collect the mapping from table / `source_id` -> `fragment_id`
1308    pub fn collect_backfill_mapping(
1309        fragments: impl Iterator<Item = (FragmentId, FragmentTypeMask, &PbStreamNode)>,
1310    ) -> HashMap<RelationId, Vec<FragmentId>> {
1311        let mut mapping = HashMap::new();
1312        for (fragment_id, fragment_type_mask, node) in fragments {
1313            let has_some_scan = fragment_type_mask
1314                .contains_any([FragmentTypeFlag::StreamScan, FragmentTypeFlag::SourceScan]);
1315            if has_some_scan {
1316                visit_stream_node_cont(node, |node| {
1317                    match node.node_body.as_ref() {
1318                        Some(NodeBody::StreamScan(stream_scan)) => {
1319                            let table_id = stream_scan.table_id;
1320                            let fragments: &mut Vec<_> =
1321                                mapping.entry(table_id.as_relation_id()).or_default();
1322                            fragments.push(fragment_id);
1323                            // each fragment should have only 1 scan node.
1324                            false
1325                        }
1326                        Some(NodeBody::SourceBackfill(source_backfill)) => {
1327                            let source_id = source_backfill.upstream_source_id;
1328                            let fragments: &mut Vec<_> =
1329                                mapping.entry(source_id.as_relation_id()).or_default();
1330                            fragments.push(fragment_id);
1331                            // each fragment should have only 1 scan node.
1332                            false
1333                        }
1334                        _ => true,
1335                    }
1336                })
1337            }
1338        }
1339        mapping
1340    }
1341
1342    /// Initially the mapping that comes from frontend is between `table_ids`.
1343    /// We should remap it to fragment level, since we track progress by actor, and we can get
1344    /// a fragment <-> actor mapping
1345    pub fn create_fragment_backfill_ordering(&self) -> UserDefinedFragmentBackfillOrder {
1346        let mapping =
1347            Self::collect_backfill_mapping(self.fragments.iter().map(|(fragment_id, fragment)| {
1348                (
1349                    fragment_id.as_global_id(),
1350                    fragment.fragment_type_mask.into(),
1351                    fragment.node.as_ref().expect("should exist node"),
1352                )
1353            }));
1354        let mut fragment_ordering: HashMap<FragmentId, Vec<FragmentId>> = HashMap::new();
1355
1356        // 1. Add backfill dependencies
1357        for (rel_id, downstream_rel_ids) in &self.backfill_order.order {
1358            let fragment_ids = mapping.get(rel_id).unwrap();
1359            for fragment_id in fragment_ids {
1360                let downstream_fragment_ids = downstream_rel_ids
1361                    .data
1362                    .iter()
1363                    .flat_map(|&downstream_rel_id| mapping.get(&downstream_rel_id).unwrap().iter())
1364                    .copied()
1365                    .collect();
1366                fragment_ordering.insert(*fragment_id, downstream_fragment_ids);
1367            }
1368        }
1369
1370        UserDefinedFragmentBackfillOrder {
1371            inner: fragment_ordering,
1372        }
1373    }
1374
1375    pub fn extend_fragment_backfill_ordering_with_locality_backfill<
1376        'a,
1377        FI: Iterator<Item = (FragmentId, FragmentTypeMask, &'a PbStreamNode)> + 'a,
1378    >(
1379        fragment_ordering: UserDefinedFragmentBackfillOrder,
1380        fragment_downstreams: &FragmentDownstreamRelation,
1381        get_fragments: impl Fn() -> FI,
1382    ) -> ExtendedFragmentBackfillOrder {
1383        let mut fragment_ordering = fragment_ordering.inner;
1384        let mapping = Self::collect_backfill_mapping(get_fragments());
1385        // If no backfill order is specified, we still need to ensure that all backfill fragments
1386        // run before LocalityProvider fragments.
1387        if fragment_ordering.is_empty() {
1388            for value in mapping.values() {
1389                for &fragment_id in value {
1390                    fragment_ordering.entry(fragment_id).or_default();
1391                }
1392            }
1393        }
1394
1395        // 2. Add dependencies: all backfill fragments should run before LocalityProvider fragments
1396        let locality_provider_dependencies = Self::find_locality_provider_dependencies(
1397            get_fragments().map(|(fragment_id, _, node)| (fragment_id, node)),
1398            fragment_downstreams,
1399        );
1400
1401        let backfill_fragments: HashSet<FragmentId> = mapping.values().flatten().copied().collect();
1402
1403        // Calculate LocalityProvider root fragments (zero indegree)
1404        // Root fragments are those that appear as keys but never appear as downstream dependencies
1405        let all_locality_provider_fragments: HashSet<FragmentId> =
1406            locality_provider_dependencies.keys().copied().collect();
1407        let downstream_locality_provider_fragments: HashSet<FragmentId> =
1408            locality_provider_dependencies
1409                .values()
1410                .flatten()
1411                .copied()
1412                .collect();
1413        let locality_provider_root_fragments: Vec<FragmentId> = all_locality_provider_fragments
1414            .difference(&downstream_locality_provider_fragments)
1415            .copied()
1416            .collect();
1417
1418        // For each backfill fragment, add only the root LocalityProvider fragments as dependents
1419        // This ensures backfill completes before any LocalityProvider starts, while minimizing dependencies
1420        for &backfill_fragment_id in &backfill_fragments {
1421            fragment_ordering
1422                .entry(backfill_fragment_id)
1423                .or_default()
1424                .extend(locality_provider_root_fragments.iter().copied());
1425        }
1426
1427        // 3. Add LocalityProvider internal dependencies
1428        for (fragment_id, downstream_fragments) in locality_provider_dependencies {
1429            fragment_ordering
1430                .entry(fragment_id)
1431                .or_default()
1432                .extend(downstream_fragments);
1433        }
1434
1435        // Deduplicate downstream entries per fragment; overlaps are common when the same fragment
1436        // is reached via multiple paths (e.g., with StreamShare) and would otherwise appear
1437        // multiple times.
1438        for downstream in fragment_ordering.values_mut() {
1439            let mut seen = HashSet::new();
1440            downstream.retain(|id| seen.insert(*id));
1441        }
1442
1443        ExtendedFragmentBackfillOrder {
1444            inner: fragment_ordering,
1445        }
1446    }
1447
1448    pub fn find_locality_provider_fragment_state_table_mapping(
1449        &self,
1450    ) -> HashMap<FragmentId, Vec<TableId>> {
1451        let mut mapping: HashMap<FragmentId, Vec<TableId>> = HashMap::new();
1452
1453        for (fragment_id, fragment) in &self.fragments {
1454            let fragment_id = fragment_id.as_global_id();
1455
1456            // Check if this fragment contains a LocalityProvider node
1457            if let Some(node) = fragment.node.as_ref() {
1458                let mut state_table_ids = Vec::new();
1459
1460                visit_stream_node_cont(node, |stream_node| {
1461                    if let Some(NodeBody::LocalityProvider(locality_provider)) =
1462                        stream_node.node_body.as_ref()
1463                    {
1464                        // Collect state table ID (except the progress table)
1465                        let state_table_id = locality_provider
1466                            .state_table
1467                            .as_ref()
1468                            .expect("must have state table")
1469                            .id;
1470                        state_table_ids.push(state_table_id);
1471                        false // Stop visiting once we find a LocalityProvider
1472                    } else {
1473                        true // Continue visiting
1474                    }
1475                });
1476
1477                if !state_table_ids.is_empty() {
1478                    mapping.insert(fragment_id, state_table_ids);
1479                }
1480            }
1481        }
1482
1483        mapping
1484    }
1485
1486    /// Find dependency relationships among fragments containing `LocalityProvider` nodes.
1487    /// Returns a mapping where each fragment ID maps to a list of fragment IDs that should be processed after it.
1488    /// Following the same semantics as `FragmentBackfillOrder`:
1489    /// `G[10] -> [1, 2, 11]` means `LocalityProvider` in fragment 10 should be processed
1490    /// before `LocalityProviders` in fragments 1, 2, and 11.
1491    ///
1492    /// This method assumes each fragment contains at most one `LocalityProvider` node.
1493    pub fn find_locality_provider_dependencies<'a>(
1494        fragments_nodes: impl Iterator<Item = (FragmentId, &'a PbStreamNode)>,
1495        fragment_downstreams: &FragmentDownstreamRelation,
1496    ) -> HashMap<FragmentId, Vec<FragmentId>> {
1497        let mut locality_provider_fragments = HashSet::new();
1498        let mut dependencies: HashMap<FragmentId, Vec<FragmentId>> = HashMap::new();
1499
1500        // First, identify all fragments that contain LocalityProvider nodes
1501        for (fragment_id, node) in fragments_nodes {
1502            let has_locality_provider = Self::fragment_has_locality_provider(node);
1503
1504            if has_locality_provider {
1505                locality_provider_fragments.insert(fragment_id);
1506                dependencies.entry(fragment_id).or_default();
1507            }
1508        }
1509
1510        // Build dependency relationships between LocalityProvider fragments
1511        // For each LocalityProvider fragment, find all downstream LocalityProvider fragments
1512        // The upstream fragment should be processed before the downstream fragments
1513        for &provider_fragment_id in &locality_provider_fragments {
1514            // Find all fragments downstream from this LocalityProvider fragment
1515            let mut visited = HashSet::new();
1516            let mut downstream_locality_providers = Vec::new();
1517
1518            Self::collect_downstream_locality_providers(
1519                provider_fragment_id,
1520                &locality_provider_fragments,
1521                fragment_downstreams,
1522                &mut visited,
1523                &mut downstream_locality_providers,
1524            );
1525
1526            // This fragment should be processed before all its downstream LocalityProvider fragments
1527            dependencies
1528                .entry(provider_fragment_id)
1529                .or_default()
1530                .extend(downstream_locality_providers);
1531        }
1532
1533        dependencies
1534    }
1535
1536    fn fragment_has_locality_provider(node: &PbStreamNode) -> bool {
1537        let mut has_locality_provider = false;
1538
1539        {
1540            visit_stream_node_cont(node, |stream_node| {
1541                if let Some(NodeBody::LocalityProvider(_)) = stream_node.node_body.as_ref() {
1542                    has_locality_provider = true;
1543                    false // Stop visiting once we find a LocalityProvider
1544                } else {
1545                    true // Continue visiting
1546                }
1547            });
1548        }
1549
1550        has_locality_provider
1551    }
1552
1553    /// Recursively collect downstream `LocalityProvider` fragments
1554    fn collect_downstream_locality_providers(
1555        current_fragment_id: FragmentId,
1556        locality_provider_fragments: &HashSet<FragmentId>,
1557        fragment_downstreams: &FragmentDownstreamRelation,
1558        visited: &mut HashSet<FragmentId>,
1559        downstream_providers: &mut Vec<FragmentId>,
1560    ) {
1561        if visited.contains(&current_fragment_id) {
1562            return;
1563        }
1564        visited.insert(current_fragment_id);
1565
1566        // Check all downstream fragments
1567        for downstream_fragment_id in fragment_downstreams
1568            .get(&current_fragment_id)
1569            .into_iter()
1570            .flat_map(|downstreams| {
1571                downstreams
1572                    .iter()
1573                    .map(|downstream| downstream.downstream_fragment_id)
1574            })
1575        {
1576            // If the downstream fragment is a LocalityProvider, add it to results
1577            if locality_provider_fragments.contains(&downstream_fragment_id) {
1578                downstream_providers.push(downstream_fragment_id);
1579            }
1580
1581            // Recursively check further downstream
1582            Self::collect_downstream_locality_providers(
1583                downstream_fragment_id,
1584                locality_provider_fragments,
1585                fragment_downstreams,
1586                visited,
1587                downstream_providers,
1588            );
1589        }
1590    }
1591}
1592
1593/// Fill snapshot epoch for `StreamScanNode` of `SnapshotBackfill`.
1594/// Return `true` when has change applied.
1595pub fn fill_snapshot_backfill_epoch(
1596    node: &mut StreamNode,
1597    snapshot_backfill_info: Option<&SnapshotBackfillInfo>,
1598    cross_db_snapshot_backfill_info: &SnapshotBackfillInfo,
1599) -> MetaResult<bool> {
1600    let mut result = Ok(());
1601    let mut applied = false;
1602    visit_stream_node_cont_mut(node, |node| {
1603        if let Some(NodeBody::StreamScan(stream_scan)) = node.node_body.as_mut()
1604            && (stream_scan.stream_scan_type == StreamScanType::SnapshotBackfill as i32
1605                || stream_scan.stream_scan_type == StreamScanType::CrossDbSnapshotBackfill as i32)
1606        {
1607            result = try {
1608                let table_id = stream_scan.table_id;
1609                let snapshot_epoch = cross_db_snapshot_backfill_info
1610                    .upstream_mv_table_id_to_backfill_epoch
1611                    .get(&table_id)
1612                    .or_else(|| {
1613                        snapshot_backfill_info.and_then(|snapshot_backfill_info| {
1614                            snapshot_backfill_info
1615                                .upstream_mv_table_id_to_backfill_epoch
1616                                .get(&table_id)
1617                        })
1618                    })
1619                    .ok_or_else(|| anyhow!("upstream table id not covered: {}", table_id))?
1620                    .ok_or_else(|| anyhow!("upstream table id not set: {}", table_id))?;
1621                if let Some(prev_snapshot_epoch) =
1622                    stream_scan.snapshot_backfill_epoch.replace(snapshot_epoch)
1623                {
1624                    Err(anyhow!(
1625                        "snapshot backfill epoch set again: {} {} {}",
1626                        table_id,
1627                        prev_snapshot_epoch,
1628                        snapshot_epoch
1629                    ))?;
1630                }
1631                applied = true;
1632            };
1633            result.is_ok()
1634        } else {
1635            true
1636        }
1637    });
1638    result.map_err(MetaError::from).map(|_| applied)
1639}
1640
1641static EMPTY_HASHMAP: LazyLock<HashMap<GlobalFragmentId, StreamFragmentEdge>> =
1642    LazyLock::new(HashMap::new);
1643
1644/// A fragment that is either being built or already exists. Used for generalize the logic of
1645/// [`crate::stream::ActorGraphBuilder`].
1646#[derive(Debug, Clone, EnumAsInner)]
1647pub(super) enum EitherFragment {
1648    /// An internal fragment that is being built for the current streaming job.
1649    Building(BuildingFragment),
1650
1651    /// An existing fragment that is external but connected to the fragments being built.
1652    Existing,
1653}
1654
1655/// A wrapper of [`StreamFragmentGraph`] that contains the additional information of pre-existing
1656/// fragments, which are connected to the graph's top-most or bottom-most fragments.
1657///
1658/// For example,
1659/// - if we're going to build a mview on an existing mview, the upstream fragment containing the
1660///   `Materialize` node will be included in this structure.
1661/// - if we're going to replace the plan of a table with downstream mviews, the downstream fragments
1662///   containing the `StreamScan` nodes will be included in this structure.
1663#[derive(Debug)]
1664pub struct CompleteStreamFragmentGraph {
1665    /// The fragment graph of the streaming job being built.
1666    building_graph: StreamFragmentGraph,
1667
1668    /// The required information of existing fragments.
1669    existing_fragments: HashMap<GlobalFragmentId, Fragment>,
1670
1671    /// Extra edges between existing fragments and the building fragments.
1672    extra_downstreams: HashMap<GlobalFragmentId, HashMap<GlobalFragmentId, StreamFragmentEdge>>,
1673
1674    /// Extra edges between existing fragments and the building fragments.
1675    extra_upstreams: HashMap<GlobalFragmentId, HashMap<GlobalFragmentId, StreamFragmentEdge>>,
1676}
1677
1678pub struct FragmentGraphUpstreamContext {
1679    /// Root fragment is the root of upstream stream graph, which can be a
1680    /// mview fragment or source fragment for cdc source job
1681    pub upstream_root_fragments: HashMap<JobId, Fragment>,
1682}
1683
1684pub struct FragmentGraphDownstreamContext {
1685    pub original_root_fragment_id: FragmentId,
1686    pub downstream_fragments: Vec<(DispatcherType, Fragment)>,
1687}
1688
1689impl CompleteStreamFragmentGraph {
1690    /// Create a new [`CompleteStreamFragmentGraph`] with empty existing fragments, i.e., there's no
1691    /// upstream mviews.
1692    #[cfg(test)]
1693    pub fn for_test(graph: StreamFragmentGraph) -> Self {
1694        Self {
1695            building_graph: graph,
1696            existing_fragments: Default::default(),
1697            extra_downstreams: Default::default(),
1698            extra_upstreams: Default::default(),
1699        }
1700    }
1701
1702    /// Create a new [`CompleteStreamFragmentGraph`] for newly created job (which has no downstreams).
1703    /// e.g., MV on MV and CDC/Source Table with the upstream existing
1704    /// `Materialize` or `Source` fragments.
1705    pub fn with_upstreams(
1706        graph: StreamFragmentGraph,
1707        upstream_context: FragmentGraphUpstreamContext,
1708        job_type: StreamingJobType,
1709    ) -> MetaResult<Self> {
1710        Self::build_helper(graph, Some(upstream_context), None, job_type)
1711    }
1712
1713    /// Create a new [`CompleteStreamFragmentGraph`] for replacing an existing table/source,
1714    /// with the downstream existing `StreamScan`/`StreamSourceScan` fragments.
1715    pub fn with_downstreams(
1716        graph: StreamFragmentGraph,
1717        downstream_context: FragmentGraphDownstreamContext,
1718        job_type: StreamingJobType,
1719    ) -> MetaResult<Self> {
1720        Self::build_helper(graph, None, Some(downstream_context), job_type)
1721    }
1722
1723    /// For replacing an existing table based on shared cdc source, which has both upstreams and downstreams.
1724    pub fn with_upstreams_and_downstreams(
1725        graph: StreamFragmentGraph,
1726        upstream_context: FragmentGraphUpstreamContext,
1727        downstream_context: FragmentGraphDownstreamContext,
1728        job_type: StreamingJobType,
1729    ) -> MetaResult<Self> {
1730        Self::build_helper(
1731            graph,
1732            Some(upstream_context),
1733            Some(downstream_context),
1734            job_type,
1735        )
1736    }
1737
1738    /// The core logic of building a [`CompleteStreamFragmentGraph`], i.e., adding extra upstream/downstream fragments.
1739    fn build_helper(
1740        mut graph: StreamFragmentGraph,
1741        upstream_ctx: Option<FragmentGraphUpstreamContext>,
1742        downstream_ctx: Option<FragmentGraphDownstreamContext>,
1743        job_type: StreamingJobType,
1744    ) -> MetaResult<Self> {
1745        let mut extra_downstreams = HashMap::new();
1746        let mut extra_upstreams = HashMap::new();
1747        let mut existing_fragments = HashMap::new();
1748
1749        if let Some(FragmentGraphUpstreamContext {
1750            upstream_root_fragments,
1751        }) = upstream_ctx
1752        {
1753            for (&id, fragment) in &mut graph.fragments {
1754                let uses_shuffled_backfill = fragment.has_shuffled_backfill();
1755
1756                for (&upstream_job_id, required_columns) in &fragment.upstream_job_columns {
1757                    let upstream_fragment = upstream_root_fragments
1758                        .get(&upstream_job_id)
1759                        .context("upstream fragment not found")?;
1760                    let upstream_root_fragment_id =
1761                        GlobalFragmentId::new(upstream_fragment.fragment_id);
1762
1763                    let edge = match job_type {
1764                        StreamingJobType::Table(TableJobType::SharedCdcSource) => {
1765                            // we traverse all fragments in the graph, and we should find out the
1766                            // CdcFilter fragment and add an edge between upstream source fragment and it.
1767                            assert_ne!(
1768                                (fragment.fragment_type_mask & FragmentTypeFlag::CdcFilter as u32),
1769                                0
1770                            );
1771
1772                            tracing::debug!(
1773                                ?upstream_root_fragment_id,
1774                                ?required_columns,
1775                                identity = ?fragment.inner.get_node().unwrap().get_identity(),
1776                                current_frag_id=?id,
1777                                "CdcFilter with upstream source fragment"
1778                            );
1779
1780                            StreamFragmentEdge {
1781                                id: EdgeId::UpstreamExternal {
1782                                    upstream_job_id,
1783                                    downstream_fragment_id: id,
1784                                },
1785                                // We always use `NoShuffle` for the exchange between the upstream
1786                                // `Source` and the downstream `StreamScan` of the new cdc table.
1787                                dispatch_strategy: DispatchStrategy {
1788                                    r#type: DispatcherType::NoShuffle as _,
1789                                    dist_key_indices: vec![], // not used for `NoShuffle`
1790                                    output_mapping: DispatchOutputMapping::identical(
1791                                        CDC_SOURCE_COLUMN_NUM as _,
1792                                    )
1793                                    .into(),
1794                                },
1795                            }
1796                        }
1797
1798                        // handle MV on MV/Source
1799                        StreamingJobType::MaterializedView
1800                        | StreamingJobType::Sink
1801                        | StreamingJobType::Index => {
1802                            // Build the extra edges between the upstream `Materialize` and
1803                            // the downstream `StreamScan` of the new job.
1804                            if upstream_fragment
1805                                .fragment_type_mask
1806                                .contains(FragmentTypeFlag::Mview)
1807                            {
1808                                // Resolve the required output columns from the upstream materialized view.
1809                                let (dist_key_indices, output_mapping) = {
1810                                    let mview_node = upstream_fragment
1811                                        .nodes
1812                                        .get_node_body()
1813                                        .unwrap()
1814                                        .as_materialize()
1815                                        .unwrap();
1816                                    let all_columns = mview_node.column_descs();
1817                                    let dist_key_indices = mview_node.dist_key_indices();
1818                                    let output_mapping = gen_output_mapping(
1819                                        required_columns,
1820                                        &all_columns,
1821                                    )
1822                                    .context(
1823                                        "BUG: column not found in the upstream materialized view",
1824                                    )?;
1825                                    (dist_key_indices, output_mapping)
1826                                };
1827                                let dispatch_strategy = mv_on_mv_dispatch_strategy(
1828                                    uses_shuffled_backfill,
1829                                    dist_key_indices,
1830                                    output_mapping,
1831                                );
1832
1833                                StreamFragmentEdge {
1834                                    id: EdgeId::UpstreamExternal {
1835                                        upstream_job_id,
1836                                        downstream_fragment_id: id,
1837                                    },
1838                                    dispatch_strategy,
1839                                }
1840                            }
1841                            // Build the extra edges between the upstream `Source` and
1842                            // the downstream `SourceBackfill` of the new job.
1843                            else if upstream_fragment
1844                                .fragment_type_mask
1845                                .contains(FragmentTypeFlag::Source)
1846                            {
1847                                let output_mapping = {
1848                                    let source_node = upstream_fragment
1849                                        .nodes
1850                                        .get_node_body()
1851                                        .unwrap()
1852                                        .as_source()
1853                                        .unwrap();
1854
1855                                    let all_columns = source_node.column_descs().unwrap();
1856                                    gen_output_mapping(required_columns, &all_columns).context(
1857                                        "BUG: column not found in the upstream source node",
1858                                    )?
1859                                };
1860
1861                                StreamFragmentEdge {
1862                                    id: EdgeId::UpstreamExternal {
1863                                        upstream_job_id,
1864                                        downstream_fragment_id: id,
1865                                    },
1866                                    // We always use `NoShuffle` for the exchange between the upstream
1867                                    // `Source` and the downstream `StreamScan` of the new MV.
1868                                    dispatch_strategy: DispatchStrategy {
1869                                        r#type: DispatcherType::NoShuffle as _,
1870                                        dist_key_indices: vec![], // not used for `NoShuffle`
1871                                        output_mapping: Some(output_mapping),
1872                                    },
1873                                }
1874                            } else {
1875                                bail!(
1876                                    "the upstream fragment should be a MView or Source, got fragment type: {:b}",
1877                                    upstream_fragment.fragment_type_mask
1878                                )
1879                            }
1880                        }
1881                        StreamingJobType::Source | StreamingJobType::Table(_) => {
1882                            bail!(
1883                                "the streaming job shouldn't have an upstream fragment, job_type: {:?}",
1884                                job_type
1885                            )
1886                        }
1887                    };
1888
1889                    // put the edge into the extra edges
1890                    extra_downstreams
1891                        .entry(upstream_root_fragment_id)
1892                        .or_insert_with(HashMap::new)
1893                        .try_insert(id, edge.clone())
1894                        .unwrap();
1895                    extra_upstreams
1896                        .entry(id)
1897                        .or_insert_with(HashMap::new)
1898                        .try_insert(upstream_root_fragment_id, edge)
1899                        .unwrap();
1900                }
1901            }
1902
1903            existing_fragments.extend(
1904                upstream_root_fragments
1905                    .into_values()
1906                    .map(|f| (GlobalFragmentId::new(f.fragment_id), f)),
1907            );
1908        }
1909
1910        if let Some(FragmentGraphDownstreamContext {
1911            original_root_fragment_id,
1912            downstream_fragments,
1913        }) = downstream_ctx
1914        {
1915            let original_table_fragment_id = GlobalFragmentId::new(original_root_fragment_id);
1916            let table_fragment_id = GlobalFragmentId::new(graph.table_fragment_id());
1917
1918            // Build the extra edges between the `Materialize` and the downstream `StreamScan` of the
1919            // existing materialized views.
1920            for (dispatcher_type, fragment) in &downstream_fragments {
1921                let id = GlobalFragmentId::new(fragment.fragment_id);
1922
1923                // Similar to `extract_upstream_columns_except_cross_db_backfill`.
1924                let output_columns = {
1925                    let mut res = None;
1926
1927                    stream_graph_visitor::visit_stream_node_body(&fragment.nodes, |node_body| {
1928                        let columns = match node_body {
1929                            NodeBody::StreamScan(stream_scan) => stream_scan.upstream_columns(),
1930                            NodeBody::SourceBackfill(source_backfill) => {
1931                                // FIXME: only pass required columns instead of all columns here
1932                                source_backfill.column_descs()
1933                            }
1934                            _ => return,
1935                        };
1936                        res = Some(columns);
1937                    });
1938
1939                    res.context("failed to locate downstream scan")?
1940                };
1941
1942                let table_fragment = graph.fragments.get(&table_fragment_id).unwrap();
1943                let nodes = table_fragment.node.as_ref().unwrap();
1944
1945                let (dist_key_indices, output_mapping) = match job_type {
1946                    StreamingJobType::Table(_) | StreamingJobType::MaterializedView => {
1947                        let mview_node = nodes.get_node_body().unwrap().as_materialize().unwrap();
1948                        let all_columns = mview_node.column_descs();
1949                        let dist_key_indices = mview_node.dist_key_indices();
1950                        let output_mapping = gen_output_mapping(&output_columns, &all_columns)
1951                            .ok_or_else(|| {
1952                                MetaError::invalid_parameter(
1953                                    "unable to drop the column due to \
1954                                     being referenced by downstream materialized views or sinks",
1955                                )
1956                            })?;
1957                        (dist_key_indices, output_mapping)
1958                    }
1959
1960                    StreamingJobType::Source => {
1961                        let source_node = nodes.get_node_body().unwrap().as_source().unwrap();
1962                        let all_columns = source_node.column_descs().unwrap();
1963                        let output_mapping = gen_output_mapping(&output_columns, &all_columns)
1964                            .ok_or_else(|| {
1965                                MetaError::invalid_parameter(
1966                                    "unable to drop the column due to \
1967                                     being referenced by downstream materialized views or sinks",
1968                                )
1969                            })?;
1970                        assert_eq!(*dispatcher_type, DispatcherType::NoShuffle);
1971                        (
1972                            vec![], // not used for `NoShuffle`
1973                            output_mapping,
1974                        )
1975                    }
1976
1977                    _ => bail!("unsupported job type for replacement: {job_type:?}"),
1978                };
1979
1980                let edge = StreamFragmentEdge {
1981                    id: EdgeId::DownstreamExternal(DownstreamExternalEdgeId {
1982                        original_upstream_fragment_id: original_table_fragment_id,
1983                        downstream_fragment_id: id,
1984                    }),
1985                    dispatch_strategy: DispatchStrategy {
1986                        r#type: *dispatcher_type as i32,
1987                        output_mapping: Some(output_mapping),
1988                        dist_key_indices,
1989                    },
1990                };
1991
1992                extra_downstreams
1993                    .entry(table_fragment_id)
1994                    .or_insert_with(HashMap::new)
1995                    .try_insert(id, edge.clone())
1996                    .unwrap();
1997                extra_upstreams
1998                    .entry(id)
1999                    .or_insert_with(HashMap::new)
2000                    .try_insert(table_fragment_id, edge)
2001                    .unwrap();
2002            }
2003
2004            existing_fragments.extend(
2005                downstream_fragments
2006                    .into_iter()
2007                    .map(|(_, f)| (GlobalFragmentId::new(f.fragment_id), f)),
2008            );
2009        }
2010
2011        Ok(Self {
2012            building_graph: graph,
2013            existing_fragments,
2014            extra_downstreams,
2015            extra_upstreams,
2016        })
2017    }
2018}
2019
2020/// Generate the `output_mapping` for [`DispatchStrategy`] from given columns.
2021fn gen_output_mapping(
2022    required_columns: &[PbColumnDesc],
2023    upstream_columns: &[PbColumnDesc],
2024) -> Option<DispatchOutputMapping> {
2025    let len = required_columns.len();
2026    let mut indices = vec![0; len];
2027    let mut types = None;
2028
2029    for (i, r) in required_columns.iter().enumerate() {
2030        let (ui, u) = upstream_columns
2031            .iter()
2032            .find_position(|&u| u.column_id == r.column_id)?;
2033        indices[i] = ui as u32;
2034
2035        // Only if we encounter type change (`ALTER TABLE ALTER COLUMN TYPE`) will we generate a
2036        // non-empty `types`.
2037        if u.column_type != r.column_type {
2038            types.get_or_insert_with(|| vec![TypePair::default(); len])[i] = TypePair {
2039                upstream: u.column_type.clone(),
2040                downstream: r.column_type.clone(),
2041            };
2042        }
2043    }
2044
2045    // If there's no type change, indicate it by empty `types`.
2046    let types = types.unwrap_or(Vec::new());
2047
2048    Some(DispatchOutputMapping { indices, types })
2049}
2050
2051fn mv_on_mv_dispatch_strategy(
2052    uses_shuffled_backfill: bool,
2053    dist_key_indices: Vec<u32>,
2054    output_mapping: DispatchOutputMapping,
2055) -> DispatchStrategy {
2056    if uses_shuffled_backfill {
2057        if !dist_key_indices.is_empty() {
2058            DispatchStrategy {
2059                r#type: DispatcherType::Hash as _,
2060                dist_key_indices,
2061                output_mapping: Some(output_mapping),
2062            }
2063        } else {
2064            DispatchStrategy {
2065                r#type: DispatcherType::Simple as _,
2066                dist_key_indices: vec![], // empty for Simple
2067                output_mapping: Some(output_mapping),
2068            }
2069        }
2070    } else {
2071        DispatchStrategy {
2072            r#type: DispatcherType::NoShuffle as _,
2073            dist_key_indices: vec![], // not used for `NoShuffle`
2074            output_mapping: Some(output_mapping),
2075        }
2076    }
2077}
2078
2079impl CompleteStreamFragmentGraph {
2080    /// Returns **all** fragment IDs in the complete graph, including the ones that are not in the
2081    /// building graph.
2082    pub(super) fn all_fragment_ids(&self) -> impl Iterator<Item = GlobalFragmentId> + '_ {
2083        self.building_graph
2084            .fragments
2085            .keys()
2086            .chain(self.existing_fragments.keys())
2087            .copied()
2088    }
2089
2090    /// Returns an iterator of **all** edges in the complete graph, including the external edges.
2091    pub(super) fn all_edges(
2092        &self,
2093    ) -> impl Iterator<Item = (GlobalFragmentId, GlobalFragmentId, &StreamFragmentEdge)> + '_ {
2094        self.building_graph
2095            .downstreams
2096            .iter()
2097            .chain(self.extra_downstreams.iter())
2098            .flat_map(|(&from, tos)| tos.iter().map(move |(&to, edge)| (from, to, edge)))
2099    }
2100
2101    /// Returns the distribution of the existing fragments.
2102    pub(super) fn existing_distribution(&self) -> HashMap<GlobalFragmentId, Distribution> {
2103        self.existing_fragments
2104            .iter()
2105            .map(|(&id, f)| (id, Distribution::from_fragment(f)))
2106            .collect()
2107    }
2108
2109    /// Generate topological order of **all** fragments in this graph, including the ones that are
2110    /// not in the building graph. Returns error if the graph is not a DAG and topological sort can
2111    /// not be done.
2112    ///
2113    /// For MV on MV, the first fragment popped out from the heap will be the top-most node, or the
2114    /// `Sink` / `Materialize` in stream graph.
2115    pub(super) fn topo_order(&self) -> MetaResult<Vec<GlobalFragmentId>> {
2116        let mut topo = Vec::new();
2117        let mut downstream_cnts = HashMap::new();
2118
2119        // Iterate all fragments.
2120        for fragment_id in self.all_fragment_ids() {
2121            // Count how many downstreams we have for a given fragment.
2122            let downstream_cnt = self.get_downstreams(fragment_id).count();
2123            if downstream_cnt == 0 {
2124                topo.push(fragment_id);
2125            } else {
2126                downstream_cnts.insert(fragment_id, downstream_cnt);
2127            }
2128        }
2129
2130        let mut i = 0;
2131        while let Some(&fragment_id) = topo.get(i) {
2132            i += 1;
2133            // Find if we can process more fragments.
2134            for (upstream_job_id, _) in self.get_upstreams(fragment_id) {
2135                let downstream_cnt = downstream_cnts.get_mut(&upstream_job_id).unwrap();
2136                *downstream_cnt -= 1;
2137                if *downstream_cnt == 0 {
2138                    downstream_cnts.remove(&upstream_job_id);
2139                    topo.push(upstream_job_id);
2140                }
2141            }
2142        }
2143
2144        if !downstream_cnts.is_empty() {
2145            // There are fragments that are not processed yet.
2146            bail!("graph is not a DAG");
2147        }
2148
2149        Ok(topo)
2150    }
2151
2152    /// Seal a [`BuildingFragment`] from the graph into a [`Fragment`], which will be further used
2153    /// to build actors on the compute nodes and persist into meta store.
2154    pub(super) fn seal_fragment(
2155        &self,
2156        id: GlobalFragmentId,
2157        distribution: Distribution,
2158        stream_node: StreamNode,
2159    ) -> Fragment {
2160        let building_fragment = self.get_fragment(id).into_building().unwrap();
2161        let internal_tables = building_fragment.extract_internal_tables();
2162        let BuildingFragment {
2163            inner,
2164            job_id,
2165            upstream_job_columns: _,
2166        } = building_fragment;
2167
2168        let distribution_type = distribution.to_distribution_type();
2169        let vnode_count = distribution.vnode_count();
2170
2171        let materialized_fragment_id =
2172            if FragmentTypeMask::from(inner.fragment_type_mask).contains(FragmentTypeFlag::Mview) {
2173                job_id.map(JobId::as_mv_table_id)
2174            } else {
2175                None
2176            };
2177
2178        let vector_index_fragment_id =
2179            if inner.fragment_type_mask & FragmentTypeFlag::VectorIndexWrite as u32 != 0 {
2180                job_id.map(JobId::as_mv_table_id)
2181            } else {
2182                None
2183            };
2184
2185        let state_table_ids = internal_tables
2186            .iter()
2187            .map(|t| t.id)
2188            .chain(materialized_fragment_id)
2189            .chain(vector_index_fragment_id)
2190            .collect();
2191
2192        Fragment {
2193            fragment_id: inner.fragment_id,
2194            fragment_type_mask: inner.fragment_type_mask.into(),
2195            distribution_type,
2196            state_table_ids,
2197            maybe_vnode_count: VnodeCount::set(vnode_count).to_protobuf(),
2198            nodes: stream_node,
2199        }
2200    }
2201
2202    /// Get a fragment from the complete graph, which can be either a building fragment or an
2203    /// existing fragment.
2204    pub(super) fn get_fragment(&self, fragment_id: GlobalFragmentId) -> EitherFragment {
2205        if self.existing_fragments.contains_key(&fragment_id) {
2206            EitherFragment::Existing
2207        } else {
2208            EitherFragment::Building(
2209                self.building_graph
2210                    .fragments
2211                    .get(&fragment_id)
2212                    .unwrap()
2213                    .clone(),
2214            )
2215        }
2216    }
2217
2218    /// Get **all** downstreams of a fragment, including the ones that are not in the building
2219    /// graph.
2220    pub(super) fn get_downstreams(
2221        &self,
2222        fragment_id: GlobalFragmentId,
2223    ) -> impl Iterator<Item = (GlobalFragmentId, &StreamFragmentEdge)> {
2224        self.building_graph
2225            .get_downstreams(fragment_id)
2226            .iter()
2227            .chain(
2228                self.extra_downstreams
2229                    .get(&fragment_id)
2230                    .into_iter()
2231                    .flatten(),
2232            )
2233            .map(|(&id, edge)| (id, edge))
2234    }
2235
2236    /// Get **all** upstreams of a fragment, including the ones that are not in the building
2237    /// graph.
2238    pub(super) fn get_upstreams(
2239        &self,
2240        fragment_id: GlobalFragmentId,
2241    ) -> impl Iterator<Item = (GlobalFragmentId, &StreamFragmentEdge)> {
2242        self.building_graph
2243            .get_upstreams(fragment_id)
2244            .iter()
2245            .chain(self.extra_upstreams.get(&fragment_id).into_iter().flatten())
2246            .map(|(&id, edge)| (id, edge))
2247    }
2248
2249    /// Returns all building fragments in the graph.
2250    pub(super) fn building_fragments(&self) -> &HashMap<GlobalFragmentId, BuildingFragment> {
2251        &self.building_graph.fragments
2252    }
2253
2254    /// Returns all building fragments in the graph, mutable.
2255    pub(super) fn building_fragments_mut(
2256        &mut self,
2257    ) -> &mut HashMap<GlobalFragmentId, BuildingFragment> {
2258        &mut self.building_graph.fragments
2259    }
2260
2261    /// Get the expected vnode count of the building graph. See documentation of the field for more details.
2262    pub(super) fn max_parallelism(&self) -> usize {
2263        self.building_graph.max_parallelism()
2264    }
2265}
2266
2267#[cfg(test)]
2268mod tests {
2269    use risingwave_common::catalog::{ColumnDesc, ColumnId};
2270    use risingwave_common::types::DataType;
2271    use risingwave_pb::catalog::SinkType as PbSinkType;
2272    use risingwave_pb::meta::table_fragments::fragment::PbFragmentDistributionType;
2273    use risingwave_pb::plan_common::StorageTableDesc;
2274    use risingwave_pb::stream_plan::{
2275        BatchPlanNode, MergeNode, ProjectNode, SinkDesc, SinkLogStoreType, SinkNode, StreamNode,
2276        StreamScanNode, StreamScanType,
2277    };
2278
2279    use super::*;
2280
2281    fn make_column(name: &str, id: i32, data_type: DataType) -> ColumnCatalog {
2282        ColumnCatalog::visible(ColumnDesc::named(name, ColumnId::new(id), data_type))
2283    }
2284
2285    fn make_field(table_name: &str, column: &ColumnCatalog) -> risingwave_pb::plan_common::Field {
2286        Field::new(
2287            format!("{}.{}", table_name, column.column_desc.name),
2288            column.data_type().clone(),
2289        )
2290        .to_prost()
2291    }
2292
2293    fn make_input_ref(index: u32, data_type: &DataType) -> PbExprNode {
2294        PbExprNode {
2295            function_type: expr_node::Type::Unspecified as i32,
2296            return_type: Some(data_type.to_protobuf()),
2297            rex_node: Some(expr_node::RexNode::InputRef(index)),
2298        }
2299    }
2300
2301    fn make_stream_scan_node(
2302        table_name: &str,
2303        table_id: u32,
2304        columns: &[ColumnCatalog],
2305    ) -> StreamNode {
2306        let merge_node = StreamNode {
2307            node_body: Some(NodeBody::Merge(Box::new(MergeNode {
2308                upstream_fragment_id: 0.into(),
2309                ..Default::default()
2310            }))),
2311            fields: columns
2312                .iter()
2313                .map(|col| make_field(table_name, col))
2314                .collect(),
2315            ..Default::default()
2316        };
2317        let batch_plan_node = StreamNode {
2318            node_body: Some(NodeBody::BatchPlan(Box::new(BatchPlanNode {
2319                ..Default::default()
2320            }))),
2321            ..Default::default()
2322        };
2323        let stream_scan_node = StreamScanNode {
2324            table_id: table_id.into(),
2325            upstream_column_ids: columns.iter().map(|c| c.column_id().get_id()).collect(),
2326            output_indices: (0..columns.len()).map(|i| i as u32).collect(),
2327            stream_scan_type: StreamScanType::ArrangementBackfill as i32,
2328            table_desc: Some(StorageTableDesc {
2329                table_id: table_id.into(),
2330                columns: columns
2331                    .iter()
2332                    .map(|col| col.column_desc.to_protobuf())
2333                    .collect(),
2334                value_indices: (0..columns.len()).map(|i| i as u32).collect(),
2335                versioned: true,
2336                ..Default::default()
2337            }),
2338            ..Default::default()
2339        };
2340        StreamNode {
2341            node_body: Some(NodeBody::StreamScan(Box::new(stream_scan_node))),
2342            fields: columns
2343                .iter()
2344                .map(|col| make_field(table_name, col))
2345                .collect(),
2346            input: vec![merge_node, batch_plan_node],
2347            ..Default::default()
2348        }
2349    }
2350
2351    fn make_project_node(
2352        table_name: &str,
2353        columns: &[ColumnCatalog],
2354        input: StreamNode,
2355    ) -> StreamNode {
2356        let select_list = columns
2357            .iter()
2358            .enumerate()
2359            .map(|(i, col)| make_input_ref(i as u32, col.data_type()))
2360            .collect();
2361        StreamNode {
2362            node_body: Some(NodeBody::Project(Box::new(ProjectNode {
2363                select_list,
2364                ..Default::default()
2365            }))),
2366            fields: columns
2367                .iter()
2368                .map(|col| make_field(table_name, col))
2369                .collect(),
2370            input: vec![input],
2371            ..Default::default()
2372        }
2373    }
2374
2375    #[tokio::test]
2376    async fn test_rewrite_refresh_schema_sink_fragment_with_project() {
2377        let env = MetaSrvEnv::for_test().await;
2378        let id_gen_manager = env.id_gen_manager().as_ref();
2379
2380        let table_name = "t";
2381        let columns = vec![
2382            make_column("a", 1, DataType::Int64),
2383            make_column("b", 2, DataType::Int64),
2384        ];
2385        let new_column = make_column("c", 3, DataType::Varchar);
2386
2387        let mut upstream_columns = columns.clone();
2388        upstream_columns.push(new_column.clone());
2389        let upstream_table = PbTable {
2390            name: table_name.to_owned(),
2391            columns: upstream_columns
2392                .iter()
2393                .map(|col| col.to_protobuf())
2394                .collect(),
2395            ..Default::default()
2396        };
2397
2398        let sink = PbSink {
2399            columns: columns.iter().map(|col| col.to_protobuf()).collect(),
2400            sink_type: PbSinkType::AppendOnly as i32,
2401            ..Default::default()
2402        };
2403
2404        let sink_desc = SinkDesc {
2405            sink_type: PbSinkType::AppendOnly as i32,
2406            column_catalogs: sink.columns.clone(),
2407            ..Default::default()
2408        };
2409
2410        let stream_scan_node = make_stream_scan_node(table_name, 1, &columns);
2411        let project_node = make_project_node(table_name, &columns, stream_scan_node);
2412
2413        let log_store_table = PbTable {
2414            columns: columns
2415                .iter()
2416                .cloned()
2417                .map(|mut col| {
2418                    col.column_desc.name = format!("{}_{}", table_name, col.column_desc.name);
2419                    col.to_protobuf()
2420                })
2421                .collect(),
2422            value_indices: (0..columns.len()).map(|i| i as i32).collect(),
2423            ..Default::default()
2424        };
2425
2426        let original_fragment = Fragment {
2427            fragment_id: 1.into(),
2428            fragment_type_mask: FragmentTypeMask::default(),
2429            distribution_type: PbFragmentDistributionType::Single,
2430            state_table_ids: vec![],
2431            maybe_vnode_count: None,
2432            nodes: StreamNode {
2433                node_body: Some(NodeBody::Sink(Box::new(SinkNode {
2434                    sink_desc: Some(sink_desc),
2435                    table: Some(log_store_table),
2436                    ..Default::default()
2437                }))),
2438                fields: columns
2439                    .iter()
2440                    .map(|col| make_field(table_name, col))
2441                    .collect(),
2442                input: vec![project_node],
2443                ..Default::default()
2444            },
2445        };
2446
2447        let (new_fragment, _, _) = rewrite_refresh_schema_sink_fragment(
2448            &original_fragment,
2449            &sink,
2450            std::slice::from_ref(&new_column),
2451            &[],
2452            &upstream_table,
2453            7.into(),
2454            id_gen_manager,
2455        )
2456        .unwrap();
2457
2458        let sink_node = &new_fragment.nodes;
2459        let [project_node] = sink_node.input.as_slice() else {
2460            panic!("Sink has more than 1 input: {:?}", sink_node.input);
2461        };
2462        let PbNodeBody::Project(project_body) = project_node.node_body.as_ref().unwrap() else {
2463            panic!(
2464                "expect PbNodeBody::Project but got: {:?}",
2465                project_node.node_body
2466            );
2467        };
2468        assert_eq!(project_body.select_list.len(), columns.len() + 1);
2469        let last_expr = project_body.select_list.last().unwrap();
2470        assert!(
2471            matches!(last_expr.rex_node, Some(expr_node::RexNode::InputRef(idx)) if idx == columns.len() as u32)
2472        );
2473        assert_eq!(project_node.fields.len(), columns.len() + 1);
2474
2475        let [stream_scan_node] = project_node.input.as_slice() else {
2476            panic!("Project has more than 1 input: {:?}", project_node.input);
2477        };
2478        let PbNodeBody::StreamScan(scan) = stream_scan_node.node_body.as_ref().unwrap() else {
2479            panic!(
2480                "expect PbNodeBody::StreamScan but got: {:?}",
2481                stream_scan_node.node_body
2482            );
2483        };
2484        assert_eq!(
2485            scan.upstream_column_ids.last().copied(),
2486            Some(new_column.column_id().get_id())
2487        );
2488        assert_eq!(
2489            scan.output_indices.last().copied(),
2490            Some(columns.len() as u32)
2491        );
2492        assert_eq!(
2493            stream_scan_node.fields.last().unwrap().name,
2494            format!("{}.{}", table_name, new_column.column_desc.name)
2495        );
2496    }
2497
2498    #[tokio::test]
2499    async fn test_rewrite_refresh_schema_sink_fragment_drop_column_with_project() {
2500        let env = MetaSrvEnv::for_test().await;
2501        let id_gen_manager = env.id_gen_manager().as_ref();
2502
2503        let table_name = "t";
2504        let columns = vec![
2505            make_column("a", 1, DataType::Int64),
2506            make_column("b", 2, DataType::Int64),
2507            make_column("tmp", 3, DataType::Varchar),
2508        ];
2509        let removed_column = columns.last().unwrap().clone();
2510        let upstream_columns = columns[..2].to_vec();
2511
2512        let upstream_table = PbTable {
2513            name: table_name.to_owned(),
2514            columns: upstream_columns
2515                .iter()
2516                .map(|col| col.to_protobuf())
2517                .collect(),
2518            ..Default::default()
2519        };
2520
2521        let sink = PbSink {
2522            columns: columns.iter().map(|col| col.to_protobuf()).collect(),
2523            sink_type: PbSinkType::AppendOnly as i32,
2524            ..Default::default()
2525        };
2526
2527        let sink_desc = SinkDesc {
2528            sink_type: PbSinkType::AppendOnly as i32,
2529            column_catalogs: sink.columns.clone(),
2530            ..Default::default()
2531        };
2532
2533        let stream_scan_node = make_stream_scan_node(table_name, 1, &columns);
2534        let project_node = make_project_node(table_name, &columns, stream_scan_node);
2535
2536        let log_store_table = PbTable {
2537            columns: columns
2538                .iter()
2539                .cloned()
2540                .map(|mut col| {
2541                    col.column_desc.name = format!("{}_{}", table_name, col.column_desc.name);
2542                    col.to_protobuf()
2543                })
2544                .collect(),
2545            value_indices: (0..columns.len()).map(|i| i as i32).collect(),
2546            ..Default::default()
2547        };
2548
2549        let original_fragment = Fragment {
2550            fragment_id: 1.into(),
2551            fragment_type_mask: FragmentTypeMask::default(),
2552            distribution_type: PbFragmentDistributionType::Single,
2553            state_table_ids: vec![],
2554            maybe_vnode_count: None,
2555            nodes: StreamNode {
2556                node_body: Some(NodeBody::Sink(Box::new(SinkNode {
2557                    sink_desc: Some(sink_desc),
2558                    table: Some(log_store_table),
2559                    log_store_type: SinkLogStoreType::KvLogStore as i32,
2560                    ..Default::default()
2561                }))),
2562                fields: columns
2563                    .iter()
2564                    .map(|col| make_field(table_name, col))
2565                    .collect(),
2566                input: vec![project_node],
2567                ..Default::default()
2568            },
2569        };
2570
2571        let (new_fragment, new_schema, new_log_store_table) = rewrite_refresh_schema_sink_fragment(
2572            &original_fragment,
2573            &sink,
2574            &[],
2575            std::slice::from_ref(&removed_column),
2576            &upstream_table,
2577            7.into(),
2578            id_gen_manager,
2579        )
2580        .unwrap();
2581
2582        assert_eq!(new_schema.len(), 2);
2583        assert!(
2584            new_schema.iter().all(|col| {
2585                col.column_desc.as_ref().map(|desc| desc.name.as_str()) != Some("tmp")
2586            })
2587        );
2588
2589        let sink_node = &new_fragment.nodes;
2590        let [project_node] = sink_node.input.as_slice() else {
2591            panic!("Sink has more than 1 input: {:?}", sink_node.input);
2592        };
2593        let PbNodeBody::Project(project_body) = project_node.node_body.as_ref().unwrap() else {
2594            panic!(
2595                "expect PbNodeBody::Project but got: {:?}",
2596                project_node.node_body
2597            );
2598        };
2599        assert_eq!(project_body.select_list.len(), 2);
2600        assert!(project_node.fields.iter().all(|f| !f.name.contains("tmp")));
2601
2602        let [stream_scan_node] = project_node.input.as_slice() else {
2603            panic!("Project has more than 1 input: {:?}", project_node.input);
2604        };
2605        let PbNodeBody::StreamScan(scan) = stream_scan_node.node_body.as_ref().unwrap() else {
2606            panic!(
2607                "expect PbNodeBody::StreamScan but got: {:?}",
2608                stream_scan_node.node_body
2609            );
2610        };
2611        assert!(
2612            !scan
2613                .upstream_column_ids
2614                .iter()
2615                .any(|&id| id == removed_column.column_id().get_id())
2616        );
2617        assert!(
2618            stream_scan_node
2619                .fields
2620                .iter()
2621                .all(|f| !f.name.contains("tmp"))
2622        );
2623
2624        let new_log_store_table = new_log_store_table.expect("log store table should be updated");
2625        assert!(
2626            new_log_store_table.columns.iter().all(|col| !col
2627                .column_desc
2628                .as_ref()
2629                .unwrap()
2630                .name
2631                .contains("tmp"))
2632        );
2633        assert_eq!(
2634            new_log_store_table.value_indices,
2635            (0..new_log_store_table.columns.len() as i32).collect::<Vec<_>>()
2636        );
2637    }
2638}