From 488f95f343769cb8bce08ac4797804f5f9463529 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Mon, 3 Aug 2026 16:28:10 +0800
Subject: [PATCH v1 4/4] Invalidate cached plans on parallel DML safety changes

A cached plan for INSERT ... SELECT embeds parallel-safety assumptions:
the plan-shape decision that the query can run in parallel mode, which
depends on the parallel DML safety of the target relation.  Previously,
such a plan was only invalidated when the change happened on the target
table itself (via the accompanying full relcache invalidation), while
altering a function's parallel safety when the function is only used by
the target's triggers, constraints, indexes or defaults left it stale.

This commit makes the parallel DML safety invalidation message
(SHAREDINVALPARALLELDML_ID) also invalidate affected cached generic
plans when it is processed.

The insert_parallel tests now verify both directions: a cached parallel
plan becomes serial after altering a trigger function to
parallel-unsafe, and becomes parallel again after the change is
reverted; and a plan that is serial because the query's own function is
parallel-unsafe is rebuilt as a parallel plan when the function becomes
parallel-safe.
---
 src/backend/optimizer/plan/planner.c          |  15 ++
 src/backend/utils/cache/inval.c               |  51 ++++-
 src/backend/utils/cache/plancache.c           |  89 ++++++++
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/nodes/plannodes.h                 |  11 +
 src/include/utils/inval.h                     |   6 +
 src/test/regress/expected/insert_parallel.out | 203 ++++++++++++++++++
 src/test/regress/sql/insert_parallel.sql      | 115 ++++++++++
 8 files changed, 492 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0439375ba93..1eef97f8fd5 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -386,6 +386,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	glob->lastPlanNodeId = 0;
 	glob->transientPlan = false;
 	glob->dependsOnRole = false;
+	glob->dependsOnParallelDmlSafety = false;
 	glob->partition_directory = NULL;
 	glob->rel_notnullatts_hash = NULL;
 
@@ -425,6 +426,19 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 		/* all the cheap tests pass, so scan the query tree */
 		glob->maxParallelHazard = max_parallel_hazard(parse);
 		glob->parallelModeOK = (glob->maxParallelHazard != PROPARALLEL_UNSAFE);
+
+		/*
+		 * For an INSERT ... SELECT admitted to parallel mode, remember that
+		 * this plan's parallel-safety assumptions can be invalidated later:
+		 * the plan may need to be rebuilt when the parallel DML safety of
+		 * the target relation changes.  The flag is deliberately also set
+		 * when the query tree itself was already parallel-unsafe (so the
+		 * plan is serial and the target's hazard was never consulted):
+		 * a function becoming parallel-safe later must still be able to
+		 * rebuild the plan, possibly into a parallel plan this time.
+		 */
+		if (is_parallel_allowed_for_modify(parse))
+			glob->dependsOnParallelDmlSafety = true;
 	}
 	else
 	{
@@ -668,6 +682,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->transientPlan = glob->transientPlan;
 	result->dependsOnRole = glob->dependsOnRole;
 	result->parallelModeNeeded = glob->parallelModeNeeded;
+	result->dependsOnParallelDmlSafety = glob->dependsOnParallelDmlSafety;
 	result->planTree = top_plan;
 	result->partPruneInfos = glob->partPruneInfos;
 	result->rtable = glob->finalrtable;
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 9c2cfd3e226..8c034c54cf3 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -273,6 +273,7 @@ int			debug_discard_caches = 0;
 #define MAX_SYSCACHE_CALLBACKS 64
 #define MAX_RELCACHE_CALLBACKS 10
 #define MAX_RELSYNC_CALLBACKS 10
+#define MAX_PARALLELDML_CALLBACKS 10
 
 static struct SYSCACHECALLBACK
 {
@@ -302,6 +303,14 @@ static struct RELSYNCCALLBACK
 
 static int	relsync_callback_count = 0;
 
+static struct PARALLELDMLCALLBACK
+{
+	ParallelDmlCallbackFunction function;
+	Datum		arg;
+}			paralleldml_callback_list[MAX_PARALLELDML_CALLBACKS];
+
+static int	paralleldml_callback_count = 0;
+
 
 /* ----------------------------------------------------------------
  *				Invalidation subgroup support functions
@@ -962,7 +971,15 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg)
 	{
 		/* We only care about our own database */
 		if (msg->pd.dbId == MyDatabaseId)
+		{
 			RelationCacheInvalidateParallelDml(msg->pd.relId);
+
+			/*
+			 * Let other subsystems (e.g. the plan cache) know about the
+			 * change in parallel DML safety.
+			 */
+			CallParallelDmlCallbacks(msg->pd.relId);
+		}
 	}
 	else
 		elog(FATAL, "unrecognized SI message ID: %d", msg->id);
@@ -2004,6 +2021,24 @@ CacheRegisterRelSyncCallback(RelSyncCallbackFunction func,
 	++relsync_callback_count;
 }
 
+/*
+ * CacheRegisterParallelDmlCallback
+ *		Register the specified function to be called for all future
+ *		parallel DML safety invalidation events.
+ */
+void
+CacheRegisterParallelDmlCallback(ParallelDmlCallbackFunction func,
+								 Datum arg)
+{
+	if (paralleldml_callback_count >= MAX_PARALLELDML_CALLBACKS)
+		elog(FATAL, "out of paralleldml_callback_list slots");
+
+	paralleldml_callback_list[paralleldml_callback_count].function = func;
+	paralleldml_callback_list[paralleldml_callback_count].arg = arg;
+
+	++paralleldml_callback_count;
+}
+
 /*
  * CallSyscacheCallbacks
  *
@@ -2030,7 +2065,7 @@ CallSyscacheCallbacks(SysCacheIdentifier cacheid, uint32 hashvalue)
 }
 
 /*
- * CallSyscacheCallbacks
+ * CallRelSyncCallbacks
  */
 void
 CallRelSyncCallbacks(Oid relid)
@@ -2043,6 +2078,20 @@ CallRelSyncCallbacks(Oid relid)
 	}
 }
 
