]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix FK fast-path scan key ordering for mismatched column order
authorAmit Langote <amitlan@postgresql.org>
Fri, 10 Apr 2026 04:33:55 +0000 (13:33 +0900)
committerAmit Langote <amitlan@postgresql.org>
Fri, 10 Apr 2026 04:33:55 +0000 (13:33 +0900)
The fast-path foreign key check introduced in 2da86c1ef9b assumed that
constraint key positions directly correspond to index column positions.
This is not always true as a FK constraint can reference PK columns in a
different order than they appear in the PK's unique index.

For example, if the PK is (a, b, c) and the FK references them as
(a, c, b), the constraint stores keys in the FK-specified order, but
the index has columns in PK order. The buggy code used the constraint
key index to access rd_opfamily[i], which retrieved the wrong operator
family when columns were reordered, causing "operator X is not a member
of opfamily Y" errors.

After fixing the opfamily lookup, a second issue started to happen:
btree index scans require scan keys to be ordered by attribute number.
The code was placing scan keys at array position i with attribute number
idx_attno, producing out-of-order keys when columns were swapped. This
caused "btree index keys must be ordered by attribute" errors.

The fix adds an index_attnos array to FastPathMeta that maps each
constraint key position to its corresponding index column position.
In ri_populate_fastpath_metadata(), we search indkey to find the actual
index column for each pk_attnums[i] and use that position for the
opfamily lookup. In build_index_scankeys(), we place each scan key at
the array position corresponding to its index column
(skeys[idx_attno-1]) rather than at the constraint key position,
ensuring scan keys are properly ordered by attribute number as btree
requires.

Reported-by: Fredrik Widlert <fredrik.widlert@digpro.se>
Author: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Junwang Zhao <zhjwpku@gmail.com>
Discussion: https://www.postgresql.org/message-id/CADfhSr-pCkbDxmiOVYSAGE5QGjsQ48KKH_W424SPk%2BpwzKZFaQ%40mail.gmail.com

src/backend/utils/adt/ri_triggers.c
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index 18ec858357dd40d8d7564d3bb889baa59a279b87..09a5ab24e5666fcd951ea45ead8791f8d747d23c 100644 (file)
@@ -156,6 +156,7 @@ typedef struct FastPathMeta
        RegProcedure regops[RI_MAX_NUMKEYS];
        Oid                     subtypes[RI_MAX_NUMKEYS];
        int                     strats[RI_MAX_NUMKEYS];
+       AttrNumber      index_attnos[RI_MAX_NUMKEYS];   /* index column positions */
 } FastPathMeta;
 
 /*
@@ -3095,14 +3096,17 @@ ri_FastPathFlushArray(RI_FastPathEntry *fpentry, TupleTableSlot *fk_slot,
         * sort and deduplicate, then walk leaf pages in order.
         *
         * PK indexes are always btree, which supports SK_SEARCHARRAY.
+        *
+        * Reference index_attnos[0] for attribute number and collation since this
+        * is a single-column fast path.
         */
        Assert(idx_rel->rd_indam->amsearcharray);
        ScanKeyEntryInitialize(&skey[0],
                                                   SK_SEARCHARRAY,
-                                                  1,   /* attno */
+                                                  fpmeta->index_attnos[0],
                                                   fpmeta->strats[0],
                                                   fpmeta->subtypes[0],
-                                                  idx_rel->rd_indcollation[0],
+                                                  idx_rel->rd_indcollation[fpmeta->index_attnos[0] - 1],
                                                   fpmeta->regops[0],
                                                   PointerGetDatum(arr));
 
