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