From f177cbfe676dc2c7ca2b206c54d6bf819feeea8b Mon Sep 17 00:00:00 2001 From: Simon Riggs Date: Sat, 29 Jun 2013 00:27:30 +0100 Subject: [PATCH] ALTER TABLE ... ALTER CONSTRAINT for FKs Allow constraint attributes to be altered, so the default setting of NOT DEFERRABLE can be altered to DEFERRABLE and back. Review by Abhijit Menon-Sen --- doc/src/sgml/ref/alter_table.sgml | 11 ++ src/backend/commands/tablecmds.c | 139 ++++++++++++++++++++++ src/backend/parser/gram.y | 15 +++ src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/foreign_key.out | 19 +++ src/test/regress/sql/foreign_key.sql | 20 ++++ 6 files changed, 205 insertions(+) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 7ee0aa8ca0..2609d4a8ea 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -46,6 +46,7 @@ ALTER TABLE [ IF EXISTS ] name ALTER [ COLUMN ] column_name SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } ADD table_constraint [ NOT VALID ] ADD table_constraint_using_index + ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] VALIDATE CONSTRAINT constraint_name DROP CONSTRAINT [ IF EXISTS ] constraint_name [ RESTRICT | CASCADE ] DISABLE TRIGGER [ trigger_name | ALL | USER ] @@ -316,6 +317,16 @@ ALTER TABLE [ IF EXISTS ] name + + ALTER CONSTRAINT + + + This form alters the attributes of a constraint that was previously + created. Currently only foreign key constraints may be altered. + + + + VALIDATE CONSTRAINT diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 50341f6ef9..ea1c309add 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -276,6 +276,8 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel, static void AlterSeqNamespaces(Relation classRel, Relation rel, Oid oldNspOid, Oid newNspOid, ObjectAddresses *objsMoved, LOCKMODE lockmode); +static void ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, + bool recurse, bool recursing, LOCKMODE lockmode); static void ATExecValidateConstraint(Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); static int transformColumnNameList(Oid relId, List *colList, @@ -2887,6 +2889,7 @@ AlterTableGetLockLevel(List *cmds) case AT_SetOptions: case AT_ResetOptions: case AT_SetStorage: + case AT_AlterConstraint: case AT_ValidateConstraint: cmd_lockmode = ShareUpdateExclusiveLock; break; @@ -3125,6 +3128,10 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, ATPrepAddInherit(rel); pass = AT_PASS_MISC; break; + case AT_AlterConstraint: /* ALTER CONSTRAINT */ + ATSimplePermissions(rel, ATT_TABLE); + pass = AT_PASS_MISC; + break; case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ ATSimplePermissions(rel, ATT_TABLE); /* Recursion occurs during execution phase */ @@ -3304,6 +3311,9 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */ ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def, lockmode); break; + case AT_AlterConstraint: /* ALTER CONSTRAINT */ + ATExecAlterConstraint(rel, cmd, false, false, lockmode); + break; case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ ATExecValidateConstraint(rel, cmd->name, false, false, lockmode); break; @@ -6175,6 +6185,135 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel, heap_close(pkrel, NoLock); } +/* + * ALTER TABLE ALTER CONSTRAINT + * + * Update the attributes of a constraint. + * + * Currently only works for Foreign Key constraints. + * Foreign keys do not inherit, so we purposely ignore the + * recursion bit here, but we keep the API the same for when + * other constraint types are supported. + */ +static void +ATExecAlterConstraint(Relation rel, AlterTableCmd *cmd, + bool recurse, bool recursing, LOCKMODE lockmode) +{ + Relation conrel; + SysScanDesc scan; + ScanKeyData key; + HeapTuple contuple; + Form_pg_constraint currcon = NULL; + Constraint *cmdcon = NULL; + bool found = false; + + Assert(IsA(cmd->def, Constraint)); + cmdcon = (Constraint *) cmd->def; + + conrel = heap_open(ConstraintRelationId, RowExclusiveLock); + + /* + * Find and check the target constraint + */ + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + scan = systable_beginscan(conrel, ConstraintRelidIndexId, + true, SnapshotNow, 1, &key); + + while (HeapTupleIsValid(contuple = systable_getnext(scan))) + { + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + if (strcmp(NameStr(currcon->conname), cmdcon->conname) == 0) + { + found = true; + break; + } + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + cmdcon->conname, RelationGetRelationName(rel)))); + + if (currcon->contype != CONSTRAINT_FOREIGN) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint", + cmdcon->conname, RelationGetRelationName(rel)))); + + if (currcon->condeferrable != cmdcon->deferrable || + currcon->condeferred != cmdcon->initdeferred) + { + HeapTuple copyTuple; + HeapTuple tgtuple; + Form_pg_constraint copy_con; + Form_pg_trigger copy_tg; + ScanKeyData tgkey; + SysScanDesc tgscan; + Relation tgrel; + + /* + * Now update the catalog, while we have the door open. + */ + copyTuple = heap_copytuple(contuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + copy_con->condeferrable = cmdcon->deferrable; + copy_con->condeferred = cmdcon->initdeferred; + simple_heap_update(conrel, ©Tuple->t_self, copyTuple); + CatalogUpdateIndexes(conrel, copyTuple); + + InvokeObjectPostAlterHook(ConstraintRelationId, + HeapTupleGetOid(contuple), 0); + + heap_freetuple(copyTuple); + + /* + * Now we need to update the multiple entries in pg_trigger + * that implement the constraint. + */ + tgrel = heap_open(TriggerRelationId, RowExclusiveLock); + + ScanKeyInit(&tgkey, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(HeapTupleGetOid(contuple))); + + tgscan = systable_beginscan(tgrel, TriggerConstraintIndexId, true, + SnapshotNow, 1, &tgkey); + + while (HeapTupleIsValid(tgtuple = systable_getnext(tgscan))) + { + copyTuple = heap_copytuple(tgtuple); + copy_tg = (Form_pg_trigger) GETSTRUCT(copyTuple); + copy_tg->tgdeferrable = cmdcon->deferrable; + copy_tg->tginitdeferred = cmdcon->initdeferred; + simple_heap_update(tgrel, ©Tuple->t_self, copyTuple); + CatalogUpdateIndexes(tgrel, copyTuple); + + InvokeObjectPostAlterHook(TriggerRelationId, + HeapTupleGetOid(tgtuple), 0); + + heap_freetuple(copyTuple); + } + + systable_endscan(tgscan); + + heap_close(tgrel, RowExclusiveLock); + + /* + * Invalidate relcache so that others see the new attributes. + */ + CacheInvalidateRelcache(rel); + } + + systable_endscan(scan); + + heap_close(conrel, RowExclusiveLock); +} + /* * ALTER TABLE VALIDATE CONSTRAINT * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c41f1b512f..0fc5b13d01 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -1943,6 +1943,21 @@ alter_table_cmd: n->def = $2; $$ = (Node *)n; } + /* ALTER TABLE ALTER CONSTRAINT ... */ + | ALTER CONSTRAINT name 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; + processCASbits($4, @4, "ALTER CONSTRAINT statement", + &c->deferrable, + &c->initdeferred, + NULL, NULL, yyscanner); + $$ = (Node *)n; + } /* ALTER TABLE VALIDATE CONSTRAINT ... */ | VALIDATE CONSTRAINT name { diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 6723647e2e..9453e1dfdf 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1209,6 +1209,7 @@ typedef enum AlterTableType AT_AddConstraint, /* add constraint */ AT_AddConstraintRecurse, /* internal to commands/tablecmds.c */ AT_ReAddConstraint, /* internal to commands/tablecmds.c */ + AT_AlterConstraint, /* alter constraint */ AT_ValidateConstraint, /* validate constraint */ AT_ValidateConstraintRecurse, /* internal to commands/tablecmds.c */ AT_ProcessedConstraint, /* pre-processed add constraint (local in diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 04668a8886..0299bfe873 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1132,6 +1132,15 @@ CREATE TEMP TABLE fktable ( id int primary key, fk int references pktable deferrable initially deferred ); +-- check ALTER CONSTRAINT +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE; +-- illegal option +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED; +ERROR: constraint declared INITIALLY DEFERRED must be DEFERRABLE +LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ... + ^ +-- reset +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED; INSERT INTO pktable VALUES (5, 10); BEGIN; -- doesn't match PK, but no error yet @@ -1142,6 +1151,16 @@ UPDATE fktable SET id = id + 1; COMMIT; ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey" DETAIL: Key (fk)=(20) is not present in table "pktable". +-- change the constraint definition and retest +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY IMMEDIATE; +BEGIN; +-- doesn't match PK, should throw error now +INSERT INTO fktable VALUES (0, 20); +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey" +DETAIL: Key (fk)=(20) is not present in table "pktable". +COMMIT; +-- reset +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED; -- check same case when insert is in a different subtransaction than update BEGIN; -- doesn't match PK, but no error yet diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 377b36c226..531c881f63 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -818,6 +818,13 @@ CREATE TEMP TABLE fktable ( fk int references pktable deferrable initially deferred ); +-- check ALTER CONSTRAINT +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE; +-- illegal option +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY DEFERRED; +-- reset +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED; + INSERT INTO pktable VALUES (5, 10); BEGIN; @@ -831,6 +838,19 @@ UPDATE fktable SET id = id + 1; -- should catch error from initial INSERT COMMIT; +-- change the constraint definition and retest +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY IMMEDIATE; + +BEGIN; + +-- doesn't match PK, should throw error now +INSERT INTO fktable VALUES (0, 20); + +COMMIT; + +-- reset +ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED; + -- check same case when insert is in a different subtransaction than update BEGIN; -- GitLab