[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum members
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 78/100
調査の方向性
client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java の serializeEnumData() から始め、次に ClickHouseEnum の検証メソッドと検索メソッドを読みます。既存の enum シリアライズテストを確認し、メンバーではない Number と、ordinal が宣言された ClickHouse の値と異なる Java enum 定数のテストカバレッジを追加します。無効な数値が拒否され、Java enum 名が宣言された値に対応付けられれば完了です。関連する client-v2 のテストを実行してください。
索引モデルが issue の本文から書いたものです。
説明
Description
SerializerUtils.serializeEnumData() validates only the String form of an Enum8/Enum16 value. The other two accepted forms are written to the wire unchecked:
Number—((Number) value).intValue()is written as-is, with no check that the integer is a declared member of the column's enum.Enum<?>—((Enum<?>) value).ordinal()is written. Java'sordinal()is a 0-based declaration index and has nothing to do with the ClickHouse enum's declared numeric values, so a Java enum constant almost always maps to the wrong value (and typically to0, which is usually not a member at all).
The server does not validate enum values on RowBinary input, so the row is accepted and stored. The invalid value then breaks reads: any text/JSON rendering of the column fails server-side with UNKNOWN_ELEMENT_OF_ENUM, and the row is effectively unrecoverable except by CAST(col, 'Int8').
Note the String path is already correct — ClickHouseEnum.value(String) throws IllegalArgumentException: Unknown enum name: .... This report is only about the numeric and Java-enum paths.
This is the clickhouse-java counterpart of https://github.com/ClickHouse/clickhouse-connect/issues/1156, where the same class of silent corruption happens (there via an unknown enum name defaulting to 0).
ClickHouse server version
26.10.3.7 (verified against a running server over HTTP).
Reproduction
TestNG test placed in client-v2/src/test/java/com/clickhouse/client/datatypes/:
package com.clickhouse.client.datatypes;
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.data_formats.RowBinaryFormatWriter;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.metadata.TableSchema;
import com.clickhouse.client.api.query.GenericRecord;
import com.clickhouse.data.ClickHouseFormat;
import org.testng.annotations.Test;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class EnumInsertValidationTest {
public enum Status { active, inactive }
@Test
public void enumInsertAcceptsNonMemberValues() throws Exception {
Client client = new Client.Builder()
.addEndpoint(Protocol.HTTP, "localhost", 8123, false)
.setUsername("default").setPassword("")
.setDefaultDatabase("default")
.build();
String table = "enum_validation_" + UUID.randomUUID().toString().replace('-', '_');
client.execute("DROP TABLE IF EXISTS " + table).get(30, TimeUnit.SECONDS);
client.execute("CREATE TABLE " + table
+ " (id UInt8, status Enum8('active' = 1, 'inactive' = 2)) ENGINE = Memory")
.get(30, TimeUnit.SECONDS);
TableSchema schema = client.getTableSchema(table);
ClickHouseFormat format = ClickHouseFormat.RowBinary;
// (a) integer that is not a member of the enum -- expected to be rejected
try (InsertResponse r = client.insert(table, out -> {
RowBinaryFormatWriter w = new RowBinaryFormatWriter(out, schema, format);
w.setValue(schema.nameToColumnIndex("id"), 2);
w.setValue(schema.nameToColumnIndex("status"), 7);
w.commitRow();
}, format, new InsertSettings()).get(30, TimeUnit.SECONDS)) {
System.out.println("non-member int accepted, rows=" + r.getWrittenRows());
}
// (b) Java enum constant -- serialized via ordinal(), not via the declared value
try (InsertResponse r = client.insert(table, out -> {
RowBinaryFormatWriter w = new RowBinaryFormatWriter(out, schema, format);
w.setValue(schema.nameToColumnIndex("id"), 3);
w.setValue(schema.nameToColumnIndex("status"), Status.active);
w.commitRow();
}, format, new InsertSettings()).get(30, TimeUnit.SECONDS)) {
System.out.println("Java enum accepted, rows=" + r.getWrittenRows());
}
for (GenericRecord rec : client.queryAll(
"SELECT id, CAST(status, 'Int8') AS raw FROM " + table + " ORDER BY id")) {
System.out.println("row id=" + rec.getInteger("id") + " raw=" + rec.getInteger("raw"));
}
// Reading the enum as text fails server-side: the stored values are not members.
try {
client.queryAll("SELECT toString(status) FROM " + table);
System.out.println("text read OK");
} catch (Exception e) {
System.out.println("text read FAILED: " + e);
}
client.close();
}
}
Actual output
non-member int accepted, rows=1
Java enum accepted, rows=1
row id=2 raw=7
row id=3 raw=0
text read FAILED: com.clickhouse.client.api.ClientException: Failed to get query response
7 is not a member of Enum8('active' = 1, 'inactive' = 2), and Status.active was written as 0 (its ordinal()) rather than as 1 (its declared value in the column). Both rows are stored, and reading the column as text then fails with UNKNOWN_ELEMENT_OF_ENUM on the server.
For contrast, the equivalent text-format insert is rejected by the server:
INSERT INTO t VALUES (2, 'Active')
-> Unknown element 'Active' for enum, maybe you meant: ['active']
Expected output
Both inserts should fail client-side with an error naming the column and the offending value, in the same style as the existing String path (IllegalArgumentException: Unknown enum name: Active), e.g.:
java.lang.IllegalArgumentException: Unknown enum value: 7
and Status.active should be written as the column's declared value for the name active (i.e. 1), or rejected if the name is not a member.
Suggested fix
client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java, serializeEnumData() (around lines 863-872):
} else if (value instanceof Number) {
enumValue = ((Number) value).intValue(); // not validated
} else if (value instanceof Enum<?>) {
enumValue = ((Enum<?>)value).ordinal(); // ordinal != declared enum value
}
ClickHouseEnum already provides the needed checks and lookups:
validate(int)— throwsIllegalArgumentException("Unknown enum value: ..."); use it for theNumberbranch.value(String)— name to declared value; theEnum<?>branch should usecolumn.getEnumConstants().value(((Enum<?>) value).name())rather thanordinal().
It would also help to include the column name and original type name in the message, as the null check immediately above already does.
Link
Relayed from https://github.com/ClickHouse/clickhouse-connect/issues/1156
Central tracking issue: https://github.com/ClickHouse/integrations-ai-playground/issues/649
- 主要言語
- 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 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
ClickHouse/clickhouse-java#3214 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ClickHouse/clickhouse-java#3212 ·
メンテナーはふだん 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 日以内に返信