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