This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 1ff771cd63487d962dcbade5d55a7ab9e18ee6e7 Author: opencode <[email protected]> AuthorDate: Fri Oct 30 10:01:59 2026 +0100 Do not report a preparation that carries no SQL in the query report interceptors createStatement guarded the prepareStatement hook against a null SQL string but handed the null straight to the prepareCall hook, so on a driver that tolerates prepareCall(null) - the pool itself never passes one; it takes a permissive driver plus an application error - the shared per-pool statistics map gained a permanently empty-named entry that consumed one of the maxQueries slots and surfaced through JMX as an empty query. The prepareCall branch now mirrors the prepareStatement guard; the proxy constructor is acquired before the hook in both branches, so skipping the report cannot disturb proxy creation, and a test pins that neither preparation hook nor the statistics map sees SQL that does not exist while the statement proxies are still created for every overload. --- .../jdbc/pool/interceptor/AbstractQueryReport.java | 4 +- .../interceptor/TestQueryReportExtraction.java | 86 +++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java index 719df84a9f..5fcfd132ec 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java @@ -200,7 +200,9 @@ public abstract class AbstractQueryReport extends AbstractCreateStatementInterce // prepareCall sql = (String) args[0]; constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class); - prepareCall(sql, time); + if (sql != null) { + prepareCall(sql, time); + } } else { // Do nothing, might be a future unsupported method // so we better bail out and let the system continue diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestQueryReportExtraction.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestQueryReportExtraction.java index 7eeaa8329a..6f2b53ecd0 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestQueryReportExtraction.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestQueryReportExtraction.java @@ -16,6 +16,15 @@ */ package org.apache.tomcat.jdbc.pool.interceptor; +import java.lang.reflect.Proxy; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -24,7 +33,9 @@ import org.junit.jupiter.api.Test; * isolation. The SQL a statement was prepared with takes precedence over * the argument of the invoked method, an argument that is not a string is * not a query - it must not abort the report - and a batch executed on a - * plain statement is reported under the name {@code batch}. + * plain statement is reported under the name {@code batch}. A preparation + * with no SQL at all is not a query either - it reaches no hook, yet the + * proxy is still created. */ class TestQueryReportExtraction { @@ -73,4 +84,77 @@ class TestQueryReportExtraction { Assertions.assertEquals("SELECT 1", REPORT.reportSlowQuery(null, new Object[] { "SELECT 1" }, "executeQuery", 0, 1)); } + + /** + * Report that records whether its preparation hooks are invoked and + * with what, and that books the statistics the real + * {@code SlowQueryReport} books. + */ + private static class RecordingReport extends SlowQueryReport { + private boolean statementHookCalled; + private boolean callHookCalled; + private String calledSQL; + + @Override + public void prepareStatement(String sql, long time) { + statementHookCalled = true; + super.prepareStatement(sql, time); + } + + @Override + public void prepareCall(String sql, long time) { + callHookCalled = true; + calledSQL = sql; + super.prepareCall(sql, time); + } + } + + private static Statement delegateStatement() { + return (Statement) Proxy.newProxyInstance(TestQueryReportExtraction.class.getClassLoader(), + new Class<?>[] { Statement.class }, (proxy, method, args) -> null); + } + + @Test + void aPreparationWithoutSQLReachesNoHookAndBooksNoStats() throws Exception { + // a driver that tolerates prepareCall(null) - the pool itself never + // passes a null SQL, only a permissive driver plus an application + // error gets this far - must report nothing for SQL that does not + // exist: neither through the hook nor through a statistics entry + // keyed by the empty query, which would occupy one of the + // maxQueries slots of the per-pool map for good + RecordingReport report = new RecordingReport(); + report.queries = new ConcurrentHashMap<>(); + + Object callable = report.createStatement(null, + Connection.class.getMethod("prepareCall", String.class), + new Object[] { null }, delegateStatement(), 0); + Assertions.assertFalse(report.callHookCalled || report.statementHookCalled, + "a null SQL must not reach the preparation hooks"); + Assertions.assertTrue(report.queries.isEmpty(), + "a null SQL must not book a statistics entry, keys were " + report.queries.keySet()); + // skipping the hook must not disturb the proxy creation either + Assertions.assertTrue(Proxy.isProxyClass(callable.getClass()) && callable instanceof CallableStatement, + "the callable statement proxy must still be created for a null SQL"); + + Object prepared = report.createStatement(null, + Connection.class.getMethod("prepareStatement", String.class), + new Object[] { null }, delegateStatement(), 0); + Assertions.assertFalse(report.statementHookCalled || report.callHookCalled, + "a null SQL must not reach the preparation hooks"); + Assertions.assertTrue(report.queries.isEmpty(), + "a null SQL must not book a statistics entry, keys were " + report.queries.keySet()); + Assertions.assertTrue(Proxy.isProxyClass(prepared.getClass()) && prepared instanceof PreparedStatement, + "the prepared statement proxy must still be created for a null SQL"); + + // a real SQL on any of the overloads is still reported as before + report.createStatement(null, + Connection.class.getMethod("prepareCall", String.class, int.class, int.class), + new Object[] { "CALL p()", Integer.valueOf(1), Integer.valueOf(2) }, delegateStatement(), 0); + Assertions.assertEquals("CALL p()", report.calledSQL, + "the SQL of the prepareCall overload must still reach the hook"); + Set<String> keys = new HashSet<>(); + keys.add("CALL p()"); + Assertions.assertEquals(keys, report.queries.keySet(), + "the SQL of the prepareCall overload must still be booked"); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
