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