diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index 9e6f0e24339..96bc496e777 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -135,6 +135,9 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION role_sp The schema_element subcommands, if any, are executed in the order they are written. + An exception is that foreign key constraint clauses in CREATE + TABLE subcommands are postponed and added at the end. + This allows circular foreign key references, which are sometimes useful. diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 151d9731273..b0c3407b23d 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -122,11 +122,14 @@ static void transformFKConstraints(CreateStmtContext *cxt, bool isAddConstraint); static void transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation); -static void transformConstraintAttrs(CreateStmtContext *cxt, +static void transformConstraintAttrs(ParseState *pstate, List *constraintList); static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column); static void checkSchemaNameRV(ParseState *pstate, const char *context_schema, RangeVar *relation); +static CreateStmt *transformCreateSchemaCreateTable(ParseState *pstate, + CreateStmt *stmt, + List **fk_elements); static void transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound); static List *transformPartitionRangeBounds(ParseState *pstate, List *blist, Relation parent); @@ -693,7 +696,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) } /* Process column constraints, if any... */ - transformConstraintAttrs(cxt, column->constraints); + transformConstraintAttrs(cxt->pstate, column->constraints); /* * First, scan the column's constraints to see if a not-null constraint @@ -4194,9 +4197,12 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, * NOTE: currently, attributes are only supported for FOREIGN KEY, UNIQUE, * EXCLUSION, and PRIMARY KEY constraints, but someday they ought to be * supported for other constraint types. + * + * NOTE: this must be idempotent in non-error cases; see + * transformCreateSchemaCreateTable. */ static void -transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) +transformConstraintAttrs(ParseState *pstate, List *constraintList) { Constraint *lastprimarycon = NULL; bool saw_deferrability = false; @@ -4225,12 +4231,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced DEFERRABLE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_deferrability) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple DEFERRABLE/NOT DEFERRABLE clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_deferrability = true; lastprimarycon->deferrable = true; break; @@ -4240,12 +4246,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced NOT DEFERRABLE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_deferrability) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple DEFERRABLE/NOT DEFERRABLE clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_deferrability = true; lastprimarycon->deferrable = false; if (saw_initially && @@ -4253,7 +4259,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("constraint declared INITIALLY DEFERRED must be DEFERRABLE"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); break; case CONSTR_ATTR_DEFERRED: @@ -4261,12 +4267,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced INITIALLY DEFERRED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_initially) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple INITIALLY IMMEDIATE/DEFERRED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_initially = true; lastprimarycon->initdeferred = true; @@ -4279,7 +4285,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("constraint declared INITIALLY DEFERRED must be DEFERRABLE"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); break; case CONSTR_ATTR_IMMEDIATE: @@ -4287,12 +4293,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced INITIALLY IMMEDIATE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_initially) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple INITIALLY IMMEDIATE/DEFERRED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_initially = true; lastprimarycon->initdeferred = false; break; @@ -4304,12 +4310,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced ENFORCED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_enforced) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_enforced = true; lastprimarycon->is_enforced = true; break; @@ -4321,12 +4327,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced NOT ENFORCED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_enforced) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_enforced = true; lastprimarycon->is_enforced = false; @@ -4384,12 +4390,17 @@ transformColumnType(CreateStmtContext *cxt, ColumnDef *column) * transformCreateSchemaStmtElements - * analyzes the elements of a CREATE SCHEMA statement * - * This is now somewhat vestigial: its only real responsibility is to complain - * if any of the elements are trying to create objects outside the new schema. + * This presently has two responsibilities. We verify that no subcommands are + * trying to create objects outside the new schema. We also pull out any + * foreign-key constraint clauses embedded in CREATE TABLE subcommands, and + * convert them to ALTER TABLE ADD CONSTRAINT commands appended to the list. + * This supports forward references in foreign keys, which is required by the + * SQL standard. + * * We used to try to re-order the commands in a way that would work even if * the user-written order would not, but that's too hard (perhaps impossible) * to do correctly with not-yet-parse-analyzed commands. Now we'll just - * execute the elements in the order given. + * execute the elements in the order given, except for foreign keys. * * "schemaName" is the name of the schema that will be used for the creation * of the objects listed. It may be obtained from the schema name defined @@ -4398,12 +4409,17 @@ transformColumnType(CreateStmtContext *cxt, ColumnDef *column) * The result is a list of parse nodes that still need to be analyzed --- * but we can't analyze the later commands until we've executed the earlier * ones, because of possible inter-object references. + * + * Note it's important that we not modify the input data structure. We create + * a new result List, and we copy any CREATE TABLE subcommands that we might + * modify. */ List * transformCreateSchemaStmtElements(ParseState *pstate, List *schemaElts, const char *schemaName) { List *elements = NIL; + List *fk_elements = NIL; ListCell *lc; /* @@ -4430,7 +4446,11 @@ transformCreateSchemaStmtElements(ParseState *pstate, List *schemaElts, CreateStmt *elp = (CreateStmt *) element; checkSchemaNameRV(pstate, schemaName, elp->relation); - elements = lappend(elements, element); + /* Pull out any foreign key clauses, add to fk_elements */ + elp = transformCreateSchemaCreateTable(pstate, + elp, + &fk_elements); + elements = lappend(elements, elp); } break; @@ -4471,7 +4491,7 @@ transformCreateSchemaStmtElements(ParseState *pstate, List *schemaElts, } } - return elements; + return list_concat(elements, fk_elements); } /* @@ -4508,6 +4528,161 @@ checkSchemaNameRV(ParseState *pstate, const char *context_schema, } } +/* + * transformCreateSchemaCreateTable + * Process one CreateStmt for transformCreateSchemaStmtElements. + * + * We remove any foreign-key clauses in the statement and convert them into + * ALTER TABLE commands, which we append to *fk_elements. + */ +static CreateStmt * +transformCreateSchemaCreateTable(ParseState *pstate, + CreateStmt *stmt, + List **fk_elements) +{ + CreateStmt *newstmt; + List *newElts = NIL; + ListCell *lc; + + /* + * Flat-copy the CreateStmt node, allowing us to replace its tableElts + * list without damaging the input data structure. Most sub-nodes will be + * shared with the input, though. + */ + newstmt = makeNode(CreateStmt); + memcpy(newstmt, stmt, sizeof(CreateStmt)); + + /* Scan for foreign-key constraints */ + foreach(lc, stmt->tableElts) + { + Node *element = lfirst(lc); + AlterTableStmt *alterstmt; + AlterTableCmd *altercmd; + + if (IsA(element, Constraint)) + { + Constraint *constr = (Constraint *) element; + + if (constr->contype != CONSTR_FOREIGN) + { + /* Other constraint types pass through unchanged */ + newElts = lappend(newElts, constr); + continue; + } + + /* Make it into an ALTER TABLE ADD CONSTRAINT command */ + altercmd = makeNode(AlterTableCmd); + altercmd->subtype = AT_AddConstraint; + altercmd->name = NULL; + altercmd->def = (Node *) copyObject(constr); + + alterstmt = makeNode(AlterTableStmt); + alterstmt->relation = copyObject(stmt->relation); + alterstmt->cmds = list_make1(altercmd); + alterstmt->objtype = OBJECT_TABLE; + + *fk_elements = lappend(*fk_elements, alterstmt); + } + else if (IsA(element, ColumnDef)) + { + ColumnDef *entry = (ColumnDef *) element; + ColumnDef *newentry; + List *entryconstraints; + bool afterFK = false; + + /* + * We must preprocess the list of column constraints to attach + * attributes such as DEFERRED to the appropriate constraint node. + * Do this on a copy. (But execution of the CreateStmt will run + * transformConstraintAttrs on the copy, so we are nonetheless + * relying on transformConstraintAttrs to be idempotent.) + */ + entryconstraints = copyObject(entry->constraints); + transformConstraintAttrs(pstate, entryconstraints); + + /* Scan the column constraints ... */ + foreach_node(Constraint, colconstr, entryconstraints) + { + switch (colconstr->contype) + { + case CONSTR_FOREIGN: + /* colconstr is already a copy, OK to modify */ + colconstr->fk_attrs = list_make1(makeString(entry->colname)); + + /* Make it into an ALTER TABLE ADD CONSTRAINT command */ + altercmd = makeNode(AlterTableCmd); + altercmd->subtype = AT_AddConstraint; + altercmd->name = NULL; + altercmd->def = (Node *) colconstr; + + alterstmt = makeNode(AlterTableStmt); + alterstmt->relation = copyObject(stmt->relation); + alterstmt->cmds = list_make1(altercmd); + alterstmt->objtype = OBJECT_TABLE; + + *fk_elements = lappend(*fk_elements, alterstmt); + + /* Remove the Constraint node from entryconstraints */ + entryconstraints = + foreach_delete_current(entryconstraints, colconstr); + + /* + * Immediately-following attribute constraints should + * be dropped, too. + */ + afterFK = true; + break; + + /* + * Column constraint lists separate a Constraint node + * from its attributes (e.g. NOT ENFORCED); so a + * column-level foreign key constraint may be + * represented by multiple Constraint nodes. After + * transformConstraintAttrs, the foreign key + * Constraint node contains all required information, + * making it okay to put into *fk_elements as a + * stand-alone Constraint. But since we removed the + * foreign key Constraint node from entryconstraints, + * we must remove any dependent attribute nodes too, + * else the later re-execution of + * transformConstraintAttrs will misbehave. + */ + case CONSTR_ATTR_DEFERRABLE: + case CONSTR_ATTR_NOT_DEFERRABLE: + case CONSTR_ATTR_DEFERRED: + case CONSTR_ATTR_IMMEDIATE: + case CONSTR_ATTR_ENFORCED: + case CONSTR_ATTR_NOT_ENFORCED: + if (afterFK) + entryconstraints = + foreach_delete_current(entryconstraints, + colconstr); + break; + + default: + /* Any following constraint attributes are unrelated */ + afterFK = false; + break; + } + } + + /* Now make a modified ColumnDef to put into newElts */ + newentry = makeNode(ColumnDef); + memcpy(newentry, entry, sizeof(ColumnDef)); + newentry->constraints = entryconstraints; + newElts = lappend(newElts, newentry); + } + else + { + /* Other node types pass through unchanged */ + newElts = lappend(newElts, element); + } + } + + newstmt->tableElts = newElts; + return newstmt; +} + /* * transformPartitionCmd * Analyze the ATTACH/DETACH/SPLIT PARTITION command diff --git a/src/test/modules/test_ddl_deparse/expected/create_schema.out b/src/test/modules/test_ddl_deparse/expected/create_schema.out index 8ab4eb03385..6ed85ef7446 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_schema.out +++ b/src/test/modules/test_ddl_deparse/expected/create_schema.out @@ -17,3 +17,28 @@ CREATE SCHEMA element_test NOTICE: DDL test: type simple, tag CREATE SCHEMA NOTICE: DDL test: type simple, tag CREATE TABLE NOTICE: DDL test: type simple, tag CREATE VIEW +CREATE SCHEMA regress_schema_1 +CREATE TABLE t4( + b INT, + a INT REFERENCES t5 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE) +CREATE TABLE t5 (a INT, b INT, PRIMARY KEY (a)) +CREATE TABLE t6 (a INT, b INT, PRIMARY KEY (a)); +NOTICE: DDL test: type simple, tag CREATE SCHEMA +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint t4_a_fkey on table regress_schema_1.t4 +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint t4_a_fkey1 on table regress_schema_1.t4 +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint fk on table regress_schema_1.t4 +DROP SCHEMA regress_schema_1 CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table regress_schema_1.t4 +drop cascades to table regress_schema_1.t5 +drop cascades to table regress_schema_1.t6 diff --git a/src/test/modules/test_ddl_deparse/sql/create_schema.sql b/src/test/modules/test_ddl_deparse/sql/create_schema.sql index f314dc2b840..145aef2a75a 100644 --- a/src/test/modules/test_ddl_deparse/sql/create_schema.sql +++ b/src/test/modules/test_ddl_deparse/sql/create_schema.sql @@ -15,3 +15,14 @@ CREATE SCHEMA IF NOT EXISTS baz; CREATE SCHEMA element_test CREATE TABLE foo (id int) CREATE VIEW bar AS SELECT * FROM foo; + +CREATE SCHEMA regress_schema_1 +CREATE TABLE t4( + b INT, + a INT REFERENCES t5 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE) +CREATE TABLE t5 (a INT, b INT, PRIMARY KEY (a)) +CREATE TABLE t6 (a INT, b INT, PRIMARY KEY (a)); + +DROP SCHEMA regress_schema_1 CASCADE; diff --git a/src/test/regress/expected/create_schema.out b/src/test/regress/expected/create_schema.out index 4ab947a60a8..b34b9988962 100644 --- a/src/test/regress/expected/create_schema.out +++ b/src/test/regress/expected/create_schema.out @@ -131,5 +131,51 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE DROP SCHEMA regress_schema_1 CASCADE; NOTICE: drop cascades to table regress_schema_1.tab RESET ROLE; +-- Test forward-referencing foreign key clauses. +CREATE SCHEMA regress_schema_fk + CREATE TABLE regress_schema_fk.t2 ( + b int, + a int REFERENCES t1 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t3 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t1 NOT DEFERRABLE) + CREATE TABLE regress_schema_fk.t1 (a int PRIMARY KEY) + CREATE TABLE t3 (a int PRIMARY KEY) + CREATE TABLE t4 ( + b int, + a int REFERENCES t5 NOT DEFERRABLE ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE INITIALLY DEFERRED) + CREATE TABLE t5 (a int, b int, PRIMARY KEY (a)) + CREATE TABLE t6 (a int, b int, PRIMARY KEY (a)); +\d regress_schema_fk.t2 + Table "regress_schema_fk.t2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Foreign-key constraints: + "fk" FOREIGN KEY (a) REFERENCES regress_schema_fk.t1(a) + "t2_a_fkey" FOREIGN KEY (a) REFERENCES regress_schema_fk.t1(a) DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + "t2_a_fkey1" FOREIGN KEY (a) REFERENCES regress_schema_fk.t3(a) DEFERRABLE INITIALLY DEFERRED + +\d regress_schema_fk.t4 + Table "regress_schema_fk.t4" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Foreign-key constraints: + "fk" FOREIGN KEY (a) REFERENCES regress_schema_fk.t6(a) DEFERRABLE INITIALLY DEFERRED + "t4_a_fkey" FOREIGN KEY (a) REFERENCES regress_schema_fk.t5(a) + "t4_a_fkey1" FOREIGN KEY (a) REFERENCES regress_schema_fk.t6(a) DEFERRABLE + +DROP SCHEMA regress_schema_fk CASCADE; +NOTICE: drop cascades to 6 other objects +DETAIL: drop cascades to table regress_schema_fk.t2 +drop cascades to table regress_schema_fk.t1 +drop cascades to table regress_schema_fk.t3 +drop cascades to table regress_schema_fk.t4 +drop cascades to table regress_schema_fk.t5 +drop cascades to table regress_schema_fk.t6 -- Clean up DROP ROLE regress_create_schema_role; diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index 4c32e1dcaf5..065f586310f 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -427,11 +427,11 @@ NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_ NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_c_seq NOTICE: END: command_tag=CREATE INDEX type=index identity=evttrig.one_idx NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.two -NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.two NOTICE: END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.id_col_d_seq NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.id NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.id_col_d_seq NOTICE: END: command_tag=CREATE VIEW type=view identity=evttrig.one_view +NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.two -- View with column additions CREATE OR REPLACE VIEW evttrig.one_view AS SELECT * FROM evttrig.two, evttrig.id; NOTICE: END: command_tag=CREATE VIEW type=view identity=evttrig.one_view diff --git a/src/test/regress/sql/create_schema.sql b/src/test/regress/sql/create_schema.sql index 62651342114..0f2accc59ec 100644 --- a/src/test/regress/sql/create_schema.sql +++ b/src/test/regress/sql/create_schema.sql @@ -71,5 +71,32 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE DROP SCHEMA regress_schema_1 CASCADE; RESET ROLE; +-- Test forward-referencing foreign key clauses. +CREATE SCHEMA regress_schema_fk + CREATE TABLE regress_schema_fk.t2 ( + b int, + a int REFERENCES t1 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t3 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t1 NOT DEFERRABLE) + + CREATE TABLE regress_schema_fk.t1 (a int PRIMARY KEY) + + CREATE TABLE t3 (a int PRIMARY KEY) + + CREATE TABLE t4 ( + b int, + a int REFERENCES t5 NOT DEFERRABLE ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE INITIALLY DEFERRED) + + CREATE TABLE t5 (a int, b int, PRIMARY KEY (a)) + + CREATE TABLE t6 (a int, b int, PRIMARY KEY (a)); + +\d regress_schema_fk.t2 +\d regress_schema_fk.t4 + +DROP SCHEMA regress_schema_fk CASCADE; + -- Clean up DROP ROLE regress_create_schema_role;