[client-v2] Native format: a LowCardinality column is read as plain values, desynchronizes the stream ("Non-empty typeName is required")
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
Start with client-v2/src/main/java/com/clickhouse/client/api/data_formats/NativeFormatReader.java, especially readBlock around the LowCardinality exclusion and generic value-reading branch. Reproduce through Client.query and newBinaryFormatReader using the supplied Native-format query, then compare with RowBinary behavior. Done means top-level LowCardinality results are read correctly without desynchronizing the stream; preserve RowBinary behavior and avoid expanding into nested shapes covered by #3156.
索引モデルが issue の本文から書いたものです。
説明
Description
NativeFormatReader cannot read a top-level LowCardinality(...) column. Over HTTP, the server sends LowCardinality in the Native format with its dictionary encoding. The reader decodes it as if each row held one plain value (the RowBinary layout). It reads the wrong bytes, the stream loses synchronization, and the reader then parses data bytes as the next column header.
The result depends on the schema:
- If another column follows the
LowCardinalitycolumn, or the result has more than one row,client.newBinaryFormatReader(response)throwsIllegalArgumentException: Non-empty typeName is required. The message does not identify the column or the cause. - For a single-row result with only the
LowCardinalitycolumn (SELECT toLowCardinality('x') AS a),reader.next()returns 12 empty records ({}) before the same exception occurs. The expected row{a=x}is never returned.
Every LowCardinality shape I tested fails: LowCardinality(String), LowCardinality(Nullable(String)), LowCardinality(FixedString(1)), LowCardinality(UInt32), a table column, a column in the middle of the schema, and a multi-block result. The same queries read correctly with RowBinaryWithNamesAndTypes.
This is separate from #3156. That issue covers Array(LowCardinality(T)) and other nested shapes, where the element sub-column is decoded row-wise. Here the column is top-level and the cause is that no LowCardinality dictionary decoder exists.
Steps to reproduce
- Create a client-v2
Clientfor an HTTP endpoint. - Run
SELECT toLowCardinality(toString(number)) AS a, toInt32(number + 100) AS b FROM numbers(2)withQuerySettings.setFormat(ClickHouseFormat.Native). - Call
client.newBinaryFormatReader(response).
Error Log or Exception StackTrace
java.lang.IllegalArgumentException: Non-empty typeName is required
at com.clickhouse.data.ClickHouseDataType.of(ClickHouseDataType.java:488)
at com.clickhouse.data.ClickHouseColumn.readColumn(ClickHouseColumn.java:714)
at com.clickhouse.data.ClickHouseColumn.of(ClickHouseColumn.java:763)
at com.clickhouse.client.api.data_formats.NativeFormatReader.readBlock(NativeFormatReader.java:87)
at com.clickhouse.client.api.data_formats.NativeFormatReader.<init>(NativeFormatReader.java:34)
at com.clickhouse.client.api.Client.newBinaryFormatReader(Client.java:2465)
at com.clickhouse.client.api.Client.newBinaryFormatReader(Client.java:2485)
Expected Behaviour
The reader returns the same rows as the server and as the RowBinaryWithNamesAndTypes path. Server output (FORMAT JSONEachRow):
{"a":"0","b":100}
{"a":"1","b":101}
Results of the reproduction, client-v2 main @ 67a6b9e90:
| Query | RowBinaryWithNamesAndTypes |
Native |
|---|---|---|
SELECT toLowCardinality(toString(number)) AS a, toInt32(number + 100) AS b FROM numbers(2) |
{a=0, b=100}, {a=1, b=101} |
IllegalArgumentException |
SELECT toLowCardinality(if(number = 1, NULL, toNullable(toString(number)))) AS a, toInt32(number + 100) AS b FROM numbers(3) |
{a=0, b=100}, {a=null, b=101}, {a=2, b=102} |
IllegalArgumentException |
SELECT toLowCardinality(toFixedString(toString(number), 1)) AS a, toInt32(number + 100) AS b FROM numbers(2) |
correct | IllegalArgumentException |
SELECT toInt32(number) AS id, toLowCardinality(toString(number)) AS a, toInt32(number + 100) AS b FROM numbers(2) |
correct | IllegalArgumentException |
SELECT toLowCardinality(toString(number % 3)) AS a, toInt32(number + 100) AS b FROM numbers(7) SETTINGS max_block_size=3 |
correct (7 rows) | IllegalArgumentException |
SELECT a, b FROM t where a LowCardinality(String) |
correct | IllegalArgumentException |
SELECT toLowCardinality('x') AS a |
{a=x} |
12 empty records, then IllegalArgumentException |
SELECT CAST(a AS String) AS a, b FROM t (workaround) |
correct | correct |
The server setting low_cardinality_allow_in_native_format=0 is not a workaround over HTTP: on 26.9 the server still sends the LowCardinality type and the dictionary encoding, and the reader fails the same way.
Code Example
QuerySettings settings = new QuerySettings().setFormat(ClickHouseFormat.Native);
try (QueryResponse response = client.query(
"SELECT toLowCardinality(toString(number)) AS a, toInt32(number + 100) AS b FROM numbers(2)",
settings).get()) {
ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response); // throws here
Map<String, Object> row;
while ((row = reader.next()) != null) {
System.out.println(row.get("a") + " " + row.get("b"));
}
}
Root cause
NativeFormatReader.readBlock has no branch for LowCardinality:
client-v2/src/main/java/com/clickhouse/client/api/data_formats/NativeFormatReader.java:130excludesLowCardinalityfrom theNullablenull-map branch (correct, becauseLowCardinality(Nullable(T))has no null map inNative).- The column then reaches the generic branch at
NativeFormatReader.java:153-158, which callsbinaryStreamReader.readValue(column)once per row. That is the RowBinary decoder. In RowBinary the server sendsLowCardinality(T)as plainTvalues, so the decoder reads aString(orUInt32, …) per row.
What the server sends in Native for one column in one block (raw bytes of SELECT toLowCardinality(toString(number)) AS a FROM numbers(2) FORMAT Native, after the header):
01 00 00 00 00 00 00 00 key serialization version (UInt64) = 1
00 06 00 00 00 00 00 00 index type + flags (UInt64) = 0x0600: UInt8 indexes, HasAdditionalKeys | NeedUpdateDictionary
03 00 00 00 00 00 00 00 dictionary size (UInt64) = 3
00 | 01 30 | 01 31 dictionary values as a Native String column: "", "0", "1"
02 00 00 00 00 00 00 00 number of indexes (UInt64) = 2
01 02 indexes (UInt8): rows -> "0", "1"
The RowBinary decoder reads 01 00 as a 1-byte string and 00 as an empty string, stops there, and then reads the rest of the column as the name and type of the next column. The type name is empty, so ClickHouseColumn.of throws at NativeFormatReader.java:87.
Suggested fix
Add a LowCardinality Native decoder and route top-level LowCardinality columns to it in readBlock (before the generic branch). Per column per block:
- Read the
UInt64key serialization version (expect1, shared dictionaries with additional keys). - Read the
UInt64index type + flags. The low 8 bits give the index width:0=UInt8,1=UInt16,2=UInt32,3=UInt64(for example, 300 distinct keys give0x0601,UInt16). - Read the
UInt64dictionary size, then the dictionary as oneNativecolumn of the inner type. ForLowCardinality(Nullable(T))the dictionary is plainTwith no null map, and index0means NULL. Raw bytes for(0, NULL, 2): dictionary"", "", "0", "2", indexes02 00 03. - Read the
UInt64number of indexes, then the indexes, and map each index to its dictionary value.
Each block repeats steps 1-4 with its own dictionary (verified with max_block_size=2), so no state carries across blocks.
Contrast cases that must keep their current behavior:
- The RowBinary formats: there the server sends
LowCardinality(T)as plainT, andreadValueis correct. LowCardinalityinsideArray/Map/Tuple: that is part of #3156. If the fix does not cover nested shapes, they should fail with a clearClientExceptionand not desynchronize the stream.
If a decoder is out of scope, a smaller fix is to reject LowCardinality columns in readBlock with a clear ClientException that names the column and suggests a RowBinary format, as the reader already does for unsupported QBit shapes. That at least removes the misleading exception and the empty records.
Workaround until then: use RowBinaryWithNamesAndTypes (the default format), or CAST(col AS T) in the query.
Provenance
Found by automated analysis of client-v2 while investigating #3156. Verified against a live server with an integration test through Client.query + newBinaryFormatReader, not by inspection.
Configuration
Client Configuration
new Client.Builder()
.addEndpoint(Protocol.HTTP, host, 8123, false)
.setUsername("default")
.setPassword(password)
.build();
Environment
- Cloud
- Client version: client-v2
0.12.0-rc1-SNAPSHOT(main@67a6b9e90) - Language version: Java 17
- OS: Linux (Docker)
ClickHouse Server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none (
low_cardinality_allow_in_native_formatdefault1) CREATE TABLEstatements for tables involved:CREATE TABLE t (a LowCardinality(String), b Int32) ENGINE = MergeTree ORDER BY b(only for the table case; the other cases usenumbers())- Sample data for all these tables:
INSERT INTO t VALUES ('x', 1), ('y', 2), ('x', 3)
- 主要言語
- Java
- スター
- 1.6k
- フォーク
- 649
- 平均マージ
- 1日 20時間
- マージ済み PR(30日)
- 39
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ClickHouse/clickhouse-java のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
ClickHouse/clickhouse-java#3349 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
ClickHouse/clickhouse-java#3347 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
ClickHouse/clickhouse-java#3326 ·
メンテナーはふだん 1 日以内に返信
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum members対応中かも @jaya232 が 6 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
ClickHouse/clickhouse-java#3304 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
ClickHouse/clickhouse-java#3214 ·
メンテナーはふだん 1 日以内に返信
ClickHouse/clickhouse-java の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
bug:unconfirmed
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
StarRocks/starrocks#83246 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 2 日以内に返信