diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index c3ea7db72ff..a53cacda254 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -304,7 +304,8 @@ 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); + Relation fk_rel, Relation pk_rel, + Relation idx_rel); static void ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel); static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, @@ -435,8 +436,9 @@ RI_FKey_check(TriggerData *trigdata) * the per-row executor overhead. * * 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. + * is found. It returns false if the checks made after opening the + * 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) && 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 * gets enough update traffic that it's probably worth being smarter. @@ -2524,6 +2526,14 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, if (cacheid == AMOPOPID) 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 * 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); - 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); 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 - * unknown state still needs ri_check_fastpath_index() before using it. + * Apply the cheap checks that depend only on riinfo; this runs for every + * 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 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. - * Cache both success and failure until the constraint information is reloaded. + * Check eligibility that depends on the index or on the FK column types + * 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. + * The index-property checks use the held relation descriptors, and the check + * of the FK column types uses fk_rel. */ 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) { /* 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; 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 * initializes its generated query, where the equality operator's function - * appears in the WHERE clause and the cast function, if any, in the cast - * applied to the parameter. Call with the user id already switched to the - * referenced table's owner. + * appears in the WHERE clause. The fast path is not used when the FK values + * need coercion (see ri_check_fastpath_index()), so that is the only function + * to check. Call with the user id already switched to the referenced table's + * owner. */ static void ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo, @@ -3633,13 +3670,16 @@ ri_InitHashTables(void) RI_INIT_CONSTRAINTHASHSIZE, &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, InvalidateConstraintCacheCallBack, (Datum) 0); CacheRegisterSyscacheCallback(AMOPOPID, InvalidateConstraintCacheCallBack, (Datum) 0); + CacheRegisterSyscacheCallback(CASTSOURCETARGET, + InvalidateConstraintCacheCallBack, + (Datum) 0); ctl.keysize = sizeof(RI_QueryKey); ctl.entrysize = sizeof(RI_QueryHashEntry); diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index df5e827d57e..b1f36f6a1bb 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -3744,11 +3744,11 @@ DETAIL: Key (a)=(3) is not present in table "fp_pk_defer". COMMIT; DROP TABLE fp_pk_defer, fp_fk_defer; -- 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 --- and can run user cast/equality code whose DML queues a further deferred --- check; that check must still fire. fk_defer_main's deferred fast-path check --- runs a cast whose function inserts a violating row into fk_defer_t2, --- queueing fk_defer_t2's own deferred check, which must still be reported. +-- not be lost. An RI check runs during the deferred firing loop and can run +-- user cast code whose DML queues a further deferred check; that check must +-- still fire. fk_defer_main's deferred check runs a cast whose function +-- inserts a violating row into fk_defer_t2, queueing fk_defer_t2's own +-- deferred check, which must still be reported. CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY); CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id) DEFERRABLE INITIALLY DEFERRED); @@ -3801,11 +3801,13 @@ NOTICE: fp_auto_pk called NOTICE: fp_auto_pk called DROP TABLE fp_fk_cci, fp_pk_cci; DROP FUNCTION fp_auto_pk; --- The fast path must check EXECUTE, as the referenced table's owner, on the --- functions it invokes on the FK values: the equality operator's function --- and, for a cross-type FK, the implicit cast function. Validation of a new --- constraint by a role that cannot use the bulk check (RLS is enabled on the --- referenced table) runs per-row checks through the fast path. +-- An FK check must check EXECUTE, as the referenced table's owner, on the +-- functions it applies to the FK values: the equality operator's function +-- and, for an FK needing an implicit cast function, the cast. Validation of +-- a new constraint by a role that cannot use the bulk check (RLS is enabled +-- 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 -- with nearby tests. @@ -3865,8 +3867,9 @@ ERROR: permission denied for function regress_ri_int4eq ROLLBACK TO SAVEPOINT s; SAVEPOINT s; 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 +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; SAVEPOINT s; 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; RESET ROLE; ROLLBACK; --- A STABLE cast used by an FK check must see changes made by earlier AFTER --- triggers, using the check's snapshot rather than the outer query's snapshot. --- Compare the per-row fast path with a partitioned-parent SPI check. +-- A STABLE equality function used by an FK check must see changes made by +-- earlier AFTER triggers, using the check's snapshot rather than the outer +-- 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; CREATE SCHEMA ri_snapshot; SET LOCAL search_path = ri_snapshot, pg_catalog; -CREATE TYPE lookup_key AS (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 $$ -DECLARE - result int; BEGIN - SELECT r.v INTO result FROM lookup_rows AS r WHERE r.v = k.v; - RETURN coalesce(result, -1); + RETURN a = b AND EXISTS (SELECT 1 FROM lookup_rows AS r WHERE r.v = a); END; $$; -CREATE CAST (lookup_key AS int) - WITH FUNCTION lookup_key_to_int(lookup_key) AS IMPLICIT; -CREATE TABLE pk_fast (v int PRIMARY KEY); -CREATE TABLE pk_spi (v int PRIMARY KEY) PARTITION BY RANGE (v); +CREATE OPERATOR === (LEFTARG = int, RIGHTARG = int, FUNCTION = lookup_eq); +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); +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 FOR VALUES FROM (MINVALUE) TO (MAXVALUE); +CREATE UNIQUE INDEX ON pk_spi (v lookup_ops); INSERT INTO pk_fast VALUES (1); INSERT INTO pk_spi VALUES (1); -CREATE TABLE fk_fast (k lookup_key REFERENCES pk_fast(v)); -CREATE TABLE fk_spi (k lookup_key REFERENCES pk_spi(v)); +CREATE TABLE fk_fast (k int REFERENCES pk_fast(v)); +CREATE TABLE fk_spi (k int REFERENCES pk_spi(v)); CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO lookup_rows VALUES ((NEW.k).v); + INSERT INTO lookup_rows VALUES (NEW.k); RETURN NEW; END; $$; @@ -3920,22 +3926,9 @@ CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_fast FOR EACH ROW EXECUTE FUNCTION add_lookup_row(); CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi FOR EACH ROW EXECUTE FUNCTION add_lookup_row(); --- Invoke the fast-path check from another FK's cast. Even with batching --- enabled, a check nested inside the end-of-batch flush takes the per-row path. -CREATE TYPE driver_key AS (v int); -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); +-- Both checks must succeed. Without the check's snapshot active, the +-- STABLE equality function misses the lookup row, so no PK row matches. +INSERT INTO fk_fast VALUES (1); SELECT count(*) FROM fk_fast; count ------- @@ -3943,7 +3936,7 @@ SELECT count(*) FROM fk_fast; (1 row) DELETE FROM lookup_rows; -INSERT INTO fk_spi VALUES (ROW(1)::lookup_key); +INSERT INTO fk_spi VALUES (1); SELECT count(*) FROM fk_spi; count ------- @@ -4028,7 +4021,7 @@ DETAIL: Key (a)=(999) is not present in table "fp_pk_dom". INSERT INTO fp_fk_dom VALUES (NULL); DROP TABLE fp_fk_dom, fp_pk_dom; 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); INSERT INTO fp_reentry_pk VALUES (1), (2); CREATE TYPE fp_vch AS (v int); @@ -4058,25 +4051,17 @@ DROP TYPE fp_vch; -- -- Cache invalidation arriving during a fast-path check. -- --- A cross-type foreign key runs the user's cast function while building its --- index scan keys. A cast that performs DDL raises an invalidation there, --- detaching the constraint's fast-path metadata while the check is still --- using it. +-- The equality function of the referenced index's opclass runs while the +-- fast path scans the index. One that performs DDL raises an invalidation +-- there, detaching the constraint's fast-path metadata while the check is +-- 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; -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); +BEGIN; -- Renames the constraint the first time it is called, and so raises an -- invalidation partway through the check. Guarded on the catalog so the -- 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 IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'fktable_inval'::regclass @@ -4084,20 +4069,25 @@ BEGIN EXECUTE 'ALTER TABLE fktable_inval' ' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2'; END IF; - RETURN format('%s', $1)::int4; + RETURN $1 OPERATOR(pg_catalog.=) $2; END $$ LANGUAGE plpgsql; -CREATE CAST (fkint AS int4) WITH FUNCTION fkint_to_int4(fkint) AS IMPLICIT; -CREATE TABLE pktable_inval (a int4, b int4, PRIMARY KEY (a, b)); +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 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); --- Cross-type on column a, so the cast above is invoked. -CREATE TABLE fktable_inval (a fkint, b int4, +CREATE TABLE fktable_inval (a int4, b int4, CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b) REFERENCES pktable_inval (a, b)); -- The first row invalidates the metadata; the second check must repopulate -- and use it safely. -INSERT INTO fktable_inval VALUES ('1', 1), ('2', 2); --- Confirms the cast actually ran and raised the invalidation. Without this --- the insert above could pass merely by not exercising the path at all. +INSERT INTO fktable_inval VALUES (1, 1), (2, 2); +-- Confirms the equality function actually ran and raised the invalidation. +-- Without this the insert above could pass merely by not exercising the path +-- at all. SELECT conname FROM pg_constraint WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f'; conname @@ -4111,14 +4101,7 @@ SELECT count(*) FROM fktable_inval; 2 (1 row) -DROP TABLE fktable_inval; -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) +ROLLBACK; -- An FK check locks the referenced row, which a read-only transaction does -- not allow unless the referenced table is temporary. 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". ROLLBACK TO SAVEPOINT fp_cid; 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; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index ce35c465b81..99ea110e848 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2713,11 +2713,11 @@ COMMIT; DROP TABLE fp_pk_defer, fp_fk_defer; -- 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 --- and can run user cast/equality code whose DML queues a further deferred --- check; that check must still fire. fk_defer_main's deferred fast-path check --- runs a cast whose function inserts a violating row into fk_defer_t2, --- queueing fk_defer_t2's own deferred check, which must still be reported. +-- not be lost. An RI check runs during the deferred firing loop and can run +-- user cast code whose DML queues a further deferred check; that check must +-- still fire. fk_defer_main's deferred check runs a cast whose function +-- inserts a violating row into fk_defer_t2, queueing fk_defer_t2's own +-- deferred check, which must still be reported. CREATE TABLE fk_defer_t2_pk (id int PRIMARY KEY); CREATE TABLE fk_defer_t2 (a int REFERENCES fk_defer_t2_pk(id) 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 FUNCTION fp_auto_pk; --- The fast path must check EXECUTE, as the referenced table's owner, on the --- functions it invokes on the FK values: the equality operator's function --- and, for a cross-type FK, the implicit cast function. Validation of a new --- constraint by a role that cannot use the bulk check (RLS is enabled on the --- referenced table) runs per-row checks through the fast path. +-- An FK check must check EXECUTE, as the referenced table's owner, on the +-- functions it applies to the FK values: the equality operator's function +-- and, for an FK needing an implicit cast function, the cast. Validation of +-- a new constraint by a role that cannot use the bulk check (RLS is enabled +-- 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 -- with nearby tests. @@ -2839,7 +2841,7 @@ ALTER TABLE fktable_acl_op ROLLBACK TO SAVEPOINT s; SAVEPOINT s; 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; SAVEPOINT s; ALTER TABLE fktable_acl_op @@ -2852,39 +2854,42 @@ ROLLBACK TO SAVEPOINT s; RESET ROLE; ROLLBACK; --- A STABLE cast used by an FK check must see changes made by earlier AFTER --- triggers, using the check's snapshot rather than the outer query's snapshot. --- Compare the per-row fast path with a partitioned-parent SPI check. +-- A STABLE equality function used by an FK check must see changes made by +-- earlier AFTER triggers, using the check's snapshot rather than the outer +-- 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; CREATE SCHEMA ri_snapshot; SET LOCAL search_path = ri_snapshot, pg_catalog; -CREATE TYPE lookup_key AS (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 $$ -DECLARE - result int; BEGIN - SELECT r.v INTO result FROM lookup_rows AS r WHERE r.v = k.v; - RETURN coalesce(result, -1); + RETURN a = b AND EXISTS (SELECT 1 FROM lookup_rows AS r WHERE r.v = a); END; $$; -CREATE CAST (lookup_key AS int) - WITH FUNCTION lookup_key_to_int(lookup_key) AS IMPLICIT; +CREATE OPERATOR === (LEFTARG = int, RIGHTARG = int, FUNCTION = lookup_eq); +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_spi (v int PRIMARY KEY) PARTITION BY RANGE (v); +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 FOR VALUES FROM (MINVALUE) TO (MAXVALUE); +CREATE UNIQUE INDEX ON pk_spi (v lookup_ops); INSERT INTO pk_fast VALUES (1); INSERT INTO pk_spi VALUES (1); -CREATE TABLE fk_fast (k lookup_key REFERENCES pk_fast(v)); -CREATE TABLE fk_spi (k lookup_key REFERENCES pk_spi(v)); +CREATE TABLE fk_fast (k int REFERENCES pk_fast(v)); +CREATE TABLE fk_spi (k int REFERENCES pk_spi(v)); CREATE FUNCTION add_lookup_row() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO lookup_rows VALUES ((NEW.k).v); + INSERT INTO lookup_rows VALUES (NEW.k); RETURN NEW; END; $$; @@ -2894,26 +2899,12 @@ CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_fast CREATE TRIGGER "AAA_lookup" AFTER INSERT ON fk_spi FOR EACH ROW EXECUTE FUNCTION add_lookup_row(); --- Invoke the fast-path check from another FK's cast. Even with batching --- enabled, a check nested inside the end-of-batch flush takes the per-row path. -CREATE TYPE driver_key AS (v int); -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); +-- Both checks must succeed. Without the check's snapshot active, the +-- STABLE equality function misses the lookup row, so no PK row matches. +INSERT INTO fk_fast VALUES (1); SELECT count(*) FROM fk_fast; DELETE FROM lookup_rows; -INSERT INTO fk_spi VALUES (ROW(1)::lookup_key); +INSERT INTO fk_spi VALUES (1); SELECT count(*) FROM fk_spi; ROLLBACK; @@ -2988,7 +2979,7 @@ INSERT INTO fp_fk_dom VALUES (NULL); DROP TABLE fp_fk_dom, fp_pk_dom; 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); INSERT INTO fp_reentry_pk VALUES (1), (2); CREATE TYPE fp_vch AS (v int); @@ -3013,22 +3004,17 @@ DROP TYPE fp_vch; -- -- Cache invalidation arriving during a fast-path check. -- --- A cross-type foreign key runs the user's cast function while building its --- index scan keys. A cast that performs DDL raises an invalidation there, --- detaching the constraint's fast-path metadata while the check is still --- using it. +-- The equality function of the referenced index's opclass runs while the +-- fast path scans the index. One that performs DDL raises an invalidation +-- there, detaching the constraint's fast-path metadata while the check is +-- 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; -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); - +BEGIN; -- Renames the constraint the first time it is called, and so raises an -- invalidation partway through the check. Guarded on the catalog so the -- 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 IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'fktable_inval'::regclass @@ -3036,35 +3022,33 @@ BEGIN EXECUTE 'ALTER TABLE fktable_inval' ' RENAME CONSTRAINT fktable_inval_fk TO fktable_inval_fk2'; END IF; - RETURN format('%s', $1)::int4; + RETURN $1 OPERATOR(pg_catalog.=) $2; 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, PRIMARY KEY (a, b)); +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); - --- Cross-type on column a, so the cast above is invoked. -CREATE TABLE fktable_inval (a fkint, b int4, +CREATE TABLE fktable_inval (a int4, b int4, CONSTRAINT fktable_inval_fk FOREIGN KEY (a, b) REFERENCES pktable_inval (a, b)); -- The first row invalidates the metadata; the second check must repopulate -- 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 --- the insert above could pass merely by not exercising the path at all. +-- Confirms the equality function actually ran and raised the invalidation. +-- Without this the insert above could pass merely by not exercising the path +-- at all. SELECT conname FROM pg_constraint WHERE conrelid = 'fktable_inval'::regclass AND contype = 'f'; SELECT count(*) FROM fktable_inval; - -DROP TABLE fktable_inval; -DROP TABLE pktable_inval; -DROP CAST (fkint AS int4); -DROP FUNCTION fkint_to_int4(fkint); -DROP TYPE fkint CASCADE; +ROLLBACK; -- An FK check locks the referenced row, which a read-only transaction does -- 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 ROLLBACK TO SAVEPOINT fp_cid; 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;