diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 6958f991604..5d55a2006ee 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -34,6 +34,7 @@ #include "catalog/pg_am_d.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" +#include "catalog/pg_index.h" #include "catalog/pg_namespace.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -100,6 +101,14 @@ typedef struct FastPathMeta FastPathMeta; +/* Eligibility checks that require the referenced table and index to be open. */ +typedef enum RI_FastPathState +{ + RI_FASTPATH_UNKNOWN, + RI_FASTPATH_USABLE, + RI_FASTPATH_UNUSABLE +} RI_FastPathState; + /* * RI_ConstraintInfo * @@ -143,7 +152,7 @@ typedef struct RI_ConstraintInfo Oid conindid; bool pk_is_partitioned; - bool pk_index_is_btree; /* is conindid a btree index? */ + RI_FastPathState fastpath_state; /* populated lazily under lock */ FastPathMeta *fpmeta; } RI_ConstraintInfo; @@ -281,7 +290,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot, TupleTableSlot *newslot, bool is_restrict, bool detectNewRows, int expect_OK); -static void ri_FastPathCheck(RI_ConstraintInfo *riinfo, +static bool ri_FastPathCheck(RI_ConstraintInfo *riinfo, Relation fk_rel, TupleTableSlot *newslot); static bool ri_FastPathProbeOne(Relation pk_rel, Relation idx_rel, IndexScanDesc scandesc, TupleTableSlot *slot, @@ -290,6 +299,8 @@ static bool ri_FastPathProbeOne(Relation pk_rel, Relation idx_rel, static bool ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap, bool *concurrently_updated); static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo); +static bool ri_check_fastpath_index(RI_ConstraintInfo *riinfo, + Relation pk_rel, Relation idx_rel); static void ri_CheckPermissions(Relation query_rel); static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, TupleTableSlot *new_slot); @@ -416,14 +427,13 @@ RI_FKey_check(TriggerData *trigdata) * lock. This is semantically equivalent to the SPI path below but avoids * the per-row executor overhead. * - * ri_FastPathCheck() reports the violation itself (via ereport) if no - * matching PK row is found, so it only returns on success. + * ri_FastPathCheck() reports the violation itself if no matching PK row + * is found. It returns false if the index checks made after opening the + * relations require a SPI fallback. */ - if (ri_fastpath_is_applicable(riinfo)) - { - ri_FastPathCheck(riinfo, fk_rel, newslot); + if (ri_fastpath_is_applicable(riinfo) && + ri_FastPathCheck(riinfo, fk_rel, newslot)) return PointerGetDatum(NULL); - } SPI_connect(); @@ -2429,8 +2439,7 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo->conindid = conForm->conindid; riinfo->pk_is_partitioned = (get_rel_relkind(riinfo->pk_relid) == RELKIND_PARTITIONED_TABLE); - riinfo->pk_index_is_btree = - (get_rel_relam(riinfo->conindid) == BTREE_AM_OID); + riinfo->fastpath_state = RI_FASTPATH_UNKNOWN; ReleaseSysCache(tup); @@ -2739,10 +2748,11 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, * Perform per row FK existence check via direct index probe, * bypassing SPI. * - * If no matching PK row exists, report the violation via ri_ReportViolation(), - * otherwise, the function returns normally. + * Return false if the index is unsuitable, so the caller can use SPI. + * Otherwise, report any violation via ri_ReportViolation(), or return true + * after a successful check. */ -static void +static bool ri_FastPathCheck(RI_ConstraintInfo *riinfo, Relation fk_rel, TupleTableSlot *newslot) { @@ -2776,6 +2786,13 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, idx_rel = index_open(riinfo->conindid, AccessShareLock); + if (!ri_check_fastpath_index(riinfo, pk_rel, idx_rel)) + { + index_close(idx_rel, NoLock); + table_close(pk_rel, NoLock); + return false; + } + /* * Only now take the snapshot the scan will use. Acquiring it before * table_open() would let an unbounded amount of time pass while we wait @@ -2840,6 +2857,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, index_close(idx_rel, NoLock); table_close(pk_rel, NoLock); + return true; } /* @@ -2954,6 +2972,10 @@ ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap, return false; /* keep compiler quiet */ } +/* + * Apply the checks that do not require opening the referenced index. An + * unknown state still needs ri_check_fastpath_index() before using it. + */ static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo) { @@ -2973,6 +2995,29 @@ ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo) if (riinfo->hasperiod) return false; + return riinfo->fastpath_state != RI_FASTPATH_UNUSABLE; +} + +/* + * Check index-dependent eligibility lazily, like fast-path scan metadata. + * Cache both success and failure until the constraint information is reloaded. + * + * The caller has locked the referenced table, reloaded conindid, and opened + * the index. Looking up index properties in ri_LoadConstraintInfo() would + * race with REINDEX CONCURRENTLY dropping an index read before that lock. + * The index-property checks use the held relation descriptors. + */ +static bool +ri_check_fastpath_index(RI_ConstraintInfo *riinfo, + Relation pk_rel, Relation idx_rel) +{ + /* Opening the index can have processed further invalidations. */ + if (!riinfo->valid) + riinfo = ri_LoadConstraintInfo(riinfo->constraint_id); + + if (riinfo->fastpath_state != RI_FASTPATH_UNKNOWN) + return riinfo->fastpath_state == RI_FASTPATH_USABLE; + /* * The fast path probes the referenced index directly. A foreign key's * referenced index need not be a primary key; transformFkeyCheckAttrs() @@ -2980,9 +3025,29 @@ ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo) * could reach here. Restrict the fast path to btree; other access * methods fall back to SPI. */ - if (!riinfo->pk_index_is_btree) + if (idx_rel->rd_rel->relam != BTREE_AM_OID) + { + riinfo->fastpath_state = RI_FASTPATH_UNUSABLE; return false; + } + /* + * Leave comparisons with a different index and referenced-column + * collation to SPI. Map index keys to table attributes because the FK + * columns need not be listed in index order. Ignore INCLUDE columns. + */ + for (int i = 0; i < idx_rel->rd_index->indnkeyatts; i++) + { + AttrNumber attnum = idx_rel->rd_index->indkey.values[i]; + + if (idx_rel->rd_indcollation[i] != RIAttCollation(pk_rel, attnum)) + { + riinfo->fastpath_state = RI_FASTPATH_UNUSABLE; + return false; + } + } + + riinfo->fastpath_state = RI_FASTPATH_USABLE; return true; } diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index eb483b9015e..ff32cf08864 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -3111,6 +3111,42 @@ CREATE TABLE test11pk (x text COLLATE case_insensitive PRIMARY KEY); CREATE TABLE test11fk (x text COLLATE case_sensitive REFERENCES test11pk (x) ON UPDATE CASCADE ON DELETE CASCADE); -- error ERROR: foreign key constraint "test11fk_x_fkey" cannot be implemented DETAIL: Key columns "x" of the referencing table and "x" of the referenced table have incompatible collations: "case_sensitive" and "case_insensitive". If either collation is nondeterministic, then both collations have to be the same. +-- The referenced index's collation can differ from the column's collation. +-- Use reversed index columns to exercise the index-to-table column mapping. +CREATE TABLE fk_collation_pk (id int, x text COLLATE case_insensitive); +CREATE UNIQUE INDEX fk_collation_idx ON fk_collation_pk (x COLLATE "C", id); +INSERT INTO fk_collation_pk VALUES (1, 'ABC'); +CREATE TABLE fk_collation_fk (id int, x text COLLATE case_insensitive); +INSERT INTO fk_collation_fk VALUES (1, 'abc'); +-- Lack of SELECT on the PK table forces per-row validation. +CREATE ROLE regress_fk_collation; +GRANT USAGE ON SCHEMA collate_tests TO regress_fk_collation; +GRANT REFERENCES ON fk_collation_pk TO regress_fk_collation; +ALTER TABLE fk_collation_fk OWNER TO regress_fk_collation; +SET ROLE regress_fk_collation; +ALTER TABLE fk_collation_fk ADD CONSTRAINT fk_collation_fkey + FOREIGN KEY (id, x) REFERENCES fk_collation_pk (id, x); +-- Ordinary DML must also use the column's case-insensitive equality. +INSERT INTO fk_collation_fk VALUES (1, 'aBc'); +INSERT INTO fk_collation_fk VALUES (2, 'abc'); -- fails +ERROR: insert or update on table "fk_collation_fk" violates foreign key constraint "fk_collation_fkey" +DETAIL: Key (id, x)=(2, abc) is not present in table "fk_collation_pk". +RESET ROLE; +DROP TABLE fk_collation_fk, fk_collation_pk; +REVOKE USAGE ON SCHEMA collate_tests FROM regress_fk_collation; +DROP ROLE regress_fk_collation; +-- Conversely, a case-insensitive index must not make the FK comparison +-- accept unequal values under the column's deterministic collation. +CREATE TABLE fk_collation_pk (x text COLLATE "C"); +CREATE UNIQUE INDEX fk_collation_idx ON fk_collation_pk (x COLLATE case_insensitive); +INSERT INTO fk_collation_pk VALUES ('ABC'); +CREATE TABLE fk_collation_fk (x text COLLATE "C", + CONSTRAINT fk_collation_fkey FOREIGN KEY (x) REFERENCES fk_collation_pk (x)); +INSERT INTO fk_collation_fk VALUES ('ABC'); +INSERT INTO fk_collation_fk VALUES ('abc'); -- fails +ERROR: insert or update on table "fk_collation_fk" violates foreign key constraint "fk_collation_fkey" +DETAIL: Key (x)=(abc) is not present in table "fk_collation_pk". +DROP TABLE fk_collation_fk, fk_collation_pk; -- foreign key actions -- Some of the behaviors are most easily visible with a -- case-insensitive collation. diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index df57ebc8bc8..d4915c5b14b 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -1115,6 +1115,40 @@ CREATE TABLE test10fk (x text COLLATE case_insensitive REFERENCES test10pk (x) O CREATE TABLE test11pk (x text COLLATE case_insensitive PRIMARY KEY); CREATE TABLE test11fk (x text COLLATE case_sensitive REFERENCES test11pk (x) ON UPDATE CASCADE ON DELETE CASCADE); -- error +-- The referenced index's collation can differ from the column's collation. +-- Use reversed index columns to exercise the index-to-table column mapping. +CREATE TABLE fk_collation_pk (id int, x text COLLATE case_insensitive); +CREATE UNIQUE INDEX fk_collation_idx ON fk_collation_pk (x COLLATE "C", id); +INSERT INTO fk_collation_pk VALUES (1, 'ABC'); +CREATE TABLE fk_collation_fk (id int, x text COLLATE case_insensitive); +INSERT INTO fk_collation_fk VALUES (1, 'abc'); +-- Lack of SELECT on the PK table forces per-row validation. +CREATE ROLE regress_fk_collation; +GRANT USAGE ON SCHEMA collate_tests TO regress_fk_collation; +GRANT REFERENCES ON fk_collation_pk TO regress_fk_collation; +ALTER TABLE fk_collation_fk OWNER TO regress_fk_collation; +SET ROLE regress_fk_collation; +ALTER TABLE fk_collation_fk ADD CONSTRAINT fk_collation_fkey + FOREIGN KEY (id, x) REFERENCES fk_collation_pk (id, x); +-- Ordinary DML must also use the column's case-insensitive equality. +INSERT INTO fk_collation_fk VALUES (1, 'aBc'); +INSERT INTO fk_collation_fk VALUES (2, 'abc'); -- fails +RESET ROLE; +DROP TABLE fk_collation_fk, fk_collation_pk; +REVOKE USAGE ON SCHEMA collate_tests FROM regress_fk_collation; +DROP ROLE regress_fk_collation; + +-- Conversely, a case-insensitive index must not make the FK comparison +-- accept unequal values under the column's deterministic collation. +CREATE TABLE fk_collation_pk (x text COLLATE "C"); +CREATE UNIQUE INDEX fk_collation_idx ON fk_collation_pk (x COLLATE case_insensitive); +INSERT INTO fk_collation_pk VALUES ('ABC'); +CREATE TABLE fk_collation_fk (x text COLLATE "C", + CONSTRAINT fk_collation_fkey FOREIGN KEY (x) REFERENCES fk_collation_pk (x)); +INSERT INTO fk_collation_fk VALUES ('ABC'); +INSERT INTO fk_collation_fk VALUES ('abc'); -- fails +DROP TABLE fk_collation_fk, fk_collation_pk; + -- foreign key actions -- Some of the behaviors are most easily visible with a -- case-insensitive collation. diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e11d3ae290b..ad4cc50faa2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2493,6 +2493,7 @@ RIX RI_CompareHashEntry RI_CompareKey RI_ConstraintInfo +RI_FastPathState RI_QueryHashEntry RI_QueryKey RTEKind