risingwave_meta/controller/catalog/
drop_op.rs1use risingwave_common::catalog::ICEBERG_SINK_PREFIX;
16use risingwave_pb::catalog::subscription::PbSubscriptionState;
17use risingwave_pb::telemetry::PbTelemetryDatabaseObject;
18use sea_orm::{ColumnTrait, DatabaseTransaction, EntityTrait, ModelTrait, QueryFilter};
19
20use super::*;
21impl CatalogController {
22 pub async fn drop_object(
25 &self,
26 object_type: ObjectType,
27 object_id: impl Into<ObjectId>,
28 drop_mode: DropMode,
29 ) -> MetaResult<(ReleaseContext, NotificationVersion)> {
30 let object_id = object_id.into();
31 let mut inner = self.inner.write().await;
32 let txn = inner.db.begin().await?;
33
34 let obj: PartialObject = Object::find_by_id(object_id)
35 .into_partial_model()
36 .one(&txn)
37 .await?
38 .ok_or_else(|| MetaError::catalog_id_not_found(object_type.as_str(), object_id))?;
39 assert_eq!(obj.obj_type, object_type);
40 let database_id = if object_type == ObjectType::Database {
41 object_id.as_database_id()
42 } else {
43 obj.database_id
44 .ok_or_else(|| anyhow!("dropped object should have database_id"))?
45 };
46
47 if obj.obj_type == ObjectType::Subscription {
49 validate_subscription_deletion(&txn, object_id.as_subscription_id()).await?;
50 }
51
52 let mut removed_objects = match drop_mode {
53 DropMode::Cascade => {
54 get_referring_objects_cascade(object_id, object_type, &txn).await?
55 }
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 let objects = validate_restrict_drop_and_collect_owned_objects(
64 object_type,
65 object_id,
66 &txn,
67 )
68 .await?;
69 for obj in objects.iter().filter(|object| {
70 object.obj_type == ObjectType::Source || object.obj_type == ObjectType::Sink
71 }) {
72 report_drop_object(obj.obj_type, obj.oid, &txn).await;
73 }
74 assert!(
75 objects.iter().all(|obj| obj.obj_type == ObjectType::Index
76 || obj.obj_type == ObjectType::Sink),
77 "only index and iceberg sink could be dropped in restrict mode"
78 );
79 for obj in &objects {
80 validate_restrict_drop_and_collect_owned_objects(
81 obj.obj_type,
82 obj.oid,
83 &txn,
84 )
85 .await?;
86 }
87 objects
88 }
89 object_type @ (ObjectType::Source | ObjectType::Sink) => {
90 validate_restrict_drop_and_collect_owned_objects(object_type, object_id, &txn)
91 .await?;
92 report_drop_object(object_type, object_id, &txn).await;
93 vec![]
94 }
95
96 ObjectType::View
97 | ObjectType::Index
98 | ObjectType::Function
99 | ObjectType::Connection
100 | ObjectType::Subscription
101 | ObjectType::Secret => {
102 validate_restrict_drop_and_collect_owned_objects(object_type, object_id, &txn)
103 .await?;
104 vec![]
105 }
106 },
107 };
108
109 removed_objects.push(obj);
110 let mut removed_object_ids: HashSet<_> =
111 removed_objects.iter().map(|obj| obj.oid).collect();
112
113 for obj in &removed_objects {
114 if obj.obj_type == ObjectType::Sink {
115 let sink = Sink::find_by_id(obj.oid.as_sink_id())
116 .one(&txn)
117 .await?
118 .ok_or_else(|| MetaError::catalog_id_not_found("sink", obj.oid))?;
119
120 if let Some(target_table) = sink.target_table
121 && !removed_object_ids.contains(&target_table.as_object_id())
122 && !has_table_been_migrated(&txn, target_table).await?
123 {
124 return Err(anyhow::anyhow!(
125 "Dropping sink into table is not allowed for unmigrated table {}. Please migrate it first.",
126 target_table
127 ).into());
128 }
129 }
130
131 if obj.obj_type == ObjectType::Source
134 && let Some(source) = Source::find_by_id(obj.oid.as_source_id()).one(&txn).await?
135 && let Some(table_id) = source.optional_associated_table_id
136 && !removed_object_ids.contains(&table_id.as_object_id())
137 {
138 return Err(MetaError::permission_denied(format!(
139 "cannot drop source \"{}\": it belongs to table {table_id}, use DROP TABLE instead",
140 source.name
141 )));
142 }
143 }
144
145 let root_objects = Object::find()
148 .filter(object::Column::Oid.is_in(removed_object_ids.iter().copied()))
149 .all(&txn)
150 .await?;
151 let belonging_objects =
152 get_belong_objects_by_ids(&txn, removed_objects.iter().map(|obj| obj.oid)).await?;
153 removed_object_ids.extend(belonging_objects.iter().map(|obj| obj.oid));
154 let mut objects_to_remove = root_objects.clone();
155 objects_to_remove.extend(belonging_objects.iter().cloned());
156 let removed_catalog_models = load_object_models(&txn, &objects_to_remove).await?;
157 removed_objects.extend(belonging_objects.into_iter().map(|obj| PartialObject {
158 oid: obj.oid,
159 obj_type: obj.obj_type,
160 schema_id: obj.schema_id,
161 database_id: obj.database_id,
162 }));
163
164 let removed_table_ids = removed_objects
165 .iter()
166 .filter(|obj| obj.obj_type == ObjectType::Table || obj.obj_type == ObjectType::Index)
167 .map(|obj| obj.oid.as_table_id());
168
169 let removed_iceberg_table_sinks: Vec<PbSink> = removed_catalog_models
170 .iter()
171 .filter_map(|object_info| match object_info {
172 PbObjectInfo::Sink(sink) if sink.name.starts_with(ICEBERG_SINK_PREFIX) => {
173 Some(sink.clone())
174 }
175 _ => None,
176 })
177 .collect();
178
179 let mut removed_iceberg_sink_ids: Vec<SinkId> = Vec::new();
183 for object_info in &removed_catalog_models {
184 if let PbObjectInfo::Sink(sink) = object_info
185 && crate::manager::iceberg_compaction::is_iceberg_sink(&sink.properties)
186 {
187 removed_iceberg_sink_ids.push(sink.id);
188 }
189 }
190
191 let removed_streaming_job_ids: Vec<JobId> = StreamingJob::find()
192 .select_only()
193 .column(streaming_job::Column::JobId)
194 .filter(streaming_job::Column::JobId.is_in(removed_object_ids))
195 .into_tuple()
196 .all(&txn)
197 .await?;
198
199 if !removed_streaming_job_ids.is_empty() {
201 let creating = StreamingJob::find()
202 .filter(
203 streaming_job::Column::JobStatus
204 .ne(JobStatus::Created)
205 .and(streaming_job::Column::JobId.is_in(removed_streaming_job_ids.clone())),
206 )
207 .count(&txn)
208 .await?;
209 if creating != 0 {
210 if creating == 1 && object_type == ObjectType::Sink {
211 info!("dropping creating sink job, it will be cancelled");
212 } else {
213 return Err(MetaError::permission_denied(format!(
214 "cannot drop {creating} streaming job(s) that are still being created; please cancel them first"
215 )));
216 }
217 }
218 }
219
220 let removed_state_table_ids: HashSet<_> = removed_table_ids.clone().collect();
221
222 let removed_source_ids: HashSet<_> = removed_objects
223 .iter()
224 .filter(|obj| obj.obj_type == ObjectType::Source)
225 .map(|obj| obj.oid.as_source_id())
226 .collect();
227
228 let removed_secret_ids = removed_objects
229 .iter()
230 .filter(|obj| obj.obj_type == ObjectType::Secret)
231 .map(|obj| obj.oid.as_secret_id())
232 .collect();
233
234 let removed_objects: HashMap<_, _> = removed_objects
235 .into_iter()
236 .map(|obj| (obj.oid, obj))
237 .collect();
238
239 for obj in removed_objects.values() {
241 if let Some(obj_database_id) = obj.database_id
242 && obj_database_id != database_id
243 {
244 return Err(MetaError::permission_denied(format!(
245 "Referenced by other objects in database {obj_database_id}, please drop them manually"
246 )));
247 }
248 }
249
250 let (removed_source_fragments, removed_sink_fragments, removed_fragments) =
251 get_fragments_for_jobs(&txn, removed_streaming_job_ids.clone()).await?;
252
253 let sink_target_fragments = fetch_target_fragments(&txn, removed_sink_fragments).await?;
254 let mut removed_sink_fragment_by_targets = HashMap::new();
255 for (sink_fragment, target_fragments) in sink_target_fragments {
256 assert!(
257 target_fragments.len() <= 1,
258 "sink should have at most one downstream fragment"
259 );
260 if let Some(target_fragment) = target_fragments.first()
261 && !removed_fragments.contains(target_fragment)
262 {
263 removed_sink_fragment_by_targets
264 .entry(*target_fragment)
265 .or_insert_with(Vec::new)
266 .push(sink_fragment);
267 }
268 }
269
270 let updated_user_ids: Vec<UserId> = UserPrivilege::find()
272 .select_only()
273 .distinct()
274 .column(user_privilege::Column::UserId)
275 .filter(user_privilege::Column::Oid.is_in(removed_objects.keys().cloned()))
276 .into_tuple()
277 .all(&txn)
278 .await?;
279 let dropped_tables = removed_catalog_models
280 .into_iter()
281 .filter_map(|object_info| match object_info {
282 PbObjectInfo::Table(table) if removed_state_table_ids.contains(&table.id) => {
283 Some(table)
284 }
285 _ => None,
286 });
287 let res = Object::delete_many()
290 .filter(object::Column::Oid.is_in(root_objects.iter().map(|obj| obj.oid)))
291 .exec(&txn)
292 .await?;
293 if res.rows_affected == 0 {
294 return Err(MetaError::catalog_id_not_found(
295 object_type.as_str(),
296 object_id,
297 ));
298 }
299 let user_infos = list_user_info_by_ids(updated_user_ids, &txn).await?;
300
301 txn.commit().await?;
302
303 let mut notifications = self.frontend_notification_batch();
305 Self::add_users_update_notifications(&mut notifications, user_infos);
306 inner
307 .dropped_tables
308 .extend(dropped_tables.map(|t| (t.id, t)));
309
310 match object_type {
311 ObjectType::Database => {
312 notifications.add(
314 NotificationOperation::Delete,
315 NotificationInfo::Database(PbDatabase {
316 id: database_id,
317 ..Default::default()
318 }),
319 );
320 }
321 ObjectType::Schema => {
322 let (schema_obj, mut to_notify_objs): (Vec<_>, Vec<_>) = removed_objects
323 .into_values()
324 .partition(|obj| obj.obj_type == ObjectType::Schema && obj.oid == object_id);
325 let schema_obj = Itertools::exactly_one(schema_obj.into_iter())
326 .expect("schema object not found");
327 to_notify_objs.push(schema_obj);
328
329 let relation_group = build_object_group_for_delete(to_notify_objs);
330 notifications.add(NotificationOperation::Delete, relation_group);
331 }
332 _ => {
333 let relation_group =
336 build_object_group_for_delete(removed_objects.into_values().collect());
337 notifications.add(NotificationOperation::Delete, relation_group);
338 }
339 }
340 let version = notifications
341 .publish()
342 .await
343 .expect("the drop notification batch contains the dropped object");
344
345 Ok((
346 ReleaseContext {
347 database_id,
348 removed_streaming_job_ids,
349 removed_state_table_ids: removed_state_table_ids.into_iter().collect(),
350 removed_source_ids: removed_source_ids.into_iter().collect(),
351 removed_secret_ids,
352 removed_source_fragments,
353 removed_fragments,
354 removed_sink_fragment_by_targets,
355 removed_iceberg_table_sinks,
356 removed_iceberg_sink_ids,
357 },
358 version,
359 ))
360 }
361
362 pub async fn try_abort_creating_subscription(
363 &self,
364 subscription_id: SubscriptionId,
365 ) -> MetaResult<()> {
366 let inner = self.inner.write().await;
367 let txn = inner.db.begin().await?;
368
369 let subscription = Subscription::find_by_id(subscription_id).one(&txn).await?;
370 let Some(subscription) = subscription else {
371 tracing::warn!(
372 %subscription_id,
373 "subscription not found when aborting creation, might be cleaned by recovery"
374 );
375 return Ok(());
376 };
377
378 if subscription.subscription_state == PbSubscriptionState::Created as i32 {
379 tracing::warn!(
380 %subscription_id,
381 "subscription is already created when aborting creation"
382 );
383 return Ok(());
384 }
385
386 subscription.delete(&txn).await?;
387 txn.commit().await?;
388 Ok(())
389 }
390}
391
392async fn report_drop_object(
393 object_type: ObjectType,
394 object_id: ObjectId,
395 txn: &DatabaseTransaction,
396) {
397 let connector_name = {
398 match object_type {
399 ObjectType::Sink => Sink::find_by_id(object_id.as_sink_id())
400 .select_only()
401 .column(sink::Column::Properties)
402 .into_tuple::<Property>()
403 .one(txn)
404 .await
405 .ok()
406 .flatten()
407 .and_then(|properties| properties.inner_ref().get("connector").cloned()),
408 ObjectType::Source => Source::find_by_id(object_id.as_source_id())
409 .select_only()
410 .column(source::Column::WithProperties)
411 .into_tuple::<Property>()
412 .one(txn)
413 .await
414 .ok()
415 .flatten()
416 .and_then(|properties| properties.inner_ref().get("connector").cloned()),
417 _ => unreachable!(),
418 }
419 };
420 if let Some(connector_name) = connector_name {
421 report_event(
422 PbTelemetryEventStage::DropStreamJob,
423 "source",
424 object_id.as_raw_id() as _,
425 Some(connector_name),
426 Some(match object_type {
427 ObjectType::Source => PbTelemetryDatabaseObject::Source,
428 ObjectType::Sink => PbTelemetryDatabaseObject::Sink,
429 _ => unreachable!(),
430 }),
431 None,
432 );
433 }
434}