From 488f95f343769cb8bce08ac4797804f5f9463529 Mon Sep 17 00:00:00 2001 From: Zhijie Hou 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