From 68c7fb371da14ad80d1724f446b8d1108fd67e78 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 14 Aug 2025 17:21:41 -0700 Subject: [PATCH 1/6] implemented value function for columns. fixed returning solid result set for database metadata --- .../com/clickhouse/data/ClickHouseColumn.java | 19 +++++ .../ClickHouseBinaryFormatReader.java | 10 +++ .../internal/AbstractBinaryFormatReader.java | 36 +++++++++- .../com/clickhouse/jdbc/ResultSetImpl.java | 11 ++- .../jdbc/metadata/DatabaseMetaDataImpl.java | 72 ++++++++++--------- .../jdbc/metadata/ResultSetMetaDataImpl.java | 22 +++++- .../jdbc/metadata/DatabaseMetaDataTest.java | 37 +++++++++- 7 files changed, 170 insertions(+), 37 deletions(-) diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java index 2ae64bf99..344a98b83 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java @@ -38,6 +38,7 @@ import java.io.Serializable; import java.lang.reflect.Array; import java.math.BigInteger; +import java.sql.SQLException; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -90,6 +91,7 @@ public final class ClickHouseColumn implements Serializable { private List nested; private List parameters; private ClickHouseEnum enumConstants; + private ValueFunction valueFunction; private int arrayLevel; private ClickHouseColumn arrayBaseColumn; @@ -787,6 +789,18 @@ public boolean isNestedType() { return dataType.isNested(); } + public boolean hasValueFunction() { + return valueFunction != null; + } + + public void setValueFunction(ValueFunction valueFunction) { + this.valueFunction = valueFunction; + } + + public ValueFunction getValueFunction() { + return valueFunction; + } + public int getArrayNestedLevel() { return arrayLevel; } @@ -1126,4 +1140,9 @@ public String toString() { } return builder.append(' ').append(originalTypeName).toString(); } + + public interface ValueFunction { + + Object produceValue(Object[] row); + } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java index ec08e4ea3..5d3d90aa5 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java @@ -1,6 +1,7 @@ package com.clickhouse.client.api.data_formats; import com.clickhouse.client.api.metadata.TableSchema; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.value.ClickHouseBitmap; import com.clickhouse.data.value.ClickHouseGeoMultiPolygonValue; import com.clickhouse.data.value.ClickHouseGeoPointValue; @@ -545,4 +546,13 @@ public interface ClickHouseBinaryFormatReader extends AutoCloseable { TemporalAmount getTemporalAmount(int index); TemporalAmount getTemporalAmount(String colName); + + /** + * ! Experimental ! Might change in the future. + * Sets a value function of a column. If column has a value function then reader will pass current row + * as Object[] to a function. The least is responsible for returning correct value or null. + * @param index - column index starting with 1 + * @param function - function that will be used to calculate column value from current row. + */ + default void setValueFunction(int index, ClickHouseColumn.ValueFunction function) {} } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 3ac073e5f..0201363d2 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -131,6 +131,11 @@ public boolean readToPOJO(Map deserializers, Obje return true; } + @Override + public void setValueFunction(int index, ClickHouseColumn.ValueFunction function) { + columns[index - 1].setValueFunction(function); + } + /** * It is still internal method and should be used with care. * Usually this method is called to read next record into internal object and affects hasNext() method. @@ -148,7 +153,11 @@ public boolean readRecord(Map record) throws IOException { } boolean firstColumn = true; + boolean hasValueFunctionColumn = false; for (ClickHouseColumn column : columns) { + if (column.hasValueFunction()) { + hasValueFunctionColumn = true; + } try { Object val = binaryStreamReader.readValue(column); if (val != null) { @@ -165,6 +174,16 @@ public boolean readRecord(Map record) throws IOException { throw e; } } + + if (hasValueFunctionColumn) { + // This variant of readRecord is called only for POJO serialization and this logic should be avoided. + Object[] row = record.values().toArray(); + for (ClickHouseColumn column : columns) { + if (column.hasValueFunction()) { + record.put(column.getColumnName(), column.getValueFunction().produceValue(row)); + } + } + } return true; } @@ -174,9 +193,14 @@ protected boolean readRecord(Object[] record) throws IOException { } boolean firstColumn = true; + boolean hasValueFunctionColumn = false; for (int i = 0; i < columns.length; i++) { try { - Object val = binaryStreamReader.readValue(columns[i]); + ClickHouseColumn column = columns[i]; + if (column.hasValueFunction()) { + hasValueFunctionColumn = true; + } + Object val = binaryStreamReader.readValue(column); if (val != null) { record[i] = val; } else { @@ -191,9 +215,19 @@ protected boolean readRecord(Object[] record) throws IOException { throw e; } } + + if (hasValueFunctionColumn) { + for (int i = 0; i < columns.length; i++) { + ClickHouseColumn column = columns[i]; + if (column.hasValueFunction()) { + record[i] = column.getValueFunction().produceValue(record); + } + } + } return true; } + @Override public T readValue(int colIndex) { if (colIndex < 1 || colIndex > getSchema().getColumns().size()) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index fa6cd26a0..a4ca79f89 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -3,6 +3,7 @@ import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.QueryResponse; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; @@ -40,7 +41,7 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper { private static final Logger log = LoggerFactory.getLogger(ResultSetImpl.class); - private ResultSetMetaData metaData; + private ResultSetMetaDataImpl metaData; protected ClickHouseBinaryFormatReader reader; private QueryResponse response; private boolean closed; @@ -138,6 +139,14 @@ public void close() throws SQLException { } } + public void setValueFunction(int colIndex, ClickHouseColumn.ValueFunction valueFunction) { + reader.setValueFunction(colIndex, valueFunction); + } + + public void hideLastNColumns(int n) { + metaData.setColumnCount(metaData.getOriginalColumnCount() - n); + } + @Override public boolean wasNull() throws SQLException { checkClosed(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index a41046425..7b8709e09 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -11,7 +11,6 @@ import com.clickhouse.jdbc.ResultSetImpl; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; -import com.clickhouse.jdbc.internal.MetadataResultSet; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; @@ -22,6 +21,7 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLType; +import java.sql.Types; import java.util.Arrays; public class DatabaseMetaDataImpl implements java.sql.DatabaseMetaData, JdbcV2Wrapper { @@ -222,7 +222,7 @@ public String getStringFunctions() throws SQLException { public String getSystemFunctions() throws SQLException { // took from below URL(not from system.functions): // https://clickhouse.com/docs/en/sql-reference/functions/other-functions/ - return "bar,basename,blockNumber,blockSerializedSize,blockSize,buildId,byteSize,countDigits,currentDatabase,currentProfiles,currentRoles,currentUser,defaultProfiles,defaultRoles,defaultValueOfArgumentType,defaultValueOfTypeName,dumpColumnStructure,enabledProfiles,enabledRoles,errorCodeToName,filesystemAvailable,filesystemCapacity,filesystemFree,finalizeAggregation,formatReadableQuantity,formatReadableSize,formatReadableTimeDelta,FQDN,getMacro,getServerPort,getSetting,getSizeOfEnumType,greatest,hasColumnInTable,hostName,identity,ifNotFinite,ignore,indexHint,initializeAggregation,initialQueryID,isConstant,isDecimalOverflow,isFinite,isInfinite,isNaN,joinGet,least,MACNumToString,MACStringToNum,MACStringToOUI,materialize,modelEvaluate,neighbor,queryID,randomFixedString,randomPrintableASCII,randomString,randomStringUTF8,replicate,rowNumberInAllBlocks,rowNumberInBlock,runningAccumulate,runningConcurrency,runningDifference,runningDifferenceStartingWithFirstValue,shardCount ,shardNum,sleep,sleepEachRow,tcpPort,throwIf,toColumnTypeName,toTypeName,transform,uptime,version,visibleWidth"; + return "bar,basename,blockNumber,blockSerializedSize,blockSize,buildId,byteSize,countDigits,currentDatabase,currentProfiles,currentRoles,currentUser,defaultProfiles,defaultRoles,defaultValueOfArgumentType,defaultValueOfTypeName,dumpColumnStructure,enabledProfiles,enabledRoles,errorCodeToName,filesystemAvailable,filesystemCapacity,filesystemFree,finalizeAggregation,formatReadableQuantity,formatReadableSize,formatReadableTimeDelta,FQDN,getMacro,getServerPort,getSetting,getSizeOfEnumType,greatest,hasColumnInTable,hostName,identity,ifNotFinite,ignore,indexHint,initializeAggregation,initialQueryID,isConstant,isDecimalOverflow,isFinite,isInfinite,isNaN,joinGet,least,MACNumToString,MACStringToNum,MACStringToOUI,materialize,modelEvaluate,neighbor,queryID,randomFixedString,randomPrintableASCII,randomString,randomStringUTF8,replicate,rowNumberInAllBlocks,rowNumberInBlock,runningAccumulate,runningConcurrency,runningDifference,runningDifferenceStartingWithFirstValue,shardCount ,shardNum,sleep,sleepEachRow,tcpPort,throwIf,toColumnTypeName,toTypeName,e,uptime,version,visibleWidth"; } @Override @@ -830,18 +830,19 @@ public ResultSet getTableTypes() throws SQLException { } } - private static final ClickHouseColumn DATA_TYPE_COL = ClickHouseColumn.of("DATA_TYPE", ClickHouseDataType.Int32.name()) ; + private static final int GET_COLUMNS_TYPE_NAME_COL = 6; + + private static final int GET_COLUMNS_DATA_TYPE_COL = 5; @Override @SuppressWarnings({"squid:S2095", "squid:S2077"}) public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - //TODO: Best way to convert type to JDBC data type // TODO: handle useCatalogs == true and return schema catalog name - String sql = "SELECT " + + final String sql = "SELECT " + catalogPlaceholder + " AS TABLE_CAT, " + "database AS TABLE_SCHEM, " + "table AS TABLE_NAME, " + "name AS COLUMN_NAME, " + - "system.columns.type AS DATA_TYPE, " + + "toInt32(" + Types.OTHER + ") AS DATA_TYPE, " + "type AS TYPE_NAME, " + "toInt32(" + generateSqlTypeSizes("system.columns.type") + ") AS COLUMN_SIZE, " + "toInt32(0) AS BUFFER_LENGTH, " + @@ -867,8 +868,9 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa " AND name LIKE " + SQLUtils.enquoteLiteral(columnNamePattern == null ? "%" : columnNamePattern) + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; try { - return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(sql)) - .transform(DATA_TYPE_COL.getColumnName(), DATA_TYPE_COL, DatabaseMetaDataImpl::columnDataTypeToSqlType); + ResultSetImpl rs = (ResultSetImpl) connection.createStatement().executeQuery(sql); + rs.setValueFunction(GET_COLUMNS_DATA_TYPE_COL, GET_COLUMNS_DATA_TYPE_FUNC); + return rs; } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } @@ -887,17 +889,22 @@ private static String generateSqlTypeSizes(String columnName) { return sql.toString(); } - private static String columnDataTypeToSqlType(String value) { - SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(value); - if (type == null) { - try { - type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", value).getDataType()); - } catch (Exception e) { - log.error("Failed to convert column data type to SQL type: {}", value, e); - type = JDBCType.OTHER; // In case of error, return SQL type 0 + private static final ClickHouseColumn.ValueFunction GET_COLUMNS_DATA_TYPE_FUNC = dataTypeValueFunction(GET_COLUMNS_TYPE_NAME_COL); + + private static ClickHouseColumn.ValueFunction dataTypeValueFunction(int srcColIndex) { + return row -> { + String typeName = (String) row[srcColIndex - 1]; + SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(typeName); + if (type == null) { + try { + type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", typeName).getDataType()); + } catch (Exception e) { + log.error("Failed to convert column data type to SQL type: {}", typeName, e); + type = JDBCType.OTHER; // In case of error, return SQL type 0 + } } - } - return String.valueOf(type.getVendorTypeNumber()); + return type.getVendorTypeNumber(); + }; } @Override @@ -1067,26 +1074,23 @@ public ResultSet getCrossReference(String parentCatalog, String parentSchema, St } } - private static final ClickHouseColumn NULLABLE_COL = ClickHouseColumn.of("NULLABLE", ClickHouseDataType.Int16.name()); + private static final int TYPE_INFO_DATA_TYPE_COL = 2; + private static final int TYPE_INFO_NULLABILITY_COL = 7; @Override @SuppressWarnings({"squid:S2095"}) public ResultSet getTypeInfo() throws SQLException { try { - return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(DATA_TYPE_INFO_SQL)) - .transform(DATA_TYPE_COL.getColumnName(), DATA_TYPE_COL, DatabaseMetaDataImpl::dataTypeToSqlTypeInt) - .transform(NULLABLE_COL.getColumnName(), NULLABLE_COL, DatabaseMetaDataImpl::dataTypeNullability); + ResultSetImpl rs = (ResultSetImpl) connection.createStatement().executeQuery(DATA_TYPE_INFO_SQL); + rs.setValueFunction(TYPE_INFO_DATA_TYPE_COL, TYPE_INFO_DATA_TYPE_VALUE_FUNC); + rs.setValueFunction(TYPE_INFO_NULLABILITY_COL, DatabaseMetaDataImpl::dataTypeNullability); + return rs; } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } } - private static String dataTypeToSqlTypeInt(String type) { - SQLType sqlType = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(type); - return sqlType == null ? String.valueOf(JDBCType.OTHER.getVendorTypeNumber()) : - String.valueOf(sqlType.getVendorTypeNumber()); - } - - private static String dataTypeNullability(String type) { + private static String dataTypeNullability(Object[] row) { + String type = (String) row[DATA_TYPE_INFO_SQL_TYPE_NAME_COL - 1]; if (type.equals(ClickHouseDataType.Nullable.name()) || type.equals(ClickHouseDataType.Dynamic.name())) { return String.valueOf(java.sql.DatabaseMetaData.typeNullable); } @@ -1095,21 +1099,23 @@ private static String dataTypeNullability(String type) { private static final String DATA_TYPE_INFO_SQL = getDataTypeInfoSql(); + private static final int DATA_TYPE_INFO_SQL_TYPE_NAME_COL = 13; + private static String getDataTypeInfoSql() { StringBuilder sql = new StringBuilder("SELECT " + "name AS TYPE_NAME, " + - "if(empty(alias_to), name, alias_to) AS DATA_TYPE, " + // passing type name or alias if exists to map then + "0::Int32 AS DATA_TYPE, " + // passing type name or alias if exists to map then "attrs.c2::Nullable(Int32) AS PRECISION, " + "NULL::Nullable(String) AS LITERAL_PREFIX, " + "NULL::Nullable(String) AS LITERAL_SUFFIX, " + "NULL::Nullable(String) AS CREATE_PARAMS, " + - "name AS NULLABLE, " + // passing type name to map for nullable + "0::Int16 AS NULLABLE, " + // passing type name to map for nullable "not(dt.case_insensitive)::Boolean AS CASE_SENSITIVE, " + java.sql.DatabaseMetaData.typeSearchable + "::Int16 AS SEARCHABLE, " + "not(attrs.c3)::Boolean AS UNSIGNED_ATTRIBUTE, " + "false AS FIXED_PREC_SCALE, " + "false AS AUTO_INCREMENT, " + - "name AS LOCAL_TYPE_NAME, " + + "if(empty(alias_to), name, alias_to) AS LOCAL_TYPE_NAME, " + "attrs.c4::Nullable(Int16) AS MINIMUM_SCALE, " + "attrs.c5::Nullable(Int16) AS MAXIMUM_SCALE, " + "0::Nullable(Int32) AS SQL_DATA_TYPE, " + @@ -1134,6 +1140,8 @@ private static String getDataTypeInfoSql() { return sql.toString(); } + private static final ClickHouseColumn.ValueFunction TYPE_INFO_DATA_TYPE_VALUE_FUNC = dataTypeValueFunction(DATA_TYPE_INFO_SQL_TYPE_NAME_COL); + @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { try { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java index 49b968b50..ba31872ca 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java @@ -5,6 +5,8 @@ import com.clickhouse.jdbc.JdbcV2Wrapper; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.UnmodifiableListIterator; import java.sql.SQLException; import java.util.List; @@ -22,13 +24,16 @@ public class ResultSetMetaDataImpl implements java.sql.ResultSetMetaData, JdbcV2 private final Map> typeClassMap; + private int columnCount; + public ResultSetMetaDataImpl(List columns, String schema, String catalog, String tableName, Map> typeClassMap) { - this.columns = columns; + this.columns = ImmutableList.copyOf(columns); this.schema = schema; this.catalog = catalog; this.tableName = tableName; this.typeClassMap = typeClassMap; + this.columnCount = columns.size(); } private ClickHouseColumn getColumn(int column) throws SQLException { @@ -41,9 +46,24 @@ private ClickHouseColumn getColumn(int column) throws SQLException { @Override public int getColumnCount() throws SQLException { + return columnCount; + } + + public int getOriginalColumnCount() { return columns.size(); } + /** + * This method used to truncate list of column so it is possible to + * "hide" columns from the end of the list. + * Note: we use this to implement column replacement. it is needed when DB calculation is too hard compare to a + * programmatic approach. + * @param columnCount + */ + public void setColumnCount(int columnCount) { + this.columnCount = columnCount; + } + @Override public boolean isAutoIncrement(int column) throws SQLException { return false; // no auto-incremental types diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java index 142c9596f..c49e29cbb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java @@ -15,6 +15,7 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; import java.util.Arrays; import java.util.Collections; @@ -33,11 +34,17 @@ public class DatabaseMetaDataTest extends JdbcIntegrationTest { @Test(groups = { "integration" }) public void testGetColumns() throws Exception { try (Connection conn = getJdbcConnection()) { + final String tableName = "get_columns_metadata_test"; + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("" + + "CREATE TABLE " + tableName + " (id Int32, name String, v1 Nullable(Int8)) " + + "ENGINE MergeTree ORDER BY ()"); + } + DatabaseMetaData dbmd = conn.getMetaData(); - try (ResultSet rs = dbmd.getColumns(null, ClickHouseServerForTest.getDatabase(), "system.numbers" - , null)) { + try (ResultSet rs = dbmd.getColumns(null, getDatabase(), tableName.substring(0, tableName.length() - 3) + "%", null)) { List expectedColumnNames = Arrays.asList( "TABLE_CAT", @@ -94,6 +101,30 @@ public void testGetColumns() throws Exception { ); assertProcedureColumns(rs.getMetaData(), expectedColumnNames, expectedColumnTypes); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "id"); + assertEquals(rs.getInt("DATA_TYPE"), Types.INTEGER); + assertEquals(rs.getObject("DATA_TYPE"), Types.INTEGER); + assertEquals(rs.getString("TYPE_NAME"), "Int32"); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "name"); + assertEquals(rs.getInt("DATA_TYPE"), Types.VARCHAR); + assertEquals(rs.getObject("DATA_TYPE"), Types.VARCHAR); + assertEquals(rs.getString("TYPE_NAME"), "String"); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "v1"); + assertEquals(rs.getInt("DATA_TYPE"), Types.TINYINT); + assertEquals(rs.getObject("DATA_TYPE"), Types.TINYINT); + assertEquals(rs.getString("TYPE_NAME"), "Nullable(Int8)"); } } } @@ -385,6 +416,8 @@ public void testGetTypeInfo() throws Exception { JdbcUtils.convertToSqlType(dataType).getVendorTypeNumber() + " but was " + rs.getInt("DATA_TYPE") + " for TYPE_NAME: " + rs.getString("TYPE_NAME")); + assertEquals(rs.getInt("DATA_TYPE"), rs.getObject("DATA_TYPE")); + assertEquals(rs.getInt("PRECISION"), dataType.getMaxPrecision()); assertNull(rs.getString("LITERAL_PREFIX")); assertNull(rs.getString("LITERAL_SUFFIX")); From b66105e279b574ae1f7ace6251dd4b3e2c96b969 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 19 Aug 2025 22:31:19 -0700 Subject: [PATCH 2/6] re-implemented with detached result set --- .../com/clickhouse/data/ClickHouseColumn.java | 19 - .../ClickHouseBinaryFormatReader.java | 17 +- .../internal/AbstractBinaryFormatReader.java | 33 +- .../clickhouse/client/query/QueryTests.java | 1 + .../com/clickhouse/jdbc/ResultSetImpl.java | 15 +- .../jdbc/internal/DetachedResultSet.java | 1073 +++++++++++++++++ .../jdbc/internal/MetadataResultSet.java | 204 ---- .../jdbc/metadata/DatabaseMetaDataImpl.java | 80 +- .../jdbc/metadata/ResultSetMetaDataImpl.java | 11 - .../jdbc/metadata/DatabaseMetaDataTest.java | 2 +- 10 files changed, 1124 insertions(+), 331 deletions(-) create mode 100644 jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java delete mode 100644 jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java index 232c73f8d..734e3fe56 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java @@ -37,8 +37,6 @@ import java.io.Serializable; import java.lang.reflect.Array; -import java.math.BigInteger; -import java.sql.SQLException; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -93,7 +91,6 @@ public final class ClickHouseColumn implements Serializable { private List parameters; private ClickHouseEnum enumConstants; private Map jsonPredefinedPaths; - private ValueFunction valueFunction; private int arrayLevel; private ClickHouseColumn arrayBaseColumn; @@ -856,18 +853,6 @@ public boolean isNestedType() { return dataType.isNested(); } - public boolean hasValueFunction() { - return valueFunction != null; - } - - public void setValueFunction(ValueFunction valueFunction) { - this.valueFunction = valueFunction; - } - - public ValueFunction getValueFunction() { - return valueFunction; - } - public int getArrayNestedLevel() { return arrayLevel; } @@ -1212,8 +1197,4 @@ public String toString() { return builder.append(' ').append(originalTypeName).toString(); } - public interface ValueFunction { - - Object produceValue(Object[] row); - } } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java index 5d3d90aa5..e6a9c2557 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java @@ -1,7 +1,6 @@ package com.clickhouse.client.api.data_formats; import com.clickhouse.client.api.metadata.TableSchema; -import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.value.ClickHouseBitmap; import com.clickhouse.data.value.ClickHouseGeoMultiPolygonValue; import com.clickhouse.data.value.ClickHouseGeoPointValue; @@ -12,7 +11,12 @@ import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; -import java.time.*; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; import java.util.List; import java.util.Map; @@ -546,13 +550,4 @@ public interface ClickHouseBinaryFormatReader extends AutoCloseable { TemporalAmount getTemporalAmount(int index); TemporalAmount getTemporalAmount(String colName); - - /** - * ! Experimental ! Might change in the future. - * Sets a value function of a column. If column has a value function then reader will pass current row - * as Object[] to a function. The least is responsible for returning correct value or null. - * @param index - column index starting with 1 - * @param function - function that will be used to calculate column value from current row. - */ - default void setValueFunction(int index, ClickHouseColumn.ValueFunction function) {} } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 0201363d2..3b148e0ee 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -131,16 +131,11 @@ public boolean readToPOJO(Map deserializers, Obje return true; } - @Override - public void setValueFunction(int index, ClickHouseColumn.ValueFunction function) { - columns[index - 1].setValueFunction(function); - } - /** * It is still internal method and should be used with care. * Usually this method is called to read next record into internal object and affects hasNext() method. * So after calling this one: - * - hasNext(), next() should not be called + * - hasNext(), next() and get methods cannot be called * - stream should be read with readRecord() method fully * * @param record @@ -153,11 +148,7 @@ public boolean readRecord(Map record) throws IOException { } boolean firstColumn = true; - boolean hasValueFunctionColumn = false; for (ClickHouseColumn column : columns) { - if (column.hasValueFunction()) { - hasValueFunctionColumn = true; - } try { Object val = binaryStreamReader.readValue(column); if (val != null) { @@ -174,16 +165,6 @@ public boolean readRecord(Map record) throws IOException { throw e; } } - - if (hasValueFunctionColumn) { - // This variant of readRecord is called only for POJO serialization and this logic should be avoided. - Object[] row = record.values().toArray(); - for (ClickHouseColumn column : columns) { - if (column.hasValueFunction()) { - record.put(column.getColumnName(), column.getValueFunction().produceValue(row)); - } - } - } return true; } @@ -193,13 +174,9 @@ protected boolean readRecord(Object[] record) throws IOException { } boolean firstColumn = true; - boolean hasValueFunctionColumn = false; for (int i = 0; i < columns.length; i++) { try { ClickHouseColumn column = columns[i]; - if (column.hasValueFunction()) { - hasValueFunctionColumn = true; - } Object val = binaryStreamReader.readValue(column); if (val != null) { record[i] = val; @@ -216,14 +193,6 @@ protected boolean readRecord(Object[] record) throws IOException { } } - if (hasValueFunctionColumn) { - for (int i = 0; i < columns.length; i++) { - ClickHouseColumn column = columns[i]; - if (column.hasValueFunction()) { - record[i] = column.getValueFunction().produceValue(record); - } - } - } return true; } diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index 5b08248b7..cf5d2c86f 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -13,6 +13,7 @@ import com.clickhouse.client.api.command.CommandResponse; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; +import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertResponse; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index 1832a76f0..4bcc48d4c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -3,7 +3,6 @@ import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.query.QueryResponse; -import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.FeatureManager; @@ -50,9 +49,9 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper { private final FeatureManager featureManager; - private static final int AFTER_LAST = -1; - private static final int BEFORE_FIRST = 0; - private static final int FIRST_ROW = 1; + public static final int AFTER_LAST = -1; + public static final int BEFORE_FIRST = 0; + public static final int FIRST_ROW = 1; private int rowPos; private int fetchSize; @@ -157,14 +156,6 @@ public void close() throws SQLException { } } - public void setValueFunction(int colIndex, ClickHouseColumn.ValueFunction valueFunction) { - reader.setValueFunction(colIndex, valueFunction); - } - - public void hideLastNColumns(int n) { - metaData.setColumnCount(metaData.getOriginalColumnCount() - n); - } - @Override public boolean wasNull() throws SQLException { checkClosed(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java new file mode 100644 index 000000000..e0df8a208 --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java @@ -0,0 +1,1073 @@ +package com.clickhouse.jdbc.internal; + +import com.clickhouse.jdbc.JdbcV2Wrapper; +import com.clickhouse.jdbc.ResultSetImpl; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.function.Consumer; + +/** + * This class intended only for internal use because is based on internal API. Do not try using it in your application. + * We may change it at any time because it is internal class. + * This class should not be used in any other places than metadata resultsets. + * This class will close parent resultset when close on this is called. + */ +public class DetachedResultSet implements ResultSet, JdbcV2Wrapper { + + private List> records; + + private ListIterator> iterator; + + private ResultSetMetaData metaData; + + private Map record; + + private boolean wasNull; + + private int row; + + private final int lastRow; + + private boolean closed; + + private Map columnMap; + + private DetachedResultSet(List> records, ResultSetMetaData metaData) throws SQLException { + this.records = records; + this.iterator = records.listIterator(); + this.metaData = metaData; + this.wasNull = false; + this.row = ResultSetImpl.BEFORE_FIRST; + this.lastRow = records.size(); + this.closed = false; + + this.columnMap = new HashMap<>(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + this.columnMap.put(metaData.getColumnName(i), i); + } + } + + public static DetachedResultSet createFromResultSet(ResultSet resultSet, Collection>> mutators) throws SQLException { + ResultSetMetaData metaData = resultSet.getMetaData(); + List> records = new ArrayList<>(); + while (resultSet.next()) { + Map record = new HashMap<>(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + record.put(metaData.getColumnLabel(i), resultSet.getObject(i)); + } + for (Consumer> mutator : mutators) { + mutator.accept(record); + } + records.add(record); + } + return new DetachedResultSet(records, metaData); + } + + @Override + public boolean next() throws SQLException { + + if (iterator.hasNext()) { + row++; + record = iterator.next(); + return true; + } + row = ResultSetImpl.AFTER_LAST; + return false; + } + + @Override + public void close() throws SQLException { + closed = true; + metaData = null; + record = null; + iterator = null; + } + + @Override + public boolean wasNull() throws SQLException { + return wasNull; + } + + @Override + public String getString(int columnIndex) throws SQLException { + return getString(metaData.getColumnLabel(columnIndex)); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return getBoolean(metaData.getColumnLabel(columnIndex)); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return getByte(metaData.getColumnLabel(columnIndex)); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return getShort(metaData.getColumnLabel(columnIndex)); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return getInt(metaData.getColumnLabel(columnIndex)); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return getLong(metaData.getColumnLabel(columnIndex)); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return getFloat(metaData.getColumnLabel(columnIndex)); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return getDouble(metaData.getColumnLabel(columnIndex)); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + return getBigDecimal(metaData.getColumnLabel(columnIndex), scale); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return getBytes(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return getDate(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return getTime(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return getTimestamp(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return getAsciiStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return getUnicodeStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return getBinaryStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return getObject(columnLabel, String.class); + } + + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getObject(columnLabel, Boolean.class); + } + + private Number getNumber(String columnLabel) throws SQLException { + return (Number) getObjectImpl(columnLabel, Number.class, BigInteger.ZERO); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getNumber(columnLabel).byteValue(); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getNumber(columnLabel).shortValue(); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getNumber(columnLabel).intValue(); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getNumber(columnLabel).longValue(); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getNumber(columnLabel).floatValue(); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getNumber(columnLabel).doubleValue(); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getObject(columnLabel, BigDecimal.class); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return getObject(columnLabel, byte[].class); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return getObject(columnLabel, Date.class); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getObject(columnLabel, Time.class); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getObject(columnLabel, Timestamp.class); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return getObject(columnLabel, InputStream.class); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return getObject(columnLabel, InputStream.class); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return getObject(columnLabel, InputStream.class); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public String getCursorName() throws SQLException { + return ""; + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return metaData; + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(columnLabel, Object.class); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + Integer index = columnMap.get(columnLabel); + if (index == null) { + throw new SQLException("Column not found: " + columnLabel, ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + return index; + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return getObject(columnIndex, Reader.class); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return getObject(columnLabel, Reader.class); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return getBigDecimal(metaData.getColumnLabel(columnIndex)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getObject(columnLabel, BigDecimal.class); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return row == ResultSetImpl.BEFORE_FIRST; + } + + @Override + public boolean isAfterLast() throws SQLException { + return row == ResultSetImpl.AFTER_LAST; + } + + @Override + public boolean isFirst() throws SQLException { + return row == ResultSetImpl.FIRST_ROW; + } + + @Override + public boolean isLast() throws SQLException { + return row == lastRow; + } + + @Override + public void beforeFirst() throws SQLException { + } + + @Override + public void afterLast() throws SQLException { + } + + @Override + public boolean first() throws SQLException { + return false; + } + + @Override + public boolean last() throws SQLException { + return false; + } + + @Override + public int getRow() throws SQLException { + return row; + } + + @Override + public boolean absolute(int row) throws SQLException { + return false; + } + + @Override + public boolean relative(int rows) throws SQLException { + return false; + } + + @Override + public boolean previous() throws SQLException { + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLException("This result set object is of FORWARD ONLY type. Only ResultSet.FETCH_FORWARD is allowed as fetchDirection."); + } + } + + @Override + public int getFetchDirection() throws SQLException { + return FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + + } + + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + @Override + public int getType() throws SQLException { + return TYPE_FORWARD_ONLY; + } + + @Override + public int getConcurrency() throws SQLException { + return CONCUR_READ_ONLY; + } + + @Override + public boolean rowUpdated() throws SQLException { + return false; + } + + @Override + public boolean rowInserted() throws SQLException { + return false; + } + + @Override + public boolean rowDeleted() throws SQLException { + return false; + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + + } + + @Override + public void insertRow() throws SQLException { + + } + + @Override + public void updateRow() throws SQLException { + + } + + @Override + public void deleteRow() throws SQLException { + + } + + @Override + public void refreshRow() throws SQLException { + + } + + @Override + public void cancelRowUpdates() throws SQLException { + + } + + @Override + public void moveToInsertRow() throws SQLException { + + } + + @Override + public void moveToCurrentRow() throws SQLException { + + } + + @Override + public Statement getStatement() throws SQLException { + return null; // should return null as it is a detached result set + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex), map); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + return getRef(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return getBlob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + return getClob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + return getArray(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + if (map == null) { + throw new SQLException("map must be not null", ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + int columnIndex = columnMap.getOrDefault(columnLabel, -1); + if (columnIndex == -1) { + throw new SQLException("column " + columnLabel + " doesn't exist", ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + + String typeName = metaData.getColumnTypeName(columnIndex); + return getObject(columnLabel, map.get(typeName)); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + return getObject(columnLabel, Ref.class); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return getObject(columnLabel, Blob.class); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + return getObject(columnLabel, Clob.class); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + return getObject(columnLabel, Array.class); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return getDate(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + return getTime(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return getTimestamp(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return null; + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + return getURL(metaData.getColumnLabel(columnIndex)); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + return getObject(columnLabel, URL.class); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + return getRowId(metaData.getColumnLabel(columnIndex)); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + return getObject(columnLabel, RowId.class); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + + } + + @Override + public int getHoldability() throws SQLException { + return HOLD_CURSORS_OVER_COMMIT; // this result set remains open + } + + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return getNClob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return getObject(columnLabel, NClob.class); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + return getSQLXML(metaData.getColumnLabel(columnIndex)); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + return getObject(columnLabel, SQLXML.class); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + + } + + @Override + public String getNString(int columnIndex) throws SQLException { + return getNString(metaData.getColumnLabel(columnIndex)); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + return getObject(columnLabel, String.class); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return getNCharacterStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return getObject(columnLabel, Reader.class); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex), type); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return (T) getObjectImpl(columnLabel, type, null); + } + + private Object getObjectImpl(String columnLabel, Class type, Object nullValue) throws SQLException { + Object value = record.get(columnLabel); + wasNull = value == null; + if (wasNull) { + return nullValue; + } + + return JdbcUtils.convert(value, type); + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java deleted file mode 100644 index 77ee23a4e..000000000 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.clickhouse.jdbc.internal; - -import com.clickhouse.client.api.metadata.TableSchema; -import com.clickhouse.data.ClickHouseColumn; -import com.clickhouse.jdbc.ResultSetImpl; -import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl; - -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.UnaryOperator; - -public class MetadataResultSet extends ResultSetImpl { - private final Map> columnTransformers = new HashMap<>(); - private final String[] cachedColumnLabels; - - private final OverridingSchemaAdaptor overridingSchemaAdaptor; - - public MetadataResultSet(ResultSetImpl resultSet) throws SQLException { - super(resultSet); - this.overridingSchemaAdaptor = new OverridingSchemaAdaptor(resultSet.getSchema()); - this.setMetaData(new ResultSetMetaDataImpl(overridingSchemaAdaptor.getColumns(), - overridingSchemaAdaptor.getDatabaseName(), "", - overridingSchemaAdaptor.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP)); - ResultSetMetaData metaData = getMetaData(); - int count = metaData.getColumnCount(); - cachedColumnLabels = new String[count]; - for (int i = 1; i <= count; i++) { - cachedColumnLabels[i - 1] = metaData.getColumnLabel(i).toUpperCase(); - } - } - - /** - * Registers a transformer function for a given column. - * The transformer takes the original String value and returns a new String. - * - * @param columnLabel The name of the column (case-insensitive). - * @param transformer The function that transforms the value. - */ - public MetadataResultSet transform(String columnLabel, ClickHouseColumn column, UnaryOperator transformer) { - if (columnLabel != null && transformer != null) { - columnTransformers.put(columnLabel.toUpperCase(), transformer); - } - overridingSchemaAdaptor.setOverriddenColumn(overridingSchemaAdaptor.nameToIndex(columnLabel), column); - return this; - } - - @Override - public String getString(String columnLabel) throws SQLException { - String value = super.getString(columnLabel); - UnaryOperator transformer = columnTransformers.get(columnLabel.toUpperCase()); - if (transformer != null && value != null) { - return transformer.apply(value); - } - return value; - } - - @Override - public int getInt(String columnLabel) throws SQLException { - String stringValue = getString(columnLabel); - if (stringValue == null || stringValue.trim().isEmpty()) { - return 0; - } - try { - return Integer.parseInt(stringValue); - } catch (NumberFormatException e) { - throw new SQLException("Value for column '" + columnLabel + "' is not a valid integer: " + stringValue, e); - } - } - - @Override - public String getString(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > cachedColumnLabels.length) { - throw new SQLException("Invalid column index: " + columnIndex); - } - return getString(cachedColumnLabels[columnIndex - 1]); - } - - @Override - public int getInt(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > cachedColumnLabels.length) { - throw new SQLException("Invalid column index: " + columnIndex); - } - return getInt(cachedColumnLabels[columnIndex - 1]); - } - - @Override - public byte getByte(int columnIndex) throws SQLException { - return (byte) getInt(columnIndex); - } - - @Override - public short getShort(int columnIndex) throws SQLException { - return (short) getInt(columnIndex); - } - - @Override - public long getLong(int columnIndex) throws SQLException { - return getInt(columnIndex); - } - - @Override - public byte getByte(String columnLabel) throws SQLException { - return (byte) getInt(columnLabel); - } - - @Override - public short getShort(String columnLabel) throws SQLException { - return (short) getInt(columnLabel); - } - - @Override - public long getLong(String columnLabel) throws SQLException { - return getInt(columnLabel); - } - - @Override - public TableSchema getSchema() { - return overridingSchemaAdaptor; - } - - private static class OverridingSchemaAdaptor extends TableSchema { - private final TableSchema originalSchema; - - private final List overriddenColumns; - - public OverridingSchemaAdaptor(TableSchema originalSchema) { - super(Collections.emptyList()); - this.originalSchema = originalSchema; - this.overriddenColumns = new ArrayList<>(originalSchema.getColumns()); - } - - public void setOverriddenColumn(int index, ClickHouseColumn column) { - if (index < 0 || index >= overriddenColumns.size()) { - throw new IndexOutOfBoundsException("Index " + index + " is out of bounds for overridden columns."); - } - overriddenColumns.set(index, column); - } - - @Override - public List getColumns() { - return overriddenColumns; - } - - @Override - public String getDatabaseName() { - return originalSchema.getDatabaseName(); - } - - @Override - public String getTableName() { - return originalSchema.getTableName(); - } - - @Override - public boolean hasDefaults() { - return originalSchema.hasDefaults(); - } - - @Override - public String getQuery() { - return originalSchema.getQuery(); - } - - @Override - public ClickHouseColumn getColumnByName(String name) { - return overriddenColumns.get(originalSchema.nameToIndex(name)); - } - - @Override - public ClickHouseColumn getColumnByIndex(int colIndex) { - return overriddenColumns.get(colIndex - 1); - } - - @Override - public String indexToName(int index) { - return originalSchema.indexToName(index); - } - - @Override - public String columnIndexToName(int index) { - return originalSchema.columnIndexToName(index); - } - - @Override - public int nameToColumnIndex(String name) { - return originalSchema.nameToColumnIndex(name); - } - - @Override - public int nameToIndex(String name) { - return originalSchema.nameToIndex(name); - } - - @Override - public String toString() { - return originalSchema.toString(); - } - } -} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index c586743e5..97ee11519 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -11,6 +11,7 @@ import com.clickhouse.jdbc.ResultSetImpl; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; +import com.clickhouse.jdbc.internal.DetachedResultSet; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; @@ -21,8 +22,14 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLType; +import java.sql.Statement; import java.sql.Types; import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; public class DatabaseMetaDataImpl implements java.sql.DatabaseMetaData, JdbcV2Wrapper { private static final Logger log = LoggerFactory.getLogger(DatabaseMetaDataImpl.class); @@ -830,9 +837,6 @@ public ResultSet getTableTypes() throws SQLException { } } - private static final int GET_COLUMNS_TYPE_NAME_COL = 6; - - private static final int GET_COLUMNS_DATA_TYPE_COL = 5; @Override @SuppressWarnings({"squid:S2095", "squid:S2077"}) public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { @@ -867,10 +871,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa " AND table LIKE " + SQLUtils.enquoteLiteral(tableNamePattern == null ? "%" : tableNamePattern) + " AND name LIKE " + SQLUtils.enquoteLiteral(columnNamePattern == null ? "%" : columnNamePattern) + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; - try { - ResultSetImpl rs = (ResultSetImpl) connection.createStatement().executeQuery(sql); - rs.setValueFunction(GET_COLUMNS_DATA_TYPE_COL, GET_COLUMNS_DATA_TYPE_FUNC); - return rs; + try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery(sql)) { + return DetachedResultSet.createFromResultSet(rs, GET_COLUMNS_RS_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } @@ -889,23 +891,23 @@ private static String generateSqlTypeSizes(String columnName) { return sql.toString(); } - private static final ClickHouseColumn.ValueFunction GET_COLUMNS_DATA_TYPE_FUNC = dataTypeValueFunction(GET_COLUMNS_TYPE_NAME_COL); - private static ClickHouseColumn.ValueFunction dataTypeValueFunction(int srcColIndex) { - return row -> { - String typeName = (String) row[srcColIndex - 1]; - SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(typeName); - if (type == null) { - try { - type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", typeName).getDataType()); - } catch (Exception e) { - log.error("Failed to convert column data type to SQL type: {}", typeName, e); - type = JDBCType.OTHER; // In case of error, return SQL type 0 - } + private static final Consumer> DATA_TYPE_VALUE_FUNCTION = row -> { + String typeName = (String) row.get("TYPE_NAME"); + SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(typeName); + if (type == null) { + try { + type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", typeName).getDataType()); + } catch (Exception e) { + log.error("Failed to convert column data type to SQL type: {}", typeName, e); + type = JDBCType.OTHER; // In case of error, return SQL type 0 } - return type.getVendorTypeNumber(); - }; - } + } + + row.put("DATA_TYPE", type.getVendorTypeNumber()); + }; + + private static final List>> GET_COLUMNS_RS_MUTATORS = Collections.singletonList(DATA_TYPE_VALUE_FUNCTION); @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { @@ -993,7 +995,6 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr "AND system.tables.database ILIKE '" + (schema == null ? "%" : schema) + "' " + "AND system.tables.name ILIKE '" + (table == null ? "%" : table) + "' " + "ORDER BY COLUMN_NAME"; - log.debug("getPrimaryKeys: %s", sql); return connection.createStatement().executeQuery(sql); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); @@ -1074,42 +1075,41 @@ public ResultSet getCrossReference(String parentCatalog, String parentSchema, St } } - private static final int TYPE_INFO_DATA_TYPE_COL = 2; - private static final int TYPE_INFO_NULLABILITY_COL = 7; @Override @SuppressWarnings({"squid:S2095"}) public ResultSet getTypeInfo() throws SQLException { - try { - ResultSetImpl rs = (ResultSetImpl) connection.createStatement().executeQuery(DATA_TYPE_INFO_SQL); - rs.setValueFunction(TYPE_INFO_DATA_TYPE_COL, TYPE_INFO_DATA_TYPE_VALUE_FUNC); - rs.setValueFunction(TYPE_INFO_NULLABILITY_COL, DatabaseMetaDataImpl::dataTypeNullability); - return rs; + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(DATA_TYPE_INFO_SQL)) { + return DetachedResultSet.createFromResultSet(rs, GET_TYPE_INFO_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } } - private static String dataTypeNullability(Object[] row) { - String type = (String) row[DATA_TYPE_INFO_SQL_TYPE_NAME_COL - 1]; + private static final Consumer> NULLABILITY_VALUE_FUNCTION = (row) -> { + String type = (String) row.get("TYPE_NAME"); + int nullability= java.sql.DatabaseMetaData.typeNoNulls; if (type.equals(ClickHouseDataType.Nullable.name()) || type.equals(ClickHouseDataType.Dynamic.name())) { - return String.valueOf(java.sql.DatabaseMetaData.typeNullable); + nullability = java.sql.DatabaseMetaData.typeNullable; } - return String.valueOf(java.sql.DatabaseMetaData.typeNoNulls); - } + row.put("NULLABLE", nullability); + }; - private static final String DATA_TYPE_INFO_SQL = getDataTypeInfoSql(); + private static final List>> GET_TYPE_INFO_MUTATORS = Arrays.asList( + DATA_TYPE_VALUE_FUNCTION, + NULLABILITY_VALUE_FUNCTION + ); - private static final int DATA_TYPE_INFO_SQL_TYPE_NAME_COL = 13; + private static final String DATA_TYPE_INFO_SQL = getDataTypeInfoSql(); private static String getDataTypeInfoSql() { StringBuilder sql = new StringBuilder("SELECT " + "name AS TYPE_NAME, " + - "0::Int32 AS DATA_TYPE, " + // passing type name or alias if exists to map then + "0::Int32 AS DATA_TYPE, " + // placeholder for data type int value "attrs.c2::Nullable(Int32) AS PRECISION, " + "NULL::Nullable(String) AS LITERAL_PREFIX, " + "NULL::Nullable(String) AS LITERAL_SUFFIX, " + "NULL::Nullable(String) AS CREATE_PARAMS, " + - "0::Int16 AS NULLABLE, " + // passing type name to map for nullable + "0::Int16 AS NULLABLE, " + // placeholder for int value "not(dt.case_insensitive)::Boolean AS CASE_SENSITIVE, " + java.sql.DatabaseMetaData.typeSearchable + "::Int16 AS SEARCHABLE, " + "not(attrs.c3)::Boolean AS UNSIGNED_ATTRIBUTE, " + @@ -1140,8 +1140,6 @@ private static String getDataTypeInfoSql() { return sql.toString(); } - private static final ClickHouseColumn.ValueFunction TYPE_INFO_DATA_TYPE_VALUE_FUNC = dataTypeValueFunction(DATA_TYPE_INFO_SQL_TYPE_NAME_COL); - @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { try { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java index ba31872ca..152eadc17 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java @@ -53,17 +53,6 @@ public int getOriginalColumnCount() { return columns.size(); } - /** - * This method used to truncate list of column so it is possible to - * "hide" columns from the end of the list. - * Note: we use this to implement column replacement. it is needed when DB calculation is too hard compare to a - * programmatic approach. - * @param columnCount - */ - public void setColumnCount(int columnCount) { - this.columnCount = columnCount; - } - @Override public boolean isAutoIncrement(int column) throws SQLException { return false; // no auto-incremental types diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java index c49e29cbb..8b93a1555 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java @@ -175,7 +175,7 @@ public void testGetColumnsWithTable() throws Exception { if (decimalDigits != null) { assertEquals(rs.getInt("DECIMAL_DIGITS"), decimalDigits.intValue()); } else { - assertEquals(0, rs.getInt("DECIMAL_DIGITS")); // should not throw exception + assertEquals(rs.getInt("DECIMAL_DIGITS"), 0); // should not throw exception assertTrue(rs.wasNull()); } Integer precisionRadix = columnRadix.get(colIndex); From 8a72b8c48676e6971e80625a95d30c74d87c21a1 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 19 Aug 2025 22:39:56 -0700 Subject: [PATCH 3/6] reverted unneed changes --- .../main/java/com/clickhouse/data/ClickHouseColumn.java | 1 - .../data_formats/internal/AbstractBinaryFormatReader.java | 5 +---- .../test/java/com/clickhouse/client/query/QueryTests.java | 3 --- .../clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java | 2 +- .../clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java | 8 -------- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java index 734e3fe56..0b5f4e28a 100644 --- a/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java +++ b/clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java @@ -1196,5 +1196,4 @@ public String toString() { } return builder.append(' ').append(originalTypeName).toString(); } - } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 3b148e0ee..d393d8503 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -176,8 +176,7 @@ protected boolean readRecord(Object[] record) throws IOException { boolean firstColumn = true; for (int i = 0; i < columns.length; i++) { try { - ClickHouseColumn column = columns[i]; - Object val = binaryStreamReader.readValue(column); + Object val = binaryStreamReader.readValue(columns[i]); if (val != null) { record[i] = val; } else { @@ -192,11 +191,9 @@ protected boolean readRecord(Object[] record) throws IOException { throw e; } } - return true; } - @Override public T readValue(int colIndex) { if (colIndex < 1 || colIndex > getSchema().getColumns().size()) { diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index cf5d2c86f..9d5ae7b93 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -10,10 +10,8 @@ import com.clickhouse.client.api.ClientException; import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.ServerException; -import com.clickhouse.client.api.command.CommandResponse; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; -import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; import com.clickhouse.client.api.enums.Protocol; import com.clickhouse.client.api.insert.InsertResponse; @@ -44,7 +42,6 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import org.testng.util.Strings; import java.io.BufferedReader; import java.io.BufferedWriter; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index 97ee11519..2d130a4a3 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -229,7 +229,7 @@ public String getStringFunctions() throws SQLException { public String getSystemFunctions() throws SQLException { // took from below URL(not from system.functions): // https://clickhouse.com/docs/en/sql-reference/functions/other-functions/ - return "bar,basename,blockNumber,blockSerializedSize,blockSize,buildId,byteSize,countDigits,currentDatabase,currentProfiles,currentRoles,currentUser,defaultProfiles,defaultRoles,defaultValueOfArgumentType,defaultValueOfTypeName,dumpColumnStructure,enabledProfiles,enabledRoles,errorCodeToName,filesystemAvailable,filesystemCapacity,filesystemFree,finalizeAggregation,formatReadableQuantity,formatReadableSize,formatReadableTimeDelta,FQDN,getMacro,getServerPort,getSetting,getSizeOfEnumType,greatest,hasColumnInTable,hostName,identity,ifNotFinite,ignore,indexHint,initializeAggregation,initialQueryID,isConstant,isDecimalOverflow,isFinite,isInfinite,isNaN,joinGet,least,MACNumToString,MACStringToNum,MACStringToOUI,materialize,modelEvaluate,neighbor,queryID,randomFixedString,randomPrintableASCII,randomString,randomStringUTF8,replicate,rowNumberInAllBlocks,rowNumberInBlock,runningAccumulate,runningConcurrency,runningDifference,runningDifferenceStartingWithFirstValue,shardCount ,shardNum,sleep,sleepEachRow,tcpPort,throwIf,toColumnTypeName,toTypeName,e,uptime,version,visibleWidth"; + return "bar,basename,blockNumber,blockSerializedSize,blockSize,buildId,byteSize,countDigits,currentDatabase,currentProfiles,currentRoles,currentUser,defaultProfiles,defaultRoles,defaultValueOfArgumentType,defaultValueOfTypeName,dumpColumnStructure,enabledProfiles,enabledRoles,errorCodeToName,filesystemAvailable,filesystemCapacity,filesystemFree,finalizeAggregation,formatReadableQuantity,formatReadableSize,formatReadableTimeDelta,FQDN,getMacro,getServerPort,getSetting,getSizeOfEnumType,greatest,hasColumnInTable,hostName,identity,ifNotFinite,ignore,indexHint,initializeAggregation,initialQueryID,isConstant,isDecimalOverflow,isFinite,isInfinite,isNaN,joinGet,least,MACNumToString,MACStringToNum,MACStringToOUI,materialize,modelEvaluate,neighbor,queryID,randomFixedString,randomPrintableASCII,randomString,randomStringUTF8,replicate,rowNumberInAllBlocks,rowNumberInBlock,runningAccumulate,runningConcurrency,runningDifference,runningDifferenceStartingWithFirstValue,shardCount ,shardNum,sleep,sleepEachRow,tcpPort,throwIf,toColumnTypeName,toTypeName,transform,uptime,version,visibleWidth"; } @Override diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java index 152eadc17..ae713da39 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java @@ -6,7 +6,6 @@ import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; import com.google.common.collect.ImmutableList; -import com.google.common.collect.UnmodifiableListIterator; import java.sql.SQLException; import java.util.List; @@ -24,8 +23,6 @@ public class ResultSetMetaDataImpl implements java.sql.ResultSetMetaData, JdbcV2 private final Map> typeClassMap; - private int columnCount; - public ResultSetMetaDataImpl(List columns, String schema, String catalog, String tableName, Map> typeClassMap) { this.columns = ImmutableList.copyOf(columns); @@ -33,7 +30,6 @@ public ResultSetMetaDataImpl(List columns, String schema, Stri this.catalog = catalog; this.tableName = tableName; this.typeClassMap = typeClassMap; - this.columnCount = columns.size(); } private ClickHouseColumn getColumn(int column) throws SQLException { @@ -46,10 +42,6 @@ private ClickHouseColumn getColumn(int column) throws SQLException { @Override public int getColumnCount() throws SQLException { - return columnCount; - } - - public int getOriginalColumnCount() { return columns.size(); } From f22eb15cafd315a61bd12250e4ac1eead08568a6 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 19 Aug 2025 23:29:20 -0700 Subject: [PATCH 4/6] added tests for detached result set --- .../jdbc/internal/DetachedResultSet.java | 241 +++++++----- .../jdbc/DetachedResultSetTest.java | 350 ++++++++++++++++++ 2 files changed, 498 insertions(+), 93 deletions(-) create mode 100644 jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java index e0df8a208..d6f758921 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java @@ -91,7 +91,7 @@ public static DetachedResultSet createFromResultSet(ResultSet resultSet, Collect @Override public boolean next() throws SQLException { - + ensureOpen(); if (iterator.hasNext()) { row++; record = iterator.next(); @@ -111,6 +111,7 @@ record = null; @Override public boolean wasNull() throws SQLException { + ensureOpen(); return wasNull; } @@ -196,41 +197,49 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException { @Override public String getString(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, String.class); } @Override public boolean getBoolean(String columnLabel) throws SQLException { - return getObject(columnLabel, Boolean.class); + ensureOpen(); + return (boolean) getObjectImpl(columnLabel, Boolean.class, Boolean.FALSE); } private Number getNumber(String columnLabel) throws SQLException { + ensureOpen(); return (Number) getObjectImpl(columnLabel, Number.class, BigInteger.ZERO); } @Override public byte getByte(String columnLabel) throws SQLException { + ensureOpen(); return getNumber(columnLabel).byteValue(); } @Override public short getShort(String columnLabel) throws SQLException { + ensureOpen(); return getNumber(columnLabel).shortValue(); } @Override public int getInt(String columnLabel) throws SQLException { + ensureOpen(); return getNumber(columnLabel).intValue(); } @Override public long getLong(String columnLabel) throws SQLException { + ensureOpen(); return getNumber(columnLabel).longValue(); } @Override public float getFloat(String columnLabel) throws SQLException { + ensureOpen(); return getNumber(columnLabel).floatValue(); } @@ -241,61 +250,72 @@ public double getDouble(String columnLabel) throws SQLException { @Override public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + ensureOpen(); return getObject(columnLabel, BigDecimal.class); } @Override public byte[] getBytes(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, byte[].class); } @Override public Date getDate(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, Date.class); } @Override public Time getTime(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, Time.class); } @Override public Timestamp getTimestamp(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, Timestamp.class); } @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, InputStream.class); } @Override public InputStream getUnicodeStream(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, InputStream.class); } @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { + ensureOpen(); return getObject(columnLabel, InputStream.class); } @Override public SQLWarning getWarnings() throws SQLException { + ensureOpen(); return null; } @Override public void clearWarnings() throws SQLException { - + ensureOpen(); } @Override public String getCursorName() throws SQLException { + ensureOpen(); return ""; } @Override public ResultSetMetaData getMetaData() throws SQLException { + ensureOpen(); return metaData; } @@ -311,6 +331,7 @@ public Object getObject(String columnLabel) throws SQLException { @Override public int findColumn(String columnLabel) throws SQLException { + ensureOpen(); Integer index = columnMap.get(columnLabel); if (index == null) { throw new SQLException("Column not found: " + columnLabel, ExceptionUtils.SQL_STATE_CLIENT_ERROR); @@ -340,64 +361,77 @@ public BigDecimal getBigDecimal(String columnLabel) throws SQLException { @Override public boolean isBeforeFirst() throws SQLException { + ensureOpen(); return row == ResultSetImpl.BEFORE_FIRST; } @Override public boolean isAfterLast() throws SQLException { + ensureOpen(); return row == ResultSetImpl.AFTER_LAST; } @Override public boolean isFirst() throws SQLException { + ensureOpen(); return row == ResultSetImpl.FIRST_ROW; } @Override public boolean isLast() throws SQLException { + ensureOpen(); return row == lastRow; } @Override public void beforeFirst() throws SQLException { + ensureOpen(); } @Override public void afterLast() throws SQLException { + ensureOpen(); } @Override public boolean first() throws SQLException { + ensureOpen(); return false; } @Override public boolean last() throws SQLException { + ensureOpen(); return false; } @Override public int getRow() throws SQLException { - return row; + ensureOpen(); + return Math.max(row, 0); } @Override public boolean absolute(int row) throws SQLException { + ensureOpen(); return false; } @Override public boolean relative(int rows) throws SQLException { + ensureOpen(); return false; } @Override public boolean previous() throws SQLException { + ensureOpen(); return false; } @Override public void setFetchDirection(int direction) throws SQLException { + ensureOpen(); if (direction != ResultSet.FETCH_FORWARD) { throw new SQLException("This result set object is of FORWARD ONLY type. Only ResultSet.FETCH_FORWARD is allowed as fetchDirection."); } @@ -405,270 +439,279 @@ public void setFetchDirection(int direction) throws SQLException { @Override public int getFetchDirection() throws SQLException { + ensureOpen(); return FETCH_FORWARD; } @Override public void setFetchSize(int rows) throws SQLException { - + ensureOpen(); } @Override public int getFetchSize() throws SQLException { + ensureOpen(); return 0; } @Override public int getType() throws SQLException { + ensureOpen(); return TYPE_FORWARD_ONLY; } @Override public int getConcurrency() throws SQLException { + ensureOpen(); return CONCUR_READ_ONLY; } @Override public boolean rowUpdated() throws SQLException { + ensureOpen(); return false; } @Override public boolean rowInserted() throws SQLException { + ensureOpen(); return false; } @Override public boolean rowDeleted() throws SQLException { + ensureOpen(); return false; } @Override public void updateNull(int columnIndex) throws SQLException { + ensureOpen(); } @Override public void updateBoolean(int columnIndex, boolean x) throws SQLException { - + ensureOpen(); } @Override public void updateByte(int columnIndex, byte x) throws SQLException { - + ensureOpen(); } @Override public void updateShort(int columnIndex, short x) throws SQLException { - + ensureOpen(); } @Override public void updateInt(int columnIndex, int x) throws SQLException { - + ensureOpen(); } @Override public void updateLong(int columnIndex, long x) throws SQLException { - + ensureOpen(); } @Override public void updateFloat(int columnIndex, float x) throws SQLException { - + ensureOpen(); } @Override public void updateDouble(int columnIndex, double x) throws SQLException { - + ensureOpen(); } @Override public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { - + ensureOpen(); } @Override public void updateString(int columnIndex, String x) throws SQLException { - + ensureOpen(); } @Override public void updateBytes(int columnIndex, byte[] x) throws SQLException { - + ensureOpen(); } @Override public void updateDate(int columnIndex, Date x) throws SQLException { - + ensureOpen(); } @Override public void updateTime(int columnIndex, Time x) throws SQLException { - + ensureOpen(); } @Override public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - + ensureOpen(); } @Override public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { - + ensureOpen(); } @Override public void updateObject(int columnIndex, Object x) throws SQLException { - + ensureOpen(); } @Override public void updateNull(String columnLabel) throws SQLException { - + ensureOpen(); } @Override public void updateBoolean(String columnLabel, boolean x) throws SQLException { - + ensureOpen(); } @Override public void updateByte(String columnLabel, byte x) throws SQLException { - + ensureOpen(); } @Override public void updateShort(String columnLabel, short x) throws SQLException { - + ensureOpen(); } @Override public void updateInt(String columnLabel, int x) throws SQLException { - + ensureOpen(); } @Override public void updateLong(String columnLabel, long x) throws SQLException { - + ensureOpen(); } @Override public void updateFloat(String columnLabel, float x) throws SQLException { - + ensureOpen(); } @Override public void updateDouble(String columnLabel, double x) throws SQLException { - + ensureOpen(); } @Override public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { - + ensureOpen(); } @Override public void updateString(String columnLabel, String x) throws SQLException { - + ensureOpen(); } @Override public void updateBytes(String columnLabel, byte[] x) throws SQLException { - + ensureOpen(); } @Override public void updateDate(String columnLabel, Date x) throws SQLException { - + ensureOpen(); } @Override public void updateTime(String columnLabel, Time x) throws SQLException { - + ensureOpen(); } @Override public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { - + ensureOpen(); } @Override public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { - + ensureOpen(); } @Override public void updateObject(String columnLabel, Object x) throws SQLException { - + ensureOpen(); } @Override public void insertRow() throws SQLException { - + ensureOpen(); } @Override public void updateRow() throws SQLException { - + ensureOpen(); } @Override public void deleteRow() throws SQLException { - + ensureOpen(); } @Override public void refreshRow() throws SQLException { - + ensureOpen(); } @Override public void cancelRowUpdates() throws SQLException { - + ensureOpen(); } @Override public void moveToInsertRow() throws SQLException { - + ensureOpen(); } @Override public void moveToCurrentRow() throws SQLException { - + ensureOpen(); } @Override public Statement getStatement() throws SQLException { + ensureOpen(); return null; // should return null as it is a detached result set } @@ -699,6 +742,7 @@ public Array getArray(int columnIndex) throws SQLException { @Override public Object getObject(String columnLabel, Map> map) throws SQLException { + ensureOpen(); if (map == null) { throw new SQLException("map must be not null", ExceptionUtils.SQL_STATE_CLIENT_ERROR); } @@ -738,6 +782,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { @Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); return null; } @@ -748,6 +793,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { @Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); return null; } @@ -758,6 +804,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException @Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); return null; } @@ -773,42 +820,42 @@ public URL getURL(String columnLabel) throws SQLException { @Override public void updateRef(int columnIndex, Ref x) throws SQLException { - + ensureOpen(); } @Override public void updateRef(String columnLabel, Ref x) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(int columnIndex, Blob x) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(String columnLabel, Blob x) throws SQLException { - + ensureOpen(); } @Override public void updateClob(int columnIndex, Clob x) throws SQLException { - + ensureOpen(); } @Override public void updateClob(String columnLabel, Clob x) throws SQLException { - + ensureOpen(); } @Override public void updateArray(int columnIndex, Array x) throws SQLException { - + ensureOpen(); } @Override public void updateArray(String columnLabel, Array x) throws SQLException { - + ensureOpen(); } @Override @@ -823,16 +870,17 @@ public RowId getRowId(String columnLabel) throws SQLException { @Override public void updateRowId(int columnIndex, RowId x) throws SQLException { - + ensureOpen(); } @Override public void updateRowId(String columnLabel, RowId x) throws SQLException { - + ensureOpen(); } @Override public int getHoldability() throws SQLException { + ensureOpen(); return HOLD_CURSORS_OVER_COMMIT; // this result set remains open } @@ -843,22 +891,22 @@ public boolean isClosed() throws SQLException { @Override public void updateNString(int columnIndex, String nString) throws SQLException { - + ensureOpen(); } @Override public void updateNString(String columnLabel, String nString) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { - + ensureOpen(); } @Override @@ -883,12 +931,12 @@ public SQLXML getSQLXML(String columnLabel) throws SQLException { @Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { - + ensureOpen(); } @Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { - + ensureOpen(); } @Override @@ -913,142 +961,142 @@ public Reader getNCharacterStream(String columnLabel) throws SQLException { @Override public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { - + ensureOpen(); } @Override public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { - + ensureOpen(); } @Override public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { - + ensureOpen(); } @Override public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { - + ensureOpen(); } @Override public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { - + ensureOpen(); } @Override public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { - + ensureOpen(); } @Override public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { - + ensureOpen(); } @Override public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { - + ensureOpen(); } @Override public void updateClob(int columnIndex, Reader reader) throws SQLException { - + ensureOpen(); } @Override public void updateClob(String columnLabel, Reader reader) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(int columnIndex, Reader reader) throws SQLException { - + ensureOpen(); } @Override public void updateNClob(String columnLabel, Reader reader) throws SQLException { - + ensureOpen(); } @Override @@ -1058,6 +1106,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { @Override public T getObject(String columnLabel, Class type) throws SQLException { + ensureOpen(); return (T) getObjectImpl(columnLabel, type, null); } @@ -1070,4 +1119,10 @@ private Object getObjectImpl(String columnLabel, Class type, Object nullValue return JdbcUtils.convert(value, type); } + + private void ensureOpen() throws SQLException { + if (closed) { + throw new SQLException("ResultSet is closed.", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION); + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java new file mode 100644 index 000000000..ddc849624 --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java @@ -0,0 +1,350 @@ +package com.clickhouse.jdbc; + +import com.clickhouse.jdbc.internal.DetachedResultSet; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Collections; +import java.util.Properties; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +public class DetachedResultSetTest extends JdbcIntegrationTest { + + @Test(groups = "integration") + public void shouldReturnColumnIndex() throws SQLException { + runQuery("CREATE TABLE rs_test_data (id UInt32, val UInt8) ENGINE = MergeTree ORDER BY (id)"); + runQuery("INSERT INTO rs_test_data VALUES (1, 10), (2, 20)"); + + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM rs_test_data ORDER BY id")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.findColumn("id"), 1); + assertEquals(rs.getInt(1), 1); + assertEquals(rs.findColumn("val"), 2); + assertEquals(rs.getInt(2), 10); + + assertTrue(rs.next()); + assertEquals(rs.findColumn("id"), 1); + assertEquals(rs.getInt(1), 2); + assertEquals(rs.findColumn("val"), 2); + assertEquals(rs.getInt(2), 20); + } + } + } + } + + @Test(groups = {"integration"}) + public void testUnsupportedOperations() throws Throwable { + + try (Connection conn = this.getJdbcConnection(); Statement stmt = conn.createStatement(); + ResultSet srcRs = stmt.executeQuery("SELECT 1")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ + rs::first, + rs::afterLast, + rs::beforeFirst, + () -> rs.absolute(-1), + () -> rs.relative(-1), + rs::moveToCurrentRow, + rs::moveToInsertRow, + rs::last, + rs::previous, + rs::refreshRow, + () -> rs.updateBoolean("col1", true), + () -> rs.updateByte("col1", (byte) 1), + () -> rs.updateShort("col1", (short) 1), + () -> rs.updateInt("col1", 1), + () -> rs.updateLong("col1", 1L), + () -> rs.updateFloat("col1", 1.1f), + () -> rs.updateDouble("col1", 1.1), + () -> rs.updateBigDecimal("col1", BigDecimal.valueOf(1.1)), + () -> rs.updateString("col1", "test"), + () -> rs.updateNString("col1", "test"), + () -> rs.updateBytes("col1", new byte[1]), + () -> rs.updateDate("col1", Date.valueOf("2020-01-01")), + () -> rs.updateTime("col1", Time.valueOf("12:34:56")), + () -> rs.updateTimestamp("col1", Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob("col1", (Blob) null), + () -> rs.updateClob("col1", new StringReader("test")), + () -> rs.updateNClob("col1", new StringReader("test")), + + () -> rs.updateBoolean(1, true), + () -> rs.updateByte(1, (byte) 1), + () -> rs.updateShort(1, (short) 1), + () -> rs.updateInt(1, 1), + () -> rs.updateLong(1, 1L), + () -> rs.updateFloat(1, 1.1f), + () -> rs.updateDouble(1, 1.1), + () -> rs.updateBigDecimal(1, BigDecimal.valueOf(1.1)), + () -> rs.updateString(1, "test"), + () -> rs.updateNString(1, "test"), + () -> rs.updateBytes(1, new byte[1]), + () -> rs.updateDate(1, Date.valueOf("2020-01-01")), + () -> rs.updateTime(1, Time.valueOf("12:34:56")), + () -> rs.updateTimestamp(1, Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob(1, (Blob) null), + () -> rs.updateClob(1, new StringReader("test")), + () -> rs.updateNClob(1, new StringReader("test")), + () -> rs.updateObject(1, 1), + () -> rs.updateObject("col1", 1), + () -> rs.updateObject(1, "test", Types.INTEGER), + () -> rs.updateObject("col1", "test", Types.INTEGER), + + () -> rs.updateCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateCharacterStream(1, new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateNCharacterStream(1, new StringReader("test")), + () -> rs.updateNCharacterStream("col1", new StringReader("test")), + () -> rs.updateBlob(1, (InputStream) null), + () -> rs.updateBlob("col1", (InputStream) null), + () -> rs.updateBlob(1, (InputStream) null, -1), + () -> rs.updateBlob("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null), + () -> rs.updateBinaryStream("col1", (InputStream) null), + () -> rs.updateBinaryStream(1, (InputStream) null, -1), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null, -1L), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1L), + () -> rs.updateAsciiStream(1, (InputStream) null), + () -> rs.updateAsciiStream("col1", (InputStream) null), + () -> rs.updateAsciiStream(1, (InputStream) null, -1), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1), + () -> rs.updateAsciiStream(1, (InputStream) null, -1L), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1L), + () -> rs.updateClob(1, (Reader) null), + () -> rs.updateClob("col1", (Reader) null), + () -> rs.updateClob(1, (Reader) null, -1), + () -> rs.updateClob("col1", (Reader) null, -1), + () -> rs.updateClob(1, (Reader) null, -1L), + () -> rs.updateClob("col1", (Reader) null, -1L), + () -> rs.updateNClob(1, (Reader) null), + () -> rs.updateNClob("col1", (Reader) null), + () -> rs.updateNClob(1, (NClob) null), + () -> rs.updateNClob("col1", (NClob) null), + () -> rs.updateNClob(1, (Reader) null, -1), + () -> rs.updateNClob("col1", (Reader) null, -1), + () -> rs.updateNClob(1, (Reader) null, -1L), + () -> rs.updateNClob("col1", (Reader) null, -1L), + () -> rs.updateRef(1, (Ref) null), + () -> rs.updateRef("col1", (Ref) null), + () -> rs.updateArray(1, (Array) null), + () -> rs.updateArray("col1", (Array) null), + rs::cancelRowUpdates, + () -> rs.updateNull(1), + () -> rs.updateNull("col1"), + () -> rs.updateRowId(1, null), + () -> rs.updateRowId("col1", null), + () -> rs.updateClob(1, (Clob) null), + () -> rs.updateClob("col1", (Clob) null), + rs::updateRow, + rs::insertRow, + rs::deleteRow, + rs::rowDeleted, + rs::rowInserted, + rs::rowUpdated, + rs::getCursorName, + }; + + for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { + op.run(); + } + } + } + + + @Test(groups = {"integration"}) + public void testCursorPosition() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertTrue(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 1); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertTrue(rs.isLast()); + Assert.assertEquals(rs.getRow(), 2); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertTrue(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + } + + try (ResultSet rs = stmt.executeQuery("select 1 LIMIT 0")) { + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + + Assert.assertFalse(rs.next()); + + Assert.assertFalse(rs.isBeforeFirst()); // we stepped over the end + Assert.assertTrue(rs.isAfterLast()); // we stepped over the end + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + } + } + } + + + @Test(groups = {"integration"}) + public void testFetchDirectionsAndSize() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + Assert.assertEquals(rs.getFetchDirection(), ResultSet.FETCH_FORWARD); + Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_REVERSE)); + Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_UNKNOWN)); + rs.setFetchDirection(ResultSet.FETCH_FORWARD); + + Assert.assertEquals(rs.getFetchSize(), 0); + rs.setFetchSize(10); + Assert.assertEquals(rs.getFetchSize(), 0); + } + } + } + + @Test(groups = {"integration"}) + public void testConstants() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + Assert.assertNull(rs.getStatement()); + Assert.assertEquals(rs.getType(), ResultSet.TYPE_FORWARD_ONLY); + Assert.assertEquals(rs.getConcurrency(), ResultSet.CONCUR_READ_ONLY); + Assert.assertEquals(rs.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + assertFalse(rs.isClosed()); + rs.close(); + assertTrue(rs.isClosed()); + assertThrows(SQLException.class, rs::next); + assertThrows(SQLException.class, rs::getStatement); + assertThrows(SQLException.class, rs::getMetaData); + } + } + } + + @Test(groups = {"integration"}) + public void testWasNull() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String sql = "select NULL::Nullable(%s) as v1"; + + try (ResultSet srcRs = stmt.executeQuery(sql.formatted("Int64"))) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + rs.next(); + Assert.assertFalse(rs.wasNull()); + + Assert.assertEquals(rs.getByte(1), (byte) 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getByte("v1"), (byte) 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getShort(1), (short) 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getShort("v1"), (short) 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getInt(1), 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getInt("v1"), 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getLong(1), 0L); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getLong("v1"), 0L); + Assert.assertTrue(rs.wasNull()); + + Assert.assertNull(rs.getBigDecimal(1)); + Assert.assertTrue(rs.wasNull()); + Assert.assertNull(rs.getBigDecimal("v1")); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getFloat(1), 0f); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getFloat("v1"), 0f); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getDouble(1), 0d); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getDouble("v1"), 0d); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getBoolean(1), false); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getBoolean("v1"), false); + Assert.assertTrue(rs.wasNull()); + } + } + } + + @Test(groups = {"integration"}) + public void testGetMetadata() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select '1'::Int32 as v1, 'test' as v2 ")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + int v1ColumnIndex = rs.findColumn("v1"); + int v2ColumnIndex = rs.findColumn("v2"); + + ResultSetMetaData metaData = rs.getMetaData(); + Assert.assertEquals(metaData.getColumnCount(), 2); + Assert.assertEquals(metaData.getColumnType(v1ColumnIndex), Types.INTEGER); + Assert.assertEquals(metaData.getColumnType(v2ColumnIndex), Types.VARCHAR); + Assert.assertEquals(metaData.getColumnTypeName(v1ColumnIndex), "Int32"); + Assert.assertEquals(metaData.getColumnTypeName(v2ColumnIndex), "String"); + } + } + } +} From ae60aceb898fbdc6285da6fec600b795b5869cbb Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Wed, 20 Aug 2025 13:51:49 -0700 Subject: [PATCH 5/6] fix table conflict in tests --- .../test/java/com/clickhouse/jdbc/DetachedResultSetTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java index ddc849624..fe861b092 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java @@ -36,8 +36,8 @@ public class DetachedResultSetTest extends JdbcIntegrationTest { @Test(groups = "integration") public void shouldReturnColumnIndex() throws SQLException { - runQuery("CREATE TABLE rs_test_data (id UInt32, val UInt8) ENGINE = MergeTree ORDER BY (id)"); - runQuery("INSERT INTO rs_test_data VALUES (1, 10), (2, 20)"); + runQuery("CREATE TABLE detached_rs_test_data (id UInt32, val UInt8) ENGINE = MergeTree ORDER BY (id)"); + runQuery("INSERT INTO detached_rs_test_data VALUES (1, 10), (2, 20)"); try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { From 19122352406a47bcbec39b1190c7293c7059b33d Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 21 Aug 2025 23:31:32 -0700 Subject: [PATCH 6/6] added reading date-time --- .../com/clickhouse/jdbc/ConnectionImpl.java | 4 + .../jdbc/internal/DetachedResultSet.java | 65 ++- .../jdbc/metadata/DatabaseMetaDataImpl.java | 4 +- .../jdbc/DetachedResultSetTest.java | 440 ++++++++++++------ 4 files changed, 371 insertions(+), 142 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index efcb8b7e2..a38515944 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -130,6 +130,10 @@ public void setDefaultQuerySettings(QuerySettings settings) { this.defaultQuerySettings = settings; } + public Calendar getDefaultCalendar() { + return defaultCalendar; + } + public String getServerVersion() throws SQLException { GenericRecord result = client.queryAll("SELECT version() as server_version").stream() .findFirst().orElseThrow(() -> new SQLException("Failed to retrieve server version.", ExceptionUtils.SQL_STATE_CLIENT_ERROR)); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java index d6f758921..5c13569b1 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java @@ -23,6 +23,11 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -46,6 +51,8 @@ public class DetachedResultSet implements ResultSet, JdbcV2Wrapper { private ResultSetMetaData metaData; + private final Calendar defaultCalendar; + private Map record; private boolean wasNull; @@ -58,10 +65,11 @@ public class DetachedResultSet implements ResultSet, JdbcV2Wrapper { private Map columnMap; - private DetachedResultSet(List> records, ResultSetMetaData metaData) throws SQLException { + private DetachedResultSet(List> records, ResultSetMetaData metaData, Calendar defaultCalendar) throws SQLException { this.records = records; this.iterator = records.listIterator(); this.metaData = metaData; + this.defaultCalendar = defaultCalendar; this.wasNull = false; this.row = ResultSetImpl.BEFORE_FIRST; this.lastRow = records.size(); @@ -73,7 +81,7 @@ private DetachedResultSet(List> records, ResultSetMetaData m } } - public static DetachedResultSet createFromResultSet(ResultSet resultSet, Collection>> mutators) throws SQLException { + public static DetachedResultSet createFromResultSet(ResultSet resultSet, Calendar defaultCalendar, Collection>> mutators) throws SQLException { ResultSetMetaData metaData = resultSet.getMetaData(); List> records = new ArrayList<>(); while (resultSet.next()) { @@ -86,7 +94,7 @@ public static DetachedResultSet createFromResultSet(ResultSet resultSet, Collect } records.add(record); } - return new DetachedResultSet(records, metaData); + return new DetachedResultSet(records, metaData, defaultCalendar); } @Override @@ -263,19 +271,19 @@ public byte[] getBytes(String columnLabel) throws SQLException { @Override public Date getDate(String columnLabel) throws SQLException { ensureOpen(); - return getObject(columnLabel, Date.class); + return getDate(columnLabel, defaultCalendar); } @Override public Time getTime(String columnLabel) throws SQLException { ensureOpen(); - return getObject(columnLabel, Time.class); + return getTime(columnLabel, defaultCalendar); } @Override public Timestamp getTimestamp(String columnLabel) throws SQLException { ensureOpen(); - return getObject(columnLabel, Timestamp.class); + return getTimestamp(columnLabel, defaultCalendar); } @Override @@ -783,7 +791,19 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { @Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { ensureOpen(); - return null; + try { + Date date = getObject(columnLabel, Date.class); + if (date != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalDate ld = date.toLocalDate(); + c.set(ld.getYear(), ld.getMonthValue() - 1, ld.getDayOfMonth(), 0, 0, 0); + date = new Date(c.getTimeInMillis()); + } + return date; + } catch (Exception e) { + throw new SQLException(String.format("Method: getDate(\"%s\") encountered an exception.", columnLabel), e); + } } @Override @@ -794,7 +814,19 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { @Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { ensureOpen(); - return null; + try { + Time time = getObject(columnLabel, Time.class); + if (time != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalTime ld = time.toLocalTime(); + c.set(1970, Calendar.JANUARY, 1, ld.getHour(), ld.getMinute(), ld.getSecond()); + time = new Time(c.getTimeInMillis()); + } + return time; + } catch (Exception e) { + throw new SQLException(String.format("Method: getTime(\"%s\") encountered an exception.", columnLabel), e); + } } @Override @@ -805,7 +837,22 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException @Override public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { ensureOpen(); - return null; + try { + Timestamp timestamp = getObject(columnLabel, Timestamp.class); + if (timestamp != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalDateTime ldt = timestamp.toLocalDateTime(); + c.set(ldt.getYear(), ldt.getMonthValue() - 1, ldt.getDayOfMonth(), ldt.getHour(), ldt.getMinute(), + ldt.getSecond()); + timestamp = new Timestamp(c.getTimeInMillis()); + timestamp.setNanos(ldt.getNano()); + return timestamp; + } + return timestamp; + } catch (Exception e) { + throw new SQLException(String.format("Method: getTimestamp(\"%s\") encountered an exception.", columnLabel), e); + } } @Override diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index 2d130a4a3..a42429548 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -872,7 +872,7 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa " AND name LIKE " + SQLUtils.enquoteLiteral(columnNamePattern == null ? "%" : columnNamePattern) + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery(sql)) { - return DetachedResultSet.createFromResultSet(rs, GET_COLUMNS_RS_MUTATORS); + return DetachedResultSet.createFromResultSet(rs, connection.getDefaultCalendar(), GET_COLUMNS_RS_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } @@ -1079,7 +1079,7 @@ public ResultSet getCrossReference(String parentCatalog, String parentSchema, St @SuppressWarnings({"squid:S2095"}) public ResultSet getTypeInfo() throws SQLException { try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(DATA_TYPE_INFO_SQL)) { - return DetachedResultSet.createFromResultSet(rs, GET_TYPE_INFO_MUTATORS); + return DetachedResultSet.createFromResultSet(rs, connection.getDefaultCalendar(), GET_TYPE_INFO_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java index fe861b092..94fee56db 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc; +import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.jdbc.internal.DetachedResultSet; import org.testng.Assert; import org.testng.annotations.Test; @@ -13,19 +14,23 @@ import java.sql.Clob; import java.sql.Connection; import java.sql.Date; -import java.sql.JDBCType; import java.sql.NClob; +import java.sql.PreparedStatement; import java.sql.Ref; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; import java.util.Collections; -import java.util.Properties; +import java.util.GregorianCalendar; +import java.util.TimeZone; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -34,6 +39,8 @@ public class DetachedResultSetTest extends JdbcIntegrationTest { + private static Calendar defaultCalendar = Calendar.getInstance(); + @Test(groups = "integration") public void shouldReturnColumnIndex() throws SQLException { runQuery("CREATE TABLE detached_rs_test_data (id UInt32, val UInt8) ENGINE = MergeTree ORDER BY (id)"); @@ -41,8 +48,8 @@ public void shouldReturnColumnIndex() throws SQLException { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { - try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM rs_test_data ORDER BY id")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_data ORDER BY id")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); assertTrue(rs.next()); assertEquals(rs.findColumn("id"), 1); assertEquals(rs.getInt(1), 1); @@ -62,127 +69,127 @@ public void shouldReturnColumnIndex() throws SQLException { @Test(groups = {"integration"}) public void testUnsupportedOperations() throws Throwable { - try (Connection conn = this.getJdbcConnection(); Statement stmt = conn.createStatement(); - ResultSet srcRs = stmt.executeQuery("SELECT 1")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); - Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ - rs::first, - rs::afterLast, - rs::beforeFirst, - () -> rs.absolute(-1), - () -> rs.relative(-1), - rs::moveToCurrentRow, - rs::moveToInsertRow, - rs::last, - rs::previous, - rs::refreshRow, - () -> rs.updateBoolean("col1", true), - () -> rs.updateByte("col1", (byte) 1), - () -> rs.updateShort("col1", (short) 1), - () -> rs.updateInt("col1", 1), - () -> rs.updateLong("col1", 1L), - () -> rs.updateFloat("col1", 1.1f), - () -> rs.updateDouble("col1", 1.1), - () -> rs.updateBigDecimal("col1", BigDecimal.valueOf(1.1)), - () -> rs.updateString("col1", "test"), - () -> rs.updateNString("col1", "test"), - () -> rs.updateBytes("col1", new byte[1]), - () -> rs.updateDate("col1", Date.valueOf("2020-01-01")), - () -> rs.updateTime("col1", Time.valueOf("12:34:56")), - () -> rs.updateTimestamp("col1", Timestamp.valueOf("2020-01-01 12:34:56.789123")), - () -> rs.updateBlob("col1", (Blob) null), - () -> rs.updateClob("col1", new StringReader("test")), - () -> rs.updateNClob("col1", new StringReader("test")), - - () -> rs.updateBoolean(1, true), - () -> rs.updateByte(1, (byte) 1), - () -> rs.updateShort(1, (short) 1), - () -> rs.updateInt(1, 1), - () -> rs.updateLong(1, 1L), - () -> rs.updateFloat(1, 1.1f), - () -> rs.updateDouble(1, 1.1), - () -> rs.updateBigDecimal(1, BigDecimal.valueOf(1.1)), - () -> rs.updateString(1, "test"), - () -> rs.updateNString(1, "test"), - () -> rs.updateBytes(1, new byte[1]), - () -> rs.updateDate(1, Date.valueOf("2020-01-01")), - () -> rs.updateTime(1, Time.valueOf("12:34:56")), - () -> rs.updateTimestamp(1, Timestamp.valueOf("2020-01-01 12:34:56.789123")), - () -> rs.updateBlob(1, (Blob) null), - () -> rs.updateClob(1, new StringReader("test")), - () -> rs.updateNClob(1, new StringReader("test")), - () -> rs.updateObject(1, 1), - () -> rs.updateObject("col1", 1), - () -> rs.updateObject(1, "test", Types.INTEGER), - () -> rs.updateObject("col1", "test", Types.INTEGER), - - () -> rs.updateCharacterStream(1, new StringReader("test"), 1), - () -> rs.updateCharacterStream("col1", new StringReader("test")), - () -> rs.updateCharacterStream("col1", new StringReader("test"), 1), - () -> rs.updateCharacterStream(1, new StringReader("test"), 1L), - () -> rs.updateCharacterStream("col1", new StringReader("test"), 1L), - () -> rs.updateCharacterStream(1, new StringReader("test")), - () -> rs.updateCharacterStream("col1", new StringReader("test")), - () -> rs.updateNCharacterStream(1, new StringReader("test"), 1), - () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1), - () -> rs.updateNCharacterStream(1, new StringReader("test"), 1L), - () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1L), - () -> rs.updateNCharacterStream(1, new StringReader("test")), - () -> rs.updateNCharacterStream("col1", new StringReader("test")), - () -> rs.updateBlob(1, (InputStream) null), - () -> rs.updateBlob("col1", (InputStream) null), - () -> rs.updateBlob(1, (InputStream) null, -1), - () -> rs.updateBlob("col1", (InputStream) null, -1), - () -> rs.updateBinaryStream(1, (InputStream) null), - () -> rs.updateBinaryStream("col1", (InputStream) null), - () -> rs.updateBinaryStream(1, (InputStream) null, -1), - () -> rs.updateBinaryStream("col1", (InputStream) null, -1), - () -> rs.updateBinaryStream(1, (InputStream) null, -1L), - () -> rs.updateBinaryStream("col1", (InputStream) null, -1L), - () -> rs.updateAsciiStream(1, (InputStream) null), - () -> rs.updateAsciiStream("col1", (InputStream) null), - () -> rs.updateAsciiStream(1, (InputStream) null, -1), - () -> rs.updateAsciiStream("col1", (InputStream) null, -1), - () -> rs.updateAsciiStream(1, (InputStream) null, -1L), - () -> rs.updateAsciiStream("col1", (InputStream) null, -1L), - () -> rs.updateClob(1, (Reader) null), - () -> rs.updateClob("col1", (Reader) null), - () -> rs.updateClob(1, (Reader) null, -1), - () -> rs.updateClob("col1", (Reader) null, -1), - () -> rs.updateClob(1, (Reader) null, -1L), - () -> rs.updateClob("col1", (Reader) null, -1L), - () -> rs.updateNClob(1, (Reader) null), - () -> rs.updateNClob("col1", (Reader) null), - () -> rs.updateNClob(1, (NClob) null), - () -> rs.updateNClob("col1", (NClob) null), - () -> rs.updateNClob(1, (Reader) null, -1), - () -> rs.updateNClob("col1", (Reader) null, -1), - () -> rs.updateNClob(1, (Reader) null, -1L), - () -> rs.updateNClob("col1", (Reader) null, -1L), - () -> rs.updateRef(1, (Ref) null), - () -> rs.updateRef("col1", (Ref) null), - () -> rs.updateArray(1, (Array) null), - () -> rs.updateArray("col1", (Array) null), - rs::cancelRowUpdates, - () -> rs.updateNull(1), - () -> rs.updateNull("col1"), - () -> rs.updateRowId(1, null), - () -> rs.updateRowId("col1", null), - () -> rs.updateClob(1, (Clob) null), - () -> rs.updateClob("col1", (Clob) null), - rs::updateRow, - rs::insertRow, - rs::deleteRow, - rs::rowDeleted, - rs::rowInserted, - rs::rowUpdated, - rs::getCursorName, - }; - - for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { - op.run(); - } + try (Connection conn = this.getJdbcConnection(); Statement stmt = conn.createStatement(); + ResultSet srcRs = stmt.executeQuery("SELECT 1")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ + rs::first, + rs::afterLast, + rs::beforeFirst, + () -> rs.absolute(-1), + () -> rs.relative(-1), + rs::moveToCurrentRow, + rs::moveToInsertRow, + rs::last, + rs::previous, + rs::refreshRow, + () -> rs.updateBoolean("col1", true), + () -> rs.updateByte("col1", (byte) 1), + () -> rs.updateShort("col1", (short) 1), + () -> rs.updateInt("col1", 1), + () -> rs.updateLong("col1", 1L), + () -> rs.updateFloat("col1", 1.1f), + () -> rs.updateDouble("col1", 1.1), + () -> rs.updateBigDecimal("col1", BigDecimal.valueOf(1.1)), + () -> rs.updateString("col1", "test"), + () -> rs.updateNString("col1", "test"), + () -> rs.updateBytes("col1", new byte[1]), + () -> rs.updateDate("col1", Date.valueOf("2020-01-01")), + () -> rs.updateTime("col1", Time.valueOf("12:34:56")), + () -> rs.updateTimestamp("col1", Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob("col1", (Blob) null), + () -> rs.updateClob("col1", new StringReader("test")), + () -> rs.updateNClob("col1", new StringReader("test")), + + () -> rs.updateBoolean(1, true), + () -> rs.updateByte(1, (byte) 1), + () -> rs.updateShort(1, (short) 1), + () -> rs.updateInt(1, 1), + () -> rs.updateLong(1, 1L), + () -> rs.updateFloat(1, 1.1f), + () -> rs.updateDouble(1, 1.1), + () -> rs.updateBigDecimal(1, BigDecimal.valueOf(1.1)), + () -> rs.updateString(1, "test"), + () -> rs.updateNString(1, "test"), + () -> rs.updateBytes(1, new byte[1]), + () -> rs.updateDate(1, Date.valueOf("2020-01-01")), + () -> rs.updateTime(1, Time.valueOf("12:34:56")), + () -> rs.updateTimestamp(1, Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob(1, (Blob) null), + () -> rs.updateClob(1, new StringReader("test")), + () -> rs.updateNClob(1, new StringReader("test")), + () -> rs.updateObject(1, 1), + () -> rs.updateObject("col1", 1), + () -> rs.updateObject(1, "test", Types.INTEGER), + () -> rs.updateObject("col1", "test", Types.INTEGER), + + () -> rs.updateCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateCharacterStream(1, new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateNCharacterStream(1, new StringReader("test")), + () -> rs.updateNCharacterStream("col1", new StringReader("test")), + () -> rs.updateBlob(1, (InputStream) null), + () -> rs.updateBlob("col1", (InputStream) null), + () -> rs.updateBlob(1, (InputStream) null, -1), + () -> rs.updateBlob("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null), + () -> rs.updateBinaryStream("col1", (InputStream) null), + () -> rs.updateBinaryStream(1, (InputStream) null, -1), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null, -1L), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1L), + () -> rs.updateAsciiStream(1, (InputStream) null), + () -> rs.updateAsciiStream("col1", (InputStream) null), + () -> rs.updateAsciiStream(1, (InputStream) null, -1), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1), + () -> rs.updateAsciiStream(1, (InputStream) null, -1L), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1L), + () -> rs.updateClob(1, (Reader) null), + () -> rs.updateClob("col1", (Reader) null), + () -> rs.updateClob(1, (Reader) null, -1), + () -> rs.updateClob("col1", (Reader) null, -1), + () -> rs.updateClob(1, (Reader) null, -1L), + () -> rs.updateClob("col1", (Reader) null, -1L), + () -> rs.updateNClob(1, (Reader) null), + () -> rs.updateNClob("col1", (Reader) null), + () -> rs.updateNClob(1, (NClob) null), + () -> rs.updateNClob("col1", (NClob) null), + () -> rs.updateNClob(1, (Reader) null, -1), + () -> rs.updateNClob("col1", (Reader) null, -1), + () -> rs.updateNClob(1, (Reader) null, -1L), + () -> rs.updateNClob("col1", (Reader) null, -1L), + () -> rs.updateRef(1, (Ref) null), + () -> rs.updateRef("col1", (Ref) null), + () -> rs.updateArray(1, (Array) null), + () -> rs.updateArray("col1", (Array) null), + rs::cancelRowUpdates, + () -> rs.updateNull(1), + () -> rs.updateNull("col1"), + () -> rs.updateRowId(1, null), + () -> rs.updateRowId("col1", null), + () -> rs.updateClob(1, (Clob) null), + () -> rs.updateClob("col1", (Clob) null), + rs::updateRow, + rs::insertRow, + rs::deleteRow, + rs::rowDeleted, + rs::rowInserted, + rs::rowUpdated, + rs::getCursorName, + }; + + for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { + op.run(); } + } } @@ -190,7 +197,7 @@ public void testUnsupportedOperations() throws Throwable { public void testCursorPosition() throws SQLException { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); Assert.assertTrue(rs.isBeforeFirst()); Assert.assertFalse(rs.isAfterLast()); Assert.assertFalse(rs.isFirst()); @@ -245,7 +252,7 @@ public void testCursorPosition() throws SQLException { public void testFetchDirectionsAndSize() throws SQLException { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); Assert.assertEquals(rs.getFetchDirection(), ResultSet.FETCH_FORWARD); Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_REVERSE)); Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_UNKNOWN)); @@ -262,7 +269,7 @@ public void testFetchDirectionsAndSize() throws SQLException { public void testConstants() throws SQLException { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); Assert.assertNull(rs.getStatement()); Assert.assertEquals(rs.getType(), ResultSet.TYPE_FORWARD_ONLY); Assert.assertEquals(rs.getConcurrency(), ResultSet.CONCUR_READ_ONLY); @@ -283,7 +290,7 @@ public void testWasNull() throws SQLException { final String sql = "select NULL::Nullable(%s) as v1"; try (ResultSet srcRs = stmt.executeQuery(sql.formatted("Int64"))) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); rs.next(); Assert.assertFalse(rs.wasNull()); @@ -332,9 +339,9 @@ public void testWasNull() throws SQLException { @Test(groups = {"integration"}) public void testGetMetadata() throws SQLException { - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { try (ResultSet srcRs = stmt.executeQuery("select '1'::Int32 as v1, 'test' as v2 ")) { - ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, Collections.emptyList()); + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); int v1ColumnIndex = rs.findColumn("v1"); int v2ColumnIndex = rs.findColumn("v2"); @@ -347,4 +354,175 @@ public void testGetMetadata() throws SQLException { } } } + + @Test(groups = { "integration" }) + public void testDateTypes() throws SQLException { + runQuery("CREATE TABLE detached_rs_test_dates (order Int8, " + + "date Date, date32 Date32, " + + "dateTime DateTime, dateTime32 DateTime32, " + + "dateTime643 DateTime64(3), dateTime646 DateTime64(6), dateTime649 DateTime64(9)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO detached_rs_test_dates VALUES ( 1, '1970-01-01', '1970-01-01', " + + "'1970-01-01 00:00:00', '1970-01-01 00:00:00', " + + "'1970-01-01 00:00:00.000', '1970-01-01 00:00:00.000000', '1970-01-01 00:00:00.000000000' )"); + + // Insert maximum values + insertData("INSERT INTO detached_rs_test_dates VALUES ( 2, '2149-06-06', '2299-12-31', " + + "'2106-02-07 06:28:15', '2106-02-07 06:28:15', " + + "'2261-12-31 23:59:59.999', '2261-12-31 23:59:59.999999', '2261-12-31 23:59:59.999999999' )"); + + // Insert random (valid) values + final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); + final LocalDateTime now = LocalDateTime.now(zoneId); + final Date date = Date.valueOf(now.toLocalDate()); + final Date date32 = Date.valueOf(now.toLocalDate()); + final java.sql.Timestamp dateTime = Timestamp.valueOf(now); + dateTime.setNanos(0); + final java.sql.Timestamp dateTime32 = Timestamp.valueOf(now); + dateTime32.setNanos(0); + final java.sql.Timestamp dateTime643 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime643.setNanos(333000000); + final java.sql.Timestamp dateTime646 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime646.setNanos(333333000); + final java.sql.Timestamp dateTime649 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime649.setNanos(333333333); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO detached_rs_test_dates VALUES ( 4, ?, ?, ?, ?, ?, ?, ?)")) { + stmt.setDate(1, date); + stmt.setDate(2, date32); + stmt.setTimestamp(3, dateTime); + stmt.setTimestamp(4, dateTime32); + stmt.setTimestamp(5, dateTime643); + stmt.setTimestamp(6, dateTime646); + stmt.setTimestamp(7, dateTime649); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); + assertEquals(rs.getTimestamp("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); + assertEquals(rs.getTimestamp("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date").toString(), date.toString()); + assertEquals(rs.getDate("date32").toString(), date32.toString()); + assertEquals(rs.getTimestamp("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertEquals(rs.getTimestamp("dateTime", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime.toString()); + assertEquals(rs.getTimestamp("dateTime32", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime32.toString()); + assertEquals(rs.getTimestamp("dateTime643", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime643.toString()); + assertEquals(rs.getTimestamp("dateTime646", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime646.toString()); + assertEquals(rs.getTimestamp("dateTime649", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime649.toString()); + + assertFalse(rs.next()); + } + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); + assertEquals(rs.getObject("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getObject("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getObject("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date").toString(), date.toString()); + assertEquals(rs.getObject("date32").toString(), date32.toString()); + + assertEquals(rs.getObject("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertFalse(rs.next()); + } + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) + { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "1970-01-01"); + assertEquals(rs.getString("date32"), "1970-01-01"); + assertEquals(rs.getString("dateTime"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime32"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime643"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime646"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime649"), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "2149-06-06"); + assertEquals(rs.getString("date32"), "2299-12-31"); + assertEquals(rs.getString("dateTime"), "2106-02-07 06:28:15.0"); + assertEquals(rs.getString("dateTime32"), "2106-02-07 06:28:15.0"); + assertEquals(rs.getString("dateTime643"), "2261-12-31 23:59:59.999"); + assertEquals(rs.getString("dateTime646"), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getString("dateTime649"), "2261-12-31 23:59:59.999999999"); + + ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); + assertTrue(rs.next()); + assertEquals( + rs.getString("date"), + Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); + assertEquals( + rs.getString("date32"), + Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); + assertFalse(rs.next()); + } + } + + private int insertData(String sql) throws SQLException { + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + return stmt.executeUpdate(sql); + } + } + } }