From cea905874ff0afe962d32b2110faf5f9e61330d8 Mon Sep 17 00:00:00 2001 From: David Christensen Date: Thu, 27 May 2021 11:08:32 -0500 Subject: [PATCH] Add support for DELETE CASCADE Proof of concept of allowing a DELETE statement to override formal FK's handling from RESTRICT/NO ACTION and treat as CASCADE instead. Syntax is "DELETE CASCADE ..." instead of "DELETE ... CASCADE" due to unresolvable bison conflicts. Sample session: postgres=# create table foo (id serial primary key, val text); CREATE TABLE postgres=# create table bar (id serial primary key, foo_id int references foo(id), val text); CREATE TABLE postgres=# insert into foo (val) values ('a'),('b'),('c'); INSERT 0 3 postgres=# insert into bar (foo_id, val) values (1,'d'),(1,'e'),(2,'f'),(2,'g'); INSERT 0 4 postgres=# select * from foo; id | val ----+----- 1 | a 2 | b 3 | c (3 rows) postgres=# select * from bar; id | foo_id | val ----+--------+----- 1 | 1 | d 2 | 1 | e 3 | 2 | f 4 | 2 | g (4 rows) postgres=# delete from foo where id = 1; ERROR: update or delete on table "foo" violates foreign key constraint "bar_foo_id_fkey" on table "bar" DETAIL: Key (id)=(1) is still referenced from table "bar". postgres=# delete cascade from foo where id = 1; DELETE 1 postgres=# select * from foo; id | val ----+----- 2 | b 3 | c (2 rows) postgres=# select * from bar; id | foo_id | val ----+--------+----- 3 | 2 | f 4 | 2 | g (2 rows) --- doc/src/sgml/ddl.sgml | 12 ++++++++++ doc/src/sgml/ref/delete.sgml | 21 ++++++++++++++-- src/backend/executor/nodeModifyTable.c | 6 +++++ src/backend/nodes/copyfuncs.c | 2 ++ src/backend/nodes/equalfuncs.c | 1 + src/backend/optimizer/plan/createplan.c | 6 +++-- src/backend/optimizer/plan/planner.c | 1 + src/backend/optimizer/util/pathnode.c | 4 ++++ src/backend/parser/analyze.c | 11 +++++++++ src/backend/parser/gram.y | 18 ++++++++++---- src/backend/utils/adt/ri_triggers.c | 29 ++++++++++++++++++++--- src/include/nodes/execnodes.h | 2 ++ src/include/nodes/parsenodes.h | 3 +++ src/include/nodes/pathnodes.h | 1 + src/include/nodes/plannodes.h | 1 + src/include/optimizer/pathnode.h | 1 + src/test/regress/expected/foreign_key.out | 24 +++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 12 ++++++++++ 18 files changed, 143 insertions(+), 12 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 498654876f..d8bf58f0d9 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1062,6 +1062,18 @@ CREATE TABLE order_items ( operation will fail. + + Note: If using DELETE with the CASCADE + option, RESTRICT and NO ACTION + constraints will be treated as if they were defined + as CASCADE constraints for the duration of the query. + This can be useful for ad-hoc cleanup of data with complicated relational + hierarchies, where you do not want to manually hunt down dependent records + yourself. Consideration should be given to whether using this form makes + sense, or whether you would be better suited in redesigning your + constraint definitions to be ON DELETE CASCADE instead. + + Analogous to ON DELETE there is also ON UPDATE which is invoked when a referenced diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml index 1b81b4e7d7..009dbfc85f 100644 --- a/doc/src/sgml/ref/delete.sgml +++ b/doc/src/sgml/ref/delete.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] -DELETE FROM [ ONLY ] table_name [ * ] [ [ AS ] alias ] +DELETE [ CASCADE ] FROM [ ONLY ] table_name [ * ] [ [ AS ] alias ] [ USING from_item [, ...] ] [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ] @@ -63,12 +63,21 @@ DELETE FROM [ ONLY ] table_name [ * output list of SELECT. + + You can provide the optional CASCADE keyword, which causes + DELETE to treat existing RESTRICT or + NO ACTION constraints as if they were defined + as CASCADE constraints. + + You must have the DELETE privilege on the table to delete from it, as well as the SELECT privilege for any table in the USING clause or whose values are read in the condition. + class="parameter">condition. If the CASCADE + option is provided, you must also have the same privileges on any tables affected + by the corresponding foreign key constraints. @@ -265,6 +274,13 @@ DELETE FROM tasks WHERE status = 'DONE' RETURNING *; c_tasks is currently positioned: DELETE FROM tasks WHERE CURRENT OF c_tasks; + + + + Delete a specific row of authors as well as any + related records in the books, regardless of constraint type: + +DELETE CASCADE FROM authors WHERE author_name = 'Herman Melville'; @@ -274,6 +290,7 @@ DELETE FROM tasks WHERE CURRENT OF c_tasks; This command conforms to the SQL standard, except that the USING and RETURNING clauses + and the CASCADE behavior are PostgreSQL extensions, as is the ability to use WITH with DELETE. diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 379b056310..b5fd7975ed 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -82,6 +82,8 @@ static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate, TupleTableSlot *slot, ResultRelInfo **partRelInfo); +extern bool force_cascade_del; + /* * Verify that the tuples to be produced by INSERT match the * target relation's rowtype @@ -1347,6 +1349,9 @@ ldelete:; ar_delete_trig_tcs = NULL; } + /* set force cascade flag */ + force_cascade_del = mtstate->forceCascade; + /* AFTER ROW DELETE Triggers */ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, ar_delete_trig_tcs); @@ -2718,6 +2723,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->operation = operation; mtstate->canSetTag = node->canSetTag; mtstate->mt_done = false; + mtstate->forceCascade = node->forceCascade; mtstate->mt_nrels = nrels; mtstate->resultRelInfo = (ResultRelInfo *) diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 90770a89b0..80586966cb 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3171,6 +3171,7 @@ _copyQuery(const Query *from) COPY_SCALAR_FIELD(hasDistinctOn); COPY_SCALAR_FIELD(hasRecursive); COPY_SCALAR_FIELD(hasModifyingCTE); + COPY_SCALAR_FIELD(forceCascade); COPY_SCALAR_FIELD(hasForUpdate); COPY_SCALAR_FIELD(hasRowSecurity); COPY_SCALAR_FIELD(isReturn); @@ -3239,6 +3240,7 @@ _copyDeleteStmt(const DeleteStmt *from) COPY_NODE_FIELD(whereClause); COPY_NODE_FIELD(returningList); COPY_NODE_FIELD(withClause); + COPY_SCALAR_FIELD(forceCascade); return newnode; } diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index ce76d093dd..2806440c08 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1036,6 +1036,7 @@ _equalDeleteStmt(const DeleteStmt *a, const DeleteStmt *b) COMPARE_NODE_FIELD(whereClause); COMPARE_NODE_FIELD(returningList); COMPARE_NODE_FIELD(withClause); + COMPARE_SCALAR_FIELD(forceCascade); return true; } diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 439e6b6426..01ed2289f2 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -311,7 +311,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam); + List *rowMarks, OnConflictExpr *onconflict, bool forceCascade, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, GatherMergePath *best_path); @@ -2756,6 +2756,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) best_path->returningLists, best_path->rowMarks, best_path->onconflict, + best_path->forceCascade, best_path->epqParam); copy_generic_path_info(&plan->plan, &best_path->path); @@ -6879,7 +6880,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam) + List *rowMarks, OnConflictExpr *onconflict, bool forceCascade, int epqParam) { ModifyTable *node = makeNode(ModifyTable); List *fdw_private_list; @@ -6906,6 +6907,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan, node->nominalRelation = nominalRelation; node->rootRelation = rootRelation; node->partColsUpdated = partColsUpdated; + node->forceCascade = forceCascade; node->resultRelations = resultRelations; if (!onconflict) { diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 1868c4eff4..c6cd2941ee 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1845,6 +1845,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction) parse->resultRelation, rootRelation, root->partColsUpdated, + parse->forceCascade, resultRelations, updateColnosLists, withCheckOptionLists, diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 9ce5f95e3b..2c311a5d79 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3629,6 +3629,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, bool partColsUpdated, + bool forceCascade, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, @@ -3644,6 +3645,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, list_length(resultRelations) == list_length(withCheckOptionLists)); Assert(returningLists == NIL || list_length(resultRelations) == list_length(returningLists)); + Assert(operation == CMD_DELETE || !forceCascade); pathnode->path.pathtype = T_ModifyTable; pathnode->path.parent = rel; @@ -3655,6 +3657,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, pathnode->path.parallel_safe = false; pathnode->path.parallel_workers = 0; pathnode->path.pathkeys = NIL; + /* copy forceCascade flag */ + pathnode->forceCascade = forceCascade; /* * Compute cost & rowcount as subpath cost & rowcount (if RETURNING) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 9cede29d6a..7c7e91c9b1 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -440,6 +440,17 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasModifyingCTE = pstate->p_hasModifyingCTE; } + /* if we have provided CASCADE, set this to true */ + if (stmt->forceCascade) + { + /* validate that this is not a RETURNING query */ + /* XXX do we actually need this, since normal CASCADE FKs would + * behave the same way. */ + if (stmt->returningList) + elog(ERROR, "cannot use DELETE CASCADE with a RETURNING clause"); + + qry->forceCascade = true; + } /* set up range table with just the result rel */ qry->resultRelation = setTargetTable(pstate, stmt->relation, stmt->relation->inh, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9ee90e3f13..555b557f89 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -466,6 +466,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type unicode_normal_form %type opt_instead +%type opt_cascade %type opt_unique opt_concurrently opt_verbose opt_full %type opt_freeze opt_analyze opt_default opt_recheck %type opt_binary copy_delimiter @@ -11133,15 +11134,17 @@ returning_clause: * *****************************************************************************/ -DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias +DeleteStmt: + opt_with_clause DELETE_P opt_cascade FROM relation_expr_opt_alias using_clause where_or_current_clause returning_clause { DeleteStmt *n = makeNode(DeleteStmt); - n->relation = $4; - n->usingClause = $5; - n->whereClause = $6; - n->returningList = $7; + n->relation = $5; + n->usingClause = $6; + n->whereClause = $7; + n->returningList = $8; n->withClause = $1; + n->forceCascade = $3; $$ = (Node *)n; } ; @@ -11151,6 +11154,11 @@ using_clause: | /*EMPTY*/ { $$ = NIL; } ; +opt_cascade: + CASCADE { $$ = true; } + | /*EMPTY*/ { $$ = false; } + ; + /***************************************************************************** * diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 96269fc2ad..102c1d003d 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -90,6 +90,7 @@ #define RI_TRIGTYPE_UPDATE 2 #define RI_TRIGTYPE_DELETE 3 +bool force_cascade_del = false; /* * RI_ConstraintInfo @@ -180,6 +181,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, TupleTableSlot *oldslot, const RI_ConstraintInfo *riinfo); static Datum ri_restrict(TriggerData *trigdata, bool is_no_action); +static Datum ri_cascade_del(TriggerData *trigdata); static Datum ri_set(TriggerData *trigdata, bool is_set_null); static void quoteOneName(char *buffer, const char *name); static void quoteRelationName(char *buffer, Relation rel); @@ -550,6 +552,11 @@ RI_FKey_noaction_del(PG_FUNCTION_ARGS) /* Check that this is a valid trigger call on the right time and event. */ ri_CheckTrigger(fcinfo, "RI_FKey_noaction_del", RI_TRIGTYPE_DELETE); + /* If we are overriding the main action to handle as a CASCADE instead, + * handle the main ri_cascade_del guts */ + if (force_cascade_del) + return ri_cascade_del((TriggerData *) fcinfo->context); + /* Share code with RESTRICT/UPDATE cases. */ return ri_restrict((TriggerData *) fcinfo->context, true); } @@ -570,6 +577,11 @@ RI_FKey_restrict_del(PG_FUNCTION_ARGS) /* Check that this is a valid trigger call on the right time and event. */ ri_CheckTrigger(fcinfo, "RI_FKey_restrict_del", RI_TRIGTYPE_DELETE); + /* If we are overriding the main action to handle as a CASCADE instead, + * handle the main ri_cascade_del guts */ + if (force_cascade_del) + return ri_cascade_del((TriggerData *) fcinfo->context); + /* Share code with NO ACTION/UPDATE cases. */ return ri_restrict((TriggerData *) fcinfo->context, false); } @@ -739,7 +751,20 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) Datum RI_FKey_cascade_del(PG_FUNCTION_ARGS) { - TriggerData *trigdata = (TriggerData *) fcinfo->context; + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "RI_FKey_cascade_del", RI_TRIGTYPE_DELETE); + + return ri_cascade_del((TriggerData *) fcinfo->context); +} + +/* + * ri_cascade_del - + * + * Shared guts for cascaded deletes; pulled out to allow override of other constraint types + */ +Datum +ri_cascade_del(TriggerData *trigdata) +{ const RI_ConstraintInfo *riinfo; Relation fk_rel; Relation pk_rel; @@ -747,8 +772,6 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS) RI_QueryKey qkey; SPIPlanPtr qplan; - /* Check that this is a valid trigger call on the right time and event. */ - ri_CheckTrigger(fcinfo, "RI_FKey_cascade_del", RI_TRIGTYPE_DELETE); riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger, trigdata->tg_relation, true); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 7795a69490..a8d94ea1ee 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1201,6 +1201,8 @@ typedef struct ModifyTableState EPQState mt_epqstate; /* for evaluating EvalPlanQual rechecks */ bool fireBSTriggers; /* do we need to fire stmt triggers? */ + bool forceCascade; /* do we need to force cascade triggers? */ + /* * These fields are used for inherited UPDATE and DELETE, to track which * target relation a given tuple is from. If there are a lot of target diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ef73342019..0080b059ce 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -142,6 +142,8 @@ typedef struct Query bool isReturn; /* is a RETURN statement */ + bool forceCascade; /* should force a restriction as a cascade */ + List *cteList; /* WITH list (of CommonTableExpr's) */ List *rtable; /* list of range table entries */ @@ -1598,6 +1600,7 @@ typedef struct DeleteStmt Node *whereClause; /* qualifications */ List *returningList; /* list of expressions to return */ WithClause *withClause; /* WITH clause */ + bool forceCascade; /* whether to force this delete as a cascade */ } DeleteStmt; /* ---------------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index b7b2817a5d..79d66a1668 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1876,6 +1876,7 @@ typedef struct ModifyTablePath Index nominalRelation; /* Parent RT index for use of EXPLAIN */ Index rootRelation; /* Root RT index, if target is partitioned */ bool partColsUpdated; /* some part key in hierarchy updated? */ + bool forceCascade; /* whether to force constraints as cascade */ List *resultRelations; /* integer list of RT indexes */ List *updateColnosLists; /* per-target-table update_colnos lists */ List *withCheckOptionLists; /* per-target-table WCO lists */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index aaa3b65d04..b5f83e342d 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -222,6 +222,7 @@ typedef struct ModifyTable Index nominalRelation; /* Parent RT index for use of EXPLAIN */ Index rootRelation; /* Root RT index, if target is partitioned */ bool partColsUpdated; /* some part key in hierarchy updated? */ + bool forceCascade; /* do we need to force cascade on DELETE FKs? */ List *resultRelations; /* integer list of RT indexes */ List *updateColnosLists; /* per-target-table update_colnos lists */ List *withCheckOptionLists; /* per-target-table WCO lists */ diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 53261ee91f..8b2e221719 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -271,6 +271,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, bool partColsUpdated, + bool forceCascade, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index bf794dce9d..17986ea2be 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -339,6 +339,30 @@ SELECT * FROM PKTABLE; 0 | Test4 (4 rows) +-- Delete without cascade (should fail) +DELETE FROM PKTABLE WHERE ptest1=1; +ERROR: update or delete on table "pktable" violates foreign key constraint "fktable_ftest1_fkey" on table "fktable" +DETAIL: Key (ptest1)=(1) is still referenced from table "fktable". +-- Delete with cascade (should succeed) +DELETE CASCADE FROM PKTABLE WHERE ptest1=1; +-- Check PKTABLE for updates +SELECT * FROM PKTABLE; + ptest1 | ptest2 +--------+-------- + 2 | Test2 + 3 | Test3 + 0 | Test4 +(3 rows) + +-- Check FKTABLE for updates +SELECT * FROM FKTABLE; + ftest1 | ftest2 +--------+-------- + 2 | 3 + 3 | 4 + | 1 +(3 rows) + DROP TABLE FKTABLE; DROP TABLE PKTABLE; -- diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index de417b62b6..039cc06d8b 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -216,6 +216,18 @@ UPDATE PKTABLE SET ptest1=0 WHERE ptest1=4; -- Check PKTABLE for updates SELECT * FROM PKTABLE; +-- Delete without cascade (should fail) +DELETE FROM PKTABLE WHERE ptest1=1; + +-- Delete with cascade (should succeed) +DELETE CASCADE FROM PKTABLE WHERE ptest1=1; + +-- Check PKTABLE for updates +SELECT * FROM PKTABLE; + +-- Check FKTABLE for updates +SELECT * FROM FKTABLE; + DROP TABLE FKTABLE; DROP TABLE PKTABLE; -- 2.30.1 (Apple Git-130)