diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 6e3a41062f..09a2ad2881 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -101,7 +101,10 @@ typedef struct RI_ConstraintInfo { Oid constraint_id; /* OID of pg_constraint entry (hash key) */ bool valid; /* successfully initialized? */ - uint32 oidHashValue; /* hash value of pg_constraint OID */ + Oid constraint_root_id; /* OID of topmost ancestor constraint; + * same as constraint_id if not inherited */ + uint32 oidHashValue; /* hash value of constraint_id */ + uint32 rootHashValue; /* hash value of constraint_root_id */ NameData conname; /* name of the FK constraint */ Oid pk_relid; /* referenced relation */ Oid fk_relid; /* referencing relation */ @@ -207,6 +210,7 @@ static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk); static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); +static Oid get_ri_constraint_root(Oid constrOid); static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel); static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, @@ -1892,7 +1896,7 @@ ri_GenerateQualCollation(StringInfo buf, Oid collation) * Construct a hashtable key for a prepared SPI plan of an FK constraint. * * key: output argument, *key is filled in based on the other arguments - * riinfo: info from pg_constraint entry + * riinfo: info derived from pg_constraint entry * constr_queryno: an internal number identifying the query type * (see RI_PLAN_XXX constants at head of file) * ---------- @@ -1902,10 +1906,27 @@ ri_BuildQueryKey(RI_QueryKey *key, const RI_ConstraintInfo *riinfo, int32 constr_queryno) { /* + * Inherited constraints with a common ancestor can share ri_query_cache + * entries for all query types except RI_PLAN_CHECK_LOOKUPPK_FROM_PK. + * Except in that case, the query processes the other table involved in + * the FK constraint (i.e., not the table on which the trigger has been + * fired), and so it will be the same for all members of the inheritance + * tree. So we may use the root constraint's OID in the hash key, rather + * than the constraint's own OID. This avoids creating duplicate SPI + * plans, saving lots of work and memory when there are many partitions + * with similar FK constraints. + * + * (Note that we must still have a separate RI_ConstraintInfo for each + * constraint, because partitions can have different column orders, + * resulting in different pk_attnums[] or fk_attnums[] array contents.) + * * We assume struct RI_QueryKey contains no padding bytes, else we'd need * to use memset to clear them. */ - key->constr_id = riinfo->constraint_id; + if (constr_queryno != RI_PLAN_CHECK_LOOKUPPK_FROM_PK) + key->constr_id = riinfo->constraint_root_id; + else + key->constr_id = riinfo->constraint_id; key->constr_queryno = constr_queryno; } @@ -2051,8 +2072,15 @@ ri_LoadConstraintInfo(Oid constraintOid) /* And extract data */ Assert(riinfo->constraint_id == constraintOid); + if (OidIsValid(conForm->conparentid)) + riinfo->constraint_root_id = + get_ri_constraint_root(conForm->conparentid); + else + riinfo->constraint_root_id = constraintOid; riinfo->oidHashValue = GetSysCacheHashValue1(CONSTROID, ObjectIdGetDatum(constraintOid)); + riinfo->rootHashValue = GetSysCacheHashValue1(CONSTROID, + ObjectIdGetDatum(riinfo->constraint_root_id)); memcpy(&riinfo->conname, &conForm->conname, sizeof(NameData)); riinfo->pk_relid = conForm->confrelid; riinfo->fk_relid = conForm->conrelid; @@ -2082,6 +2110,30 @@ ri_LoadConstraintInfo(Oid constraintOid) return riinfo; } +/* + * get_ri_constraint_root + * Returns the OID of the constraint's root parent + */ +static Oid +get_ri_constraint_root(Oid constrOid) +{ + for (;;) + { + HeapTuple tuple; + Oid constrParentOid; + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", constrOid); + constrParentOid = ((Form_pg_constraint) GETSTRUCT(tuple))->conparentid; + ReleaseSysCache(tuple); + if (!OidIsValid(constrParentOid)) + break; /* we reached the root constraint */ + constrOid = constrParentOid; + } + return constrOid; +} + /* * Callback for pg_constraint inval events * @@ -2117,7 +2169,14 @@ InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) RI_ConstraintInfo *riinfo = dlist_container(RI_ConstraintInfo, valid_link, iter.cur); - if (hashvalue == 0 || riinfo->oidHashValue == hashvalue) + /* + * We must invalidate not only entries directly matching the given + * hash value, but also child entries, in case the invalidation + * affects a root constraint. + */ + if (hashvalue == 0 || + riinfo->oidHashValue == hashvalue || + riinfo->rootHashValue == hashvalue) { riinfo->valid = false; /* Remove invalidated entries from the list, too */ diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 07bd5b6434..7386f4d635 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -2470,3 +2470,21 @@ DROP SCHEMA fkpart9 CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table fkpart9.pk drop cascades to table fkpart9.fk +-- test that ri_Check_Pk_Match() scans the correct partition for a deferred +-- ON DELETE/UPDATE NO ACTION constraint +CREATE SCHEMA fkpart10 + CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1) + CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1) + CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue) + CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fkpart10.tbl1 VALUES (0), (1); +INSERT INTO fkpart10.tbl2 VALUES (0), (1); +BEGIN; +DELETE FROM fkpart10.tbl1 WHERE f1 = 0; +UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1; +INSERT INTO fkpart10.tbl1 VALUES (0), (1); +COMMIT; +DROP SCHEMA fkpart10 CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table fkpart10.tbl1 +drop cascades to table fkpart10.tbl2 diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index c5c9011afc..67aa20435d 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -1738,3 +1738,19 @@ DELETE FROM fkpart9.pk WHERE a=35; SELECT * FROM fkpart9.pk; SELECT * FROM fkpart9.fk; DROP SCHEMA fkpart9 CASCADE; + +-- test that ri_Check_Pk_Match() scans the correct partition for a deferred +-- ON DELETE/UPDATE NO ACTION constraint +CREATE SCHEMA fkpart10 + CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1) + CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1) + CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue) + CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fkpart10.tbl1 VALUES (0), (1); +INSERT INTO fkpart10.tbl2 VALUES (0), (1); +BEGIN; +DELETE FROM fkpart10.tbl1 WHERE f1 = 0; +UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1; +INSERT INTO fkpart10.tbl1 VALUES (0), (1); +COMMIT; +DROP SCHEMA fkpart10 CASCADE;