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:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user