On Tue, Oct 2, 2018 at 3:40 AM Michael Paquier <mich...@paquier.xyz> wrote:
> > The last patch set does not apply, so this is moved to next CF, waiting > on author as new status. > Updated the last patch so it can apply cleanly on HEAD. About the bugfixes, do you think it is better to move to another thread? Best regards, -- Matheus de Oliveira
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index f13a6cd944..5910680cf3 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 3e112b4ef4..86dabc9bbc 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -7843,8 +7843,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; @@ -7862,6 +7897,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, ©Tuple->t_self, copyTuple); InvokeObjectPostAlterHook(ConstraintRelationId, @@ -7898,23 +7935,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 createForeignKeyTriggers 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, ©Tuple->t_self, copyTuple); InvokeObjectPostAlterHook(TriggerRelationId, diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index e8ea59e34a..fa8573dbfa 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2898,6 +2898,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/outfuncs.c b/src/backend/nodes/outfuncs.c index 69731ccdea..1761b81073 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -3526,6 +3526,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 6d23bfb0b3..184f1c27eb 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -184,7 +184,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); @@ -537,7 +538,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 @@ -2269,7 +2271,7 @@ alter_table_cmd: $$ = (Node *)n; } /* ALTER TABLE <name> ALTER CONSTRAINT ... */ - | ALTER CONSTRAINT name ConstraintAttributeSpec + | ALTER CONSTRAINT name opt_key_actions ConstraintAttributeSpec { AlterTableCmd *n = makeNode(AlterTableCmd); Constraint *c = makeNode(Constraint); @@ -2277,9 +2279,11 @@ 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); + processCASbits($5, @4, "ALTER CONSTRAINT statement", + &c->deferrable, &c->was_deferrable_set, + &c->initdeferred, &c->was_initdeferred_set, NULL, NULL, yyscanner); $$ = (Node *)n; } @@ -3674,7 +3678,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; @@ -3691,8 +3695,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 @@ -3706,8 +3710,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 @@ -3722,8 +3726,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 @@ -3737,8 +3741,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 ')' @@ -3756,8 +3760,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 @@ -3773,7 +3777,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; @@ -3853,7 +3858,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 @@ -3868,6 +3873,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; } ; @@ -5380,8 +5402,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; } @@ -16195,7 +16217,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 */ @@ -16206,6 +16229,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 aa4a0dba2a..b541ed3035 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2093,6 +2093,7 @@ 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' @@ -2110,6 +2111,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 68cd3e5676..3948140d46 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -714,6 +714,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) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED +(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) ON DELETE CASCADE DEFERRABLE +(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) ON DELETE CASCADE +(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 52164e89d2..dd709462a5 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1781,3 +1781,127 @@ INSERT INTO fk_notpartitioned_pk VALUES (1600, 601), (1600, 1601); ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1600); -- leave these tables around intentionally +-- 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 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 6890346637..af898b49fc 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -514,6 +514,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 f387004855..d92ac23737 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -1289,3 +1289,80 @@ ALTER TABLE fk_partitioned_fk ATTACH PARTITION fk_partitioned_fk_2 FOR VALUES IN (1600); -- leave these tables around intentionally + +-- 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 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;