From 98c30cee57b170ddcb8b6f48e0cb3c7143ebd525 Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Fri, 12 Jun 2026 19:16:12 +0800
Subject: [PATCH] Fix foreign-key fast path for domain-typed FK columns

Commit 2da86c1 added a fast path for RI checks that is the first caller to
pass the cross-type pf_eq_oprs operator to ri_HashCompareOp().  Its new
cross-type handling treated a comparison as needing no cast only when the
FK column's type was exactly equal to the operator's right-hand input type.
When the FK column is a domain over that type, the test failed and the code
went on to conclude that no usable conversion existed, raising

    ERROR:  no conversion function from <domain> to <type>

for perfectly valid foreign-key inserts.  For example, an FK column of a
domain over int8 referencing an int4 primary key rejected every row.  The
pre-fast-path SPI-based check handled this correctly because the cast was
applied by the planner, which looks through domains transparently.

Look through the domain to its base type when deciding whether a cast is
needed: a domain is binary-coercible to its base type, so a domain over the
operator's right-hand input type needs no cast.

Add a regression test covering a domain-typed FK column whose base type
differs from the referenced primary-key column's type.
---
 src/backend/utils/adt/ri_triggers.c       |  7 +++++--
 src/test/regress/expected/foreign_key.out | 15 +++++++++++++++
 src/test/regress/sql/foreign_key.sql      | 14 ++++++++++++++
 3 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index dc89c686394..0cdb5f1b926 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -4054,9 +4054,12 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)
 		 * pf_eq_oprs (used by the fast path) can be cross-type when the FK
 		 * and PK columns differ in type, e.g. int48eq for int4 PK / int8 FK.
 		 * If the FK column's type already matches what the operator expects
-		 * as its right-hand input, no cast is needed.
+		 * as its right-hand input, no cast is needed.  Look through a domain
+		 * to its base type here: a domain is binary-coercible to its base
+		 * type, so a domain over the operator's right-hand input type needs
+		 * no cast either, and must not be rejected below.
 		 */
-		if (typeid == righttype)
+		if (getBaseType(typeid) == righttype)
 			castfunc = InvalidOid;	/* simplest case */
 		else
 		{
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 8b3b268de0f..33079b9fd6e 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -3705,6 +3705,21 @@ INSERT INTO fp_fk_cross VALUES (999);
 ERROR:  insert or update on table "fp_fk_cross" violates foreign key constraint "fp_fk_cross_a_fkey"
 DETAIL:  Key (a)=(999) is not present in table "fp_pk_cross".
 DROP TABLE fp_fk_cross, fp_pk_cross;
+-- Domain-typed FK column whose base type differs from the PK type: the
+-- fast path must look through the domain to its base type when deciding
+-- whether the cross-type comparison needs a cast.  Otherwise valid rows
+-- were wrongly rejected with "no conversion function from ... to ...".
+CREATE DOMAIN fp_int8dom AS int8;
+CREATE TABLE fp_pk_dom (a int4 PRIMARY KEY);
+INSERT INTO fp_pk_dom SELECT generate_series(1, 200);
+CREATE TABLE fp_fk_dom (a fp_int8dom REFERENCES fp_pk_dom);
+INSERT INTO fp_fk_dom SELECT generate_series(1, 200);
+INSERT INTO fp_fk_dom VALUES (999);
+ERROR:  insert or update on table "fp_fk_dom" violates foreign key constraint "fp_fk_dom_a_fkey"
+DETAIL:  Key (a)=(999) is not present in table "fp_pk_dom".
+INSERT INTO fp_fk_dom VALUES (NULL);
+DROP TABLE fp_fk_dom, fp_pk_dom;
+DROP DOMAIN fp_int8dom;
 -- Duplicate FK values: when using the batched SAOP path, every
 -- row must be recognized as satisfied, not just the first match
 CREATE TABLE fp_pk_dup (a int PRIMARY KEY);
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 7eb86b188f0..19be85af34c 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -2673,6 +2673,20 @@ INSERT INTO fp_fk_cross SELECT generate_series(1, 200);
 INSERT INTO fp_fk_cross VALUES (999);
 DROP TABLE fp_fk_cross, fp_pk_cross;
 
+-- Domain-typed FK column whose base type differs from the PK type: the
+-- fast path must look through the domain to its base type when deciding
+-- whether the cross-type comparison needs a cast.  Otherwise valid rows
+-- were wrongly rejected with "no conversion function from ... to ...".
+CREATE DOMAIN fp_int8dom AS int8;
+CREATE TABLE fp_pk_dom (a int4 PRIMARY KEY);
+INSERT INTO fp_pk_dom SELECT generate_series(1, 200);
+CREATE TABLE fp_fk_dom (a fp_int8dom REFERENCES fp_pk_dom);
+INSERT INTO fp_fk_dom SELECT generate_series(1, 200);
+INSERT INTO fp_fk_dom VALUES (999);
+INSERT INTO fp_fk_dom VALUES (NULL);
+DROP TABLE fp_fk_dom, fp_pk_dom;
+DROP DOMAIN fp_int8dom;
+
 -- Duplicate FK values: when using the batched SAOP path, every
 -- row must be recognized as satisfied, not just the first match
 CREATE TABLE fp_pk_dup (a int PRIMARY KEY);
-- 
2.47.3

