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
10 changes: 6 additions & 4 deletions docs/clickhouse-docs/jdbc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ as is to the driver. Driver has some own properties that aren't part of the clie
| `jdbc_use_max_result_rows` | `false` | Enables using server property `max_result_rows` to limit number of rows returned by query. When enabled, overrides user-set overflow mode. See JavaDoc for details. |
| `jdbc_sql_parser` | `JAVACC` | Configures which SQL parser to use. Choices: `ANTLR4`, `ANTLR4_PARAMS_PARSER`, `JAVACC`. |
| `remember_last_set_roles` | `true` | Remember last set roles for the connection. |
| `jdbc_multi_dimensional_array_element_type` | `true` | `Array.getBaseTypeName()` and `Array.getBaseType()` of a multi-dimensional array return the element type, which is the next array level. Set to `false` to return the innermost non-array type, as in earlier versions. See **Array Element Type Metadata** below. |

<Info>
**Server Settings**
Expand Down Expand Up @@ -372,13 +373,14 @@ The general mapping rules for arrays are:
| `Array(<Parameterized Type>(<N>))` | `<Parameterized Type>(<N>)` | `<JDBC type of base type>` |
| `Array(Nullable(<Type>))` | `Nullable(<Type>)` | `<JDBC type of inner Type>` |
| `Array(LowCardinality(<Type>))` | `LowCardinality(<Type>)` | `<JDBC type of inner Type>` |
| `Array(Array(...(<Type>)))` | `<Type>` (innermost element) | `<JDBC type of innermost>` |
| `Array(Array(...(<Type>)))` | `Array(...(<Type>))` (next array level) | `ARRAY` |
| `Array(Tuple(...))` | `Tuple(...)` (full definition) | `OTHER` |
| `Array(Enum8(...))` / `Array(Enum16(...))` | `Enum8(...)` / `Enum16(...)` (full definition) | `VARCHAR` |

Notes on the rules above:
- Wrapper types (`Nullable`, `LowCardinality`) are preserved in `getBaseTypeName()` but `getBaseType()` resolves to the inner type's JDBC code.
- Nested arrays are flattened in the metadata: `getBaseTypeName()` returns the innermost non-array element type, not the immediate child.
- Nested arrays are not flattened in the metadata: `getBaseTypeName()` returns the immediate child type, which is the next array level, and `getBaseType()` returns `ARRAY`. The `VALUE` column of `getResultSet()` has the same type; call `getArray()` on it to read the next level.
Set the driver property `jdbc_multi_dimensional_array_element_type=false` to get the earlier behavior: `getBaseTypeName()` and `getBaseType()` return the innermost non-array type (`Int32` and `INTEGER` for `Array(Array(Int32))`) at each array level. The `VALUE` column metadata of `getResultSet()` always reports the next array level and is not affected by this property.
- Parameterized types (`FixedString(N)`, full `Enum`/`Tuple` definitions) keep their parameters in `getBaseTypeName()`.

