From 2e09cf307dfca3a90dfe83558dc01deb58cdf36c Mon Sep 17 00:00:00 2001 From: Meemaw Date: Thu, 8 Oct 2026 21:27:40 +0200 Subject: [PATCH] Evict cached statements on schema change for tables with 10+ columns TABLE_SCHEMA_CHANGE_ERROR_MESSAGE_PATTERN matched the column counts with \d, a single digit, and String.matches requires a full match. So "bind message has 10 result formats but query has 11 columns" never matched, the stale cached statement was never evicted, and every later execution on the connection failed with the same error until the connection was closed. Postgres reports this error when a proxy such as RDS Proxy parses the named statement again after the schema change, before the client binds. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Meemaw --- .../codec/ExtendedQueryPgCommandMessage.java | 2 +- .../pgclient/PreparedStatementCachedTest.java | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ExtendedQueryPgCommandMessage.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ExtendedQueryPgCommandMessage.java index 5d5673d54..27d8af815 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ExtendedQueryPgCommandMessage.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ExtendedQueryPgCommandMessage.java @@ -27,7 +27,7 @@ public class ExtendedQueryPgCommandMessage> private PgEncoder encoder; - private static final String TABLE_SCHEMA_CHANGE_ERROR_MESSAGE_PATTERN = "bind message has \\d result formats but query has \\d columns"; + private static final String TABLE_SCHEMA_CHANGE_ERROR_MESSAGE_PATTERN = "bind message has \\d+ result formats but query has \\d+ columns"; private PgPreparedStatement ps; diff --git a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PreparedStatementCachedTest.java b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PreparedStatementCachedTest.java index f1c75b6c7..202d5899b 100644 --- a/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PreparedStatementCachedTest.java +++ b/vertx-pg-client/src/test/java/io/vertx/tests/pgclient/PreparedStatementCachedTest.java @@ -21,6 +21,7 @@ import io.vertx.ext.unit.TestContext; import io.vertx.pgclient.PgConnectOptions; import io.vertx.pgclient.PgConnection; +import io.vertx.pgclient.PgException; import io.vertx.sqlclient.Tuple; import org.junit.Test; @@ -75,6 +76,40 @@ public void testOneShotPreparedQueryCacheRefreshOnTableSchemaChange(TestContext })); } + @Test + public void testPreparedQueryCacheRefreshOnWideTableSchemaChangeAfterReparse(TestContext ctx) { + // A proxy such as RDS Proxy parses the cached statement again on its backend connection, so after a schema change + // the bind fails with "bind message has 10 result formats but query has 11 columns" instead of + // "cached plan must not change result type". Simulate that by re-preparing the statement under the same name. + Async async = ctx.async(); + PgConnection.connect(vertx, options()).onComplete(ctx.asyncAssertSuccess(conn -> { + String select = "SELECT * FROM unstable_wide WHERE id=$1"; + conn.query("DROP TABLE IF EXISTS unstable_wide").execute() + .compose(v -> conn.query("CREATE TABLE unstable_wide (id integer PRIMARY KEY, c1 integer, c2 integer, c3 integer, " + + "c4 integer, c5 integer, c6 integer, c7 integer, c8 integer, c9 integer)").execute()) + .compose(v -> conn.query("INSERT INTO unstable_wide (id) VALUES (1)").execute()) + .compose(v -> conn.preparedQuery(select).execute(Tuple.of(1))) + .compose(res1 -> { + ctx.assertEquals(10, res1.columnsNames().size()); + return conn.query("ALTER TABLE unstable_wide ADD COLUMN c10 integer").execute(); + }) + .compose(v -> conn.query("SELECT name FROM pg_prepared_statements WHERE statement = '" + select + "'").execute()) + .compose(names -> { + String name = names.iterator().next().getString(0); + return conn.query("DEALLOCATE \"" + name + "\"; PREPARE \"" + name + "\" (int4) AS " + select).execute(); + }) + .compose(v -> conn.preparedQuery(select).execute(Tuple.of(1))) + .onComplete(ctx.asyncAssertFailure(failure -> { + ctx.assertEquals("bind message has 10 result formats but query has 11 columns", ((PgException) failure).getErrorMessage()); + conn.preparedQuery(select).execute(Tuple.of(1)).onComplete(ctx.asyncAssertSuccess(res2 -> { + ctx.assertEquals(11, res2.columnsNames().size()); + conn.close(); + async.complete(); + })); + })); + })); + } + @Test public void testMaxPreparedStatementEviction(TestContext ctx) { testPreparedStatements(ctx, options().setCachePreparedStatements(true).setPreparedStatementCacheMaxSize(16), 128, 16);