From 96226a6d87e32637e960394aa720e4f5358b57b5 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Tue, 10 Feb 2026 18:05:33 -0300 Subject: [PATCH v3 3/4] Add foreign key support to CREATE SCHEMA ... LIKE This commit extends CREATE SCHEMA ... LIKE to copy foreign key constraints between tables: - FK constraints referencing tables within the same source schema are recreated in the new schema, pointing to the corresponding copied tables. - FK constraints referencing tables in external schemas are skipped with a WARNING, since the referenced table is not being copied. - All FK properties are preserved: ON UPDATE/DELETE actions (CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION), match type (FULL, PARTIAL, SIMPLE), deferrable/deferred settings, and ON DELETE SET NULL/DEFAULT column lists. FK constraints are applied after all tables are created using ALTER TABLE ... ADD CONSTRAINT, since both the referencing and referenced tables must exist. --- src/backend/commands/schemacmds.c | 176 ++++++++++++++++++++ src/test/regress/expected/create_schema.out | 148 +++++++++++++++- src/test/regress/sql/create_schema.sql | 100 ++++++++++- 3 files changed, 422 insertions(+), 2 deletions(-) diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c index d28e3429266..d99419c317f 100644 --- a/src/backend/commands/schemacmds.c +++ b/src/backend/commands/schemacmds.c @@ -27,6 +27,7 @@ #include "catalog/pg_partitioned_table.h" #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" +#include "catalog/pg_constraint.h" #include "catalog/pg_database.h" #include "catalog/pg_namespace.h" #include "commands/event_trigger.h" @@ -48,6 +49,7 @@ static void AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId); static List *collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, bits32 options); +static List *collectSchemaForeignKeysLike(Oid srcNspOid, const char *newSchemaName); static PartitionSpec *buildPartitionSpecForRelation(Oid relid); static PartitionBoundSpec *getPartitionBoundSpec(Oid partOid); @@ -419,6 +421,157 @@ collectSchemaTablesLike(Oid srcNspOid, const char *newSchemaName, return result; } +/* + * Collect foreign key constraints from source schema tables. + * + * Returns a list of AlterTableStmt nodes that add FK constraints to tables + * in the new schema. Only FKs that reference tables within the same source + * schema are included; FKs referencing external schemas are skipped with + * a WARNING. + * + * This must be called after tables are created, as FKs require both the + * referencing and referenced tables to exist. + */ +static List * +collectSchemaForeignKeysLike(Oid srcNspOid, const char *newSchemaName) +{ + List *result = NIL; + Relation pg_constraint; + SysScanDesc scan; + HeapTuple tuple; + char *srcSchemaName; + ScanKeyData key; + + srcSchemaName = get_namespace_name(srcNspOid); + + ScanKeyInit(&key, + Anum_pg_constraint_connamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(srcNspOid)); + + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + + /* Scan constraints filtering for FKs on source schema tables */ + scan = systable_beginscan(pg_constraint, InvalidOid, false, + NULL, 1, &key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(tuple); + Oid relid = con->conrelid; + Oid confrelid = con->confrelid; + Oid refNspOid; + AlterTableStmt *alterStmt; + AlterTableCmd *cmd; + Constraint *fkcon; + RangeVar *rel; + RangeVar *pktable; + int numkeys; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + char *relname; + char *refrelname; + AttrNumber fkdelsetcols[INDEX_MAX_KEYS]; + int numfkdelsetcols; + + + /* Only process foreign key constraints */ + if (con->contype != CONSTRAINT_FOREIGN) + continue; + + /* Check if referenced table is in source schema */ + refNspOid = get_rel_namespace(confrelid); + if (refNspOid != srcNspOid) + { + ereport(WARNING, + (errmsg("skipping foreign key \"%s\" on table \"%s.%s\" because it references table \"%s.%s\" in a different schema", + NameStr(con->conname), + srcSchemaName, + get_rel_name(relid), + get_namespace_name(refNspOid), + get_rel_name(confrelid)))); + continue; + } + + /* Extract FK constraint details */ + DeconstructFkConstraintRow(tuple, &numkeys, conkey, confkey, + NULL, NULL, NULL, + &numfkdelsetcols, fkdelsetcols); + + /* Build the Constraint node for the FK */ + fkcon = makeNode(Constraint); + fkcon->contype = CONSTR_FOREIGN; + fkcon->conname = pstrdup(NameStr(con->conname)); + fkcon->deferrable = con->condeferrable; + fkcon->initdeferred = con->condeferred; + fkcon->is_enforced = con->conenforced; + fkcon->skip_validation = false; + fkcon->initially_valid = con->convalidated; + fkcon->location = -1; + + /* Build FK column list */ + relname = get_rel_name(relid); + fkcon->fk_attrs = NIL; + for (int i = 0; i < numkeys; i++) + { + char *attname = get_attname(relid, conkey[i], false); + + fkcon->fk_attrs = lappend(fkcon->fk_attrs, makeString(attname)); + } + + /* Build PK column list and reference table */ + refrelname = get_rel_name(confrelid); + pktable = makeRangeVar(pstrdup(newSchemaName), + pstrdup(refrelname), + -1); + fkcon->pktable = pktable; + fkcon->pk_attrs = NIL; + for (int i = 0; i < numkeys; i++) + { + char *attname = get_attname(confrelid, confkey[i], false); + + fkcon->pk_attrs = lappend(fkcon->pk_attrs, makeString(attname)); + } + + /* Set FK actions */ + fkcon->fk_matchtype = con->confmatchtype; + fkcon->fk_upd_action = con->confupdtype; + fkcon->fk_del_action = con->confdeltype; + fkcon->fk_del_set_cols = NIL; + /* Handle ON DELETE SET NULL/DEFAULT (col1, col2, ...) */ + for (int i = 0; i < numfkdelsetcols; i++) + { + char *attname = get_attname(relid, fkdelsetcols[i], false); + + fkcon->fk_del_set_cols = lappend(fkcon->fk_del_set_cols, + makeString(attname)); + } + + /* Build ALTER TABLE ADD CONSTRAINT statement */ + alterStmt = makeNode(AlterTableStmt); + cmd = makeNode(AlterTableCmd); + + rel = makeRangeVar(pstrdup(newSchemaName), + pstrdup(relname), + -1); + + cmd->subtype = AT_AddConstraint; + cmd->def = (Node *) fkcon; + + alterStmt->relation = rel; + alterStmt->cmds = list_make1(cmd); + alterStmt->objtype = OBJECT_TABLE; + alterStmt->missing_ok = false; + + result = lappend(result, alterStmt); + } + + systable_endscan(scan); + table_close(pg_constraint, AccessShareLock); + + return result; +} + /* * CREATE SCHEMA * @@ -444,6 +597,7 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, AclResult aclresult; ObjectAddress address; StringInfoData pathbuf; + List *fk_stmts = NIL; GetUserIdAndSecContext(&saved_uid, &save_sec_context); @@ -570,6 +724,10 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, /* * Process LIKE clause if present. We collect objects from the source * schema and append them to the schema elements list. + * + * Note: FK constraints are collected separately and executed after all + * tables are created, since they require both referencing and referenced + * tables to exist. */ if (stmt->like_clause != NULL) { @@ -596,6 +754,15 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, like->options); } + /* + * If INCLUDING TABLE INCLUDING INDEX or INCLUDING ALL is used also + * collect FK's references to create on new schema. + */ + if (like->options & CREATE_SCHEMA_LIKE_ALL || + (like->options & CREATE_SCHEMA_LIKE_TABLE + && like->options & CREATE_TABLE_LIKE_INDEXES)) + fk_stmts = collectSchemaForeignKeysLike(srcNspOid, schemaName); + /* Append LIKE-generated statements to explicit schema elements */ stmt->schemaElts = list_concat(like_stmts, stmt->schemaElts); } @@ -610,6 +777,15 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, parsetree_list = transformCreateSchemaStmtElements(stmt->schemaElts, schemaName); + + /* + * Append foreign key constraints from LIKE clause. This must be done + * after all tables are created, since FKs require both the referencing + * and referenced tables to exist. + */ + if (fk_stmts != NIL) + parsetree_list = list_concat(parsetree_list, fk_stmts); + /* * Execute each command contained in the CREATE SCHEMA. Since the grammar * allows only utility commands in CREATE SCHEMA, there is no need to pass diff --git a/src/test/regress/expected/create_schema.out b/src/test/regress/expected/create_schema.out index 0ecbea18fda..bfde6dffbe2 100644 --- a/src/test/regress/expected/create_schema.out +++ b/src/test/regress/expected/create_schema.out @@ -105,7 +105,7 @@ CREATE TABLE regress_source_schema.t1 ( created_at timestamp DEFAULT now() ); CREATE TABLE regress_source_schema.t2 ( - id int REFERENCES regress_source_schema.t1(id), + id int, data jsonb ); CREATE INDEX idx_t1_name ON regress_source_schema.t1(name); @@ -304,6 +304,152 @@ Partition key: HASH (id) Partitions: regress_hash_part_copy.events_0 FOR VALUES WITH (modulus 2, remainder 0), regress_hash_part_copy.events_1 FOR VALUES WITH (modulus 2, remainder 1) +-- +-- Test foreign key handling +-- +-- Create a schema with tables that have FK relationships within the schema +CREATE SCHEMA regress_fk_source; +CREATE TABLE regress_fk_source.parent ( + id int PRIMARY KEY, + name text +); +CREATE TABLE regress_fk_source.child ( + id int PRIMARY KEY, + parent_id int REFERENCES regress_fk_source.parent(id) ON DELETE CASCADE, + data text +); +-- Add another FK relationship +CREATE TABLE regress_fk_source.grandchild ( + id int PRIMARY KEY, + child_id int REFERENCES regress_fk_source.child(id) ON UPDATE CASCADE ON DELETE SET NULL, + info text +); +-- Test ON DELETE SET NULL with column list (fk_del_set_cols) +CREATE TABLE regress_fk_source.multi_col_parent ( + id int, + sub_id int, + PRIMARY KEY (id, sub_id) +); +CREATE TABLE regress_fk_source.multi_col_child ( + id int PRIMARY KEY, + parent_id int, + parent_sub_id int, + extra_data text, + FOREIGN KEY (parent_id, parent_sub_id) + REFERENCES regress_fk_source.multi_col_parent(id, sub_id) + ON DELETE SET NULL (parent_id) -- only parent_id set to NULL, not parent_sub_id +); +-- Copy the schema - FKs should be recreated pointing to new schema tables +CREATE SCHEMA regress_fk_copy LIKE regress_fk_source INCLUDING ALL; +-- Verify the FK constraints were copied and actions were preserved +\d regress_fk_copy.child + Table "regress_fk_copy.child" + Column | Type | Collation | Nullable | Default +-----------+---------+-----------+----------+--------- + id | integer | | not null | + parent_id | integer | | | + data | text | | | +Indexes: + "child_pkey" PRIMARY KEY, btree (id) +Foreign-key constraints: + "child_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES regress_fk_copy.parent(id) ON DELETE CASCADE +Referenced by: + TABLE "regress_fk_copy.grandchild" CONSTRAINT "grandchild_child_id_fkey" FOREIGN KEY (child_id) REFERENCES regress_fk_copy.child(id) ON UPDATE CASCADE ON DELETE SET NULL + +\d regress_fk_copy.grandchild + Table "regress_fk_copy.grandchild" + Column | Type | Collation | Nullable | Default +----------+---------+-----------+----------+--------- + id | integer | | not null | + child_id | integer | | | + info | text | | | +Indexes: + "grandchild_pkey" PRIMARY KEY, btree (id) +Foreign-key constraints: + "grandchild_child_id_fkey" FOREIGN KEY (child_id) REFERENCES regress_fk_copy.child(id) ON UPDATE CASCADE ON DELETE SET NULL + +-- Verify ON DELETE SET NULL (column_list) was preserved +\d regress_fk_copy.multi_col_child + Table "regress_fk_copy.multi_col_child" + Column | Type | Collation | Nullable | Default +---------------+---------+-----------+----------+--------- + id | integer | | not null | + parent_id | integer | | | + parent_sub_id | integer | | | + extra_data | text | | | +Indexes: + "multi_col_child_pkey" PRIMARY KEY, btree (id) +Foreign-key constraints: + "multi_col_child_parent_id_parent_sub_id_fkey" FOREIGN KEY (parent_id, parent_sub_id) REFERENCES regress_fk_copy.multi_col_parent(id, sub_id) ON DELETE SET NULL (parent_id) + +-- Test FK to external schema (should be skipped with WARNING) +CREATE SCHEMA regress_fk_external; +CREATE TABLE regress_fk_external.external_ref ( + id int PRIMARY KEY +); +CREATE SCHEMA regress_fk_mixed; +CREATE TABLE regress_fk_mixed.internal_parent ( + id int PRIMARY KEY +); +-- Table with FK to table in same schema (should be copied) +CREATE TABLE regress_fk_mixed.internal_child ( + id int PRIMARY KEY, + parent_id int REFERENCES regress_fk_mixed.internal_parent(id) +); +-- Table with FK to external schema (FK should be skipped with WARNING) +CREATE TABLE regress_fk_mixed.external_child ( + id int PRIMARY KEY, + ref_id int REFERENCES regress_fk_external.external_ref(id) +); +-- Copy should warn about external FK but copy internal FK +CREATE SCHEMA regress_fk_mixed_copy LIKE regress_fk_mixed INCLUDING ALL; +WARNING: skipping foreign key "external_child_ref_id_fkey" on table "regress_fk_mixed.external_child" because it references table "regress_fk_external.external_ref" in a different schema +-- Verify only internal FK was copied (external FK should be skipped) +\d regress_fk_mixed_copy.internal_child + Table "regress_fk_mixed_copy.internal_child" + Column | Type | Collation | Nullable | Default +-----------+---------+-----------+----------+--------- + id | integer | | not null | + parent_id | integer | | | +Indexes: + "internal_child_pkey" PRIMARY KEY, btree (id) +Foreign-key constraints: + "internal_child_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES regress_fk_mixed_copy.internal_parent(id) + +\d egress_fk_mixed_copy.external_child -- should be empty +-- Test default EXCLUDING with FK and ensure that ALTER TABLE ADD CONSTRAINT is +-- not executed. +CREATE SCHEMA regress_copy_empty LIKE regress_fk_source; +\d+ regress_copy_empty +-- Clean up FK tests +DROP SCHEMA regress_fk_source CASCADE; +NOTICE: drop cascades to 5 other objects +DETAIL: drop cascades to table regress_fk_source.parent +drop cascades to table regress_fk_source.child +drop cascades to table regress_fk_source.grandchild +drop cascades to table regress_fk_source.multi_col_parent +drop cascades to table regress_fk_source.multi_col_child +DROP SCHEMA regress_fk_copy CASCADE; +NOTICE: drop cascades to 5 other objects +DETAIL: drop cascades to table regress_fk_copy.child +drop cascades to table regress_fk_copy.grandchild +drop cascades to table regress_fk_copy.multi_col_child +drop cascades to table regress_fk_copy.multi_col_parent +drop cascades to table regress_fk_copy.parent +DROP SCHEMA regress_fk_external CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table regress_fk_external.external_ref +drop cascades to constraint external_child_ref_id_fkey on table regress_fk_mixed.external_child +DROP SCHEMA regress_fk_mixed CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table regress_fk_mixed.internal_parent +drop cascades to table regress_fk_mixed.internal_child +drop cascades to table regress_fk_mixed.external_child +DROP SCHEMA regress_fk_mixed_copy CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table regress_fk_mixed_copy.external_child +drop cascades to table regress_fk_mixed_copy.internal_child +drop cascades to table regress_fk_mixed_copy.internal_parent -- Clean up partition tests DROP SCHEMA regress_part_source CASCADE; NOTICE: drop cascades to 5 other objects diff --git a/src/test/regress/sql/create_schema.sql b/src/test/regress/sql/create_schema.sql index 8102149e5ea..f6f9ef2f719 100644 --- a/src/test/regress/sql/create_schema.sql +++ b/src/test/regress/sql/create_schema.sql @@ -80,7 +80,7 @@ CREATE TABLE regress_source_schema.t1 ( ); CREATE TABLE regress_source_schema.t2 ( - id int REFERENCES regress_source_schema.t1(id), + id int, data jsonb ); @@ -223,6 +223,104 @@ CREATE SCHEMA regress_hash_part_copy LIKE regress_hash_part_source INCLUDING ALL -- Verify hash partitioned table structure and that partitions were attached correctly \d+ regress_hash_part_copy.events +-- +-- Test foreign key handling +-- + +-- Create a schema with tables that have FK relationships within the schema +CREATE SCHEMA regress_fk_source; + +CREATE TABLE regress_fk_source.parent ( + id int PRIMARY KEY, + name text +); + +CREATE TABLE regress_fk_source.child ( + id int PRIMARY KEY, + parent_id int REFERENCES regress_fk_source.parent(id) ON DELETE CASCADE, + data text +); + +-- Add another FK relationship +CREATE TABLE regress_fk_source.grandchild ( + id int PRIMARY KEY, + child_id int REFERENCES regress_fk_source.child(id) ON UPDATE CASCADE ON DELETE SET NULL, + info text +); + +-- Test ON DELETE SET NULL with column list (fk_del_set_cols) +CREATE TABLE regress_fk_source.multi_col_parent ( + id int, + sub_id int, + PRIMARY KEY (id, sub_id) +); + +CREATE TABLE regress_fk_source.multi_col_child ( + id int PRIMARY KEY, + parent_id int, + parent_sub_id int, + extra_data text, + FOREIGN KEY (parent_id, parent_sub_id) + REFERENCES regress_fk_source.multi_col_parent(id, sub_id) + ON DELETE SET NULL (parent_id) -- only parent_id set to NULL, not parent_sub_id +); + +-- Copy the schema - FKs should be recreated pointing to new schema tables +CREATE SCHEMA regress_fk_copy LIKE regress_fk_source INCLUDING ALL; + +-- Verify the FK constraints were copied and actions were preserved +\d regress_fk_copy.child +\d regress_fk_copy.grandchild + +-- Verify ON DELETE SET NULL (column_list) was preserved +\d regress_fk_copy.multi_col_child + +-- Test FK to external schema (should be skipped with WARNING) +CREATE SCHEMA regress_fk_external; + +CREATE TABLE regress_fk_external.external_ref ( + id int PRIMARY KEY +); + +CREATE SCHEMA regress_fk_mixed; + +CREATE TABLE regress_fk_mixed.internal_parent ( + id int PRIMARY KEY +); + +-- Table with FK to table in same schema (should be copied) +CREATE TABLE regress_fk_mixed.internal_child ( + id int PRIMARY KEY, + parent_id int REFERENCES regress_fk_mixed.internal_parent(id) +); + +-- Table with FK to external schema (FK should be skipped with WARNING) +CREATE TABLE regress_fk_mixed.external_child ( + id int PRIMARY KEY, + ref_id int REFERENCES regress_fk_external.external_ref(id) +); + +-- Copy should warn about external FK but copy internal FK +CREATE SCHEMA regress_fk_mixed_copy LIKE regress_fk_mixed INCLUDING ALL; + +-- Verify only internal FK was copied (external FK should be skipped) +\d regress_fk_mixed_copy.internal_child +\d egress_fk_mixed_copy.external_child -- should be empty + +-- Test default EXCLUDING with FK and ensure that ALTER TABLE ADD CONSTRAINT is +-- not executed. +CREATE SCHEMA regress_copy_empty LIKE regress_fk_source; + +\d+ regress_copy_empty + + +-- Clean up FK tests +DROP SCHEMA regress_fk_source CASCADE; +DROP SCHEMA regress_fk_copy CASCADE; +DROP SCHEMA regress_fk_external CASCADE; +DROP SCHEMA regress_fk_mixed CASCADE; +DROP SCHEMA regress_fk_mixed_copy CASCADE; + -- Clean up partition tests DROP SCHEMA regress_part_source CASCADE; DROP SCHEMA regress_part_copy CASCADE; -- 2.52.0