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