From cea905874ff0afe962d32b2110faf5f9e61330d8 Mon Sep 17 00:00:00 2001
From: David Christensen <david.christensen@crunchydata.com>
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.
    </para>
 
+   <para>
+    Note: If using <command>DELETE</command> with the <literal>CASCADE</literal>
+    option, <literal>RESTRICT</literal> and <literal>NO ACTION</literal>
+    constraints will be treated as if they were defined
+    as <literal>CASCADE</literal> 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 <literal>ON DELETE CASCADE</literal> instead.
+   </para>
+
    <para>
     Analogous to <literal>ON DELETE</literal> there is also
     <literal>ON UPDATE</literal> 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
  <refsynopsisdiv>
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
-DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+DELETE [ CASCADE ] FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
     [ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
     [ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
@@ -63,12 +63,21 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    output list of <command>SELECT</command>.
   </para>
 
+  <para>
+   You can provide the optional <literal>CASCADE</literal> keyword, which causes
+   <command>DELETE</command> to treat existing <literal>RESTRICT</literal> or
+   <literal>NO ACTION</literal> constraints as if they were defined
+   as <literal>CASCADE</literal> constraints.
+  </para>
+
   <para>
    You must have the <literal>DELETE</literal> privilege on the table
    to delete from it, as well as the <literal>SELECT</literal>
    privilege for any table in the <literal>USING</literal> clause or
    whose values are read in the <replaceable
-   class="parameter">condition</replaceable>.
+   class="parameter">condition</replaceable>.  If the <literal>CASCADE</literal>
+   option is provided, you must also have the same privileges on any tables affected
+   by the corresponding foreign key constraints.
   </para>
  </refsect1>
 
@@ -265,6 +274,13 @@ DELETE FROM tasks WHERE status = 'DONE' RETURNING *;
    <literal>c_tasks</literal> is currently positioned:
 <programlisting>
 DELETE FROM tasks WHERE CURRENT OF c_tasks;
+</programlisting></para>
+
+   <para>
+    Delete a specific row of <structname>authors</structname> as well as any
+    related records in the <structname>books</structname>, regardless of constraint type:
+<programlisting>
+DELETE CASCADE FROM authors WHERE author_name = 'Herman Melville';
 </programlisting></para>
  </refsect1>
 
@@ -274,6 +290,7 @@ DELETE FROM tasks WHERE CURRENT OF c_tasks;
   <para>
    This command conforms to the <acronym>SQL</acronym> standard, except
    that the <literal>USING</literal> and <literal>RETURNING</literal> clauses
+   and the <literal>CASCADE</literal> behavior
    are <productname>PostgreSQL</productname> extensions, as is the ability
    to use <literal>WITH</literal> with <command>DELETE</command>.
   </para>
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 <str>		unicode_normal_form
 
 %type <boolean> opt_instead
+%type <boolean> opt_cascade
 %type <boolean> opt_unique opt_concurrently opt_verbose opt_full
 %type <boolean> opt_freeze opt_analyze opt_default opt_recheck
 %type <defelt>	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)

