From d97d55dcdb1730984a4e28325c0a6044d698d4ef Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:08:11 +0000 Subject: [PATCH] [jdbc-v2] Build the getMetaData DESCRIBE query from the parser's placeholder positions PreparedStatement.getMetaData() rewrote the statement's SQL with a regex that only knew quoted tokens, so a '?' inside a comment was replaced with NULL and an odd "'" inside a comment mis-paired the quote alternative, leaving a real placeholder unreplaced. The DESCRIBE then failed and the driver silently fell back to untyped metadata. The metadata query is now composed from the placeholder positions the statement parser already recorded, the same ones used to compile a parameterized execution. Fixes: https://github.com/ClickHouse/clickhouse-java/issues/3011 --- CHANGELOG.md | 7 ++++++ .../jdbc/PreparedStatementImpl.java | 14 ++++++++++- .../jdbc/PreparedStatementTest.java | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c92a57a..e6a35b5bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,13 @@ ### Bug Fixes +- **[jdbc-v2]** Fixed `PreparedStatement.getMetaData()` losing the result-set schema for a statement whose SQL + contains a comment. The `DESCRIBE` query used to resolve the metadata was built by re-scanning the SQL with a + regex that knew only quoted tokens, so a `?` inside a `--` / `#` / `/* */` comment was rewritten to `NULL` and + an odd `'` inside a comment mis-paired the quote alternative, leaving a real placeholder unreplaced — the + `DESCRIBE` then failed and the driver silently returned untyped metadata. The metadata query is now built from + the placeholder positions the statement parser already computed, so it always matches the SQL that a + parameterized execution produces. (https://github.com/ClickHouse/clickhouse-java/issues/3011) - **[client-v2]** Fixed LZ4 input streams not closing their underlying HTTP response stream. Closing an LZ4 stream returned by `QueryResponse.getInputStream()` now releases the wrapped transport stream, including after a partial read. (https://github.com/ClickHouse/clickhouse-java/issues/2985) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index ebfa46ebc..c2487d289 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -131,6 +131,18 @@ private String buildSQL() throws SQLException { return compiledSql.toString(); } + private String substituteParameters(String value) { + StringBuilder compiledSql = new StringBuilder(originalSql); + int posOffset = 0; + int[] positions = parsedPreparedStatement.getParamPositions(); + for (int i = 0; i < argCount; i++) { + int p = positions[i] + posOffset; + compiledSql.replace(p, p + 1, value); + posOffset += value.length() - 1; + } + return compiledSql.toString(); + } + @Override public ResultSet executeQuery() throws SQLException { ensureOpen(); @@ -411,7 +423,7 @@ public ResultSetMetaData getMetaData() throws SQLException { if (parsedPreparedStatement.isHasResultSet()) { try { // Replace '?' with NULL to make SQL valid for DESCRIBE - String sql = replaceQuestionMarks(originalSql, NULL_LITERAL); + String sql = substituteParameters(NULL_LITERAL); TableSchema tSchema = connection.getClient().getTableSchemaFromQuery(sql); resultSetMetaData = new ResultSetMetaDataImpl(tSchema.getColumns(), connection.getSchema(), connection.getCatalog(), diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 14c19f7a9..42499165a 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -1675,6 +1675,30 @@ public void testParameterCount() throws Exception { } } + @Test(dataProvider = "testGetMetadataIgnoresCommentsDataProvider") + public void testGetMetadataIgnoresComments(String sql) throws Exception { + try (Connection conn = getJdbcConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + ResultSetMetaData metaData = stmt.getMetaData(); + assertEquals(metaData.getColumnCount(), 3); + assertEquals(metaData.getColumnName(1), "x"); + assertEquals(metaData.getColumnName(2), "y"); + assertEquals(metaData.getColumnName(3), "z"); + assertEquals(metaData.getColumnType(1), Types.VARCHAR); + assertEquals(metaData.getColumnType(3), Types.VARCHAR); + } + } + + @DataProvider(name = "testGetMetadataIgnoresCommentsDataProvider") + static Object[][] testGetMetadataIgnoresCommentsDataProvider() { + return new Object[][] { + {"SELECT 'a' AS x -- it's ?\n, ? AS y, 'z' AS z"}, + {"SELECT 'a' AS x # it's ?\n, ? AS y, 'z' AS z"}, + {"SELECT 'a' AS x /* it's ? */, ? AS y, 'z' AS z"}, + {"SELECT 'a?b' AS x, ? AS y, 'z' AS z"} + }; + } + @Test public void testEncodingArray() throws Exception { try (Connection conn = getJdbcConnection();) {