@@ -3414,15 +3418,20 @@ build_index_scankeys(const RI_ConstraintInfo *riinfo,
 
        /*
         * Set up ScanKeys for the index scan. This is essentially how
-        * ExecIndexBuildScanKeys() sets them up.
+        * ExecIndexBuildScanKeys() sets them up.  Use the cached index_attnos and
+        * the corresponding collation since FK columns may be in a different
+        * order than PK index columns.  Place each scan key at the array position
+        * corresponding to its index column, since btree requires keys to be
+        * ordered by attribute number.
         */
        for (int i = 0; i < riinfo->nkeys; i++)
        {
-               int                     pkattrno = i + 1;
+               AttrNumber      pkattrno = fpmeta->index_attnos[i];
+               int                     skey_pos = pkattrno - 1;        /* 0-based array position */
 
-               ScanKeyEntryInitialize(&skeys[i], 0, pkattrno,
+               ScanKeyEntryInitialize(&skeys[skey_pos], 0, pkattrno,
                                                           fpmeta->strats[i], fpmeta->subtypes[i],
-                                                          idx_rel->rd_indcollation[i], fpmeta->regops[i],
+                                                          idx_rel->rd_indcollation[skey_pos], fpmeta->regops[i],
                                                           pk_vals[i]);
        }
 }
@@ -3451,6 +3460,23 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo,
                Oid                     typeid = RIAttType(fk_rel, riinfo->fk_attnums[i]);
                Oid                     lefttype;
                RI_CompareHashEntry *entry = ri_HashCompareOp(eq_opr, typeid);
+               int                     idx_col;
+
+               /*
+                * Find the index column position for this constraint key.  The FK
+                * constraint may reference columns in a different order than they
+                * appear in the PK index, so we must map pk_attnums[i] to the
+                * corresponding index column position.
+                */
+               for (idx_col = 0; idx_col < riinfo->nkeys; idx_col++)
+               {
+                       if (idx_rel->rd_index->indkey.values[idx_col] == riinfo->pk_attnums[i])
+                               break;
+               }
+               Assert(idx_col < riinfo->nkeys);
+
+               /* 1-based attribute number */
+               fpmeta->index_attnos[i] = idx_col + 1;
 
                fmgr_info_copy(&fpmeta->cast_func_finfo[i], &entry->cast_func_finfo,
                                           CurrentMemoryContext);
@@ -3459,7 +3485,7 @@ ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo,
                fpmeta->regops[i] = get_opcode(eq_opr);
 
                get_op_opfamily_properties(eq_opr,
-                                                                  idx_rel->rd_opfamily[i],
+                                                                  idx_rel->rd_opfamily[idx_col],
                                                                   false,
                                                                   &fpmeta->strats[i],
                                                                   &lefttype,
index 91295754bab450eaa158156d26d607e449aa4cac..9fa2e22329a8a9b96e7ab6180e5c00a55394e5cd 100644 (file)
@@ -3653,6 +3653,25 @@ INSERT INTO fp_fk_multi VALUES (1, 999, 999);
 ERROR:  insert or update on table "fp_fk_multi" violates foreign key constraint "fp_fk_multi_a_b_fkey"
 DETAIL:  Key (a, b)=(999, 999) is not present in table "fp_pk_multi".
 DROP TABLE fp_fk_multi, fp_pk_multi;
+-- Multi-column FK with columns in different order than PK index.
+-- The FK references columns in a different order than they appear in the
+-- PK's primary key, which requires mapping constraint key positions to
+-- index column positions when building scan keys.
+CREATE TABLE fp_pk_order (a int, b text, c int, PRIMARY KEY (a, b, c));
+INSERT INTO fp_pk_order VALUES (1, 'one', 10), (2, 'two', 20);
+CREATE TABLE fp_fk_order (
+    x int,
+    c int,
+    b text,
+    a int,
+    FOREIGN KEY (a, c, b) REFERENCES fp_pk_order (a, c, b)  -- c and b swapped
+);
+INSERT INTO fp_fk_order VALUES (1, 10, 'one', 1);  -- should succeed
+INSERT INTO fp_fk_order VALUES (2, 20, 'two', 2);  -- should succeed
+INSERT INTO fp_fk_order VALUES (3, 99, 'none', 9);  -- should fail
+ERROR:  insert or update on table "fp_fk_order" violates foreign key constraint "fp_fk_order_a_c_b_fkey"
+DETAIL:  Key (a, c, b)=(9, 99, none) is not present in table "fp_pk_order".
+DROP TABLE fp_fk_order, fp_pk_order;
 -- Deferred constraint: batch flushed at COMMIT, not at statement end
 CREATE TABLE fp_pk_commit (a int PRIMARY KEY);
 CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit
index f646dd1040188bcdbc8a21ee171142d344781acd..9afee64d1e01386c9939dc8ae697ec2396753350 100644 (file)
@@ -2625,6 +2625,24 @@ INSERT INTO fp_fk_multi SELECT i, i, i FROM generate_series(1, 100) i;
 INSERT INTO fp_fk_multi VALUES (1, 999, 999);
 DROP TABLE fp_fk_multi, fp_pk_multi;
 
+-- Multi-column FK with columns in different order than PK index.
+-- The FK references columns in a different order than they appear in the
+-- PK's primary key, which requires mapping constraint key positions to
+-- index column positions when building scan keys.
+CREATE TABLE fp_pk_order (a int, b text, c int, PRIMARY KEY (a, b, c));
+INSERT INTO fp_pk_order VALUES (1, 'one', 10), (2, 'two', 20);
+CREATE TABLE fp_fk_order (
+    x int,
+    c int,
+    b text,
+    a int,
+    FOREIGN KEY (a, c, b) REFERENCES fp_pk_order (a, c, b)  -- c and b swapped
+);
+INSERT INTO fp_fk_order VALUES (1, 10, 'one', 1);  -- should succeed
+INSERT INTO fp_fk_order VALUES (2, 20, 'two', 2);  -- should succeed
+INSERT INTO fp_fk_order VALUES (3, 99, 'none', 9);  -- should fail
+DROP TABLE fp_fk_order, fp_pk_order;
+
 -- Deferred constraint: batch flushed at COMMIT, not at statement end
 CREATE TABLE fp_pk_commit (a int PRIMARY KEY);
 CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit