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