From c8a118fc4c5867992e3357586db2fc1d781c9def Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Fri, 31 Jul 2026 21:59:34 -0400
Subject: [PATCH 1/2] Add isolation test for the RI fast path's cross-type
 recheck

A foreign key may use a cross-type equality operator: a "date" primary key and
a "timestamp" referencing column give "=(date,timestamp without time zone)",
whose left input is the PK type and whose right input is the FK type.  When
the referenced row is updated while a check is locking it, the check re-runs
against the new version -- and that recheck passes the value it read out of
the primary key as the operator's right-hand argument, the side declared as
the FK type.

A date counts days and a timestamp counts microseconds, so reading one as the
other compares two unrelated numbers.  Both types are pass-by-value, so
nothing about this turns on how a value is stored, only on which side of the
operator it is read from.

The test needs no unusual values to show it.  s1 moves the key away and puts
it back inside one transaction, so it is present throughout; s2's INSERT is
two rows in one statement, which is what batches the checks and reaches the
recheck.  On a correct server the INSERT succeeds, as it does in the
same-type permutation.  Today it reports

  ERROR:  insert or update on table "fkct_fk" violates foreign key constraint

for a key the next step then prints from the referenced table.

The expected file records the correct behaviour, so the test is red until this
is fixed -- deliberately, to keep the case documented in the tree.

The recheck arrived in b7b27eb41 ("Optimize fast-path FK checks with batched
index probes"), so this is PostgreSQL 19 and master; earlier branches have
only the SPI path, which passes the FK value as a typed parameter.
---
 .../expected/fk-crosstype-recheck.out         | 23 +++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../isolation/specs/fk-crosstype-recheck.spec | 48 +++++++++++++++++++
 3 files changed, 72 insertions(+)
 create mode 100644 src/test/isolation/expected/fk-crosstype-recheck.out
 create mode 100644 src/test/isolation/specs/fk-crosstype-recheck.spec

diff --git a/src/test/isolation/expected/fk-crosstype-recheck.out b/src/test/isolation/expected/fk-crosstype-recheck.out
new file mode 100644
index 000000000..6a66483ea
--- /dev/null
+++ b/src/test/isolation/expected/fk-crosstype-recheck.out
@@ -0,0 +1,23 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1b s1away s1back s2ins s1c s2sel
+step s1b: BEGIN;
+step s1away: UPDATE fkct_pk SET k = '2020-06-01' WHERE payload = 'p1';
+step s1back: UPDATE fkct_pk SET k = '2020-01-01' WHERE payload = 'p1';
+step s2ins: INSERT INTO fkct_fk SELECT g, '2020-01-01'::timestamp FROM generate_series(1,2) g; <waiting ...>
+step s1c: COMMIT;
+step s2ins: <... completed>
+step s2sel: SELECT k FROM fkct_pk;
+         k
+----------
+01-01-2020
+(1 row)
+
+
+starting permutation: s1b s1aways s1backs s2inss s1c
+step s1b: BEGIN;
+step s1aways: UPDATE fkct_pk_same SET k = '2020-06-01' WHERE payload = 'p1';
+step s1backs: UPDATE fkct_pk_same SET k = '2020-01-01' WHERE payload = 'p1';
+step s2inss: INSERT INTO fkct_fk_same SELECT g, '2020-01-01'::timestamp FROM generate_series(1,2) g; <waiting ...>
+step s1c: COMMIT;
+step s2inss: <... completed>
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 26abed9f9..da69e27fa 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -30,6 +30,7 @@ test: detach-partition-concurrently-2
 test: detach-partition-concurrently-3
 test: detach-partition-concurrently-4
 test: fk-contention
+test: fk-crosstype-recheck
 test: fk-deadlock
 test: fk-deadlock2
 test: fk-partitioned-1
diff --git a/src/test/isolation/specs/fk-crosstype-recheck.spec b/src/test/isolation/specs/fk-crosstype-recheck.spec
new file mode 100644
index 000000000..04c8e1a3f
--- /dev/null
+++ b/src/test/isolation/specs/fk-crosstype-recheck.spec
@@ -0,0 +1,48 @@
+# A foreign key may use a cross-type equality operator: a "date" primary key
+# and a "timestamp" referencing column give "=(date,timestamp without time
+# zone)", whose left input is the PK type and whose right input is the FK type.
+#
+# When the referenced row is updated while a check is locking it, the check
+# has to re-check against the new version of the row.  That re-check must
+# still pass each value to the side of the operator that expects it.  A date
+# counts days and a timestamp counts microseconds, so reading one as the other
+# compares two unrelated numbers.  Both types are pass-by-value, so nothing
+# here turns on how a value is stored, only on which side it is read from.
+#
+# Below the referenced key is present the whole time -- s1 moves it away and
+# puts it back inside one transaction -- so the INSERT must succeed, exactly as
+# it does for the same-type case in the second permutation.
+
+setup
+{
+  CREATE TABLE fkct_pk (k date PRIMARY KEY, payload text);
+  CREATE TABLE fkct_fk (id int, t timestamp REFERENCES fkct_pk(k));
+  INSERT INTO fkct_pk VALUES ('2020-01-01', 'p1');
+
+  CREATE TABLE fkct_pk_same (k timestamp PRIMARY KEY, payload text);
+  CREATE TABLE fkct_fk_same (id int, t timestamp REFERENCES fkct_pk_same(k));
+  INSERT INTO fkct_pk_same VALUES ('2020-01-01', 'p1');
+}
+
+teardown
+{
+  DROP TABLE fkct_fk, fkct_pk, fkct_fk_same, fkct_pk_same;
+}
+
+session s1
+step s1b      { BEGIN; }
+step s1away   { UPDATE fkct_pk SET k = '2020-06-01' WHERE payload = 'p1'; }
+step s1back   { UPDATE fkct_pk SET k = '2020-01-01' WHERE payload = 'p1'; }
+step s1aways  { UPDATE fkct_pk_same SET k = '2020-06-01' WHERE payload = 'p1'; }
+step s1backs  { UPDATE fkct_pk_same SET k = '2020-01-01' WHERE payload = 'p1'; }
+step s1c      { COMMIT; }
+
+# Two rows in one statement, so the checks are batched -- that is what reaches
+# the re-check path under test.
+session s2
+step s2ins    { INSERT INTO fkct_fk SELECT g, '2020-01-01'::timestamp FROM generate_series(1,2) g; }
+step s2inss   { INSERT INTO fkct_fk_same SELECT g, '2020-01-01'::timestamp FROM generate_series(1,2) g; }
+step s2sel    { SELECT k FROM fkct_pk; }
+
+permutation s1b s1away s1back s2ins s1c s2sel
+permutation s1b s1aways s1backs s2inss s1c
-- 
2.53.0

