1mod 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
116pub struct CatalogController {
118 pub(crate) env: MetaSrvEnv,
119 pub(crate) inner: RwLock<CatalogControllerInner>,
120}
121
122#[derive(Clone, Default, Debug)]
123pub struct DropTableConnectorContext {
124 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 pub(crate) removed_state_table_ids: Vec<TableId>,
136
137 pub(crate) removed_secret_ids: Vec<SecretId>,
139 pub(crate) removed_source_ids: Vec<SourceId>,
141 pub(crate) removed_source_fragments: HashMap<SourceId, BTreeSet<FragmentId>>,
144
145 pub(crate) removed_fragments: HashSet<FragmentId>,
146
147 pub(crate) removed_sink_fragment_by_targets: HashMap<FragmentId, Vec<FragmentId>>,
149
150 pub(crate) removed_iceberg_table_sinks: Vec<PbSink>,
152
153 pub(crate) removed_iceberg_sink_ids: Vec<SinkId>,
156
157 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 pub(crate) dropped_table_ids: Vec<TableId>,
168 pub(crate) source_ids: Vec<SourceId>,
169 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 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 #[expect(clippy::type_complexity)]
313 pub creating_table_finish_notifier:
314 HashMap<DatabaseId, HashMap<JobId, Vec<Sender<Result<NotificationVersion, String>>>>>,
315 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 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 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 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 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 pub async fn get_connector_usage(&self) -> MetaResult<jsonbb::Value> {
454 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 Ok(())
579 }
580
581 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 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 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 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 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 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 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
1026pub 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 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 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 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 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 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 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 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 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}