From e22ebef0116a35422a0bdec27db8cf5372aac15b Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 23 Jun 2026 22:32:00 -0300 Subject: [PATCH] Support for DROP SCHEMA CASCADE --- doc/sql.extensions/README.schemas.md | 20 +- src/dsql/DdlNodes.epp | 278 +++++++++++++++++++++++++-- src/dsql/DdlNodes.h | 12 +- src/dsql/parse.y | 3 +- src/jrd/Attachment.h | 1 + 5 files changed, 290 insertions(+), 24 deletions(-) diff --git a/doc/sql.extensions/README.schemas.md b/doc/sql.extensions/README.schemas.md index 7de25bf5fe2..c32f2e4e096 100644 --- a/doc/sql.extensions/README.schemas.md +++ b/doc/sql.extensions/README.schemas.md @@ -256,11 +256,25 @@ ALTER SCHEMA ### DROP SCHEMA ```sql -DROP SCHEMA [IF EXISTS] +DROP SCHEMA [IF EXISTS] [CASCADE | RESTRICT] ``` -Currently, only empty schemas can be dropped. In the future, a `CASCADE` sub-clause will be introduced, allowing -schemas to be dropped along with all their contained objects. +`RESTRICT` is the default. With `RESTRICT`, the statement fails if the schema contains any objects. + +With `CASCADE`, all objects contained in the schema are automatically dropped before the schema itself is removed. +Objects are dropped in a fixed broad order chosen to remove the most common dependents before the objects they use: +triggers, packages, views, tables, procedures, functions, sequences, exceptions, collations, character sets, and domains. +Within that process, dependencies between objects in the same schema are handled as internal to the cascade operation, so +cycles or mutual references inside the schema do not by themselves prevent the schema from being dropped. Cross-schema +dependents — objects in other schemas that reference objects in the schema being dropped — are not removed automatically; +their presence causes the operation to fail with a dependency error, leaving the schema and all its contents intact. + +#### DDL triggers and CASCADE + +`DROP SCHEMA ... CASCADE` fires DDL triggers for the schema drop itself, but does not fire object-level DDL triggers for +objects removed internally by the cascade. For example, `BEFORE DROP TABLE` and `AFTER DROP TABLE` triggers do not fire +for tables dropped as part of a `DROP SCHEMA ... CASCADE`. Direct `DROP` statements for those objects continue to fire +their corresponding DDL triggers normally. ### CURRENT_SCHEMA diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index 02cde2fd3d2..8292e574f67 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -1083,8 +1083,8 @@ void DdlNode::executeDdlTrigger(thread_db* tdbb, jrd_tra* transaction, DdlTrigge { Attachment* const attachment = transaction->tra_attachment; - // do nothing if user doesn't want database triggers - if (attachment->att_flags & ATT_no_db_triggers) + // do nothing if user doesn't want database triggers or DROP SCHEMA CASCADE is running + if (attachment->att_flags & (ATT_no_db_triggers | ATT_schema_cascade_dropping)) return; fb_assert(action > 0); // first element is NULL @@ -18144,6 +18144,7 @@ string DropSchemaNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, name); NODE_PRINT(printer, silent); + NODE_PRINT(printer, cascade); return "DropSchemaNode"; } @@ -18177,7 +18178,11 @@ void DropSchemaNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_DROP_SCHEMA, qualifiedName, {}); - if (collectObjects(tdbb, transaction)) + checkDependencies(tdbb, transaction); + + if (cascade) + dropObjects(tdbb, dsqlScratch, transaction); + else if (collectObjects(tdbb, transaction)) status_exception::raise(Arg::Gds(isc_dyn_cannot_drop_non_emptyschema) << name.toQuotedString()); ERASE SCH; @@ -18234,12 +18239,36 @@ void DropSchemaNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, savePoint.release(); // everything is ok } -bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, - Array>* objects) +// Check if others schemas depends on our schema. +void DropSchemaNode::checkDependencies(thread_db* tdbb, jrd_tra* transaction) +{ + static const CachedRequestId requestHandleId; + AutoCacheRequest requestHandle(tdbb, requestHandleId); + + FOR (REQUEST_HANDLE requestHandle TRANSACTION_HANDLE transaction) + FIRST 1 + DEP IN RDB$DEPENDENCIES + WITH DEP.RDB$DEPENDED_ON_SCHEMA_NAME EQ name.c_str() AND + DEP.RDB$DEPENDENT_SCHEMA_NAME NE name.c_str() + { + status_exception::raise( + Arg::Gds(isc_no_meta_update) << + Arg::Gds(isc_no_delete) << + Arg::Gds(isc_dependency) << Arg::Num(1)); + } + END_FOR +} + +bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, Array* objects) { if (objects) objects->clear(); + const auto addObject = [&](ObjectType type, const MetaName& objName, bool isLTT = false) + { + objects->add({type, objName, isLTT}); + }; + const auto attachment = tdbb->getAttachment(); for (const auto& lttEntry : attachment->att_local_temporary_tables) @@ -18249,7 +18278,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, if (lttName.schema == name) { if (objects) - objects->add({obj_relation, lttName.object}); + addObject(obj_relation, lttName.object, true); else return true; } @@ -18264,10 +18293,13 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, WITH FLD.RDB$SCHEMA_NAME EQ name.c_str() SORTED BY FLD.RDB$FIELD_NAME { - if (objects) - objects->add({obj_field, FLD.RDB$FIELD_NAME}); - else - return true; + if (!fb_utils::implicit_domain(FLD.RDB$FIELD_NAME)) + { + if (objects) + addObject(obj_field, FLD.RDB$FIELD_NAME); + else + return true; + } } END_FOR } @@ -18282,7 +18314,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY REL.RDB$RELATION_NAME { if (objects) - objects->add({obj_relation, REL.RDB$RELATION_NAME}); + addObject((REL.RDB$VIEW_BLR.NULL ? obj_relation : obj_view), REL.RDB$RELATION_NAME); else return true; } @@ -18300,7 +18332,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY TRG.RDB$TRIGGER_NAME { if (objects) - objects->add({obj_trigger, TRG.RDB$TRIGGER_NAME}); + addObject(obj_trigger, TRG.RDB$TRIGGER_NAME); else return true; } @@ -18318,7 +18350,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY FUN.RDB$FUNCTION_NAME { if (objects) - objects->add({obj_udf, FUN.RDB$FUNCTION_NAME}); + addObject(obj_udf, FUN.RDB$FUNCTION_NAME); else return true; } @@ -18335,7 +18367,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY GEN.RDB$GENERATOR_NAME { if (objects) - objects->add({obj_generator, GEN.RDB$GENERATOR_NAME}); + addObject(obj_generator, GEN.RDB$GENERATOR_NAME); else return true; } @@ -18353,7 +18385,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY PRC.RDB$PROCEDURE_NAME { if (objects) - objects->add({obj_procedure, PRC.RDB$PROCEDURE_NAME}); + addObject(obj_procedure, PRC.RDB$PROCEDURE_NAME); else return true; } @@ -18370,7 +18402,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY CSC.RDB$CHARACTER_SET_NAME { if (objects) - objects->add({obj_charset, CSC.RDB$CHARACTER_SET_NAME}); + addObject(obj_charset, CSC.RDB$CHARACTER_SET_NAME); else return true; } @@ -18387,7 +18419,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY COL.RDB$COLLATION_NAME { if (objects) - objects->add({obj_collation, COL.RDB$COLLATION_NAME}); + addObject(obj_collation, COL.RDB$COLLATION_NAME); else return true; } @@ -18404,7 +18436,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY XCP.RDB$EXCEPTION_NAME { if (objects) - objects->add({obj_exception, XCP.RDB$EXCEPTION_NAME}); + addObject(obj_exception, XCP.RDB$EXCEPTION_NAME); else return true; } @@ -18421,7 +18453,7 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, SORTED BY PKG.RDB$PACKAGE_NAME { if (objects) - objects->add({obj_package_header, PKG.RDB$PACKAGE_NAME}); + addObject(obj_package_header, PKG.RDB$PACKAGE_NAME); else return true; } @@ -18431,4 +18463,212 @@ bool DropSchemaNode::collectObjects(thread_db* tdbb, jrd_tra* transaction, return objects && objects->hasData(); } +// Drop every object contained in the schema (DROP SCHEMA ... CASCADE). +// Cross-schema dependencies are rejected before this routine runs. +void DropSchemaNode::dropObjects(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) +{ + Array items; + collectObjects(tdbb, transaction, &items); + + if (!items.hasData()) + return; + + AutoSetRestoreFlag autoSchemaCascadeDropping(&transaction->tra_attachment->att_flags, + ATT_schema_cascade_dropping, true); + + MemoryPool& pool = *tdbb->getDefaultPool(); + + // Drop FK constraints first. + // This is important when there are cyclic FK dependencies between tables in the schema. + // Dropping the constraints first allows us to drop the tables in any order, + // without worrying about FK dependencies. + + { // scope + struct ForeignKey + { + QualifiedName relationName; + MetaName constraintName; + }; + + Array foreignKeys; + + static const CachedRequestId requestHandleId; + AutoCacheRequest requestHandle(tdbb, requestHandleId); + + FOR (REQUEST_HANDLE requestHandle TRANSACTION_HANDLE transaction) + RCC IN RDB$RELATION_CONSTRAINTS + WITH RCC.RDB$SCHEMA_NAME EQ name.c_str() AND + RCC.RDB$CONSTRAINT_TYPE EQ FOREIGN_KEY + { + foreignKeys.add({ + QualifiedName(RCC.RDB$RELATION_NAME, RCC.RDB$SCHEMA_NAME), + MetaName(RCC.RDB$CONSTRAINT_NAME) + }); + } + END_FOR + + for (const auto& foreignKey : foreignKeys) + { + AutoSetRestoreFlag autoNoMetadataSaved(&dsqlScratch->flags, + DsqlCompilerScratch::FLAG_METADATA_SAVED, false); + AutoSetRestore autoNullRelation(&dsqlScratch->relation, nullptr); + + const auto sourceNode = FB_NEW_POOL(pool) RelationSourceNode(pool, foreignKey.relationName); + const auto alterNode = FB_NEW_POOL(pool) AlterRelationNode(pool, sourceNode); + auto clause = FB_NEW_POOL(pool) RelationNode::DropConstraintClause(pool); + + clause->name = foreignKey.constraintName; + clause->silent = true; + alterNode->clauses.add(clause); + alterNode->execute(tdbb, dsqlScratch, transaction); + } + } + + // Helper to create and execute the appropriate silent drop node for an object. + const auto dropObject = [&](const CollectedObject& item) + { + const QualifiedName qualifiedName(item.objName, name); + DdlNode* node = nullptr; + + switch (item.type) + { + case obj_relation: + case obj_view: + { + const auto dropNode = FB_NEW_POOL(pool) DropRelationNode(pool, qualifiedName, + (item.type == obj_view)); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_trigger: + { + const auto dropNode = FB_NEW_POOL(pool) DropTriggerNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_udf: + { + const auto dropNode = FB_NEW_POOL(pool) DropFunctionNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_procedure: + { + const auto dropNode = FB_NEW_POOL(pool) DropProcedureNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_generator: + { + const auto dropNode = FB_NEW_POOL(pool) DropSequenceNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_exception: + { + const auto dropNode = FB_NEW_POOL(pool) DropExceptionNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_collation: + { + const auto dropNode = FB_NEW_POOL(pool) DropCollationNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_field: + { + const auto dropNode = FB_NEW_POOL(pool) DropDomainNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_package_header: + { + const auto dropNode = FB_NEW_POOL(pool) DropPackageNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + case obj_package_body: + { + const auto dropNode = FB_NEW_POOL(pool) DropPackageBodyNode(pool, qualifiedName); + dropNode->silent = true; + node = dropNode; + break; + } + + default: + // No DDL drop node (e.g. character sets, which cannot exist in a user schema); nothing to do. + fb_assert(false); + break; + } + + if (node) + node->execute(tdbb, dsqlScratch, transaction); + }; + + // Drop Created LTTs, triggers and package bodies. + // Stored objects cannot reference them. + for (const auto& item : items) + { + if (item.isLTT || item.type == obj_trigger) + dropObject(item); + else if (item.type == obj_package_header) + { + auto bodyItem = item; + bodyItem.type = obj_package_body; + dropObject(bodyItem); + } + } + + // Drop functions, procedures, package headers, views and tables. + for (const auto& item : items) + { + if (!item.isLTT && + (item.type == obj_udf || + item.type == obj_procedure || + item.type == obj_package_header || + item.type == obj_view || + item.type == obj_relation)) + { + dropObject(item); + } + } + + // Drop domains. + // This must be after tables, views, procedures, and functions, because drop domain check for + // dependencies in its execution (before DFW). + for (const auto& item : items) + { + if (item.type == obj_field) + dropObject(item); + } + + // Drop generators, exceptions, collations. + // These are the last objects to be dropped, because they never have dependencies on other objects, + // and other objects can depend on them. + for (const auto& item : items) + { + if (item.type == obj_generator || item.type == obj_exception || item.type == obj_collation) + dropObject(item); + } +} + } // namespace Jrd diff --git a/src/dsql/DdlNodes.h b/src/dsql/DdlNodes.h index 0282c7765d9..0c3471e062f 100644 --- a/src/dsql/DdlNodes.h +++ b/src/dsql/DdlNodes.h @@ -2995,13 +2995,23 @@ class DropSchemaNode final : public DdlNode } private: + struct CollectedObject + { + ObjectType type; + MetaName objName; + bool isLTT = false; + }; + bool collectObjects(thread_db* tdbb, jrd_tra* transaction, - Firebird::Array>* objects = nullptr); + Firebird::Array* objects = nullptr); + void checkDependencies(thread_db* tdbb, jrd_tra* transaction); + void dropObjects(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction); public: MetaName name; bool silent = false; bool recreate = false; + bool cascade = false; }; diff --git a/src/dsql/parse.y b/src/dsql/parse.y index e77d3dd202c..a606c1344cf 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -5500,10 +5500,11 @@ drop_clause node->silentDrop = $3; $$ = node; } - | SCHEMA if_exists_opt symbol_schema_name + | SCHEMA if_exists_opt symbol_schema_name drop_behaviour { const auto node = newNode(*$3); node->silent = $2; + node->cascade = $4; $$ = node; } ; diff --git a/src/jrd/Attachment.h b/src/jrd/Attachment.h index 1ec05c22623..44156d10557 100644 --- a/src/jrd/Attachment.h +++ b/src/jrd/Attachment.h @@ -170,6 +170,7 @@ inline constexpr ULONG ATT_replicating = 0x200000L; // Replication is active inline constexpr ULONG ATT_resetting = 0x400000L; // Session reset is in progress inline constexpr ULONG ATT_worker = 0x800000L; // Worker attachment, managed by the engine inline constexpr ULONG ATT_gbak_restore_has_schema = 0x1000000L; +inline constexpr ULONG ATT_schema_cascade_dropping = 0x2000000L; // DROP SCHEMA CASCADE dropping contained objects inline constexpr ULONG ATT_NO_CLEANUP = (ATT_no_cleanup | ATT_notify_gc);