Skip to content

[JDBC] Connection#createArray & Connection#createStruct #2523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,34 @@ public synchronized <T> List<T> asList() {
}
return (List<T>) list;
}

/**
* Returns internal array. This method is only useful to work with array of primitives (int[], boolean[]).
* Otherwise use {@link #getArrayOfObjects()}
*
* @return
*/
public Object getArray() {
return array;
}

/**
* Returns array of objects.
* If item type is primitive then all elements will be converted into objects.
*
* @return
*/
public Object[] getArrayOfObjects() {
if (itemType.isPrimitive()) {
Object[] result = new Object[length];
for (int i = 0; i < length; i++) {
result[i] = Array.get(array, i);
}
return result;
} else {
return (Object[]) array;
}
}
}

public static class EnumValue extends Number {
Expand Down
60 changes: 44 additions & 16 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.clickhouse.client.api.metadata.TableSchema;
import com.clickhouse.client.api.query.GenericRecord;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.jdbc.internal.ExceptionUtils;
import com.clickhouse.jdbc.internal.JdbcConfiguration;
Expand All @@ -22,6 +23,7 @@
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.JDBCType;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
Expand Down Expand Up @@ -59,6 +61,8 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper {
private String schema;
private String appName;
private QuerySettings defaultQuerySettings;
private boolean readOnly;
private int holdability;

private final DatabaseMetaDataImpl metadata;
protected final Calendar defaultCalendar;
Expand All @@ -73,6 +77,8 @@ public ConnectionImpl(String url, Properties info) throws SQLException {
this.onCluster = false;
this.cluster = null;
this.appName = "";
this.readOnly = false;
this.holdability = ResultSet.HOLD_CURSORS_OVER_COMMIT;
String clientName = "ClickHouse JDBC Driver V2/" + Driver.driverVersion;

Map<String, String> clientProperties = config.getClientProperties();
Expand Down Expand Up @@ -229,15 +235,16 @@ public DatabaseMetaData getMetaData() throws SQLException {
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
ensureOpen();
if (!config.isIgnoreUnsupportedRequests() && readOnly) {
throw new SQLFeatureNotSupportedException("read-only=true unsupported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED);
}
// This method is just a hint for the driver. Documentation doesn't tell to block update operations.
// Currently, we do not use this hint but some connection pools may use this property.
// So we just save and return
this.readOnly = readOnly;
}

@Override
public boolean isReadOnly() throws SQLException {
ensureOpen();
return false;
return readOnly;
}

@Override
Expand Down Expand Up @@ -279,13 +286,13 @@ public void clearWarnings() throws SQLException {
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
ensureOpen();
return createStatement(resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT);
return createStatement(resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT);
}

@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
ensureOpen();
return prepareStatement(sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT);
return prepareStatement(sql, resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT);
}

@Override
Expand Down Expand Up @@ -319,13 +326,19 @@ public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
@Override
public void setHoldability(int holdability) throws SQLException {
ensureOpen();
//TODO: Should this be supported?
if (holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT && holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT) {
throw new SQLException("Only ResultSet.HOLD_CURSORS_OVER_COMMIT and ResultSet.CLOSE_CURSORS_AT_COMMIT allowed for holdability");
}
// we do not support transactions and almost always use auto-commit.
// holdability regulates is result set is open or closed on commit.
// currently we ignore value and always set what we support.
this.holdability = ResultSet.HOLD_CURSORS_OVER_COMMIT;
}

@Override
public int getHoldability() throws SQLException {
ensureOpen();
return ResultSet.HOLD_CURSORS_OVER_COMMIT;//TODO: Check if this is correct
return holdability;
}

@Override
Expand Down Expand Up @@ -563,25 +576,40 @@ public Properties getClientInfo() throws SQLException {

@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
ensureOpen();
if (typeName == null) {
throw new SQLFeatureNotSupportedException("typeName cannot be null");
}

int parentPos = typeName.indexOf('(');
String clickhouseDataTypeName = (typeName.substring(0, parentPos == -1 ? typeName.length() : parentPos)).trim();
ClickHouseDataType dataType = ClickHouseDataType.valueOf(clickhouseDataTypeName);
if (dataType.equals(ClickHouseDataType.Array)) {
throw new SQLFeatureNotSupportedException("Array cannot be a base type. In case of nested array provide most deep element type name.");
}
try {
List<Object> list =
(elements == null || elements.length == 0) ? Collections.emptyList() : Arrays.stream(elements, 0, elements.length).collect(Collectors.toList());
return new com.clickhouse.jdbc.types.Array(list, typeName, JdbcUtils.convertToSqlType(ClickHouseDataType.valueOf(typeName)).getVendorTypeNumber());
return new com.clickhouse.jdbc.types.Array(clickhouseDataTypeName,
JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(dataType, JDBCType.OTHER).getVendorTypeNumber(), elements);
} catch (Exception e) {
throw new SQLException("Failed to create array", ExceptionUtils.SQL_STATE_CLIENT_ERROR, e);
}
}

@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
//TODO: Should this be supported?
if (!config.isIgnoreUnsupportedRequests()) {
throw new SQLFeatureNotSupportedException("createStruct not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED);
ensureOpen();
if (typeName == null) {
throw new SQLFeatureNotSupportedException("typeName cannot be null");
}
ClickHouseColumn column = ClickHouseColumn.of("v", typeName);
if (column.getDataType().equals(ClickHouseDataType.Tuple)) {
return new com.clickhouse.jdbc.types.Struct(column, attributes);
} else {
throw new SQLException("Only Tuple datatype is supported for Struct", ExceptionUtils.SQL_STATE_CLIENT_ERROR);
}

return null;
}


@Override
public void setSchema(String schema) throws SQLException {
ensureOpen();
Expand Down
136 changes: 62 additions & 74 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.sql.SQLType;
import java.sql.SQLXML;
import java.sql.Statement;
import java.sql.Struct;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
Expand Down Expand Up @@ -749,102 +750,91 @@ public final int executeUpdate(String sql, String[] columnNames) throws SQLExcep
"executeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!",
ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE);
}
private static String encodeObject(Object x) throws SQLException {
private String encodeObject(Object x) throws SQLException {
return encodeObject(x, null);
}

private static final char QUOTE = '\'';

private static String encodeObject(Object x, Long length) throws SQLException {
private String encodeObject(Object x, Long length) throws SQLException {
LOG.trace("Encoding object: {}", x);

try {
if (x == null) {
return "NULL";
} else if (x instanceof String) {
return "'" + SQLUtils.escapeSingleQuotes((String) x) + "'";
return QUOTE + SQLUtils.escapeSingleQuotes((String) x) + QUOTE;
} else if (x instanceof Boolean) {
return (Boolean) x ? "1" : "0";
} else if (x instanceof Date) {
return "'" + DataTypeUtils.DATE_FORMATTER.format(((Date) x).toLocalDate()) + "'";
return QUOTE + DataTypeUtils.DATE_FORMATTER.format(((Date) x).toLocalDate()) + QUOTE;
} else if (x instanceof LocalDate) {
return "'" + DataTypeUtils.DATE_FORMATTER.format((LocalDate) x) + "'";
return QUOTE + DataTypeUtils.DATE_FORMATTER.format((LocalDate) x) + QUOTE;
} else if (x instanceof Time) {
return "'" + TIME_FORMATTER.format(((Time) x).toLocalTime()) + "'";
return QUOTE + TIME_FORMATTER.format(((Time) x).toLocalTime()) + QUOTE;
} else if (x instanceof LocalTime) {
return "'" + TIME_FORMATTER.format((LocalTime) x) + "'";
return QUOTE + TIME_FORMATTER.format((LocalTime) x) + QUOTE;
} else if (x instanceof Timestamp) {
return "'" + DATETIME_FORMATTER.format(((Timestamp) x).toLocalDateTime()) + "'";
return QUOTE + DATETIME_FORMATTER.format(((Timestamp) x).toLocalDateTime()) + QUOTE;
} else if (x instanceof LocalDateTime) {
return "'" + DATETIME_FORMATTER.format((LocalDateTime) x) + "'";
return QUOTE + DATETIME_FORMATTER.format((LocalDateTime) x) + QUOTE;
} else if (x instanceof OffsetDateTime) {
return encodeObject(((OffsetDateTime) x).toInstant());
} else if (x instanceof ZonedDateTime) {
return encodeObject(((ZonedDateTime) x).toInstant());
} else if (x instanceof Instant) {
return "fromUnixTimestamp64Nano(" + (((Instant) x).getEpochSecond() * 1_000_000_000L + ((Instant) x).getNano()) + ")";
} else if (x instanceof InetAddress) {
return "'" + ((InetAddress) x).getHostAddress() + "'";
} else if (x instanceof Array) {
return QUOTE + ((InetAddress) x).getHostAddress() + QUOTE;
} else if (x instanceof java.sql.Array) {
StringBuilder listString = new StringBuilder();
listString.append("[");
int i = 0;
for (Object item : (Object[]) ((Array) x).getArray()) {
if (i > 0) {
listString.append(", ");
}
listString.append(encodeObject(item));
i++;
}
listString.append("]");
listString.append('[');
appendArrayElements((Object[]) ((Array) x).getArray(), listString);
listString.append(']');

return listString.toString();
} else if (x.getClass().isArray()) {
StringBuilder listString = new StringBuilder();
listString.append("[");


listString.append('[');
if (x.getClass().getComponentType().isPrimitive()) {
int len = java.lang.reflect.Array.getLength(x);
for (int i = 0; i < len; i++) {
if (i > 0) {
listString.append(", ");
}
listString.append(encodeObject(java.lang.reflect.Array.get(x, i)));
listString.append(encodeObject(java.lang.reflect.Array.get(x, i))).append(',');
}
} else {
int i = 0;
for (Object item : (Object[]) x) {
if (i > 0) {
listString.append(", ");
}
listString.append(encodeObject(item));
i++;
if (len > 0) {
listString.setLength(listString.length() - 1);
}
} else {
appendArrayElements((Object[]) x, listString);
}
listString.append("]");
listString.append(']');

return listString.toString();
} else if (x instanceof Collection) {
StringBuilder listString = new StringBuilder();
listString.append("[");
for (Object item : (Collection<?>) x) {
listString.append(encodeObject(item)).append(", ");
listString.append('[');
Collection<?> collection = (Collection<?>) x;
for (Object item : collection) {
listString.append(encodeObject(item)).append(',');
}
if (listString.length() > 1) {
listString.delete(listString.length() - 2, listString.length());
if (!collection.isEmpty()) {
listString.setLength(listString.length() - 1);
}
listString.append("]");
listString.append(']');

return listString.toString();
} else if (x instanceof Map) {
Map<?, ?> tmpMap = (Map<?, ?>) x;
StringBuilder mapString = new StringBuilder();
mapString.append("{");
mapString.append('{');
for (Object key : tmpMap.keySet()) {
mapString.append(encodeObject(key)).append(": ").append(encodeObject(tmpMap.get(key))).append(", ");
mapString.append(encodeObject(key)).append(": ").append(encodeObject(tmpMap.get(key))).append(',');
}
if (!tmpMap.isEmpty()) {
mapString.setLength(mapString.length() - 1);
}
if (!tmpMap.isEmpty())
mapString.delete(mapString.length() - 2, mapString.length());
mapString.append("}");

mapString.append('}');

return mapString.toString();
} else if (x instanceof Reader) {
Expand All @@ -853,35 +843,16 @@ private static String encodeObject(Object x, Long length) throws SQLException {
return encodeCharacterStream((InputStream) x, length);
} else if (x instanceof Object[]) {
StringBuilder arrayString = new StringBuilder();
arrayString.append("[");
int i = 0;
for (Object item : (Object[]) x) {
if (i > 0) {
arrayString.append(", ");
}
arrayString.append(encodeObject(item));
i++;
}
arrayString.append("]");

arrayString.append('[');
appendArrayElements((Object[]) x, arrayString);
arrayString.append(']');
return arrayString.toString();
} else if (x instanceof Tuple) {
StringBuilder tupleString = new StringBuilder();
tupleString.append("(");
Tuple t = (Tuple) x;
Object [] values = t.getValues();
int i = 0;
for (Object item : values) {
if (i > 0) {
tupleString.append(", ");
}
tupleString.append(encodeObject(item));
i++;
}
tupleString.append(")");
return tupleString.toString();
return arrayToTuple(((Tuple)x).getValues());
} else if (x instanceof Struct) {
return arrayToTuple(((Struct)x).getAttributes());
} else if (x instanceof UUID) {
return "'" + ((UUID) x).toString() + "'";
return QUOTE + ((UUID) x).toString() + QUOTE;
}

return SQLUtils.escapeSingleQuotes(x.toString()); //Escape single quotes
Expand All @@ -891,6 +862,23 @@ private static String encodeObject(Object x, Long length) throws SQLException {
}
}

private void appendArrayElements(Object[] array, StringBuilder sb) throws SQLException {
for (Object item : array) {
sb.append(encodeObject(item)).append(',');
}
if (array.length > 0) {
sb.setLength(sb.length() - 1);
}
}

private String arrayToTuple(Object[] array) throws SQLException {
StringBuilder tupleString = new StringBuilder();
tupleString.append('(');
appendArrayElements(array, tupleString);
tupleString.append(')');
return tupleString.toString();
}

private static String encodeCharacterStream(InputStream stream, Long length) throws SQLException {
return encodeCharacterStream(new InputStreamReader(stream, StandardCharsets.UTF_8), length);
}
Expand Down
Loading
Loading