Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public final class ClientUtils {
private ClientUtils() {}

public static boolean isNotBlank(String str) {
return str != null && !str.trim().isEmpty();
return !isBlank(str);
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
}
12 changes: 8 additions & 4 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.clickhouse.jdbc;

import com.clickhouse.client.api.Client;
Expand All @@ -13,6 +13,7 @@
import com.clickhouse.jdbc.internal.ParsedPreparedStatement;
import com.clickhouse.jdbc.internal.SqlParserFacade;
import com.clickhouse.jdbc.metadata.DatabaseMetaDataImpl;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -36,7 +37,6 @@
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
Expand Down Expand Up @@ -68,6 +68,7 @@
private Executor networkTimeoutExecutor;

private final FeatureManager featureManager;
private volatile ImmutableMap<String, Class<?>> typeMap;

public ConnectionImpl(String url, Properties info) throws SQLException {
try {
Expand Down Expand Up @@ -119,6 +120,7 @@
this.sqlParser = SqlParserFacade.getParser(config.getDriverProperty(DriverProperties.SQL_PARSER.getKey(),
DriverProperties.SQL_PARSER.getDefaultValue()), config);
this.featureManager = new FeatureManager(this.config);
this.typeMap = ImmutableMap.<String, Class<?>>builder().putAll(this.config.getTypeMap()).buildKeepingLast();
} catch (SQLException e) {
throw e;
} catch (Exception e) {
Expand Down Expand Up @@ -297,14 +299,16 @@
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
ensureOpen();
featureManager.unsupportedFeatureThrow("getTypeMap()");
return Collections.emptyMap();
return this.typeMap;
}

@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
ensureOpen();
featureManager.unsupportedFeatureThrow("setTypeMap(Map<String, Class<?>>)");
if (map == null) {
throw new SQLException("Type map cannot be null", ExceptionUtils.SQL_STATE_CLIENT_ERROR);
}
this.typeMap = ImmutableMap.<String, Class<?>>builder().putAll(map).buildKeepingLast();
}

@Override
Expand Down
16 changes: 16 additions & 0 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/DriverProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ public enum DriverProperties {
*/
CLUSTER_NAME("jdbc_cluster_name", null),

/**
* Define custom type mappings for JDBC ResultSet#getObject() method.
* Format of the property is 'key=value'.
* Key is the ClickHouse type name.
* Value is the Java class name.
* Example: 'UInt64=java.lang.String'
*/
JDBC_TYPE_MAPPINGS("jdbc_type_mappings", null),

/**
* Deprecated and will be removed.
* This property is here to keep backward compatibility with `typeMappings` property.
* Use `jdbc_type_mappings` instead
*/
@Deprecated
TYPE_MAPPINGS("typeMappings", null),
;


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.clickhouse.jdbc;

import com.clickhouse.client.api.DataTypeUtils;
Expand Down Expand Up @@ -415,7 +415,7 @@
TableSchema tSchema = connection.getClient().getTableSchemaFromQuery(sql);
resultSetMetaData = new ResultSetMetaDataImpl(tSchema.getColumns(),
connection.getSchema(), connection.getCatalog(),
tSchema.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP);
tSchema.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP, connection.getTypeMap());
} catch (Exception e) {
LOG.warn("Failed to get schema for statement '{}'", originalSql);
}
Expand All @@ -427,7 +427,7 @@
.collect(Collectors.toList());
resultSetMetaData = new ResultSetMetaDataImpl(columns,
connection.getSchema(), connection.getCatalog(),
"", JdbcUtils.DATA_TYPE_CLASS_MAP);
"", JdbcUtils.DATA_TYPE_CLASS_MAP, connection.getTypeMap());
Comment thread
chernser marked this conversation as resolved.
}
} else if (currentResultSet != null) {
resultSetMetaData = currentResultSet.getMetaData();
Expand Down
76 changes: 9 additions & 67 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.clickhouse.jdbc;

import com.clickhouse.client.api.DataTypeUtils;
Expand All @@ -10,7 +10,6 @@
import com.clickhouse.jdbc.internal.FeatureManager;
import com.clickhouse.jdbc.internal.JdbcUtils;
import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -69,7 +68,8 @@
private final int maxRows;

private Consumer<Exception> onDataTransferException;
private final Map<String, ColumnTypeBinding> columnTypeBindings;

private final Map<String, Class<?>> connTypeMap;

public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, ClickHouseBinaryFormatReader reader,
Consumer<Exception> onDataTransferException) throws SQLException {
Expand All @@ -84,15 +84,18 @@
this.reader = reader;
this.featureManager = new FeatureManager(parentStatement.getConnection().getJdbcConfig());
TableSchema tableMetadata = reader.getSchema();
this.connTypeMap = parentStatement.getConnection().getTypeMap();

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

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

private static Map<String, ColumnTypeBinding> buildColumnTypeBindings(TableSchema schema,
Map<ClickHouseDataType, Class<?>> typeMap) {
ImmutableMap.Builder<String, ColumnTypeBinding> bindings = ImmutableMap.builder();

for (ClickHouseColumn column : schema.getColumns()) {
ClickHouseDataType dataType = column.getDataType();
bindings.put(column.getColumnName(), new ColumnTypeBinding(typeMap.get(dataType),
JdbcUtils.convertToSqlType(dataType)));
}
return bindings.buildKeepingLast();
}

/**
* Immutable pair of pre-resolved values for a single column: the Java class to materialize when
* no typeMap is supplied, and the JDBC {@link SQLType} that corresponds to the column's ClickHouse
* data type (used as a secondary key when looking up a user-provided typeMap).
*/
private static final class ColumnTypeBinding {
private final Class<?> aClass;
private final SQLType jdbcType;

ColumnTypeBinding(Class<?> aClass, SQLType jdbcType) {
this.aClass = aClass;
this.jdbcType = jdbcType;
}

public Class<?> getAClass() {
return aClass;
}

public SQLType getJdbcType() {
return jdbcType;
}
}

private void checkClosed() throws SQLException {
if (closed) {
throw new SQLException("ResultSet is closed.", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION);
Expand Down Expand Up @@ -1502,7 +1470,7 @@

@Override
public Object getObject(String columnLabel) throws SQLException {
return getObjectImpl(columnLabel, null, Collections.emptyMap());
return getObjectImpl(columnLabel, null, connTypeMap);
}

@Override
Expand Down Expand Up @@ -1540,7 +1508,7 @@
wasNull = false;

if (type == null) {
type = resolveTargetType(columnLabel, column, typeMap);
type = metaData.resolveColumnClass(columnLabel, typeMap);
} else {
/// shortcut
if (type == Timestamp.class) {
Expand All @@ -1567,32 +1535,6 @@
}
}

private Class<?> resolveTargetType(String columnLabel, ClickHouseColumn column, Map<String, Class<?>> typeMap) {
switch (column.getDataType()) {
case Point:
case Ring:
case LineString:
case Polygon:
case MultiPolygon:
case MultiLineString:
case Geometry:
return null; // read as is
default:
break;
}

ColumnTypeBinding binding = columnTypeBindings.get(columnLabel);
if (typeMap == null || typeMap.isEmpty()) {
return binding.getAClass();
}

Class<?> resolved = typeMap.get(column.getDataType().name());
if (resolved == null) {
resolved = typeMap.get(binding.getJdbcType().getName());
}
return resolved;
}

@Override
public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException {
updateObject(columnIndexToName(columnIndex), x, targetSqlType, scaleOrLength);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.nio.charset.StandardCharsets;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
Expand All @@ -40,6 +42,31 @@
"[A-Za-z0-9!#$%&'*+\\.\\^_`\\|~-]+");

private final boolean disableFrameworkDetection;
private final Map<String, Class<?>> typeMap;
private static final Map<String, Class<?>> COMMON_CLASSES;

static {
ImmutableMap.Builder<String, Class<?>> mapBuilder = ImmutableMap.builder();

Arrays.stream(new Class<?>[] {
String.class, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Boolean.class, Character.class, Object.class,
java.math.BigDecimal.class, java.math.BigInteger.class,
java.util.UUID.class, java.util.Date.class, java.util.Map.class, java.util.List.class,
java.time.LocalDate.class, java.time.LocalDateTime.class, java.time.LocalTime.class,
java.time.OffsetDateTime.class, java.time.ZonedDateTime.class,
com.clickhouse.data.ClickHouseDataType.class
}).forEach(c -> mapBuilder.put(c.getName(), c));


Arrays.stream(new Class<?>[] {
String.class, Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, Boolean.class,
java.math.BigDecimal.class, java.math.BigInteger.class,
}).forEach(c -> mapBuilder.put(c.getSimpleName(), c));

COMMON_CLASSES = mapBuilder.buildKeepingLast();
}

final Map<String, String> clientProperties;
public Map<String, String> getClientProperties() {
Expand All @@ -60,6 +87,10 @@
return isIgnoreUnsupportedRequests;
}

public Map<String, Class<?>> getTypeMap() {
return typeMap;
}

private static final Set<String> DRIVER_PROP_KEYS;
static {
ImmutableSet.Builder<String> driverPropertiesMapBuilder = ImmutableSet.builder();
Expand Down Expand Up @@ -102,6 +133,36 @@

this.connectionUrl = createConnectionURL(tmpConnectionUrl, useSSL);
this.isIgnoreUnsupportedRequests = Boolean.parseBoolean(getDriverProperty(DriverProperties.IGNORE_UNSUPPORTED_VALUES.getKey(), "false"));

this.typeMap = loadTypeMap();
}

private Map<String, Class<?>> loadTypeMap() throws SQLException {
String typeMappings = driverProperties.get(DriverProperties.JDBC_TYPE_MAPPINGS.getKey());
String legacyTypeMappings = driverProperties.get(DriverProperties.TYPE_MAPPINGS.getKey());

Check warning on line 142 in jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "TYPE_MAPPINGS"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ6bn9TRsXRxejqE5mxU&open=AZ6bn9TRsXRxejqE5mxU&pullRequest=2866
if (typeMappings != null && legacyTypeMappings != null) {
throw new SQLException("Only one of " + DriverProperties.JDBC_TYPE_MAPPINGS.getKey() + " or " + DriverProperties.TYPE_MAPPINGS.getKey() + " can be specified.");

Check warning on line 144 in jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "TYPE_MAPPINGS"; it is deprecated.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ6bn9TRsXRxejqE5mxV&open=AZ6bn9TRsXRxejqE5mxV&pullRequest=2866
}
String mappingsStr = typeMappings != null ? typeMappings : legacyTypeMappings;
return (mappingsStr == null || mappingsStr.trim().isEmpty()) ? Collections.emptyMap() : parseTypeMappings(mappingsStr);
}

private Map<String, Class<?>> parseTypeMappings(String mappingsStr) throws SQLException {
Map<String, Class<?>> map = new HashMap<>();
Map<String, String> parsed = ClientConfigProperties.toKeyValuePairs(mappingsStr);
for (Map.Entry<String, String> entry : parsed.entrySet()) {
String className = entry.getValue();
Class<?> clazz = COMMON_CLASSES.get(className);
if (clazz == null) {
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new SQLException("Class not found for type mapping: " + className, e);
}
}
map.put(entry.getKey(), clazz);
}
return ImmutableMap.<String, Class<?>>builder().putAll(map).buildKeepingLast();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.clickhouse.jdbc.internal;

import com.clickhouse.client.api.DataTypeUtils;
Expand Down Expand Up @@ -163,6 +163,21 @@
return ImmutableMap.copyOf(map);
}

// Reverse of SQL_TYPE_TO_CLASS_MAP. Used to resolve a JDBC type when a column's Java class has been
// overridden via a user supplied type map. Several SQL types share the same Java class, so a preferred
// SQL type is registered first for the ambiguous classes.
public static final Map<Class<?>, SQLType> CLASS_TO_SQL_TYPE_MAP = generateClassToSqlTypeMap();

Check warning on line 169 in jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this member "protected".

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ6bn9UGsXRxejqE5mxX&open=AZ6bn9UGsXRxejqE5mxX&pullRequest=2866
private static Map<Class<?>, SQLType> generateClassToSqlTypeMap() {
Map<Class<?>, SQLType> map = new HashMap<>();
map.put(String.class, JDBCType.VARCHAR);
map.put(Float.class, JDBCType.FLOAT); // prefer FLOAT over REAL
map.put(byte[].class, JDBCType.VARBINARY);
for (Map.Entry<SQLType, Class<?>> entry : SQL_TYPE_TO_CLASS_MAP.entrySet()) {
map.putIfAbsent(entry.getValue(), entry.getKey());
}
return ImmutableMap.copyOf(map);
}

public static final Set<ClickHouseDataType> INVALID_TARGET_TYPES = EnumSet.of(ClickHouseDataType.Nested, ClickHouseDataType.Enum8, ClickHouseDataType.Enum16, ClickHouseDataType.Enum,
ClickHouseDataType.Tuple, ClickHouseDataType.Map, ClickHouseDataType.Nothing, ClickHouseDataType.Nullable, ClickHouseDataType.Variant);

Expand Down
Loading
Loading