Skip to main content

risingwave_meta/controller/catalog/
drop_op.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    // Drop all kinds of objects including databases,
23    // schemas, relations, connections, functions, etc.
24    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        // Check the cross-db dependency info to see if the subscription can be dropped.
48        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            // A table owns its source, and deleting the source also deletes the table row through
132            // a foreign key.
133            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        // Load all objects that belong to the dropped objects before deletion. Cascaded rows are
146        // still needed for notifications and resource cleanup.
147        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        // Iceberg sinks can be user-created with arbitrary
180        // names, so unlike the iceberg-table cleanup above, identify them by
181        // inspecting properties rather than by name prefix.
182        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        // Check if there are any streaming jobs that are creating.
200        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        // TODO: Support drop cascade for cross-database query.
240        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        // Find affect users with privileges on all this objects.
271        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        // Delete the explicitly selected objects. The self foreign key cascades to every object
288        // that belongs to them.
289        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        // notify about them.
304        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                // TODO: Notify objects in other databases when the cross-database query is supported.
313                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                // Hummock observers and compactor observers are notified once the corresponding barrier is completed.
334                // They only need RelationInfo::Table.
335                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}