Skip to main content

risingwave_meta/controller/catalog/
mod.rs

1// Copyright 2024 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
15mod alter_op;
16mod create_op;
17mod drop_op;
18mod get_op;
19mod list_op;
20mod test;
21mod util;
22
23use std::collections::{BTreeSet, HashMap, HashSet};
24use std::iter;
25use std::mem::take;
26use std::sync::Arc;
27
28use anyhow::{Context, anyhow};
29use itertools::Itertools;
30use risingwave_common::catalog::{
31    DEFAULT_SCHEMA_NAME, FragmentTypeFlag, FragmentTypeMask, SYSTEM_SCHEMAS, TableOption,
32};
33use risingwave_common::config::streaming::CacheRefillPolicy;
34use risingwave_common::config::{StreamingConfig, merge_streaming_config_section};
35use risingwave_common::current_cluster_version;
36use risingwave_common::id::JobId;
37use risingwave_common::secret::LocalSecretManager;
38use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont_mut;
39use risingwave_connector::source::UPSTREAM_SOURCE_KEY;
40use risingwave_connector::source::cdc::build_cdc_table_id;
41use risingwave_meta_model::object::ObjectType;
42use risingwave_meta_model::prelude::*;
43use risingwave_meta_model::table::TableType;
44use risingwave_meta_model::{
45    ColumnCatalogArray, ConnectionId, CreateType, DatabaseId, FragmentId, I32Array, IndexId,
46    JobStatus, ObjectId, Property, SchemaId, SecretId, SinkFormatDesc, SinkId, SourceId,
47    StreamNode, StreamSourceInfo, StreamingParallelism, SubscriptionId, TableId, TableIdArray,
48    UserId, ViewId, connection, database, fragment, function, index, object, object_dependency,
49    pending_sink_state, schema, secret, sink, source, streaming_job, subscription, table,
50    user_privilege, view,
51};
52use risingwave_pb::catalog::connection::Info as ConnectionInfo;
53use risingwave_pb::catalog::subscription::SubscriptionState;
54use risingwave_pb::catalog::table::{OptionalAssociatedSourceId, PbTableType};
55use risingwave_pb::catalog::{
56    PbComment, PbConnection, PbDatabase, PbFunction, PbIndex, PbSchema, PbSecret, PbSink, PbSource,
57    PbSubscription, PbTable, PbView,
58};
59use risingwave_pb::meta::cancel_creating_jobs_request::PbCreatingJobInfo;
60use risingwave_pb::meta::object::PbObjectInfo;
61use risingwave_pb::meta::subscribe_response::{
62    Info as NotificationInfo, Info, Operation as NotificationOperation, Operation,
63};
64use risingwave_pb::meta::table_cache_refill_policies::PbTableCacheRefillPolicy;
65use risingwave_pb::meta::{
66    PbObject, PbObjectGroup, PbTableCacheRefillPolicies, PbTableRefillRuntimeConfig,
67};
68use risingwave_pb::telemetry::PbTelemetryEventStage;
69use risingwave_pb::user::PbUserInfo;
70use sea_orm::ActiveValue::Set;
71use sea_orm::sea_query::{Expr, Query, SimpleExpr};
72use sea_orm::{
73    ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
74    IntoActiveModel, JoinType, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait,
75    SelectColumns, TransactionTrait, Value,
76};
77use tokio::sync::oneshot::Sender;
78use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
79use tracing::info;
80
81pub(crate) use self::util::load_object_models;
82use super::utils::{
83    check_subscription_name_duplicate, get_internal_tables_by_id, load_streaming_jobs_by_ids,
84    rename_relation, rename_relation_refer,
85};
86use crate::controller::ObjectModel;
87use crate::controller::catalog::util::{
88    prepare_object_models_for_schema_change, update_internal_tables,
89};
90use crate::controller::fragment::FragmentTypeMaskExt;
91use crate::controller::utils::*;
92use crate::manager::{
93    FrontendNotificationBatch, IGNORED_NOTIFICATION_VERSION, MetaSrvEnv, NotificationVersion,
94    get_referred_connection_ids_from_source, get_referred_secret_ids_from_source,
95};
96use crate::rpc::ddl_controller::DropMode;
97use crate::telemetry::{MetaTelemetryJobDesc, report_event};
98use crate::{MetaError, MetaResult};
99
100pub type Catalog = (
101    Vec<PbDatabase>,
102    Vec<PbSchema>,
103    Vec<PbTable>,
104    Vec<PbSource>,
105    Vec<PbSink>,
106    Vec<PbSubscription>,
107    Vec<PbIndex>,
108    Vec<PbView>,
109    Vec<PbFunction>,
110    Vec<PbConnection>,
111    Vec<PbSecret>,
112);
113
114pub type CatalogControllerRef = Arc<CatalogController>;
115
116const STREAMING_CACHE_REFILL_POLICY_CONFIG_PATH: &str = "streaming.developer.cache_refill_policy";
117
118/// `CatalogController` is the controller for catalog related operations, including database, schema, table, view, etc.
119pub struct CatalogController {
120    pub(crate) env: MetaSrvEnv,
121    pub(crate) inner: RwLock<CatalogControllerInner>,
122}
123
124#[derive(Clone, Default, Debug)]
125pub struct DropTableConnectorContext {
126    // we only apply one drop connector action for one table each time, so no need to vector here
127    pub(crate) to_change_streaming_job_id: JobId,
128    pub(crate) to_remove_state_table_ids: Vec<TableId>,
129    pub(crate) to_remove_source_id: SourceId,
130}
131
132#[derive(Clone, Default, Debug)]
133pub struct ReleaseContext {
134    pub(crate) database_id: DatabaseId,
135    pub(crate) removed_streaming_job_ids: Vec<JobId>,
136    /// Dropped state table list, need to unregister from hummock.
137    pub(crate) removed_state_table_ids: Vec<TableId>,
138
139    /// Dropped secrets, need to remove from secret manager.
140    pub(crate) removed_secret_ids: Vec<SecretId>,
141    /// Dropped sources (when `DROP SOURCE`), need to unregister from source manager.
142    pub(crate) removed_source_ids: Vec<SourceId>,
143    /// Dropped Source fragments (when `DROP MATERIALIZED VIEW` referencing sources),
144    /// need to unregister from source manager.
145    pub(crate) removed_source_fragments: HashMap<SourceId, BTreeSet<FragmentId>>,
146
147    pub(crate) removed_fragments: HashSet<FragmentId>,
148
149    /// Removed sink fragment by target fragment.
150    pub(crate) removed_sink_fragment_by_targets: HashMap<FragmentId, Vec<FragmentId>>,
151
152    /// Dropped iceberg table sinks
153    pub(crate) removed_iceberg_table_sinks: Vec<PbSink>,
154
155    /// Dropped iceberg sink ids. Retire coordinator registrations and all maintenance together.
156    pub(crate) removed_iceberg_sink_ids: Vec<SinkId>,
157}
158
159#[derive(Default)]
160pub(crate) struct CleanedDirtyStreamingJobs {
161    pub(crate) streaming_job_ids: Vec<JobId>,
162    /// Only populated for per-database recovery.
163    pub(crate) dropped_table_ids: Vec<TableId>,
164    pub(crate) source_ids: Vec<SourceId>,
165    /// Cleaned dirty sink jobs, whose iceberg maintenance state must be cleared.
166    pub(crate) sink_ids: Vec<SinkId>,
167}
168
169fn explicit_cache_refill_policy(config_override: &str) -> MetaResult<Option<CacheRefillPolicy>> {
170    if config_override.trim().is_empty() {
171        return Ok(None);
172    }
173
174    let table: toml::Table =
175        toml::from_str(config_override).context("invalid streaming job config override")?;
176    let has_explicit_policy = table
177        .get("streaming")
178        .and_then(toml::Value::as_table)
179        .and_then(|table| table.get("developer"))
180        .and_then(toml::Value::as_table)
181        .is_some_and(|table| table.contains_key("cache_refill_policy"));
182    if !has_explicit_policy {
183        return Ok(None);
184    }
185
186    let merged = merge_streaming_config_section(&StreamingConfig::default(), config_override)
187        .context("invalid streaming job config override")?
188        .context("empty streaming job config override")?;
189    Ok(Some(merged.developer.cache_refill_policy))
190}
191
192impl CatalogControllerInner {
193    pub(crate) async fn table_cache_refill_policies_snapshot_if_job_has_explicit_policy(
194        &self,
195        job_id: JobId,
196    ) -> MetaResult<Option<PbTableCacheRefillPolicies>> {
197        let config_override: Option<String> = StreamingJob::find_by_id(job_id)
198            .select_only()
199            .column(streaming_job::Column::ConfigOverride)
200            .into_tuple()
201            .one(&self.db)
202            .await?
203            .ok_or_else(|| MetaError::catalog_id_not_found("streaming job", job_id))?;
204
205        if explicit_cache_refill_policy(&config_override.unwrap_or_default())?.is_none() {
206            return Ok(None);
207        }
208
209        Ok(Some(self.table_cache_refill_policies_snapshot().await?))
210    }
211
212    pub async fn table_cache_refill_policies_snapshot(
213        &self,
214    ) -> MetaResult<PbTableCacheRefillPolicies> {
215        let job_configs = StreamingJob::find()
216            .select_only()
217            .column(streaming_job::Column::JobId)
218            .column(streaming_job::Column::ConfigOverride)
219            .filter(
220                streaming_job::Column::JobId.in_subquery(
221                    Query::select()
222                        .distinct()
223                        .column(fragment::Column::JobId)
224                        .from(Fragment)
225                        .to_owned(),
226                ),
227            )
228            .into_tuple::<(JobId, Option<String>)>()
229            .all(&self.db)
230            .await?;
231        let mut policies_by_job = HashMap::new();
232        for (job_id, config_override) in job_configs {
233            if let Some(policy) =
234                explicit_cache_refill_policy(&config_override.unwrap_or_default())?
235            {
236                policies_by_job.insert(job_id, policy);
237            }
238        }
239        if policies_by_job.is_empty() {
240            return Ok(PbTableCacheRefillPolicies::default());
241        }
242
243        let result_table_ids = policies_by_job
244            .keys()
245            .map(|job_id| job_id.as_mv_table_id())
246            .collect_vec();
247        let tables: Vec<(TableId, TableType, Option<JobId>)> = Table::find()
248            .select_only()
249            .column(table::Column::TableId)
250            .column(table::Column::TableType)
251            .column(table::Column::BelongsToJobId)
252            .filter(
253                table::Column::TableType
254                    .eq(TableType::Internal)
255                    .and(table::Column::BelongsToJobId.is_in(policies_by_job.keys().copied()))
256                    .or(table::Column::TableId.is_in(result_table_ids)),
257            )
258            .into_tuple()
259            .all(&self.db)
260            .await?;
261
262        let mut table_policies = Vec::new();
263        let mut internal_table_policies = Vec::new();
264        for (table_id, table_type, belongs_to_job_id) in tables {
265            if table_type == TableType::Internal {
266                let Some(policy) =
267                    belongs_to_job_id.and_then(|job_id| policies_by_job.get(&job_id))
268                else {
269                    continue;
270                };
271                internal_table_policies.push(PbTableCacheRefillPolicy {
272                    table_id: table_id.as_raw_id(),
273                    policy: policy.to_protobuf() as i32,
274                });
275                continue;
276            }
277
278            let Some(policy) = policies_by_job.get(&table_id.as_job_id()) else {
279                continue;
280            };
281            table_policies.push(PbTableCacheRefillPolicy {
282                table_id: table_id.as_raw_id(),
283                policy: policy.to_protobuf() as i32,
284            });
285        }
286        table_policies.sort_unstable_by_key(|policy| policy.table_id);
287        internal_table_policies.sort_unstable_by_key(|policy| policy.table_id);
288
289        Ok(PbTableCacheRefillPolicies {
290            table_policies,
291            internal_table_policies,
292        })
293    }
294}
295
296impl CatalogController {
297    pub(crate) fn frontend_notification_batch(&self) -> FrontendNotificationBatch<'_> {
298        FrontendNotificationBatch::new(self.env.notification_manager())
299    }
300
301    pub(crate) async fn notify_hummock_table_cache_refill_policy_if_explicit(
302        &self,
303        inner: &CatalogControllerInner,
304        job_id: JobId,
305    ) -> MetaResult<()> {
306        if let Some(policies) = inner
307            .table_cache_refill_policies_snapshot_if_job_has_explicit_policy(job_id)
308            .await?
309        {
310            self.env.notification_manager().notify_hummock(
311                NotificationOperation::Update,
312                NotificationInfo::TableRefillRuntimeConfig(PbTableRefillRuntimeConfig {
313                    table_cache_refill_policies: Some(policies),
314                    ..Default::default()
315                }),
316            );
317        }
318        Ok(())
319    }
320
321    pub async fn table_cache_refill_policies_snapshot(
322        &self,
323    ) -> MetaResult<PbTableCacheRefillPolicies> {
324        let inner = self.inner.read().await;
325        inner.table_cache_refill_policies_snapshot().await
326    }
327
328    pub async fn new(env: MetaSrvEnv) -> MetaResult<Self> {
329        let meta_store = env.meta_store();
330        let catalog_controller = Self {
331            env,
332            inner: RwLock::new(CatalogControllerInner {
333                db: meta_store.conn,
334                creating_table_finish_notifier: HashMap::new(),
335                dropped_tables: HashMap::new(),
336            }),
337        };
338
339        catalog_controller.init().await?;
340        Ok(catalog_controller)
341    }
342
343    /// Used in `NotificationService::subscribe`.
344    /// Need to pay attention to the order of acquiring locks to prevent deadlock problems.
345    pub async fn get_inner_read_guard(&self) -> RwLockReadGuard<'_, CatalogControllerInner> {
346        self.inner.read().await
347    }
348
349    pub async fn get_inner_write_guard(&self) -> RwLockWriteGuard<'_, CatalogControllerInner> {
350        self.inner.write().await
351    }
352}
353
354pub struct CatalogControllerInner {
355    pub(crate) db: DatabaseConnection,
356    /// Registered finish notifiers for creating tables.
357    ///
358    /// `DdlController` will update this map, and pass the `tx` side to `CatalogController`.
359    /// On notifying, we can remove the entry from this map.
360    #[expect(clippy::type_complexity)]
361    pub creating_table_finish_notifier:
362        HashMap<DatabaseId, HashMap<JobId, Vec<Sender<Result<NotificationVersion, String>>>>>,
363    /// Tables have been dropped from the meta store, but the corresponding barrier remains unfinished.
364    pub dropped_tables: HashMap<TableId, PbTable>,
365}
366
367impl CatalogController {
368    #[must_use]
369    pub(crate) async fn notify_frontend(
370        &self,
371        operation: NotificationOperation,
372        info: NotificationInfo,
373    ) -> NotificationVersion {
374        let mut notifications = self.frontend_notification_batch();
375        notifications.add(operation, info);
376        notifications
377            .publish()
378            .await
379            .expect("the frontend notification batch contains the added notification")
380    }
381
382    #[must_use]
383    pub(crate) async fn notify_frontend_relation_info(
384        &self,
385        operation: NotificationOperation,
386        relation_info: PbObjectInfo,
387    ) -> NotificationVersion {
388        self.notify_frontend(
389            operation,
390            NotificationInfo::ObjectGroup(PbObjectGroup {
391                objects: vec![PbObject {
392                    object_info: relation_info.into(),
393                }],
394                dependencies: vec![],
395            }),
396        )
397        .await
398    }
399
400    /// Return the latest version published to frontend catalog observers.
401    pub(crate) async fn current_notification_version(&self) -> NotificationVersion {
402        self.env.notification_manager().current_version().await
403    }
404}
405
406impl CatalogController {
407    pub async fn finish_create_subscription_catalog(
408        &self,
409        subscription_id: SubscriptionId,
410    ) -> MetaResult<()> {
411        let inner = self.inner.write().await;
412        let txn = inner.db.begin().await?;
413
414        // update `created_at` as now() and `created_at_cluster_version` as current cluster version.
415        let res = Object::update_many()
416            .col_expr(object::Column::CreatedAt, Expr::current_timestamp().into())
417            .col_expr(
418                object::Column::CreatedAtClusterVersion,
419                current_cluster_version().into(),
420            )
421            .filter(object::Column::Oid.eq(subscription_id))
422            .exec(&txn)
423            .await?;
424        if res.rows_affected == 0 {
425            return Err(MetaError::catalog_id_not_found(
426                "subscription",
427                subscription_id,
428            ));
429        }
430
431        // mark the target subscription as `Create`.
432        let job = subscription::ActiveModel {
433            subscription_id: Set(subscription_id),
434            subscription_state: Set(SubscriptionState::Created.into()),
435            ..Default::default()
436        };
437        Subscription::update(job).exec(&txn).await?;
438
439        let _ = grant_default_privileges_automatically(&txn, subscription_id).await?;
440
441        txn.commit().await?;
442
443        Ok(())
444    }
445
446    pub async fn notify_create_subscription(
447        &self,
448        subscription_id: SubscriptionId,
449    ) -> MetaResult<NotificationVersion> {
450        let inner = self.inner.read().await;
451        let txn = inner.db.begin().await?;
452        let (subscription, obj) = Subscription::find_by_id(subscription_id)
453            .find_also_related(Object)
454            .filter(subscription::Column::SubscriptionState.eq(SubscriptionState::Created as i32))
455            .one(&txn)
456            .await?
457            .ok_or_else(|| MetaError::catalog_id_not_found("subscription", subscription_id))?;
458
459        let dependencies =
460            list_object_dependencies_by_object_id(&txn, subscription_id.into()).await?;
461        txn.commit().await?;
462
463        let mut notifications = self.frontend_notification_batch();
464        notifications.add(
465            NotificationOperation::Add,
466            NotificationInfo::ObjectGroup(PbObjectGroup {
467                objects: vec![PbObject {
468                    object_info: PbObjectInfo::Subscription(
469                        ObjectModel(subscription, obj.unwrap(), None).into(),
470                    )
471                    .into(),
472                }],
473                dependencies,
474            }),
475        );
476
477        // notify default privileges about the new subscription
478        let updated_user_ids: Vec<UserId> = UserPrivilege::find()
479            .select_only()
480            .distinct()
481            .column(user_privilege::Column::UserId)
482            .filter(user_privilege::Column::Oid.eq(subscription_id.as_object_id()))
483            .into_tuple()
484            .all(&inner.db)
485            .await?;
486
487        if !updated_user_ids.is_empty() {
488            let updated_user_infos = list_user_info_by_ids(updated_user_ids, &inner.db).await?;
489            Self::add_users_update_notifications(&mut notifications, updated_user_infos);
490        }
491        let version = notifications
492            .publish()
493            .await
494            .expect("the subscription notification batch contains the subscription");
495
496        Ok(version)
497    }
498
499    // for telemetry
500    pub async fn get_connector_usage(&self) -> MetaResult<jsonbb::Value> {
501        // get connector usage by source/sink
502        // the expect format is like:
503        // {
504        //     "source": [{
505        //         "$source_id": {
506        //             "connector": "kafka",
507        //             "format": "plain",
508        //             "encode": "json"
509        //         },
510        //     }],
511        //     "sink": [{
512        //         "$sink_id": {
513        //             "connector": "pulsar",
514        //             "format": "upsert",
515        //             "encode": "avro"
516        //         },
517        //     }],
518        // }
519
520        let inner = self.inner.read().await;
521        let source_props_and_info: Vec<(i32, Property, Option<StreamSourceInfo>)> = Source::find()
522            .select_only()
523            .column(source::Column::SourceId)
524            .column(source::Column::WithProperties)
525            .column(source::Column::SourceInfo)
526            .into_tuple()
527            .all(&inner.db)
528            .await?;
529        let sink_props_and_info: Vec<(i32, Property, Option<SinkFormatDesc>)> = Sink::find()
530            .select_only()
531            .column(sink::Column::SinkId)
532            .column(sink::Column::Properties)
533            .column(sink::Column::SinkFormatDesc)
534            .into_tuple()
535            .all(&inner.db)
536            .await?;
537        drop(inner);
538
539        let get_connector_from_property = |property: &Property| -> String {
540            property
541                .0
542                .get(UPSTREAM_SOURCE_KEY)
543                .cloned()
544                .unwrap_or_default()
545        };
546
547        let source_report: Vec<jsonbb::Value> = source_props_and_info
548            .iter()
549            .map(|(oid, property, info)| {
550                let connector_name = get_connector_from_property(property);
551                let mut format = None;
552                let mut encode = None;
553                if let Some(info) = info {
554                    let pb_info = info.to_protobuf();
555                    format = Some(pb_info.format().as_str_name());
556                    encode = Some(pb_info.row_encode().as_str_name());
557                }
558                jsonbb::json!({
559                    oid.to_string(): {
560                        "connector": connector_name,
561                        "format": format,
562                        "encode": encode,
563                    },
564                })
565            })
566            .collect_vec();
567
568        let sink_report: Vec<jsonbb::Value> = sink_props_and_info
569            .iter()
570            .map(|(oid, property, info)| {
571                let connector_name = get_connector_from_property(property);
572                let mut format = None;
573                let mut encode = None;
574                if let Some(info) = info {
575                    let pb_info = info.to_protobuf();
576                    format = Some(pb_info.format().as_str_name());
577                    encode = Some(pb_info.encode().as_str_name());
578                }
579                jsonbb::json!({
580                    oid.to_string(): {
581                        "connector": connector_name,
582                        "format": format,
583                        "encode": encode,
584                    },
585                })
586            })
587            .collect_vec();
588
589        Ok(jsonbb::json!({
590                "source": source_report,
591                "sink": sink_report,
592        }))
593    }
594
595    pub async fn clean_dirty_subscription(
596        &self,
597        database_id: Option<DatabaseId>,
598    ) -> MetaResult<()> {
599        let inner = self.inner.write().await;
600        let txn = inner.db.begin().await?;
601        let filter_condition = object::Column::ObjType.eq(ObjectType::Subscription).and(
602            object::Column::Oid.not_in_subquery(
603                Query::select()
604                    .column(subscription::Column::SubscriptionId)
605                    .from(Subscription)
606                    .and_where(
607                        subscription::Column::SubscriptionState
608                            .eq(SubscriptionState::Created as i32),
609                    )
610                    .take(),
611            ),
612        );
613
614        let filter_condition = if let Some(database_id) = database_id {
615            filter_condition.and(object::Column::DatabaseId.eq(database_id))
616        } else {
617            filter_condition
618        };
619        Object::delete_many()
620            .filter(filter_condition)
621            .exec(&txn)
622            .await?;
623        txn.commit().await?;
624        // We don't need to notify the frontend, because the Init subscription is not send to frontend.
625        Ok(())
626    }
627
628    /// Cleans up jobs in `Initial` status. Creating jobs are cleaned up when their creation
629    /// progress cannot be recovered, or when `clean_all_foreground_jobs_on_recovery` is enabled
630    /// and they use foreground creation.
631    pub(crate) async fn clean_dirty_creating_jobs(
632        &self,
633        database_id: Option<DatabaseId>,
634    ) -> MetaResult<CleanedDirtyStreamingJobs> {
635        let mut inner = self.inner.write().await;
636        let txn = inner.db.begin().await?;
637        let clean_all_foreground_jobs = self.env.opts.clean_all_foreground_jobs_on_recovery;
638
639        let candidate_job_filter =
640            streaming_job::Column::JobStatus.is_in([JobStatus::Initial, JobStatus::Creating]);
641        let candidate_job_filter = if let Some(database_id) = database_id {
642            candidate_job_filter.and(object::Column::DatabaseId.eq(database_id))
643        } else {
644            candidate_job_filter
645        };
646        type CandidateJob = (
647            JobId,
648            JobStatus,
649            CreateType,
650            ObjectId,
651            ObjectType,
652            Option<SchemaId>,
653            Option<DatabaseId>,
654        );
655        let candidate_jobs: Vec<CandidateJob> = streaming_job::Entity::find()
656            .select_only()
657            .columns([
658                streaming_job::Column::JobId,
659                streaming_job::Column::JobStatus,
660                streaming_job::Column::CreateType,
661            ])
662            .columns([
663                object::Column::Oid,
664                object::Column::ObjType,
665                object::Column::SchemaId,
666                object::Column::DatabaseId,
667            ])
668            .join(JoinType::InnerJoin, streaming_job::Relation::Object.def())
669            .filter(candidate_job_filter)
670            .into_tuple()
671            .all(&txn)
672            .await?;
673        let creating_job_ids = candidate_jobs
674            .iter()
675            .filter(|(_, job_status, create_type, ..)| {
676                *job_status == JobStatus::Creating
677                    && (!clean_all_foreground_jobs || *create_type == CreateType::Foreground)
678            })
679            .map(|(job_id, ..)| *job_id)
680            .collect_vec();
681
682        let dirty_creating_job_ids: HashSet<JobId> =
683            if clean_all_foreground_jobs || creating_job_ids.is_empty() {
684                creating_job_ids.into_iter().collect()
685            } else {
686                Fragment::find()
687                    .select_only()
688                    .column(fragment::Column::JobId)
689                    .filter(fragment::Column::JobId.is_in(creating_job_ids))
690                    .filter(FragmentTypeMask::contains_non_recoverable_fragment())
691                    .into_tuple()
692                    .all(&txn)
693                    .await?
694                    .into_iter()
695                    .collect()
696            };
697
698        let mut dirty_job_objs = candidate_jobs
699            .into_iter()
700            .filter(|(job_id, job_status, ..)| {
701                *job_status == JobStatus::Initial || dirty_creating_job_ids.contains(job_id)
702            })
703            .map(
704                |(_, _, _, oid, obj_type, schema_id, database_id)| PartialObject {
705                    oid,
706                    obj_type,
707                    schema_id,
708                    database_id,
709                },
710            )
711            .collect_vec();
712
713        // Check if there are any pending iceberg table jobs.
714        let dirty_iceberg_jobs = find_dirty_iceberg_table_jobs(&txn, database_id).await?;
715        if !dirty_iceberg_jobs.is_empty() {
716            dirty_job_objs.extend(dirty_iceberg_jobs);
717        }
718
719        Self::clean_dirty_sink_downstreams(&txn).await?;
720
721        if dirty_job_objs.is_empty() {
722            return Ok(CleanedDirtyStreamingJobs::default());
723        }
724
725        self.log_cleaned_dirty_jobs(&dirty_job_objs, &txn).await?;
726
727        let mut dirty_objects = Object::find()
728            .filter(object::Column::Oid.is_in(dirty_job_objs.iter().map(|obj| obj.oid)))
729            .all(&txn)
730            .await?;
731        dirty_objects.extend(
732            get_belong_objects_by_ids(&txn, dirty_job_objs.iter().map(|obj| obj.oid)).await?,
733        );
734        let dirty_object_ids = dirty_objects
735            .iter()
736            .map(|object| object.oid)
737            .collect::<HashSet<_>>();
738        let dirty_catalog_models = load_object_models(&txn, &dirty_objects).await?;
739        let dirty_job_ids = StreamingJob::find()
740            .select_only()
741            .column(streaming_job::Column::JobId)
742            .filter(streaming_job::Column::JobId.is_in(dirty_object_ids.iter().copied()))
743            .into_tuple()
744            .all(&txn)
745            .await?;
746        let dirty_sink_ids = dirty_objects
747            .iter()
748            .filter(|object| object.obj_type == ObjectType::Sink)
749            .map(|object| object.oid.as_sink_id())
750            .collect_vec();
751        // Object deletion cascades to the fragments of the dirty streaming jobs. Keep their IDs
752        // before the transaction deletes them so the serving mapping can be notified after commit.
753        let dirty_fragment_ids: Vec<FragmentId> = Fragment::find()
754            .select_only()
755            .column(fragment::Column::FragmentId)
756            .filter(fragment::Column::JobId.is_in(dirty_job_ids.iter().copied()))
757            .into_tuple()
758            .all(&txn)
759            .await?;
760        // Frontend deletion is idempotent, so notify every dirty job even if frontend did not
761        // observe its creating catalog or it is a temporary replacement object.
762        let to_notify_objs = dirty_job_objs.clone();
763        let mut objects_to_notify = to_notify_objs.clone();
764        objects_to_notify.extend(
765            get_belong_objects_by_ids(&txn, to_notify_objs.iter().map(|obj| obj.oid))
766                .await?
767                .into_iter()
768                .map(PartialObject::from),
769        );
770
771        // The source ids for dirty tables with connector.
772        // FIXME: we should also clean dirty sources.
773        let dirty_associated_source_ids: Vec<SourceId> = dirty_catalog_models
774            .iter()
775            .filter_map(|object_info| match object_info {
776                PbObjectInfo::Table(table) => {
777                    table
778                        .optional_associated_source_id
779                        .map(|source_id| match source_id {
780                            OptionalAssociatedSourceId::AssociatedSourceId(source_id) => source_id,
781                        })
782                }
783                _ => None,
784            })
785            .collect_vec();
786        // Per-database recovery does not run the global Hummock purge. Keep table catalogs so the
787        // caller can reuse the normal dropped-table cleanup path after the catalog rows are deleted.
788        let dropped_tables = if database_id.is_some() {
789            dirty_catalog_models
790                .iter()
791                .filter_map(|object_info| match object_info {
792                    PbObjectInfo::Table(table) => Some(table.clone()),
793                    _ => None,
794                })
795                .collect_vec()
796        } else {
797            vec![]
798        };
799        let dropped_table_ids = dropped_tables.iter().map(|table| table.id).collect_vec();
800
801        let res = Object::delete_many()
802            .filter(object::Column::Oid.is_in(dirty_job_objs.iter().map(|obj| obj.oid)))
803            .exec(&txn)
804            .await?;
805        assert!(res.rows_affected > 0);
806
807        txn.commit().await?;
808        inner
809            .dropped_tables
810            .extend(dropped_tables.into_iter().map(|t| (t.id, t)));
811
812        self.env
813            .notification_manager()
814            .notify_serving_fragment_mapping_delete(dirty_fragment_ids);
815
816        let object_group = build_object_group_for_delete(objects_to_notify);
817
818        let _version = self
819            .notify_frontend(NotificationOperation::Delete, object_group)
820            .await;
821
822        Ok(CleanedDirtyStreamingJobs {
823            streaming_job_ids: dirty_job_ids,
824            dropped_table_ids,
825            source_ids: dirty_associated_source_ids,
826            sink_ids: dirty_sink_ids,
827        })
828    }
829
830    pub async fn comment_on(&self, comment: PbComment) -> MetaResult<NotificationVersion> {
831        let inner = self.inner.write().await;
832        let txn = inner.db.begin().await?;
833        ensure_object_id(ObjectType::Database, comment.database_id, &txn).await?;
834        ensure_object_id(ObjectType::Schema, comment.schema_id, &txn).await?;
835        let (table_obj, streaming_job) = Object::find_by_id(comment.table_id)
836            .find_also_related(StreamingJob)
837            .one(&txn)
838            .await?
839            .ok_or_else(|| MetaError::catalog_id_not_found("object", comment.table_id))?;
840
841        let table = if let Some(col_idx) = comment.column_index {
842            let columns: ColumnCatalogArray = Table::find_by_id(comment.table_id)
843                .select_only()
844                .column(table::Column::Columns)
845                .into_tuple()
846                .one(&txn)
847                .await?
848                .ok_or_else(|| MetaError::catalog_id_not_found("table", comment.table_id))?;
849            let mut pb_columns = columns.to_protobuf();
850
851            let column = pb_columns
852                .get_mut(col_idx as usize)
853                .ok_or_else(|| MetaError::catalog_id_not_found("column", col_idx))?;
854            let column_desc = column.column_desc.as_mut().ok_or_else(|| {
855                anyhow!(
856                    "column desc at index {} for table id {} not found",
857                    col_idx,
858                    comment.table_id
859                )
860            })?;
861            column_desc.description = comment.description;
862            table::ActiveModel {
863                table_id: Set(comment.table_id),
864                columns: Set(pb_columns.into()),
865                ..Default::default()
866            }
867            .update(&txn)
868            .await?
869        } else {
870            table::ActiveModel {
871                table_id: Set(comment.table_id),
872                description: Set(comment.description),
873                ..Default::default()
874            }
875            .update(&txn)
876            .await?
877        };
878        txn.commit().await?;
879
880        let version = self
881            .notify_frontend_relation_info(
882                NotificationOperation::Update,
883                PbObjectInfo::Table(ObjectModel(table, table_obj, streaming_job).into()),
884            )
885            .await;
886
887        Ok(version)
888    }
889
890    fn notify_hummock_dropped_tables(&self, tables: Vec<PbTable>) {
891        if tables.is_empty() {
892            return;
893        }
894        let objects = tables
895            .into_iter()
896            .map(|t| PbObject {
897                object_info: Some(PbObjectInfo::Table(t)),
898            })
899            .collect();
900        let group = NotificationInfo::ObjectGroup(PbObjectGroup {
901            objects,
902            dependencies: vec![],
903        });
904        self.env
905            .notification_manager()
906            .notify_hummock(NotificationOperation::Delete, group.clone());
907        self.env
908            .notification_manager()
909            .notify_compactor(NotificationOperation::Delete, group);
910    }
911
912    pub async fn complete_dropped_tables(&self, table_ids: impl IntoIterator<Item = TableId>) {
913        let mut inner = self.inner.write().await;
914        let tables = inner.complete_dropped_tables(table_ids);
915        self.notify_hummock_dropped_tables(tables);
916    }
917
918    pub async fn cleanup_dropped_tables(&self) {
919        let mut inner = self.inner.write().await;
920        let tables = inner.dropped_tables.drain().map(|(_, t)| t).collect();
921        self.notify_hummock_dropped_tables(tables);
922    }
923
924    pub async fn stats(&self) -> MetaResult<CatalogStats> {
925        let inner = self.inner.read().await;
926
927        let mut table_num_map: HashMap<_, _> = Table::find()
928            .select_only()
929            .column(table::Column::TableType)
930            .column_as(table::Column::TableId.count(), "num")
931            .group_by(table::Column::TableType)
932            .having(table::Column::TableType.ne(TableType::Internal))
933            .into_tuple::<(TableType, i64)>()
934            .all(&inner.db)
935            .await?
936            .into_iter()
937            .map(|(table_type, num)| (table_type, num as u64))
938            .collect();
939
940        let source_num = Source::find().count(&inner.db).await?;
941        let sink_num = Sink::find().count(&inner.db).await?;
942        let function_num = Function::find().count(&inner.db).await?;
943        let streaming_job_num = StreamingJob::find().count(&inner.db).await?;
944
945        let actor_num = {
946            let guard = self.env.shared_actor_info.read_guard();
947            guard
948                .iter_over_fragments()
949                .map(|(_, fragment)| fragment.actors.len() as u64)
950                .sum::<u64>()
951        };
952        let database_num = Database::find().count(&inner.db).await?;
953
954        Ok(CatalogStats {
955            table_num: table_num_map.remove(&TableType::Table).unwrap_or(0),
956            mview_num: table_num_map
957                .remove(&TableType::MaterializedView)
958                .unwrap_or(0),
959            index_num: table_num_map.remove(&TableType::Index).unwrap_or(0),
960            source_num,
961            sink_num,
962            function_num,
963            streaming_job_num,
964            actor_num,
965            database_num,
966        })
967    }
968
969    pub async fn fetch_sink_with_state_table_ids(
970        &self,
971        sink_ids: HashSet<SinkId>,
972    ) -> MetaResult<HashMap<SinkId, Vec<TableId>>> {
973        let inner = self.inner.read().await;
974
975        let query = Fragment::find()
976            .select_only()
977            .columns([fragment::Column::JobId, fragment::Column::StateTableIds])
978            .filter(
979                fragment::Column::JobId
980                    .is_in(sink_ids)
981                    .and(FragmentTypeMask::intersects(FragmentTypeFlag::Sink)),
982            );
983
984        let rows: Vec<(JobId, TableIdArray)> = query.into_tuple().all(&inner.db).await?;
985
986        debug_assert!(rows.iter().map(|(job_id, _)| job_id).all_unique());
987
988        let result = rows
989            .into_iter()
990            .map(|(job_id, table_id_array)| (job_id.as_sink_id(), table_id_array.0))
991            .collect::<HashMap<_, _>>();
992
993        Ok(result)
994    }
995
996    pub async fn list_all_pending_sinks(
997        &self,
998        database_id: Option<DatabaseId>,
999    ) -> MetaResult<HashSet<SinkId>> {
1000        let inner = self.inner.read().await;
1001
1002        let mut query = pending_sink_state::Entity::find()
1003            .select_only()
1004            .columns([pending_sink_state::Column::SinkId])
1005            .filter(
1006                pending_sink_state::Column::SinkState.eq(pending_sink_state::SinkState::Pending),
1007            )
1008            .distinct();
1009
1010        if let Some(db_id) = database_id {
1011            query = query
1012                .join(
1013                    JoinType::InnerJoin,
1014                    pending_sink_state::Relation::Object.def(),
1015                )
1016                .filter(object::Column::DatabaseId.eq(db_id));
1017        }
1018
1019        let result: Vec<SinkId> = query.into_tuple().all(&inner.db).await?;
1020
1021        Ok(result.into_iter().collect())
1022    }
1023
1024    pub async fn abort_pending_sink_epochs(
1025        &self,
1026        sink_committed_epoch: HashMap<SinkId, u64>,
1027    ) -> MetaResult<()> {
1028        let inner = self.inner.write().await;
1029        let txn = inner.db.begin().await?;
1030
1031        for (sink_id, committed_epoch) in sink_committed_epoch {
1032            pending_sink_state::Entity::update_many()
1033                .col_expr(
1034                    pending_sink_state::Column::SinkState,
1035                    Expr::value(pending_sink_state::SinkState::Aborted),
1036                )
1037                .filter(
1038                    pending_sink_state::Column::SinkId
1039                        .eq(sink_id)
1040                        .and(pending_sink_state::Column::Epoch.gt(committed_epoch as i64)),
1041                )
1042                .exec(&txn)
1043                .await?;
1044        }
1045
1046        txn.commit().await?;
1047        Ok(())
1048    }
1049}
1050
1051/// `CatalogStats` is a struct to store the statistics of all catalogs.
1052pub struct CatalogStats {
1053    pub table_num: u64,
1054    pub mview_num: u64,
1055    pub index_num: u64,
1056    pub source_num: u64,
1057    pub sink_num: u64,
1058    pub function_num: u64,
1059    pub streaming_job_num: u64,
1060    pub actor_num: u64,
1061    pub database_num: u64,
1062}
1063
1064impl CatalogControllerInner {
1065    pub async fn snapshot(&self) -> MetaResult<(Catalog, Vec<PbUserInfo>)> {
1066        let databases = self.list_databases().await?;
1067        let schemas = self.list_schemas().await?;
1068        let tables = self.list_tables().await?;
1069        let sources = self.list_sources().await?;
1070        let sinks = self.list_sinks().await?;
1071        let subscriptions = self.list_subscriptions().await?;
1072        let indexes = self.list_indexes().await?;
1073        let views = self.list_views().await?;
1074        let functions = self.list_functions().await?;
1075        let connections = self.list_connections().await?;
1076        let secrets = self.list_secrets().await?;
1077
1078        let users = self.list_users().await?;
1079
1080        Ok((
1081            (
1082                databases,
1083                schemas,
1084                tables,
1085                sources,
1086                sinks,
1087                subscriptions,
1088                indexes,
1089                views,
1090                functions,
1091                connections,
1092                secrets,
1093            ),
1094            users,
1095        ))
1096    }
1097
1098    async fn list_databases(&self) -> MetaResult<Vec<PbDatabase>> {
1099        let db_objs = Database::find()
1100            .find_also_related(Object)
1101            .all(&self.db)
1102            .await?;
1103        Ok(db_objs
1104            .into_iter()
1105            .map(|(db, obj)| ObjectModel(db, obj.unwrap(), None).into())
1106            .collect())
1107    }
1108
1109    async fn list_schemas(&self) -> MetaResult<Vec<PbSchema>> {
1110        let schema_objs = Schema::find()
1111            .find_also_related(Object)
1112            .all(&self.db)
1113            .await?;
1114
1115        Ok(schema_objs
1116            .into_iter()
1117            .map(|(schema, obj)| ObjectModel(schema, obj.unwrap(), None).into())
1118            .collect())
1119    }
1120
1121    async fn list_users(&self) -> MetaResult<Vec<PbUserInfo>> {
1122        let mut user_infos: Vec<PbUserInfo> = User::find()
1123            .all(&self.db)
1124            .await?
1125            .into_iter()
1126            .map(Into::into)
1127            .collect();
1128
1129        for user_info in &mut user_infos {
1130            user_info.grant_privileges = get_user_privilege(user_info.id as _, &self.db).await?;
1131        }
1132        Ok(user_infos)
1133    }
1134
1135    /// `list_all_tables` return all tables and internal tables.
1136    pub async fn list_all_state_tables(&self) -> MetaResult<Vec<PbTable>> {
1137        let table_objs = Table::find()
1138            .find_also_related(Object)
1139            .all(&self.db)
1140            .await?;
1141        let streaming_jobs = load_streaming_jobs_by_ids(
1142            &self.db,
1143            table_objs.iter().map(|(table, _)| table.job_id()),
1144        )
1145        .await?;
1146
1147        Ok(table_objs
1148            .into_iter()
1149            .map(|(table, obj)| {
1150                let job_id = table.job_id();
1151                let streaming_job = streaming_jobs.get(&job_id).cloned();
1152                ObjectModel(table, obj.unwrap(), streaming_job).into()
1153            })
1154            .collect())
1155    }
1156
1157    /// `list_tables` returns all streaming-job tables and their internal tables.
1158    async fn list_tables(&self) -> MetaResult<Vec<PbTable>> {
1159        let mut table_objs = Table::find()
1160            .find_also_related(Object)
1161            .all(&self.db)
1162            .await?;
1163
1164        let all_streaming_jobs: HashMap<JobId, streaming_job::Model> = StreamingJob::find()
1165            .all(&self.db)
1166            .await?
1167            .into_iter()
1168            .map(|job| (job.job_id, job))
1169            .collect();
1170        table_objs.retain(|(table, _)| {
1171            all_streaming_jobs
1172                .get(&table.job_id())
1173                .is_some_and(|job| job.job_status != JobStatus::Initial)
1174        });
1175
1176        let mut tables = Vec::with_capacity(table_objs.len());
1177        for (table, obj) in table_objs {
1178            let job_id = table.job_id();
1179            let streaming_job = all_streaming_jobs.get(&job_id).cloned();
1180            let pb_table: PbTable = ObjectModel(table, obj.unwrap(), streaming_job).into();
1181            tables.push(pb_table);
1182        }
1183        Ok(tables)
1184    }
1185
1186    /// `list_sources` returns all sources, including shared and associated sources whose streaming
1187    /// jobs are still being created.
1188    async fn list_sources(&self) -> MetaResult<Vec<PbSource>> {
1189        let mut source_objs = Source::find()
1190            .find_also_related(Object)
1191            .join(JoinType::LeftJoin, object::Relation::StreamingJob.def())
1192            .filter(
1193                streaming_job::Column::JobStatus
1194                    .is_null()
1195                    .or(streaming_job::Column::JobStatus.ne(JobStatus::Initial)),
1196            )
1197            .all(&self.db)
1198            .await?;
1199
1200        let visible_table_ids: HashSet<TableId> = Table::find()
1201            .select_only()
1202            .column(table::Column::TableId)
1203            .join(JoinType::InnerJoin, table::Relation::Object1.def())
1204            .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
1205            .filter(
1206                table::Column::OptionalAssociatedSourceId
1207                    .is_not_null()
1208                    .and(streaming_job::Column::JobStatus.ne(JobStatus::Initial)),
1209            )
1210            .into_tuple()
1211            .all(&self.db)
1212            .await?
1213            .into_iter()
1214            .collect();
1215        source_objs.retain(|(source, _)| {
1216            source
1217                .optional_associated_table_id
1218                .is_none_or(|table_id| visible_table_ids.contains(&table_id))
1219        });
1220
1221        Ok(source_objs
1222            .into_iter()
1223            .map(|(source, obj)| ObjectModel(source, obj.unwrap(), None).into())
1224            .collect())
1225    }
1226
1227    /// `list_sinks` return all sinks.
1228    async fn list_sinks(&self) -> MetaResult<Vec<PbSink>> {
1229        let sink_objs = Sink::find()
1230            .find_also_related(Object)
1231            .join(JoinType::LeftJoin, object::Relation::StreamingJob.def())
1232            .filter(
1233                streaming_job::Column::JobStatus
1234                    .is_null()
1235                    .or(streaming_job::Column::JobStatus.ne(JobStatus::Initial)),
1236            )
1237            .all(&self.db)
1238            .await?;
1239        let streaming_jobs = load_streaming_jobs_by_ids(
1240            &self.db,
1241            sink_objs.iter().map(|(sink, _)| sink.sink_id.as_job_id()),
1242        )
1243        .await?;
1244
1245        Ok(sink_objs
1246            .into_iter()
1247            .map(|(sink, obj)| {
1248                let streaming_job = streaming_jobs.get(&sink.sink_id.as_job_id()).cloned();
1249                ObjectModel(sink, obj.unwrap(), streaming_job).into()
1250            })
1251            .collect())
1252    }
1253
1254    /// `list_subscriptions` return all `CREATED` subscriptions.
1255    async fn list_subscriptions(&self) -> MetaResult<Vec<PbSubscription>> {
1256        let subscription_objs = Subscription::find()
1257            .find_also_related(Object)
1258            .filter(subscription::Column::SubscriptionState.eq(SubscriptionState::Created as i32))
1259            .all(&self.db)
1260            .await?;
1261
1262        Ok(subscription_objs
1263            .into_iter()
1264            .map(|(subscription, obj)| ObjectModel(subscription, obj.unwrap(), None).into())
1265            .collect())
1266    }
1267
1268    async fn list_views(&self) -> MetaResult<Vec<PbView>> {
1269        let view_objs = View::find().find_also_related(Object).all(&self.db).await?;
1270
1271        Ok(view_objs
1272            .into_iter()
1273            .map(|(view, obj)| ObjectModel(view, obj.unwrap(), None).into())
1274            .collect())
1275    }
1276
1277    /// `list_indexes` returns all streaming indexes.
1278    async fn list_indexes(&self) -> MetaResult<Vec<PbIndex>> {
1279        let index_objs = Index::find()
1280            .find_also_related(Object)
1281            .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
1282            .filter(streaming_job::Column::JobStatus.ne(JobStatus::Initial))
1283            .all(&self.db)
1284            .await?;
1285        let streaming_jobs = load_streaming_jobs_by_ids(
1286            &self.db,
1287            index_objs
1288                .iter()
1289                .map(|(index, _)| index.index_id.as_job_id()),
1290        )
1291        .await?;
1292        Ok(index_objs
1293            .into_iter()
1294            .map(|(index, obj)| {
1295                let streaming_job = streaming_jobs.get(&index.index_id.as_job_id()).cloned();
1296                ObjectModel(index, obj.unwrap(), streaming_job).into()
1297            })
1298            .collect())
1299    }
1300
1301    async fn list_connections(&self) -> MetaResult<Vec<PbConnection>> {
1302        let conn_objs = Connection::find()
1303            .find_also_related(Object)
1304            .all(&self.db)
1305            .await?;
1306
1307        Ok(conn_objs
1308            .into_iter()
1309            .map(|(conn, obj)| ObjectModel(conn, obj.unwrap(), None).into())
1310            .collect())
1311    }
1312
1313    pub async fn list_secrets(&self) -> MetaResult<Vec<PbSecret>> {
1314        let secret_objs = Secret::find()
1315            .find_also_related(Object)
1316            .all(&self.db)
1317            .await?;
1318        Ok(secret_objs
1319            .into_iter()
1320            .map(|(secret, obj)| ObjectModel(secret, obj.unwrap(), None).into())
1321            .collect())
1322    }
1323
1324    async fn list_functions(&self) -> MetaResult<Vec<PbFunction>> {
1325        let func_objs = Function::find()
1326            .find_also_related(Object)
1327            .all(&self.db)
1328            .await?;
1329
1330        Ok(func_objs
1331            .into_iter()
1332            .map(|(func, obj)| ObjectModel(func, obj.unwrap(), None).into())
1333            .collect())
1334    }
1335
1336    pub(crate) fn register_finish_notifier(
1337        &mut self,
1338        database_id: DatabaseId,
1339        id: JobId,
1340        sender: Sender<Result<NotificationVersion, String>>,
1341    ) {
1342        self.creating_table_finish_notifier
1343            .entry(database_id)
1344            .or_default()
1345            .entry(id)
1346            .or_default()
1347            .push(sender);
1348    }
1349
1350    pub(crate) async fn streaming_job_is_finished(&mut self, id: JobId) -> MetaResult<bool> {
1351        let status = StreamingJob::find()
1352            .select_only()
1353            .column(streaming_job::Column::JobStatus)
1354            .filter(streaming_job::Column::JobId.eq(id))
1355            .into_tuple::<JobStatus>()
1356            .one(&self.db)
1357            .await?;
1358
1359        status
1360            .map(|status| status == JobStatus::Created)
1361            .ok_or_else(|| {
1362                MetaError::catalog_id_not_found("streaming job", "may have been cancelled/dropped")
1363            })
1364    }
1365
1366    pub(crate) fn notify_finish_failed(&mut self, database_id: Option<DatabaseId>, err: String) {
1367        if let Some(database_id) = database_id {
1368            if let Some(creating_tables) = self.creating_table_finish_notifier.remove(&database_id)
1369            {
1370                for tx in creating_tables.into_values().flatten() {
1371                    let _ = tx.send(Err(err.clone()));
1372                }
1373            }
1374        } else {
1375            for tx in take(&mut self.creating_table_finish_notifier)
1376                .into_values()
1377                .flatten()
1378                .flat_map(|(_, txs)| txs.into_iter())
1379            {
1380                let _ = tx.send(Err(err.clone()));
1381            }
1382        }
1383    }
1384
1385    pub async fn list_time_travel_table_ids(&self) -> MetaResult<Vec<TableId>> {
1386        let table_ids: Vec<TableId> = Table::find()
1387            .select_only()
1388            .filter(table::Column::TableType.is_in(vec![
1389                TableType::Table,
1390                TableType::MaterializedView,
1391                TableType::Index,
1392            ]))
1393            .column(table::Column::TableId)
1394            .into_tuple()
1395            .all(&self.db)
1396            .await?;
1397        Ok(table_ids)
1398    }
1399
1400    /// Since the tables have been dropped from both meta store and streaming jobs, this method removes those table copies.
1401    /// Returns the removed table copies.
1402    pub(crate) fn complete_dropped_tables(
1403        &mut self,
1404        table_ids: impl IntoIterator<Item = TableId>,
1405    ) -> Vec<PbTable> {
1406        table_ids
1407            .into_iter()
1408            .filter_map(|table_id| {
1409                self.dropped_tables.remove(&table_id).map_or_else(
1410                    || {
1411                        tracing::warn!(%table_id, "table not found");
1412                        None
1413                    },
1414                    Some,
1415                )
1416            })
1417            .collect()
1418    }
1419}