+/*
+ * CallParallelDmlCallbacks
+ */
+void
+CallParallelDmlCallbacks(Oid relId)
+{
+	for (int i = 0; i < paralleldml_callback_count; i++)
+	{
+		struct PARALLELDMLCALLBACK *ccitem = paralleldml_callback_list + i;
+
+		ccitem->function(ccitem->arg, relId);
+	}
+}
+
 /*
  * LogLogicalInvalidations
  *
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 26f1bd64515..c125da4876c 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -63,6 +63,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
 #include "tcop/pquery.h"
@@ -106,6 +107,7 @@ static void ScanQueryForLocks(Query *parsetree, bool acquire);
 static bool ScanQueryWalker(Node *node, bool *acquire);
 static TupleDesc PlanCacheComputeResultDesc(List *stmt_list);
 static void PlanCacheRelCallback(Datum arg, Oid relid);
+static void PlanCacheParallelDmlCallback(Datum arg, Oid relId);
 static void PlanCacheObjectCallback(Datum arg, SysCacheIdentifier cacheid,
 									uint32 hashvalue);
 static void PlanCacheSysCallback(Datum arg, SysCacheIdentifier cacheid,
@@ -148,6 +150,7 @@ void
 InitPlanCache(void)
 {
 	CacheRegisterRelcacheCallback(PlanCacheRelCallback, (Datum) 0);
+	CacheRegisterParallelDmlCallback(PlanCacheParallelDmlCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(PROCOID, PlanCacheObjectCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(TYPEOID, PlanCacheObjectCallback, (Datum) 0);
 	CacheRegisterSyscacheCallback(NAMESPACEOID, PlanCacheSysCallback, (Datum) 0);
@@ -2199,6 +2202,92 @@ PlanCacheRelCallback(Datum arg, Oid relid)
 	}
 }
 
+/*
+ * PlanCacheParallelDmlCallback
+ *		Parallel DML safety inval callback function
+ *
+ * Invalidate cached generic plans whose shape depended on the parallel
+ * DML safety of the given relation (i.e. that have
+ * dependsOnParallelDmlSafety set), or on the parallel DML safety of any
+ * relation when relId is InvalidOid, meaning that some function's
+ * parallel safety changed.
+ *
+ * The hazard level cached in relcache is being reset, so any plan that
+ * was shaped by it must be rebuilt.  Only generic plans are affected;
+ * custom plans are rebuilt on each use anyway, so they always see the
+ * current safety.
+ *
+ * Note that the global (InvalidOid) case invalidates every
+ * parallel-DML-safety-dependent plan in this database, even if the altered
+ * function is not actually used by the plan's target table.  That is
+ * intentionally coarse: tracking exactly which tables use a function would
+ * require locking and visibility handling we deliberately avoid, and
+ * changing a function's parallel safety is expected to be rare.
+ */
+static void
+PlanCacheParallelDmlCallback(Datum arg, Oid relId)
+{
+	dlist_iter	iter;
+
+	dlist_foreach(iter, &saved_plan_list)
+	{
+		CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+													   node, iter.cur);
+		ListCell   *lc;
+
+		Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
+
+		/* No work if it's already invalidated */
+		if (!plansource->is_valid)
+			continue;
+
+		/* Never invalidate if parse/plan would be a no-op anyway */
+		if (!StmtPlanRequiresRevalidation(plansource))
+			continue;
+
+		/*
+		 * The dependency is a planning-time decision, so only the generic
+		 * plan needs to be rebuilt; the rewritten querytree is unaffected.
+		 */
+		if (plansource->gplan && plansource->gplan->is_valid)
+		{
+			foreach(lc, plansource->gplan->stmt_list)
+			{
+				PlannedStmt *plannedstmt = lfirst_node(PlannedStmt, lc);
+
+				if (!plannedstmt->dependsOnParallelDmlSafety)
+					continue;
+
+				if (OidIsValid(relId))
+				{
+					ModifyTable *mt;
+					RangeTblEntry *rte;
+
+					/*
+					 * The plan is affected only if its modify target is the
+					 * relation whose parallel DML safety changed.  For a
+					 * partitioned target, a change on any partition is
+					 * reported as a change on the partitioned root itself
+					 * (see CacheInvalidateParallelDmlSafetyForAncestors),
+					 * so partitions are covered without having to record
+					 * them in the plan.
+					 */
+					if (!IsA(plannedstmt->planTree, ModifyTable))
+						continue;
+					mt = (ModifyTable *) plannedstmt->planTree;
+					rte = rt_fetch(mt->nominalRelation, plannedstmt->rtable);
+					if (rte->relid != relId)
+						continue;
+				}
+
+				/* Invalidate the generic plan only */
+				plansource->gplan->is_valid = false;
+				break;			/* out of stmt_list scan */
+			}
+		}
+	}
+}
+
 /*
  * PlanCacheObjectCallback
  *		Syscache inval callback function for PROCOID and TYPEOID caches
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c6815b7..f7161d8aa79 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -256,6 +256,9 @@ typedef struct PlannerGlobal
 	/* parallel mode actually required? */
 	bool		parallelModeNeeded;
 
