While testing the RI fixes with my AI harness, I found a separate invalidation hole in plancache.c. A prepared statement keeps using the old cast function after the cast is replaced, while direct SQL uses the new one. This reproduces without the RI patches, with both functions created before prepare.
Attached is a standalone fix registering CASTSOURCETARGET with PlanCacheSysCallback, plus a regression test. I reproduced it on master, PG18 and PG14, including generic/custom plans and DDL from another session. Regression and isolation pass with the fix on all three. The patch applies unchanged to current PG14–19 and master. PG15–17 and PG19 were checked for source and patch applicability only. Thanks, Nik
From 82e8de67a13796b26a954909251756cdf091bcda Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov <[email protected]> Date: Mon, 14 Sep 2026 16:58:00 -0700 Subject: [PATCH] Invalidate cached plans when casts change Parse analysis replaces a cast with its selected coercion expression, so a cached plan can depend on the old cast function without retaining a dependency on the pg_cast row that selected it. Dropping and recreating the cast with a different existing function then leaves prepared statements using the old coercion even though freshly parsed SQL uses the new one. Register CASTSOURCETARGET with PlanCacheSysCallback, conservatively invalidating saved plans and cached expressions as for other catalogs whose individual dependencies are not tracked. Add a regression test that creates both cast functions before preparing the statement, so pg_proc invalidation cannot hide the missing pg_cast invalidation. --- src/backend/utils/cache/plancache.c | 1 + src/test/regress/expected/plancache.out | 29 +++++++++++++++++++++++++ src/test/regress/sql/plancache.sql | 20 +++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index a1b406cee29..e3efe3829a1 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -158,6 +158,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(TYPEOID, PlanCacheObjectCallback, (Datum) 0); CacheRegisterSyscacheCallback(NAMESPACEOID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(OPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(CASTSOURCETARGET, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); diff --git a/src/test/regress/expected/plancache.out b/src/test/regress/expected/plancache.out index d58534ca1cd..d4b564b672a 100644 --- a/src/test/regress/expected/plancache.out +++ b/src/test/regress/expected/plancache.out @@ -402,3 +402,32 @@ select name, generic_plans, custom_plans from pg_prepared_statements (1 row) drop table test_mode; +-- Replacing a cast must invalidate plans that used the old cast function. +-- Create both functions first so pg_proc invalidation cannot mask the issue. +create type pc_cast_arg as (v int); +create function pc_cast_old(pc_cast_arg) returns int + language sql immutable strict as 'select ($1).v'; +create function pc_cast_new(pc_cast_arg) returns int + language sql immutable strict as 'select ($1).v + 100'; +create cast (pc_cast_arg as int) + with function pc_cast_old(pc_cast_arg) as implicit; +prepare pc_cast_plan(pc_cast_arg) as select $1::int; +execute pc_cast_plan(row(1)::pc_cast_arg); + int4 +------ + 1 +(1 row) + +drop cast (pc_cast_arg as int); +create cast (pc_cast_arg as int) + with function pc_cast_new(pc_cast_arg) as implicit; +execute pc_cast_plan(row(1)::pc_cast_arg); + int4 +------ + 101 +(1 row) + +deallocate pc_cast_plan; +drop cast (pc_cast_arg as int); +drop function pc_cast_old(pc_cast_arg), pc_cast_new(pc_cast_arg); +drop type pc_cast_arg; diff --git a/src/test/regress/sql/plancache.sql b/src/test/regress/sql/plancache.sql index aed388d03a1..fbafc73b246 100644 --- a/src/test/regress/sql/plancache.sql +++ b/src/test/regress/sql/plancache.sql @@ -228,3 +228,23 @@ select name, generic_plans, custom_plans from pg_prepared_statements where name = 'test_mode_pp'; drop table test_mode; + +-- Replacing a cast must invalidate plans that used the old cast function. +-- Create both functions first so pg_proc invalidation cannot mask the issue. +create type pc_cast_arg as (v int); +create function pc_cast_old(pc_cast_arg) returns int + language sql immutable strict as 'select ($1).v'; +create function pc_cast_new(pc_cast_arg) returns int + language sql immutable strict as 'select ($1).v + 100'; +create cast (pc_cast_arg as int) + with function pc_cast_old(pc_cast_arg) as implicit; +prepare pc_cast_plan(pc_cast_arg) as select $1::int; +execute pc_cast_plan(row(1)::pc_cast_arg); +drop cast (pc_cast_arg as int); +create cast (pc_cast_arg as int) + with function pc_cast_new(pc_cast_arg) as implicit; +execute pc_cast_plan(row(1)::pc_cast_arg); +deallocate pc_cast_plan; +drop cast (pc_cast_arg as int); +drop function pc_cast_old(pc_cast_arg), pc_cast_new(pc_cast_arg); +drop type pc_cast_arg; base-commit: a625fc570c22e199471e1a2656e2c32b8dc0c0fd -- 2.50.1 (Apple Git-155)
