1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

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 <nik@postgres.ai>
Author: Nikolay Samokhvalov <nik@postgres.ai>
Discussion: https://www.postgr.es/m/CAM527d9PzFzagr67N0%3DEx2ng1p5HzrcAszy3j5OoZKHXQMARXA%40mail.gmail.com
Backpatch-through: 19
This commit is contained in:
Amit Langote
2026-09-19 15:27:52 +09:00
parent 2fc654e113
commit c86738fd4a
3 changed files with 166 additions and 2 deletions

View File

@@ -2482,7 +2482,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.
@@ -2507,6 +2507,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
@@ -3048,6 +3059,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;
}
@@ -3528,10 +3566,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);

View File

@@ -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.

View File

@@ -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.