ClickHouse / ClickHouse/clickhouse-java
[client-v2] Compiled POJO setter emits invalid bytecode (VerifyError) when the column type does not match the primitive field type
- 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
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