Skip to content

Commit 1b0d729

Browse files
committed
improved resultset target type lookup
1 parent 4833321 commit 1b0d729

2 files changed

Lines changed: 153 additions & 22 deletions

File tree

jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import com.clickhouse.client.api.metadata.TableSchema;
66
import com.clickhouse.client.api.query.QueryResponse;
77
import com.clickhouse.data.ClickHouseColumn;
8+
import com.clickhouse.data.ClickHouseDataType;
89
import com.clickhouse.jdbc.internal.ExceptionUtils;
910
import com.clickhouse.jdbc.internal.FeatureManager;
1011
import com.clickhouse.jdbc.internal.JdbcUtils;
1112
import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl;
13+
import com.google.common.collect.ImmutableMap;
1214
import org.slf4j.Logger;
1315
import org.slf4j.LoggerFactory;
1416

@@ -34,13 +36,8 @@
3436
import java.sql.Statement;
3537
import java.sql.Time;
3638
import java.sql.Timestamp;
37-
import java.time.Instant;
3839
import java.time.LocalDate;
3940
import java.time.LocalDateTime;
40-
import java.time.LocalDate;
41-
import java.time.LocalDateTime;
42-
import java.time.LocalTime;
43-
import java.time.ZoneOffset;
4441
import java.time.ZonedDateTime;
4542
import java.util.Calendar;
4643
import java.util.Collections;
@@ -72,19 +69,30 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper {
7269
private final int maxRows;
7370

7471
private Consumer<Exception> onDataTransferException;
72+
private final Map<String, ColumnTypeBinding> columnTypeBindings;
7573

7674
public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, ClickHouseBinaryFormatReader reader,
7775
Consumer<Exception> onDataTransferException) throws SQLException {
76+
this(parentStatement, response, reader, onDataTransferException, JdbcUtils.DATA_TYPE_CLASS_MAP);
77+
}
78+
79+
public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, ClickHouseBinaryFormatReader reader,
80+
Consumer<Exception> onDataTransferException,
81+
Map<ClickHouseDataType, Class<?>> defaultTypeMap) throws SQLException {
7882
this.parentStatement = parentStatement;
7983
this.response = response;
8084
this.reader = reader;
8185
this.featureManager = new FeatureManager(parentStatement.getConnection().getJdbcConfig());
8286
TableSchema tableMetadata = reader.getSchema();
8387

88+
final Map<ClickHouseDataType, Class<?>> resolvedDefaultTypeMap =
89+
defaultTypeMap != null ? defaultTypeMap : JdbcUtils.DATA_TYPE_CLASS_MAP;
90+
this.columnTypeBindings = buildColumnTypeBindings(tableMetadata, resolvedDefaultTypeMap);
91+
8492
// Result set contains columns from one database (there is a special table engine 'Merge' to do cross DB queries)
8593
this.metaData = new ResultSetMetaDataImpl(tableMetadata
8694
.getColumns(), response.getSettings().getDatabase(), "", tableMetadata.getTableName(),
87-
JdbcUtils.DATA_TYPE_CLASS_MAP);
95+
resolvedDefaultTypeMap);
8896
this.closed = false;
8997
this.wasNull = false;
9098
this.defaultCalendar = parentStatement.getConnection().defaultCalendar;
@@ -96,6 +104,41 @@ public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, Clic
96104
this.onDataTransferException = onDataTransferException;
97105
}
98106

