Skip to main content

risingwave_meta/rpc/
ddl_controller.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::cmp::Ordering;
16use std::collections::{HashMap, HashSet};
17use std::num::NonZeroUsize;
18use std::sync::Arc;
19use std::sync::atomic::AtomicU64;
20use std::time::Duration;
21
22use anyhow::{Context, anyhow};
23use await_tree::InstrumentAwait;
24use either::Either;
25use itertools::Itertools;
26use risingwave_common::catalog::{
27    AlterDatabaseParam, ColumnCatalog, ColumnId, Field, FragmentTypeFlag,
28};
29use risingwave_common::hash::VnodeCountCompat;
30use risingwave_common::id::{JobId, TableId};
31use risingwave_common::secret::{LocalSecretManager, SecretEncryption};
32use risingwave_common::system_param::adaptive_parallelism_strategy::parse_strategy;
33use risingwave_common::system_param::reader::SystemParamsRead;
34use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont_mut;
35use risingwave_common::{bail, bail_not_implemented};
36use risingwave_connector::WithOptionsSecResolved;
37use risingwave_connector::connector_common::validate_connection;
38use risingwave_connector::sink::SinkParam;
39use risingwave_connector::sink::iceberg::IcebergSink;
40use risingwave_connector::source::cdc::CdcScanOptions;
41use risingwave_connector::source::{
42    ConnectorProperties, SourceEnumeratorContext, UPSTREAM_SOURCE_KEY,
43};
44use risingwave_meta_model::object::ObjectType;
45use risingwave_meta_model::refresh_job::RefreshState;
46use risingwave_meta_model::{
47    ConnectionId, DatabaseId, DispatcherType, FragmentId, FunctionId, IndexId, JobStatus, ObjectId,
48    SchemaId, SecretId, SinkId, SourceId, StreamingParallelism, SubscriptionId, UserId, ViewId,
49    streaming_job,
50};
51use risingwave_pb::catalog::{
52    Comment, Connection, CreateType, Database, Function, PbTable, Schema, Secret, Source,
53    Subscription, Table, View,
54};
55use risingwave_pb::ddl_service::alter_owner_request::Object;
56use risingwave_pb::ddl_service::{
57    DdlProgress, TableJobType, WaitVersion, alter_name_request, alter_set_schema_request,
58    alter_swap_rename_request, streaming_job_resource_type,
59};
60use risingwave_pb::meta::table_fragments::fragment::FragmentDistributionType as PbFragmentDistributionType;
61use risingwave_pb::plan_common::{PbColumnCatalog, PbExternalTableDesc};
62use risingwave_pb::stream_plan::stream_node::NodeBody;
63use risingwave_pb::stream_plan::{
64    PbDispatchOutputMapping, PbStreamFragmentGraph, PbStreamNode, PbUpstreamSinkInfo,
65    StreamFragmentGraph as StreamFragmentGraphProto,
66};
67use risingwave_pb::telemetry::{PbTelemetryDatabaseObject, PbTelemetryEventStage};
68use strum::Display;
69use thiserror_ext::AsReport;
70use tokio::sync::Semaphore;
71use tokio::time::sleep;
72use tracing::Instrument;
73
74use crate::barrier::{BarrierManagerRef, Command};
75use crate::controller::catalog::{DropTableConnectorContext, ReleaseContext};
76use crate::controller::streaming_job::{FinishAutoRefreshSchemaSinkContext, SinkIntoTableContext};
77use crate::controller::utils::build_select_node_list;
78use crate::error::{MetaErrorInner, bail_invalid_parameter};
79use crate::manager::iceberg_pk_index_sink::IcebergPkIndexSinkManager;
80use crate::manager::sink_coordination::SinkCoordinatorManager;
81use crate::manager::{
82    IGNORED_NOTIFICATION_VERSION, LocalNotification, MetaSrvEnv, MetadataManager,
83    NotificationVersion, StreamingJob, StreamingJobType,
84};
85use crate::model::{
86    DownstreamFragmentRelation, FragmentDownstreamRelation, FragmentId as CatalogFragmentId,
87    StreamContext, StreamJobFragments, StreamJobFragmentsToCreate,
88};
89use crate::stream::cdc::{
90    parallel_cdc_table_backfill_fragment, try_init_parallel_cdc_table_snapshot_splits,
91};
92use crate::stream::{
93    ActorGraphBuildResult, ActorGraphBuilder, AutoRefreshSchemaSinkContext,
94    CompleteStreamFragmentGraph, CreateStreamingJobContext, CreateStreamingJobOption,
95    FragmentGraphDownstreamContext, FragmentGraphUpstreamContext, GlobalStreamManagerRef,
96    ParallelismPolicy, ReplaceStreamJobContext, ReschedulePolicy, SourceChange, SourceManagerRef,
97    StreamFragmentGraph, UpstreamSinkInfo, check_sink_fragments_support_refresh_schema,
98    cleanup_dropped_sink_runtimes, create_source_worker, first_variant_column,
99    rewrite_refresh_schema_sink_fragment, state_match, validate_sink,
100};
101use crate::telemetry::report_event;
102use crate::{MetaError, MetaResult};
103
104#[derive(PartialEq)]
105pub enum DropMode {
106    Restrict,
107    Cascade,
108}
109
110impl DropMode {
111    pub fn from_request_setting(cascade: bool) -> DropMode {
112        if cascade {
113            DropMode::Cascade
114        } else {
115            DropMode::Restrict
116        }
117    }
118}
119
120#[derive(strum::AsRefStr)]
121pub enum StreamingJobId {
122    MaterializedView(TableId),
123    Sink(SinkId),
124    Table(Option<SourceId>, TableId),
125    Index(IndexId),
126}
127
128impl std::fmt::Display for StreamingJobId {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(f, "{}", self.as_ref())?;
131        write!(f, "({})", self.id())
132    }
133}
134
135impl StreamingJobId {
136    fn id(&self) -> JobId {
137        match self {
138            StreamingJobId::MaterializedView(id) | StreamingJobId::Table(_, id) => id.as_job_id(),
139            StreamingJobId::Index(id) => id.as_job_id(),
140            StreamingJobId::Sink(id) => id.as_job_id(),
141        }
142    }
143}
144
145/// It’s used to describe the information of the job that needs to be replaced
146/// and it will be used during replacing table and creating sink into table operations.
147pub struct ReplaceStreamJobInfo {
148    pub streaming_job: StreamingJob,
149    pub fragment_graph: StreamFragmentGraphProto,
150}
151
152#[derive(Display)]
153pub enum DdlCommand {
154    CreateDatabase(Database),
155    DropDatabase(DatabaseId),
156    CreateSchema(Schema),
157    DropSchema(SchemaId, DropMode),
158    CreateNonSharedSource(Source, Option<TableId>),
159    DropSource(SourceId, DropMode),
160    ResetSource(SourceId),
161    CreateFunction(Function),
162    DropFunction(FunctionId, DropMode),
163    CreateView(View, HashSet<ObjectId>),
164    DropView(ViewId, DropMode),
165    CreateStreamingJob {
166        stream_job: StreamingJob,
167        fragment_graph: StreamFragmentGraphProto,
168        dependencies: HashSet<ObjectId>,
169        resource_type: streaming_job_resource_type::ResourceType,
170        if_not_exists: bool,
171        refresh_interval_sec: Option<u64>,
172        replace_sink: Option<SinkId>,
173        since_timestamp_epoch: Option<u64>,
174    },
175    DropStreamingJob {
176        job_id: StreamingJobId,
177        drop_mode: DropMode,
178    },
179    AlterName(alter_name_request::Object, String),
180    AlterSwapRename(alter_swap_rename_request::Object),
181    ReplaceStreamJob(ReplaceStreamJobInfo),
182    AlterNonSharedSource(Source),
183    AlterObjectOwner(Object, UserId),
184    AlterSetSchema(alter_set_schema_request::Object, SchemaId),
185    CreateConnection(Connection),
186    DropConnection(ConnectionId, DropMode),
187    CreateSecret(Secret),
188    AlterSecret(Secret),
189    DropSecret(SecretId, DropMode),
190    CommentOn(Comment),
191    CreateSubscription(Subscription),
192    DropSubscription(SubscriptionId, DropMode),
193    AlterSubscriptionRetention {
194        subscription_id: SubscriptionId,
195        retention_seconds: u64,
196        definition: String,
197    },
198    AlterDatabaseParam(DatabaseId, AlterDatabaseParam),
199    AlterDatabaseResourceGroup(DatabaseId, Option<String>, bool),
200    AlterStreamingJobConfig(JobId, HashMap<String, String>, Vec<String>),
201}
202
203impl DdlCommand {
204    /// Returns the name or ID of the object that this command operates on, for observability and debugging.
205    fn object(&self) -> Either<String, ObjectId> {
206        use Either::*;
207        match self {
208            DdlCommand::CreateDatabase(database) => Left(database.name.clone()),
209            DdlCommand::DropDatabase(id) => Right(id.as_object_id()),
210            DdlCommand::CreateSchema(schema) => Left(schema.name.clone()),
211            DdlCommand::DropSchema(id, _) => Right(id.as_object_id()),
212            DdlCommand::CreateNonSharedSource(source, _) => Left(source.name.clone()),
213            DdlCommand::DropSource(id, _) => Right(id.as_object_id()),
214            DdlCommand::ResetSource(id) => Right(id.as_object_id()),
215            DdlCommand::CreateFunction(function) => Left(function.name.clone()),
216            DdlCommand::DropFunction(id, _) => Right(id.as_object_id()),
217            DdlCommand::CreateView(view, _) => Left(view.name.clone()),
218            DdlCommand::DropView(id, _) => Right(id.as_object_id()),
219            DdlCommand::CreateStreamingJob { stream_job, .. } => Left(stream_job.name()),
220            DdlCommand::DropStreamingJob { job_id, .. } => Right(job_id.id().as_object_id()),
221            DdlCommand::AlterName(object, _) => Left(format!("{object:?}")),
222            DdlCommand::AlterSwapRename(object) => Left(format!("{object:?}")),
223            DdlCommand::ReplaceStreamJob(info) => Left(info.streaming_job.name()),
224            DdlCommand::AlterNonSharedSource(source) => Left(source.name.clone()),
225            DdlCommand::AlterObjectOwner(object, _) => Left(format!("{object:?}")),
226            DdlCommand::AlterSetSchema(object, _) => Left(format!("{object:?}")),
227            DdlCommand::CreateConnection(connection) => Left(connection.name.clone()),
228            DdlCommand::DropConnection(id, _) => Right(id.as_object_id()),
229            DdlCommand::CreateSecret(secret) => Left(secret.name.clone()),
230            DdlCommand::AlterSecret(secret) => Left(secret.name.clone()),
231            DdlCommand::DropSecret(id, _) => Right(id.as_object_id()),
232            DdlCommand::CommentOn(comment) => Right(comment.table_id.into()),
233            DdlCommand::CreateSubscription(subscription) => Left(subscription.name.clone()),
234            DdlCommand::DropSubscription(id, _) => Right(id.as_object_id()),
235            DdlCommand::AlterSubscriptionRetention {
236                subscription_id, ..
237            } => Right(subscription_id.as_object_id()),
238            DdlCommand::AlterDatabaseParam(id, _) => Right(id.as_object_id()),
239            DdlCommand::AlterDatabaseResourceGroup(id, _, _) => Right(id.as_object_id()),
240            DdlCommand::AlterStreamingJobConfig(job_id, _, _) => Right(job_id.as_object_id()),
241        }
242    }
243
244    fn allow_in_recovery(&self) -> bool {
245        match self {
246            DdlCommand::DropDatabase(_)
247            | DdlCommand::DropSchema(_, _)
248            | DdlCommand::DropSource(_, _)
249            | DdlCommand::DropFunction(_, _)
250            | DdlCommand::DropView(_, _)
251            | DdlCommand::DropStreamingJob { .. }
252            | DdlCommand::DropConnection(_, _)
253            | DdlCommand::DropSecret(_, _)
254            | DdlCommand::DropSubscription(_, _)
255            | DdlCommand::AlterName(_, _)
256            | DdlCommand::AlterObjectOwner(_, _)
257            | DdlCommand::AlterSetSchema(_, _)
258            | DdlCommand::CreateDatabase(_)
259            | DdlCommand::CreateSchema(_)
260            | DdlCommand::CreateFunction(_)
261            | DdlCommand::CreateView(_, _)
262            | DdlCommand::CreateConnection(_)
263            | DdlCommand::CommentOn(_)
264            | DdlCommand::CreateSecret(_)
265            | DdlCommand::AlterSecret(_)
266            | DdlCommand::AlterSwapRename(_)
267            | DdlCommand::AlterDatabaseParam(_, _)
268            | DdlCommand::AlterDatabaseResourceGroup(_, _, _)
269            | DdlCommand::AlterStreamingJobConfig(_, _, _)
270            | DdlCommand::AlterSubscriptionRetention { .. } => true,
271            DdlCommand::CreateStreamingJob { .. }
272            | DdlCommand::CreateNonSharedSource(_, _)
273            | DdlCommand::ReplaceStreamJob(_)
274            | DdlCommand::AlterNonSharedSource(_)
275            | DdlCommand::ResetSource(_)
276            | DdlCommand::CreateSubscription(_) => false,
277        }
278    }
279}
280
281#[derive(Clone)]
282pub struct DdlController {
283    pub(crate) env: MetaSrvEnv,
284
285    pub(crate) metadata_manager: MetadataManager,
286    pub(crate) stream_manager: GlobalStreamManagerRef,
287    pub(crate) source_manager: SourceManagerRef,
288    barrier_manager: BarrierManagerRef,
289    sink_manager: SinkCoordinatorManager,
290    iceberg_pk_index_sink_manager: IcebergPkIndexSinkManager,
291
292    // The semaphore is used to limit the number of concurrent streaming job creation.
293    pub(crate) creating_streaming_job_permits: Arc<CreatingStreamingJobPermit>,
294
295    /// Sequence number for DDL commands, used for observability and debugging.
296    seq: Arc<AtomicU64>,
297}
298
299#[derive(Clone)]
300pub struct CreatingStreamingJobPermit {
301    pub(crate) semaphore: Arc<Semaphore>,
302}
303
304impl CreatingStreamingJobPermit {
305    async fn new(env: &MetaSrvEnv) -> Self {
306        let mut permits = env
307            .system_params_reader()
308            .await
309            .max_concurrent_creating_streaming_jobs() as usize;
310        if permits == 0 {
311            // if the system parameter is set to zero, use the max permitted value.
312            permits = Semaphore::MAX_PERMITS;
313        }
314        let semaphore = Arc::new(Semaphore::new(permits));
315
316        let (local_notification_tx, mut local_notification_rx) =
317            tokio::sync::mpsc::unbounded_channel();
318        env.notification_manager()
319            .insert_local_sender(local_notification_tx);
320        let semaphore_clone = semaphore.clone();
321        tokio::spawn(async move {
322            while let Some(notification) = local_notification_rx.recv().await {
323                let LocalNotification::SystemParamsChange(p) = &notification else {
324                    continue;
325                };
326                let mut new_permits = p.max_concurrent_creating_streaming_jobs() as usize;
327                if new_permits == 0 {
328                    new_permits = Semaphore::MAX_PERMITS;
329                }
330                match permits.cmp(&new_permits) {
331                    Ordering::Less => {
332                        semaphore_clone.add_permits(new_permits - permits);
333                    }
334                    Ordering::Equal => continue,
335                    Ordering::Greater => {
336                        let to_release = permits - new_permits;
337                        let reduced = semaphore_clone.forget_permits(to_release);
338                        // TODO: implement dynamic semaphore with limits by ourself.
339                        if reduced != to_release {
340                            tracing::warn!(
341                                "no enough permits to release, expected {}, but reduced {}",
342                                to_release,
343                                reduced
344                            );
345                        }
346                    }
347                }
348                tracing::info!(
349                    "max_concurrent_creating_streaming_jobs changed from {} to {}",
350                    permits,
351                    new_permits
352                );
353                permits = new_permits;
354            }
355        });
356
357        Self { semaphore }
358    }
359}
360
361impl DdlController {
362    fn validate_specified_parallelism(
363        specified_parallelism: Option<NonZeroUsize>,
364        specified_backfill_parallelism: Option<NonZeroUsize>,
365        max_parallelism: NonZeroUsize,
366    ) -> MetaResult<()> {
367        if let Some(parallelism) = specified_parallelism
368            && parallelism > max_parallelism
369        {
370            bail_invalid_parameter!(
371                "specified parallelism {} should not exceed max parallelism {}",
372                parallelism,
373                max_parallelism,
374            );
375        }
376        if let Some(backfill_parallelism) = specified_backfill_parallelism
377            && backfill_parallelism > max_parallelism
378        {
379            bail_invalid_parameter!(
380                "specified backfill parallelism {} should not exceed max parallelism {}",
381                backfill_parallelism,
382                max_parallelism,
383            );
384        }
385        Ok(())
386    }
387
388    fn validate_serverless_backfill_enabled(
389        &self,
390        resource_type: &streaming_job_resource_type::ResourceType,
391    ) -> MetaResult<()> {
392        if matches!(
393            resource_type,
394            streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
395        ) && self.env.opts.serverless_backfill_controller_addr.is_empty()
396        {
397            bail_invalid_parameter!(
398                "Serverless Backfill is disabled. Use RisingWave cloud at https://cloud.risingwave.com/auth/signup to try this feature"
399            );
400        }
401
402        Ok(())
403    }
404
405    pub async fn new(
406        env: MetaSrvEnv,
407        metadata_manager: MetadataManager,
408        stream_manager: GlobalStreamManagerRef,
409        source_manager: SourceManagerRef,
410        barrier_manager: BarrierManagerRef,
411        sink_manager: SinkCoordinatorManager,
412        iceberg_pk_index_sink_manager: IcebergPkIndexSinkManager,
413    ) -> Self {
414        let creating_streaming_job_permits = Arc::new(CreatingStreamingJobPermit::new(&env).await);
415        Self {
416            env,
417            metadata_manager,
418            stream_manager,
419            source_manager,
420            barrier_manager,
421            sink_manager,
422            iceberg_pk_index_sink_manager,
423            creating_streaming_job_permits,
424            seq: Arc::new(AtomicU64::new(0)),
425        }
426    }
427
428    /// Obtains the next sequence number for DDL commands, for observability and debugging purposes.
429    pub fn next_seq(&self) -> u64 {
430        // This is a simple atomic increment operation.
431        self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
432    }
433
434    /// `run_command` spawns a tokio coroutine to execute the target ddl command. When the client
435    /// has been interrupted during executing, the request will be cancelled by tonic. Since we have
436    /// a lot of logic for revert, status management, notification and so on, ensuring consistency
437    /// would be a huge hassle and pain if we don't spawn here.
438    ///
439    /// Though returning `Option`, it's always `Some`, to simplify the handling logic
440    pub async fn run_command(&self, command: DdlCommand) -> MetaResult<Option<WaitVersion>> {
441        if !command.allow_in_recovery() {
442            self.barrier_manager.check_status_running()?;
443        }
444
445        let await_tree_key = format!("DDL Command {}", self.next_seq());
446        let await_tree_span = await_tree::span!("{command}({})", command.object());
447
448        let ctrl = self.clone();
449        let fut = Box::pin(async move {
450            match command {
451                DdlCommand::CreateDatabase(database) => ctrl.create_database(database).await,
452                DdlCommand::DropDatabase(database_id) => ctrl.drop_database(database_id).await,
453                DdlCommand::CreateSchema(schema) => ctrl.create_schema(schema).await,
454                DdlCommand::DropSchema(schema_id, drop_mode) => {
455                    ctrl.drop_schema(schema_id, drop_mode).await
456                }
457                DdlCommand::CreateNonSharedSource(source, iceberg_table_id) => {
458                    ctrl.create_non_shared_source(source, iceberg_table_id)
459                        .await
460                }
461                DdlCommand::DropSource(source_id, drop_mode) => {
462                    ctrl.drop_source(source_id, drop_mode).await
463                }
464                DdlCommand::ResetSource(source_id) => ctrl.reset_source(source_id).await,
465                DdlCommand::CreateFunction(function) => ctrl.create_function(function).await,
466                DdlCommand::DropFunction(function_id, drop_mode) => {
467                    ctrl.drop_function(function_id, drop_mode).await
468                }
469                DdlCommand::CreateView(view, dependencies) => {
470                    ctrl.create_view(view, dependencies).await
471                }
472                DdlCommand::DropView(view_id, drop_mode) => {
473                    ctrl.drop_view(view_id, drop_mode).await
474                }
475                DdlCommand::CreateStreamingJob {
476                    stream_job,
477                    fragment_graph,
478                    dependencies,
479                    resource_type,
480                    if_not_exists,
481                    refresh_interval_sec,
482                    replace_sink,
483                    since_timestamp_epoch,
484                } => {
485                    ctrl.create_streaming_job(
486                        stream_job,
487                        fragment_graph,
488                        dependencies,
489                        resource_type,
490                        if_not_exists,
491                        refresh_interval_sec,
492                        replace_sink,
493                        since_timestamp_epoch,
494                    )
495                    .await
496                }
497                DdlCommand::DropStreamingJob { job_id, drop_mode } => {
498                    ctrl.drop_streaming_job(job_id, drop_mode).await
499                }
500                DdlCommand::ReplaceStreamJob(ReplaceStreamJobInfo {
501                    streaming_job,
502                    fragment_graph,
503                }) => ctrl.replace_job(streaming_job, fragment_graph).await,
504                DdlCommand::AlterName(relation, name) => ctrl.alter_name(relation, &name).await,
505                DdlCommand::AlterObjectOwner(object, owner_id) => {
506                    ctrl.alter_owner(object, owner_id).await
507                }
508                DdlCommand::AlterSetSchema(object, new_schema_id) => {
509                    ctrl.alter_set_schema(object, new_schema_id).await
510                }
511                DdlCommand::CreateConnection(connection) => {
512                    ctrl.create_connection(connection).await
513                }
514                DdlCommand::DropConnection(connection_id, drop_mode) => {
515                    ctrl.drop_connection(connection_id, drop_mode).await
516                }
517                DdlCommand::CreateSecret(secret) => ctrl.create_secret(secret).await,
518                DdlCommand::DropSecret(secret_id, drop_mode) => {
519                    ctrl.drop_secret(secret_id, drop_mode).await
520                }
521                DdlCommand::AlterSecret(secret) => ctrl.alter_secret(secret).await,
522                DdlCommand::AlterNonSharedSource(source) => {
523                    ctrl.alter_non_shared_source(source).await
524                }
525                DdlCommand::CommentOn(comment) => ctrl.comment_on(comment).await,
526                DdlCommand::CreateSubscription(subscription) => {
527                    ctrl.create_subscription(subscription).await
528                }
529                DdlCommand::DropSubscription(subscription_id, drop_mode) => {
530                    ctrl.drop_subscription(subscription_id, drop_mode).await
531                }
532                DdlCommand::AlterSubscriptionRetention {
533                    subscription_id,
534                    retention_seconds,
535                    definition,
536                } => {
537                    ctrl.alter_subscription_retention(
538                        subscription_id,
539                        retention_seconds,
540                        definition,
541                    )
542                    .await
543                }
544                DdlCommand::AlterSwapRename(objects) => ctrl.alter_swap_rename(objects).await,
545                DdlCommand::AlterDatabaseParam(database_id, param) => {
546                    ctrl.alter_database_param(database_id, param).await
547                }
548                DdlCommand::AlterDatabaseResourceGroup(database_id, resource_group, deferred) => {
549                    ctrl.alter_database_resource_group(database_id, resource_group, deferred)
550                        .await
551                }
552                DdlCommand::AlterStreamingJobConfig(job_id, entries_to_add, keys_to_remove) => {
553                    ctrl.alter_streaming_job_config(job_id, entries_to_add, keys_to_remove)
554                        .await
555                }
556            }
557        })
558        .in_current_span();
559        let fut = (self.env.await_tree_reg())
560            .register(await_tree_key, await_tree_span)
561            .instrument(Box::pin(fut));
562        let notification_version = tokio::spawn(fut).await.map_err(|e| anyhow!(e))??;
563        Ok(Some(WaitVersion {
564            catalog_version: notification_version,
565            hummock_version_id: self.barrier_manager.get_hummock_version_id().await,
566        }))
567    }
568
569    pub async fn get_ddl_progress(&self) -> MetaResult<Vec<DdlProgress>> {
570        self.barrier_manager.get_ddl_progress().await
571    }
572
573    async fn create_database(&self, database: Database) -> MetaResult<NotificationVersion> {
574        let (version, updated_db) = self
575            .metadata_manager
576            .catalog_controller
577            .create_database(database)
578            .await?;
579        // If persistent successfully, notify `GlobalBarrierManager` to create database asynchronously.
580        self.barrier_manager
581            .update_database_barrier(
582                updated_db.database_id,
583                updated_db.barrier_interval_ms.map(|v| v as u32),
584                updated_db.checkpoint_frequency.map(|v| v as u64),
585            )
586            .await?;
587        Ok(version)
588    }
589
590    #[tracing::instrument(skip(self), level = "debug")]
591    pub async fn reschedule_streaming_job(
592        &self,
593        job_id: JobId,
594        target: ReschedulePolicy,
595        mut deferred: bool,
596    ) -> MetaResult<()> {
597        tracing::info!("altering parallelism for job {}", job_id);
598        if self.barrier_manager.check_status_running().is_err() {
599            tracing::info!(
600                "alter parallelism is set to deferred mode because the system is in recovery state"
601            );
602            deferred = true;
603        }
604
605        self.stream_manager
606            .reschedule_streaming_job(job_id, target, deferred)
607            .await
608    }
609
610    pub async fn reschedule_streaming_job_backfill_parallelism(
611        &self,
612        job_id: JobId,
613        parallelism: Option<ParallelismPolicy>,
614        mut deferred: bool,
615    ) -> MetaResult<()> {
616        tracing::info!("altering backfill parallelism for job {}", job_id);
617        if self.barrier_manager.check_status_running().is_err() {
618            tracing::info!(
619                "alter backfill parallelism is set to deferred mode because the system is in recovery state"
620            );
621            deferred = true;
622        }
623
624        self.stream_manager
625            .reschedule_streaming_job_backfill_parallelism(job_id, parallelism, deferred)
626            .await
627    }
628
629    pub async fn reschedule_cdc_table_backfill(
630        &self,
631        job_id: JobId,
632        target: ReschedulePolicy,
633    ) -> MetaResult<()> {
634        tracing::info!("alter CDC table backfill parallelism");
635        if self.barrier_manager.check_status_running().is_err() {
636            return Err(anyhow::anyhow!("CDC table backfill reschedule is unavailable because the system is in recovery state").into());
637        }
638        self.stream_manager
639            .reschedule_cdc_table_backfill(job_id, target)
640            .await
641    }
642
643    pub async fn reschedule_fragments(
644        &self,
645        fragment_targets: HashMap<FragmentId, Option<StreamingParallelism>>,
646    ) -> MetaResult<()> {
647        tracing::info!(
648            "altering parallelism for fragments {:?}",
649            fragment_targets.keys()
650        );
651        let fragment_targets = fragment_targets
652            .into_iter()
653            .map(|(fragment_id, parallelism)| (fragment_id as CatalogFragmentId, parallelism))
654            .collect();
655
656        self.stream_manager
657            .reschedule_fragments(fragment_targets)
658            .await
659    }
660
661    async fn drop_database(&self, database_id: DatabaseId) -> MetaResult<NotificationVersion> {
662        self.drop_object(ObjectType::Database, database_id, DropMode::Cascade)
663            .await
664    }
665
666    async fn create_schema(&self, schema: Schema) -> MetaResult<NotificationVersion> {
667        self.metadata_manager
668            .catalog_controller
669            .create_schema(schema)
670            .await
671    }
672
673    async fn drop_schema(
674        &self,
675        schema_id: SchemaId,
676        drop_mode: DropMode,
677    ) -> MetaResult<NotificationVersion> {
678        self.drop_object(ObjectType::Schema, schema_id, drop_mode)
679            .await
680    }
681
682    /// Shared source is handled in [`Self::create_streaming_job`]
683    async fn create_non_shared_source(
684        &self,
685        source: Source,
686        iceberg_table_id: Option<TableId>,
687    ) -> MetaResult<NotificationVersion> {
688        if let Some(cdc_table_desc) = source
689            .info
690            .as_ref()
691            .and_then(|info| info.external_table.as_ref())
692        {
693            assert!(iceberg_table_id.is_none());
694            // A CDC table source has no streaming job, so validate the declared schema against
695            // the upstream table here, the same way `CREATE TABLE ... FROM <cdc source>` does
696            // while creating its job.
697            self.validate_cdc_table_desc(cdc_table_desc).await?;
698            let (_, version) = self
699                .metadata_manager
700                .catalog_controller
701                .create_source(source, iceberg_table_id)
702                .await?;
703            return Ok(version);
704        }
705
706        let handle = create_source_worker(
707            &source,
708            self.source_manager.metrics.clone(),
709            self.env.await_tree_reg().clone(),
710        )
711        .await
712        .context("failed to create source worker")?;
713
714        let (source_id, version) = self
715            .metadata_manager
716            .catalog_controller
717            .create_source(source, iceberg_table_id)
718            .await?;
719        self.source_manager
720            .register_source_with_handle(source_id, handle)
721            .await;
722        Ok(version)
723    }
724
725    async fn drop_source(
726        &self,
727        source_id: SourceId,
728        drop_mode: DropMode,
729    ) -> MetaResult<NotificationVersion> {
730        self.drop_object(ObjectType::Source, source_id, drop_mode)
731            .await
732    }
733
734    async fn reset_source(&self, source_id: SourceId) -> MetaResult<NotificationVersion> {
735        tracing::info!(source_id = %source_id, "resetting CDC source offset to latest");
736
737        // Get database_id for the source
738        let database_id = self
739            .metadata_manager
740            .catalog_controller
741            .get_object_database_id(source_id)
742            .await?;
743
744        self.stream_manager
745            .barrier_scheduler
746            .run_command(database_id, Command::ResetSource { source_id })
747            .await?;
748
749        // RESET SOURCE doesn't modify catalog, so return the current catalog version.
750        let version = self
751            .metadata_manager
752            .catalog_controller
753            .current_notification_version()
754            .await;
755        Ok(version)
756    }
757
758    /// This replaces the source in the catalog.
759    /// Note: `StreamSourceInfo` in downstream MVs' `SourceExecutor`s are not updated.
760    async fn alter_non_shared_source(&self, source: Source) -> MetaResult<NotificationVersion> {
761        self.metadata_manager
762            .catalog_controller
763            .alter_non_shared_source(source)
764            .await
765    }
766
767    async fn create_function(&self, function: Function) -> MetaResult<NotificationVersion> {
768        self.metadata_manager
769            .catalog_controller
770            .create_function(function)
771            .await
772    }
773
774    async fn drop_function(
775        &self,
776        function_id: FunctionId,
777        drop_mode: DropMode,
778    ) -> MetaResult<NotificationVersion> {
779        self.drop_object(ObjectType::Function, function_id, drop_mode)
780            .await
781    }
782
783    async fn create_view(
784        &self,
785        view: View,
786        dependencies: HashSet<ObjectId>,
787    ) -> MetaResult<NotificationVersion> {
788        self.metadata_manager
789            .catalog_controller
790            .create_view(view, dependencies)
791            .await
792    }
793
794    async fn drop_view(
795        &self,
796        view_id: ViewId,
797        drop_mode: DropMode,
798    ) -> MetaResult<NotificationVersion> {
799        self.drop_object(ObjectType::View, view_id, drop_mode).await
800    }
801
802    async fn create_connection(&self, connection: Connection) -> MetaResult<NotificationVersion> {
803        validate_connection(&connection).await?;
804        self.metadata_manager
805            .catalog_controller
806            .create_connection(connection)
807            .await
808    }
809
810    async fn drop_connection(
811        &self,
812        connection_id: ConnectionId,
813        drop_mode: DropMode,
814    ) -> MetaResult<NotificationVersion> {
815        self.drop_object(ObjectType::Connection, connection_id, drop_mode)
816            .await
817    }
818
819    async fn alter_database_param(
820        &self,
821        database_id: DatabaseId,
822        param: AlterDatabaseParam,
823    ) -> MetaResult<NotificationVersion> {
824        let (version, updated_db) = self
825            .metadata_manager
826            .catalog_controller
827            .alter_database_param(database_id, param)
828            .await?;
829        // If persistent successfully, notify `GlobalBarrierManager` to update param asynchronously.
830        self.barrier_manager
831            .update_database_barrier(
832                database_id,
833                updated_db.barrier_interval_ms.map(|v| v as u32),
834                updated_db.checkpoint_frequency.map(|v| v as u64),
835            )
836            .await?;
837        Ok(version)
838    }
839
840    async fn alter_database_resource_group(
841        &self,
842        database_id: DatabaseId,
843        resource_group: Option<String>,
844        _deferred: bool,
845    ) -> MetaResult<NotificationVersion> {
846        let version = self
847            .metadata_manager
848            .catalog_controller
849            .alter_database_resource_group(database_id, resource_group)
850            .await?;
851
852        Ok(version)
853    }
854
855    // The 'secret' part of the request we receive from the frontend is in plaintext;
856    // here, we need to encrypt it before storing it in the catalog.
857    fn get_encrypted_payload(&self, secret: &Secret) -> MetaResult<Vec<u8>> {
858        let secret_store_private_key = self
859            .env
860            .opts
861            .secret_store_private_key
862            .clone()
863            .ok_or_else(|| anyhow!("secret_store_private_key is not configured"))?;
864
865        let encrypted_payload = SecretEncryption::encrypt(
866            secret_store_private_key.as_slice(),
867            secret.get_value().as_slice(),
868        )
869        .context(format!("failed to encrypt secret {}", secret.name))?;
870        Ok(encrypted_payload
871            .serialize()
872            .context(format!("failed to serialize secret {}", secret.name))?)
873    }
874
875    async fn create_secret(&self, mut secret: Secret) -> MetaResult<NotificationVersion> {
876        // The 'secret' part of the request we receive from the frontend is in plaintext;
877        // here, we need to encrypt it before storing it in the catalog.
878        let secret_plain_payload = secret.value.clone();
879        let encrypted_payload = self.get_encrypted_payload(&secret)?;
880        secret.value = encrypted_payload;
881
882        self.metadata_manager
883            .catalog_controller
884            .create_secret(secret, secret_plain_payload)
885            .await
886    }
887
888    async fn drop_secret(
889        &self,
890        secret_id: SecretId,
891        drop_mode: DropMode,
892    ) -> MetaResult<NotificationVersion> {
893        self.drop_object(ObjectType::Secret, secret_id, drop_mode)
894            .await
895    }
896
897    async fn alter_secret(&self, mut secret: Secret) -> MetaResult<NotificationVersion> {
898        let secret_plain_payload = secret.value.clone();
899        let encrypted_payload = self.get_encrypted_payload(&secret)?;
900        secret.value = encrypted_payload;
901        self.metadata_manager
902            .catalog_controller
903            .alter_secret(secret, secret_plain_payload)
904            .await
905    }
906
907    async fn create_subscription(
908        &self,
909        mut subscription: Subscription,
910    ) -> MetaResult<NotificationVersion> {
911        tracing::debug!("create subscription");
912        let _permit = self
913            .creating_streaming_job_permits
914            .semaphore
915            .acquire()
916            .await
917            .unwrap();
918        let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
919        self.metadata_manager
920            .catalog_controller
921            .create_subscription_catalog(&mut subscription)
922            .await?;
923        if let Err(err) = self.stream_manager.create_subscription(&subscription).await {
924            tracing::debug!(error = %err.as_report(), "failed to create subscription");
925            let _ = self
926                .metadata_manager
927                .catalog_controller
928                .try_abort_creating_subscription(subscription.id)
929                .await
930                .inspect_err(|e| {
931                    tracing::error!(
932                        error = %e.as_report(),
933                        "failed to abort create subscription after failure"
934                    );
935                });
936            return Err(err);
937        }
938
939        let version = self
940            .metadata_manager
941            .catalog_controller
942            .notify_create_subscription(subscription.id)
943            .await?;
944        tracing::debug!("finish create subscription");
945        Ok(version)
946    }
947
948    async fn drop_subscription(
949        &self,
950        subscription_id: SubscriptionId,
951        drop_mode: DropMode,
952    ) -> MetaResult<NotificationVersion> {
953        tracing::debug!("preparing drop subscription");
954        let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
955        let subscription = self
956            .metadata_manager
957            .catalog_controller
958            .get_subscription_by_id(subscription_id)
959            .await?;
960        let table_id = subscription.dependent_table_id;
961        let database_id = subscription.database_id;
962        let (_, version) = self
963            .metadata_manager
964            .catalog_controller
965            .drop_object(ObjectType::Subscription, subscription_id, drop_mode)
966            .await?;
967        self.stream_manager
968            .drop_subscription(database_id, subscription_id, table_id)
969            .await;
970        tracing::debug!("finish drop subscription");
971        Ok(version)
972    }
973
974    async fn alter_subscription_retention(
975        &self,
976        subscription_id: SubscriptionId,
977        retention_seconds: u64,
978        definition: String,
979    ) -> MetaResult<NotificationVersion> {
980        tracing::debug!("alter subscription retention");
981        let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
982        let (version, subscription) = self
983            .metadata_manager
984            .catalog_controller
985            .alter_subscription_retention(subscription_id, retention_seconds, definition)
986            .await?;
987        self.stream_manager
988            .alter_subscription_retention(
989                subscription.database_id,
990                subscription.id,
991                subscription.dependent_table_id,
992                subscription.retention_seconds,
993            )
994            .await?;
995        tracing::debug!("finish alter subscription retention");
996        Ok(version)
997    }
998
999    /// Validates the connect properties in the `cdc_table_desc` stored in the `StreamCdcScan` node
1000    #[await_tree::instrument]
1001    pub(crate) async fn validate_cdc_table(
1002        &self,
1003        table: &Table,
1004        table_fragments: &StreamJobFragments,
1005    ) -> MetaResult<()> {
1006        let stream_scan_fragment =
1007            Itertools::exactly_one(table_fragments.fragments.values().filter(|f| {
1008                f.fragment_type_mask.contains(FragmentTypeFlag::StreamScan)
1009                    || f.fragment_type_mask
1010                        .contains(FragmentTypeFlag::StreamCdcScan)
1011            }))
1012            .ok()
1013            .with_context(|| {
1014                format!(
1015                    "expect exactly one stream scan fragment, got: {:?}",
1016                    table_fragments.fragments
1017                )
1018            })?;
1019        fn assert_parallelism(
1020            distribution_type: PbFragmentDistributionType,
1021            node_body: &Option<NodeBody>,
1022        ) {
1023            if let Some(NodeBody::StreamCdcScan(node)) = node_body {
1024                if let Some(o) = node.options
1025                    && CdcScanOptions::from_proto(&o).is_parallelized_backfill()
1026                {
1027                    // Use parallel CDC backfill.
1028                } else {
1029                    assert_eq!(
1030                        distribution_type,
1031                        PbFragmentDistributionType::Single,
1032                        "Non-parallelized CDC scan fragment should have Single distribution"
1033                    );
1034                }
1035            }
1036        }
1037        let mut found_cdc_scan = false;
1038        match &stream_scan_fragment.nodes.node_body {
1039            Some(NodeBody::StreamCdcScan(_)) => {
1040                assert_parallelism(
1041                    stream_scan_fragment.distribution_type,
1042                    &stream_scan_fragment.nodes.node_body,
1043                );
1044                if self
1045                    .validate_cdc_table_inner(&stream_scan_fragment.nodes.node_body, table.id)
1046                    .await?
1047                {
1048                    found_cdc_scan = true;
1049                }
1050            }
1051            // When there's generated columns, the cdc scan node is wrapped in a project node
1052            Some(NodeBody::Project(_)) => {
1053                for input in &stream_scan_fragment.nodes.input {
1054                    assert_parallelism(stream_scan_fragment.distribution_type, &input.node_body);
1055                    if self
1056                        .validate_cdc_table_inner(&input.node_body, table.id)
1057                        .await?
1058                    {
1059                        found_cdc_scan = true;
1060                    }
1061                }
1062            }
1063            _ => {
1064                bail!("Unexpected node body for stream cdc scan");
1065            }
1066        };
1067        if !found_cdc_scan {
1068            bail!("No stream cdc scan node found in stream scan fragment");
1069        }
1070        Ok(())
1071    }
1072
1073    async fn validate_cdc_table_inner(
1074        &self,
1075        node_body: &Option<NodeBody>,
1076        table_id: TableId,
1077    ) -> MetaResult<bool> {
1078        if let Some(NodeBody::StreamCdcScan(stream_cdc_scan)) = node_body
1079            && let Some(ref cdc_table_desc) = stream_cdc_scan.cdc_table_desc
1080        {
1081            self.validate_cdc_table_desc(cdc_table_desc).await?;
1082            tracing::debug!(?table_id, "validate cdc table success");
1083            Ok(true)
1084        } else {
1085            Ok(false)
1086        }
1087    }
1088
1089    /// Validates a CDC table descriptor against its upstream table, by creating a throw-away
1090    /// split enumerator: the connector validator checks that the table exists and that the
1091    /// declared columns and primary key match the upstream ones.
1092    pub(crate) async fn validate_cdc_table_desc(
1093        &self,
1094        cdc_table_desc: &PbExternalTableDesc,
1095    ) -> MetaResult<()> {
1096        let options_with_secret = WithOptionsSecResolved::new(
1097            cdc_table_desc.connect_properties.clone(),
1098            cdc_table_desc.secret_refs.clone(),
1099        );
1100
1101        let mut props = ConnectorProperties::extract(options_with_secret, true)?;
1102        props.init_from_pb_cdc_table_desc(cdc_table_desc);
1103
1104        let _enumerator = props
1105            .create_split_enumerator(SourceEnumeratorContext::dummy().into())
1106            .await?;
1107
1108        Ok(())
1109    }
1110
1111    pub async fn validate_table_for_sink(&self, table_id: TableId) -> MetaResult<()> {
1112        let migrated = self
1113            .metadata_manager
1114            .catalog_controller
1115            .has_table_been_migrated(table_id)
1116            .await?;
1117        if !migrated {
1118            Err(anyhow::anyhow!("Creating sink into table is not allowed for unmigrated table {}. Please migrate it first.", table_id).into())
1119        } else {
1120            Ok(())
1121        }
1122    }
1123
1124    /// For [`CreateType::Foreground`], the function will only return after backfilling finishes
1125    /// ([`crate::manager::MetadataManager::wait_streaming_job_finished`]).
1126    #[await_tree::instrument(boxed, "create_streaming_job({streaming_job})")]
1127    pub async fn create_streaming_job(
1128        &self,
1129        mut streaming_job: StreamingJob,
1130        fragment_graph: StreamFragmentGraphProto,
1131        dependencies: HashSet<ObjectId>,
1132        resource_type: streaming_job_resource_type::ResourceType,
1133        if_not_exists: bool,
1134        refresh_interval_sec: Option<u64>,
1135        replace_sink: Option<SinkId>,
1136        since_timestamp_epoch: Option<u64>,
1137    ) -> MetaResult<NotificationVersion> {
1138        let replace_sink_info = if let Some(old_sink_id) = replace_sink {
1139            let StreamingJob::Sink(sink, _) = &streaming_job else {
1140                bail!("replace sink requires a sink job")
1141            };
1142            if sink.target_table.is_some() {
1143                bail_not_implemented!("replace sink into table")
1144            }
1145
1146            Some(old_sink_id)
1147        } else {
1148            if let StreamingJob::Sink(sink, _) = &streaming_job
1149                && let Some(target_table) = sink.target_table
1150            {
1151                self.validate_table_for_sink(target_table).await?;
1152            }
1153            None
1154        };
1155        self.validate_serverless_backfill_enabled(&resource_type)?;
1156        let ctx = StreamContext::from_protobuf(fragment_graph.get_ctx().unwrap());
1157        let adaptive_parallelism_strategy =
1158            (!fragment_graph.adaptive_parallelism_strategy.is_empty()).then(|| {
1159                parse_strategy(&fragment_graph.adaptive_parallelism_strategy)
1160                    .expect("adaptive parallelism strategy should be validated in frontend")
1161            });
1162        let backfill_adaptive_parallelism_strategy = (!fragment_graph
1163            .backfill_adaptive_parallelism_strategy
1164            .is_empty())
1165        .then(|| {
1166            parse_strategy(&fragment_graph.backfill_adaptive_parallelism_strategy)
1167                .expect("backfill adaptive parallelism strategy should be validated in frontend")
1168        });
1169
1170        let streaming_job_model = match self
1171            .metadata_manager
1172            .catalog_controller
1173            .create_job_catalog(
1174                &mut streaming_job,
1175                &ctx,
1176                &fragment_graph.parallelism,
1177                fragment_graph.max_parallelism as _,
1178                dependencies,
1179                resource_type.clone(),
1180                &fragment_graph.backfill_parallelism,
1181                adaptive_parallelism_strategy,
1182                backfill_adaptive_parallelism_strategy,
1183                replace_sink_info.as_ref(),
1184                refresh_interval_sec,
1185            )
1186            .await
1187        {
1188            Ok(model) => model,
1189            Err(meta_err) => {
1190                if !if_not_exists {
1191                    return Err(meta_err);
1192                }
1193                return if let MetaErrorInner::Duplicated(_, _, Some(job_id)) = meta_err.inner() {
1194                    if streaming_job.create_type() == CreateType::Foreground {
1195                        let database_id = streaming_job.database_id();
1196                        self.metadata_manager
1197                            .wait_streaming_job_finished(database_id, *job_id)
1198                            .await
1199                    } else {
1200                        Ok(IGNORED_NOTIFICATION_VERSION)
1201                    }
1202                } else {
1203                    Err(meta_err)
1204                };
1205            }
1206        };
1207        let job_id = streaming_job.id();
1208        if let Some(old_sink_id) = replace_sink_info.as_ref() {
1209            tracing::debug!(
1210                old_sink_id = %old_sink_id,
1211                new_sink_id = %job_id,
1212                definition = streaming_job.definition(),
1213                create_type = streaming_job.create_type().as_str_name(),
1214                "starting replacement sink",
1215            );
1216        } else {
1217            tracing::debug!(
1218                id = %job_id,
1219                definition = streaming_job.definition(),
1220                create_type = streaming_job.create_type().as_str_name(),
1221                job_type = ?streaming_job.job_type(),
1222                "starting streaming job",
1223            );
1224        }
1225        // TODO: acquire permits for recovered background DDLs.
1226        let permit = self
1227            .creating_streaming_job_permits
1228            .semaphore
1229            .clone()
1230            .acquire_owned()
1231            .instrument_await("acquire_creating_streaming_job_permit")
1232            .await
1233            .unwrap();
1234        let reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
1235
1236        let name = streaming_job.name();
1237        let definition = streaming_job.definition();
1238        let source_id = match &streaming_job {
1239            StreamingJob::Table(Some(src), _, _) | StreamingJob::Source(src) => Some(src.id),
1240            _ => None,
1241        };
1242        let create_result = match self
1243            .generate_streaming_job(
1244                ctx,
1245                streaming_job,
1246                fragment_graph,
1247                resource_type.clone(),
1248                streaming_job_model,
1249                replace_sink_info,
1250                since_timestamp_epoch,
1251            )
1252            .await
1253        {
1254            Ok((stream_job_fragments, ctx)) => {
1255                self.stream_manager
1256                    .create_streaming_job(stream_job_fragments, ctx, permit, reschedule_job_lock)
1257                    .await
1258            }
1259            Err(err) => Err((err, false, None)),
1260        };
1261
1262        match create_result {
1263            Ok(version) => Ok(version),
1264            Err((err, is_cancelled, cancel_notifier)) => {
1265                tracing::error!(id = %job_id, error = %err.as_report(), "failed to create streaming job");
1266                let event = risingwave_pb::meta::event_log::EventCreateStreamJobFail {
1267                    id: job_id,
1268                    name,
1269                    definition,
1270                    error: err.as_report().to_string(),
1271                };
1272                self.env.event_log_manager_ref().add_event_logs(vec![
1273                    risingwave_pb::meta::event_log::Event::CreateStreamJobFail(event),
1274                ]);
1275                let abort_result = self
1276                    .metadata_manager
1277                    .catalog_controller
1278                    .try_abort_creating_streaming_job(job_id, is_cancelled)
1279                    .await?;
1280                let aborted = abort_result.aborted;
1281                self.stream_manager
1282                    .cleanup_aborted_streaming_job(abort_result)
1283                    .await?;
1284                if let Some(cancel_notifier) = cancel_notifier {
1285                    let _ = cancel_notifier.send(true).inspect_err(|err| {
1286                        tracing::warn!("failed to notify cancellation result: {err}")
1287                    });
1288                }
1289                if aborted {
1290                    tracing::warn!(id = %job_id, is_cancelled, "aborted streaming job");
1291                    // FIXME: might also need other cleanup here
1292                    if let Some(source_id) = source_id {
1293                        self.source_manager
1294                            .apply_source_change(SourceChange::DropSource {
1295                                dropped_source_ids: vec![source_id],
1296                            })
1297                            .await;
1298                    }
1299                }
1300                Err(err)
1301            }
1302        }
1303    }
1304
1305    #[await_tree::instrument(boxed)]
1306    async fn generate_streaming_job(
1307        &self,
1308        ctx: StreamContext,
1309        mut streaming_job: StreamingJob,
1310        fragment_graph: StreamFragmentGraphProto,
1311        resource_type: streaming_job_resource_type::ResourceType,
1312        streaming_job_model: streaming_job::Model,
1313        replace_sink: Option<SinkId>,
1314        since_timestamp_epoch: Option<u64>,
1315    ) -> MetaResult<(StreamJobFragmentsToCreate, CreateStreamingJobContext)> {
1316        let mut fragment_graph =
1317            StreamFragmentGraph::new(&self.env, fragment_graph, &streaming_job)?;
1318        streaming_job.set_info_from_graph(&fragment_graph);
1319
1320        // create internal table catalogs and refill table id.
1321        let incomplete_internal_tables = fragment_graph
1322            .incomplete_internal_tables()
1323            .into_values()
1324            .collect_vec();
1325        let table_id_map = self
1326            .metadata_manager
1327            .catalog_controller
1328            .create_internal_table_catalog(&streaming_job, incomplete_internal_tables)
1329            .await?;
1330        fragment_graph.refill_internal_table_ids(table_id_map);
1331
1332        // create fragment and actor catalogs.
1333        tracing::debug!(id = %streaming_job.id(), "building streaming job");
1334        let (mut ctx, stream_job_fragments) = self
1335            .build_stream_job(
1336                ctx,
1337                streaming_job,
1338                fragment_graph,
1339                resource_type,
1340                streaming_job_model,
1341                since_timestamp_epoch,
1342            )
1343            .await?;
1344        ctx.replace_sink = replace_sink;
1345
1346        let streaming_job = &ctx.streaming_job;
1347
1348        match streaming_job {
1349            StreamingJob::Table(None, table, TableJobType::SharedCdcSource) => {
1350                self.validate_cdc_table(table, &stream_job_fragments)
1351                    .await?;
1352            }
1353            StreamingJob::Table(Some(source), ..) => {
1354                // Register the source on the connector node.
1355                self.source_manager.register_source(source).await?;
1356                let connector_name = source
1357                    .get_with_properties()
1358                    .get(UPSTREAM_SOURCE_KEY)
1359                    .cloned();
1360                let attr = source.info.as_ref().map(|source_info| {
1361                    jsonbb::json!({
1362                            "format": source_info.format().as_str_name(),
1363                            "encode": source_info.row_encode().as_str_name(),
1364                    })
1365                });
1366                report_create_object(
1367                    streaming_job.id(),
1368                    "source",
1369                    PbTelemetryDatabaseObject::Source,
1370                    connector_name,
1371                    attr,
1372                );
1373            }
1374            StreamingJob::Sink(sink, _) => {
1375                if sink.auto_refresh_schema_from_table.is_some() {
1376                    check_sink_fragments_support_refresh_schema(&stream_job_fragments.fragments)?;
1377                }
1378                // Validate the sink on the connector node.
1379                validate_sink(sink).await?;
1380                // For Iceberg pk-index sinks, spawn the per-sink commit worker now
1381                // so it's ready to receive epoch reports from the very first
1382                // barrier instead of relying on lazy registration on every
1383                // commit.
1384                if crate::manager::iceberg_pk_index_sink::is_iceberg_pk_index_sink(&sink.properties)
1385                {
1386                    self.iceberg_pk_index_sink_manager
1387                        .register_sink(
1388                            sink.id,
1389                            crate::barrier::to_partial_graph_id(
1390                                sink.database_id,
1391                                Some(sink.id.as_job_id()),
1392                            ),
1393                        )
1394                        .await
1395                        .map_err(|e| anyhow!(e).context("register v3 sink worker"))?;
1396                }
1397                let connector_name = sink.get_properties().get(UPSTREAM_SOURCE_KEY).cloned();
1398                let attr = sink.format_desc.as_ref().map(|sink_info| {
1399                    jsonbb::json!({
1400                        "format": sink_info.format().as_str_name(),
1401                        "encode": sink_info.encode().as_str_name(),
1402                    })
1403                });
1404                report_create_object(
1405                    streaming_job.id(),
1406                    "sink",
1407                    PbTelemetryDatabaseObject::Sink,
1408                    connector_name,
1409                    attr,
1410                );
1411            }
1412            StreamingJob::Source(source) => {
1413                // Register the source on the connector node.
1414                self.source_manager.register_source(source).await?;
1415                let connector_name = source
1416                    .get_with_properties()
1417                    .get(UPSTREAM_SOURCE_KEY)
1418                    .cloned();
1419                let attr = source.info.as_ref().map(|source_info| {
1420                    jsonbb::json!({
1421                            "format": source_info.format().as_str_name(),
1422                            "encode": source_info.row_encode().as_str_name(),
1423                    })
1424                });
1425                report_create_object(
1426                    streaming_job.id(),
1427                    "source",
1428                    PbTelemetryDatabaseObject::Source,
1429                    connector_name,
1430                    attr,
1431                );
1432            }
1433            _ => {}
1434        }
1435
1436        let backfill_orders = ctx.fragment_backfill_ordering.to_meta_model();
1437        self.metadata_manager
1438            .catalog_controller
1439            .prepare_stream_job_fragments(
1440                &stream_job_fragments,
1441                streaming_job,
1442                false,
1443                Some(backfill_orders),
1444            )
1445            .await?;
1446
1447        Ok((stream_job_fragments, ctx))
1448    }
1449
1450    /// `target_replace_info`: when dropping a sink into table, we need to replace the table.
1451    pub async fn drop_object(
1452        &self,
1453        object_type: ObjectType,
1454        object_id: impl Into<ObjectId>,
1455        drop_mode: DropMode,
1456    ) -> MetaResult<NotificationVersion> {
1457        let object_id = object_id.into();
1458        // Fence reschedule and source tick before catalog deletion so post-collect split updates
1459        // cannot race with dropped fragments.
1460        let _reschedule_job_lock = self.stream_manager.reschedule_lock_read_guard().await;
1461        let _source_tick_pause_guard = self.source_manager.pause_tick().await;
1462
1463        let (release_ctx, version) = self
1464            .metadata_manager
1465            .catalog_controller
1466            .drop_object(object_type, object_id, drop_mode)
1467            .await?;
1468
1469        if object_type == ObjectType::Source {
1470            self.env
1471                .notification_manager_ref()
1472                .notify_local_subscribers(LocalNotification::SourceDropped(object_id));
1473        }
1474
1475        let ReleaseContext {
1476            database_id,
1477            removed_streaming_job_ids,
1478            removed_state_table_ids,
1479            removed_source_ids,
1480            removed_secret_ids: secret_ids,
1481            removed_source_fragments,
1482            removed_fragments,
1483            removed_sink_fragment_by_targets,
1484            removed_iceberg_table_sinks,
1485            removed_iceberg_sink_ids,
1486        } = release_ctx;
1487        self.iceberg_pk_index_sink_manager
1488            .retire_sinks(removed_iceberg_sink_ids);
1489
1490        // Notify serving module about deleted fragments so it can clean up serving vnode mappings.
1491        // This is driven by the fragment model deletion (cascade from Object::delete_many),
1492        // decoupled from the barrier-driven streaming mapping notifications.
1493        self.env
1494            .notification_manager_ref()
1495            .notify_serving_fragment_mapping_delete(
1496                removed_fragments.iter().map(|id| *id as _).collect(),
1497            );
1498
1499        self.stream_manager
1500            .drop_streaming_jobs(
1501                database_id,
1502                removed_streaming_job_ids,
1503                removed_state_table_ids,
1504                removed_sink_fragment_by_targets
1505                    .into_iter()
1506                    .map(|(target, sinks)| {
1507                        (target as _, sinks.into_iter().map(|id| id as _).collect())
1508                    })
1509                    .collect(),
1510            )
1511            .await;
1512
1513        // clean up sources after dropping streaming jobs.
1514        // Otherwise, e.g., Kafka consumer groups might be recreated after deleted.
1515        self.source_manager
1516            .apply_source_change(SourceChange::DropSource {
1517                dropped_source_ids: removed_source_ids.into_iter().map(|id| id as _).collect(),
1518            })
1519            .await;
1520
1521        // unregister fragments and actors from source manager.
1522        // FIXME: need also unregister source backfill fragments.
1523        let dropped_source_fragments = removed_source_fragments;
1524        self.source_manager
1525            .apply_source_change(SourceChange::DropMv {
1526                dropped_source_fragments,
1527            })
1528            .await;
1529
1530        // clean up iceberg table sinks
1531        for sink in removed_iceberg_table_sinks {
1532            let sink_param = SinkParam::try_from_sink_catalog(sink.into())
1533                .expect("Iceberg sink should be valid");
1534            let iceberg_sink =
1535                IcebergSink::try_from(sink_param).expect("Iceberg sink should be valid");
1536            if let Ok(iceberg_catalog) = iceberg_sink.config.create_catalog().await {
1537                let table_identifier = iceberg_sink.config.full_table_name().unwrap();
1538                tracing::info!(
1539                    "dropping iceberg table {} for dropped sink",
1540                    table_identifier
1541                );
1542
1543                let _ = iceberg_catalog
1544                    .drop_table(&table_identifier)
1545                    .await
1546                    .inspect_err(|err| {
1547                        tracing::error!(
1548                            "failed to drop iceberg table {} during cleanup: {}",
1549                            table_identifier,
1550                            err.as_report()
1551                        );
1552                    });
1553            }
1554        }
1555
1556        // remove secrets.
1557        for secret in secret_ids {
1558            LocalSecretManager::global().remove_secret(secret);
1559        }
1560        Ok(version)
1561    }
1562
1563    /// This is used for `ALTER TABLE ADD/DROP COLUMN` / `ALTER SOURCE ADD COLUMN`.
1564    #[await_tree::instrument(boxed, "replace_streaming_job({streaming_job})")]
1565    pub async fn replace_job(
1566        &self,
1567        mut streaming_job: StreamingJob,
1568        fragment_graph: StreamFragmentGraphProto,
1569    ) -> MetaResult<NotificationVersion> {
1570        match &streaming_job {
1571            StreamingJob::Table(..)
1572            | StreamingJob::Source(..)
1573            | StreamingJob::MaterializedView(..) => {}
1574            StreamingJob::Sink(..) | StreamingJob::Index(..) => {
1575                bail_not_implemented!("schema change for {}", streaming_job.job_type_str())
1576            }
1577        }
1578
1579        let job_id = streaming_job.id();
1580
1581        let _reschedule_job_lock = self.stream_manager.reschedule_lock_write_guard().await;
1582        if let StreamingJob::Table(_, table, _) = &streaming_job
1583            && self
1584                .metadata_manager
1585                .catalog_controller
1586                .get_refresh_job_state(table.id)
1587                .await?
1588                .is_some_and(|state| state != RefreshState::Idle)
1589        {
1590            bail!(
1591                "Cannot alter table {} because it is being refreshed",
1592                table.name
1593            );
1594        }
1595        let ctx = StreamContext::from_protobuf(fragment_graph.get_ctx().unwrap());
1596
1597        // Ensure the max parallelism unchanged before replacing table.
1598        let original_max_parallelism = self
1599            .metadata_manager
1600            .get_job_max_parallelism(streaming_job.id())
1601            .await?;
1602        let fragment_graph = PbStreamFragmentGraph {
1603            max_parallelism: original_max_parallelism as _,
1604            ..fragment_graph
1605        };
1606
1607        // 1. build fragment graph.
1608        let fragment_graph = StreamFragmentGraph::new(&self.env, fragment_graph, &streaming_job)?;
1609        streaming_job.set_info_from_graph(&fragment_graph);
1610
1611        // make it immutable
1612        let streaming_job = streaming_job;
1613
1614        let auto_refresh_schema_sinks = if let StreamingJob::Table(_, table, _) = &streaming_job {
1615            let auto_refresh_schema_sinks = self
1616                .metadata_manager
1617                .catalog_controller
1618                .get_sink_auto_refresh_schema_from(table.id)
1619                .await?;
1620            if !auto_refresh_schema_sinks.is_empty() {
1621                let original_table_columns = self
1622                    .metadata_manager
1623                    .catalog_controller
1624                    .get_table_columns(table.id)
1625                    .await?;
1626                // compare column id to find newly added and removed columns
1627                let original_table_column_ids: HashSet<_> = original_table_columns
1628                    .iter()
1629                    .map(|col| col.column_id())
1630                    .collect();
1631                let new_table_column_ids: HashSet<_> = table
1632                    .columns
1633                    .iter()
1634                    .map(|col| ColumnId::new(col.column_desc.as_ref().unwrap().column_id as _))
1635                    .collect();
1636                let newly_added_columns = table
1637                    .columns
1638                    .iter()
1639                    .filter(|col| {
1640                        !original_table_column_ids.contains(&ColumnId::new(
1641                            col.column_desc.as_ref().unwrap().column_id as _,
1642                        ))
1643                    })
1644                    .map(|col| ColumnCatalog::from(col.clone()))
1645                    .collect_vec();
1646                let removed_columns = original_table_columns
1647                    .iter()
1648                    .filter(|col| !new_table_column_ids.contains(&col.column_id()))
1649                    .cloned()
1650                    .collect_vec();
1651                // Fail before any fragment rewrite or catalog persistence so a rejected ALTER
1652                // leaves no partial state.
1653                if let Some(variant_column) = first_variant_column(&newly_added_columns) {
1654                    let sink_names = auto_refresh_schema_sinks
1655                        .iter()
1656                        .map(|sink| format!("`{}`", sink.name))
1657                        .join(", ");
1658                    return Err(MetaError::invalid_parameter(format!(
1659                        "cannot add VARIANT column `{}` because sink(s) {} with auto schema refresh do not support VARIANT",
1660                        variant_column.name_with_hidden(),
1661                        sink_names,
1662                    )));
1663                }
1664                let mut sinks = Vec::with_capacity(auto_refresh_schema_sinks.len());
1665                for sink in auto_refresh_schema_sinks {
1666                    let sink_job_fragments = self
1667                        .metadata_manager
1668                        .get_job_fragments_by_id(sink.id.as_job_id())
1669                        .await?;
1670                    if sink_job_fragments.fragments.len() != 1 {
1671                        return Err(anyhow!(
1672                            "auto schema refresh sink must have only one fragment, but got {}",
1673                            sink_job_fragments.fragments.len()
1674                        )
1675                        .into());
1676                    }
1677                    let sink_ctx = sink_job_fragments.ctx;
1678                    let original_sink_fragment =
1679                        sink_job_fragments.fragments.into_values().next().unwrap();
1680                    let (new_sink_fragment, new_schema, new_log_store_table) =
1681                        rewrite_refresh_schema_sink_fragment(
1682                            &original_sink_fragment,
1683                            &sink,
1684                            &newly_added_columns,
1685                            &removed_columns,
1686                            table,
1687                            fragment_graph.table_fragment_id(),
1688                            self.env.id_gen_manager(),
1689                        )?;
1690
1691                    let streaming_job = StreamingJob::Sink(sink, None);
1692
1693                    let tmp_sink_model = self
1694                        .metadata_manager
1695                        .catalog_controller
1696                        .create_job_catalog_for_replace(&streaming_job, None, None, None)
1697                        .await?;
1698                    let tmp_sink_id = tmp_sink_model.job_id.as_sink_id();
1699                    let StreamingJob::Sink(sink, _) = streaming_job else {
1700                        unreachable!()
1701                    };
1702
1703                    sinks.push(AutoRefreshSchemaSinkContext {
1704                        tmp_sink_id,
1705                        original_sink: sink,
1706                        original_fragment: original_sink_fragment,
1707                        new_schema,
1708                        newly_add_fields: newly_added_columns
1709                            .iter()
1710                            .map(|col| Field::from(&col.column_desc))
1711                            .collect(),
1712                        removed_column_names: removed_columns
1713                            .iter()
1714                            .map(|col| col.name.clone())
1715                            .collect(),
1716                        new_fragment: new_sink_fragment,
1717                        new_log_store_table: new_log_store_table.map(Box::new),
1718                        ctx: sink_ctx,
1719                    });
1720                }
1721                Some(sinks)
1722            } else {
1723                None
1724            }
1725        } else {
1726            None
1727        };
1728
1729        let streaming_job_model = self
1730            .metadata_manager
1731            .catalog_controller
1732            .create_job_catalog_for_replace(
1733                &streaming_job,
1734                Some(&ctx),
1735                fragment_graph.specified_parallelism().as_ref(),
1736                Some(fragment_graph.max_parallelism()),
1737            )
1738            .await?;
1739        let tmp_id = streaming_job_model.job_id;
1740
1741        let tmp_sink_ids = auto_refresh_schema_sinks.as_ref().map(|sinks| {
1742            sinks
1743                .iter()
1744                .map(|sink| sink.tmp_sink_id.as_object_id())
1745                .collect_vec()
1746        });
1747
1748        tracing::debug!(id = %job_id, "building replace streaming job");
1749        let mut updated_sink_catalogs = vec![];
1750
1751        let mut drop_table_connector_ctx = None;
1752        let result: MetaResult<_> = try {
1753            let (mut ctx, mut stream_job_fragments) = self
1754                .build_replace_job(
1755                    ctx,
1756                    &streaming_job,
1757                    fragment_graph,
1758                    tmp_id,
1759                    auto_refresh_schema_sinks,
1760                    streaming_job_model,
1761                )
1762                .await?;
1763            drop_table_connector_ctx = ctx.drop_table_connector_ctx.clone();
1764            let auto_refresh_schema_sink_finish_ctx =
1765                ctx.auto_refresh_schema_sinks.as_ref().map(|sinks| {
1766                    sinks
1767                        .iter()
1768                        .map(|sink| FinishAutoRefreshSchemaSinkContext {
1769                            tmp_sink_id: sink.tmp_sink_id,
1770                            original_sink_id: sink.original_sink.id,
1771                            columns: sink.new_schema.clone(),
1772                            new_log_store_table: sink.new_log_store_table.clone(),
1773                        })
1774                        .collect()
1775                });
1776
1777            // Handle table that has incoming sinks.
1778            if let StreamingJob::Table(_, table, ..) = &streaming_job {
1779                let union_fragment = stream_job_fragments.inner.union_fragment_for_table();
1780                let upstream_infos = self
1781                    .metadata_manager
1782                    .catalog_controller
1783                    .get_all_upstream_sink_infos(table, union_fragment.fragment_id as _)
1784                    .await?;
1785                refill_upstream_sink_union_in_table(&mut union_fragment.nodes, &upstream_infos);
1786
1787                for upstream_info in &upstream_infos {
1788                    let upstream_fragment_id = upstream_info.sink_fragment_id;
1789                    ctx.upstream_fragment_downstreams
1790                        .entry(upstream_fragment_id)
1791                        .or_default()
1792                        .push(upstream_info.new_sink_downstream.clone());
1793                    if upstream_info.sink_original_target_columns.is_empty() {
1794                        updated_sink_catalogs.push(upstream_info.sink_id);
1795                    }
1796                }
1797            }
1798
1799            let replace_upstream = ctx.replace_upstream.clone();
1800
1801            if let Some(sinks) = &ctx.auto_refresh_schema_sinks {
1802                let empty_downstreams = FragmentDownstreamRelation::default();
1803                for sink in sinks {
1804                    self.metadata_manager
1805                        .catalog_controller
1806                        .prepare_streaming_job(
1807                            sink.tmp_sink_id.as_job_id(),
1808                            || [&sink.new_fragment].into_iter(),
1809                            &empty_downstreams,
1810                            true,
1811                            None,
1812                            None,
1813                        )
1814                        .await?;
1815                }
1816            }
1817
1818            self.metadata_manager
1819                .catalog_controller
1820                .prepare_stream_job_fragments(&stream_job_fragments, &streaming_job, true, None)
1821                .await?;
1822
1823            self.stream_manager
1824                .replace_stream_job(stream_job_fragments, ctx)
1825                .await?;
1826            (replace_upstream, auto_refresh_schema_sink_finish_ctx)
1827        };
1828
1829        match result {
1830            Ok((replace_upstream, auto_refresh_schema_sink_finish_ctx)) => {
1831                let version = self
1832                    .metadata_manager
1833                    .catalog_controller
1834                    .finish_replace_streaming_job(
1835                        tmp_id,
1836                        streaming_job,
1837                        replace_upstream,
1838                        SinkIntoTableContext {
1839                            updated_sink_catalogs,
1840                        },
1841                        drop_table_connector_ctx.as_ref(),
1842                        auto_refresh_schema_sink_finish_ctx,
1843                    )
1844                    .await?;
1845                if let Some(drop_table_connector_ctx) = &drop_table_connector_ctx {
1846                    self.source_manager
1847                        .apply_source_change(SourceChange::DropSource {
1848                            dropped_source_ids: vec![drop_table_connector_ctx.to_remove_source_id],
1849                        })
1850                        .await;
1851                }
1852                Ok(version)
1853            }
1854            Err(err) => {
1855                tracing::error!(id = %job_id, error = ?err.as_report(), "failed to replace job");
1856                let _ = self.metadata_manager
1857                    .catalog_controller
1858                    .try_abort_replacing_streaming_job(tmp_id, tmp_sink_ids)
1859                    .await.inspect_err(|err| {
1860                    tracing::error!(id = %job_id, error = ?err.as_report(), "failed to abort replacing job");
1861                });
1862                Err(err)
1863            }
1864        }
1865    }
1866
1867    #[await_tree::instrument(boxed, "drop_streaming_job{}({job_id})", if let DropMode::Cascade = drop_mode { "_cascade" } else { "" }
1868    )]
1869    async fn drop_streaming_job(
1870        &self,
1871        job_id: StreamingJobId,
1872        drop_mode: DropMode,
1873    ) -> MetaResult<NotificationVersion> {
1874        let (object_id, object_type) = match job_id {
1875            StreamingJobId::MaterializedView(id) => (id.as_object_id(), ObjectType::Table),
1876            StreamingJobId::Sink(id) => (id.as_object_id(), ObjectType::Sink),
1877            StreamingJobId::Table(_, id) => (id.as_object_id(), ObjectType::Table),
1878            StreamingJobId::Index(idx) => (idx.as_object_id(), ObjectType::Index),
1879        };
1880
1881        let job_status = self
1882            .metadata_manager
1883            .catalog_controller
1884            .get_streaming_job_status(job_id.id())
1885            .await?;
1886        let version = match job_status {
1887            JobStatus::Initial => {
1888                let abort_result = self
1889                    .metadata_manager
1890                    .catalog_controller
1891                    .try_abort_creating_streaming_job(job_id.id(), true)
1892                    .await?;
1893                cleanup_dropped_sink_runtimes(
1894                    &self.sink_manager,
1895                    &self.iceberg_pk_index_sink_manager,
1896                    abort_result
1897                        .aborted_sink_ids
1898                        .iter()
1899                        .map(|id| id.as_job_id()),
1900                )
1901                .await;
1902                IGNORED_NOTIFICATION_VERSION
1903            }
1904            JobStatus::Creating => {
1905                self.stream_manager
1906                    .cancel_streaming_jobs(vec![job_id.id()])
1907                    .await?;
1908                IGNORED_NOTIFICATION_VERSION
1909            }
1910            JobStatus::Created => self.drop_object(object_type, object_id, drop_mode).await?,
1911        };
1912
1913        Ok(version)
1914    }
1915
1916    /// Builds the actor graph:
1917    /// - Add the upstream fragments to the fragment graph
1918    /// - Schedule the fragments based on their distribution
1919    /// - Expand each fragment into one or several actors
1920    /// - Construct the fragment level backfill order control.
1921    #[await_tree::instrument]
1922    pub(crate) async fn build_stream_job(
1923        &self,
1924        stream_ctx: StreamContext,
1925        mut stream_job: StreamingJob,
1926        fragment_graph: StreamFragmentGraph,
1927        resource_type: streaming_job_resource_type::ResourceType,
1928        streaming_job_model: streaming_job::Model,
1929        since_timestamp_epoch: Option<u64>,
1930    ) -> MetaResult<(CreateStreamingJobContext, StreamJobFragmentsToCreate)> {
1931        let id = stream_job.id();
1932        let max_parallelism = NonZeroUsize::new(fragment_graph.max_parallelism()).unwrap();
1933        Self::validate_specified_parallelism(
1934            fragment_graph.specified_parallelism(),
1935            fragment_graph.specified_backfill_parallelism(),
1936            max_parallelism,
1937        )?;
1938
1939        // 1. Fragment Level ordering graph
1940        let fragment_backfill_ordering = fragment_graph.create_fragment_backfill_ordering();
1941
1942        // 2. Resolve the upstream fragments, extend the fragment graph to a complete graph that
1943        // contains all information needed for building the actor graph.
1944
1945        let (snapshot_backfill_info, cross_db_snapshot_backfill_info) =
1946            fragment_graph.collect_snapshot_backfill_info()?;
1947        assert!(
1948            snapshot_backfill_info
1949                .iter()
1950                .chain([&cross_db_snapshot_backfill_info])
1951                .flat_map(|info| info.upstream_mv_table_id_to_backfill_epoch.values())
1952                .all(|backfill_epoch| backfill_epoch.is_none()),
1953            "should not set backfill epoch when initially build the job: {:?} {:?}",
1954            snapshot_backfill_info,
1955            cross_db_snapshot_backfill_info
1956        );
1957
1958        let locality_fragment_state_table_mapping =
1959            fragment_graph.find_locality_provider_fragment_state_table_mapping();
1960
1961        // check if log store exists for all cross-db upstreams
1962        self.metadata_manager
1963            .catalog_controller
1964            .validate_cross_db_snapshot_backfill(&cross_db_snapshot_backfill_info)
1965            .await?;
1966
1967        let upstream_table_ids = fragment_graph
1968            .dependent_table_ids()
1969            .iter()
1970            .filter(|id| {
1971                !cross_db_snapshot_backfill_info
1972                    .upstream_mv_table_id_to_backfill_epoch
1973                    .contains_key(*id)
1974            })
1975            .cloned()
1976            .collect();
1977
1978        let upstream_root_fragments = self
1979            .metadata_manager
1980            .get_upstream_root_fragments(&upstream_table_ids)
1981            .await?;
1982
1983        if snapshot_backfill_info.is_some() {
1984            match stream_job {
1985                StreamingJob::MaterializedView(_)
1986                | StreamingJob::Sink(..)
1987                | StreamingJob::Index(_, _) => {}
1988                StreamingJob::Table(_, _, _) | StreamingJob::Source(_) => {
1989                    return Err(
1990                        anyhow!("snapshot_backfill not enabled for table and source").into(),
1991                    );
1992                }
1993            }
1994        }
1995
1996        let complete_graph = CompleteStreamFragmentGraph::with_upstreams(
1997            fragment_graph,
1998            FragmentGraphUpstreamContext {
1999                upstream_root_fragments,
2000            },
2001            (&stream_job).into(),
2002        )?;
2003        let database_resource_group = self
2004            .metadata_manager
2005            .get_database_resource_group(stream_job.database_id())
2006            .await?;
2007        let is_serverless_backfill = matches!(
2008            &resource_type,
2009            streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
2010        );
2011
2012        // 3. Build the actor graph.
2013        let actor_graph_builder = ActorGraphBuilder::new(complete_graph)?;
2014
2015        let ActorGraphBuildResult {
2016            graph,
2017            downstream_fragment_relations,
2018            upstream_fragment_downstreams,
2019            replace_upstream,
2020        } = actor_graph_builder.generate_graph()?;
2021        assert!(replace_upstream.is_empty());
2022
2023        // 4. Build the table fragments structure that will be persisted in the stream manager,
2024        // and the context that contains all information needed for building the
2025        // actors on the compute nodes.
2026
2027        let stream_job_fragments =
2028            StreamJobFragments::new(id, graph, stream_ctx.clone(), max_parallelism.get());
2029
2030        if let Some(mview_fragment) = stream_job_fragments.mview_fragment() {
2031            stream_job.set_table_vnode_count(mview_fragment.vnode_count());
2032        }
2033
2034        let new_upstream_sink = if let StreamingJob::Sink(sink, _) = &stream_job
2035            && let Ok(table_id) = sink.get_target_table()
2036        {
2037            let tables = self
2038                .metadata_manager
2039                .get_table_catalog_by_ids(&[*table_id])
2040                .await?;
2041            let target_table = tables
2042                .first()
2043                .ok_or_else(|| MetaError::catalog_id_not_found("table", *table_id))?;
2044            let sink_fragment = stream_job_fragments
2045                .sink_fragment()
2046                .ok_or_else(|| anyhow::anyhow!("sink fragment not found for sink {}", sink.id))?;
2047            let mview_fragment_id = self
2048                .metadata_manager
2049                .catalog_controller
2050                .get_mview_fragment_by_id(table_id.as_job_id())
2051                .await?;
2052            let upstream_sink_info = build_upstream_sink_info(
2053                sink.id,
2054                sink.original_target_columns.clone(),
2055                sink_fragment.fragment_id as _,
2056                target_table,
2057                mview_fragment_id,
2058            )?;
2059            Some(upstream_sink_info)
2060        } else {
2061            None
2062        };
2063
2064        let mut cdc_table_snapshot_splits = None;
2065        if let StreamingJob::Table(None, table, TableJobType::SharedCdcSource) = &stream_job
2066            && let Some((_, stream_cdc_scan)) =
2067                parallel_cdc_table_backfill_fragment(stream_job_fragments.fragments.values())
2068        {
2069            {
2070                // Create parallel splits for a CDC table. The resulted split assignments are persisted and immutable.
2071                let splits = try_init_parallel_cdc_table_snapshot_splits(
2072                    table.id,
2073                    stream_cdc_scan.cdc_table_desc.as_ref().unwrap(),
2074                    self.env.meta_store_ref(),
2075                    stream_cdc_scan.options.as_ref().unwrap(),
2076                    self.env.opts.cdc_table_split_init_insert_batch_size,
2077                    self.env.opts.cdc_table_split_init_sleep_interval_splits,
2078                    self.env.opts.cdc_table_split_init_sleep_duration_millis,
2079                )
2080                .await?;
2081                cdc_table_snapshot_splits = Some(splits);
2082            }
2083        }
2084
2085        let ctx = CreateStreamingJobContext {
2086            upstream_fragment_downstreams,
2087            database_resource_group,
2088            definition: stream_job.definition(),
2089            create_type: stream_job.create_type(),
2090            job_type: (&stream_job).into(),
2091            streaming_job: stream_job,
2092            new_upstream_sink,
2093            option: CreateStreamingJobOption {},
2094            snapshot_backfill_info,
2095            cross_db_snapshot_backfill_info,
2096            fragment_backfill_ordering,
2097            locality_fragment_state_table_mapping,
2098            cdc_table_snapshot_splits,
2099            is_serverless_backfill,
2100            resource_type,
2101            streaming_job_model: streaming_job_model.clone(),
2102            replace_sink: None,
2103            refresh_interval_sec: streaming_job_model.refresh_interval_sec.map(|s| s as u64),
2104            since_timestamp_epoch,
2105        };
2106
2107        Ok((
2108            ctx,
2109            StreamJobFragmentsToCreate {
2110                inner: stream_job_fragments,
2111                downstreams: downstream_fragment_relations,
2112            },
2113        ))
2114    }
2115
2116    /// `build_replace_table` builds a job replacement and returns the context and new job
2117    /// fragments.
2118    ///
2119    /// Note that we use a dummy ID for the new job fragments and replace it with the real one after
2120    /// replacement is finished.
2121    pub(crate) async fn build_replace_job(
2122        &self,
2123        stream_ctx: StreamContext,
2124        stream_job: &StreamingJob,
2125        mut fragment_graph: StreamFragmentGraph,
2126        tmp_job_id: JobId,
2127        auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
2128        streaming_job_model: streaming_job::Model,
2129    ) -> MetaResult<(ReplaceStreamJobContext, StreamJobFragmentsToCreate)> {
2130        match &stream_job {
2131            StreamingJob::Table(..)
2132            | StreamingJob::Source(..)
2133            | StreamingJob::MaterializedView(..) => {}
2134            StreamingJob::Sink(..) | StreamingJob::Index(..) => {
2135                bail_not_implemented!("schema change for {}", stream_job.job_type_str())
2136            }
2137        }
2138
2139        let id = stream_job.id();
2140
2141        // check if performing drop table connector
2142        let mut drop_table_associated_source_id = None;
2143        if let StreamingJob::Table(None, _, _) = &stream_job {
2144            drop_table_associated_source_id = self
2145                .metadata_manager
2146                .get_table_associated_source_id(id.as_mv_table_id())
2147                .await?;
2148        }
2149
2150        let old_fragments = self.metadata_manager.get_job_fragments_by_id(id).await?;
2151        let old_internal_table_ids = old_fragments.internal_table_ids();
2152
2153        // handle drop table's associated source
2154        let mut drop_table_connector_ctx = None;
2155        if let Some(to_remove_source_id) = drop_table_associated_source_id {
2156            let old_internal_tables = self
2157                .metadata_manager
2158                .get_table_catalog_by_ids(&old_internal_table_ids)
2159                .await?;
2160            let to_remove_state_table_ids = fragment_graph
2161                .fit_internal_tables_for_drop_connector(&old_fragments, old_internal_tables)?;
2162
2163            drop_table_connector_ctx = Some(DropTableConnectorContext {
2164                // we do not remove the original table catalog as it's still needed for the streaming job
2165                // just need to remove the refs to the state tables of the connector
2166                to_change_streaming_job_id: id,
2167                to_remove_state_table_ids,
2168                to_remove_source_id,
2169            });
2170        } else if stream_job.is_materialized_view() {
2171            // If it's ALTER MV, use `state::match` to match the internal tables, which is more complicated
2172            // but more robust.
2173            let old_fragments_upstreams = self
2174                .metadata_manager
2175                .catalog_controller
2176                .upstream_fragments(old_fragments.fragment_ids())
2177                .await?;
2178
2179            let old_state_graph =
2180                state_match::Graph::from_existing(&old_fragments, &old_fragments_upstreams);
2181            let new_state_graph = state_match::Graph::from_building(&fragment_graph);
2182            let result = state_match::match_graph(&new_state_graph, &old_state_graph)
2183                .context("incompatible altering on the streaming job states")?;
2184
2185            fragment_graph.fit_internal_table_ids_with_mapping(result.table_matches);
2186            fragment_graph.fit_snapshot_backfill_epochs(result.snapshot_backfill_epochs);
2187        } else {
2188            // If it's ALTER TABLE or SOURCE, use a trivial table id matching algorithm to keep the original behavior.
2189            // TODO(alter-mv): this is actually a special case of ALTER MV, can we merge the two branches?
2190            let old_internal_tables = self
2191                .metadata_manager
2192                .get_table_catalog_by_ids(&old_internal_table_ids)
2193                .await?;
2194            fragment_graph.fit_internal_tables_trivial(old_internal_tables)?;
2195        }
2196
2197        // 1. Resolve the edges to the downstream fragments, extend the fragment graph to a complete
2198        // graph that contains all information needed for building the actor graph.
2199        let original_root_fragment = old_fragments
2200            .root_fragment()
2201            .expect("root fragment not found");
2202
2203        let job_type = StreamingJobType::from(stream_job);
2204
2205        // Extract the downstream fragments from the fragment graph.
2206        let mut downstream_fragments = self.metadata_manager.get_downstream_fragments(id).await?;
2207
2208        if let Some(auto_refresh_schema_sinks) = &auto_refresh_schema_sinks {
2209            let mut remaining_fragment: HashSet<_> = auto_refresh_schema_sinks
2210                .iter()
2211                .map(|sink| sink.original_fragment.fragment_id)
2212                .collect();
2213            for (_, downstream_fragment) in &mut downstream_fragments {
2214                if let Some(sink) = auto_refresh_schema_sinks.iter().find(|sink| {
2215                    sink.original_fragment.fragment_id == downstream_fragment.fragment_id
2216                }) {
2217                    assert!(remaining_fragment.remove(&downstream_fragment.fragment_id));
2218                    // Actor locations will be resolved inside barrier worker during rendering.
2219                    // For now, just replace fragment info and nodes.
2220                    *downstream_fragment = sink.new_fragment.clone();
2221                }
2222            }
2223            assert!(remaining_fragment.is_empty());
2224        }
2225
2226        // build complete graph based on the table job type
2227        let complete_graph = match &job_type {
2228            StreamingJobType::Table(TableJobType::General) | StreamingJobType::Source => {
2229                CompleteStreamFragmentGraph::with_downstreams(
2230                    fragment_graph,
2231                    FragmentGraphDownstreamContext {
2232                        original_root_fragment_id: original_root_fragment.fragment_id,
2233                        downstream_fragments,
2234                    },
2235                    job_type,
2236                )?
2237            }
2238            StreamingJobType::Table(TableJobType::SharedCdcSource)
2239            | StreamingJobType::MaterializedView => {
2240                // CDC tables or materialized views can have upstream jobs as well.
2241                let upstream_root_fragments = self
2242                    .metadata_manager
2243                    .get_upstream_root_fragments(fragment_graph.dependent_table_ids())
2244                    .await?;
2245
2246                CompleteStreamFragmentGraph::with_upstreams_and_downstreams(
2247                    fragment_graph,
2248                    FragmentGraphUpstreamContext {
2249                        upstream_root_fragments,
2250                    },
2251                    FragmentGraphDownstreamContext {
2252                        original_root_fragment_id: original_root_fragment.fragment_id,
2253                        downstream_fragments,
2254                    },
2255                    job_type,
2256                )?
2257            }
2258            _ => unreachable!(),
2259        };
2260
2261        let resource_group = self
2262            .metadata_manager
2263            .get_database_resource_group(stream_job.database_id())
2264            .await?;
2265
2266        let actor_graph_builder = ActorGraphBuilder::new(complete_graph)?;
2267
2268        let ActorGraphBuildResult {
2269            graph,
2270            downstream_fragment_relations,
2271            upstream_fragment_downstreams,
2272            mut replace_upstream,
2273        } = actor_graph_builder.generate_graph()?;
2274
2275        // general table & source does not have upstream job, so the dispatchers should be empty
2276        if matches!(
2277            job_type,
2278            StreamingJobType::Source | StreamingJobType::Table(TableJobType::General)
2279        ) {
2280            assert!(upstream_fragment_downstreams.is_empty());
2281        }
2282
2283        // 3. Build the table fragments structure that will be persisted in the stream manager, and
2284        // the context that contains all information needed for building the actors on the compute
2285        // nodes.
2286        let stream_job_fragments =
2287            StreamJobFragments::new(tmp_job_id, graph, stream_ctx, old_fragments.max_parallelism);
2288
2289        if let Some(sinks) = &auto_refresh_schema_sinks {
2290            for sink in sinks {
2291                replace_upstream
2292                    .remove(&sink.new_fragment.fragment_id)
2293                    .expect("should exist");
2294            }
2295        }
2296
2297        // Note: no need to set `vnode_count` as it's already set by the frontend.
2298        // See `get_replace_table_plan`.
2299
2300        let ctx = ReplaceStreamJobContext {
2301            old_fragments,
2302            replace_upstream,
2303            upstream_fragment_downstreams,
2304            streaming_job: stream_job.clone(),
2305            database_resource_group: resource_group,
2306            tmp_id: tmp_job_id,
2307            drop_table_connector_ctx,
2308            auto_refresh_schema_sinks,
2309            streaming_job_model,
2310        };
2311
2312        Ok((
2313            ctx,
2314            StreamJobFragmentsToCreate {
2315                inner: stream_job_fragments,
2316                downstreams: downstream_fragment_relations,
2317            },
2318        ))
2319    }
2320
2321    async fn alter_name(
2322        &self,
2323        relation: alter_name_request::Object,
2324        new_name: &str,
2325    ) -> MetaResult<NotificationVersion> {
2326        let (obj_type, id): (ObjectType, ObjectId) = match relation {
2327            alter_name_request::Object::TableId(id) => (ObjectType::Table, id.into()),
2328            alter_name_request::Object::ViewId(id) => (ObjectType::View, id.into()),
2329            alter_name_request::Object::IndexId(id) => (ObjectType::Index, id.into()),
2330            alter_name_request::Object::SinkId(id) => (ObjectType::Sink, id.into()),
2331            alter_name_request::Object::SourceId(id) => (ObjectType::Source, id.into()),
2332            alter_name_request::Object::SchemaId(id) => (ObjectType::Schema, id.into()),
2333            alter_name_request::Object::DatabaseId(id) => (ObjectType::Database, id.into()),
2334            alter_name_request::Object::SubscriptionId(id) => (ObjectType::Subscription, id.into()),
2335        };
2336        self.metadata_manager
2337            .catalog_controller
2338            .alter_name(obj_type, id, new_name)
2339            .await
2340    }
2341
2342    async fn alter_swap_rename(
2343        &self,
2344        object: alter_swap_rename_request::Object,
2345    ) -> MetaResult<NotificationVersion> {
2346        let (obj_type, src_id, dst_id) = match object {
2347            alter_swap_rename_request::Object::Schema(_) => unimplemented!("schema swap"),
2348            alter_swap_rename_request::Object::Table(objs) => {
2349                let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2350                (ObjectType::Table, src_id, dst_id)
2351            }
2352            alter_swap_rename_request::Object::View(objs) => {
2353                let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2354                (ObjectType::View, src_id, dst_id)
2355            }
2356            alter_swap_rename_request::Object::Source(objs) => {
2357                let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2358                (ObjectType::Source, src_id, dst_id)
2359            }
2360            alter_swap_rename_request::Object::Sink(objs) => {
2361                let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2362                (ObjectType::Sink, src_id, dst_id)
2363            }
2364            alter_swap_rename_request::Object::Subscription(objs) => {
2365                let (src_id, dst_id) = (objs.src_object_id, objs.dst_object_id);
2366                (ObjectType::Subscription, src_id, dst_id)
2367            }
2368        };
2369
2370        self.metadata_manager
2371            .catalog_controller
2372            .alter_swap_rename(obj_type, src_id, dst_id)
2373            .await
2374    }
2375
2376    async fn alter_owner(
2377        &self,
2378        object: Object,
2379        owner_id: UserId,
2380    ) -> MetaResult<NotificationVersion> {
2381        let (obj_type, id): (ObjectType, ObjectId) = match object {
2382            Object::TableId(id) => (ObjectType::Table, id.into()),
2383            Object::ViewId(id) => (ObjectType::View, id.into()),
2384            Object::SourceId(id) => (ObjectType::Source, id.into()),
2385            Object::SinkId(id) => (ObjectType::Sink, id.into()),
2386            Object::SchemaId(id) => (ObjectType::Schema, id.into()),
2387            Object::DatabaseId(id) => (ObjectType::Database, id.into()),
2388            Object::SubscriptionId(id) => (ObjectType::Subscription, id.into()),
2389            Object::ConnectionId(id) => (ObjectType::Connection, id.into()),
2390            Object::FunctionId(id) => (ObjectType::Function, id.into()),
2391            Object::SecretId(id) => (ObjectType::Secret, id.into()),
2392        };
2393        self.metadata_manager
2394            .catalog_controller
2395            .alter_owner(obj_type, id, owner_id as _)
2396            .await
2397    }
2398
2399    async fn alter_set_schema(
2400        &self,
2401        object: alter_set_schema_request::Object,
2402        new_schema_id: SchemaId,
2403    ) -> MetaResult<NotificationVersion> {
2404        let (obj_type, id): (ObjectType, ObjectId) = match object {
2405            alter_set_schema_request::Object::TableId(id) => (ObjectType::Table, id.into()),
2406            alter_set_schema_request::Object::ViewId(id) => (ObjectType::View, id.into()),
2407            alter_set_schema_request::Object::SourceId(id) => (ObjectType::Source, id.into()),
2408            alter_set_schema_request::Object::SinkId(id) => (ObjectType::Sink, id.into()),
2409            alter_set_schema_request::Object::FunctionId(id) => (ObjectType::Function, id.into()),
2410            alter_set_schema_request::Object::ConnectionId(id) => {
2411                (ObjectType::Connection, id.into())
2412            }
2413            alter_set_schema_request::Object::SubscriptionId(id) => {
2414                (ObjectType::Subscription, id.into())
2415            }
2416        };
2417        self.metadata_manager
2418            .catalog_controller
2419            .alter_schema(obj_type, id, new_schema_id)
2420            .await
2421    }
2422
2423    pub async fn wait(&self, job_id: Option<JobId>) -> MetaResult<WaitVersion> {
2424        if let Some(job_id) = job_id {
2425            let database_id = self
2426                .metadata_manager
2427                .catalog_controller
2428                .get_object_database_id(job_id)
2429                .await?;
2430            let catalog_version = self
2431                .metadata_manager
2432                .wait_streaming_job_finished(database_id, job_id)
2433                .await?;
2434            let hummock_version_id = self.barrier_manager.get_hummock_version_id().await;
2435            return Ok(WaitVersion {
2436                catalog_version,
2437                hummock_version_id,
2438            });
2439        }
2440
2441        let timeout_ms = 2 * 60 * 60 * 1000;
2442        let poll_interval = Duration::from_millis(100);
2443        for _ in 0..(timeout_ms / poll_interval.as_millis() as usize) {
2444            let creating_jobs = self
2445                .metadata_manager
2446                .catalog_controller
2447                .list_creating_jobs(true, None)
2448                .await?;
2449            if creating_jobs.is_empty() {
2450                let catalog_version = self
2451                    .metadata_manager
2452                    .catalog_controller
2453                    .current_notification_version()
2454                    .await;
2455                let hummock_version_id = self.barrier_manager.get_hummock_version_id().await;
2456                return Ok(WaitVersion {
2457                    catalog_version,
2458                    hummock_version_id,
2459                });
2460            }
2461
2462            sleep(poll_interval).await;
2463        }
2464        Err(MetaError::cancelled(format!(
2465            "timeout after {timeout_ms}ms"
2466        )))
2467    }
2468
2469    async fn comment_on(&self, comment: Comment) -> MetaResult<NotificationVersion> {
2470        self.metadata_manager
2471            .catalog_controller
2472            .comment_on(comment)
2473            .await
2474    }
2475
2476    async fn alter_streaming_job_config(
2477        &self,
2478        job_id: JobId,
2479        entries_to_add: HashMap<String, String>,
2480        keys_to_remove: Vec<String>,
2481    ) -> MetaResult<NotificationVersion> {
2482        self.metadata_manager
2483            .catalog_controller
2484            .alter_streaming_job_config(job_id, entries_to_add, keys_to_remove)
2485            .await
2486    }
2487}
2488
2489fn report_create_object(
2490    job_id: JobId,
2491    event_name: &str,
2492    obj_type: PbTelemetryDatabaseObject,
2493    connector_name: Option<String>,
2494    attr_info: Option<jsonbb::Value>,
2495) {
2496    report_event(
2497        PbTelemetryEventStage::CreateStreamJob,
2498        event_name,
2499        job_id.as_raw_id() as _,
2500        connector_name,
2501        Some(obj_type),
2502        attr_info,
2503    );
2504}
2505
2506pub fn build_upstream_sink_info(
2507    sink_id: SinkId,
2508    original_target_columns: Vec<PbColumnCatalog>,
2509    sink_fragment_id: FragmentId,
2510    target_table: &PbTable,
2511    target_fragment_id: FragmentId,
2512) -> MetaResult<UpstreamSinkInfo> {
2513    let sink_columns = if !original_target_columns.is_empty() {
2514        original_target_columns.clone()
2515    } else {
2516        // This is due to the fact that the value did not exist in earlier versions,
2517        // which means no schema changes such as `ADD/DROP COLUMN` have been made to the table.
2518        // Therefore the columns of the table at this point are `original_target_columns`.
2519        // This value of sink will be filled on the meta.
2520        target_table.columns.clone()
2521    };
2522
2523    let sink_output_fields = sink_columns
2524        .iter()
2525        .map(|col| Field::from(col.column_desc.as_ref().unwrap()).to_prost())
2526        .collect_vec();
2527    let output_indices = (0..sink_output_fields.len())
2528        .map(|i| i as u32)
2529        .collect_vec();
2530
2531    let dist_key_indices: anyhow::Result<Vec<u32>> = try {
2532        let sink_idx_by_col_id = sink_columns
2533            .iter()
2534            .enumerate()
2535            .map(|(idx, col)| {
2536                let column_id = col.column_desc.as_ref().unwrap().column_id;
2537                (column_id, idx as u32)
2538            })
2539            .collect::<HashMap<_, _>>();
2540        target_table
2541            .distribution_key
2542            .iter()
2543            .map(|dist_idx| {
2544                let column_id = target_table.columns[*dist_idx as usize]
2545                    .column_desc
2546                    .as_ref()
2547                    .unwrap()
2548                    .column_id;
2549                let sink_idx = sink_idx_by_col_id
2550                    .get(&column_id)
2551                    .ok_or_else(|| anyhow::anyhow!("column id {} not found in sink", column_id))?;
2552                Ok(*sink_idx)
2553            })
2554            .collect::<anyhow::Result<Vec<_>>>()?
2555    };
2556    let dist_key_indices =
2557        dist_key_indices.map_err(|e| e.context("failed to get distribution key indices"))?;
2558    let downstream_fragment_id = target_fragment_id as _;
2559    let new_downstream_relation = DownstreamFragmentRelation {
2560        downstream_fragment_id,
2561        dispatcher_type: DispatcherType::Hash,
2562        dist_key_indices,
2563        output_mapping: PbDispatchOutputMapping::simple(output_indices),
2564    };
2565    let current_target_columns = target_table.get_columns();
2566    let project_exprs = build_select_node_list(&sink_columns, current_target_columns)?;
2567    Ok(UpstreamSinkInfo {
2568        sink_id,
2569        sink_fragment_id: sink_fragment_id as _,
2570        sink_output_fields,
2571        sink_original_target_columns: original_target_columns,
2572        project_exprs,
2573        new_sink_downstream: new_downstream_relation,
2574    })
2575}
2576
2577pub fn refill_upstream_sink_union_in_table(
2578    union_fragment_root: &mut PbStreamNode,
2579    upstream_sink_infos: &Vec<UpstreamSinkInfo>,
2580) {
2581    visit_stream_node_cont_mut(union_fragment_root, |node| {
2582        if let Some(NodeBody::UpstreamSinkUnion(upstream_sink_union)) = &mut node.node_body {
2583            let init_upstreams = upstream_sink_infos
2584                .iter()
2585                .map(|info| PbUpstreamSinkInfo {
2586                    upstream_fragment_id: info.sink_fragment_id,
2587                    sink_output_schema: info.sink_output_fields.clone(),
2588                    project_exprs: info.project_exprs.clone(),
2589                })
2590                .collect();
2591            upstream_sink_union.init_upstreams = init_upstreams;
2592            false
2593        } else {
2594            true
2595        }
2596    });
2597}
2598
2599#[cfg(test)]
2600mod tests {
2601    use std::num::NonZeroUsize;
2602
2603    use super::*;
2604
2605    #[test]
2606    fn test_validate_specified_parallelism_accepts_within_max() {
2607        DdlController::validate_specified_parallelism(
2608            Some(NonZeroUsize::new(4).unwrap()),
2609            Some(NonZeroUsize::new(8).unwrap()),
2610            NonZeroUsize::new(8).unwrap(),
2611        )
2612        .unwrap();
2613    }
2614
2615    #[test]
2616    fn test_validate_specified_parallelism_rejects_parallelism_over_max() {
2617        let result = DdlController::validate_specified_parallelism(
2618            Some(NonZeroUsize::new(9).unwrap()),
2619            None,
2620            NonZeroUsize::new(8).unwrap(),
2621        );
2622        assert!(matches!(
2623            result,
2624            Err(ref e) if matches!(e.inner(), MetaErrorInner::InvalidParameter(_))
2625        ));
2626    }
2627
2628    #[test]
2629    fn test_validate_specified_parallelism_rejects_backfill_parallelism_over_max() {
2630        let result = DdlController::validate_specified_parallelism(
2631            None,
2632            Some(NonZeroUsize::new(9).unwrap()),
2633            NonZeroUsize::new(8).unwrap(),
2634        );
2635        assert!(matches!(
2636            result,
2637            Err(ref e) if matches!(e.inner(), MetaErrorInner::InvalidParameter(_))
2638        ));
2639    }
2640}