From 2754aa1914fbb3d1726de2e590ef214e8513de96 Mon Sep 17 00:00:00 2001 From: Zhijie Hou Date: Mon, 3 Aug 2026 14:02:38 +0800 Subject: [PATCH v1 2/4] Invalidate cached parallel DML safety via a new sinval message type The previous commit cached each relation's parallel DML safety hazard level in its relcache entry (rd_paralleldml). This commit adds the machinery that keeps the cached value up to date. A new shared-invalidation message type, SHAREDINVALPARALLELDML_ID, carries a database OID and a relation OID; it resets the cached rd_paralleldml of just that relcache entry (without flushing any other relcache data), or of every relcache entry in the database when the relation OID is InvalidOid. Since the message does not destroy the relcache entry, sending it requires no lock on the target table. The message is only needed for changes that do not already discard the cached value by other means. Adding or dropping a parallel-safety- related object on a table itself (a trigger, index, constraint or column default) always also updates the table's pg_class or pg_attribute row, which forces a relcache rebuild of the table; the rebuild discards rd_paralleldml as a side effect, and the accompanying relcache invalidation also invalidates dependent cached plans. The regression tests verify this per command via the raw cached value. So invalidation is sent when: * a function's parallel safety property changes (ALTER FUNCTION ... PARALLEL, or CREATE OR REPLACE FUNCTION that changes proparallel). We deliberately do not try to find the tables using the function; that would require additional locking and visibility handling. Instead, the cached values of all relcache entries in the database are invalidated. * a parallel-safety-related object is added to or dropped from a partition: the hazard level cached for a partitioned table is computed recursively over its partitions, so the ancestors' cached values must be invalidated too. The partition's own cached value needs no message (see above), so CacheInvalidateParallelDmlSafetyForAncestors() sends the invalidation only to the ancestors, which it finds with get_partition_ancestors() -- safe because the caller holds a lock on the relation, so its place in the partition tree cannot change meanwhile (ATTACH/DETACH PARTITION take conflicting locks). * a constraint is dropped (RemoveConstraintById). For a table constraint, the cached value of its ancestors (if any) is invalidated. * a partition is attached or detached: the ancestors' cached values were computed without or with the partition (ATExecAttachPartition, DetachPartitionFinalize). The parent's own cached value is already discarded by the relcache flush in StorePartitionBound or by the explicit relcache invalidation, so again only the ancestors need the message. A cached value may momentarily be stale when a function's parallel safety is changed concurrently with in-flight queries, since the message requires no lock on the target table; this is an accepted trade-off, because users can already alter a function's parallel safety without locking the tables that use it. But the relcache will eventually be invalidated, and the cached value will be recomputed to the correct value on next use. The regression tests in test_parallel_dml_safety verify that the cached value is recomputed after each kind of change, in particular that changes on a partition propagate to all of its ancestors. --- src/backend/catalog/heap.c | 9 + src/backend/catalog/index.c | 18 + src/backend/catalog/pg_attrdef.c | 10 + src/backend/catalog/pg_constraint.c | 12 + src/backend/catalog/pg_proc.c | 13 + src/backend/commands/functioncmds.c | 19 +- src/backend/commands/tablecmds.c | 27 ++ src/backend/commands/trigger.c | 18 + src/backend/utils/cache/inval.c | 119 ++++++ src/backend/utils/cache/relcache.c | 38 ++ src/include/storage/sinval.h | 12 + src/include/utils/inval.h | 4 + src/include/utils/relcache.h | 2 + .../expected/parallel_dml_safety.out | 377 ++++++++++++++++++ .../sql/parallel_dml_safety.sql | 133 ++++++ .../test_parallel_dml_safety--1.0.sql | 5 + .../test_parallel_dml_safety.c | 21 + 17 files changed, 836 insertions(+), 1 deletion(-) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 88087654de9..17b3edbeaf4 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2715,6 +2715,15 @@ AddRelationNewConstraints(Relation rel, */ SetRelationNumChecks(rel, numchecks); + /* + * Adding a constraint or column default can change the parallel DML + * safety hazard levels cached for the relation's ancestors, if it is a + * partition. (The relation's own cached value is already discarded by + * the pg_class/pg_attribute updates above, which force a relcache + * rebuild, so no message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(RelationGetRelid(rel)); + return cookedConstraints; } diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 7c1b94407a9..2495fca6a98 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1294,6 +1294,15 @@ index_create(Relation heapRelation, */ index_close(indexRelation, NoLock); + /* + * The parallel DML safety hazard levels cached in relcache for the heap + * relation's ancestors, if it is a partition, may have changed; + * invalidate them. (The heap relation's own cached value is already + * discarded by the pg_class update above, which forces a relcache + * rebuild.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(heapRelationId); + return indexRelationId; } @@ -2424,6 +2433,15 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode) */ CacheInvalidateRelcache(userHeapRelation); + /* + * Dropping the index can also change the parallel DML safety hazard + * levels cached for the heap relation's ancestors, if it is a + * partition. (The heap relation's own cached value is already + * discarded by the relcache invalidation above, so no message is needed + * for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(heapId); + /* * Close owning rel, but keep lock */ diff --git a/src/backend/catalog/pg_attrdef.c b/src/backend/catalog/pg_attrdef.c index 24815090d3d..7fbe7bf10c5 100644 --- a/src/backend/catalog/pg_attrdef.c +++ b/src/backend/catalog/pg_attrdef.c @@ -24,6 +24,7 @@ #include "catalog/pg_attrdef.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/inval.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -265,6 +266,15 @@ RemoveAttrDefaultById(Oid attrdefId) */ table_close(attr_rel, RowExclusiveLock); + /* + * Dropping a column default can change the parallel DML safety hazard + * levels cached for the relation's ancestors, if it is a partition. + * (The relation's own cached value is already discarded by the + * pg_attribute update above, which forces a relcache rebuild, so no + * message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(myrelid); + /* Keep lock on attribute's rel until end of xact */ relation_close(myrel, NoLock); } diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index b12765ae691..78086d60105 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -32,6 +32,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -986,6 +987,17 @@ RemoveConstraintById(Oid conId) /* Fry the constraint itself */ CatalogTupleDelete(conDesc, &tup->t_self); + /* + * Dropping the constraint can change the parallel DML safety hazard + * level cached in relcache: for a table constraint, invalidate the + * cached value of the table and, if it is a partition, its ancestors + * (whose cached values are computed recursively over their partitions). + * Domain constraints are not part of the cached hazard level, so there + * is nothing to do for them. + */ + if (OidIsValid(con->conrelid)) + CacheInvalidateParallelDmlSafetyForAncestors(con->conrelid); + /* Clean up */ ReleaseSysCache(tup); table_close(conDesc, RowExclusiveLock); diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index 4c6dfb5ca90..00c2da06b18 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -39,6 +39,7 @@ #include "tcop/tcopprot.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/regproc.h" #include "utils/rel.h" @@ -587,6 +588,18 @@ ProcedureCreate(const char *procedureName, tup = heap_modify_tuple(oldtup, tupDesc, values, nulls, replaces); CatalogTupleUpdate(rel, &tup->t_self, tup); + /* + * If the function's parallel safety changed, the parallel DML + * safety hazard level cached in relcache for any table that uses + * this function (e.g. in a trigger, constraint or index expression) + * may no longer be accurate. We intentionally don't try to track + * down those tables (that would require additional locking and + * visibility handling); instead, invalidate the cached hazard level + * of all relcache entries in this database. + */ + if (oldproc->proparallel != parallel) + CacheInvalidateParallelDmlSafety(InvalidOid); + ReleaseSysCache(oldtup); is_update = true; } diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index 3afd762e9dc..e3b678a3a3b 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -67,6 +67,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -1486,7 +1487,23 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) procForm->prosupport = newsupport; } if (parallel_item) - procForm->proparallel = interpret_func_parallel(parallel_item); + { + char proparallel = interpret_func_parallel(parallel_item); + + /* + * If the function's parallel safety changes, the parallel DML + * safety hazard level cached in relcache for any table that uses + * this function (e.g. in a trigger, constraint or index expression) + * may no longer be accurate. We intentionally don't try to track + * down those tables (that would require additional locking and + * visibility handling); instead, invalidate the cached hazard level + * of all relcache entries in this database. + */ + if (proparallel != procForm->proparallel) + CacheInvalidateParallelDmlSafety(InvalidOid); + + procForm->proparallel = proparallel; + } if (set_items) { Datum datum; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index bf73b87edae..268262cc60b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8033,6 +8033,15 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, else { CacheInvalidateRelcache(rel); + + /* + * The parallel DML safety hazard levels cached in relcache for the + * parent's ancestors, if any, were computed including the detached + * partition, so they must be invalidated as well. (The parent's own + * cached value is already discarded by the relcache invalidation above, + * so no message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(RelationGetRelid(rel)); } } @@ -21320,6 +21329,15 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel)); + /* + * The parallel DML safety hazard levels cached in relcache for the + * partitioned table's ancestors, if any, were computed without the new + * partition, so they must be invalidated. (The partitioned table's own + * cached value is already discarded by the relcache invalidation in + * StorePartitionBound, so no message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(RelationGetRelid(rel)); + /* * If the partition we just attached is partitioned itself, invalidate * relcache for all descendent partitions too to ensure that their @@ -22171,6 +22189,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, */ CacheInvalidateRelcache(rel); + /* + * The parallel DML safety hazard levels cached in relcache for the + * parent's ancestors, if any, were computed including the detached + * partition, so they must be invalidated as well. (The parent's own + * cached value is already discarded by the relcache invalidation above, + * so no message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(RelationGetRelid(rel)); + /* * If the partition we just detached is partitioned itself, invalidate * relcache for all descendent partitions too to ensure that their diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 401baddbfc6..ecf8c106932 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -1215,6 +1215,16 @@ CreateTriggerFiringOn(const CreateTrigStmt *stmt, const char *queryString, MemoryContextDelete(perChildCxt); } + /* + * The parallel DML safety hazard levels cached in relcache for the + * target relation's ancestors, if it is a partition, may have changed; + * invalidate them. (The relation's own cached value is already + * discarded by the pg_class update above, which forces a relcache + * rebuild.) Note that relOid can be InvalidOid here (the relation was + * then found by name), so use the opened relation's OID. + */ + CacheInvalidateParallelDmlSafetyForAncestors(RelationGetRelid(rel)); + /* Keep lock on target rel until end of xact */ table_close(rel, NoLock); @@ -1370,6 +1380,14 @@ RemoveTriggerById(Oid trigOid) */ CacheInvalidateRelcache(rel); + /* + * Dropping a trigger can also change the parallel DML safety hazard + * levels cached for the relation's ancestors, if it is a partition. + * (The relation's own cached value is already discarded by the relcache + * invalidation above, so no message is needed for it.) + */ + CacheInvalidateParallelDmlSafetyForAncestors(relid); + /* Keep lock on trigger's rel until end of xact */ table_close(rel, NoLock); } diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c index 63dc36d4d91..9c2cfd3e226 100644 --- a/src/backend/utils/cache/inval.c +++ b/src/backend/utils/cache/inval.c @@ -119,6 +119,7 @@ #include "access/xloginsert.h" #include "catalog/catalog.h" #include "catalog/pg_constraint.h" +#include "catalog/partition.h" #include "miscadmin.h" #include "storage/procnumber.h" #include "storage/sinval.h" @@ -552,6 +553,38 @@ AddSnapshotInvalidationMessage(InvalidationMsgsGroup *group, AddInvalidationMessage(group, RelCacheMsgs, &msg); } +/* + * Add a parallel DML safety inval entry + * + * We put these into the relcache subgroup for simplicity. + */ +static void +AddParallelDmlInvalidationMessage(InvalidationMsgsGroup *group, + Oid dbId, Oid relId) +{ + SharedInvalidationMessage msg; + + /* + * Don't add a duplicate item. We assume dbId need not be checked + * because it will never change. InvalidOid for relId means all + * relations, so we don't need to add individual ones when it is present. + */ + ProcessMessageSubGroup(group, RelCacheMsgs, + if (msg->pd.id == SHAREDINVALPARALLELDML_ID && + (msg->pd.relId == relId || + msg->pd.relId == InvalidOid)) + return); + + /* OK, add the item */ + msg.pd.id = SHAREDINVALPARALLELDML_ID; + msg.pd.dbId = dbId; + msg.pd.relId = relId; + /* check AddCatcacheInvalidationMessage() for an explanation */ + VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg)); + + AddInvalidationMessage(group, RelCacheMsgs, &msg); +} + /* * Append one group of invalidation messages to another, resetting * the source group to empty. @@ -674,6 +707,34 @@ RegisterSnapshotInvalidation(InvalidationInfo *info, Oid dbId, Oid relId) AddSnapshotInvalidationMessage(&info->CurrentCmdInvalidMsgs, dbId, relId); } +/* + * RegisterParallelDmlInvalidation + * + * As above, but register an invalidation event for the cached parallel DML + * safety hazard level of one relation, or of all relations in the database + * if relId is InvalidOid. + */ +static void +RegisterParallelDmlInvalidation(InvalidationInfo *info, Oid dbId, Oid relId) +{ + AddParallelDmlInvalidationMessage(&info->CurrentCmdInvalidMsgs, + dbId, relId); + + /* + * Parallel DML safety invalidation is not associated with system catalog + * updates. Quick hack to ensure that the next CommandCounterIncrement() + * will think that we need to do CommandEndInvalidationMessages(). + */ + (void) GetCurrentCommandId(true); + + /* + * Note that there is no need to zap the relcache init file here: + * rd_paralleldml is never trusted from the init file (see + * load_relcache_init_file), so a stale init file cannot propagate a + * stale hazard level to other sessions. + */ +} + /* * PrepareInvalidationState * Initialize inval data for the current (sub)transaction. @@ -897,6 +958,12 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg) if (msg->rs.dbId == MyDatabaseId) CallRelSyncCallbacks(msg->rs.relid); } + else if (msg->id == SHAREDINVALPARALLELDML_ID) + { + /* We only care about our own database */ + if (msg->pd.dbId == MyDatabaseId) + RelationCacheInvalidateParallelDml(msg->pd.relId); + } else elog(FATAL, "unrecognized SI message ID: %d", msg->id); } @@ -1723,6 +1790,58 @@ CacheInvalidateRelSyncAll(void) CacheInvalidateRelSync(InvalidOid); } +/* + * CacheInvalidateParallelDmlSafety + * Register invalidation of the cached parallel DML safety hazard level + * (rd_paralleldml) of the given relation at the end of command. If + * relId is InvalidOid, all relcache entries in the current database + * are invalidated. + * + * Unlike the relcache invalidation messages, this only discards the cached + * hazard level; the relcache entries themselves are kept. + * + * The caller is not required to hold a lock on the relation: resetting the + * cached value is always safe. It merely means that a concurrent query + * that has already planned using the old value may not notice the change; + * this is an accepted trade-off, since users can already alter a function's + * parallel safety without locking the tables that use it. + */ +void +CacheInvalidateParallelDmlSafety(Oid relId) +{ + RegisterParallelDmlInvalidation(PrepareInvalidationState(), + MyDatabaseId, relId); +} + +/* + * CacheInvalidateParallelDmlSafetyForAncestors + * Register invalidation of the cached parallel DML safety hazard levels + * of all of the given partition's ancestor partitioned tables, but not + * of the given relation itself. + * + * The hazard level cached for a partitioned table is computed recursively + * over its partitions, so adding a parallel-safety-related object to (or + * dropping one from) a partition must invalidate the ancestors' cached + * values. The partition's own cached value needs no message: such DDL + * always also updates the partition's own pg_class or pg_attribute row, + * which forces a relcache rebuild that discards it (see the previous + * commit). This is safe to call while modifying the relation: the caller + * holds a lock on it, so its place in the partition tree cannot change + * meanwhile (ATTACH/DETACH PARTITION take conflicting locks), which is what + * makes looking up its ancestors without locking them safe. + */ +void +CacheInvalidateParallelDmlSafetyForAncestors(Oid relId) +{ + List *ancestors; + ListCell *lc; + + ancestors = get_partition_ancestors(relId); + foreach(lc, ancestors) + CacheInvalidateParallelDmlSafety(lfirst_oid(lc)); + list_free(ancestors); +} + /* * CacheInvalidateSmgr * Register invalidation of smgr references to a physical relation. diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index ef7f71deaa6..088fd00194e 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -2964,6 +2964,44 @@ RelationCacheInvalidateEntry(Oid relationId) } } +/* + * RelationCacheInvalidateParallelDml + * Reset the cached parallel DML safety hazard level (rd_paralleldml) of + * the relcache entry for the given relation, or of every relcache entry + * if relationId is InvalidOid, forcing it to be recomputed on next use. + * + * Unlike RelationCacheInvalidateEntry, the relcache entry itself is + * kept: only the derived flag is discarded, so entries that are in use + * need no special handling. Resetting an entry that is currently in use + * by the planner or executor is harmless; the hazard level in use at + * that point has already been read. + * + * If the relation is not cached in this backend, there is nothing to do. + */ +void +RelationCacheInvalidateParallelDml(Oid relationId) +{ + Relation relation; + + if (OidIsValid(relationId)) + { + RelationIdCacheLookup(relationId, relation); + + if (relation) + relation->rd_paralleldml = 0; + } + else + { + HASH_SEQ_STATUS status; + RelIdCacheEnt *idhentry; + + hash_seq_init(&status, RelationIdCache); + + while ((idhentry = (RelIdCacheEnt *) hash_seq_search(&status)) != NULL) + idhentry->reldesc->rd_paralleldml = 0; + } +} + /* * RelationCacheInvalidate * Blow away cached relation descriptors that have zero reference counts, diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.h index 4d33fdcabe1..78da9005aa4 100644 --- a/src/include/storage/sinval.h +++ b/src/include/storage/sinval.h @@ -28,6 +28,8 @@ * * invalidate the mapped-relation mapping for a given database * * invalidate any saved snapshot that might be used to scan a given relation * * invalidate a RelationSyncCache entry for a specific relation + * * invalidate the cached parallel DML safety hazard level of one or all + * relcache entries * More types could be added if needed. The message type is identified by * the first "int8" field of the message struct. Zero or positive means a * specific-catcache inval message (and also serves as the catcache ID field). @@ -121,6 +123,15 @@ typedef struct * RelationSyncCache */ } SharedInvalRelSyncMsg; +#define SHAREDINVALPARALLELDML_ID (-7) + +typedef struct +{ + int8 id; /* type field --- must be first */ + Oid dbId; /* database ID */ + Oid relId; /* relation ID, or 0 for all relations */ +} SharedInvalParallelDmlMsg; + typedef union { int8 id; /* type field --- must be first */ @@ -131,6 +142,7 @@ typedef union SharedInvalRelmapMsg rm; SharedInvalSnapshotMsg sn; SharedInvalRelSyncMsg rs; + SharedInvalParallelDmlMsg pd; } SharedInvalidationMessage; diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h index 735e42f7310..9358e179c22 100644 --- a/src/include/utils/inval.h +++ b/src/include/utils/inval.h @@ -79,6 +79,10 @@ extern void CacheInvalidateRelSync(Oid relid); extern void CacheInvalidateRelSyncAll(void); +extern void CacheInvalidateParallelDmlSafety(Oid relId); + +extern void CacheInvalidateParallelDmlSafetyForAncestors(Oid relId); + extern void CacheInvalidateSmgr(RelFileLocatorBackend rlocator); extern void CacheInvalidateRelmap(Oid databaseId); diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index f6244d88eac..568977dbab1 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -137,6 +137,8 @@ extern void RelationForgetRelation(Oid rid); extern void RelationCacheInvalidateEntry(Oid relationId); +extern void RelationCacheInvalidateParallelDml(Oid relationId); + extern void RelationCacheInvalidate(bool debug_discard); #ifdef USE_ASSERT_CHECKING diff --git a/src/test/modules/test_parallel_dml_safety/expected/parallel_dml_safety.out b/src/test/modules/test_parallel_dml_safety/expected/parallel_dml_safety.out index c9c83d39d9e..8cff6a8eb6b 100644 --- a/src/test/modules/test_parallel_dml_safety/expected/parallel_dml_safety.out +++ b/src/test/modules/test_parallel_dml_safety/expected/parallel_dml_safety.out @@ -190,3 +190,380 @@ SELECT test_parallel_dml_safety('pdml_temp'); r (1 row) +-- +-- Invalidation tests +-- +-- Note that adding or dropping a trigger, constraint, index or default on a +-- table itself causes a full relcache invalidation of that table, so those +-- cases are not very interesting here. The cases below are the ones where +-- no full relcache invalidation happens: changes to a partition (which must +-- propagate to its ancestors) and changes of a function's parallel safety. +-- (Domain constraint changes need no invalidation, since domain constraints +-- are not part of the cached hazard level.) +-- +-- adding an unsafe trigger to a partition invalidates the ancestors' cached +-- hazard levels (via the propagated message), while the partition's own +-- value is discarded by its relcache rebuild +CREATE TABLE pdml_inv_part (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_part_p1 PARTITION OF pdml_inv_part FOR VALUES FROM (0) TO (100); +SELECT test_parallel_dml_safety('pdml_inv_part'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_part_p1'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +CREATE TRIGGER trg BEFORE INSERT ON pdml_inv_part_p1 + FOR EACH ROW EXECUTE FUNCTION pdml_trg_unsafe_fn(); +SELECT test_parallel_dml_safety_cached('pdml_inv_part_p1'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety_cached('pdml_inv_part'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_part'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +DROP TRIGGER trg ON pdml_inv_part_p1; +SELECT test_parallel_dml_safety_cached('pdml_inv_part'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_part'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- ... and propagation works across multiple levels +CREATE TABLE pdml_inv_gp (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_p PARTITION OF pdml_inv_gp FOR VALUES FROM (0) TO (100) + PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_p_p1 PARTITION OF pdml_inv_p FOR VALUES FROM (0) TO (50); +SELECT test_parallel_dml_safety('pdml_inv_gp'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_p'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_p_p1'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER TABLE pdml_inv_p_p1 ADD CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a)); +SELECT test_parallel_dml_safety_cached('pdml_inv_p_p1'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety_cached('pdml_inv_p'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety_cached('pdml_inv_gp'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_gp'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_p'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +ALTER TABLE pdml_inv_p_p1 DROP CONSTRAINT chk_u; +SELECT test_parallel_dml_safety('pdml_inv_gp'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- an unsafe index on a partition likewise propagates to the parent +CREATE TABLE pdml_inv_idx (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_idx_p1 PARTITION OF pdml_inv_idx FOR VALUES FROM (0) TO (100); +SELECT test_parallel_dml_safety('pdml_inv_idx'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +CREATE INDEX pdml_inv_idx_p1_i ON pdml_inv_idx_p1 ((CASE WHEN pdml_unsafe_fn(a) THEN a END)); +SELECT test_parallel_dml_safety_cached('pdml_inv_idx_p1'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety_cached('pdml_inv_idx'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_inv_idx'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +DROP INDEX pdml_inv_idx_p1_i; +SELECT test_parallel_dml_safety('pdml_inv_idx'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- attaching a partition that has an unsafe object invalidates the values +-- cached for all of its ancestors (the parent itself is covered by the +-- relcache rebuild in StorePartitionBound; the grandparent only by the +-- propagated message) +CREATE TABLE pdml_att_gp (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_att_p PARTITION OF pdml_att_gp FOR VALUES FROM (0) TO (100) + PARTITION BY RANGE (a); +SELECT test_parallel_dml_safety('pdml_att_gp'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +SELECT test_parallel_dml_safety('pdml_att_p'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +CREATE TABLE pdml_att_child (a int, CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a))); +ALTER TABLE pdml_att_p ATTACH PARTITION pdml_att_child FOR VALUES FROM (0) TO (50); +SELECT test_parallel_dml_safety_cached('pdml_att_p'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety_cached('pdml_att_gp'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_att_gp'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +SELECT test_parallel_dml_safety('pdml_att_p'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +-- detaching it invalidates them again +ALTER TABLE pdml_att_p DETACH PARTITION pdml_att_child; +SELECT test_parallel_dml_safety('pdml_att_gp'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +SELECT test_parallel_dml_safety('pdml_att_p'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- altering a function's parallel safety invalidates all cached values +CREATE FUNCTION pdml_alterable_fn(int) RETURNS bool LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE + AS $$ BEGIN RETURN $1 > 0; END $$; +CREATE TABLE pdml_inv_fn (a int CHECK (pdml_alterable_fn(a))); +SELECT test_parallel_dml_safety('pdml_inv_fn'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER FUNCTION pdml_alterable_fn(int) PARALLEL UNSAFE; +SELECT test_parallel_dml_safety('pdml_inv_fn'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +CREATE OR REPLACE FUNCTION pdml_alterable_fn(int) RETURNS bool LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE + AS $$ BEGIN RETURN $1 > 0; END $$; +SELECT test_parallel_dml_safety('pdml_inv_fn'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- adding an unsafe object to a table itself needs no invalidation message: +-- each of these DDL commands updates the table's pg_class/pg_attribute row, +-- which forces a relcache rebuild that discards the cached value (and +-- invalidates dependent plans via the relcache invalidation) +CREATE TABLE pdml_self (a int); +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +CREATE TRIGGER trg BEFORE INSERT ON pdml_self + FOR EACH ROW EXECUTE FUNCTION pdml_trg_unsafe_fn(); +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +DROP TRIGGER trg ON pdml_self; +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER TABLE pdml_self ADD CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a)); +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +ALTER TABLE pdml_self DROP CONSTRAINT chk_u; +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +CREATE INDEX pdml_self_i ON pdml_self ((CASE WHEN pdml_unsafe_fn(a) THEN a END)); +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +DROP INDEX pdml_self_i; +SELECT test_parallel_dml_safety_cached('pdml_self'); + test_parallel_dml_safety_cached +--------------------------------- + 0 +(1 row) + +SELECT test_parallel_dml_safety('pdml_self'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- column default changes are reflected too +CREATE TABLE pdml_inv_def (a int); +SELECT test_parallel_dml_safety('pdml_inv_def'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER TABLE pdml_inv_def ALTER COLUMN a SET DEFAULT (CASE WHEN pdml_unsafe_fn(0) THEN 1 ELSE 0 END); +SELECT test_parallel_dml_safety('pdml_inv_def'); + test_parallel_dml_safety +-------------------------- + u +(1 row) + +ALTER TABLE pdml_inv_def ALTER COLUMN a DROP DEFAULT; +SELECT test_parallel_dml_safety('pdml_inv_def'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +-- adding or dropping a domain constraint does not change the cached hazard +-- level, since domain constraints are not examined at all +CREATE DOMAIN pdml_inv_dom AS int; +CREATE TABLE pdml_inv_dom_t (a pdml_inv_dom); +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER DOMAIN pdml_inv_dom ADD CONSTRAINT dom_u CHECK (pdml_unsafe_fn(VALUE)); +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + +ALTER DOMAIN pdml_inv_dom DROP CONSTRAINT dom_u; +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); + test_parallel_dml_safety +-------------------------- + s +(1 row) + diff --git a/src/test/modules/test_parallel_dml_safety/sql/parallel_dml_safety.sql b/src/test/modules/test_parallel_dml_safety/sql/parallel_dml_safety.sql index f502cf793d3..9ae8d2c257c 100644 --- a/src/test/modules/test_parallel_dml_safety/sql/parallel_dml_safety.sql +++ b/src/test/modules/test_parallel_dml_safety/sql/parallel_dml_safety.sql @@ -107,3 +107,136 @@ SELECT test_parallel_dml_safety('pdml_ft'); CREATE TEMP TABLE pdml_temp (a int); SELECT test_parallel_dml_safety('pdml_temp'); + +-- +-- Invalidation tests +-- +-- Note that adding or dropping a trigger, constraint, index or default on a +-- table itself causes a full relcache invalidation of that table, so those +-- cases are not very interesting here. The cases below are the ones where +-- no full relcache invalidation happens: changes to a partition (which must +-- propagate to its ancestors) and changes of a function's parallel safety. +-- (Domain constraint changes need no invalidation, since domain constraints +-- are not part of the cached hazard level.) +-- + +-- adding an unsafe trigger to a partition invalidates the ancestors' cached +-- hazard levels (via the propagated message), while the partition's own +-- value is discarded by its relcache rebuild +CREATE TABLE pdml_inv_part (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_part_p1 PARTITION OF pdml_inv_part FOR VALUES FROM (0) TO (100); +SELECT test_parallel_dml_safety('pdml_inv_part'); +SELECT test_parallel_dml_safety('pdml_inv_part_p1'); +CREATE TRIGGER trg BEFORE INSERT ON pdml_inv_part_p1 + FOR EACH ROW EXECUTE FUNCTION pdml_trg_unsafe_fn(); +SELECT test_parallel_dml_safety_cached('pdml_inv_part_p1'); +SELECT test_parallel_dml_safety_cached('pdml_inv_part'); +SELECT test_parallel_dml_safety('pdml_inv_part'); +DROP TRIGGER trg ON pdml_inv_part_p1; +SELECT test_parallel_dml_safety_cached('pdml_inv_part'); +SELECT test_parallel_dml_safety('pdml_inv_part'); + +-- ... and propagation works across multiple levels +CREATE TABLE pdml_inv_gp (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_p PARTITION OF pdml_inv_gp FOR VALUES FROM (0) TO (100) + PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_p_p1 PARTITION OF pdml_inv_p FOR VALUES FROM (0) TO (50); +SELECT test_parallel_dml_safety('pdml_inv_gp'); +SELECT test_parallel_dml_safety('pdml_inv_p'); +SELECT test_parallel_dml_safety('pdml_inv_p_p1'); +ALTER TABLE pdml_inv_p_p1 ADD CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a)); +SELECT test_parallel_dml_safety_cached('pdml_inv_p_p1'); +SELECT test_parallel_dml_safety_cached('pdml_inv_p'); +SELECT test_parallel_dml_safety_cached('pdml_inv_gp'); +SELECT test_parallel_dml_safety('pdml_inv_gp'); +SELECT test_parallel_dml_safety('pdml_inv_p'); +ALTER TABLE pdml_inv_p_p1 DROP CONSTRAINT chk_u; +SELECT test_parallel_dml_safety('pdml_inv_gp'); + +-- an unsafe index on a partition likewise propagates to the parent +CREATE TABLE pdml_inv_idx (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_inv_idx_p1 PARTITION OF pdml_inv_idx FOR VALUES FROM (0) TO (100); +SELECT test_parallel_dml_safety('pdml_inv_idx'); +CREATE INDEX pdml_inv_idx_p1_i ON pdml_inv_idx_p1 ((CASE WHEN pdml_unsafe_fn(a) THEN a END)); +SELECT test_parallel_dml_safety_cached('pdml_inv_idx_p1'); +SELECT test_parallel_dml_safety_cached('pdml_inv_idx'); +SELECT test_parallel_dml_safety('pdml_inv_idx'); +DROP INDEX pdml_inv_idx_p1_i; +SELECT test_parallel_dml_safety('pdml_inv_idx'); + +-- attaching a partition that has an unsafe object invalidates the values +-- cached for all of its ancestors (the parent itself is covered by the +-- relcache rebuild in StorePartitionBound; the grandparent only by the +-- propagated message) +CREATE TABLE pdml_att_gp (a int) PARTITION BY RANGE (a); +CREATE TABLE pdml_att_p PARTITION OF pdml_att_gp FOR VALUES FROM (0) TO (100) + PARTITION BY RANGE (a); +SELECT test_parallel_dml_safety('pdml_att_gp'); +SELECT test_parallel_dml_safety('pdml_att_p'); +CREATE TABLE pdml_att_child (a int, CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a))); +ALTER TABLE pdml_att_p ATTACH PARTITION pdml_att_child FOR VALUES FROM (0) TO (50); +SELECT test_parallel_dml_safety_cached('pdml_att_p'); +SELECT test_parallel_dml_safety_cached('pdml_att_gp'); +SELECT test_parallel_dml_safety('pdml_att_gp'); +SELECT test_parallel_dml_safety('pdml_att_p'); +-- detaching it invalidates them again +ALTER TABLE pdml_att_p DETACH PARTITION pdml_att_child; +SELECT test_parallel_dml_safety('pdml_att_gp'); +SELECT test_parallel_dml_safety('pdml_att_p'); + +-- altering a function's parallel safety invalidates all cached values +CREATE FUNCTION pdml_alterable_fn(int) RETURNS bool LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE + AS $$ BEGIN RETURN $1 > 0; END $$; +CREATE TABLE pdml_inv_fn (a int CHECK (pdml_alterable_fn(a))); +SELECT test_parallel_dml_safety('pdml_inv_fn'); +ALTER FUNCTION pdml_alterable_fn(int) PARALLEL UNSAFE; +SELECT test_parallel_dml_safety('pdml_inv_fn'); +CREATE OR REPLACE FUNCTION pdml_alterable_fn(int) RETURNS bool LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE + AS $$ BEGIN RETURN $1 > 0; END $$; +SELECT test_parallel_dml_safety('pdml_inv_fn'); + +-- adding an unsafe object to a table itself needs no invalidation message: +-- each of these DDL commands updates the table's pg_class/pg_attribute row, +-- which forces a relcache rebuild that discards the cached value (and +-- invalidates dependent plans via the relcache invalidation) +CREATE TABLE pdml_self (a int); +SELECT test_parallel_dml_safety('pdml_self'); +CREATE TRIGGER trg BEFORE INSERT ON pdml_self + FOR EACH ROW EXECUTE FUNCTION pdml_trg_unsafe_fn(); +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); +DROP TRIGGER trg ON pdml_self; +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); + +ALTER TABLE pdml_self ADD CONSTRAINT chk_u CHECK (pdml_unsafe_fn(a)); +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); +ALTER TABLE pdml_self DROP CONSTRAINT chk_u; +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); + +CREATE INDEX pdml_self_i ON pdml_self ((CASE WHEN pdml_unsafe_fn(a) THEN a END)); +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); +DROP INDEX pdml_self_i; +SELECT test_parallel_dml_safety_cached('pdml_self'); +SELECT test_parallel_dml_safety('pdml_self'); + +-- column default changes are reflected too +CREATE TABLE pdml_inv_def (a int); +SELECT test_parallel_dml_safety('pdml_inv_def'); +ALTER TABLE pdml_inv_def ALTER COLUMN a SET DEFAULT (CASE WHEN pdml_unsafe_fn(0) THEN 1 ELSE 0 END); +SELECT test_parallel_dml_safety('pdml_inv_def'); +ALTER TABLE pdml_inv_def ALTER COLUMN a DROP DEFAULT; +SELECT test_parallel_dml_safety('pdml_inv_def'); + +-- adding or dropping a domain constraint does not change the cached hazard +-- level, since domain constraints are not examined at all +CREATE DOMAIN pdml_inv_dom AS int; +CREATE TABLE pdml_inv_dom_t (a pdml_inv_dom); +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); +ALTER DOMAIN pdml_inv_dom ADD CONSTRAINT dom_u CHECK (pdml_unsafe_fn(VALUE)); +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); +ALTER DOMAIN pdml_inv_dom DROP CONSTRAINT dom_u; +SELECT test_parallel_dml_safety('pdml_inv_dom_t'); diff --git a/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety--1.0.sql b/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety--1.0.sql index 9cf44f51b9a..6d587be6117 100644 --- a/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety--1.0.sql +++ b/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety--1.0.sql @@ -4,3 +4,8 @@ CREATE FUNCTION test_parallel_dml_safety(regclass) RETURNS "char" AS 'MODULE_PATHNAME' LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_parallel_dml_safety_cached(regclass) +RETURNS "char" +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety.c b/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety.c index 8a8d2d6f48a..50556f0802a 100644 --- a/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety.c +++ b/src/test/modules/test_parallel_dml_safety/test_parallel_dml_safety.c @@ -26,6 +26,7 @@ PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(test_parallel_dml_safety); +PG_FUNCTION_INFO_V1(test_parallel_dml_safety_cached); /* * Return the relation's parallel DML safety hazard level ('s', 'r' or 'u'), @@ -44,3 +45,23 @@ test_parallel_dml_safety(PG_FUNCTION_ARGS) PG_RETURN_CHAR(hazard); } + +/* + * Return the relation's cached parallel DML safety hazard level ('s', 'r' + * or 'u'), or '0' if it has not been computed yet. Unlike + * test_parallel_dml_safety(), this does not compute the value, so tests can + * tell exactly which cached values an invalidation has discarded. + */ +Datum +test_parallel_dml_safety_cached(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + Relation rel; + char hazard; + + rel = table_open(relid, AccessShareLock); + hazard = rel->rd_paralleldml; + table_close(rel, AccessShareLock); + + PG_RETURN_CHAR(hazard == 0 ? '0' : hazard); +} -- 2.43.0