Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@

### Bug Fixes

- **[client-v2]** Fixed the compiled POJO setter emitting invalid bytecode (`VerifyError` on the first row read) when a
column is bound to a POJO field of a different primitive type. The value read from the stream is now converted to the
field's primitive type following Java's narrowing/widening rules, so e.g. an `Int64`/`UInt32`/`Float32`/`Float64`/
`BFloat16` column can be read into a `byte`, `short`, `char` or `boolean` field, and an `Int8`/`UInt8`/`Int16`/
`Enum8`/`Enum16`/`Bool` column into a `long`, `float` or `double` field. A primitive field bound to a column that the
reader decodes into a `Number` (`Int128`, `UInt128`, `Int256`, `UInt256`, `Decimal*`) is now unboxed instead of
producing invalid bytecode; for a column whose value is not a number (e.g. `Date`, `IPv4`, `String`, `UUID`) reading
into a numeric primitive field now fails with a clear `IllegalArgumentException`.
(https://github.com/ClickHouse/clickhouse-java/issues/2999)
- **[client-v2]** Fixed LZ4 input streams not closing their underlying HTTP response stream. Closing an LZ4 stream
returned by `QueryResponse.getInputStream()` now releases the wrapped transport stream, including after a partial
read. (https://github.com/ClickHouse/clickhouse-java/issues/2985)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1130,10 +1130,36 @@ public static <T extends Enum<T>> Set<T> parseEnumList(String value, Class<T> en
return values;
}

/**
* Used from the bytecode generated by {@link #compilePOJOSetter(Method, ClickHouseColumn)} to unbox a value
* into a primitive POJO field. Does all needed conversions so have to accept Object instead of Number.
*
* @param value value returned by {@code BinaryStreamReader.readValue}
* @return the value as a {@link Number}
*/
public static Number objectToNumber(Object value) {
if (value instanceof Number) {
return (Number) value;
}
throw new IllegalArgumentException("Cannot convert " +
(value == null ? "null" : value.getClass().getName()) + " to a number");
}

public static boolean numberToBoolean(Number value) {
return value.doubleValue() != 0;
}

/**
* Used from the bytecode generated by {@link #compilePOJOSetter(Method, ClickHouseColumn)} to set a
* {@code boolean} field from a numeric column.
*
* @param value numeric value read from the stream
* @return {@code false} when the value is zero, {@code true} otherwise
*/
public static boolean numberToBoolean(double value) {
return value != 0;
}

public static boolean convertToBoolean(Object value) {
if (value instanceof Boolean) {
return (Boolean) value;
Expand Down Expand Up @@ -1172,10 +1198,11 @@ public static List<?> convertArrayValueToList(Object value) {
* @see SerializerUtils#stringValueToString(Object)
* @see SerializerUtils#stringValueToByteArray(Object)
* @see SerializerUtils#binaryReaderMethodForType(MethodVisitor, Class, ClickHouseDataType)
* @see SerializerUtils#objectToNumber(Object)
* @see SerializerUtils#convertToBoolean(Object)
* @see SerializerUtils#numberToBoolean(double)
* @see SerializerUtils#emitPrimitiveConversion(MethodVisitor, Class, Class)
* @see SerializerUtils#intToOpcode(Class)
* @see SerializerUtils#longToOpcode(Class)
* @see SerializerUtils#floatToOpcode(Class)
* @see SerializerUtils#doubleToOpcode(Class)
* @see BinaryStreamReader#readValue(ClickHouseColumn, Class)
* @see BinaryStreamReader#readByte()
* @see BinaryStreamReader#readUnsignedByte()
Expand Down Expand Up @@ -1225,8 +1252,6 @@ public static POJOFieldDeserializer compilePOJOSetter(Method setterMethod, Click
Type.getType(ClickHouseColumn.class)), null, new String[]{"java/io/IOException"});

Class<?> targetType = setterMethod.getParameterTypes()[0];
Class<?> targetPrimitiveType = ClickHouseDataType.toPrimitiveType(targetType); // will return object class if no primitive


mv.visitCode();
mv.visitVarInsn(ALOAD, 1); // load target object
Expand All @@ -1235,14 +1260,50 @@ public static POJOFieldDeserializer compilePOJOSetter(Method setterMethod, Click

if (targetType.isPrimitive() && BinaryStreamReader.isReadToPrimitive(column.getDataType())) {
binaryReaderMethodForType(mv,
targetPrimitiveType, column.getDataType());
targetType, column.getDataType());
} else if (targetType.isPrimitive() && column.getDataType() == ClickHouseDataType.UInt64) {
mv.visitTypeInsn(CHECKCAST, Type.getInternalName(BigInteger.class));
mv.visitMethodInsn(INVOKEVIRTUAL,
Type.getInternalName(BigInteger.class),
targetType.getSimpleName() + "Value",
"()" + Type.getDescriptor(targetType),
false);
} else if (targetType.isPrimitive()) {
// The reader has no primitive read method for this column type, so the value is read as an object
// and unboxed. A primitive class cannot be loaded as a class constant, hence the wrapper class is
// passed as the type hint.
mv.visitVarInsn(ALOAD, 3); // column
mv.visitLdcInsn(Type.getType(ClickHouseDataType.toObjectType(targetType)));
mv.visitMethodInsn(INVOKEVIRTUAL,
Type.getInternalName(BinaryStreamReader.class),
"readValue",
Type.getMethodDescriptor(
Type.getType(Object.class),
Type.getType(ClickHouseColumn.class),
Type.getType(Class.class)),
false);

if (targetType == boolean.class) {
mv.visitMethodInsn(INVOKESTATIC,
Type.getInternalName(SerializerUtils.class),
"convertToBoolean",
Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)),
false);
} else {
mv.visitMethodInsn(INVOKESTATIC,
Type.getInternalName(SerializerUtils.class),
"objectToNumber",
Type.getMethodDescriptor(Type.getType(Number.class), Type.getType(Object.class)),
false);
// Number has no charValue() so a char field is read as an int and narrowed
Class<?> numberValueType = targetType == char.class ? int.class : targetType;
mv.visitMethodInsn(INVOKEVIRTUAL,
Type.getInternalName(Number.class),
numberValueType.getSimpleName() + "Value",
Type.getMethodDescriptor(Type.getType(numberValueType)),
false);
emitPrimitiveConversion(mv, numberValueType, targetType);
}
} else {
mv.visitVarInsn(ALOAD, 3); // column
// load target class into stack
Expand Down Expand Up @@ -1306,69 +1367,61 @@ public static POJOFieldDeserializer compilePOJOSetter(Method setterMethod, Click
}

private static void binaryReaderMethodForType(MethodVisitor mv, Class<?> targetType, ClickHouseDataType dataType) {
String readerMethod = null;
String readerMethodReturnType = null;
int convertOpcode = -1;
String readerMethod;
Class<?> readerReturnType;

switch (dataType) {
case Int8:
readerMethod = "readByte";
readerMethodReturnType = Type.getDescriptor(byte.class);
readerReturnType = byte.class;
break;
case UInt8:
readerMethod = "readUnsignedByte";
readerMethodReturnType = Type.getDescriptor(short.class);
readerReturnType = short.class;
break;
case Int16:
readerMethod = "readShortLE";
readerMethodReturnType = Type.getDescriptor(short.class);
readerReturnType = short.class;
break;
case UInt16:
readerMethod = "readUnsignedShortLE";
readerMethodReturnType = Type.getDescriptor(int.class);
convertOpcode = intToOpcode(targetType);
readerReturnType = int.class;
break;
case Int32:
readerMethod = "readIntLE";
readerMethodReturnType = Type.getDescriptor(int.class);
convertOpcode = intToOpcode(targetType);
readerReturnType = int.class;
break;
case UInt32:
readerMethod = "readUnsignedIntLE";
readerMethodReturnType = Type.getDescriptor(long.class);
convertOpcode = longToOpcode(targetType);
readerReturnType = long.class;
break;
case Int64:
readerMethod = "readLongLE";
readerMethodReturnType = Type.getDescriptor(long.class);
convertOpcode = longToOpcode(targetType);
readerReturnType = long.class;
break;
case BFloat16:
readerMethod = "readBFloat16LE";
readerMethodReturnType = Type.getDescriptor(float.class);
convertOpcode = floatToOpcode(targetType);
readerReturnType = float.class;
break;
case Float32:
readerMethod = "readFloatLE";
readerMethodReturnType = Type.getDescriptor(float.class);
convertOpcode = floatToOpcode(targetType);
readerReturnType = float.class;
break;
case Float64:
readerMethod = "readDoubleLE";
readerMethodReturnType = Type.getDescriptor(double.class);
convertOpcode = doubleToOpcode(targetType);
readerReturnType = double.class;
break;
case Enum8:
readerMethod = "readByte";
readerMethodReturnType = Type.getDescriptor(byte.class);
readerReturnType = byte.class;
break;
case Enum16:
readerMethod = "readShortLE";
readerMethodReturnType = Type.getDescriptor(short.class);
readerReturnType = short.class;
break;
case Bool:
readerMethod = "readByte";
readerMethodReturnType = Type.getDescriptor(byte.class);
readerReturnType = byte.class;
break;
default:
throw new ClientException("Column type '" + dataType + "' cannot be set to a primitive type '" + targetType + "'");
Expand All @@ -1377,8 +1430,69 @@ private static void binaryReaderMethodForType(MethodVisitor mv, Class<?> targetT
mv.visitMethodInsn(INVOKEVIRTUAL,
Type.getInternalName(BinaryStreamReader.class),
readerMethod,
"()" +readerMethodReturnType,
Type.getMethodDescriptor(Type.getType(readerReturnType)),
false);
emitPrimitiveConversion(mv, readerReturnType, targetType);
}

/**
* Emits the conversion of the value currently on the operand stack from {@code sourceType} to
* {@code targetType}. Both types are primitive; the conversion follows Java narrowing/widening rules, so a
* wider value is first narrowed to {@code int} and only then to {@code byte}, {@code short} or {@code char}.
* Without this the generated class does not verify, because the value left on the stack does not match the
* setter descriptor.
*
* @param mv method visitor to write instructions to
* @param sourceType primitive type of the value on the stack
* @param targetType primitive type expected by the setter
*/
private static void emitPrimitiveConversion(MethodVisitor mv, Class<?> sourceType, Class<?> targetType) {
if (sourceType == targetType) {
return;
}

if (targetType == boolean.class) {
// there is no opcode converting a number into 0/1
emitPrimitiveConversion(mv, sourceType, double.class);
mv.visitMethodInsn(INVOKESTATIC,
Type.getInternalName(SerializerUtils.class),
"numberToBoolean",
Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.DOUBLE_TYPE),
false);
return;
}

// byte, short, char, int (and boolean) values are all kept as int on the operand stack
if (sourceType == long.class) {
if (targetType == float.class) {
mv.visitInsn(Opcodes.L2F);
return;
} else if (targetType == double.class) {
mv.visitInsn(Opcodes.L2D);
return;
}
mv.visitInsn(Opcodes.L2I);
} else if (sourceType == float.class) {
if (targetType == long.class) {
mv.visitInsn(Opcodes.F2L);
return;
} else if (targetType == double.class) {
mv.visitInsn(Opcodes.F2D);
return;
}
mv.visitInsn(Opcodes.F2I);
} else if (sourceType == double.class) {
if (targetType == long.class) {
mv.visitInsn(Opcodes.D2L);
return;
} else if (targetType == float.class) {
mv.visitInsn(Opcodes.D2F);
return;
}
mv.visitInsn(Opcodes.D2I);
}

int convertOpcode = intToOpcode(targetType);
if (convertOpcode != -1) {
mv.visitInsn(convertOpcode);
}
Expand All @@ -1401,39 +1515,6 @@ private static int intToOpcode(Class<?> targetType) {
return -1;
}

private static int longToOpcode(Class<?> targetType) {
if (targetType == int.class) {
return Opcodes.L2I;
} else if (targetType == float.class) {
return Opcodes.L2F;
} else if (targetType == double.class) {
return Opcodes.L2D;
}
return -1;
}

private static int floatToOpcode(Class<?> targetType) {
if (targetType == int.class) {
return Opcodes.F2I;
} else if (targetType == long.class) {
return Opcodes.F2L;
} else if (targetType == double.class) {
return Opcodes.F2D;
}
return -1;
}

private static int doubleToOpcode(Class<?> targetType) {
if (targetType == int.class) {
return Opcodes.D2I;
} else if (targetType == long.class) {
return Opcodes.D2L;
} else if (targetType == float.class) {
return Opcodes.D2F;
}
return -1;
}

public static class DynamicClassLoader extends ClassLoader {

public DynamicClassLoader(ClassLoader classLoader) {
Expand Down
Loading
Loading