From 35aee67822e810a7c6a84e787d55cf13284d6e54 Mon Sep 17 00:00:00 2001 From: reshke Date: Fri, 18 Sep 2026 22:25:13 +0300 Subject: [PATCH v1] Fix NOT NULL NOT VALID constraints import in FDW --- .../postgres_fdw/expected/postgres_fdw.out | 40 +++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 36 ++++++++++++++++- contrib/postgres_fdw/sql/postgres_fdw.sql | 19 +++++++++ src/backend/catalog/heap.c | 2 +- src/backend/parser/parse_utilcmd.c | 10 +++++ 5 files changed, 104 insertions(+), 3 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 739f43af7bb..60fdbd6d55d 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -10989,6 +10989,46 @@ IMPORT FOREIGN SCHEMA import_source EXCEPT (t1, "x 4", nonesuch, t4_part) import_dest4 | x 6 | loopback | (schema_name 'import_source', table_name 'x 6') | (7 rows) +-- NOT VALID NOT NULL constraints must be imported as NOT VALID +CREATE TABLE import_source.t_notvalid (c1 int); +INSERT INTO import_source.t_notvalid VALUES (NULL), (1); +ALTER TABLE import_source.t_notvalid + ADD CONSTRAINT t_notvalid_nn NOT NULL c1 NOT VALID; +CREATE SCHEMA import_dest6; +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t_notvalid) + FROM SERVER loopback INTO import_dest6; +\d import_dest6.t_notvalid + Foreign table "import_dest6.t_notvalid" + Column | Type | Collation | Nullable | Default | FDW options +--------+---------+-----------+----------+---------+-------------------- + c1 | integer | | not null | | (column_name 'c1') +Server: loopback +FDW options: (schema_name 'import_source', table_name 't_notvalid') + +SELECT a.attnotnull, c.conname, c.convalidated + FROM pg_attribute a JOIN pg_constraint c + ON c.conrelid = a.attrelid AND a.attnum = ANY (c.conkey) + WHERE a.attrelid = 'import_dest6.t_notvalid'::regclass + AND a.attname = 'c1' AND c.contype = 'n'; + attnotnull | conname | convalidated +------------+------------------------+-------------- + t | t_notvalid_c1_not_null | f +(1 row) + +SET constraint_exclusion = on; +SELECT * FROM import_dest6.t_notvalid WHERE c1 IS NULL; + c1 +---- + +(1 row) + +SELECT * FROM import_dest6.t_notvalid WHERE c1 IS NOT NULL; + c1 +---- + 1 +(1 row) + +RESET constraint_exclusion; -- Assorted error cases IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest4; ERROR: relation "t1" already exists diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index fbe01fdde85..b3bf3e77e8a 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -6621,6 +6621,23 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) appendStringInfoString(&buf, " NULL, NULL "); + /* + * Fetch a flag indicating whether the column's NOT NULL constraint is + * NOT VALID (supported since Postgres 18): the remote table might + * still contain NULLs. + */ + if (PQserverVersion(conn) >= 180000) + appendStringInfoString(&buf, + ", EXISTS (" + " SELECT 1 FROM pg_catalog.pg_constraint con " + " WHERE con.conrelid = c.oid " + " AND con.contype = 'n' " + " AND a.attnum = ANY (con.conkey) " + " AND NOT con.convalidated) "); + else + appendStringInfoString(&buf, + ", false "); + appendStringInfoString(&buf, "FROM pg_class c " " JOIN pg_namespace n ON " @@ -6693,8 +6710,10 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) { char *tablename = PQgetvalue(res, i, 0); bool first_item = true; + StringInfoData nn_not_valid; resetStringInfo(&buf); + initStringInfo(&nn_not_valid); appendStringInfo(&buf, "CREATE FOREIGN TABLE %s (\n", quote_identifier(tablename)); @@ -6704,6 +6723,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) char *attname; char *typename; char *attnotnull; + char *attnotnull_not_valid; char *attgenerated; char *attdefault; char *collname; @@ -6716,6 +6736,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) attname = PQgetvalue(res, i, 1); typename = PQgetvalue(res, i, 2); attnotnull = PQgetvalue(res, i, 3); + attnotnull_not_valid = PQgetvalue(res, i, 8); attdefault = PQgetisnull(res, i, 4) ? NULL : PQgetvalue(res, i, 4); attgenerated = PQgetisnull(res, i, 5) ? NULL : @@ -6764,13 +6785,24 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) attdefault); } - /* Add NOT NULL if needed */ + /* Add NOT NULL if needed; NOT VALID ones as table constraints */ if (import_not_null && attnotnull[0] == 't') - appendStringInfoString(&buf, " NOT NULL"); + { + if (attnotnull_not_valid[0] == 't') + appendStringInfo(&nn_not_valid, ",\nNOT NULL %s NOT VALID", + quote_identifier(attname)); + else + appendStringInfoString(&buf, " NOT NULL"); + } } while (++i < numrows && strcmp(PQgetvalue(res, i, 0), tablename) == 0); + /* Emit any NOT NULL NOT VALID constraints */ + if (nn_not_valid.len > 0) + appendStringInfoString(&buf, nn_not_valid.data); + pfree(nn_not_valid.data); + /* * Add server name and table-level options. We specify remote schema * and table name as options (the latter to ensure that renaming the diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index f1ca3204382..d00db7d7803 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -3537,6 +3537,25 @@ IMPORT FOREIGN SCHEMA import_source EXCEPT (t1, "x 4", nonesuch, t4_part) FROM SERVER loopback INTO import_dest4; \det+ import_dest4.* +-- NOT VALID NOT NULL constraints must be imported as NOT VALID +CREATE TABLE import_source.t_notvalid (c1 int); +INSERT INTO import_source.t_notvalid VALUES (NULL), (1); +ALTER TABLE import_source.t_notvalid + ADD CONSTRAINT t_notvalid_nn NOT NULL c1 NOT VALID; +CREATE SCHEMA import_dest6; +IMPORT FOREIGN SCHEMA import_source LIMIT TO (t_notvalid) + FROM SERVER loopback INTO import_dest6; +\d import_dest6.t_notvalid +SELECT a.attnotnull, c.conname, c.convalidated + FROM pg_attribute a JOIN pg_constraint c + ON c.conrelid = a.attrelid AND a.attnum = ANY (c.conkey) + WHERE a.attrelid = 'import_dest6.t_notvalid'::regclass + AND a.attname = 'c1' AND c.contype = 'n'; +SET constraint_exclusion = on; +SELECT * FROM import_dest6.t_notvalid WHERE c1 IS NULL; +SELECT * FROM import_dest6.t_notvalid WHERE c1 IS NOT NULL; +RESET constraint_exclusion; + -- Assorted error cases IMPORT FOREIGN SCHEMA import_source FROM SERVER loopback INTO import_dest4; IMPORT FOREIGN SCHEMA nonesuch FROM SERVER loopback INTO import_dest4; diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 1c188b7a0ff..42c943ff5ed 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -3085,7 +3085,7 @@ AddRelationNotNullConstraints(Relation rel, List *constraints, nnnames = lappend(nnnames, conname); StoreRelNotNull(rel, conname, - attnum, true, true, + attnum, constr->initially_valid, true, inhcount, constr->is_no_inherit); nncols = lappend_int(nncols, attnum); diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index f838311090b..60bbbb43c3f 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -369,6 +369,16 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) */ transformCheckConstraints(&cxt, !cxt.isforeign); + /* Likewise for not-null constraints: foreign tables can have data. */ + if (!cxt.isforeign) + { + foreach_node(Constraint, nn, cxt.nnconstraints) + { + nn->skip_validation = false; + nn->initially_valid = true; + } + } + /* * Output results. */ -- 2.43.0