**Examples:**
Expand All @@ -404,8 +406,8 @@ Notes on the rules above:
| Array(Nullable(String)) | Nullable(String) | VARCHAR |
| Array(LowCardinality(String)) | LowCardinality(String) | VARCHAR |
| Array(LowCardinality(Nullable(String))) | LowCardinality(Nullable(String)) | VARCHAR |
| Array(Array(Int32)) | Int32 | INTEGER |
| Array(Array(Array(String))) | String | VARCHAR |
| Array(Array(Int32)) | Array(Int32) | ARRAY |
| Array(Array(Array(String))) | Array(Array(String)) | ARRAY |
| Array(Tuple(name String, val Int32)) | Tuple(name String, val Int32) | OTHER |
| Array(Enum8('alpha' = 1, 'beta' = 2, 'gamma' = 3)) | Enum8('alpha' = 1, 'beta' = 2, 'gamma' = 3) | VARCHAR |

Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Compatibility-sensitive traits:
- Type mapping and conversions: Maps ClickHouse types to JDBC types and Java classes, including date/time handling and `java.time` support.
- QBit type mapping: For ClickHouse `25.10+`, JDBC exposes the experimental `QBit(element_type, dimension)` type as `ARRAY`, returning the vector as a `java.sql.Array` of the element type from `getObject()`/`getArray()`. Supported element types are `BFloat16` and `Float32` (both `java.lang.Float`) and `Float64` (`java.lang.Double`). The `allow_experimental_qbit_type` server setting is required to create a `QBit` column.
- Custom result-set type map: `ResultSet#getObject(int|String, Map<String, Class<?>>)` accepts both ClickHouse type names and JDBC `SQLType` names as map keys. Only unwrapped type names are used — `Nullable(...)` and `LowCardinality(...)` wrappers are stripped before lookup, so a key like `"Int32"` matches both `Int32` and `Nullable(Int32)` columns, and keys like `"Nullable(Int32)"` are not recognized. Lookup order is `ClickHouseColumn#getDataType().name()` (e.g. `"Int32"`, `"String"`, `"DateTime"`) then `SQLType.getName()` (e.g. `"INTEGER"`, `"VARCHAR"`, `"TIMESTAMP"`); a missing entry leaves the value uncoerced (read as-is). The feature is supported for primitive ClickHouse types only — `Array`, `Tuple`, `Map`, `Nested`, and geometry types (`Point`, `Ring`, `LineString`, `MultiPoint`, `Polygon`, `MultiPolygon`, `MultiLineString`, `Geometry`) bypass the type map and are returned in their native form.
- Arrays and tuples: Supports JDBC arrays plus ClickHouse tuple values through custom `Array` and `Struct` implementations.
- Arrays and tuples: Supports JDBC arrays plus ClickHouse tuple values through custom `Array` and `Struct` implementations. For a multi-dimensional `Array(Array(...))` value, `java.sql.Array#getBaseType()`/`getBaseTypeName()` report the element type, which is the next array level (`ARRAY`/`Array(T)`), the same as the `VALUE` column of its `getResultSet()`. The driver property `jdbc_multi_dimensional_array_element_type=false` (default `true`) selects the earlier behavior, where they report the innermost non-array type (`T`); the `VALUE` column metadata of `getResultSet()` is not affected.
- Nested columns: Un-flattened `Nested(f1 T1, ..., fN TN)` columns (tables created with `flatten_nested = 0`) are exposed as JDBC `ARRAY` whose element type is `Tuple(f1 T1, ..., fN TN)`. They can be inserted through `Connection#createArrayOf`/`setArray` or `setObject` (a Java array of tuples) and read back through `getArray`/`getObject`; `java.sql.Array#getResultSet()` iterates the nested rows as `(INDEX, VALUE)` pairs where each `VALUE` is the tuple. An `Array(Nested(...))` column is read as a JDBC `ARRAY` whose elements are the nested values, each an array of tuples (`Object[]` of `Object[]`); its `getResultSet()` returns each element as a `java.sql.Array` equal to a top-level `Nested(...)` value.
- Geometry type mapping: For ClickHouse `25.11+`, where `Geometry` changed from a string alias to `Variant(Point, Ring, LineString, MultiLineString, Polygon, MultiPolygon)`, JDBC exposes `Geometry` as `ARRAY`, returns nested Java arrays from `getObject()`/`getArray()`, and accepts `Struct` or nested `Array` inputs for prepared-statement inserts depending on the geometry shape. For ClickHouse `26.8+` the variant also carries `MultiPoint`.
- MultiPoint type mapping: For ClickHouse `26.8+`, JDBC exposes `MultiPoint` as `ARRAY` with type name `MultiPoint`, returns `double[][]` from `getObject()` and a `java.sql.Array` from `getArray()`, and accepts a nested `Array` (`createArrayOf("Array(Point)", ...)`) for prepared-statement inserts, the same as `Ring` and `LineString`.
Expand Down
1 change: 1 addition & 0 deletions docs/integration-jdbc.md
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ Key JDBC-specific properties (see [`DriverProperties`](../jdbc-v2/src/main/java/
| `default_query_settings` | — | Default settings for all queries |
| `jdbc_metrics_recorder` | — | Custom `MetricsRecorder` implementation class name |
| `jdbc_metadata_use_show_statements` | `true` | `DatabaseMetaData` schemas, tables and columns via `SHOW` / `DESCRIBE` instead of `system.*` tables |
| `jdbc_multi_dimensional_array_element_type` | `true` | `Array#getBaseType()` / `getBaseTypeName()` of a multi-dimensional array return the next array level; `false` returns the innermost non-array type |


## Observability & Monitoring
Expand Down
6 changes: 6 additions & 0 deletions history/latest/3190.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **[jdbc-v2]** Breaking change: `java.sql.Array#getBaseType()`/`getBaseTypeName()` of a multi-dimensional array
now report the element type, which is the next array level (`ARRAY`/`Array(UInt8)` for `Array(Array(UInt8))`).
Before, they reported the innermost type (`SMALLINT`/`UInt8`), which did not match the element values. This also
applies to arrays from `Connection#createArrayOf`. The new driver property
`jdbc_multi_dimensional_array_element_type` (default `true`) controls this behavior. Set it to `false` to get the
innermost type, as in earlier versions. (https://github.com/ClickHouse/clickhouse-java/issues/3190)
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,8 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti

ClickHouseColumn column = ClickHouseColumn.of("array", typeName);
try {
return new com.clickhouse.jdbc.types.Array(column, elements);
return new com.clickhouse.jdbc.types.Array(column, elements,
config.isFlagSet(DriverProperties.MULTI_DIMENSIONAL_ARRAY_ELEMENT_TYPE));
} catch (Exception e) {
throw new SQLException("Failed to create array", ExceptionUtils.SQL_STATE_CLIENT_ERROR, e);
}
Expand Down
11 changes: 11 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 @@ -171,6 +171,17 @@ public enum DriverProperties {
*/
METADATA_USE_SHOW_STATEMENTS("jdbc_metadata_use_show_statements", String.valueOf(Boolean.TRUE)),

/**
* Controls what {@link java.sql.Array#getBaseType()} and {@link java.sql.Array#getBaseTypeName()} return for a
* multi-dimensional array like {@code Array(Array(Int32))}. When enabled, they return the element type, which is
* the next array level ({@code ARRAY} and {@code Array(Int32)}). When disabled, they return the innermost
* non-array type ({@code INTEGER} and {@code Int32}), as in earlier versions. One-level arrays are not affected.
* The metadata of the {@code VALUE} column of {@link java.sql.Array#getResultSet()} always reports the next array
* level and is not affected.
* Default: true
*/
MULTI_DIMENSIONAL_ARRAY_ELEMENT_TYPE("jdbc_multi_dimensional_array_element_type", String.valueOf(Boolean.TRUE)),

;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper {

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

private final boolean multiDimensionalArrayElementType;

public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, ClickHouseFormatReader reader,
Consumer<Exception> onDataTransferException) throws SQLException {
this(parentStatement, response, reader, onDataTransferException, JdbcUtils.DATA_TYPE_CLASS_MAP);
Expand All @@ -86,6 +88,8 @@ public ResultSetImpl(StatementImpl parentStatement, QueryResponse response, Clic
this.featureManager = new FeatureManager(parentStatement.getConnection().getJdbcConfig());
TableSchema tableMetadata = reader.getSchema();
this.connTypeMap = parentStatement.getConnection().getTypeMap();
this.multiDimensionalArrayElementType = parentStatement.getConnection().getJdbcConfig()
.isFlagSet(DriverProperties.MULTI_DIMENSIONAL_ARRAY_ELEMENT_TYPE);

final Map<ClickHouseDataType, Class<?>> resolvedDefaultTypeMap =
defaultTypeMap != null ? defaultTypeMap : JdbcUtils.DATA_TYPE_CLASS_MAP;
Expand Down Expand Up @@ -1552,7 +1556,7 @@ public <T> T getObjectImpl(String columnLabel, Class<?> type, Map<String, Class<
"Use getObject(...) to read this column.");
}

return (T) JdbcUtils.convert(value, type, column);
return (T) JdbcUtils.convert(value, type, column, multiDimensionalArrayElementType);
} else {
wasNull = true;
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,15 @@
}

public static Object convert(Object value, Class<?> type, ClickHouseColumn column) throws SQLException {
return convert(value, type, column, true);
}

/**
* @param multiDimensionalArrayElementType passed to the created {@link Array}, see
* {@link Array#Array(ClickHouseColumn, Object[], boolean)}
*/
public static Object convert(Object value, Class<?> type, ClickHouseColumn column,

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaFDX8Natl1XFX6T7bDz&open=AaFDX8Natl1XFX6T7bDz&pullRequest=3215
boolean multiDimensionalArrayElementType) throws SQLException {
if (value == null || type == null) {
return value;
}
Expand All @@ -316,16 +325,16 @@
ClickHouseDataType baseType = baseColumn.isNested() ? ClickHouseDataType.Tuple : baseColumn.getDataType();
Object[] convertedValues = convertList(listValue, convertToJavaClass(baseType),
getArrayDimensions(column));
return new Array(column, convertedValues);
return new Array(column, convertedValues, multiDimensionalArrayElementType);
}

// base type is unknown. all objects should be converted
return new Array(column, listValue.toArray());
return new Array(column, listValue.toArray(), multiDimensionalArrayElementType);
}

if (value.getClass().isArray()) {
if (type == java.sql.Array.class) {
return new Array(column, arrayToObjectArray(value));
return new Array(column, arrayToObjectArray(value), multiDimensionalArrayElementType);
} else if (type == Tuple.class) {
return new Tuple(true, value);
}
Expand All @@ -338,10 +347,10 @@
ClickHouseDataType baseType = column.getArrayBaseColumn().getDataType();
Object[] convertedValues = convertArray(arrayValue.getArray(), convertToJavaClass(baseType),
column.getArrayNestedLevel());
return new Array(column, convertedValues);
return new Array(column, convertedValues, multiDimensionalArrayElementType);
}

return new Array(column, arrayValue.getArrayOfObjects());
return new Array(column, arrayValue.getArrayOfObjects(), multiDimensionalArrayElementType);
}

return convertObject(value, type, column);
Expand Down
23 changes: 19 additions & 4 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,32 @@
private final String elementTypeName;
private boolean valid;
private final ClickHouseDataType baseDataType;
private final boolean multiDimensionalElementType;
private ArrayResultSet arrayResultSet;

public Array(ClickHouseColumn column, Object[] elements) throws SQLException {
this(column, elements, true);
}

/**
* @param multiDimensionalElementType if {@code true}, {@link #getBaseType()} and {@link #getBaseTypeName()} of a
* multi-dimensional array return the element type (the next array level);
* if {@code false}, they return the innermost non-array type. See
* {@link com.clickhouse.jdbc.DriverProperties#MULTI_DIMENSIONAL_ARRAY_ELEMENT_TYPE}.
*/
public Array(ClickHouseColumn column, Object[] elements, boolean multiDimensionalElementType) throws SQLException {

Check warning on line 38 in jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.sql.SQLException', as it cannot be thrown from constructor's body.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaFDX8QItl1XFX6T7bD0&open=AaFDX8QItl1XFX6T7bD0&pullRequest=3215
this.column = column;
this.array = elements;
this.multiDimensionalElementType = multiDimensionalElementType;
// QBit is not an Array (isArray() == false) but is still a one-level array of its element
// type, so use the array nesting level (set for both Array and QBit) to find the element column.
ClickHouseColumn baseColumn = (this.column.getArrayNestedLevel() > 0 ? this.column.getArrayBaseColumn() : this.column);
this.baseDataType = baseColumn.getDataType();
this.elementTypeName = baseColumn.getOriginalTypeName();
this.type = JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(baseDataType, JDBCType.OTHER).getVendorTypeNumber();
// Elements of a multi-dimensional array are arrays of the next level, not values of the base column.
Comment thread
polyglotAI-bot marked this conversation as resolved.
ClickHouseColumn elementColumn = multiDimensionalElementType && this.column.getArrayNestedLevel() > 1
? this.column.getNestedColumns().get(0) : baseColumn;
this.elementTypeName = elementColumn.getOriginalTypeName();
this.type = JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.getOrDefault(elementColumn.getDataType(), JDBCType.OTHER).getVendorTypeNumber();
this.valid = true;
}

Expand Down Expand Up @@ -94,7 +109,7 @@
public synchronized ResultSet getResultSet() throws SQLException {
ensureValid();
if (arrayResultSet == null) {
arrayResultSet = new ArrayResultSet(array, column);
arrayResultSet = new ArrayResultSet(array, column, multiDimensionalElementType);
}
return arrayResultSet;
}
Expand All @@ -107,7 +122,7 @@
@Override
public ResultSet getResultSet(long index, int count) throws SQLException {
ensureValid();
return new ArrayResultSet(getArray(index, count), column);
return new ArrayResultSet(getArray(index, count), column, multiDimensionalElementType);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,20 @@ public class ArrayResultSet implements ResultSet {
private final Class<?> defaultClass;
private final ClickHouseColumn column;
private final int columnCount;
private final boolean multiDimensionalElementType;

public ArrayResultSet(Object array, ClickHouseColumn column) {
this(array, column, true);
}

/**
* @param multiDimensionalElementType passed to the {@link com.clickhouse.jdbc.types.Array} values of the
* {@code VALUE} column, see
* {@link com.clickhouse.jdbc.types.Array#Array(ClickHouseColumn, Object[], boolean)}
*/
public ArrayResultSet(Object array, ClickHouseColumn column, boolean multiDimensionalElementType) {
this.array = array;
this.multiDimensionalElementType = multiDimensionalElementType;
this.length = java.lang.reflect.Array.getLength(array);
this.pos = -1;
this.column = column;
Expand Down Expand Up @@ -162,7 +173,8 @@ private Object getValueAsObject(int columnIndex, Class<?> type, Object defaultVa
}

if (type == Array.class) {
return new com.clickhouse.jdbc.types.Array(elementColumn(column), JdbcUtils.arrayToObjectArray(value));
return new com.clickhouse.jdbc.types.Array(elementColumn(column), JdbcUtils.arrayToObjectArray(value),
multiDimensionalElementType);
} else {
Map<Class<?>, Function<Object, Object>> valueConverterMap = initValueConverterMapIfNeeded(value);
return convertValue(value, type, valueConverterMap);
Expand Down
Loading
Loading