+	/* did we consult a modify target's parallel DML safety? */
+	bool		dependsOnParallelDmlSafety;
+
 	/* worst PROPARALLEL hazard level */
 	char		maxParallelHazard;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b880ce0f4be..ccb724f8b85 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -92,6 +92,17 @@ typedef struct PlannedStmt
 	/* parallel mode required to execute? */
 	bool		parallelModeNeeded;
 
+	/*
+	 * Can the plan's parallel-safety assumptions be invalidated by a
+	 * parallel DML safety change?  This is set for every INSERT ... SELECT
+	 * admitted to the parallel-mode gate (even when the plan ended up
+	 * serial because the query tree was parallel-unsafe), so that the plan
+	 * is rebuilt when the parallel DML safety of the target relation
+	 * changes, or when any function's parallel safety changes; see
+	 * PlanCacheParallelDmlCallback().
+	 */
+	bool		dependsOnParallelDmlSafety;
+
 	/* which forms of JIT should be performed */
 	int			jitFlags;
 
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 9358e179c22..d289ed2add2 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -43,6 +43,7 @@ typedef void (*SyscacheCallbackFunction) (Datum arg, SysCacheIdentifier cacheid,
 										  uint32 hashvalue);
 typedef void (*RelcacheCallbackFunction) (Datum arg, Oid relid);
 typedef void (*RelSyncCallbackFunction) (Datum arg, Oid relid);
+typedef void (*ParallelDmlCallbackFunction) (Datum arg, Oid relId);
 
 
 extern void AcceptInvalidationMessages(void);
@@ -97,6 +98,11 @@ extern void CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
 extern void CacheRegisterRelSyncCallback(RelSyncCallbackFunction func,
 										 Datum arg);
 
