Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@
`getFloat`/`setFloat` and `getObject` accessors, and reported as such by `ResultSetMetaData` and `DatabaseMetaData`.
Previously reading or writing a `BFloat16` column failed with an
unsupported-data-type error. (https://github.com/ClickHouse/clickhouse-java/issues/2279)
- **[client-v2, jdbc-v2]** Added support for the experimental `QBit(element_type, dimension[, stride])` vector data type
(ClickHouse `25.10+`; the `allow_experimental_qbit_type` server setting is required to create a column). The type-name
parser accepts two or three parameters (the optional third is the stride) and recognizes the documented element types
`Int8`, `BFloat16`, `Float32`, and `Float64`; an element type outside that set is parsed with a warning rather than
rejected, so a newer server-side element type keeps parsing. A `QBit` value is transmitted over `RowBinary` exactly like
`Array(element_type)`, so it is read and written as a Java array of the element type (`float[]` for
`BFloat16`/`Float32`, `double[]` for `Float64`) through generic records, binary readers, and POJO binding, via a
dedicated `QBit` read/serialize path. A `QBit` held inside a `Dynamic`/`Variant`/`JSON` column is also decoded (its
binary type encoding is read back to the concrete `QBit(...)` type). In the
JDBC driver (`jdbc-v2`) `QBit` maps to `java.sql.Types.ARRAY` and is returned as a `java.sql.Array` from
`getObject`/`getArray`. Previously `QBit` was an unimplemented type constant and reading or writing such a column
failed. Reading `QBit` through the `Native` output format is not supported — the server transmits it there using a
different internal layout — and fails fast with a clear error; use a `RowBinary` format instead.
(https://github.com/ClickHouse/clickhouse-java/issues/2610)
- **[client-v2, jdbc-v2]** Added TLS cipher suite selection. `Client.Builder.setSSLCipherSuites(String...)` (client-v2)
and the comma-separated `ssl_cipher_suites` connection property (client-v2 and jdbc-v2) restrict the cipher suites
enabled on secure connections; when unset, the transport defaults are used. Cipher-suite selection is independent of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@
import com.clickhouse.data.value.array.ClickHouseIntArrayValue;
import com.clickhouse.data.value.array.ClickHouseLongArrayValue;
import com.clickhouse.data.value.array.ClickHouseShortArrayValue;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -73,6 +76,17 @@ public final class ClickHouseColumn implements Serializable {
private static final String KEYWORD_NESTED = ClickHouseDataType.Nested.name();
private static final String KEYWORD_VARIANT = ClickHouseDataType.Variant.name();
private static final String KEYWORD_JSON = ClickHouseDataType.JSON.name();
private static final String KEYWORD_QBIT = ClickHouseDataType.QBit.name();

private static final Logger log = LoggerFactory.getLogger(ClickHouseColumn.class);

// Element types documented for QBit(element_type, dimension[, stride]).
// See https://clickhouse.com/docs/sql-reference/data-types/qbit . This is a soft (warning-only)
// allow-list: a newer server may add element types, and parsing such a type is not a client
// error, so an unlisted element type is warned about rather than rejected.
private static final Set<ClickHouseDataType> QBIT_ELEMENT_TYPES = Collections.unmodifiableSet(
EnumSet.of(ClickHouseDataType.Int8, ClickHouseDataType.BFloat16,
ClickHouseDataType.Float32, ClickHouseDataType.Float64));

private int columnCount;
private int columnIndex;
Expand Down Expand Up @@ -146,6 +160,17 @@ private static ClickHouseColumn update(ClickHouseColumn column) {
}
}
break;
case QBit:
Comment thread
polyglotAI-bot marked this conversation as resolved.
// QBit(element_type, dimension) is a one-level array of its element type on the
// wire; the dimension parameter is kept as the column precision.
if (!column.nested.isEmpty()) {
column.arrayLevel = 1;
column.arrayBaseColumn = column.nested.get(0);
}
if (size > 1) {
column.precision = Integer.parseInt(column.parameters.get(1).trim());
}
break;
case Bool:
column.template = ClickHouseBoolValue.ofNull();
break;
Expand Down Expand Up @@ -567,6 +592,36 @@ protected static int readColumn(String args, int startIndex, int len, String nam
fixedLength = false;
estimatedLength++;
}
} else if (args.startsWith(KEYWORD_QBIT, i)) {
int index = args.indexOf('(', i + KEYWORD_QBIT.length());
if (index < i) {
throw new IllegalArgumentException(ERROR_MISSING_NESTED_TYPE);
}
List<String> params = new LinkedList<>();
i = ClickHouseUtils.readParameters(args, index, len, params);
// QBit(element_type, dimension[, stride]) accepts two or three parameters: the element
// type, the vector dimension, and an optional stride.
// See https://clickhouse.com/docs/sql-reference/data-types/qbit .
if (params.size() < 2 || params.size() > 3) {
throw new IllegalArgumentException(
"QBit requires an element type and a dimension, with an optional stride, "
+ "e.g. QBit(Float32, 8) or QBit(BFloat16, 4096, 1024)");
}
// QBit(element_type, dimension) is transmitted over RowBinary exactly like
// Array(element_type): a var-int length followed by that many element values. The
// first parameter is the element type and drives the nested (item) column.
List<ClickHouseColumn> nestedColumns = new LinkedList<>();
ClickHouseColumn elementColumn = ClickHouseColumn.of("", params.get(0));
if (!QBIT_ELEMENT_TYPES.contains(elementColumn.getDataType())) {
log.warn("QBit element type '%s' is not one of the documented QBit element types %s;"
+ " parsing '%s' anyway", elementColumn.getDataType().name(),
QBIT_ELEMENT_TYPES, args.substring(startIndex, i));
}
nestedColumns.add(elementColumn);
column = new ClickHouseColumn(ClickHouseDataType.QBit, name, args.substring(startIndex, i),
nullable, lowCardinality, params, nestedColumns);
fixedLength = false;
estimatedLength++;
}

if (column == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,54 @@ public Object[][] testJSONBinaryFormat_dp() {
{"JSON(max_dynamic_types=3,max_dynamic_paths=3, SKIP REGEXP '^-.*',SKIP ff, flags Array(Array(Array(Int8))), SKIP alt_count)", 2, Arrays.asList("flags")},
};
}

@DataProvider(name = "qbitTypesProvider")
private static Object[][] qbitTypesProvider() {
return new Object[][] {
// typeName, elementType, dimension (see https://clickhouse.com/docs/sql-reference/data-types/qbit)
{ "QBit(Int8, 8)", ClickHouseDataType.Int8, 8 },
{ "QBit(BFloat16, 4)", ClickHouseDataType.BFloat16, 4 },
{ "QBit(Float32, 16)", ClickHouseDataType.Float32, 16 },
{ "QBit(Float64, 1536)", ClickHouseDataType.Float64, 1536 },
Comment thread
polyglotAI-bot marked this conversation as resolved.
{ "QBit(Float32, 4096)", ClickHouseDataType.Float32, 4096 },
// optional stride (3rd parameter); the dimension is still the 2nd parameter
{ "QBit(BFloat16, 4096, 1024)", ClickHouseDataType.BFloat16, 4096 },
};
}

@Test(groups = { "unit" }, dataProvider = "qbitTypesProvider")
public void testParseQBit(String typeName, ClickHouseDataType elementType, int dimension) {
ClickHouseColumn column = ClickHouseColumn.of("vec", typeName);
Assert.assertEquals(column.getDataType(), ClickHouseDataType.QBit);
Assert.assertEquals(column.getOriginalTypeName(), typeName);
// QBit(element_type, dimension) is a one-level array of its element type on the wire.
Assert.assertEquals(column.getArrayNestedLevel(), 1);
Assert.assertNotNull(column.getArrayBaseColumn());
Assert.assertEquals(column.getArrayBaseColumn().getDataType(), elementType);
Assert.assertEquals(column.getNestedColumns().size(), 1);
Assert.assertEquals(column.getNestedColumns().get(0).getDataType(), elementType);
// The fixed vector dimension is retained as the column precision.
Assert.assertEquals(column.getPrecision(), dimension);
}

@Test(groups = { "unit" }, expectedExceptions = IllegalArgumentException.class)
public void testParseQBitRequiresDimension() {
ClickHouseColumn.of("vec", "QBit(Float32)");
}

@Test(groups = { "unit" }, expectedExceptions = IllegalArgumentException.class)
public void testParseQBitRejectsTooManyParameters() {
ClickHouseColumn.of("vec", "QBit(Float32, 8, 8, 8)");
}

@Test(groups = { "unit" })
public void testParseQBitAllowsUndocumentedElementType() {
// An element type outside the documented set is warned about, not rejected, so that a
// newer server-side element type keeps parsing without a client code change.
ClickHouseColumn column = ClickHouseColumn.of("vec", "QBit(Int16, 4)");
Assert.assertEquals(column.getDataType(), ClickHouseDataType.QBit);
Assert.assertNotNull(column.getArrayBaseColumn());
Assert.assertEquals(column.getArrayBaseColumn().getDataType(), ClickHouseDataType.Int16);
Assert.assertEquals(column.getPrecision(), 4);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
return true;
}

private boolean readBlock() throws IOException {

Check failure on line 71 in client-v2/src/main/java/com/clickhouse/client/api/data_formats/NativeFormatReader.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ-J9-1qRYQuxmCUBDar&open=AZ-J9-1qRYQuxmCUBDar&pullRequest=2939
int nColumns;
try {
nColumns = BinaryStreamReader.readVarInt(input);
Expand All @@ -91,6 +91,21 @@
names.add(column.getColumnName());
types.add(column.getDataType().name());

if (containsQBit(column)) {
// QBit is transmitted in the Native format using its internal bit-transposed
Comment thread
chernser marked this conversation as resolved.
// Tuple(FixedString(...)) layout, which is NOT the Array(element_type)-like
// representation used in RowBinary (the only representation this reader decodes for
// QBit). Reading it through the columnar/per-row paths below would misread those bytes
// and desynchronize the block, corrupting the columns that follow. Fail loudly instead
// of silently decoding garbage. This also covers a QBit nested inside another type
// (e.g. Map(String, QBit(...))). QBit can be read through a RowBinary format.
throw new ClientException("Reading column '" + column.getColumnName() + "' ("
+ column.getOriginalTypeName() + ") from the Native format is not supported "
+ "because it contains a QBit type: QBit is serialized in the Native format "
+ "using an internal layout this reader does not decode. Use a RowBinary format "
+ "(e.g. RowBinaryWithNamesAndTypes) to read QBit values");
}

List<Object> values = new ArrayList<>(nRows);
if (column.isArray()) {
int[] sizes = new int[nRows];
Expand All @@ -116,6 +131,26 @@
return true;
}

/**
* Returns {@code true} if {@code column} is a {@code QBit} or contains a {@code QBit} anywhere in
* its nested type tree (e.g. {@code Array(QBit(...))}, {@code Tuple(..., QBit(...))},
* {@code Map(String, QBit(...))}). {@code QBit} uses a different, internal wire layout in the
* Native format than in RowBinary, so this reader cannot decode it and rejects such columns
* up-front rather than misreading the block. {@code Nullable}/{@code LowCardinality} wrappers are
* flags on the column, so a wrapped {@code QBit} still reports {@code dataType == QBit} here.
*/
private static boolean containsQBit(ClickHouseColumn column) {
if (column.getDataType() == ClickHouseDataType.QBit) {
return true;
}
for (ClickHouseColumn nested : column.getNestedColumns()) {
if (nested != column && containsQBit(nested)) {
return true;
}
}
return false;
}

private static class Block {
final List<String> names;
final List<String> types;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ private <T> T readValue(ClickHouseColumn column, Class<?> typeHint, boolean stri
return (T) readJsonData(input, actualColumn);
}
// case Object: // deprecated https://clickhouse.com/docs/en/sql-reference/data-types/object-data-type
case QBit:
return readQBit(actualColumn, typeHint);
case Array:
if (typeHint == null) { typeHint = arrayDefaultTypeHint;}
return convertArray(readArray(actualColumn), typeHint);
Expand Down Expand Up @@ -628,6 +630,38 @@ public static byte[] readNBytesLE(InputStream input, byte[] buffer, int offset,
return bytes;
}

/**
* Reads a {@code QBit(element_type, dimension)} value.
* <p>
* Over RowBinary a QBit is transmitted exactly like {@code Array(element_type)} — a var-int
* element count followed by that many element values — so the array reader is reused for the
* wire decoding. This is deliberately a dedicated method (rather than folding QBit into the
* {@code Array} case) because QBit is a distinct type whose Array-like RowBinary layout is an
* implementation detail, not an equivalence: in the Native format QBit uses a different internal
* layout (see {@link com.clickhouse.client.api.data_formats.NativeFormatReader}).
*
* @param column QBit column information
* @param typeHint requested element/array representation, may be {@code null}
* @return materialized QBit value
* @throws IOException when an IO error occurs
*/
private <T> T readQBit(ClickHouseColumn column, Class<?> typeHint) throws IOException {
ArrayValue array = readArray(column);
// QBit is a fixed-dimension vector. Over RowBinary the element count is a var-int length
// prefix that readArray consumes together with exactly that many elements, so the stream
// stays aligned regardless of the count. Validate the count against the declared dimension
// as a defensive, symmetric counterpart to the write-side check
// (SerializerUtils.serializeQBitData): for a well-formed QBit the count always equals the
// dimension, so a mismatch signals corrupt input and is surfaced as a clear error instead
// of a silently wrong-length vector.
int dimension = column.getPrecision();
if (array.length() != dimension) {
throw new ClientException("QBit column '" + column.getColumnName() + "' expected exactly "
+ dimension + " elements but received " + array.length());
}
return convertArray(array, typeHint == null ? arrayDefaultTypeHint : typeHint);
}
Comment thread
polyglotAI-bot marked this conversation as resolved.

/**
* Reads a array into an ArrayValue object.
* @param column - column information
Expand Down Expand Up @@ -1520,6 +1554,14 @@ private ClickHouseColumn readDynamicData() throws IOException {
ClickHouseColumn column = readDynamicData();
return ClickHouseColumn.of("v", "Nullable(" + column.getOriginalTypeName() + ")");
}
case QBit: {
Comment thread
cursor[bot] marked this conversation as resolved.
// 0x36 <element_type_encoding> <var_uint dimension> -> QBit(T, N).
// The element type and dimension MUST be consumed here so a QBit nested in a
// Dynamic/Variant/JSON column does not desynchronize the stream.
ClickHouseColumn elementColumn = readDynamicData();
int dimension = readVarInt(input);
return ClickHouseColumn.of("v", "QBit(" + elementColumn.getOriginalTypeName() + ", " + dimension + ")");
}
case Time64: {
byte precision = readByte();
return ClickHouseColumn.of("v", "Time64(" + precision + ")");
Expand Down
Loading
Loading