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)

Reply via email to