mirror of
https://github.com/postgres/postgres.git
synced 2026-10-23 13:29:09 +03:00
Use SPI for RI checks whose FK values need a cast function
When the FK column's type needs a cast function to reach the equality operator's input type, the RI fast path calls that function directly. The SPI path instead applies the cast as an expression in its query, which takes care of the function's declared arguments, its input collation, a NULL result, volatility, and replanning when the function changes. The direct call doesn't cover all of that; for example, a NULL result raises an internal error rather than a foreign key violation, and if the cast's function is dropped and replaced, later checks fail with "cache lookup failed for function". Coercions that aren't a call to one function, array coercion and coercion via I/O, fail outright with "no conversion function". Rather than replicate expression evaluation, leave FK values that aren't binary-coercible to the operator's input type to SPI. The fast path keeps handling cross-type operators, domains and binary-compatible types. As eligibility now depends on pg_cast, invalidate cached constraint information on pg_cast changes too. Because the check needs catalog lookups, it is made in ri_check_fastpath_index(), which caches its result in fastpath_state until the constraint's cached information is invalidated, rather than for every row in ri_fastpath_is_applicable(). The two tests that used a cast function to exercise fast-path code that still exists (metadata invalidation and the active snapshot) now use the equality function of a custom opclass, the only user code a fast-path check still runs. The fast-path code that calls cast functions is now unused; the next commit removes it. Reported-by: Nikolay Samokhvalov <nik@postgres.ai> Reported-by: Amit Langote <amitlangote09@gmail.com> Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com> Discussion: https://postgr.es/m/CAM527d9BgPjeOOYmbCBTd57R145qHCk-dzw9qNq+nOrDq1j__A@mail.gmail.com Backpatch-through: 19
This commit is contained in:
@@ -304,7 +304,8 @@ static bool ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap,
|
|||||||
bool *concurrently_updated);
|
bool *concurrently_updated);
|
||||||
static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo);
|
static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo);
|
||||||
static bool ri_check_fastpath_index(RI_ConstraintInfo *riinfo,
|
static bool ri_check_fastpath_index(RI_ConstraintInfo *riinfo,
|
||||||
Relation pk_rel, Relation idx_rel);
|
Relation fk_rel, Relation pk_rel,
|
||||||
|
Relation idx_rel);
|
||||||
static void ri_CheckPermissions(const RI_ConstraintInfo *riinfo,
|
static void ri_CheckPermissions(const RI_ConstraintInfo *riinfo,
|
||||||
Relation query_rel);
|
Relation query_rel);
|
||||||
static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys,
|
static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys,
|
||||||
@@ -435,8 +436,9 @@ RI_FKey_check(TriggerData *trigdata)
|
|||||||
* the per-row executor overhead.
|
* the per-row executor overhead.
|
||||||
*
|
*
|
||||||
* ri_FastPathCheck() reports the violation itself if no matching PK row
|
* ri_FastPathCheck() reports the violation itself if no matching PK row
|
||||||
* is found. It returns false if the index checks made after opening the
|
* is found. It returns false if the checks made after opening the
|
||||||
* relations require a SPI fallback.
|
* relations, on the index and on whether the FK values need a coercion to
|
||||||
|
* reach the equality operator's input type, require a SPI fallback.
|
||||||
*/
|
*/
|
||||||
if (ri_fastpath_is_applicable(riinfo) &&
|
if (ri_fastpath_is_applicable(riinfo) &&
|
||||||
ri_FastPathCheck(riinfo, fk_rel, newslot))
|
ri_FastPathCheck(riinfo, fk_rel, newslot))
|
||||||
@@ -2488,7 +2490,7 @@ get_ri_constraint_root(Oid constrOid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Callback for pg_constraint and pg_amop inval events
|
* Callback for pg_constraint, pg_amop and pg_cast inval events
|
||||||
*
|
*
|
||||||
* While most syscache callbacks just flush all their entries, pg_constraint
|
* While most syscache callbacks just flush all their entries, pg_constraint
|
||||||
* gets enough update traffic that it's probably worth being smarter.
|
* gets enough update traffic that it's probably worth being smarter.
|
||||||
@@ -2524,6 +2526,14 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid,
|
|||||||
if (cacheid == AMOPOPID)
|
if (cacheid == AMOPOPID)
|
||||||
hashvalue = 0;
|
hashvalue = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Likewise for pg_cast: whether a constraint's FK values are
|
||||||
|
* binary-coercible to the operator's input type, which decides fast-path
|
||||||
|
* eligibility, can depend on a binary cast, which may be replaced.
|
||||||
|
*/
|
||||||
|
if (cacheid == CASTSOURCETARGET)
|
||||||
|
hashvalue = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the list of currently valid entries gets excessively large, we mark
|
* If the list of currently valid entries gets excessively large, we mark
|
||||||
* them all invalid so we can empty the list. This arrangement avoids
|
* them all invalid so we can empty the list. This arrangement avoids
|
||||||
@@ -2813,7 +2823,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo,
|
|||||||
|
|
||||||
idx_rel = index_open(riinfo->conindid, AccessShareLock);
|
idx_rel = index_open(riinfo->conindid, AccessShareLock);
|
||||||
|
|
||||||
if (!ri_check_fastpath_index(riinfo, pk_rel, idx_rel))
|
if (!ri_check_fastpath_index(riinfo, fk_rel, pk_rel, idx_rel))
|
||||||
{
|
{
|
||||||
index_close(idx_rel, NoLock);
|
index_close(idx_rel, NoLock);
|
||||||
table_close(pk_rel, NoLock);
|
table_close(pk_rel, NoLock);
|
||||||
@@ -3017,8 +3027,11 @@ ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Apply the checks that do not require opening the referenced index. An
|
* Apply the cheap checks that depend only on riinfo; this runs for every
|
||||||
* unknown state still needs ri_check_fastpath_index() before using it.
|
* row. Checks that need the referenced index or catalog lookups are made
|
||||||
|
* once by ri_check_fastpath_index(), which caches the result in
|
||||||
|
* fastpath_state; an unknown state still needs it before using the fast
|
||||||
|
* path.
|
||||||
*/
|
*/
|
||||||
static bool
|
static bool
|
||||||
ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo)
|
ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo)
|
||||||
@@ -3043,16 +3056,18 @@ ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check index-dependent eligibility lazily, like fast-path scan metadata.
|
* Check eligibility that depends on the index or on the FK column types
|
||||||
* Cache both success and failure until the constraint information is reloaded.
|
* 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 caller has locked the referenced table, reloaded conindid, and opened
|
||||||
* the index. Looking up index properties in ri_LoadConstraintInfo() would
|
* the index. Looking up index properties in ri_LoadConstraintInfo() would
|
||||||
* race with REINDEX CONCURRENTLY dropping an index read before that lock.
|
* race with REINDEX CONCURRENTLY dropping an index read before that lock.
|
||||||
* The index-property checks use the held relation descriptors.
|
* The index-property checks use the held relation descriptors, and the check
|
||||||
|
* of the FK column types uses fk_rel.
|
||||||
*/
|
*/
|
||||||
static bool
|
static bool
|
||||||
ri_check_fastpath_index(RI_ConstraintInfo *riinfo,
|
ri_check_fastpath_index(RI_ConstraintInfo *riinfo, Relation fk_rel,
|
||||||
Relation pk_rel, Relation idx_rel)
|
Relation pk_rel, Relation idx_rel)
|
||||||
{
|
{
|
||||||
/* Opening the index can have processed further invalidations. */
|
/* Opening the index can have processed further invalidations. */
|
||||||
@@ -3118,6 +3133,27 @@ ri_check_fastpath_index(RI_ConstraintInfo *riinfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The fast path passes FK values to the equality operator as they are, so
|
||||||
|
* it needs the FK type to be binary-coercible to the operator's right
|
||||||
|
* input type. That covers cross-type operators, which take the FK type
|
||||||
|
* directly, domains, and binary-compatible types. Any other coercion is
|
||||||
|
* left to SPI, which applies it as part of the query.
|
||||||
|
*/
|
||||||
|
for (int i = 0; i < riinfo->nkeys; i++)
|
||||||
|
{
|
||||||
|
Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
|
||||||
|
Oid lefttype;
|
||||||
|
Oid righttype;
|
||||||
|
|
||||||
|
op_input_types(riinfo->pf_eq_oprs[i], &lefttype, &righttype);
|
||||||
|
if (!IsBinaryCoercible(fk_type, righttype))
|
||||||
|
{
|
||||||
|
riinfo->fastpath_state = RI_FASTPATH_UNUSABLE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
riinfo->fastpath_state = RI_FASTPATH_USABLE;
|
riinfo->fastpath_state = RI_FASTPATH_USABLE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3235,9 +3271,10 @@ recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys,
|
|||||||
*
|
*
|
||||||
* This parallels the checks ExecInitFunc() performs when the SPI path
|
* This parallels the checks ExecInitFunc() performs when the SPI path
|
||||||
* initializes its generated query, where the equality operator's function
|
* initializes its generated query, where the equality operator's function
|
||||||
* appears in the WHERE clause and the cast function, if any, in the cast
|
* appears in the WHERE clause. The fast path is not used when the FK values
|
||||||
* applied to the parameter. Call with the user id already switched to the
|
* need coercion (see ri_check_fastpath_index()), so that is the only function
|
||||||
* referenced table's owner.
|
* to check. Call with the user id already switched to the referenced table's
|
||||||
|
* owner.
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo,
|
ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo,
|
||||||
@@ -3633,13 +3670,16 @@ ri_InitHashTables(void)
|
|||||||
RI_INIT_CONSTRAINTHASHSIZE,
|
RI_INIT_CONSTRAINTHASHSIZE,
|
||||||
&ctl, HASH_ELEM | HASH_BLOBS);
|
&ctl, HASH_ELEM | HASH_BLOBS);
|
||||||
|
|
||||||
/* Arrange to flush cache on pg_constraint or pg_amop changes */
|
/* Arrange to flush cache on pg_constraint, pg_amop or pg_cast changes */
|
||||||
CacheRegisterSyscacheCallback(CONSTROID,
|
CacheRegisterSyscacheCallback(CONSTROID,
|
||||||
InvalidateConstraintCacheCallBack,
|
InvalidateConstraintCacheCallBack,
|
||||||
(Datum) 0);
|
(Datum) 0);
|
||||||
CacheRegisterSyscacheCallback(AMOPOPID,
|
CacheRegisterSyscacheCallback(AMOPOPID,
|
||||||
InvalidateConstraintCacheCallBack,
|
InvalidateConstraintCacheCallBack,
|
||||||
(Datum) 0);
|
(Datum) 0);
|
||||||
|
CacheRegisterSyscacheCallback(CASTSOURCETARGET,
|
||||||
|
InvalidateConstraintCacheCallBack,
|
||||||
|
(Datum) 0);
|
||||||
|
|
||||||
ctl.keysize = sizeof(RI_QueryKey);
|
ctl.keysize = sizeof(RI_QueryKey);
|
||||||
ctl.entrysize = sizeof(RI_QueryHashEntry);
|
ctl.entrysize = sizeof(RI_QueryHashEntry);
|
||||||
|
|||||||
@@ -3744,11 +3744,11 @@ DETAIL: Key (a)=(3) is not present in table "fp_pk_defer".
|
|||||||
COMMIT;
|
COMMIT;
|
||||||
DROP TABLE fp_pk_defer, fp_fk_defer;
|
DROP TABLE fp_pk_defer, fp_fk_defer;
|
||||||
-- A deferred FK check queued while firing deferred triggers at commit must
|
-- A deferred FK check queued while firing deferred triggers at commit must
|
||||||
-- not be lost. The RI fast-path runs during the deferred firing loop
|
-- not be lost. An RI check runs during the deferred firing loop and can run
|
||||||
-- and can run user cast/equality code whose DML queues a further deferred
|
-- user cast code whose DML queues a further deferred check; that check must
|
||||||
-- check; that check must still fire. fk_defer_main's deferred fast-path check
|
-- still fire. fk_defer_main's deferred check runs a cast whose function
|
||||||
-- runs a cast whose function inserts a violating row into fk_defer_t2,
|
-- inserts a violating row into fk_defer_t2, queueing fk_defer_t2's own
|
||||||
-- queueing fk_defer_t2's own deferred check, which must still be reported.
|
-- deferred check, which must still be reported.
|
||||||
CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY);
|
CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY);
|
||||||
CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id)
|
CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id)
|
||||||
DEFERRABLE INITIALLY DEFERRED);
|
DEFERRABLE INITIALLY DEFERRED);
|
||||||
@@ -3801,11 +3801,13 @@ NOTICE: fp_auto_pk called
|
|||||||
NOTICE: fp_auto_pk called
|
NOTICE: fp_auto_pk called
|
||||||
DROP TABLE fp_fk_cci, fp_pk_cci;
|
DROP TABLE fp_fk_cci, fp_pk_cci;
|
||||||
DROP FUNCTION fp_auto_pk;
|
DROP FUNCTION fp_auto_pk;
|
||||||
-- The fast path must check EXECUTE, as the referenced table's owner, on the
|
-- An FK check must check EXECUTE, as the referenced table's owner, on the
|
||||||
-- functions it invokes on the FK values: the equality operator's function
|
-- functions it applies to the FK values: the equality operator's function
|
||||||
-- and, for a cross-type FK, the implicit cast function. Validation of a new
|
-- and, for an FK needing an implicit cast function, the cast. Validation of
|
||||||
-- constraint by a role that cannot use the bulk check (RLS is enabled on the
|
-- a new constraint by a role that cannot use the bulk check (RLS is enabled
|
||||||
-- referenced table) runs per-row checks through the fast path.
|
-- on the referenced table) runs per-row checks, through the fast path for
|
||||||
|
-- the operator case and through SPI for the cast case, since FK values that
|
||||||
|
-- need a cast function are not checked by the fast path.
|
||||||
--
|
--
|
||||||
-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere
|
-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere
|
||||||
-- with nearby tests.
|
-- with nearby tests.
|
||||||
@@ -3865,8 +3867,9 @@ ERROR: permission denied for function regress_ri_int4eq
|
|||||||
ROLLBACK TO SAVEPOINT s;
|
ROLLBACK TO SAVEPOINT s;
|
||||||
SAVEPOINT s;
|
SAVEPOINT s;
|
||||||
ALTER TABLE fktable_acl_cast
|
ALTER TABLE fktable_acl_cast
|
||||||
ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails
|
ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails (SPI path message)
|
||||||
ERROR: permission denied for function fkenum_to_int4
|
ERROR: permission denied for function fkenum_to_int4
|
||||||
|
CONTEXT: SQL statement "SELECT 1 FROM ONLY "public"."pktable_acl_cast" x WHERE "a" OPERATOR(pg_catalog.=) $1::pg_catalog.int4 FOR KEY SHARE OF x"
|
||||||
ROLLBACK TO SAVEPOINT s;
|
ROLLBACK TO SAVEPOINT s;
|
||||||
SAVEPOINT s;
|
SAVEPOINT s;
|
||||||
ALTER TABLE fktable_acl_op
|
ALTER TABLE fktable_acl_op
|
||||||
@@ -3882,36 +3885,39 @@ CONTEXT: SQL statement "SELECT 1 FROM "public"."pktable_acl_cast_part" x WHERE
|
|||||||
ROLLBACK TO SAVEPOINT s;
|
ROLLBACK TO SAVEPOINT s;
|
||||||
RESET ROLE;
|
RESET ROLE;
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
-- A STABLE cast used by an FK check must see changes made by earlier AFTER
|
-- A STABLE equality function used by an FK check must see changes made by
|
||||||
-- triggers, using the check's snapshot rather than the outer query's snapshot.
|
-- earlier AFTER triggers, using the check's snapshot rather than the outer
|
||||||
-- Compare the per-row fast path with a partitioned-parent SPI check.
|
-- query's snapshot. Compare the fast path (pk_fast) with a partitioned-parent
|
||||||
|
-- SPI check (pk_spi). The equality function of the referenced index's
|
||||||
|
-- opclass is the user code that runs during a fast-path check.
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE SCHEMA ri_snapshot;
|
CREATE SCHEMA ri_snapshot;
|
||||||
SET LOCAL search_path = ri_snapshot, pg_catalog;
|
SET LOCAL search_path = ri_snapshot, pg_catalog;
|
||||||
CREATE TYPE lookup_key AS (v int);
|
|
||||||
CREATE TABLE lookup_rows (v int);
|
CREATE TABLE lookup_rows (v int);
|
||||||
CREATE FUNCTION lookup_key_to_int(k lookup_key) RETURNS int
|
-- Considers two values equal only if a lookup row exists for them.
|
||||||
|
CREATE FUNCTION lookup_eq(a int, b int) RETURNS bool
|
||||||
LANGUAGE plpgsql STABLE AS $$
|
LANGUAGE plpgsql STABLE AS $$
|
||||||
DECLARE
|
|
||||||
result int;
|
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT r.v INTO result FROM lookup_rows AS r WHERE r.v = k.v;
|
RETURN a = b AND EXISTS (SELECT 1 FROM lookup_rows AS r WHERE r.v = a);
|
||||||
RETURN coalesce(result, -1);
|
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
CREATE CAST (lookup_key AS int)
|
CREATE OPERATOR === (LEFTARG = int, RIGHTARG = int, FUNCTION = lookup_eq);
|
||||||
WITH FUNCTION lookup_key_to_int(lookup_key) AS IMPLICIT;
|
CREATE OPERATOR CLASS lookup_ops FOR TYPE int USING btree AS
|
||||||
CREATE TABLE pk_fast (v int PRIMARY KEY);
|
OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >,
|
||||||
CREATE TABLE pk_spi (v int PRIMARY KEY) PARTITION BY RANGE (v);
|
FUNCTION 1 btint4cmp(int, int);
|
||||||
|
CREATE TABLE pk_fast (v int);
|
||||||
|
CREATE UNIQUE INDEX ON pk_fast (v lookup_ops);
|
||||||
|
CREATE TABLE pk_spi (v int) PARTITION BY RANGE (v lookup_ops);
|
||||||
CREATE TABLE pk_spi_p PARTITION OF pk_spi
|
CREATE TABLE pk_spi_p PARTITION OF pk_spi
|
||||||
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
|
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
|
||||||
|
CREATE UNIQUE INDEX ON pk_spi (v lookup_ops);
|
||||||
INSERT INTO pk_fast VALUES (1);
|
INSERT INTO pk_fast VALUES (1);
|
||||||
INSERT INTO pk_spi VALUES (1);
|
INSERT INTO pk_spi VALUES (1);
|
||||||
CREATE TABLE fk_fast (k lookup_key REFERENCES pk_fast(v));
|
CREATE TABLE fk_fast (k int REFERENCES pk_fast(v));
|
||||||
CREATE TABLE fk_spi (k lookup_key REFERENCES pk_spi(v));
|
CREATE TABLE fk_spi (k int REFERENCES pk_spi(v));
|
||||||
CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$
|
CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO lookup_rows VALUES ((NEW.k).v);
|
INSERT INTO lookup_rows VALUES (NEW.k);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -3920,22 +3926,9 @@ CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_fast
|
|||||||
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
||||||
CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi
|
CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi
|
||||||
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
||||||
-- Invoke the fast-path check from another FK's cast. Even with batching
|
-- Both checks must succeed. Without the check's snapshot active, the
|
||||||
-- enabled, a check nested inside the end-of-batch flush takes the per-row path.
|
-- STABLE equality function misses the lookup row, so no PK row matches.
|
||||||
CREATE TYPE driver_key AS (v int);
|
INSERT INTO fk_fast VALUES (1);
|
||||||
CREATE FUNCTION driver_key_to_int(k driver_key) RETURNS int
|
|
||||||
LANGUAGE plpgsql VOLATILE AS $$
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO fk_fast VALUES (ROW(k.v)::lookup_key);
|
|
||||||
RETURN k.v;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
CREATE CAST (driver_key AS int)
|
|
||||||
WITH FUNCTION driver_key_to_int(driver_key) AS IMPLICIT;
|
|
||||||
CREATE TABLE driver (k driver_key REFERENCES pk_fast(v));
|
|
||||||
-- Both checks must succeed. Without an active snapshot for the per-row
|
|
||||||
-- check, its STABLE cast misses the lookup row and returns -1 instead of 1.
|
|
||||||
INSERT INTO driver VALUES (ROW(1)::driver_key);
|
|
||||||
SELECT count(*) FROM fk_fast;
|
SELECT count(*) FROM fk_fast;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
@@ -3943,7 +3936,7 @@ SELECT count(*) FROM fk_fast;
|
|||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
DELETE FROM lookup_rows;
|
DELETE FROM lookup_rows;
|
||||||
INSERT INTO fk_spi VALUES (ROW(1)::lookup_key);
|
INSERT INTO fk_spi VALUES (1);
|
||||||
SELECT count(*) FROM fk_spi;
|
SELECT count(*) FROM fk_spi;
|
||||||
count
|
count
|
||||||
-------
|
-------
|
||||||
@@ -4028,7 +4021,7 @@ DETAIL: Key (a)=(999) is not present in table "fp_pk_dom".
|
|||||||
INSERT INTO fp_fk_dom VALUES (NULL);
|
INSERT INTO fp_fk_dom VALUES (NULL);
|
||||||
DROP TABLE fp_fk_dom, fp_pk_dom;
|
DROP TABLE fp_fk_dom, fp_pk_dom;
|
||||||
DROP DOMAIN fp_int8dom;
|
DROP DOMAIN fp_int8dom;
|
||||||
-- Re-entrant FK fast-path: DML on the same FK table from a cast function
|
-- Re-entrant FK check: DML on the same FK table from a cast function
|
||||||
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
||||||
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
||||||
CREATE TYPE fp_vch AS (v int);
|
CREATE TYPE fp_vch AS (v int);
|
||||||
@@ -4058,25 +4051,17 @@ DROP TYPE fp_vch;
|
|||||||
--
|
--
|
||||||
-- Cache invalidation arriving during a fast-path check.
|
-- Cache invalidation arriving during a fast-path check.
|
||||||
--
|
--
|
||||||
-- A cross-type foreign key runs the user's cast function while building its
|
-- The equality function of the referenced index's opclass runs while the
|
||||||
-- index scan keys. A cast that performs DDL raises an invalidation there,
|
-- fast path scans the index. One that performs DDL raises an invalidation
|
||||||
-- detaching the constraint's fast-path metadata while the check is still
|
-- there, detaching the constraint's fast-path metadata while the check is
|
||||||
-- using it.
|
-- still using it. This is done in a transaction that is rolled back, so that
|
||||||
|
-- other tests never see the operator class.
|
||||||
--
|
--
|
||||||
CREATE TYPE fkint;
|
BEGIN;
|
||||||
CREATE FUNCTION fkint_in(cstring) RETURNS fkint
|
|
||||||
AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
|
|
||||||
NOTICE: return type fkint is only a shell
|
|
||||||
CREATE FUNCTION fkint_out(fkint) RETURNS cstring
|
|
||||||
AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
|
|
||||||
NOTICE: argument type fkint is only a shell
|
|
||||||
LINE 1: CREATE FUNCTION fkint_out(fkint) RETURNS cstring
|
|
||||||
^
|
|
||||||
CREATE TYPE fkint (INPUT = fkint_in, OUTPUT = fkint_out, LIKE = int4);
|
|
||||||
-- Renames the constraint the first time it is called, and so raises an
|
-- Renames the constraint the first time it is called, and so raises an
|
||||||
-- invalidation partway through the check. Guarded on the catalog so the
|
-- invalidation partway through the check. Guarded on the catalog so the
|
||||||
-- second and later calls are no-ops.
|
-- second and later calls are no-ops.
|
||||||
CREATE FUNCTION fkint_to_int4(fkint) RETURNS int4 AS $$
|
CREATE FUNCTION fp_inval_eq(int4, int4) RETURNS bool AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM pg_constraint
|
IF EXISTS (SELECT 1 FROM pg_constraint
|
||||||
WHERE conrelid = 'fktable_inval'::regclass
|
WHERE conrelid = 'fktable_inval'::regclass
|
||||||
@@ -4084,20 +4069,25 @@ BEGIN
|
|||||||
EXECUTE 'ALTER TABLE fktable_inval'
|
EXECUTE 'ALTER TABLE fktable_inval'
|
||||||
' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2';
|
' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2';
|
||||||
END IF;
|
END IF;
|
||||||
RETURN format('%s', $1)::int4;
|
RETURN $1 OPERATOR(pg_catalog.=) $2;
|
||||||
END $$ LANGUAGE plpgsql;
|
END $$ LANGUAGE plpgsql;
|
||||||
CREATE CAST (fkint AS int4) WITH FUNCTION fkint_to_int4(fkint) AS IMPLICIT;
|
CREATE OPERATOR =@= (LEFTARG = int4, RIGHTARG = int4, FUNCTION = fp_inval_eq);
|
||||||
CREATE TABLE pktable_inval (a int4, b int4, PRIMARY KEY (a, b));
|
CREATE OPERATOR CLASS fp_inval_ops FOR TYPE int4 USING btree AS
|
||||||
|
OPERATOR 1 pg_catalog.<, OPERATOR 2 pg_catalog.<=, OPERATOR 3 =@=,
|
||||||
|
OPERATOR 4 pg_catalog.>=, OPERATOR 5 pg_catalog.>,
|
||||||
|
FUNCTION 1 btint4cmp(int4, int4);
|
||||||
|
CREATE TABLE pktable_inval (a int4, b int4);
|
||||||
|
CREATE UNIQUE INDEX pktable_inval_a_b ON pktable_inval (a fp_inval_ops, b);
|
||||||
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
||||||
-- Cross-type on column a, so the cast above is invoked.
|
CREATE TABLE fktable_inval (a int4, b int4,
|
||||||
CREATE TABLE fktable_inval (a fkint, b int4,
|
|
||||||
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
||||||
REFERENCES pktable_inval (a, b));
|
REFERENCES pktable_inval (a, b));
|
||||||
-- The first row invalidates the metadata; the second check must repopulate
|
-- The first row invalidates the metadata; the second check must repopulate
|
||||||
-- and use it safely.
|
-- and use it safely.
|
||||||
INSERT INTO fktable_inval VALUES ('1', 1), ('2', 2);
|
INSERT INTO fktable_inval VALUES (1, 1), (2, 2);
|
||||||
-- Confirms the cast actually ran and raised the invalidation. Without this
|
-- Confirms the equality function actually ran and raised the invalidation.
|
||||||
-- the insert above could pass merely by not exercising the path at all.
|
-- Without this the insert above could pass merely by not exercising the path
|
||||||
|
-- at all.
|
||||||
SELECT conname FROM pg_constraint
|
SELECT conname FROM pg_constraint
|
||||||
WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f';
|
WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f';
|
||||||
conname
|
conname
|
||||||
@@ -4111,14 +4101,7 @@ SELECT count(*) FROM fktable_inval;
|
|||||||
2
|
2
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
DROP TABLE fktable_inval;
|
ROLLBACK;
|
||||||
DROP TABLE pktable_inval;
|
|
||||||
DROP CAST (fkint AS int4);
|
|
||||||
DROP FUNCTION fkint_to_int4(fkint);
|
|
||||||
DROP TYPE fkint CASCADE;
|
|
||||||
NOTICE: drop cascades to 2 other objects
|
|
||||||
DETAIL: drop cascades to function fkint_in(cstring)
|
|
||||||
drop cascades to function fkint_out(fkint)
|
|
||||||
-- An FK check locks the referenced row, which a read-only transaction does
|
-- An FK check locks the referenced row, which a read-only transaction does
|
||||||
-- not allow unless the referenced table is temporary.
|
-- not allow unless the referenced table is temporary.
|
||||||
CREATE TABLE fp_pk_ro (a int PRIMARY KEY);
|
CREATE TABLE fp_pk_ro (a int PRIMARY KEY);
|
||||||
@@ -4180,3 +4163,107 @@ ERROR: insert or update on table "fp_fk_cid" violates foreign key constraint "f
|
|||||||
DETAIL: Key (a)=(1) is not present in table "fp_pk_cid".
|
DETAIL: Key (a)=(1) is not present in table "fp_pk_cid".
|
||||||
ROLLBACK TO SAVEPOINT fp_cid;
|
ROLLBACK TO SAVEPOINT fp_cid;
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
|
--
|
||||||
|
-- FK values that need a coercion to reach the equality operator's input type
|
||||||
|
-- are checked through SPI, which applies the coercion as part of the query.
|
||||||
|
-- The fast path used to call the cast function directly.
|
||||||
|
--
|
||||||
|
BEGIN;
|
||||||
|
CREATE SCHEMA ri_cast;
|
||||||
|
SET LOCAL search_path = ri_cast, pg_catalog;
|
||||||
|
-- A cast that returns NULL for a non-NULL key: an FK violation, as for any
|
||||||
|
-- other key without a match, not an internal error.
|
||||||
|
CREATE TYPE ri_word AS ENUM ('one', 'none');
|
||||||
|
CREATE FUNCTION ri_word_to_int4(ri_word) RETURNS int4
|
||||||
|
LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN $1::text = 'one' THEN 1 END $$;
|
||||||
|
CREATE CAST (ri_word AS int4) WITH FUNCTION ri_word_to_int4(ri_word) AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_pk (id int4 PRIMARY KEY);
|
||||||
|
INSERT INTO ri_pk VALUES (1);
|
||||||
|
CREATE TABLE ri_fk (a ri_word REFERENCES ri_pk);
|
||||||
|
INSERT INTO ri_fk VALUES ('one');
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk VALUES ('none'); -- fails
|
||||||
|
ERROR: insert or update on table "ri_fk" violates foreign key constraint "ri_fk_a_fkey"
|
||||||
|
DETAIL: Key (a)=(none) is not present in table "ri_pk".
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
-- Replacing the cast and dropping its old function must not leave a later
|
||||||
|
-- check in this session calling the dropped function.
|
||||||
|
CREATE FUNCTION ri_word_to_int4_v2(ri_word) RETURNS int4
|
||||||
|
LANGUAGE sql IMMUTABLE AS $$ SELECT 1 $$;
|
||||||
|
DROP CAST (ri_word AS int4);
|
||||||
|
CREATE CAST (ri_word AS int4) WITH FUNCTION ri_word_to_int4_v2(ri_word) AS IMPLICIT;
|
||||||
|
DROP FUNCTION ri_word_to_int4(ri_word);
|
||||||
|
INSERT INTO ri_fk VALUES ('none'); -- succeeds through the new cast
|
||||||
|
-- A collation-sensitive cast from a collatable type gets the input
|
||||||
|
-- collation an expression would give it.
|
||||||
|
CREATE TYPE ri_ctext;
|
||||||
|
CREATE FUNCTION ri_ctext_in(cstring) RETURNS ri_ctext
|
||||||
|
AS 'textin' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
NOTICE: return type ri_ctext is only a shell
|
||||||
|
CREATE FUNCTION ri_ctext_out(ri_ctext) RETURNS cstring
|
||||||
|
AS 'textout' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
NOTICE: argument type ri_ctext is only a shell
|
||||||
|
LINE 1: CREATE FUNCTION ri_ctext_out(ri_ctext) RETURNS cstring
|
||||||
|
^
|
||||||
|
CREATE TYPE ri_ctext (INPUT = ri_ctext_in, OUTPUT = ri_ctext_out,
|
||||||
|
LIKE = text, COLLATABLE = true);
|
||||||
|
CREATE FUNCTION ri_ctext_lower(ri_ctext) RETURNS text
|
||||||
|
AS 'lower' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE CAST (ri_ctext AS text) WITH FUNCTION ri_ctext_lower(ri_ctext) AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_pk_text (k text PRIMARY KEY);
|
||||||
|
INSERT INTO ri_pk_text VALUES ('abc');
|
||||||
|
CREATE TABLE ri_fk_text (a ri_ctext REFERENCES ri_pk_text);
|
||||||
|
INSERT INTO ri_fk_text VALUES ('ABC'); -- succeeds; the cast lowercases
|
||||||
|
-- An array coercion (int2[] to int4[], allowed with an opclass declared for
|
||||||
|
-- int4[]) has no single cast function to call.
|
||||||
|
CREATE FUNCTION ri_i4a_lt(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_lt' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_le(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_le' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_eq(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_eq' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_ge(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_ge' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_gt(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_gt' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_cmp(int4[], int4[]) RETURNS int4
|
||||||
|
AS 'btarraycmp' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE OPERATOR ri_cast.<<< (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_lt);
|
||||||
|
CREATE OPERATOR ri_cast.<<= (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_le);
|
||||||
|
CREATE OPERATOR ri_cast.=== (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_eq);
|
||||||
|
CREATE OPERATOR ri_cast.>>= (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_ge);
|
||||||
|
CREATE OPERATOR ri_cast.>>> (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_gt);
|
||||||
|
CREATE OPERATOR CLASS ri_i4a_ops FOR TYPE int4[] USING btree AS
|
||||||
|
OPERATOR 1 ri_cast.<<<, OPERATOR 2 ri_cast.<<=, OPERATOR 3 ri_cast.===,
|
||||||
|
OPERATOR 4 ri_cast.>>=, OPERATOR 5 ri_cast.>>>,
|
||||||
|
FUNCTION 1 ri_i4a_cmp(int4[], int4[]);
|
||||||
|
CREATE TABLE ri_pk_arr (k int4[]);
|
||||||
|
CREATE UNIQUE INDEX ON ri_pk_arr (k ri_i4a_ops);
|
||||||
|
INSERT INTO ri_pk_arr VALUES ('{1,2}');
|
||||||
|
CREATE TABLE ri_fk_arr (a int2[] REFERENCES ri_pk_arr (k));
|
||||||
|
INSERT INTO ri_fk_arr VALUES ('{1,2}'); -- succeeds
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk_arr VALUES ('{3}'); -- fails
|
||||||
|
ERROR: insert or update on table "ri_fk_arr" violates foreign key constraint "ri_fk_arr_a_fkey"
|
||||||
|
DETAIL: Key (a)=({3}) is not present in table "ri_pk_arr".
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
-- A coercion via I/O has no cast function to call either.
|
||||||
|
CREATE TYPE ri_ioint;
|
||||||
|
CREATE FUNCTION ri_ioint_in(cstring) RETURNS ri_ioint
|
||||||
|
AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
NOTICE: return type ri_ioint is only a shell
|
||||||
|
CREATE FUNCTION ri_ioint_out(ri_ioint) RETURNS cstring
|
||||||
|
AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
NOTICE: argument type ri_ioint is only a shell
|
||||||
|
LINE 1: CREATE FUNCTION ri_ioint_out(ri_ioint) RETURNS cstring
|
||||||
|
^
|
||||||
|
CREATE TYPE ri_ioint (INPUT = ri_ioint_in, OUTPUT = ri_ioint_out, LIKE = int4);
|
||||||
|
CREATE CAST (ri_ioint AS int4) WITH INOUT AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_fk_io (a ri_ioint REFERENCES ri_pk);
|
||||||
|
INSERT INTO ri_fk_io VALUES ('1'); -- succeeds
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk_io VALUES ('2'); -- fails
|
||||||
|
ERROR: insert or update on table "ri_fk_io" violates foreign key constraint "ri_fk_io_a_fkey"
|
||||||
|
DETAIL: Key (a)=(2) is not present in table "ri_pk".
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
ROLLBACK;
|
||||||
|
|||||||
@@ -2713,11 +2713,11 @@ COMMIT;
|
|||||||
DROP TABLE fp_pk_defer, fp_fk_defer;
|
DROP TABLE fp_pk_defer, fp_fk_defer;
|
||||||
|
|
||||||
-- A deferred FK check queued while firing deferred triggers at commit must
|
-- A deferred FK check queued while firing deferred triggers at commit must
|
||||||
-- not be lost. The RI fast-path runs during the deferred firing loop
|
-- not be lost. An RI check runs during the deferred firing loop and can run
|
||||||
-- and can run user cast/equality code whose DML queues a further deferred
|
-- user cast code whose DML queues a further deferred check; that check must
|
||||||
-- check; that check must still fire. fk_defer_main's deferred fast-path check
|
-- still fire. fk_defer_main's deferred check runs a cast whose function
|
||||||
-- runs a cast whose function inserts a violating row into fk_defer_t2,
|
-- inserts a violating row into fk_defer_t2, queueing fk_defer_t2's own
|
||||||
-- queueing fk_defer_t2's own deferred check, which must still be reported.
|
-- deferred check, which must still be reported.
|
||||||
CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY);
|
CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY);
|
||||||
CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id)
|
CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id)
|
||||||
DEFERRABLE INITIALLY DEFERRED);
|
DEFERRABLE INITIALLY DEFERRED);
|
||||||
@@ -2771,11 +2771,13 @@ INSERT INTO fp_fk_cci VALUES (1), (2), (3);
|
|||||||
DROP TABLE fp_fk_cci, fp_pk_cci;
|
DROP TABLE fp_fk_cci, fp_pk_cci;
|
||||||
DROP FUNCTION fp_auto_pk;
|
DROP FUNCTION fp_auto_pk;
|
||||||
|
|
||||||
-- The fast path must check EXECUTE, as the referenced table's owner, on the
|
-- An FK check must check EXECUTE, as the referenced table's owner, on the
|
||||||
-- functions it invokes on the FK values: the equality operator's function
|
-- functions it applies to the FK values: the equality operator's function
|
||||||
-- and, for a cross-type FK, the implicit cast function. Validation of a new
|
-- and, for an FK needing an implicit cast function, the cast. Validation of
|
||||||
-- constraint by a role that cannot use the bulk check (RLS is enabled on the
|
-- a new constraint by a role that cannot use the bulk check (RLS is enabled
|
||||||
-- referenced table) runs per-row checks through the fast path.
|
-- on the referenced table) runs per-row checks, through the fast path for
|
||||||
|
-- the operator case and through SPI for the cast case, since FK values that
|
||||||
|
-- need a cast function are not checked by the fast path.
|
||||||
--
|
--
|
||||||
-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere
|
-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere
|
||||||
-- with nearby tests.
|
-- with nearby tests.
|
||||||
@@ -2839,7 +2841,7 @@ ALTER TABLE fktable_acl_op
|
|||||||
ROLLBACK TO SAVEPOINT s;
|
ROLLBACK TO SAVEPOINT s;
|
||||||
SAVEPOINT s;
|
SAVEPOINT s;
|
||||||
ALTER TABLE fktable_acl_cast
|
ALTER TABLE fktable_acl_cast
|
||||||
ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails
|
ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails (SPI path message)
|
||||||
ROLLBACK TO SAVEPOINT s;
|
ROLLBACK TO SAVEPOINT s;
|
||||||
SAVEPOINT s;
|
SAVEPOINT s;
|
||||||
ALTER TABLE fktable_acl_op
|
ALTER TABLE fktable_acl_op
|
||||||
@@ -2852,39 +2854,42 @@ ROLLBACK TO SAVEPOINT s;
|
|||||||
RESET ROLE;
|
RESET ROLE;
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
|
|
||||||
-- A STABLE cast used by an FK check must see changes made by earlier AFTER
|
-- A STABLE equality function used by an FK check must see changes made by
|
||||||
-- triggers, using the check's snapshot rather than the outer query's snapshot.
|
-- earlier AFTER triggers, using the check's snapshot rather than the outer
|
||||||
-- Compare the per-row fast path with a partitioned-parent SPI check.
|
-- query's snapshot. Compare the fast path (pk_fast) with a partitioned-parent
|
||||||
|
-- SPI check (pk_spi). The equality function of the referenced index's
|
||||||
|
-- opclass is the user code that runs during a fast-path check.
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE SCHEMA ri_snapshot;
|
CREATE SCHEMA ri_snapshot;
|
||||||
SET LOCAL search_path = ri_snapshot, pg_catalog;
|
SET LOCAL search_path = ri_snapshot, pg_catalog;
|
||||||
|
|
||||||
CREATE TYPE lookup_key AS (v int);
|
|
||||||
CREATE TABLE lookup_rows (v int);
|
CREATE TABLE lookup_rows (v int);
|
||||||
CREATE FUNCTION lookup_key_to_int(k lookup_key) RETURNS int
|
-- Considers two values equal only if a lookup row exists for them.
|
||||||
|
CREATE FUNCTION lookup_eq(a int, b int) RETURNS bool
|
||||||
LANGUAGE plpgsql STABLE AS $$
|
LANGUAGE plpgsql STABLE AS $$
|
||||||
DECLARE
|
|
||||||
result int;
|
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT r.v INTO result FROM lookup_rows AS r WHERE r.v = k.v;
|
RETURN a = b AND EXISTS (SELECT 1 FROM lookup_rows AS r WHERE r.v = a);
|
||||||
RETURN coalesce(result, -1);
|
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
CREATE CAST (lookup_key AS int)
|
CREATE OPERATOR === (LEFTARG = int, RIGHTARG = int, FUNCTION = lookup_eq);
|
||||||
WITH FUNCTION lookup_key_to_int(lookup_key) AS IMPLICIT;
|
CREATE OPERATOR CLASS lookup_ops FOR TYPE int USING btree AS
|
||||||
|
OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >,
|
||||||
|
FUNCTION 1 btint4cmp(int, int);
|
||||||
|
|
||||||
CREATE TABLE pk_fast (v int PRIMARY KEY);
|
CREATE TABLE pk_fast (v int);
|
||||||
CREATE TABLE pk_spi (v int PRIMARY KEY) PARTITION BY RANGE (v);
|
CREATE UNIQUE INDEX ON pk_fast (v lookup_ops);
|
||||||
|
CREATE TABLE pk_spi (v int) PARTITION BY RANGE (v lookup_ops);
|
||||||
CREATE TABLE pk_spi_p PARTITION OF pk_spi
|
CREATE TABLE pk_spi_p PARTITION OF pk_spi
|
||||||
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
|
FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
|
||||||
|
CREATE UNIQUE INDEX ON pk_spi (v lookup_ops);
|
||||||
INSERT INTO pk_fast VALUES (1);
|
INSERT INTO pk_fast VALUES (1);
|
||||||
INSERT INTO pk_spi VALUES (1);
|
INSERT INTO pk_spi VALUES (1);
|
||||||
CREATE TABLE fk_fast (k lookup_key REFERENCES pk_fast(v));
|
CREATE TABLE fk_fast (k int REFERENCES pk_fast(v));
|
||||||
CREATE TABLE fk_spi (k lookup_key REFERENCES pk_spi(v));
|
CREATE TABLE fk_spi (k int REFERENCES pk_spi(v));
|
||||||
|
|
||||||
CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$
|
CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO lookup_rows VALUES ((NEW.k).v);
|
INSERT INTO lookup_rows VALUES (NEW.k);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -2894,26 +2899,12 @@ CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_fast
|
|||||||
CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi
|
CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi
|
||||||
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
FOR EACH ROW EXECUTE FUNCTION add_lookup_row();
|
||||||
|
|
||||||
-- Invoke the fast-path check from another FK's cast. Even with batching
|
-- Both checks must succeed. Without the check's snapshot active, the
|
||||||
-- enabled, a check nested inside the end-of-batch flush takes the per-row path.
|
-- STABLE equality function misses the lookup row, so no PK row matches.
|
||||||
CREATE TYPE driver_key AS (v int);
|
INSERT INTO fk_fast VALUES (1);
|
||||||
CREATE FUNCTION driver_key_to_int(k driver_key) RETURNS int
|
|
||||||
LANGUAGE plpgsql VOLATILE AS $$
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO fk_fast VALUES (ROW(k.v)::lookup_key);
|
|
||||||
RETURN k.v;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
CREATE CAST (driver_key AS int)
|
|
||||||
WITH FUNCTION driver_key_to_int(driver_key) AS IMPLICIT;
|
|
||||||
CREATE TABLE driver (k driver_key REFERENCES pk_fast(v));
|
|
||||||
|
|
||||||
-- Both checks must succeed. Without an active snapshot for the per-row
|
|
||||||
-- check, its STABLE cast misses the lookup row and returns -1 instead of 1.
|
|
||||||
INSERT INTO driver VALUES (ROW(1)::driver_key);
|
|
||||||
SELECT count(*) FROM fk_fast;
|
SELECT count(*) FROM fk_fast;
|
||||||
DELETE FROM lookup_rows;
|
DELETE FROM lookup_rows;
|
||||||
INSERT INTO fk_spi VALUES (ROW(1)::lookup_key);
|
INSERT INTO fk_spi VALUES (1);
|
||||||
SELECT count(*) FROM fk_spi;
|
SELECT count(*) FROM fk_spi;
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
|
|
||||||
@@ -2988,7 +2979,7 @@ INSERT INTO fp_fk_dom VALUES (NULL);
|
|||||||
DROP TABLE fp_fk_dom, fp_pk_dom;
|
DROP TABLE fp_fk_dom, fp_pk_dom;
|
||||||
DROP DOMAIN fp_int8dom;
|
DROP DOMAIN fp_int8dom;
|
||||||
|
|
||||||
-- Re-entrant FK fast-path: DML on the same FK table from a cast function
|
-- Re-entrant FK check: DML on the same FK table from a cast function
|
||||||
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
CREATE TABLE fp_reentry_pk (id int PRIMARY KEY);
|
||||||
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
INSERT INTO fp_reentry_pk VALUES (1), (2);
|
||||||
CREATE TYPE fp_vch AS (v int);
|
CREATE TYPE fp_vch AS (v int);
|
||||||
@@ -3013,22 +3004,17 @@ DROP TYPE fp_vch;
|
|||||||
--
|
--
|
||||||
-- Cache invalidation arriving during a fast-path check.
|
-- Cache invalidation arriving during a fast-path check.
|
||||||
--
|
--
|
||||||
-- A cross-type foreign key runs the user's cast function while building its
|
-- The equality function of the referenced index's opclass runs while the
|
||||||
-- index scan keys. A cast that performs DDL raises an invalidation there,
|
-- fast path scans the index. One that performs DDL raises an invalidation
|
||||||
-- detaching the constraint's fast-path metadata while the check is still
|
-- there, detaching the constraint's fast-path metadata while the check is
|
||||||
-- using it.
|
-- still using it. This is done in a transaction that is rolled back, so that
|
||||||
|
-- other tests never see the operator class.
|
||||||
--
|
--
|
||||||
CREATE TYPE fkint;
|
BEGIN;
|
||||||
CREATE FUNCTION fkint_in(cstring) RETURNS fkint
|
|
||||||
AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
|
|
||||||
CREATE FUNCTION fkint_out(fkint) RETURNS cstring
|
|
||||||
AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
|
|
||||||
CREATE TYPE fkint (INPUT = fkint_in, OUTPUT = fkint_out, LIKE = int4);
|
|
||||||
|
|
||||||
-- Renames the constraint the first time it is called, and so raises an
|
-- Renames the constraint the first time it is called, and so raises an
|
||||||
-- invalidation partway through the check. Guarded on the catalog so the
|
-- invalidation partway through the check. Guarded on the catalog so the
|
||||||
-- second and later calls are no-ops.
|
-- second and later calls are no-ops.
|
||||||
CREATE FUNCTION fkint_to_int4(fkint) RETURNS int4 AS $$
|
CREATE FUNCTION fp_inval_eq(int4, int4) RETURNS bool AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM pg_constraint
|
IF EXISTS (SELECT 1 FROM pg_constraint
|
||||||
WHERE conrelid = 'fktable_inval'::regclass
|
WHERE conrelid = 'fktable_inval'::regclass
|
||||||
@@ -3036,35 +3022,33 @@ BEGIN
|
|||||||
EXECUTE 'ALTER TABLE fktable_inval'
|
EXECUTE 'ALTER TABLE fktable_inval'
|
||||||
' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2';
|
' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2';
|
||||||
END IF;
|
END IF;
|
||||||
RETURN format('%s', $1)::int4;
|
RETURN $1 OPERATOR(pg_catalog.=) $2;
|
||||||
END $$ LANGUAGE plpgsql;
|
END $$ LANGUAGE plpgsql;
|
||||||
|
CREATE OPERATOR =@= (LEFTARG = int4, RIGHTARG = int4, FUNCTION = fp_inval_eq);
|
||||||
|
CREATE OPERATOR CLASS fp_inval_ops FOR TYPE int4 USING btree AS
|
||||||
|
OPERATOR 1 pg_catalog.<, OPERATOR 2 pg_catalog.<=, OPERATOR 3 =@=,
|
||||||
|
OPERATOR 4 pg_catalog.>=, OPERATOR 5 pg_catalog.>,
|
||||||
|
FUNCTION 1 btint4cmp(int4, int4);
|
||||||
|
|
||||||
CREATE CAST (fkint AS int4) WITH FUNCTION fkint_to_int4(fkint) AS IMPLICIT;
|
CREATE TABLE pktable_inval (a int4, b int4);
|
||||||
|
CREATE UNIQUE INDEX pktable_inval_a_b ON pktable_inval (a fp_inval_ops, b);
|
||||||
CREATE TABLE pktable_inval (a int4, b int4, PRIMARY KEY (a, b));
|
|
||||||
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
INSERT INTO pktable_inval VALUES (1, 1), (2, 2);
|
||||||
|
CREATE TABLE fktable_inval (a int4, b int4,
|
||||||
-- Cross-type on column a, so the cast above is invoked.
|
|
||||||
CREATE TABLE fktable_inval (a fkint, b int4,
|
|
||||||
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b)
|
||||||
REFERENCES pktable_inval (a, b));
|
REFERENCES pktable_inval (a, b));
|
||||||
|
|
||||||
-- The first row invalidates the metadata; the second check must repopulate
|
-- The first row invalidates the metadata; the second check must repopulate
|
||||||
-- and use it safely.
|
-- and use it safely.
|
||||||
INSERT INTO fktable_inval VALUES ('1', 1), ('2', 2);
|
INSERT INTO fktable_inval VALUES (1, 1), (2, 2);
|
||||||
|
|
||||||
-- Confirms the cast actually ran and raised the invalidation. Without this
|
-- Confirms the equality function actually ran and raised the invalidation.
|
||||||
-- the insert above could pass merely by not exercising the path at all.
|
-- Without this the insert above could pass merely by not exercising the path
|
||||||
|
-- at all.
|
||||||
SELECT conname FROM pg_constraint
|
SELECT conname FROM pg_constraint
|
||||||
WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f';
|
WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f';
|
||||||
|
|
||||||
SELECT count(*) FROM fktable_inval;
|
SELECT count(*) FROM fktable_inval;
|
||||||
|
ROLLBACK;
|
||||||
DROP TABLE fktable_inval;
|
|
||||||
DROP TABLE pktable_inval;
|
|
||||||
DROP CAST (fkint AS int4);
|
|
||||||
DROP FUNCTION fkint_to_int4(fkint);
|
|
||||||
DROP TYPE fkint CASCADE;
|
|
||||||
|
|
||||||
-- An FK check locks the referenced row, which a read-only transaction does
|
-- An FK check locks the referenced row, which a read-only transaction does
|
||||||
-- not allow unless the referenced table is temporary.
|
-- not allow unless the referenced table is temporary.
|
||||||
@@ -3115,3 +3099,99 @@ SAVEPOINT fp_cid;
|
|||||||
INSERT INTO fp_fk_cid VALUES (1); -- fails, as a violation
|
INSERT INTO fp_fk_cid VALUES (1); -- fails, as a violation
|
||||||
ROLLBACK TO SAVEPOINT fp_cid;
|
ROLLBACK TO SAVEPOINT fp_cid;
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- FK values that need a coercion to reach the equality operator's input type
|
||||||
|
-- are checked through SPI, which applies the coercion as part of the query.
|
||||||
|
-- The fast path used to call the cast function directly.
|
||||||
|
--
|
||||||
|
BEGIN;
|
||||||
|
CREATE SCHEMA ri_cast;
|
||||||
|
SET LOCAL search_path = ri_cast, pg_catalog;
|
||||||
|
|
||||||
|
-- A cast that returns NULL for a non-NULL key: an FK violation, as for any
|
||||||
|
-- other key without a match, not an internal error.
|
||||||
|
CREATE TYPE ri_word AS ENUM ('one', 'none');
|
||||||
|
CREATE FUNCTION ri_word_to_int4(ri_word) RETURNS int4
|
||||||
|
LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN $1::text = 'one' THEN 1 END $$;
|
||||||
|
CREATE CAST (ri_word AS int4) WITH FUNCTION ri_word_to_int4(ri_word) AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_pk (id int4 PRIMARY KEY);
|
||||||
|
INSERT INTO ri_pk VALUES (1);
|
||||||
|
CREATE TABLE ri_fk (a ri_word REFERENCES ri_pk);
|
||||||
|
INSERT INTO ri_fk VALUES ('one');
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk VALUES ('none'); -- fails
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
|
||||||
|
-- Replacing the cast and dropping its old function must not leave a later
|
||||||
|
-- check in this session calling the dropped function.
|
||||||
|
CREATE FUNCTION ri_word_to_int4_v2(ri_word) RETURNS int4
|
||||||
|
LANGUAGE sql IMMUTABLE AS $$ SELECT 1 $$;
|
||||||
|
DROP CAST (ri_word AS int4);
|
||||||
|
CREATE CAST (ri_word AS int4) WITH FUNCTION ri_word_to_int4_v2(ri_word) AS IMPLICIT;
|
||||||
|
DROP FUNCTION ri_word_to_int4(ri_word);
|
||||||
|
INSERT INTO ri_fk VALUES ('none'); -- succeeds through the new cast
|
||||||
|
|
||||||
|
-- A collation-sensitive cast from a collatable type gets the input
|
||||||
|
-- collation an expression would give it.
|
||||||
|
CREATE TYPE ri_ctext;
|
||||||
|
CREATE FUNCTION ri_ctext_in(cstring) RETURNS ri_ctext
|
||||||
|
AS 'textin' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_ctext_out(ri_ctext) RETURNS cstring
|
||||||
|
AS 'textout' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE TYPE ri_ctext (INPUT = ri_ctext_in, OUTPUT = ri_ctext_out,
|
||||||
|
LIKE = text, COLLATABLE = true);
|
||||||
|
CREATE FUNCTION ri_ctext_lower(ri_ctext) RETURNS text
|
||||||
|
AS 'lower' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE CAST (ri_ctext AS text) WITH FUNCTION ri_ctext_lower(ri_ctext) AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_pk_text (k text PRIMARY KEY);
|
||||||
|
INSERT INTO ri_pk_text VALUES ('abc');
|
||||||
|
CREATE TABLE ri_fk_text (a ri_ctext REFERENCES ri_pk_text);
|
||||||
|
INSERT INTO ri_fk_text VALUES ('ABC'); -- succeeds; the cast lowercases
|
||||||
|
|
||||||
|
-- An array coercion (int2[] to int4[], allowed with an opclass declared for
|
||||||
|
-- int4[]) has no single cast function to call.
|
||||||
|
CREATE FUNCTION ri_i4a_lt(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_lt' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_le(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_le' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_eq(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_eq' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_ge(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_ge' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_gt(int4[], int4[]) RETURNS bool
|
||||||
|
AS 'array_gt' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_i4a_cmp(int4[], int4[]) RETURNS int4
|
||||||
|
AS 'btarraycmp' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE OPERATOR ri_cast.<<< (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_lt);
|
||||||
|
CREATE OPERATOR ri_cast.<<= (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_le);
|
||||||
|
CREATE OPERATOR ri_cast.=== (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_eq);
|
||||||
|
CREATE OPERATOR ri_cast.>>= (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_ge);
|
||||||
|
CREATE OPERATOR ri_cast.>>> (LEFTARG = int4[], RIGHTARG = int4[], FUNCTION = ri_i4a_gt);
|
||||||
|
CREATE OPERATOR CLASS ri_i4a_ops FOR TYPE int4[] USING btree AS
|
||||||
|
OPERATOR 1 ri_cast.<<<, OPERATOR 2 ri_cast.<<=, OPERATOR 3 ri_cast.===,
|
||||||
|
OPERATOR 4 ri_cast.>>=, OPERATOR 5 ri_cast.>>>,
|
||||||
|
FUNCTION 1 ri_i4a_cmp(int4[], int4[]);
|
||||||
|
CREATE TABLE ri_pk_arr (k int4[]);
|
||||||
|
CREATE UNIQUE INDEX ON ri_pk_arr (k ri_i4a_ops);
|
||||||
|
INSERT INTO ri_pk_arr VALUES ('{1,2}');
|
||||||
|
CREATE TABLE ri_fk_arr (a int2[] REFERENCES ri_pk_arr (k));
|
||||||
|
INSERT INTO ri_fk_arr VALUES ('{1,2}'); -- succeeds
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk_arr VALUES ('{3}'); -- fails
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
|
||||||
|
-- A coercion via I/O has no cast function to call either.
|
||||||
|
CREATE TYPE ri_ioint;
|
||||||
|
CREATE FUNCTION ri_ioint_in(cstring) RETURNS ri_ioint
|
||||||
|
AS 'int4in' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE FUNCTION ri_ioint_out(ri_ioint) RETURNS cstring
|
||||||
|
AS 'int4out' LANGUAGE internal IMMUTABLE STRICT;
|
||||||
|
CREATE TYPE ri_ioint (INPUT = ri_ioint_in, OUTPUT = ri_ioint_out, LIKE = int4);
|
||||||
|
CREATE CAST (ri_ioint AS int4) WITH INOUT AS IMPLICIT;
|
||||||
|
CREATE TABLE ri_fk_io (a ri_ioint REFERENCES ri_pk);
|
||||||
|
INSERT INTO ri_fk_io VALUES ('1'); -- succeeds
|
||||||
|
SAVEPOINT s;
|
||||||
|
INSERT INTO ri_fk_io VALUES ('2'); -- fails
|
||||||
|
ROLLBACK TO SAVEPOINT s;
|
||||||
|
ROLLBACK;
|
||||||
|
|||||||
Reference in New Issue
Block a user