From 0e9d1d4c489733b9cca7a88019b2b5ebe32abef3 Mon Sep 17 00:00:00 2001
From: Jacob Brazeal <jacob.brazeal@gmail.com>
Date: Thu, 23 Jul 2026 04:03:58 +0000
Subject: [PATCH] Fix temporal foreign key validation of existing rows

NewConstraint.conwithperiod is not set correctly in three paths that
queue foreign keys for ALTER TABLE phase 3 validation.  This can cause
ATTACH PARTITION, VALIDATE CONSTRAINT, and ALTER CONSTRAINT ... ENFORCED
to validate temporal foreign keys as non-temporal.

Set conwithperiod from the value already available at each site.  Add
regression tests for all three paths.
---
 src/backend/commands/tablecmds.c              |  4 +-
 .../regress/expected/without_overlaps.out     | 85 ++++++++++++++++++
 src/test/regress/sql/without_overlaps.sql     | 90 +++++++++++++++++++
 3 files changed, 178 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6d4c457b8..4f33a4019 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11243,7 +11243,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 			newcon->refrelid = RelationGetRelid(pkrel);
 			newcon->refindid = indexOid;
 			newcon->conid = parentConstr;
-			newcon->conwithperiod = fkconstraint->fk_with_period;
+			newcon->conwithperiod = with_period;
 			newcon->qual = (Node *) fkconstraint;
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -12682,6 +12682,7 @@ ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
 			newcon->refrelid = currcon->confrelid;
 			newcon->refindid = currcon->conindid;
 			newcon->conid = currcon->oid;
+			newcon->conwithperiod = currcon->conperiod;
 			newcon->qual = (Node *) fkconstraint;
 
 			/* Find or create work queue entry for this table */
@@ -13570,6 +13571,7 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel,
 		newcon->refrelid = con->confrelid;
 		newcon->refindid = con->conindid;
 		newcon->conid = con->oid;
