risingwave_meta/controller/catalog/
drop_op.rs1use risingwave_common::catalog::{ICEBERG_SINK_PREFIX, ICEBERG_SOURCE_PREFIX};
16use risingwave_pb::catalog::PbTable;
17use risingwave_pb::catalog::subscription::PbSubscriptionState;
18use risingwave_pb::telemetry::PbTelemetryDatabaseObject;
19use sea_orm::{ColumnTrait, DatabaseTransaction, EntityTrait, ModelTrait, QueryFilter};
20
21use super::*;
22impl CatalogController {
23 pub async fn drop_object(
26 &self,
27 object_type: ObjectType,
28 object_id: impl Into<ObjectId>,
29 drop_mode: DropMode,
30 ) -> MetaResult<(ReleaseContext, NotificationVersion)> {
31 let object_id = object_id.into();
32 let mut inner = self.inner.write().await;
33 let txn = inner.db.begin().await?;
34
35 let obj: PartialObject = Object::find_by_id(object_id)
36 .into_partial_model()
37 .one(&txn)
38 .await?
39 .ok_or_else(|| MetaError::catalog_id_not_found(object_type.as_str(), object_id))?;
40 assert_eq!(obj.obj_type, object_type);
41 let drop_database = object_type == ObjectType::Database;
42 let database_id = if object_type == ObjectType::Database {
43 object_id.as_database_id()
44 } else {
45 obj.database_id
46 .ok_or_else(|| anyhow!("dropped object should have database_id"))?
47 };
48
49 if obj.obj_type == ObjectType::Subscription {
51 validate_subscription_deletion(&txn, object_id.as_subscription_id()).await?;
52 }
53
54 let mut removed_objects = match drop_mode {
55 DropMode::Cascade => get_referring_objects_cascade(object_id, &txn).await?,
56 DropMode::Restrict => match object_type {
57 ObjectType::Database => unreachable!("database always be dropped in cascade mode"),
58 ObjectType::Schema => {
59 ensure_schema_empty(object_id.as_schema_id(), &txn).await?;
60 Default::default()
61 }
62 ObjectType::Table => {
63 check_object_refer_for_drop(object_type, object_id, &txn).await?;
64 let objects = get_referring_objects(object_id, &txn).await?;
65 for obj in objects.iter().filter(|object| {
66 object.obj_type == ObjectType::Source || object.obj_type == ObjectType::Sink
67 }) {
68 report_drop_object(obj.obj_type, obj.oid, &txn).await;
69 }
70 assert!(
71 objects.iter().all(|obj| obj.obj_type == ObjectType::Index
72 || obj.obj_type == ObjectType::Sink),
73 "only index and iceberg sink could be dropped in restrict mode"
74 );
75 for obj in &objects {
76 check_object_refer_for_drop(obj.obj_type, obj.oid, &txn).await?;
77 }
78 objects
79 }
80 object_type @ (ObjectType::Source | ObjectType::Sink) => {
81 check_object_refer_for_drop(object_type, object_id, &txn).await?;
82 report_drop_object(object_type, object_id, &txn).await;
83 vec![]
84 }
85
86 ObjectType::View
87 | ObjectType::Index
88 | ObjectType::Function
89 | ObjectType::Connection
90 | ObjectType::Subscription
91 | ObjectType::Secret => {
92 check_object_refer_for_drop(object_type, object_id, &txn).await?;
93 vec![]
94 }
95 },
96 };
97
98 if obj.obj_type == ObjectType::Table {
100 let table_name = Table::find_by_id(object_id.as_table_id())
101 .select_only()
102 .column(table::Column::Name)
103 .into_tuple::<String>()
104 .one(&txn)
105 .await?
106 .ok_or_else(|| MetaError::catalog_id_not_found("table", object_id))?;
107 let iceberg_source = Source::find()
108 .inner_join(Object)
109 .filter(
110 object::Column::DatabaseId
111 .eq(database_id)
112 .and(object::Column::SchemaId.eq(obj.schema_id.unwrap()))
113 .and(
114 source::Column::Name
115 .eq(format!("{}{}", ICEBERG_SOURCE_PREFIX, table_name)),
116 ),
117 )
118 .into_partial_model()
119 .one(&txn)
120 .await?;
121 if let Some(iceberg_source) = iceberg_source {
122 removed_objects.push(iceberg_source);
123 }
124 }
125
126 removed_objects.push(obj);
127 let mut removed_object_ids: HashSet<_> =
128 removed_objects.iter().map(|obj| obj.oid).collect();
129
130 let incoming_sink_ids: Vec<SinkId> = Sink::find()
133 .select_only()
134 .column(sink::Column::SinkId)
135 .filter(sink::Column::TargetTable.is_in(removed_object_ids.clone()))
136 .into_tuple()
137 .all(&txn)
138 .await?;
139 if !incoming_sink_ids.is_empty() {
140 if self.env.opts.protect_drop_table_with_incoming_sink {
141 let sink_names: Vec<String> = Sink::find()
142 .select_only()
143 .column(sink::Column::Name)
144 .filter(sink::Column::SinkId.is_in(incoming_sink_ids.clone()))
145 .into_tuple()
146 .all(&txn)
147 .await?;
148
149 return Err(MetaError::permission_denied(format!(
150 "Table used by incoming sinks: {:?}, please drop them manually",
151 sink_names
152 )));
153 }
154
155 let removed_sink_objs: Vec<PartialObject> = Object::find()
156 .filter(object::Column::Oid.is_in(incoming_sink_ids))
157 .into_partial_model()
158 .all(&txn)
159 .await?;
160
161 removed_object_ids.extend(removed_sink_objs.iter().map(|obj| obj.oid));
162 removed_objects.extend(removed_sink_objs);
163 }
164
165 for obj in &removed_objects {
166 if obj.obj_type == ObjectType::Sink {
167 let sink = Sink::find_by_id(obj.oid.as_sink_id())
168 .one(&txn)
169 .await?
170 .ok_or_else(|| MetaError::catalog_id_not_found("sink", obj.oid))?;
171
172 if let Some(target_table) = sink.target_table
173 && !removed_object_ids.contains(&target_table.as_object_id())
174 && !has_table_been_migrated(&txn, target_table).await?
175 {
176 return Err(anyhow::anyhow!(
177 "Dropping sink into table is not allowed for unmigrated table {}. Please migrate it first.",
178 target_table
179 ).into());
180 }
181 }
182 }
183
184 if object_type != ObjectType::Table || drop_database {
187 for obj in &removed_objects {
188 if obj.obj_type == ObjectType::Table {
190 let table = Table::find_by_id(obj.oid.as_table_id())
191 .one(&txn)
192 .await?
193 .ok_or_else(|| MetaError::catalog_id_not_found("table", obj.oid))?;
194 if matches!(table.engine, Some(table::Engine::Iceberg)) {
195 return Err(MetaError::permission_denied(format!(
196 "Found iceberg table in dependency: {}, please drop it manually",
197 table.name,
198 )));
199 }
200 }
201 }
202 }
203
204 let removed_table_ids = removed_objects
205 .iter()
206 .filter(|obj| obj.obj_type == ObjectType::Table || obj.obj_type == ObjectType::Index)
207 .map(|obj| obj.oid.as_table_id());
208
209 let removed_iceberg_table_sinks: Vec<PbSink> = Sink::find()
210 .find_also_related(Object)
211 .filter(
212 sink::Column::SinkId
213 .is_in(removed_object_ids.clone())
214 .and(sink::Column::Name.like(format!("{}%", ICEBERG_SINK_PREFIX))),
215 )
216 .all(&txn)
217 .await?
218 .into_iter()
219 .map(|(sink, obj)| ObjectModel(sink, obj.unwrap(), None).into())
220 .collect();
221
222 let mut removed_iceberg_sink_ids: Vec<SinkId> = Vec::new();
226 let mut removed_iceberg_pk_index_sink_ids: Vec<SinkId> = Vec::new();
227 for sink in Sink::find()
228 .filter(sink::Column::SinkId.is_in(removed_object_ids.clone()))
229 .all(&txn)
230 .await?
231 {
232 let properties = sink.properties.inner_ref();
233 if crate::manager::iceberg_compaction::is_iceberg_sink(properties) {
234 removed_iceberg_sink_ids.push(sink.sink_id);
235 }
236 if crate::manager::iceberg_pk_index_sink::is_iceberg_pk_index_sink(properties) {
237 removed_iceberg_pk_index_sink_ids.push(sink.sink_id);
238 }
239 }
240
241 let removed_streaming_job_ids: Vec<JobId> = StreamingJob::find()
242 .select_only()
243 .column(streaming_job::Column::JobId)
244 .filter(streaming_job::Column::JobId.is_in(removed_object_ids))
245 .into_tuple()
246 .all(&txn)
247 .await?;
248
249 if !removed_streaming_job_ids.is_empty() {
251 let creating = StreamingJob::find()
252 .filter(
253 streaming_job::Column::JobStatus
254 .ne(JobStatus::Created)
255 .and(streaming_job::Column::JobId.is_in(removed_streaming_job_ids.clone())),
256 )
257 .count(&txn)
258 .await?;
259 if creating != 0 {
260 if creating == 1 && object_type == ObjectType::Sink {
261 info!("dropping creating sink job, it will be cancelled");
262 } else {
263 return Err(MetaError::permission_denied(format!(
264 "can not drop {creating} creating streaming job, please cancel them firstly"
265 )));
266 }
267 }
268 }
269
270 let mut removed_state_table_ids: HashSet<_> = removed_table_ids.clone().collect();
271
272 if !drop_database {
273 let removed_source_ids: Vec<SourceId> = Table::find()
275 .select_only()
276 .column(table::Column::OptionalAssociatedSourceId)
277 .filter(
278 table::Column::TableId
279 .is_in(removed_table_ids)
280 .and(table::Column::OptionalAssociatedSourceId.is_not_null()),
281 )
282 .into_tuple()
283 .all(&txn)
284 .await?;
285 let removed_source_objs: Vec<PartialObject> = Object::find()
286 .filter(object::Column::Oid.is_in(removed_source_ids))
287 .into_partial_model()
288 .all(&txn)
289 .await?;
290 removed_objects.extend(removed_source_objs);
291 }
292
293 let removed_source_ids: HashSet<_> = removed_objects
294 .iter()
295 .filter(|obj| obj.obj_type == ObjectType::Source)
296 .map(|obj| obj.oid.as_source_id())
297 .collect();
298
299 let removed_secret_ids = removed_objects
300 .iter()
301 .filter(|obj| obj.obj_type == ObjectType::Secret)
302 .map(|obj| obj.oid.as_secret_id())
303 .collect();
304
305 if !removed_streaming_job_ids.is_empty() {
306 let removed_internal_table_objs: Vec<PartialObject> = Object::find()
307 .select_only()
308 .columns([
309 object::Column::Oid,
310 object::Column::ObjType,
311 object::Column::SchemaId,
312 object::Column::DatabaseId,
313 ])
314 .join(JoinType::InnerJoin, object::Relation::Table.def())
315 .filter(table::Column::BelongsToJobId.is_in(removed_streaming_job_ids.clone()))
316 .into_partial_model()
317 .all(&txn)
318 .await?;
319
320 removed_state_table_ids.extend(
321 removed_internal_table_objs
322 .iter()
323 .map(|obj| obj.oid.as_table_id()),
324 );
325 removed_objects.extend(removed_internal_table_objs);
326 }
327
328 let removed_objects: HashMap<_, _> = removed_objects
329 .into_iter()
330 .map(|obj| (obj.oid, obj))
331 .collect();
332
333 for obj in removed_objects.values() {
335 if let Some(obj_database_id) = obj.database_id
336 && obj_database_id != database_id
337 {
338 return Err(MetaError::permission_denied(format!(
339 "Referenced by other objects in database {obj_database_id}, please drop them manually"
340 )));
341 }
342 }
343
344 let (removed_source_fragments, removed_sink_fragments, removed_fragments) =
345 get_fragments_for_jobs(&txn, removed_streaming_job_ids.clone()).await?;
346
347 let sink_target_fragments = fetch_target_fragments(&txn, removed_sink_fragments).await?;
348 let mut removed_sink_fragment_by_targets = HashMap::new();
349 for (sink_fragment, target_fragments) in sink_target_fragments {
350 assert!(
351 target_fragments.len() <= 1,
352 "sink should have at most one downstream fragment"
353 );
354 if let Some(target_fragment) = target_fragments.first()
355 && !removed_fragments.contains(target_fragment)
356 {
357 removed_sink_fragment_by_targets
358 .entry(*target_fragment)
359 .or_insert_with(Vec::new)
360 .push(sink_fragment);
361 }
362 }
363
364 let updated_user_ids: Vec<UserId> = UserPrivilege::find()
366 .select_only()
367 .distinct()
368 .column(user_privilege::Column::UserId)
369 .filter(user_privilege::Column::Oid.is_in(removed_objects.keys().cloned()))
370 .into_tuple()
371 .all(&txn)
372 .await?;
373 let dropped_tables = Table::find()
374 .find_also_related(Object)
375 .filter(
376 table::Column::TableId.is_in(
377 removed_state_table_ids
378 .iter()
379 .copied()
380 .collect::<HashSet<TableId>>(),
381 ),
382 )
383 .all(&txn)
384 .await?
385 .into_iter()
386 .map(|(table, obj)| PbTable::from(ObjectModel(table, obj.unwrap(), None)));
387 let res = Object::delete_many()
389 .filter(object::Column::Oid.is_in(removed_objects.keys().cloned()))
390 .exec(&txn)
391 .await?;
392 if res.rows_affected == 0 {
393 return Err(MetaError::catalog_id_not_found(
394 object_type.as_str(),
395 object_id,
396 ));
397 }
398 let user_infos = list_user_info_by_ids(updated_user_ids, &txn).await?;
399
400 txn.commit().await?;
401
402 self.notify_users_update(user_infos).await;
404 inner
405 .dropped_tables
406 .extend(dropped_tables.map(|t| (t.id, t)));
407
408 let version = match object_type {
409 ObjectType::Database => {
410 self.notify_frontend(
412 NotificationOperation::Delete,
413 NotificationInfo::Database(PbDatabase {
414 id: database_id,
415 ..Default::default()
416 }),
417 )
418 .await
419 }
420 ObjectType::Schema => {
421 let (schema_obj, mut to_notify_objs): (Vec<_>, Vec<_>) = removed_objects
422 .into_values()
423 .partition(|obj| obj.obj_type == ObjectType::Schema && obj.oid == object_id);
424 let schema_obj = Itertools::exactly_one(schema_obj.into_iter())
425 .expect("schema object not found");
426 to_notify_objs.push(schema_obj);
427
428 let relation_group = build_object_group_for_delete(to_notify_objs);
429 self.notify_frontend(NotificationOperation::Delete, relation_group)
430 .await
431 }
432 _ => {
433 let relation_group =
436 build_object_group_for_delete(removed_objects.into_values().collect());
437 self.notify_frontend(NotificationOperation::Delete, relation_group)
438 .await
439 }
440 };
441
442 Ok((
443 ReleaseContext {
444 database_id,
445 removed_streaming_job_ids,
446 removed_state_table_ids: removed_state_table_ids.into_iter().collect(),
447 removed_source_ids: removed_source_ids.into_iter().collect(),
448 removed_secret_ids,
449 removed_source_fragments,
450 removed_fragments,
451 removed_sink_fragment_by_targets,
452 removed_iceberg_table_sinks,
453 removed_iceberg_sink_ids,
454 removed_iceberg_pk_index_sink_ids,
455 },
456 version,
457 ))
458 }
459
460 pub async fn try_abort_creating_subscription(
461 &self,
462 subscription_id: SubscriptionId,
463 ) -> MetaResult<()> {
464 let inner = self.inner.write().await;
465 let txn = inner.db.begin().await?;
466
467 let subscription = Subscription::find_by_id(subscription_id).one(&txn).await?;
468 let Some(subscription) = subscription else {
469 tracing::warn!(
470 %subscription_id,
471 "subscription not found when aborting creation, might be cleaned by recovery"
472 );
473 return Ok(());
474 };
475
476 if subscription.subscription_state == PbSubscriptionState::Created as i32 {
477 tracing::warn!(
478 %subscription_id,
479 "subscription is already created when aborting creation"
480 );
481 return Ok(());
482 }
483
484 subscription.delete(&txn).await?;
485 txn.commit().await?;
486 Ok(())
487 }
488}
489
490async fn report_drop_object(
491 object_type: ObjectType,
492 object_id: ObjectId,
493 txn: &DatabaseTransaction,
494) {
495 let connector_name = {
496 match object_type {
497 ObjectType::Sink => Sink::find_by_id(object_id.as_sink_id())
498 .select_only()
499 .column(sink::Column::Properties)
500 .into_tuple::<Property>()
501 .one(txn)
502 .await
503 .ok()
504 .flatten()
505 .and_then(|properties| properties.inner_ref().get("connector").cloned()),
506 ObjectType::Source => Source::find_by_id(object_id.as_source_id())
507 .select_only()
508 .column(source::Column::WithProperties)
509 .into_tuple::<Property>()
510 .one(txn)
511 .await
512 .ok()
513 .flatten()
514 .and_then(|properties| properties.inner_ref().get("connector").cloned()),
515 _ => unreachable!(),
516 }
517 };
518 if let Some(connector_name) = connector_name {
519 report_event(
520 PbTelemetryEventStage::DropStreamJob,
521 "source",
522 object_id.as_raw_id() as _,
523 Some(connector_name),
524 Some(match object_type {
525 ObjectType::Source => PbTelemetryDatabaseObject::Source,
526 ObjectType::Sink => PbTelemetryDatabaseObject::Sink,
527 _ => unreachable!(),
528 }),
529 None,
530 );
531 }
532}