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