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