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