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