Execute foreign key constraints in CREATE SCHEMA at the end.

The previous patch simplified CREATE SCHEMA's behavior to "execute all
subcommands in the order they are written".  However, that's a bit too
simple, as the spec clearly requires forward references in foreign key
constraint clauses to work, see feature F311-01.  (Most other SQL
implementations seem to read more into the spec than that, but it's
not clear that there's justification for more in the text, and this is
the only case that doesn't introduce unresolvable issues.)  We never
implemented that before, but let's do so now.

To fix it, transform FOREIGN KEY clauses into ALTER TABLE ... ADD
FOREIGN KEY commands and append them to the end of the CREATE SCHEMA's
subcommand list.  This works because the foreign key constraints are
independent and don't affect any other DDL that might be in CREATE
SCHEMA.  For simplicity, we do this for all FOREIGN KEY clauses even
if they would have worked where they were.

Author: Jian He <jian.universality@gmail.com>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/1075425.1732993688@sss.pgh.pa.us
This commit is contained in:
Tom Lane 2026-04-06 14:52:28 -04:00
parent a9c350d9ee
commit 404db8f9ed
7 changed files with 310 additions and 23 deletions

View file

@ -135,6 +135,9 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp
<para>
The <replaceable class="parameter">schema_element</replaceable>
subcommands, if any, are executed in the order they are written.
An exception is that foreign key constraint clauses in <command>CREATE
TABLE</command> subcommands are postponed and added at the end.
This allows circular foreign key references, which are sometimes useful.
</para>
</refsect1>

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;