mirror of
https://github.com/postgres/postgres.git
synced 2026-10-23 13:29:09 +03:00
Fall back to SPI for RI checks with mismatched index collations
The fast path probes using the referenced index's collation. When that collation differs from the referenced column's, it can reject valid references or accept invalid ones. Changing the scan key's collation is not sufficient, since btree navigation must use the ordering under which the index was built. Cache index eligibility lazily after locking the referenced table, reloading the constraint information, and opening the index. Compare index and column collations using the held relation descriptors, and fall back to SPI on a mismatch. Also move the btree eligibility check here, avoiding a lookup of an index that REINDEX CONCURRENTLY could drop before the referenced-table lock is acquired. Cache both acceptance and rejection until the constraint information is reloaded, so subsequent rows need not repeat the comparisons. Check before probing a row, and let the caller use SPI if the index is unsuitable. Add ICU regression coverage for per-row validation, ordinary inserts, reordered index columns, and both directions of collation mismatch. Reported-by: Amit Kapila <amit.kapila16@gmail.com> Discussion: https://postgr.es/m/CAA4eK1Lk1DfgiFPL-HccZm-Nm+exg+FcsODH8KPw6BOBuzfHtA@mail.gmail.com Backpatch-through: 19
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2493,6 +2493,7 @@ RIX
|
||||
RI_CompareHashEntry
|
||||
RI_CompareKey
|
||||
RI_ConstraintInfo
|
||||
RI_FastPathState
|
||||
RI_QueryHashEntry
|
||||
RI_QueryKey
|
||||
RTEKind
|
||||
|
||||
Reference in New Issue
Block a user