Skip to content

Commit 4574dec

Browse files
authored
Merge pull request #2866 from ClickHouse/06/02/26/custom_type_mapping
custom type mapping implementation
2 parents 7c4448c + 0fdaafa commit 4574dec

14 files changed

Lines changed: 489 additions & 96 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/internal/ClientUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ public final class ClientUtils {
88
private ClientUtils() {}
99

1010
public static boolean isNotBlank(String str) {
11-
return str != null && !str.trim().isEmpty();
11+
return !isBlank(str);
12+
}
13+
14+
public static boolean isBlank(String str) {
15+
return str == null || str.trim().isEmpty();
1216
}
1317
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.clickhouse.jdbc.internal.ParsedPreparedStatement;
1414
import com.clickhouse.jdbc.internal.SqlParserFacade;
1515
import com.clickhouse.jdbc.metadata.DatabaseMetaDataImpl;
16+
import com.google.common.collect.ImmutableMap;
1617
import org.slf4j.Logger;
1718
import org.slf4j.LoggerFactory;
1819

@@ -36,7 +37,6 @@
3637
import java.time.Duration;
3738
import java.time.temporal.ChronoUnit;
3839
import java.util.Calendar;
39-
import java.util.Collections;
4040
import java.util.HashSet;
4141
import java.util.Map;
4242
import java.util.Properties;
@@ -68,6 +68,7 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper {
6868
private Executor networkTimeoutExecutor;
6969

7070
private final FeatureManager featureManager;
71+
private volatile ImmutableMap<String, Class<?>> typeMap;
7172

7273
public ConnectionImpl(String url, Properties info) throws SQLException {
7374
try {
@@ -119,6 +120,7 @@ public ConnectionImpl(String url, Properties info) throws SQLException {
119120
this.sqlParser = SqlParserFacade.getParser(config.getDriverProperty(DriverProperties.SQL_PARSER.getKey(),
120121
DriverProperties.SQL_PARSER.getDefaultValue()), config);
121122
this.featureManager = new FeatureManager(this.config);
123+
this.typeMap = ImmutableMap.<String, Class<?>>builder().putAll(this.config.getTypeMap()).buildKeepingLast();
122124
} catch (SQLException e) {
123125
throw e;
124126
} catch (Exception e) {
@@ -297,14 +299,16 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe
297299
@Override
298300
public Map<String, Class<?>> getTypeMap() throws SQLException {
299301
ensureOpen();
300-
featureManager.unsupportedFeatureThrow("getTypeMap()");
301-
return Collections.emptyMap();
302+
return this.typeMap;
302303
}
303304

304305
@Override
305306
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
306307
ensureOpen();
307-
featureManager.unsupportedFeatureThrow("setTypeMap(Map<String, Class<?>>)");
308+
if (map == null) {
309+
throw new SQLException("Type map cannot be null", ExceptionUtils.SQL_STATE_CLIENT_ERROR);
310+
}
311+
this.typeMap = ImmutableMap.<String, Class<?>>builder().putAll(map).buildKeepingLast();
308312
}
309313

310314
@Override

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ public enum DriverProperties {
129129
*/
130130
CLUSTER_NAME("jdbc_cluster_name", null),
131131

132+
/**
133+
* Define custom type mappings for JDBC ResultSet#getObject() method.
134+
* Format of the property is 'key=value'.
135+
* Key is the ClickHouse type name.
136+
* Value is the Java class name.
137+
* Example: 'UInt64=java.lang.String'
138+
*/
139+
JDBC_TYPE_MAPPINGS("jdbc_type_mappings", null),
140+
141+
/**
142+
* Deprecated and will be removed.
143+
* This property is here to keep backward compatibility with `typeMappings` property.
144+
* Use `jdbc_type_mappings` instead
145+
*/
146+
@Deprecated
147+
TYPE_MAPPINGS("typeMappings", null),
132148
;
133149

134150

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ public ResultSetMetaData getMetaData() throws SQLException {
415415
TableSchema tSchema = connection.getClient().getTableSchemaFromQuery(sql);
416416
resultSetMetaData = new ResultSetMetaDataImpl(tSchema.getColumns(),
417417
connection.getSchema(), connection.getCatalog(),
418-
tSchema.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP);
418+
tSchema.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP, connection.getTypeMap());
419419
} catch (Exception e) {
420420
LOG.warn("Failed to get schema for statement '{}'", originalSql);
421421
}
@@ -427,7 +427,7 @@ public ResultSetMetaData getMetaData() throws SQLException {
427427
.collect(Collectors.toList());
428428
resultSetMetaData = new ResultSetMetaDataImpl(columns,
429429
connection.getSchema(), connection.getCatalog(),
430-
"", JdbcUtils.DATA_TYPE_CLASS_MAP);
430+
"", JdbcUtils.DATA_TYPE_CLASS_MAP, connection.getTypeMap());
431431
}
432432
} else if (currentResultSet != null) {
433433
resultSetMetaData = currentResultSet.getMetaData();

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

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import com.clickhouse.jdbc.internal.FeatureManager;
1111
import com.clickhouse.jdbc.internal.JdbcUtils;
1212
import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl;
13-
import com.google.common.collect.ImmutableMap;
1413
import org.slf4j.Logger;
1514
import org.slf4j.LoggerFactory;
1615

@@ -69,7 +68,8 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper {
6968
private final int maxRows;
7069

7170
private Consumer<Exception> onDataTransferException;
72-
private final Map<String, ColumnTypeBinding> columnTypeBindings;
71+
72+
private final Map<String, Class<?>> connTypeMap;
7373

7474
public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, ClickHouseBinaryFormatReader reader,
7575
Consumer<Exception> onDataTransferException) throws SQLException {
@@ -84,15 +84,18 @@ public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, Clic
8484
this.reader = reader;
8585
this.featureManager = new FeatureManager(parentStatement.getConnection().getJdbcConfig());
8686
TableSchema tableMetadata = reader.getSchema();
87+
this.connTypeMap = parentStatement.getConnection().getTypeMap();
8788

8889
final Map<ClickHouseDataType, Class<?>> resolvedDefaultTypeMap =
8990
defaultTypeMap != null ? defaultTypeMap : JdbcUtils.DATA_TYPE_CLASS_MAP;
90-
this.columnTypeBindings = buildColumnTypeBindings(tableMetadata, resolvedDefaultTypeMap);
9191

9292
// Result set contains columns from one database (there is a special table engine 'Merge' to do cross DB queries)
93+
// The metadata owns all column type bindings; this result set reuses them via resolveColumnClass(...).
94+
// Use the single connection type map snapshot taken above so metadata binding and getObject(...) stay
95+
// consistent even if the connection type map is replaced concurrently during iteration.
9396
this.metaData = new ResultSetMetaDataImpl(tableMetadata
9497
.getColumns(), response.getSettings().getDatabase(), "", tableMetadata.getTableName(),
95-
resolvedDefaultTypeMap);
98+
resolvedDefaultTypeMap, this.connTypeMap);
9699
this.closed = false;
97100
this.wasNull = false;
98101
this.defaultCalendar = parentStatement.getConnection().defaultCalendar;
@@ -104,41 +107,6 @@ public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, Clic
104107
this.onDataTransferException = onDataTransferException;
105108
}
106109

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-
142110
private void checkClosed() throws SQLException {
143111
if (closed) {
144112
throw new SQLException("ResultSet is closed.", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION);
@@ -1502,7 +1470,7 @@ public Object getObject(int columnIndex) throws SQLException {
15021470

15031471
@Override
15041472
public Object getObject(String columnLabel) throws SQLException {
1505-
return getObjectImpl(columnLabel, null, Collections.emptyMap());
1473+
return getObjectImpl(columnLabel, null, connTypeMap);
15061474
}
15071475

15081476
@Override
@@ -1540,7 +1508,7 @@ public <T> T getObjectImpl(String columnLabel, Class<?> type, Map<String, Class<
15401508
wasNull = false;
15411509

15421510
if (type == null) {
1543-
type = resolveTargetType(columnLabel, column, typeMap);
1511+
type = metaData.resolveColumnClass(columnLabel, typeMap);
15441512
} else {
15451513
/// shortcut
15461514
if (type == Timestamp.class) {
@@ -1567,32 +1535,6 @@ public <T> T getObjectImpl(String columnLabel, Class<?> type, Map<String, Class<
15671535
}
15681536
}
15691537

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-
15961538
@Override
15971539
public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException {
15981540
updateObject(columnIndexToName(columnIndex), x, targetSqlType, scaleOrLength);

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.nio.charset.StandardCharsets;
1919
import java.sql.DriverPropertyInfo;
2020
import java.sql.SQLException;
21+
import java.util.Arrays;
22+
import java.util.Collections;
2123
import java.util.Comparator;
2224
import java.util.HashMap;
2325
import java.util.List;
@@ -40,6 +42,31 @@ public class JdbcConfiguration {
4042
"[A-Za-z0-9!#$%&'*+\\.\\^_`\\|~-]+");
4143

4244
private final boolean disableFrameworkDetection;
45+
private final Map<String, Class<?>> typeMap;
46+
private static final Map<String, Class<?>> COMMON_CLASSES;
47+
48+
static {
49+
ImmutableMap.Builder<String, Class<?>> mapBuilder = ImmutableMap.builder();
50+
51+
Arrays.stream(new Class<?>[] {
52+
String.class, Byte.class, Short.class, Integer.class, Long.class,
53+
Float.class, Double.class, Boolean.class, Character.class, Object.class,
54+
java.math.BigDecimal.class, java.math.BigInteger.class,
55+
java.util.UUID.class, java.util.Date.class, java.util.Map.class, java.util.List.class,
56+
java.time.LocalDate.class, java.time.LocalDateTime.class, java.time.LocalTime.class,
57+
java.time.OffsetDateTime.class, java.time.ZonedDateTime.class,
58+
com.clickhouse.data.ClickHouseDataType.class
59+
}).forEach(c -> mapBuilder.put(c.getName(), c));
60+
61+
62+
Arrays.stream(new Class<?>[] {
63+
String.class, Byte.class, Short.class, Integer.class, Long.class,
64+
Float.class, Double.class, Boolean.class,
65+
java.math.BigDecimal.class, java.math.BigInteger.class,
66+
}).forEach(c -> mapBuilder.put(c.getSimpleName(), c));
67+
68+
COMMON_CLASSES = mapBuilder.buildKeepingLast();
69+
}
4370

4471
final Map<String, String> clientProperties;
4572
public Map<String, String> getClientProperties() {
@@ -60,6 +87,10 @@ public boolean isIgnoreUnsupportedRequests() {
6087
return isIgnoreUnsupportedRequests;
6188
}
6289

90+
public Map<String, Class<?>> getTypeMap() {
91+
return typeMap;
92+
}
93+
6394
private static final Set<String> DRIVER_PROP_KEYS;
6495
static {
6596
ImmutableSet.Builder<String> driverPropertiesMapBuilder = ImmutableSet.builder();
@@ -102,6 +133,36 @@ public JdbcConfiguration(String url, Properties info) throws SQLException {
102133

103134
this.connectionUrl = createConnectionURL(tmpConnectionUrl, useSSL);
104135
this.isIgnoreUnsupportedRequests = Boolean.parseBoolean(getDriverProperty(DriverProperties.IGNORE_UNSUPPORTED_VALUES.getKey(), "false"));
136+
137+
this.typeMap = loadTypeMap();
138+
}
139+
140+
private Map<String, Class<?>> loadTypeMap() throws SQLException {
141+
String typeMappings = driverProperties.get(DriverProperties.JDBC_TYPE_MAPPINGS.getKey());
142+
String legacyTypeMappings = driverProperties.get(DriverProperties.TYPE_MAPPINGS.getKey());
143+
if (typeMappings != null && legacyTypeMappings != null) {
144+
throw new SQLException("Only one of " + DriverProperties.JDBC_TYPE_MAPPINGS.getKey() + " or " + DriverProperties.TYPE_MAPPINGS.getKey() + " can be specified.");
145+
}
146+
String mappingsStr = typeMappings != null ? typeMappings : legacyTypeMappings;
147+
return (mappingsStr == null || mappingsStr.trim().isEmpty()) ? Collections.emptyMap() : parseTypeMappings(mappingsStr);
148+
}
149+
150+
private Map<String, Class<?>> parseTypeMappings(String mappingsStr) throws SQLException {
151+
Map<String, Class<?>> map = new HashMap<>();
152+
Map<String, String> parsed = ClientConfigProperties.toKeyValuePairs(mappingsStr);
153+
for (Map.Entry<String, String> entry : parsed.entrySet()) {
154+
String className = entry.getValue();
155+
Class<?> clazz = COMMON_CLASSES.get(className);
156+
if (clazz == null) {
157+
try {
158+
clazz = Class.forName(className);
159+
} catch (ClassNotFoundException e) {
160+
throw new SQLException("Class not found for type mapping: " + className, e);
161+
}
162+
}
163+
map.put(entry.getKey(), clazz);
164+
}
165+
return ImmutableMap.<String, Class<?>>builder().putAll(map).buildKeepingLast();
105166
}
106167

107168
/**

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ private static Map<SQLType, Class<?>> generateClassMap() {
163163
return ImmutableMap.copyOf(map);
164164
}
165165

166+
// Reverse of SQL_TYPE_TO_CLASS_MAP. Used to resolve a JDBC type when a column's Java class has been
167+
// overridden via a user supplied type map. Several SQL types share the same Java class, so a preferred
168+
// SQL type is registered first for the ambiguous classes.
169+
public static final Map<Class<?>, SQLType> CLASS_TO_SQL_TYPE_MAP = generateClassToSqlTypeMap();
170+
private static Map<Class<?>, SQLType> generateClassToSqlTypeMap() {
171+
Map<Class<?>, SQLType> map = new HashMap<>();
172+
map.put(String.class, JDBCType.VARCHAR);
173+
map.put(Float.class, JDBCType.FLOAT); // prefer FLOAT over REAL
174+
map.put(byte[].class, JDBCType.VARBINARY);
175+
for (Map.Entry<SQLType, Class<?>> entry : SQL_TYPE_TO_CLASS_MAP.entrySet()) {
176+
map.putIfAbsent(entry.getValue(), entry.getKey());
177+
}
178+
return ImmutableMap.copyOf(map);
179+
}
180+
166181
public static final Set<ClickHouseDataType> INVALID_TARGET_TYPES = EnumSet.of(ClickHouseDataType.Nested, ClickHouseDataType.Enum8, ClickHouseDataType.Enum16, ClickHouseDataType.Enum,
167182
ClickHouseDataType.Tuple, ClickHouseDataType.Map, ClickHouseDataType.Nothing, ClickHouseDataType.Nullable, ClickHouseDataType.Variant);
168183

0 commit comments

Comments
 (0)