+extern void CacheRegisterParallelDmlCallback(ParallelDmlCallbackFunction func,
+											 Datum arg);
+
+extern void CallParallelDmlCallbacks(Oid relId);
+
 extern void CallSyscacheCallbacks(SysCacheIdentifier cacheid, uint32 hashvalue);
 
 extern void CallRelSyncCallbacks(Oid relid);
diff --git a/src/test/regress/expected/insert_parallel.out b/src/test/regress/expected/insert_parallel.out
index 0fc70ce4c34..9f9ae44d826 100644
--- a/src/test/regress/expected/insert_parallel.out
+++ b/src/test/regress/expected/insert_parallel.out
@@ -353,3 +353,206 @@ select count(*) from para_insert;
 
 set debug_parallel_query = off;
 rollback;
+--
+-- Cached-plan invalidation when the parallel safety of the target table
+-- changes.  (This is outside the transaction block above, so that plan
+-- cache effects of previous statements don't leak in.)
+--
+-- A plan whose shape depended on the target table's parallel DML safety
+-- is marked with dependsOnParallelDmlSafety, and is invalidated whenever
+-- the parallel DML safety invalidation message arrives for its target
+-- (including messages propagated from partitions) or for all relations
+-- (a function's parallel safety changed).
+--
+-- encourage use of parallel plans (again, the ones above were reset by
+-- the rollback)
+set parallel_setup_cost = 0;
+set parallel_tuple_cost = 0;
+set min_parallel_table_scan_size = 0;
+set max_parallel_workers_per_gather = 4;
+-- force use of cached generic plans
+set plan_cache_mode = force_generic_plan;
+-- adding an unsafe trigger to the target table invalidates the cached
+-- plan (via the relcache invalidation of the table), so the next EXECUTE
+-- builds a new, non-parallel plan
+create table pi_plan_a (a int, b name);
+prepare pa as insert into pi_plan_a select unique1, stringu1 from tenk1;
+explain (costs off) execute pa;
+               QUERY PLAN               
+----------------------------------------
+ Insert on pi_plan_a
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+(4 rows)
+
+create trigger trg before insert on pi_plan_a
+	for each row execute function trg_unsafe_fn();
+explain (costs off) execute pa;
+       QUERY PLAN        
+-------------------------
+ Insert on pi_plan_a
+   ->  Seq Scan on tenk1
+(2 rows)
+
+deallocate pa;
+-- adding an unsafe trigger to a partition of the target table invalidates
+-- the cached plan: the invalidation message is propagated to the
+-- partitioned root, and the plan's modify target matches it; dropping the
+-- trigger invalidates it again, and it is rebuilt as a parallel plan
+create table pi_plan_b (a int, b name) partition by range (a);
+create table pi_plan_b_p1 partition of pi_plan_b for values from (0) to (5000);
+create table pi_plan_b_p2 partition of pi_plan_b for values from (5000) to (maxvalue);
+prepare pb as insert into pi_plan_b select unique1, stringu1 from tenk1;
+explain (costs off) execute pb;
+               QUERY PLAN               
+----------------------------------------
+ Insert on pi_plan_b
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+(4 rows)
+
+create trigger trg before insert on pi_plan_b_p1
+	for each row execute function trg_unsafe_fn();
+explain (costs off) execute pb;
+       QUERY PLAN        
+-------------------------
+ Insert on pi_plan_b
+   ->  Seq Scan on tenk1
+(2 rows)
+
+drop trigger trg on pi_plan_b_p1;
+explain (costs off) execute pb;
+               QUERY PLAN               
+----------------------------------------
+ Insert on pi_plan_b
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+(4 rows)
+
+deallocate pb;
+-- altering the parallel safety of a function used in the plan invalidates
+-- the cached plan (via the pg_proc invalidation)
+create function alterable_fn(int) returns bool
+language plpgsql immutable parallel safe as $$
+begin
+	return $1 > 0;
+end; $$;
+create table pi_plan_c (a int, b name);
+prepare pc as insert into pi_plan_c select unique1, stringu1 from tenk1
+	where alterable_fn(unique1);
+explain (costs off) execute pc;
+                 QUERY PLAN                  
+---------------------------------------------
+ Insert on pi_plan_c
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+               Filter: alterable_fn(unique1)
+(5 rows)
+
+alter function alterable_fn(int) parallel unsafe;
+explain (costs off) execute pc;
+              QUERY PLAN               
+---------------------------------------
+ Insert on pi_plan_c
+   ->  Seq Scan on tenk1
+         Filter: alterable_fn(unique1)
+(3 rows)
+
+deallocate pc;
+-- altering the parallel safety of a function that is only used by the
+-- target table's trigger invalidates the cached plan too: a function
+-- parallel-safety change invalidates all plans that consulted a target's
+-- hazard (intentionally coarse --- the plan is rebuilt even though this
+-- function might be unrelated to the plan's target); altering it back
+-- rebuilds the parallel plan
+create function trg_alterable_fn() returns trigger
+language plpgsql parallel safe as $$
+begin
+	return new;
+end; $$;
+create table pi_plan_d (a int, b name);
+create trigger trg before insert on pi_plan_d
+	for each row execute function trg_alterable_fn();
+prepare pd as insert into pi_plan_d select unique1, stringu1 from tenk1;
+explain (costs off) execute pd;
+               QUERY PLAN               
+----------------------------------------
+ Insert on pi_plan_d
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+(4 rows)
+
+alter function trg_alterable_fn() parallel unsafe;
+explain (costs off) execute pd;
+       QUERY PLAN        
+-------------------------
+ Insert on pi_plan_d
+   ->  Seq Scan on tenk1
+(2 rows)
+
+alter function trg_alterable_fn() parallel safe;
+explain (costs off) execute pd;
+               QUERY PLAN               
+----------------------------------------
+ Insert on pi_plan_d
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+(4 rows)
+
+execute pd;
+select count(*) from pi_plan_d;
+ count 
+-------
+ 10000
+(1 row)
+
+deallocate pd;
+-- the reverse direction also works: a plan built while the query's own
+-- function is parallel-unsafe (so the target's hazard was never even
+-- consulted) is rebuilt as a parallel plan when the function becomes
+-- parallel-safe
+create function unsafe_to_safe_fn(int) returns bool
+language plpgsql immutable parallel unsafe as $$
+begin
+	return $1 > 0;
+end; $$;
+prepare pe as insert into pi_plan_c select unique1, stringu1 from tenk1
+	where unsafe_to_safe_fn(unique1);
+explain (costs off) execute pe;
+                 QUERY PLAN                 
+--------------------------------------------
+ Insert on pi_plan_c
+   ->  Seq Scan on tenk1
+         Filter: unsafe_to_safe_fn(unique1)
+(3 rows)
+
+alter function unsafe_to_safe_fn(int) parallel safe;
+explain (costs off) execute pe;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Insert on pi_plan_c
+   ->  Gather
+         Workers Planned: 4
+         ->  Parallel Seq Scan on tenk1
+               Filter: unsafe_to_safe_fn(unique1)
+(5 rows)
+
+deallocate pe;
+reset plan_cache_mode;
+-- clean up objects created outside the transaction block
+drop table pi_plan_a, pi_plan_b, pi_plan_c, pi_plan_d;
+drop function alterable_fn(int);
+drop function trg_alterable_fn();
+drop function unsafe_to_safe_fn(int);
+drop function fullname_parallel_unsafe(text, text);
+drop function fullname_parallel_restricted(text, text);
+drop function bdefault_unsafe();
+drop function cdefault_restricted();
+drop function trg_unsafe_fn();
+drop function trg_restricted_fn();
diff --git a/src/test/regress/sql/insert_parallel.sql b/src/test/regress/sql/insert_parallel.sql
index 0c01780a5e3..8e24e5ef99d 100644
--- a/src/test/regress/sql/insert_parallel.sql
+++ b/src/test/regress/sql/insert_parallel.sql
@@ -204,3 +204,118 @@ select count(*) from para_insert;
 set debug_parallel_query = off;
 
 rollback;
+
+--
+-- Cached-plan invalidation when the parallel safety of the target table
+-- changes.  (This is outside the transaction block above, so that plan
+-- cache effects of previous statements don't leak in.)
+--
+-- A plan whose shape depended on the target table's parallel DML safety
+-- is marked with dependsOnParallelDmlSafety, and is invalidated whenever
+-- the parallel DML safety invalidation message arrives for its target
+-- (including messages propagated from partitions) or for all relations
+-- (a function's parallel safety changed).
+--
+
+-- encourage use of parallel plans (again, the ones above were reset by
+-- the rollback)
+set parallel_setup_cost = 0;
+set parallel_tuple_cost = 0;
+set min_parallel_table_scan_size = 0;
+set max_parallel_workers_per_gather = 4;
+
+-- force use of cached generic plans
+set plan_cache_mode = force_generic_plan;
+
+-- adding an unsafe trigger to the target table invalidates the cached
+-- plan (via the relcache invalidation of the table), so the next EXECUTE
+-- builds a new, non-parallel plan
+create table pi_plan_a (a int, b name);
+prepare pa as insert into pi_plan_a select unique1, stringu1 from tenk1;
+explain (costs off) execute pa;
+create trigger trg before insert on pi_plan_a
+	for each row execute function trg_unsafe_fn();
+explain (costs off) execute pa;
+deallocate pa;
+-- adding an unsafe trigger to a partition of the target table invalidates
+-- the cached plan: the invalidation message is propagated to the
+-- partitioned root, and the plan's modify target matches it; dropping the
+-- trigger invalidates it again, and it is rebuilt as a parallel plan
+create table pi_plan_b (a int, b name) partition by range (a);
+create table pi_plan_b_p1 partition of pi_plan_b for values from (0) to (5000);
+create table pi_plan_b_p2 partition of pi_plan_b for values from (5000) to (maxvalue);
+prepare pb as insert into pi_plan_b select unique1, stringu1 from tenk1;
+explain (costs off) execute pb;
+create trigger trg before insert on pi_plan_b_p1
+	for each row execute function trg_unsafe_fn();
+explain (costs off) execute pb;
+drop trigger trg on pi_plan_b_p1;
+explain (costs off) execute pb;
+deallocate pb;
+-- altering the parallel safety of a function used in the plan invalidates
+-- the cached plan (via the pg_proc invalidation)
+create function alterable_fn(int) returns bool
+language plpgsql immutable parallel safe as $$
+begin
+	return $1 > 0;
+end; $$;
+create table pi_plan_c (a int, b name);
+prepare pc as insert into pi_plan_c select unique1, stringu1 from tenk1
+	where alterable_fn(unique1);
+explain (costs off) execute pc;
+alter function alterable_fn(int) parallel unsafe;
+explain (costs off) execute pc;
+deallocate pc;
+
+-- altering the parallel safety of a function that is only used by the
+-- target table's trigger invalidates the cached plan too: a function
+-- parallel-safety change invalidates all plans that consulted a target's
+-- hazard (intentionally coarse --- the plan is rebuilt even though this
+-- function might be unrelated to the plan's target); altering it back
+-- rebuilds the parallel plan
+create function trg_alterable_fn() returns trigger
+language plpgsql parallel safe as $$
+begin
+	return new;
+end; $$;
+create table pi_plan_d (a int, b name);
+create trigger trg before insert on pi_plan_d
+	for each row execute function trg_alterable_fn();
+prepare pd as insert into pi_plan_d select unique1, stringu1 from tenk1;
+explain (costs off) execute pd;
+alter function trg_alterable_fn() parallel unsafe;
+explain (costs off) execute pd;
+alter function trg_alterable_fn() parallel safe;
+explain (costs off) execute pd;
+execute pd;
+select count(*) from pi_plan_d;
+deallocate pd;
+-- the reverse direction also works: a plan built while the query's own
+-- function is parallel-unsafe (so the target's hazard was never even
+-- consulted) is rebuilt as a parallel plan when the function becomes
+-- parallel-safe
+create function unsafe_to_safe_fn(int) returns bool
+language plpgsql immutable parallel unsafe as $$
+begin
+	return $1 > 0;
+end; $$;
+prepare pe as insert into pi_plan_c select unique1, stringu1 from tenk1
+	where unsafe_to_safe_fn(unique1);
+explain (costs off) execute pe;
+alter function unsafe_to_safe_fn(int) parallel safe;
+explain (costs off) execute pe;
+deallocate pe;
+
+reset plan_cache_mode;
+
+-- clean up objects created outside the transaction block
+drop table pi_plan_a, pi_plan_b, pi_plan_c, pi_plan_d;
+drop function alterable_fn(int);
+drop function trg_alterable_fn();
+drop function unsafe_to_safe_fn(int);
+drop function fullname_parallel_unsafe(text, text);
+drop function fullname_parallel_restricted(text, text);
+drop function bdefault_unsafe();
+drop function cdefault_restricted();
+drop function trg_unsafe_fn();
+drop function trg_restricted_fn();
-- 
2.43.0

