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, 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    // Drop all kinds of objects including databases,
24    // schemas, relations, connections, functions, etc.
25    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        // Check the cross-db dependency info to see if the subscription can be dropped.
50        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        // check iceberg source.
99        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        // TODO: record dependency info in object_dependency table for sink into table.
131        // Special handling for 'sink into table'.
132        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        // 1. Detect when an Iceberg table is part of the dependencies.
185        // 2. Drop database with iceberg tables in it is not supported.
186        if object_type != ObjectType::Table || drop_database {
187            for obj in &removed_objects {
188                // if the obj is iceberg engine table, bail out
189                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        // Iceberg sinks (and the pk-index subset) can be user-created with arbitrary
223        // names, so unlike the iceberg-table cleanup above, identify them by
224        // inspecting properties rather than by name prefix.
225        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        // Check if there are any streaming jobs that are creating.
250        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            // Add associated sources.
274            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        // TODO: Support drop cascade for cross-database query.
334        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        // Find affect users with privileges on all this objects.
365        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        // delete all in to_drop_objects.
388        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        // notify about them.
403        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                // TODO: Notify objects in other databases when the cross-database query is supported.
411                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                // Hummock observers and compactor observers are notified once the corresponding barrier is completed.
434                // They only need RelationInfo::Table.
435                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}