Sorry about the long delay in answering that, I hope to get to a consensus
on how to do that feature, which I think it is really valuable. Sending few
options and observations bellow...

On Sun, Jul 28, 2019 at 2:37 PM Tom Lane <t...@sss.pgh.pa.us> wrote:

> Matheus de Oliveira <matioli.math...@gmail.com> writes:
> > [ postgresql-alter-constraint.v5.patch ]
>
> Somebody seems to have marked this Ready For Committer without posting
> any review, which is not very kosher,


Sorry. I know Lucas, will talk to him for a better review ;D


> but I took a quick look at it
> anyway.
>
>
Thank you so much by that.

* It's failing to apply, as noted by the cfbot, because somebody added
> an unrelated test to the same spot in foreign_key.sql.  I fixed that
> in the attached rebase.
>
>
That was a mistake on rebase, sorry.


> * It also doesn't pass "git diff --check" whitespace checks, so
> I ran it through pgindent.
>
>
Still learning here, will take more care.


> * Grepping around for other references to struct Constraint,
> I noticed that you'd missed updating equalfuncs.c.  I did not
> fix that.
>
>
Certainly true, I fixed that just to keep it OK for now.


> The main issue I've got though is a definitional one --- I'm not at all
> sold on your premise that constraint deferrability syntax should mean
> different things depending on the previous state of the constraint.
>

I see the point, but I really believe we should have a simpler way to
change just specific properties
of the constraint without touching the others, and I do believe it is
valuable. So I'd like to check with
you all what would be a good option to have that.

Just as a demonstration, and a PoC, I have changed the patch to accept two
different syntaxes:
1. The one we have today with ALTER CONSTRAINT, and it change every
constraint property
2. A similar one with SET keyword in the middle, to force changing only the
given properties, e.g.:
        ALTER TABLE table_name ALTER CONSTRAINT constr_name *SET* ON UPDATE
CASCADE;

I'm not at all happy with the syntax, doens't seem very clear. But I
proceeded this way nonetheless
just to verify the code on tablecmds.c would work. Please, does NOT
consider the patch as "ready",
it is more like a WIP and demonstration now (specially the test part, which
is no longer complete,
and gram.y that I changed the lazy way - both easy to fix if the syntax is
good).

I would really appreciate opinions on that, and I'm glad to work on a
better patch after we decide
the best syntax and approach.


> We don't generally act that way in other ALTER commands


That is true. I think one exception is ALTER COLUMN, which just acts on the
changes explicitly provided.
And I truly believe most people would expect changes on only provided
information on ALTER CONSTRAINT
as well. But I have no real research on that, more like a feeling :P


> and I don't see
> a strong argument to start doing so here.  If you didn't do this then
> you wouldn't (I think) need the extra struct Constraint fields in the
> first place, which is why I didn't run off and change equalfuncs.c.
>
>
Indeed true, changes on `Constraint` struct were only necessary due to
that, the patch would in fact
be way simpler without it (that is why I still insist on finding some way
to make it happen, perhaps
with a better syntax).


> In short, I'm inclined to argue that this variant of ALTER TABLE
> should replace *all* the fields of the constraint with the same
> properties it'd have if you'd created it fresh using the same syntax.
> This is by analogy to CREATE OR REPLACE commands, which don't
> preserve any of the old properties of the replaced object.


I agree for CREATE OR REPLACE, but in my POV REPLACE makes it clearer to
the user that
*everything* is changed, ALTER not so much. Again, this is just *my
opinion*, not a very strong
one though, but following first messages on that thread current behaviour
can be easily confused
with a bug (although it is not, the code clear shows it is expected,
specially on tests).


> Given
> the interactions between these fields, I think you're going to end up
> with a surprising mess of ad-hoc choices if you do differently.
> Indeed, you already have, but I think it'll get worse if anyone
> tries to extend the feature set further.
>
>
Certainly agree with that, the code is harder that way, as I said above.
Still thinking that
having the option is valuable though, we should be able to find a better
syntax/approach
for that.


> Perhaps the right way to attack it, given that, is to go ahead and
> invent "ALTER TABLE t ADD OR REPLACE CONSTRAINT c ...".  At least
> in the case at hand with FK constraints, we could apply suitable
> optimizations (ie skip revalidation) when the new definition shares
> the right properties with the old, and otherwise treat it like a
> drop-and-add.
>

I believe this path is quite easy for me to do now, if you all agree it is
a good approach.
What worries me is that we already have ALTER CONSTRAINT syntax, so what
would
we do with that? I see a few options:
1. Leave ALTER CONSTRAINT to only change given properties (as I proposed at
first), and let
    ADD OR REPLACE to do a full change
2. Have only ADD OR REPLACE and deprecate ALTER CONSTRAINT (I think it is
too harsh
    for users already using it, a big compatibility change)
