Re: [PATCH] Invalidate cached plans when casts change

From: Haibo Yan <tristan(dot)yim(at)gmail(dot)com>
To: Nikolay Samokhvalov <nik(at)postgres(dot)ai>
Cc: pgsql-hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>, Amit Langote <amitlangote09(at)gmail(dot)com>
Subject: Re: [PATCH] Invalidate cached plans when casts change
Date: 2026-09-16 03:27:37
Message-ID: CABXr29GKyUD2iy15viRNhAb4j4erg6oQ3xHueQi0qAXtTp9Tfw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Tue, Sep 15, 2026 at 2:01 AM Nikolay Samokhvalov <nik(at)postgres(dot)ai> wrote:
>
> For completeness, here's the exact reproducer I should have included:
>
> drop schema if exists cast_repro cascade;
> create schema cast_repro;
> set search_path = cast_repro, pg_catalog;
>
> create type key_t as (v int);
> create function cast_old(key_t) returns int
> language sql immutable strict as 'select ($1).v';
> create function cast_new(key_t) returns int
> language sql immutable strict as 'select ($1).v + 100';
>
> create cast (key_t as int) with function cast_old(key_t) as implicit;
> prepare q(key_t) as select $1::int;
> execute q(row(1)::key_t);
>
> drop cast (key_t as int);
> create cast (key_t as int) with function cast_new(key_t) as implicit;
> select row(1)::key_t::int as direct_after;
> execute q(row(1)::key_t);
>
> On unpatched master this returns 1, 101, 1. The last value should be 101.
> With the patch it returns 1, 101, 101.
>
> Thanks,
> Nik
>
>
Hi, Nikolay

Thanks for reporting this. I took a closer look at the reproducer and the
proposed fix, and I agree that this is a real plan-cache invalidation bug.

What made me look a bit further was the nature of the dependency. In the cast
case, the cached query can remember the object selected during parse analysis,
but that is not necessarily the same thing as remembering the catalog state
that caused that object to be selected. If that catalog state changes, fresh
parse analysis may make a different choice without modifying the object that
the cached query currently depends on.

I checked whether the same pattern exists elsewhere in parse-time resolution,
and found a similar problem with function candidate resolution.

For example:

CREATE FUNCTION f(bigint) RETURNS text
LANGUAGE sql AS $$ SELECT 'bigint' $$;

PREPARE q AS SELECT f(1);
EXECUTE q; -- bigint

CREATE FUNCTION f(int) RETURNS text
LANGUAGE sql AS $$ SELECT 'int' $$;

SELECT f(1); -- int
EXECUTE q; -- bigint

The cached query records a dependency on the function that was originally
selected, but function lookup depends on the whole candidate set. The newly
created `f(int)` obviously could not have been recorded as a dependency when
the query was analyzed.

I found a few other manifestations of the same issue. For example, creating a
same-signature function in an earlier, already-existing search_path schema can
leave a cached query using the function from the later schema. RENAME and SET
SCHEMA can similarly introduce a better visible candidate. There are also
in-place cases: changing the number of default arguments or changing variadic
status with CREATE OR REPLACE can change which calls a function is a candidate
for without changing its OID or its name/signature syscache key.

I put together a separate patch for this.

The existing precise PROCOID invalidation is left unchanged. Instead, catalog
operations that can change the callable candidate set send a broad invalidation
for the PROCNAMEARGSNSP syscache. The plan cache treats that as a reason to
discard cached query trees and redo parse analysis. Ordinary function changes
such as replacing the body or changing cost/strictness continue to use the
existing precise PROCOID invalidation and do not reset the whole plan cache.

The patch covers:

- creation of a new function/procedure/aggregate candidate;
- RENAME and SET SCHEMA;
- CREATE OR REPLACE changes to the number of default arguments;
- CREATE OR REPLACE changes to variadic status.

It intentionally does not send the broad invalidation for DROP: removing the
selected user-defined function is already covered by its PROCOID dependency,
while removing a candidate that lost the original resolution cannot change an
already-selected winner.

I added three regression cases:

1. adding a better overload in the same schema;
2. adding the same-signature function in an earlier, already-existing
search_path schema;
3. CREATE OR REPLACE adding a default argument so that fresh analysis
becomes ambiguous.

All three fail on current master without the patch and pass with it. The full
regression suite also passes with assertions enabled.

I've attached the patch separately since this is independent of the pg_cast fix.

Thanks again for finding the original invalidation hole — it turned out to
expose a broader distinction between dependencies on the selected object and
dependencies on the state used to resolve that object.

Best,
Haibo

Attachment Content-Type Size
v2-0001-Invalidate-cached-plans-when-the-pg_proc-candidate.patch application/octet-stream 17.8 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Amit Langote 2026-09-16 03:29:08 Re: SQL/JSON DEFAULT ON ERROR/ON EMPTY evaluation fail should rethrow error unconditionally
Previous Message Masahiko Sawada 2026-09-16 03:25:43 Re: Proposal: Conflict log history table for Logical Replication