+		newcon->conwithperiod = con->conperiod;
 		newcon->qual = (Node *) fkconstraint;
 
 		/* Find or create work queue entry for this table */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index de2f8bc47..433a85475 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -2698,4 +2698,89 @@ ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
 --
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
+--
+-- Temporal foreign key validation of existing rows
+--
+-- ATTACH PARTITION
+CREATE TABLE temporal_attach_pk (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_attach_pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal_attach_pk VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+CREATE TABLE temporal_attach_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_attach_fk_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_attach_pk
+) PARTITION BY LIST (id);
+CREATE TABLE temporal_attach_fk_p1 PARTITION OF temporal_attach_fk FOR VALUES IN ('[1,2)');
+CREATE TABLE temporal_attach_fk_p2 (id int4range, valid_at daterange, parent_id int4range);
+INSERT INTO temporal_attach_fk_p2
+  VALUES ('[2,3)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+-- invalid period
+ALTER TABLE temporal_attach_fk ATTACH PARTITION temporal_attach_fk_p2 FOR VALUES IN ('[2,3)');
+ERROR:  insert or update on table "temporal_attach_fk_p2" violates foreign key constraint "temporal_attach_fk_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2005-01-01,2020-01-01)) is not present in table "temporal_attach_pk".
+-- covered period
+DELETE FROM temporal_attach_fk_p2;
+INSERT INTO temporal_attach_fk_p2
+  VALUES ('[2,3)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_attach_fk ATTACH PARTITION temporal_attach_fk_p2 FOR VALUES IN ('[2,3)');
+DROP TABLE temporal_attach_fk;
+DROP TABLE temporal_attach_pk;
+-- VALIDATE CONSTRAINT
+CREATE TABLE temporal_validate_pk (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_validate_pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal_validate_pk VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+CREATE TABLE temporal_validate_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range
+);
+INSERT INTO temporal_validate_fk
+  VALUES ('[1,2)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+ALTER TABLE temporal_validate_fk
+  ADD CONSTRAINT temporal_validate_fk_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_validate_pk
+  NOT VALID;
+-- invalid period
+ALTER TABLE temporal_validate_fk VALIDATE CONSTRAINT temporal_validate_fk_fk;
+ERROR:  insert or update on table "temporal_validate_fk" violates foreign key constraint "temporal_validate_fk_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2005-01-01,2020-01-01)) is not present in table "temporal_validate_pk".
+-- covered period
+DELETE FROM temporal_validate_fk;
+INSERT INTO temporal_validate_fk
+  VALUES ('[1,2)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_validate_fk VALIDATE CONSTRAINT temporal_validate_fk_fk;
+DROP TABLE temporal_validate_fk;
+-- ALTER CONSTRAINT ... ENFORCED
+CREATE TABLE temporal_enforce_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range
+);
+INSERT INTO temporal_enforce_fk
+  VALUES ('[1,2)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+ALTER TABLE temporal_enforce_fk
+  ADD CONSTRAINT temporal_enforce_fk_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_validate_pk
+  NOT ENFORCED;
+-- invalid period
+ALTER TABLE temporal_enforce_fk ALTER CONSTRAINT temporal_enforce_fk_fk ENFORCED;
+ERROR:  insert or update on table "temporal_enforce_fk" violates foreign key constraint "temporal_enforce_fk_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2005-01-01,2020-01-01)) is not present in table "temporal_validate_pk".
+-- covered period
+DELETE FROM temporal_enforce_fk;
+INSERT INTO temporal_enforce_fk
+  VALUES ('[1,2)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_enforce_fk ALTER CONSTRAINT temporal_enforce_fk_fk ENFORCED;
+DROP TABLE temporal_enforce_fk;
+DROP TABLE temporal_validate_pk;
 RESET datestyle;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 4833b8ac5..266145bb1 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -2146,4 +2146,94 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
+--
+-- Temporal foreign key validation of existing rows
+--
+
+-- ATTACH PARTITION
+CREATE TABLE temporal_attach_pk (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_attach_pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal_attach_pk VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+
+CREATE TABLE temporal_attach_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_attach_fk_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_attach_pk
+) PARTITION BY LIST (id);
+CREATE TABLE temporal_attach_fk_p1 PARTITION OF temporal_attach_fk FOR VALUES IN ('[1,2)');
+
+CREATE TABLE temporal_attach_fk_p2 (id int4range, valid_at daterange, parent_id int4range);
+INSERT INTO temporal_attach_fk_p2
+  VALUES ('[2,3)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+-- invalid period
+ALTER TABLE temporal_attach_fk ATTACH PARTITION temporal_attach_fk_p2 FOR VALUES IN ('[2,3)');
+
+-- covered period
+DELETE FROM temporal_attach_fk_p2;
+INSERT INTO temporal_attach_fk_p2
+  VALUES ('[2,3)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_attach_fk ATTACH PARTITION temporal_attach_fk_p2 FOR VALUES IN ('[2,3)');
+
+DROP TABLE temporal_attach_fk;
+DROP TABLE temporal_attach_pk;
+
+-- VALIDATE CONSTRAINT
+CREATE TABLE temporal_validate_pk (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_validate_pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal_validate_pk VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+
+CREATE TABLE temporal_validate_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range
+);
+INSERT INTO temporal_validate_fk
+  VALUES ('[1,2)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+ALTER TABLE temporal_validate_fk
+  ADD CONSTRAINT temporal_validate_fk_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_validate_pk
+  NOT VALID;
+-- invalid period
+ALTER TABLE temporal_validate_fk VALIDATE CONSTRAINT temporal_validate_fk_fk;
+
+-- covered period
+DELETE FROM temporal_validate_fk;
+INSERT INTO temporal_validate_fk
+  VALUES ('[1,2)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_validate_fk VALIDATE CONSTRAINT temporal_validate_fk_fk;
+DROP TABLE temporal_validate_fk;
+
+-- ALTER CONSTRAINT ... ENFORCED
+CREATE TABLE temporal_enforce_fk (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range
+);
+INSERT INTO temporal_enforce_fk
+  VALUES ('[1,2)', daterange('2005-01-01', '2020-01-01'), '[1,2)');
+ALTER TABLE temporal_enforce_fk
+  ADD CONSTRAINT temporal_enforce_fk_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_validate_pk
+  NOT ENFORCED;
+-- invalid period
+ALTER TABLE temporal_enforce_fk ALTER CONSTRAINT temporal_enforce_fk_fk ENFORCED;
+
+-- covered period
+DELETE FROM temporal_enforce_fk;
+INSERT INTO temporal_enforce_fk
+  VALUES ('[1,2)', daterange('2002-01-01', '2008-01-01'), '[1,2)');
+ALTER TABLE temporal_enforce_fk ALTER CONSTRAINT temporal_enforce_fk_fk ENFORCED;
+DROP TABLE temporal_enforce_fk;
+DROP TABLE temporal_validate_pk;
+
 RESET datestyle;
-- 
2.43.0
