With default MATCH SIMPLE behavior, a referencing row need not satisfy the foreign key constraint if any of its referencing columns are null. The (non-default) MATCH FULL behavior enforces your desired behavior. See:
Also, your whole setup can be radically simplified with the NULLS NOT DISTINCT clause for the UNIQUE constraint since Postgres 15. See:
So:
CREATE TABLE p (
item_id int NOT NULL
, extra_item_id int
, UNIQUE NULLS NOT DISTINCT (item_id, extra_item_id) -- !
);
CREATE TABLE c (
intermediate_item_id int
, intermediate_extra_item_id int
, FOREIGN KEY (intermediate_item_id, intermediate_extra_item_id)
REFERENCES p (item_id, extra_item_id) MATCH FULL -- !
);
fiddle
Wouldn't work for an actual PRIMARY KEY (instead of the UNIQUE constraint) like you formulated a bit loosely at first, since PK columns are NOT NULL implicitly. See:
MATCH SIMPLEis the default, which allows this. You wantMATCH FULL. See eg dbfiddle.uk/vsN_MLZbMATCH PARTIAL is not yet implemented. (Of course, NOT NULL constraints can be applied to the referencing column(s) to prevent these cases from arising.)(version 18, currently in Beta)