3. Just have both syntaxes and add a syntax similar to the SET I'm sending
here to keep
    current properties (works well, but the syntax seems ugly to me, better
ideas?)


Best regards,
-- 
Matheus de Oliveira
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index cb9b60415d..e22f4f924e 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -55,7 +55,9 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ALTER [ COLUMN ] <replaceable class="parameter">column_name</replaceable> SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN }
     ADD <replaceable class="parameter">table_constraint</replaceable> [ NOT VALID ]
     ADD <replaceable class="parameter">table_constraint_using_index</replaceable>
-    ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+    ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
+      [ ON DELETE <replaceable class="parameter">action</replaceable> ] [ ON UPDATE <replaceable class="parameter">action</replaceable> ]
+      [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
     VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
     DROP CONSTRAINT [ IF EXISTS ]  <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
     DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 05593f3316..689bd8add5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9007,8 +9007,43 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
 				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint",
 						cmdcon->conname, RelationGetRelationName(rel))));
 
+	/*
+	 * Verify for FKCONSTR_ACTION_UNKNOWN, if found, replace by current
+	 * action. We could handle FKCONSTR_ACTION_UNKNOWN bellow, but since we
+	 * already have to handle the case of changing to the same action, seems
+	 * simpler to simple replace FKCONSTR_ACTION_UNKNOWN by the current action
+	 * here.
+	 */
+	if (cmdcon->fk_del_action == FKCONSTR_ACTION_UNKNOWN)
+		cmdcon->fk_del_action = currcon->confdeltype;
+
+	if (cmdcon->fk_upd_action == FKCONSTR_ACTION_UNKNOWN)
+		cmdcon->fk_upd_action = currcon->confupdtype;
+
+	/*
+	 * Do the same for deferrable attributes. But consider that if changed
+	 * only initdeferred attribute and to true, force deferrable to be also
+	 * true. On the other hand, if changed only deferrable attribute and to
+	 * false, force initdeferred to be also false.
+	 */
+	if (!cmdcon->was_deferrable_set)
+		cmdcon->deferrable = cmdcon->initdeferred ? true : currcon->condeferrable;
+
+	if (!cmdcon->was_initdeferred_set)
+		cmdcon->initdeferred = !cmdcon->deferrable ? false : currcon->condeferred;
+
+	/*
+	 * This is a safe check only, should never happen here.
+	 */
+	if (cmdcon->initdeferred && !cmdcon->deferrable)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("constraint declared INITIALLY DEFERRED must be DEFERRABLE")));
+
 	if (currcon->condeferrable != cmdcon->deferrable ||
-		currcon->condeferred != cmdcon->initdeferred)
+		currcon->condeferred != cmdcon->initdeferred ||
+		currcon->confdeltype != cmdcon->fk_del_action ||
+		currcon->confupdtype != cmdcon->fk_upd_action)
 	{
 		HeapTuple	copyTuple;
 		HeapTuple	tgtuple;
@@ -9026,6 +9061,8 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
 		copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
 		copy_con->condeferrable = cmdcon->deferrable;
 		copy_con->condeferred = cmdcon->initdeferred;
+		copy_con->confdeltype = cmdcon->fk_del_action;
+		copy_con->confupdtype = cmdcon->fk_upd_action;
 		CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
 
 		InvokeObjectPostAlterHook(ConstraintRelationId,
@@ -9062,23 +9099,106 @@ ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd,
 				otherrelids = list_append_unique_oid(otherrelids,
 													 tgform->tgrelid);
 
-			/*
-			 * Update deferrability of RI_FKey_noaction_del,
-			 * RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
-			 * triggers, but not others; see createForeignKeyActionTriggers
-			 * and CreateFKCheckTrigger.
-			 */
-			if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
-				tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
-				tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
-				tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
-				continue;
-
 			copyTuple = heap_copytuple(tgtuple);
 			copy_tg = (Form_pg_trigger) GETSTRUCT(copyTuple);
 
+			/*
+			 * Set deferrability here, but note that it may be overridden
+			 * bellow if the pg_trigger entry is on the referencing table and
+			 * depending on the action used for ON UPDATE/DELETE. But for
+			 * check triggers (in the referenced table) it is kept as is
+			 * (since ON UPDATE/DELETE actions makes no difference for the
+			 * check triggers).
+			 */
 			copy_tg->tgdeferrable = cmdcon->deferrable;
 			copy_tg->tginitdeferred = cmdcon->initdeferred;
+
+			/*
+			 * Set ON DELETE action
+			 */
+			if (tgform->tgfoid == F_RI_FKEY_NOACTION_DEL ||
+				tgform->tgfoid == F_RI_FKEY_RESTRICT_DEL ||
+				tgform->tgfoid == F_RI_FKEY_CASCADE_DEL ||
+				tgform->tgfoid == F_RI_FKEY_SETNULL_DEL ||
+				tgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL)
+			{
+				switch (cmdcon->fk_del_action)
+				{
+					case FKCONSTR_ACTION_NOACTION:
+						copy_tg->tgdeferrable = cmdcon->deferrable;
+						copy_tg->tginitdeferred = cmdcon->initdeferred;
+						copy_tg->tgfoid = F_RI_FKEY_NOACTION_DEL;
+						break;
+					case FKCONSTR_ACTION_RESTRICT:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_RESTRICT_DEL;
+						break;
+					case FKCONSTR_ACTION_CASCADE:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_CASCADE_DEL;
+						break;
+					case FKCONSTR_ACTION_SETNULL:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_SETNULL_DEL;
+						break;
+					case FKCONSTR_ACTION_SETDEFAULT:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_SETDEFAULT_DEL;
+						break;
+					default:
+						elog(ERROR, "unrecognized FK action type: %d",
+							 (int) cmdcon->fk_del_action);
+						break;
+				}
+			}
+
+			/*
+			 * Set ON UPDATE action
+			 */
+			if (tgform->tgfoid == F_RI_FKEY_NOACTION_UPD ||
+				tgform->tgfoid == F_RI_FKEY_RESTRICT_UPD ||
+				tgform->tgfoid == F_RI_FKEY_CASCADE_UPD ||
+				tgform->tgfoid == F_RI_FKEY_SETNULL_UPD ||
+				tgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD)
+			{
+				switch (cmdcon->fk_upd_action)
+				{
+					case FKCONSTR_ACTION_NOACTION:
+						copy_tg->tgdeferrable = cmdcon->deferrable;
+						copy_tg->tginitdeferred = cmdcon->initdeferred;
+						copy_tg->tgfoid = F_RI_FKEY_NOACTION_UPD;
+						break;
+					case FKCONSTR_ACTION_RESTRICT:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_RESTRICT_UPD;
+						break;
+					case FKCONSTR_ACTION_CASCADE:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_CASCADE_UPD;
+						break;
+					case FKCONSTR_ACTION_SETNULL:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_SETNULL_UPD;
+						break;
+					case FKCONSTR_ACTION_SETDEFAULT:
+						copy_tg->tgdeferrable = false;
+						copy_tg->tginitdeferred = false;
+						copy_tg->tgfoid = F_RI_FKEY_SETDEFAULT_UPD;
+						break;
+					default:
+						elog(ERROR, "unrecognized FK action type: %d",
+							 (int) cmdcon->fk_upd_action);
+						break;
+				}
+			}
+
 			CatalogTupleUpdate(tgrel, &copyTuple->t_self, copyTuple);
 
 			InvokeObjectPostAlterHook(TriggerRelationId, currcon->oid, 0);
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 3432bb921d..58241c1214 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2910,6 +2910,8 @@ _copyConstraint(const Constraint *from)
 	COPY_SCALAR_FIELD(deferrable);
 	COPY_SCALAR_FIELD(initdeferred);
 	COPY_LOCATION_FIELD(location);
+	COPY_SCALAR_FIELD(was_deferrable_set);
+	COPY_SCALAR_FIELD(was_initdeferred_set);
 	COPY_SCALAR_FIELD(is_no_inherit);
 	COPY_NODE_FIELD(raw_expr);
 	COPY_STRING_FIELD(cooked_expr);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 18cb014373..584e2e32a0 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2594,6 +2594,8 @@ _equalConstraint(const Constraint *a, const Constraint *b)
 	COMPARE_SCALAR_FIELD(deferrable);
 	COMPARE_SCALAR_FIELD(initdeferred);
 	COMPARE_LOCATION_FIELD(location);
+	COMPARE_SCALAR_FIELD(was_deferrable_set);
+	COMPARE_SCALAR_FIELD(was_initdeferred_set);
 	COMPARE_SCALAR_FIELD(is_no_inherit);
 	COMPARE_NODE_FIELD(raw_expr);
 	COMPARE_STRING_FIELD(cooked_expr);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b0dcd02ff6..dc45f154e3 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3459,6 +3459,8 @@ _outConstraint(StringInfo str, const Constraint *node)
 	WRITE_BOOL_FIELD(deferrable);
 	WRITE_BOOL_FIELD(initdeferred);
 	WRITE_LOCATION_FIELD(location);
+	WRITE_BOOL_FIELD(was_deferrable_set);
+	WRITE_BOOL_FIELD(was_initdeferred_set);
 
 	appendStringInfoString(str, " :contype ");
 	switch (node->contype)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3f67aaf30e..3cd5579515 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -185,7 +185,8 @@ static void SplitColQualList(List *qualList,
 							 List **constraintList, CollateClause **collClause,
 							 core_yyscan_t yyscanner);
 static void processCASbits(int cas_bits, int location, const char *constrType,
-			   bool *deferrable, bool *initdeferred, bool *not_valid,
+			   bool *deferrable, bool *was_deferrable_set,
+			   bool *initdeferred, bool *was_initdeferred_set, bool *not_valid,
 			   bool *no_inherit, core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -543,7 +544,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
-%type <ival>	key_actions key_delete key_match key_update key_action
+%type <ival>	key_actions opt_key_actions
+%type <ival>	key_delete key_match key_update key_action
 %type <ival>	ConstraintAttributeSpec ConstraintAttributeElem
 %type <str>		ExistingIndex
 
@@ -2275,8 +2277,25 @@ alter_table_cmd:
 					n->def = $2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> ALTER CONSTRAINT ... SET */
+			| ALTER CONSTRAINT name SET opt_key_actions ConstraintAttributeSpec
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					Constraint *c = makeNode(Constraint);
+					n->subtype = AT_AlterConstraint;
+					n->def = (Node *) c;
+					c->contype = CONSTR_FOREIGN; /* others not supported, yet */
+					c->conname = $3;
+					c->fk_upd_action = (char) ($5 >> 8);
+					c->fk_del_action = (char) ($5 & 0xFF);
+					processCASbits($6, @5, "ALTER CONSTRAINT statement",
+									&c->deferrable, &c->was_deferrable_set,
+									&c->initdeferred, &c->was_initdeferred_set,
+									NULL, NULL, yyscanner);
+					$$ = (Node *)n;
+				}
 			/* ALTER TABLE <name> ALTER CONSTRAINT ... */
-			| ALTER CONSTRAINT name ConstraintAttributeSpec
+			| ALTER CONSTRAINT name key_actions ConstraintAttributeSpec
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
 					Constraint *c = makeNode(Constraint);
@@ -2284,9 +2303,14 @@ alter_table_cmd:
 					n->def = (Node *) c;
 					c->contype = CONSTR_FOREIGN; /* others not supported, yet */
 					c->conname = $3;
-					processCASbits($4, @4, "ALTER CONSTRAINT statement",
-									&c->deferrable,
-									&c->initdeferred,
+					c->fk_upd_action = (char) ($4 >> 8);
+					c->fk_del_action = (char) ($4 & 0xFF);
+					/* Without SET, always change deferrability */
+					c->was_deferrable_set = true;
+					c->was_initdeferred_set = true;
+					processCASbits($5, @4, "ALTER CONSTRAINT statement",
+									&c->deferrable, NULL,
+									&c->initdeferred, NULL,
 									NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
@@ -3643,7 +3667,7 @@ ConstraintElem:
 					n->raw_expr = $3;
 					n->cooked_expr = NULL;
 					processCASbits($5, @5, "CHECK",
-								   NULL, NULL, &n->skip_validation,
+								   NULL, NULL, NULL, NULL, &n->skip_validation,
 								   &n->is_no_inherit, yyscanner);
 					n->initially_valid = !n->skip_validation;
 					$$ = (Node *)n;
@@ -3660,8 +3684,8 @@ ConstraintElem:
 					n->indexname = NULL;
 					n->indexspace = $7;
 					processCASbits($8, @8, "UNIQUE",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
 			| UNIQUE ExistingIndex ConstraintAttributeSpec
@@ -3675,8 +3699,8 @@ ConstraintElem:
 					n->indexname = $2;
 					n->indexspace = NULL;
 					processCASbits($3, @3, "UNIQUE",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
 			| PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
@@ -3691,8 +3715,8 @@ ConstraintElem:
 					n->indexname = NULL;
 					n->indexspace = $8;
 					processCASbits($9, @9, "PRIMARY KEY",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
 			| PRIMARY KEY ExistingIndex ConstraintAttributeSpec
@@ -3706,8 +3730,8 @@ ConstraintElem:
 					n->indexname = $3;
 					n->indexspace = NULL;
 					processCASbits($4, @4, "PRIMARY KEY",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
 			| EXCLUDE access_method_clause '(' ExclusionConstraintList ')'
@@ -3725,8 +3749,8 @@ ConstraintElem:
 					n->indexspace		= $8;
 					n->where_clause		= $9;
 					processCASbits($10, @10, "EXCLUDE",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					$$ = (Node *)n;
 				}
 			| FOREIGN KEY '(' columnList ')' REFERENCES qualified_name
@@ -3742,7 +3766,8 @@ ConstraintElem:
 					n->fk_upd_action	= (char) ($10 >> 8);
 					n->fk_del_action	= (char) ($10 & 0xFF);
 					processCASbits($11, @11, "FOREIGN KEY",
-								   &n->deferrable, &n->initdeferred,
+								   &n->deferrable, NULL,
+								   &n->initdeferred, NULL,
 								   &n->skip_validation, NULL,
 								   yyscanner);
 					n->initially_valid = !n->skip_validation;
@@ -3822,7 +3847,7 @@ ExclusionWhereClause:
  * We combine the update and delete actions into one value temporarily
  * for simplicity of parsing, and then break them down again in the
  * calling production.  update is in the left 8 bits, delete in the right.
- * Note that NOACTION is the default.
+ * Note that NOACTION is the default. See also opt_key_actions.
  */
 key_actions:
 			key_update
@@ -3837,6 +3862,23 @@ key_actions:
 				{ $$ = (FKCONSTR_ACTION_NOACTION << 8) | (FKCONSTR_ACTION_NOACTION & 0xFF); }
 		;
 
+/*
+ * Basically the same as key_actions, but using FKCONSTR_ACTION_UNKNOWN
+ * as the default one instead of NOACTION.
+ */
+opt_key_actions:
+			key_update
+				{ $$ = ($1 << 8) | (FKCONSTR_ACTION_UNKNOWN & 0xFF); }
+			| key_delete
+				{ $$ = (FKCONSTR_ACTION_UNKNOWN << 8) | ($1 & 0xFF); }
+			| key_update key_delete
+				{ $$ = ($1 << 8) | ($2 & 0xFF); }
+			| key_delete key_update
+				{ $$ = ($2 << 8) | ($1 & 0xFF); }
+			| /*EMPTY*/
+				{ $$ = (FKCONSTR_ACTION_UNKNOWN << 8) | (FKCONSTR_ACTION_UNKNOWN & 0xFF); }
+		;
+
 key_update: ON UPDATE key_action		{ $$ = $3; }
 		;
 
@@ -5390,8 +5432,8 @@ CreateTrigStmt:
 					n->transitionRels = NIL;
 					n->isconstraint  = true;
 					processCASbits($10, @10, "TRIGGER",
-								   &n->deferrable, &n->initdeferred, NULL,
-								   NULL, yyscanner);
+								   &n->deferrable, NULL, &n->initdeferred, NULL,
+								   NULL, NULL, yyscanner);
 					n->constrrel = $9;
 					$$ = (Node *)n;
 				}
@@ -16247,7 +16289,8 @@ SplitColQualList(List *qualList,
  */
 static void
 processCASbits(int cas_bits, int location, const char *constrType,
-			   bool *deferrable, bool *initdeferred, bool *not_valid,
+			   bool *deferrable, bool *was_deferrable_set,
+			   bool *initdeferred, bool *was_initdeferred_set, bool *not_valid,
 			   bool *no_inherit, core_yyscan_t yyscanner)
 {
 	/* defaults */
@@ -16258,6 +16301,14 @@ processCASbits(int cas_bits, int location, const char *constrType,
 	if (not_valid)
 		*not_valid = false;
 
+	if (was_deferrable_set)
+		*was_deferrable_set = cas_bits & (CAS_DEFERRABLE
+										  | CAS_NOT_DEFERRABLE) ? true : false;
+
+	if (was_initdeferred_set)
+		*was_initdeferred_set = cas_bits & (CAS_INITIALLY_DEFERRED
+											| CAS_INITIALLY_IMMEDIATE) ? true : false;
+
 	if (cas_bits & (CAS_DEFERRABLE | CAS_INITIALLY_DEFERRED))
 	{
 		if (deferrable)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d93a79a554..141dd43df7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2117,6 +2117,8 @@ typedef enum ConstrType			/* types of constraints */
 #define FKCONSTR_ACTION_CASCADE		'c'
 #define FKCONSTR_ACTION_SETNULL		'n'
 #define FKCONSTR_ACTION_SETDEFAULT	'd'
+#define FKCONSTR_ACTION_UNKNOWN		'u' /* unknown is used only for ALTER
+										 * CONSTRAINT */
 
 /* Foreign key matchtype codes */
 #define FKCONSTR_MATCH_FULL			'f'
@@ -2134,6 +2136,10 @@ typedef struct Constraint
 	bool		initdeferred;	/* INITIALLY DEFERRED? */
 	int			location;		/* token location, or -1 if unknown */
 
+	/* Fields used by ALTER CONSTRAINT to verify if a change was actually made */
+	bool		was_deferrable_set;		/* Was DEFERRABLE informed? */
+	bool		was_initdeferred_set;	/* Was INITIALLY DEFERRED informed? */
+
 	/* Fields used for constraints with expressions (CHECK and DEFAULT): */
 	bool		is_no_inherit;	/* is constraint non-inheritable? */
 	Node	   *raw_expr;		/* expr, as untransformed parse tree */
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 23d4265555..8146ad9eb5 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -704,15 +704,15 @@ ORDER BY 1,2,3;
 ---------+------------------------+--------+--------------+----------------
  fkdd    | "RI_FKey_cascade_del"  |      9 | f            | f
  fkdd    | "RI_FKey_noaction_upd" |     17 | t            | t
- fkdd2   | "RI_FKey_cascade_del"  |      9 | f            | f
+ fkdd2   | "RI_FKey_noaction_del" |      9 | t            | t
  fkdd2   | "RI_FKey_noaction_upd" |     17 | t            | t
  fkdi    | "RI_FKey_cascade_del"  |      9 | f            | f
  fkdi    | "RI_FKey_noaction_upd" |     17 | t            | f
- fkdi2   | "RI_FKey_cascade_del"  |      9 | f            | f
+ fkdi2   | "RI_FKey_noaction_del" |      9 | t            | f
  fkdi2   | "RI_FKey_noaction_upd" |     17 | t            | f
  fknd    | "RI_FKey_cascade_del"  |      9 | f            | f
  fknd    | "RI_FKey_noaction_upd" |     17 | f            | f
- fknd2   | "RI_FKey_cascade_del"  |      9 | f            | f
+ fknd2   | "RI_FKey_noaction_del" |      9 | f            | f
  fknd2   | "RI_FKey_noaction_upd" |     17 | f            | f
 (12 rows)
 
@@ -736,6 +736,28 @@ ORDER BY 1,2,3;
  fknd2   | "RI_FKey_check_upd" |     17 | f            | f
 (12 rows)
 
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+              pg_get_constraintdef               
+-------------------------------------------------
+ FOREIGN KEY (ftest1) REFERENCES pktable(ptest1)
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 INITIALLY IMMEDIATE;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+              pg_get_constraintdef               
+-------------------------------------------------
+ FOREIGN KEY (ftest1) REFERENCES pktable(ptest1)
+(1 row)
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 NOT DEFERRABLE;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+              pg_get_constraintdef               
+-------------------------------------------------
+ FOREIGN KEY (ftest1) REFERENCES pktable(ptest1)
+(1 row)
+
 -- temp tables should go away by themselves, need not drop them.
 -- test check constraint adding
 create table atacc1 ( test int );
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 894084f94f..5792b032b1 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2394,3 +2394,128 @@ DROP SCHEMA fkpart8 CASCADE;
 NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to table fkpart8.tbl1
 drop cascades to table fkpart8.tbl2
+\set VERBOSITY default
+-- ALTER CONSTRAINT changing ON UPDATE/DELETE.
+-- Try all combinations and validate the diff with a created constraint
+CREATE SCHEMA createtest; -- created constraints with target action, validation
+CREATE SCHEMA altertest;  -- created with source and altered to target, test
+DO
+$test_alter_con$
+DECLARE
+    v_result json;
+    method text;
+    from_action text;
+    to_action text;
+BEGIN
+    FOR method, from_action, to_action IN
+        WITH act(action) AS (
+            SELECT unnest('{NO ACTION,RESTRICT,CASCADE,SET DEFAULT,SET NULL}'::text[])
+        )
+        SELECT
+            m.method, a1.action, a2.action
+        FROM unnest('{UPDATE,DELETE}'::text[]) AS m(method), act a1, act a2
+    LOOP
+        EXECUTE format(
+            $sql$
+                 -- Alter from ON %1$s %2$s to ON %1$s %3$s
+                CREATE TABLE createtest.foo(id integer primary key);
+                CREATE TABLE createtest.bar(foo_id integer DEFAULT 0 REFERENCES createtest.foo ON %1$s %3$s, val text);
+
+                CREATE TABLE altertest.foo(id integer primary key);
+                INSERT INTO altertest.foo VALUES(0),(1),(2),(3);
+
+                CREATE TABLE altertest.bar(foo_id integer DEFAULT 0 REFERENCES altertest.foo ON %1$s %2$s, val text);
+
+                ALTER TABLE altertest.bar ALTER CONSTRAINT bar_foo_id_fkey SET ON %1$s %3$s;
+
+            $sql$, method, from_action, to_action);
+
+        SELECT json_agg(t)
+        INTO v_result
+        FROM (
+            -- Do EXCEPT of the "altertest" and "createtest" constraints, if they are equal (as expected), it should return empty
+            SELECT
+                rel.relname, replace(tg.tgname, tg.oid::text, 'OID') AS tgname,
+                tg.tgfoid::regproc, con.conname, con.confupdtype, con.confdeltype, tg.tgdeferrable,
+                regexp_replace(pg_get_constraintdef(con.oid), '(createtest\.|altertest\.)', '') AS condef
+            FROM pg_trigger tg
+                JOIN pg_constraint con ON con.oid = tg.tgconstraint
+                JOIN pg_class rel ON tg.tgrelid = rel.oid
+            WHERE tg.tgrelid IN ('altertest.foo'::regclass, 'altertest.bar'::regclass)
+            EXCEPT
+            SELECT
+                rel.relname, replace(tg.tgname, tg.oid::text, 'OID') AS tgname,
+                tg.tgfoid::regproc, con.conname, con.confupdtype, con.confdeltype, tg.tgdeferrable,
+                regexp_replace(pg_get_constraintdef(con.oid), '(createtest\.|altertest\.)', '') AS condef
+            FROM pg_trigger tg
+                JOIN pg_constraint con ON con.oid = tg.tgconstraint
+                JOIN pg_class rel ON tg.tgrelid = rel.oid
+            WHERE tg.tgrelid IN ('createtest.foo'::regclass, 'createtest.bar'::regclass)
+        ) t;
+
+        DROP TABLE createtest.bar;
+        DROP TABLE createtest.foo;
+        DROP TABLE altertest.bar;
+        DROP TABLE altertest.foo;
+
+        IF (v_result IS NULL) THEN
+            RAISE INFO 'ON % from % to %: OK.', method, from_action, to_action;
+        ELSE
+            RAISE EXCEPTION 'ON % from % to %. FAILED! Unmatching rows: %', method, from_action, to_action, v_result;
+        END IF;
+    END LOOP;
+END;
+$test_alter_con$
+;
+INFO:  ON UPDATE from NO ACTION to NO ACTION: OK.
+INFO:  ON UPDATE from RESTRICT to NO ACTION: OK.
+INFO:  ON UPDATE from CASCADE to NO ACTION: OK.
+INFO:  ON UPDATE from SET DEFAULT to NO ACTION: OK.
+INFO:  ON UPDATE from SET NULL to NO ACTION: OK.
+INFO:  ON DELETE from NO ACTION to NO ACTION: OK.
+INFO:  ON DELETE from RESTRICT to NO ACTION: OK.
+INFO:  ON DELETE from CASCADE to NO ACTION: OK.
+INFO:  ON DELETE from SET DEFAULT to NO ACTION: OK.
+INFO:  ON DELETE from SET NULL to NO ACTION: OK.
+INFO:  ON UPDATE from NO ACTION to RESTRICT: OK.
+INFO:  ON UPDATE from RESTRICT to RESTRICT: OK.
+INFO:  ON UPDATE from CASCADE to RESTRICT: OK.
+INFO:  ON UPDATE from SET DEFAULT to RESTRICT: OK.
+INFO:  ON UPDATE from SET NULL to RESTRICT: OK.
+INFO:  ON DELETE from NO ACTION to RESTRICT: OK.
+INFO:  ON DELETE from RESTRICT to RESTRICT: OK.
+INFO:  ON DELETE from CASCADE to RESTRICT: OK.
+INFO:  ON DELETE from SET DEFAULT to RESTRICT: OK.
+INFO:  ON DELETE from SET NULL to RESTRICT: OK.
+INFO:  ON UPDATE from NO ACTION to CASCADE: OK.
+INFO:  ON UPDATE from RESTRICT to CASCADE: OK.
+INFO:  ON UPDATE from CASCADE to CASCADE: OK.
+INFO:  ON UPDATE from SET DEFAULT to CASCADE: OK.
+INFO:  ON UPDATE from SET NULL to CASCADE: OK.
+INFO:  ON DELETE from NO ACTION to CASCADE: OK.
+INFO:  ON DELETE from RESTRICT to CASCADE: OK.
+INFO:  ON DELETE from CASCADE to CASCADE: OK.
+INFO:  ON DELETE from SET DEFAULT to CASCADE: OK.
+INFO:  ON DELETE from SET NULL to CASCADE: OK.
+INFO:  ON UPDATE from NO ACTION to SET DEFAULT: OK.
+INFO:  ON UPDATE from RESTRICT to SET DEFAULT: OK.
+INFO:  ON UPDATE from CASCADE to SET DEFAULT: OK.
+INFO:  ON UPDATE from SET DEFAULT to SET DEFAULT: OK.
+INFO:  ON UPDATE from SET NULL to SET DEFAULT: OK.
+INFO:  ON DELETE from NO ACTION to SET DEFAULT: OK.
+INFO:  ON DELETE from RESTRICT to SET DEFAULT: OK.
+INFO:  ON DELETE from CASCADE to SET DEFAULT: OK.
+INFO:  ON DELETE from SET DEFAULT to SET DEFAULT: OK.
+INFO:  ON DELETE from SET NULL to SET DEFAULT: OK.
+INFO:  ON UPDATE from NO ACTION to SET NULL: OK.
+INFO:  ON UPDATE from RESTRICT to SET NULL: OK.
+INFO:  ON UPDATE from CASCADE to SET NULL: OK.
+INFO:  ON UPDATE from SET DEFAULT to SET NULL: OK.
+INFO:  ON UPDATE from SET NULL to SET NULL: OK.
+INFO:  ON DELETE from NO ACTION to SET NULL: OK.
+INFO:  ON DELETE from RESTRICT to SET NULL: OK.
+INFO:  ON DELETE from CASCADE to SET NULL: OK.
+INFO:  ON DELETE from SET DEFAULT to SET NULL: OK.
+INFO:  ON DELETE from SET NULL to SET NULL: OK.
+DROP SCHEMA createtest;
+DROP SCHEMA altertest;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 99af0b851b..5223142b8f 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -528,6 +528,16 @@ FROM pg_trigger JOIN pg_constraint con ON con.oid = tgconstraint
 WHERE tgrelid = 'fktable'::regclass
 ORDER BY 1,2,3;
 
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 INITIALLY IMMEDIATE;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+
+ALTER TABLE FKTABLE ALTER CONSTRAINT fkdi2 NOT DEFERRABLE;
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'fktable'::regclass AND conname = 'fkdi2';
+
 -- temp tables should go away by themselves, need not drop them.
 
 -- test check constraint adding
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index b67bef01df..18cfa1ab16 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1679,3 +1679,81 @@ INSERT INTO fkpart8.tbl2 VALUES(1);
 ALTER TABLE fkpart8.tbl2 DROP CONSTRAINT tbl2_f1_fkey;
 COMMIT;
 DROP SCHEMA fkpart8 CASCADE;
+\set VERBOSITY default
+
+-- ALTER CONSTRAINT changing ON UPDATE/DELETE.
+-- Try all combinations and validate the diff with a created constraint
+CREATE SCHEMA createtest; -- created constraints with target action, validation
+CREATE SCHEMA altertest;  -- created with source and altered to target, test
+
+DO
+$test_alter_con$
+DECLARE
+    v_result json;
+    method text;
+    from_action text;
+    to_action text;
+BEGIN
+    FOR method, from_action, to_action IN
+        WITH act(action) AS (
+            SELECT unnest('{NO ACTION,RESTRICT,CASCADE,SET DEFAULT,SET NULL}'::text[])
+        )
+        SELECT
+            m.method, a1.action, a2.action
+        FROM unnest('{UPDATE,DELETE}'::text[]) AS m(method), act a1, act a2
+    LOOP
+        EXECUTE format(
+            $sql$
+                 -- Alter from ON %1$s %2$s to ON %1$s %3$s
+                CREATE TABLE createtest.foo(id integer primary key);
+                CREATE TABLE createtest.bar(foo_id integer DEFAULT 0 REFERENCES createtest.foo ON %1$s %3$s, val text);
+
+                CREATE TABLE altertest.foo(id integer primary key);
+                INSERT INTO altertest.foo VALUES(0),(1),(2),(3);
+
+                CREATE TABLE altertest.bar(foo_id integer DEFAULT 0 REFERENCES altertest.foo ON %1$s %2$s, val text);
+
+                ALTER TABLE altertest.bar ALTER CONSTRAINT bar_foo_id_fkey SET ON %1$s %3$s;
+
+            $sql$, method, from_action, to_action);
+
+        SELECT json_agg(t)
+        INTO v_result
+        FROM (
+            -- Do EXCEPT of the "altertest" and "createtest" constraints, if they are equal (as expected), it should return empty
+            SELECT
+                rel.relname, replace(tg.tgname, tg.oid::text, 'OID') AS tgname,
+                tg.tgfoid::regproc, con.conname, con.confupdtype, con.confdeltype, tg.tgdeferrable,
+                regexp_replace(pg_get_constraintdef(con.oid), '(createtest\.|altertest\.)', '') AS condef
+            FROM pg_trigger tg
+                JOIN pg_constraint con ON con.oid = tg.tgconstraint
+                JOIN pg_class rel ON tg.tgrelid = rel.oid
+            WHERE tg.tgrelid IN ('altertest.foo'::regclass, 'altertest.bar'::regclass)
+            EXCEPT
+            SELECT
+                rel.relname, replace(tg.tgname, tg.oid::text, 'OID') AS tgname,
+                tg.tgfoid::regproc, con.conname, con.confupdtype, con.confdeltype, tg.tgdeferrable,
+                regexp_replace(pg_get_constraintdef(con.oid), '(createtest\.|altertest\.)', '') AS condef
+            FROM pg_trigger tg
+                JOIN pg_constraint con ON con.oid = tg.tgconstraint
+                JOIN pg_class rel ON tg.tgrelid = rel.oid
+            WHERE tg.tgrelid IN ('createtest.foo'::regclass, 'createtest.bar'::regclass)
+        ) t;
+
+        DROP TABLE createtest.bar;
+        DROP TABLE createtest.foo;
+        DROP TABLE altertest.bar;
+        DROP TABLE altertest.foo;
+
+        IF (v_result IS NULL) THEN
+            RAISE INFO 'ON % from % to %: OK.', method, from_action, to_action;
+        ELSE
+            RAISE EXCEPTION 'ON % from % to %. FAILED! Unmatching rows: %', method, from_action, to_action, v_result;
+        END IF;
+    END LOOP;
+END;
+$test_alter_con$
+;
+
+DROP SCHEMA createtest;
+DROP SCHEMA altertest;

Reply via email to