[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum members

オープン 初心者向け
#3,304 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@jaya232 がすでに取り組んでいます。

2026年11月7日 から。

  • #3312 @jaya232 による — オープン

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
78/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java
領域
backend, databases

調査の方向性

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's ordinal() 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 to 0, 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) — throws IllegalArgumentException("Unknown enum value: ..."); use it for the Number branch.
  • value(String) — name to declared value; the Enum<?> branch should use column.getEnumConstants().value(((Enum<?>) value).name()) rather than ordinal().

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

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ClickHouse/clickhouse-java のほかの issue

ClickHouse/clickhouse-java の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。