From 27bb608e25b18148dc3f55b08a62fc489c09158d Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Fri, 18 Sep 2026 17:55:30 +0900 Subject: [PATCH 2/2] Invalidate RI fast-path metadata on operator family changes The RI fast path checks a foreign key by probing the referenced unique index directly, using the equality operator recorded for the constraint. Whether the fast path can be used is decided once and cached in RI_ConstraintInfo, and that cache is invalidated on pg_constraint changes but not on pg_amop. So after an ALTER OPERATOR FAMILY drops the recorded operator and adds another in its place, the cached decision is stale, and the next fast-path check probes the index with an operator no longer in the opfamily and errors out with "operator XXX is not a member of opfamily XXX". The SPI path is unaffected, because the planner just stops matching the index. To fix, register an AMOPOPID syscache callback to flush the RI cache on pg_amop changes, and have ri_check_fastpath_index() recheck that the recorded operator is still the equality member of the index's opfamily, falling back to SPI when it is not. Add regression test coverage. Reported-by: Nikolay Samokhvalov Author: Nikolay Samokhvalov Discussion: https://www.postgr.es/m/CAM527d9PzFzagr67N0%3DEx2ng1p5HzrcAszy3j5OoZKHXQMARXA%40mail.gmail.com Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 37 ++++++++- src/test/regress/expected/foreign_key.out | 92 +++++++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 58 ++++++++++++++ 3 files changed, 185 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 3196d971939..bd428a4d740 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -2602,7 +2602,7 @@ get_ri_constraint_root(Oid constrOid) } /* - * Callback for pg_constraint inval events + * Callback for pg_constraint and pg_amop inval events * * While most syscache callbacks just flush all their entries, pg_constraint * gets enough update traffic that it's probably worth being smarter. @@ -2627,6 +2627,10 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, Assert(ri_constraint_cache != NULL); + /* pg_amop changes can affect any constraint's fast-path metadata. */ + if (cacheid == AMOPOPID) + hashvalue = 0; + /* * If the list of currently valid entries gets excessively large, we mark * them all invalid so we can empty the list. This arrangement avoids @@ -3567,6 +3571,32 @@ ri_check_fastpath_index(RI_ConstraintInfo *riinfo, } } + /* + * The equality operator stored in pg_constraint must still be an equality + * member of the index opfamily. A loose cross-type member can be + * replaced without changing the constraint itself; leave that case to + * SPI, which continues to use the operator recorded by the constraint. + */ + for (int i = 0; i < riinfo->nkeys; i++) + { + int idx_col; + + for (idx_col = 0; idx_col < idx_rel->rd_index->indnkeyatts; idx_col++) + { + if (idx_rel->rd_index->indkey.values[idx_col] == + riinfo->pk_attnums[i]) + break; + } + Assert(idx_col < idx_rel->rd_index->indnkeyatts); + + if (get_op_opfamily_strategy(riinfo->pf_eq_oprs[i], + idx_rel->rd_opfamily[idx_col]) != BTEqualStrategyNumber) + { + riinfo->fastpath_state = RI_FASTPATH_UNUSABLE; + return false; + } + } + riinfo->fastpath_state = RI_FASTPATH_USABLE; return true; } @@ -4047,10 +4077,13 @@ ri_InitHashTables(void) RI_INIT_CONSTRAINTHASHSIZE, &ctl, HASH_ELEM | HASH_BLOBS); - /* Arrange to flush cache on pg_constraint changes */ + /* Arrange to flush cache on pg_constraint or pg_amop changes */ CacheRegisterSyscacheCallback(CONSTROID, InvalidateConstraintCacheCallBack, (Datum) 0); + CacheRegisterSyscacheCallback(AMOPOPID, + InvalidateConstraintCacheCallBack, + (Datum) 0); ctl.keysize = sizeof(RI_QueryKey); ctl.entrysize = sizeof(RI_QueryHashEntry); diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index c54a9895b55..93b401ce221 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1081,6 +1081,98 @@ CREATE TABLE PKTABLE (ptest1 int, ptest2 inet, ptest3 int, ptest4 inet, PRIMARY ptest3) REFERENCES pktable); ERROR: foreign key constraint "pktable_ptest4_ptest3_fkey" cannot be implemented DETAIL: Key columns "ptest4" of the referencing table and "ptest1" of the referenced table are of incompatible types: inet and integer. +-- Replacing a loose cross-type operator family member must invalidate the +-- fast-path metadata. The FK continues using its stored equality operator, +-- which need no longer be a member of the index family. Use the very same +-- implementation for the replacement, keeping the family semantics unchanged. +create schema fk_opfamily; +set search_path = fk_opfamily, pg_catalog; +create operator family fam using btree; +create operator class int_ops for type integer using btree family fam as + operator 1 <(integer,integer), operator 2 <=(integer,integer), + operator 3 =(integer,integer), operator 4 >=(integer,integer), + operator 5 >(integer,integer), function 1 btint4cmp(integer,integer); +alter operator family fam using btree add + operator 1 <(integer,bigint), operator 2 <=(integer,bigint), + operator 3 =(integer,bigint), operator 4 >=(integer,bigint), + operator 5 >(integer,bigint), + operator 1 <(bigint,integer), operator 2 <=(bigint,integer), + operator 3 =(bigint,integer), operator 4 >=(bigint,integer), + operator 5 >(bigint,integer), + operator 1 <(bigint,bigint), operator 2 <=(bigint,bigint), + operator 3 =(bigint,bigint), operator 4 >=(bigint,bigint), + operator 5 >(bigint,bigint), + function 1 (integer,bigint) btint48cmp(integer,bigint), + function 1 (bigint,integer) btint84cmp(bigint,integer), + function 1 (bigint,bigint) btint8cmp(bigint,bigint); +create operator =#= (leftarg=integer, rightarg=bigint, function=int48eq); +create table p(k integer); +create unique index p_idx on p(k int_ops); +create table warm(k bigint references p(k)); +create table cold(k bigint references p(k)); +insert into p values (1), (2); +insert into warm values (1); +select amvalidate(oid) from pg_opclass +where opcnamespace = 'fk_opfamily'::regnamespace; + amvalidate +------------ + t +(1 row) + +select exists (select from pg_backend_memory_contexts + where name = 'RI fast-path finfo scratch') as metadata_cached; + metadata_cached +----------------- + t +(1 row) + +-- Change only pg_amop after warming the cache. +begin; +alter operator family fam using btree drop operator 3(integer,bigint); +alter operator family fam using btree add operator 3 =#=(integer,bigint); +commit; +select amvalidate(oid) from pg_opclass +where opcnamespace = 'fk_opfamily'::regnamespace; + amvalidate +------------ + t +(1 row) + +select exists (select from pg_backend_memory_contexts + where name = 'RI fast-path finfo scratch') as metadata_cached; + metadata_cached +----------------- + f +(1 row) + +insert into warm values (2); +insert into warm values (99); +ERROR: insert or update on table "warm" violates foreign key constraint "warm_k_fkey" +DETAIL: Key (k)=(99) is not present in table "p". +-- This constraint has no cached fast-path metadata yet. +insert into cold values (2); +insert into cold values (99); +ERROR: insert or update on table "cold" violates foreign key constraint "cold_k_fkey" +DETAIL: Key (k)=(99) is not present in table "p". +select * from warm order by k; + k +--- + 1 + 2 +(2 rows) + +select * from cold order by k; + k +--- + 2 +(1 row) + +reset search_path; +drop table fk_opfamily.warm, fk_opfamily.cold, fk_opfamily.p; +drop operator class fk_opfamily.int_ops using btree; +drop operator family fk_opfamily.fam using btree; +drop operator fk_opfamily.=#=(integer,bigint); +drop schema fk_opfamily; -- -- Now some cases with inheritance -- Basic 2 table case: 1 column of matching types. diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index c013d1f8834..9415a3f938a 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -747,6 +747,64 @@ ptest3) REFERENCES pktable(ptest1, ptest2)); CREATE TABLE PKTABLE (ptest1 int, ptest2 inet, ptest3 int, ptest4 inet, PRIMARY KEY(ptest1, ptest2), FOREIGN KEY(ptest4, ptest3) REFERENCES pktable); +-- Replacing a loose cross-type operator family member must invalidate the +-- fast-path metadata. The FK continues using its stored equality operator, +-- which need no longer be a member of the index family. Use the very same +-- implementation for the replacement, keeping the family semantics unchanged. +create schema fk_opfamily; +set search_path = fk_opfamily, pg_catalog; +create operator family fam using btree; +create operator class int_ops for type integer using btree family fam as + operator 1 <(integer,integer), operator 2 <=(integer,integer), + operator 3 =(integer,integer), operator 4 >=(integer,integer), + operator 5 >(integer,integer), function 1 btint4cmp(integer,integer); +alter operator family fam using btree add + operator 1 <(integer,bigint), operator 2 <=(integer,bigint), + operator 3 =(integer,bigint), operator 4 >=(integer,bigint), + operator 5 >(integer,bigint), + operator 1 <(bigint,integer), operator 2 <=(bigint,integer), + operator 3 =(bigint,integer), operator 4 >=(bigint,integer), + operator 5 >(bigint,integer), + operator 1 <(bigint,bigint), operator 2 <=(bigint,bigint), + operator 3 =(bigint,bigint), operator 4 >=(bigint,bigint), + operator 5 >(bigint,bigint), + function 1 (integer,bigint) btint48cmp(integer,bigint), + function 1 (bigint,integer) btint84cmp(bigint,integer), + function 1 (bigint,bigint) btint8cmp(bigint,bigint); +create operator =#= (leftarg=integer, rightarg=bigint, function=int48eq); +create table p(k integer); +create unique index p_idx on p(k int_ops); +create table warm(k bigint references p(k)); +create table cold(k bigint references p(k)); +insert into p values (1), (2); +insert into warm values (1); +select amvalidate(oid) from pg_opclass +where opcnamespace = 'fk_opfamily'::regnamespace; +select exists (select from pg_backend_memory_contexts + where name = 'RI fast-path finfo scratch') as metadata_cached; +-- Change only pg_amop after warming the cache. +begin; +alter operator family fam using btree drop operator 3(integer,bigint); +alter operator family fam using btree add operator 3 =#=(integer,bigint); +commit; +select amvalidate(oid) from pg_opclass +where opcnamespace = 'fk_opfamily'::regnamespace; +select exists (select from pg_backend_memory_contexts + where name = 'RI fast-path finfo scratch') as metadata_cached; +insert into warm values (2); +insert into warm values (99); +-- This constraint has no cached fast-path metadata yet. +insert into cold values (2); +insert into cold values (99); +select * from warm order by k; +select * from cold order by k; +reset search_path; +drop table fk_opfamily.warm, fk_opfamily.cold, fk_opfamily.p; +drop operator class fk_opfamily.int_ops using btree; +drop operator family fk_opfamily.fam using btree; +drop operator fk_opfamily.=#=(integer,bigint); +drop schema fk_opfamily; + -- -- Now some cases with inheritance -- Basic 2 table case: 1 column of matching types. -- 2.47.3