ClickHouse / ClickHouse/clickhouse-java

[client-v2] Compiled POJO setter emits invalid bytecode (VerifyError) when the column type does not match the primitive field type

Open
#2,999 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
1.6k
Forks
636
Avg merge
2d 16h
Merged PRs (30d)
28

Description

### Describe the bug

`client-v2` compiles a POJO field setter into bytecode (`SerializerUtils.compilePOJOSetter`). When the column type
and the POJO field's primitive type do not match exactly, the generated class is invalid and reading fails with
`java.lang.VerifyError` (thrown when the compiled setter class is first linked, i.e. on the first row read).

Two distinct groups of combinations are affected.

**1. The value left on the stack is not converted to the setter's primitive type.**
`binaryReaderMethodForType` only computed a conversion opcode for a few reader/field combinations
(`longToOpcode`, `floatToOpcode` and `doubleToOpcode` return `-1` for `byte`/`short`/`char`/`boolean`, and the
`Int8`/`UInt8`/`Int16`/`Enum8`/`Enum16`/`Bool` branches computed no conversion at all). So, for example:

* an `Int64`, `UInt32`, `Float32`, `Float64` or `BFloat16` column bound to a `byte`, `short`, `char` or `boolean` field
* an `Int8`, `UInt8`, `Int16`, `Enum8`, `Enum16` or `Bool` column bound to a `long`, `float` or `double` field

leave a value of the wrong type on the operand stack for the setter descriptor, and the generated class does not verify.

**2. A primitive field bound to a column that is not read into a primitive.**
The generic branch emits `LDC` of a class constant for the target type and `CHECKCAST` with its internal name.
For a primitive target that produces a class constant named `"I"`/`"J"` and a `CHECKCAST int`, and the value read
by `readValue` (an `Object`) is passed to a primitive setter descriptor. So `Int128`, `UInt128`, `Int256`, `UInt256`
and `Decimal*` columns cannot be read into a primitive field at all, even though the value is a `Number`.

### Steps to reproduce

```java
public class Pojo {
private short v;
public short getV() { return v; }
public void setV(short v) { this.v = v; }
}
```

```java
String sql = "SELECT toInt64(300) AS v"; // also: toFloat64(-2.7), toInt8(-5) into a long field, ...
TableSchema schema = client.getTableSchemaFromQuery(sql);
client.register(Pojo.class, schema);
client.queryAll(sql, Pojo.class, schema); // -> java.lang.VerifyError
```

The same happens with `SELECT toInt128(-2) AS v` or `SELECT toDecimal64(123.45, 2) AS v` bound to a `long`/`double`
field (group 2).

### Expected behaviour

The value is converted to the field's primitive type following Java's narrowing/widening rules (as already happens
for an `Int32` column bound to a `byte` field), and a `Number`-valued column can be read into a primitive field.

### Error log

```
java.lang.VerifyError
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
...
at com.clickhouse.client.api.data_formats.internal.SerializerUtils.compilePOJOSetter(SerializerUtils.java:...)
```

### Configuration

* Client version: `main` (0.11.0-rc1)
* Language: Java
* Client: `client-v2` (POJO binding / `queryAll(sql, Pojo.class, schema)`)

Contributor guide

Open the contributing guide

Research direction

Reproduce the failure with the shown client-v2 POJO query, then inspect SerializerUtils.compilePOJOSetter and binaryReaderMethodForType, including the longToOpcode, floatToOpcode, and doubleToOpcode branches. Done means mismatched primitive bindings generate verifiable setters with Java narrowing or widening conversions, and Number-valued columns can be read into primitive fields without VerifyError.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.