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