src/backend/commands/tablecmds.c: addFkRecurseReferencing if (wqueue && !old_check_ok && !fkconstraint->skip_validation && fkconstraint->is_enforced) { NewConstraint *newcon; AlteredTableInfo *tab; tab = ATGetQueueEntry(wqueue, rel); newcon = palloc0_object(NewConstraint); newcon->name = get_constraint_name(parentConstr); newcon->contype = CONSTR_FOREIGN; newcon->refrelid = RelationGetRelid(pkrel); newcon->refindid = indexOid; newcon->conid = parentConstr; newcon->conwithperiod = fkconstraint->fk_with_period; newcon->qual = (Node *) fkconstraint; tab->constraints = lappend(tab->constraints, newcon); } CloneFkReferencing: /* No dice. Set up to create our own constraint */ fkconstraint = makeNode(Constraint); fkconstraint->contype = CONSTRAINT_FOREIGN; /* ->conname determined below */ fkconstraint->deferrable = constrForm->condeferrable; fkconstraint->initdeferred = constrForm->condeferred; fkconstraint->location = -1; fkconstraint->pktable = NULL; /* ->fk_attrs determined below */ fkconstraint->pk_attrs = NIL; fkconstraint->fk_matchtype = constrForm->confmatchtype; fkconstraint->fk_upd_action = constrForm->confupdtype; fkconstraint->fk_del_action = constrForm->confdeltype; fkconstraint->fk_del_set_cols = NIL; fkconstraint->old_conpfeqop = NIL; fkconstraint->old_pktable_oid = InvalidOid; fkconstraint->is_enforced = constrForm->conenforced; fkconstraint->skip_validation = false; fkconstraint->initially_valid = constrForm->convalidated; CloneFkReferencing does not assign the fkconstraint->fk_with_period properly then `` newcon->conwithperiod = fkconstraint->fk_with_period; `` is wrong? if wrong, make a minmium test case demo it. HINT: validateForeignKeyConstraint use this boolean; validateForeignKeyConstraint(fkconstraint->conname, rel, refrel, con->refindid, con->conid, con->conwithperiod); ---------------------------------------------------------------------------------------------------- Yes, that's a real bug, and your instinct about `validateForeignKeyConstraint` is exactly the right place to look. ## Confirming it `CloneFkReferencing` builds the `Constraint` node with `makeNode` (11750), which zeroes it, and the explicit assignments at 11751–11767 never touch `fk_with_period` or `pk_with_period`. So the node says `false`. The function then reads the truth from the catalog separately — `with_period = constrForm->conperiod;` (11779) — and threads *that* into `addFkConstraint` (11789) and `addFkRecurseReferencing` (11813). So `pg_constraint.conperiod` on the cloned child constraint is correct; only the node is stale. Then `addFkRecurseReferencing` has `with_period` right there as a parameter (11183) but line 11236 reads the node instead. Result: `newcon->conwithperiod == false` for a temporal FK. The fix is one line: ```c - newcon->conwithperiod = fkconstraint->fk_with_period; + newcon->conwithperiod = with_period; ``` And that's safe for every caller, not just the clone path. In `ATAddForeignKeyConstraint`, `with_period = fk_with_period || pk_with_period` (10307) and 10308 immediately errors out if `with_period && !fk_with_period` — so there the two are provably equal. The parameter is always at least as correct as the node field, and in the one path where they diverge the node field is the wrong one. ## Why the symptom is the bad direction `hasperiod=false` makes 14258 take the `RI_Initial_Check` LEFT JOIN shortcut instead of the per-row trigger loop. And the join is built from `conpfeqop`, where the period column's operator is *overlaps*, not equality — see `for_overlaps = with_period && i == numpks - 1` at 10497. So you get: ```sql ... FROM ONLY tfk2 fk LEFT JOIN ONLY tp pk ON (pk.id = fk.parent_id AND pk.valid_at && fk.valid_at) WHERE pk.id IS NULL AND ... ``` That asks "does this row overlap *some* PK row?" Temporal FK semantics require "is this row's period contained in the *union* of the matching PK rows' periods" — which is what the trigger path computes via `range_agg`. Overlap is strictly weaker than containment, so the buggy check **under-reports**: rows that poke out past the union pass. You end up attaching a partition full of FK-violating rows, with the constraint left marked validated. Also worth noting: `without_overlaps.sql` has zero `ATTACH PARTITION` coverage, which is why this survived. ## Minimal repro I couldn't execute this — no flex/bison in this sandbox and no root to install them — so treat it as untested. Run as superuser with no RLS, otherwise `RI_Initial_Check` bails out on permissions (1738/1746) and silently falls through to the *correct* trigger loop, masking the bug. ```sql CREATE TABLE tp ( id int4range, valid_at daterange, CONSTRAINT tp_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ); INSERT INTO tp VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01')); CREATE TABLE tfk ( id int4range, valid_at daterange, parent_id int4range, CONSTRAINT tfk_fk FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES tp (id, PERIOD valid_at) ) PARTITION BY LIST (id); -- Baseline: the runtime trigger correctly rejects this row. CREATE TABLE tfk1 PARTITION OF tfk FOR VALUES IN ('[1,2)'); INSERT INTO tfk VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[1,2)'); -- ERROR: insert or update on table "tfk1" violates foreign key constraint "tfk_fk" -- Now the same row via ATTACH PARTITION, which goes through CloneFkReferencing. CREATE TABLE tfk2 (LIKE tfk); -- plain LIKE: no FK copied, so we clone rather than attach INSERT INTO tfk2 VALUES ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[1,2)'); ALTER TABLE tfk ATTACH PARTITION tfk2 FOR VALUES IN ('[2,3)'); -- expected: ERROR, same violation -- actual: succeeds -- the row *overlaps* [2000-01-01,2000-02-01) so the LEFT JOIN finds a match ``` Then show the catalog is now lying: ```sql SELECT conname, conperiod, convalidated FROM pg_constraint WHERE conrelid = 'tfk2'::regclass AND contype = 'f'; -- conperiod = t, convalidated = t UPDATE tfk2 SET parent_id = parent_id; -- fires the real RI check -- ERROR -- the row ATTACH just validated is in fact a violation ``` That last step is the money shot for a bug report: the identical row is accepted by the validation scan and rejected by the enforcement trigger, which can only mean the scan used the wrong semantics. Affects 18 and up — temporal FKs went in with 89f908a6d0a (2024-09-17) — so it's back-patchable to REL_18_STABLE. Worth adding the `ATTACH PARTITION` cases to `without_overlaps.sql` in the same patch.