From 53990342380b532512344eada99c15b8bc093b80 Mon Sep 17 00:00:00 2001 From: Alexey Pavlenko Date: Thu, 20 Jun 2024 14:56:05 +0300 Subject: [PATCH] [clickhouse-jdbc] Return Metadata for SELECT queries that haven't yet been executed --- CHANGELOG.md | 3 + .../jdbc/ClickHousePreparedStatement.java | 12 ++- .../internal/SqlBasedPreparedStatement.java | 40 ++++++++++ .../jdbc/ClickHousePreparedStatementTest.java | 76 +++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 564f4140c..cc889c5bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Latest +### New Features +- Describe non-executed SELECT queries in prepared statements to provide metadata (https://github.com/ClickHouse/clickhouse-java/issues/1430) + ## 0.6.1 ### New Features diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHousePreparedStatement.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHousePreparedStatement.java index 7e7788e36..1b82d5d1e 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHousePreparedStatement.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/ClickHousePreparedStatement.java @@ -109,7 +109,17 @@ default void setClob(int parameterIndex, Clob x) throws SQLException { @Override default ResultSetMetaData getMetaData() throws SQLException { ResultSet currentResult = getResultSet(); - return currentResult != null ? currentResult.getMetaData() : null; + if (currentResult != null) { + return currentResult.getMetaData(); + } else if (getLargeUpdateCount() != -1L) { + return null; // Update query + } + + return describeQueryResult(); + } + + default ResultSetMetaData describeQueryResult() throws SQLException { + return null; } @Override diff --git a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java index 0bf2a22ba..0745f1a19 100644 --- a/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java +++ b/clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/SqlBasedPreparedStatement.java @@ -5,7 +5,9 @@ import java.sql.Date; import java.sql.ParameterMetaData; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.time.LocalDate; @@ -35,7 +37,9 @@ import com.clickhouse.data.value.ClickHouseStringValue; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; +import com.clickhouse.jdbc.ClickHouseConnection; import com.clickhouse.jdbc.ClickHousePreparedStatement; +import com.clickhouse.jdbc.ClickHouseResultSetMetaData; import com.clickhouse.jdbc.JdbcParameterizedQuery; import com.clickhouse.jdbc.SqlExceptionUtils; import com.clickhouse.jdbc.parser.ClickHouseSqlStatement; @@ -239,6 +243,42 @@ protected int getMaxParameterIndex() { return templates.length; } + @Override + public ResultSetMetaData describeQueryResult() throws SQLException { + // No metadata unless query has been recognized as SELECT + if (!parsedStmt.isRecognized() || !parsedStmt.isQuery()) { + return null; + } + + final String[] vals; + if (batch.isEmpty()) { + vals = new String[values.length]; + System.arraycopy(this.values, 0, vals, 0, values.length); + } else { + vals = batch.get(0); + } + for (int i = 0; i < values.length; i++) { + if (vals[i] == null) { + vals[i] = ClickHouseValues.NULL_EXPR; + } + } + + StringBuilder sb = new StringBuilder("desc ("); + preparedQuery.apply(sb, vals); + sb.append(')'); + + List columns = new LinkedList<>(); + ClickHouseConnection conn = getConnection(); + try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sb.toString())) { + while (rs.next()) { + columns.add(ClickHouseColumn.of(rs.getString(1), rs.getString(2))); + } + } + + return ClickHouseResultSetMetaData.of(conn.getJdbcConfig(), conn.getCurrentDatabase(), "", + Collections.unmodifiableList(new ArrayList<>(columns)), mapper, conn.getTypeMap()); + } + @Override public ResultSet executeQuery() throws SQLException { ensureParams(); diff --git a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java index 677d459f7..985d35b3e 100644 --- a/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java +++ b/clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHousePreparedStatementTest.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.math.BigDecimal; +import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.MalformedURLException; @@ -16,6 +17,7 @@ import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; @@ -2006,6 +2008,80 @@ public void testInsertWithSettings() throws SQLException { } } + @Test(groups = "integration") + public void testGetMetadataTypes() throws SQLException { + try (Connection conn = newConnection(new Properties()); + PreparedStatement ps = conn.prepareStatement("select ? a, ? b")) { + ResultSetMetaData md = ps.getMetaData(); + Assert.assertEquals(md.getColumnCount(), 2); + Assert.assertEquals(md.getColumnName(1), "a"); + Assert.assertEquals(md.getColumnTypeName(1), "Nullable(Nothing)"); + Assert.assertEquals(md.getColumnName(2), "b"); + Assert.assertEquals(md.getColumnTypeName(2), "Nullable(Nothing)"); + + ps.setString(1, "x"); + md = ps.getMetaData(); + Assert.assertEquals(md.getColumnCount(), 2); + Assert.assertEquals(md.getColumnName(1), "a"); + Assert.assertEquals(md.getColumnTypeName(1), "String"); + Assert.assertEquals(md.getColumnName(2), "b"); + Assert.assertEquals(md.getColumnTypeName(2), "Nullable(Nothing)"); + + ps.setObject(2, new BigInteger("12345")); + md = ps.getMetaData(); + Assert.assertEquals(md.getColumnCount(), 2); + Assert.assertEquals(md.getColumnName(1), "a"); + Assert.assertEquals(md.getColumnTypeName(1), "String"); + Assert.assertEquals(md.getColumnName(2), "b"); + Assert.assertEquals(md.getColumnTypeName(2), "UInt16"); + + ps.addBatch(); + ps.setInt(1, 2); + md = ps.getMetaData(); + Assert.assertEquals(md.getColumnCount(), 2); + Assert.assertEquals(md.getColumnName(1), "a"); + Assert.assertEquals(md.getColumnTypeName(1), "String"); + Assert.assertEquals(md.getColumnName(2), "b"); + Assert.assertEquals(md.getColumnTypeName(2), "UInt16"); + + ps.clearBatch(); + ps.clearParameters(); + md = ps.getMetaData(); + Assert.assertEquals(md.getColumnCount(), 2); + Assert.assertEquals(md.getColumnName(1), "a"); + Assert.assertEquals(md.getColumnTypeName(1), "Nullable(Nothing)"); + Assert.assertEquals(md.getColumnName(2), "b"); + Assert.assertEquals(md.getColumnTypeName(2), "Nullable(Nothing)"); + } + } + + @Test(groups = "integration") + public void testGetMetadataStatements() throws SQLException { + try (Connection conn = newConnection(new Properties()); + PreparedStatement createPs = conn.prepareStatement("create table test_get_metadata_statements (col String) Engine=Log"); + PreparedStatement selectPs = conn.prepareStatement("select 'Hello, World!'"); + PreparedStatement insertPs = conn.prepareStatement( + "insert into test_get_metadata_statements select 'Hello, World!'"); + PreparedStatement updatePs = conn.prepareStatement( + "update test_get_metadata_statements set col = 'Bye, World!'"); + PreparedStatement grantPs = conn.prepareStatement("grant select on * to default"); + PreparedStatement commitPS = conn.prepareStatement("commit");) { + + // Only select shall have valid metadata + ResultSetMetaData selectMetaData = selectPs.getMetaData(); + Assert.assertNotNull(selectMetaData); + Assert.assertEquals(selectMetaData.getColumnCount(), 1); + Assert.assertEquals(selectMetaData.getColumnTypeName(1), "String"); + + // The rest shall return null + Assert.assertNull(createPs.getMetaData()); + Assert.assertNull(insertPs.getMetaData()); + Assert.assertNull(updatePs.getMetaData()); + Assert.assertNull(grantPs.getMetaData()); + Assert.assertNull(commitPS.getMetaData()); + } + } + @Test(groups = "integration") public void testGetParameterMetaData() throws SQLException { try (Connection conn = newConnection(new Properties());