diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 3196d971939..059dafce836 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,17 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, Assert(ri_constraint_cache != NULL); + /* + * pg_amop changes can affect any constraint's fast-path metadata, and + * this pg_amop hashvalue can't be matched against the pg_constraint-keyed + * cache entries, so flush them all via the match-everything path below as + * the large-list reset below does. Being selective would mean mapping + * the change back to the affected constraints, not worth it for DDL this + * rare. + */ + 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 +3578,33 @@ ri_check_fastpath_index(RI_ConstraintInfo *riinfo, } } + /* + * The equality operator stored in pg_constraint must still be an equality + * member of the index opfamily. When it is not, the direct fast-path + * probe errors, so mark the fast path unusable and fall back to SPI, + * which uses the same operator in a query where the planner simply + * declines the index. + */ + 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 +4085,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..861451ef291 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -1081,6 +1081,73 @@ 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. +-- Replace the equality operator the FK recorded with an identical +-- implementation, so only opfamily membership changes. The recorded operator +-- is now absent from the family; the fast path must fall back to SPI instead +-- of probing with it. +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); +-- Two identical FKs to exercise both cache states: warm gets a row now so its +-- fast-path metadata is built and cached; cold is left empty and has none. +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); +-- Change only pg_amop. warm's cached metadata now names an operator the +-- opfamily no longer contains; cold is still evaluated fresh. +begin; +alter operator family fam using btree drop operator 3(integer,bigint); +alter operator family fam using btree add operator 3 =#=(integer,bigint); +commit; +-- A present key must be accepted and a missing one rejected, via SPI. +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". +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..b4d53a50b09 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -747,6 +747,62 @@ 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); +-- Replace the equality operator the FK recorded with an identical +-- implementation, so only opfamily membership changes. The recorded operator +-- is now absent from the family; the fast path must fall back to SPI instead +-- of probing with it. +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); + +-- Two identical FKs to exercise both cache states: warm gets a row now so its +-- fast-path metadata is built and cached; cold is left empty and has none. +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); + +-- Change only pg_amop. warm's cached metadata now names an operator the +-- opfamily no longer contains; cold is still evaluated fresh. +begin; +alter operator family fam using btree drop operator 3(integer,bigint); +alter operator family fam using btree add operator 3 =#=(integer,bigint); +commit; + +-- A present key must be accepted and a missing one rejected, via SPI. +insert into warm values (2); +insert into warm values (99); +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.