107+
private static Map<String, ColumnTypeBinding> buildColumnTypeBindings(TableSchema schema,
108+
Map<ClickHouseDataType, Class<?>> typeMap) {
109+
ImmutableMap.Builder<String, ColumnTypeBinding> bindings = ImmutableMap.builder();
110+
111+
for (ClickHouseColumn column : schema.getColumns()) {
112+
ClickHouseDataType dataType = column.getDataType();
113+
bindings.put(column.getColumnName(), new ColumnTypeBinding(typeMap.get(dataType),
114+
JdbcUtils.convertToSqlType(dataType)));
115+
}
116+
return bindings.buildKeepingLast();
117+
}
118+
119+
/**
120+
* Immutable pair of pre-resolved values for a single column: the Java class to materialize when
121+
* no typeMap is supplied, and the JDBC {@link SQLType} that corresponds to the column's ClickHouse
122+
* data type (used as a secondary key when looking up a user-provided typeMap).
123+
*/
124+
private static final class ColumnTypeBinding {
125+
private final Class<?> aClass;
126+
private final SQLType jdbcType;
127+
128+
ColumnTypeBinding(Class<?> aClass, SQLType jdbcType) {
129+
this.aClass = aClass;
130+
this.jdbcType = jdbcType;
131+
}
132+
133+
public Class<?> getAClass() {
134+
return aClass;
135+
}
136+
137+
public SQLType getJdbcType() {
138+
return jdbcType;
139+
}
140+
}
141+
99142
private void checkClosed() throws SQLException {
100143
if (closed) {
101144
throw new SQLException("ResultSet is closed.", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION);
@@ -1497,22 +1540,7 @@ public <T> T getObjectImpl(String columnLabel, Class<?> type, Map<String, Class<
14971540
wasNull = false;
14981541

14991542
if (type == null) {
1500-
switch (column.getDataType()) {
1501-
case Point:
1502-
case Ring:
1503-
case LineString:
1504-
case Polygon:
1505-
case MultiPolygon:
1506-
case MultiLineString:
1507-
case Geometry:
1508-
break; // read as is
1509-
default:
1510-
if (typeMap == null || typeMap.isEmpty()) {
1511-
type = JdbcUtils.convertToJavaClass(column.getDataType());
1512-
} else {
1513-
type = typeMap.get(JdbcUtils.convertToSqlType(column.getDataType()).getName());
1514-
}
1515-
}
1543+
type = resolveTargetType(columnLabel, column, typeMap);
15161544
} else {
15171545
/// shortcut
15181546
if (type == Timestamp.class) {
@@ -1539,6 +1567,32 @@ public <T> T getObjectImpl(String columnLabel, Class<?> type, Map<String, Class<
15391567
}
15401568
}
15411569

1570+
private Class<?> resolveTargetType(String columnLabel, ClickHouseColumn column, Map<String, Class<?>> typeMap) {
1571+
switch (column.getDataType()) {
1572+
case Point:
1573+
case Ring:
1574+
case LineString:
1575+
case Polygon:
1576+
case MultiPolygon:
1577+
case MultiLineString:
1578+
case Geometry:
1579+
return null; // read as is
1580+
default:
1581+
break;
1582+
}
1583+
1584+
ColumnTypeBinding binding = columnTypeBindings.get(columnLabel);
1585+
if (typeMap == null || typeMap.isEmpty()) {
1586+
return binding.getAClass();
1587+
}
1588+
1589+
Class<?> resolved = typeMap.get(column.getDataType().name());
1590+
if (resolved == null) {
1591+
resolved = typeMap.get(binding.getJdbcType().getName());
1592+
}
1593+
return resolved;
1594+
}
1595+
15421596
@Override
15431597
public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException {
15441598
updateObject(columnIndexToName(columnIndex), x, targetSqlType, scaleOrLength);

jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import java.sql.Time;
2626
import java.sql.Timestamp;
2727
import java.sql.Types;
28+
import java.time.LocalDate;
29+
import java.time.LocalDateTime;
30+
import java.util.HashMap;
31+
import java.util.Map;
2832
import java.util.Properties;
2933

3034
import static org.testng.Assert.assertEquals;
@@ -481,4 +485,77 @@ public void testGetResultSetFromArrayTimestamp() throws Exception {
481485
}
482486
}
483487
}
488+
489+
@Test(groups = {"integration"})
490+
public void testGetObjectWithSqlTypeNameMap() throws SQLException {
491+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement();
492+
ResultSet rs = stmt.executeQuery("SELECT 1::Int32 AS i, toDateTime('2024-12-01 12:34:56') AS ts, " +
493+
"toDate('2024-12-01') AS d")) {
494+
assertTrue(rs.next());
495+
496+
Map<String, Class<?>> sqlTypeMap = new HashMap<>();
497+
sqlTypeMap.put(JDBCType.INTEGER.getName(), Long.class);
498+
sqlTypeMap.put(JDBCType.TIMESTAMP.getName(), LocalDateTime.class);
499+
sqlTypeMap.put(JDBCType.DATE.getName(), LocalDate.class);
500+
501+
assertEquals(rs.getObject("i", sqlTypeMap), 1L);
502+
assertEquals(rs.getObject("ts", sqlTypeMap), LocalDateTime.of(2024, 12, 1, 12, 34, 56));
503+
assertEquals(rs.getObject("d", sqlTypeMap), LocalDate.of(2024, 12, 1));
504+
}
505+
}
506+
507+
@Test(groups = {"integration"})
508+
public void testGetObjectWithClickHouseTypeNameMap() throws SQLException {
509+
// typeMap keyed by ClickHouseDataType name (e.g. "Int32"): the V1-style direct lookup
510+
// that ResultSetImpl#getObjectImpl now supports in addition to JDBC SQLType names.
511+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement();
512+
ResultSet rs = stmt.executeQuery("SELECT 1::Int32 AS i, 1::UInt64 AS u, " +
513+
"toDateTime('2024-12-01 12:34:56') AS ts")) {
514+
assertTrue(rs.next());
515+
516+
Map<String, Class<?>> chTypeMap = new HashMap<>();
517+
chTypeMap.put("Int32", Long.class);
518+
chTypeMap.put("UInt64", String.class);
519+
chTypeMap.put("DateTime", LocalDateTime.class);
520+
521+
assertEquals(rs.getObject("i", chTypeMap), 1L);
522+
assertEquals(rs.getObject("u", chTypeMap), "1");
523+
assertEquals(rs.getObject("ts", chTypeMap), LocalDateTime.of(2024, 12, 1, 12, 34, 56));
524+
}
525+
}
526+
527+
@Test(groups = {"integration"})
528+
public void testGetObjectWithMixedTypeNameMap() throws SQLException {
529+
// Single typeMap mixing ClickHouseDataType names and SQLType names: CH-name lookup is tried
530+
// first, then SQLType-name lookup, so the user can address columns by either convention.
531+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement();
532+
ResultSet rs = stmt.executeQuery("SELECT 1::Int32 AS i, 'abc'::String AS s")) {
533+
assertTrue(rs.next());
534+
535+
Map<String, Class<?>> mixed = new HashMap<>();
536+
mixed.put("Int32", Long.class);
537+
mixed.put(JDBCType.VARCHAR.getName(), String.class);
538+
539+
assertEquals(rs.getObject("i", mixed), 1L);
540+
assertEquals(rs.getObject("s", mixed), "abc");
541+
}
542+
}
543+
544+
@Test(groups = {"integration"})
545+
public void testGetObjectWithTypeMapMissingEntry() throws SQLException {
546+
// typeMap that does not contain an entry for the column type: ResultSetImpl#getObjectImpl
547+
// falls back to "read as is" (no conversion) as documented by JDBC.
548+
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement();
549+
ResultSet rs = stmt.executeQuery("SELECT 1::Int32 AS i, 'abc'::String AS s")) {
550+
assertTrue(rs.next());
551+
552+
Map<String, Class<?>> partial = new HashMap<>();
553+
partial.put("Int32", Long.class);
554+
555+
assertEquals(rs.getObject("i", partial), 1L);
556+
// String column has no entry: value comes back as the reader's native representation
557+
// and is not coerced into the default Java class.
558+
Assert.assertNotNull(rs.getObject("s", partial));
559+
}
560+
}
484561
}

0 commit comments

Comments
 (0)