From da0de1891966e0aebfe5e5386c80cc1fb0b7bad7 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 25 Nov 2025 10:58:05 -0800 Subject: [PATCH 01/15] Added array result set --- .../internal/ValueConverters.java | 94 ++ .../clickhouse/jdbc/types/ArrayResultSet.java | 1205 +++++++++++++++++ .../clickhouse/jdbc/ResultSetImplTest.java | 25 + .../jdbc/types/ArrayResultSetTest.java | 124 ++ 4 files changed, 1448 insertions(+) create mode 100644 client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java create mode 100644 jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java create mode 100644 jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java new file mode 100644 index 000000000..117ac0648 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -0,0 +1,94 @@ +package com.clickhouse.client.api.data_formats.internal; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class ValueConverters { + + // Boolean to any + public Number convertBooleanToNumber(Object value) { + return ((Boolean)value) ? 1 : 0; + } + + public String convertBooleanToString(Object value) { + return String.valueOf(value); + } + + // String to any + public String convertStringToString(Object value) { + return (String) value; + } + + public byte[] convertStringToBytes(Object value) { + return ((String) value).getBytes(); + } + + public boolean convertStringToBoolean(Object value) { + return Boolean.parseBoolean((String) value); + } + + public byte convertStringToByte(Object value) { + return Byte.parseByte((String) value); + } + + public short convertStringToShort(Object value) { + return Short.parseShort((String) value); + } + + public int convertStringToInt(Object value) { + return Integer.parseInt((String) value); + } + + public long convertStringToLong(Object value) { + return Long.parseLong((String) value); + } + + public float convertStringToFloat(Object value) { + return Float.parseFloat((String) value); + } + + public double convertStringToDouble(Object value) { + return Double.parseDouble((String) value); + } + + // Number to any + public String convertNumberToString(Object value) { + return String.valueOf(value); + } + + public boolean convertNumberToBoolean(Object value) { + return ((Number) value).floatValue() != 0.0f; + } + + public byte convertNumberToByte(Object value) { + return ((Number) value).byteValue(); + } + + public short convertNumberToShort(Object value) { + return ((Number) value).shortValue(); + } + + public int convertNumberToInt(Object value) { + return ((Number) value).intValue(); + } + + public long convertNumberToLong(Object value) { + return ((Number) value).longValue(); + } + + public float convertNumberToFloat(Object value) { + return ((Number) value).floatValue(); + } + + public double convertNumberToDouble(Object value) { + return ((Number) value).doubleValue(); + } + + public BigInteger convertNumberToBigInteger(Object value) { + return BigInteger.valueOf(((Number) value).longValue()); + } + + public BigDecimal convertNumberToBigDecimal(Object value) { + return BigDecimal.valueOf(((Number) value).doubleValue()); + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java new file mode 100644 index 000000000..bf272cbc4 --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -0,0 +1,1205 @@ +package com.clickhouse.jdbc.types; + +import com.clickhouse.client.api.data_formats.internal.ValueConverters; +import com.clickhouse.data.ClickHouseColumn; +import com.clickhouse.data.ClickHouseDataType; +import com.clickhouse.jdbc.internal.JdbcUtils; +import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class ArrayResultSet implements ResultSet { + + private final Object[] array; + private final int length; + private Integer pos; + private boolean closed; + private ResultSetMetaDataImpl metadata; + + private static final ClickHouseColumn INDEX_COLUMN = ClickHouseColumn.of("INDEX", ClickHouseDataType.UInt32, false, 0, 0); + private static final String VALUE_COLUMN = "VALUE"; + private int fetchDirection = ResultSet.FETCH_FORWARD; + private int fetchSize = 0; + private boolean wasNull = false; + private final Map, Function> converterMap; + + public ArrayResultSet(Object[] array, ClickHouseColumn column) { + this.array = array; + this.length = java.lang.reflect.Array.getLength(array); + this.pos = -1; + this.converterMap = new HashMap<>(); + + List nestedColumns = column.getNestedColumns(); + ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1? column.getArrayBaseColumn() : nestedColumns.get(0); + this.metadata = new ResultSetMetaDataImpl(Arrays.asList(INDEX_COLUMN, valueColumn) + , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP); + + if (array.length > 1) { + ValueConverters converters = new ValueConverters(); + converterMap.put(Object.class, o -> o); // default conversion + if (array[0] instanceof Number) { + converterMap.put(Byte.class, converters::convertNumberToByte); + converterMap.put(Short.class, converters::convertNumberToShort); + converterMap.put(Integer.class, converters::convertNumberToInt); + converterMap.put(Long.class, converters::convertNumberToLong); + converterMap.put(Float.class, converters::convertNumberToFloat); + converterMap.put(Double.class, converters::convertNumberToDouble); + converterMap.put(BigInteger.class, converters::convertNumberToBigInteger); + converterMap.put(BigDecimal.class, converters::convertNumberToBigDecimal); + converterMap.put(Boolean.class, converters::convertNumberToBoolean); + converterMap.put(String.class, converters::convertNumberToString); + } else if (array[0] instanceof Boolean) { + converterMap.put(Boolean.class, converters::convertBooleanToNumber); + converterMap.put(String.class, converters::convertBooleanToString); + } else if (array[0] instanceof String) { + converterMap.put(String.class, converters::convertStringToString); + converterMap.put(Boolean.class, converters::convertStringToBoolean); + converterMap.put(byte[].class, converters::convertStringToBytes); + } + } + } + + @Override + public boolean next() throws SQLException { + if (pos == length || length == 0) { + return false; + } + pos++; + return true; + } + + private void checkColumnIndex(int columnIndex) throws SQLException { + if (columnIndex < 1 || columnIndex > length) { + throw new SQLException("Invalid column index: " + columnIndex); + } + } + + + private void checkRowPosition() throws SQLException { + if (pos < 0 || pos >= length) { + throw new SQLException("No current row"); + } + } + + private Object getValueAsObject(int columnIndex, Class type, Object defaultValue) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + return pos; + } + Object value = array[pos]; + if (value != null) { + Function converter = converterMap.get(type); + if (converter != null) { + value = converter.apply(value); + } else { + throw new SQLException("Value cannot be converted to " + type); + } + } + wasNull = value == null; + return value == null ? defaultValue : value; + } + + + private String getValueAsString(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + return String.valueOf(pos); + } + return String.valueOf(array[pos]); + } + + @Override + public void close() throws SQLException { + this.closed = true; + } + + @Override + public boolean wasNull() throws SQLException { + boolean tmp = wasNull; + wasNull = false; + return tmp; + } + + @Override + public String getString(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + return getValueAsString(columnIndex); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as boolean"); + } + return (Boolean) getValueAsObject(columnIndex, Boolean.class, false); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + if (columnIndex == 1) { + if (pos < Byte.MAX_VALUE) { + return (byte) getRow(); + } else { + throw new SQLException("INDEX column value too big and cannot be get as byte"); + } + } + return (Byte) getValueAsObject(columnIndex, Byte.class, 0); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + if (columnIndex == 1) { + if (pos < Short.MAX_VALUE) { + return (short) getRow(); + } else { + throw new SQLException("INDEX column value too big and cannot be get as short"); + } + } + return (Short) getValueAsObject(columnIndex, Short.class, 0); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + if (columnIndex == 1) { + return getRow(); + } + return (Integer) getValueAsObject(columnIndex, Integer.class, 0); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + if (columnIndex == 1) { + return getRow(); + } + return (Long) getValueAsObject(columnIndex, Long.class, 0L); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + if (columnIndex == 1) { + return (float) getRow(); + } + return (Float) getValueAsObject(columnIndex, Float.class, 0.0f); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + if (columnIndex == 1) { + return getRow(); + } + return (Double) getValueAsObject(columnIndex, Double.class, 0.0d); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + if (columnIndex == 1) { + return BigDecimal.valueOf(getRow()); + } + return (BigDecimal) getValueAsObject(columnIndex, BigDecimal.class, null); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as bytes"); + } + return (byte[])getValueAsObject(columnIndex, byte[].class, null); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as date"); + } + return (Date) getValueAsObject(columnIndex, Date.class, null); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as time"); + } + return (Time) getValueAsObject(columnIndex, Time.class, null); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as timestamp"); + } + return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as ascii stream"); + } + throw new SQLFeatureNotSupportedException("getAsciiStream is not implemented"); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as unicode stream"); + } + throw new SQLFeatureNotSupportedException("getUnicodeStream is not implemented"); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as binary stream"); + } + throw new SQLFeatureNotSupportedException("getBinaryStream is not implemented"); + } + + private int getColumnIndex(String columnLabel) throws SQLException { + if (columnLabel.equalsIgnoreCase(INDEX_COLUMN.getColumnName())) { + return 1; + } + if (columnLabel.equalsIgnoreCase(VALUE_COLUMN)) { + return 2; + } + + throw new SQLException("Unknown column label `" + columnLabel + "`"); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return getString(getColumnIndex(columnLabel)); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(getColumnIndex(columnLabel)); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(getColumnIndex(columnLabel)); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(getColumnIndex(columnLabel)); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(getColumnIndex(columnLabel)); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(getColumnIndex(columnLabel)); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(getColumnIndex(columnLabel)); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getDouble(getColumnIndex(columnLabel)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(getColumnIndex(columnLabel), scale); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return getBytes(getColumnIndex(columnLabel)); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + return getDate(getColumnIndex(columnLabel)); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(getColumnIndex(columnLabel)); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(getColumnIndex(columnLabel)); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + return null; + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + return null; + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + return null; + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public String getCursorName() throws SQLException { + return ""; + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return metadata; + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return getValueAsObject(columnIndex, Object.class, null); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(getColumnIndex(columnLabel)); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return getColumnIndex(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return null; + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return null; + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as big decimal"); + } + return (BigDecimal) getValueAsObject(columnIndex, BigDecimal.class, null); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getBigDecimal(getColumnIndex(columnLabel)); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return pos == -1; + } + + @Override + public boolean isAfterLast() throws SQLException { + return pos >= length; + } + + @Override + public boolean isFirst() throws SQLException { + return pos == 0; + } + + @Override + public boolean isLast() throws SQLException { + return pos == length - 1; + } + + @Override + public void beforeFirst() throws SQLException { + pos = -1; + } + + @Override + public void afterLast() throws SQLException { + pos = length; + } + + @Override + public boolean first() throws SQLException { + if (length > 0) { + pos = 0; + return true; + } + return false; + } + + @Override + public boolean last() throws SQLException { + if (length > 0) { + pos = length - 1; + return true; + } + return false; + } + + @Override + public int getRow() throws SQLException { + if (isBeforeFirst() || isAfterLast()) { + return 0; + } + return pos + 1; + } + + @Override + public boolean absolute(int row) throws SQLException { + pos = row - 1; + return !(isAfterLast() || isBeforeFirst()); + } + + @Override + public boolean relative(int rows) throws SQLException { + return absolute((pos + 1) + rows); + } + + @Override + public boolean previous() throws SQLException { + return absolute(pos); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + if (!(direction == FETCH_FORWARD || direction == FETCH_REVERSE || direction == FETCH_UNKNOWN)) { + throw new SQLException("Invalid fetch direction: " + direction + ". Should be one of [ResultSet.FETCH_FORWARD, ResultSet.FETCH_REVERSE, ResultSet.FETCH_UNKNOWN]"); + } + this.fetchDirection = direction; + } + + @Override + public int getFetchDirection() throws SQLException { + return fetchDirection; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + // ignored as we fetched array already + this.fetchSize = rows; + } + + @Override + public int getFetchSize() throws SQLException { + return fetchSize; + } + + @Override + public int getType() throws SQLException { + return ResultSet.TYPE_SCROLL_INSENSITIVE; + } + + @Override + public int getConcurrency() throws SQLException { + return ResultSet.CONCUR_READ_ONLY; + } + + @Override + public boolean rowUpdated() throws SQLException { + return false; + } + + @Override + public boolean rowInserted() throws SQLException { + return false; + } + + @Override + public boolean rowDeleted() throws SQLException { + return false; + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + + } + + private void throwReadOnlyException() throws SQLException { + throw new SQLException("ResultSet is read-only"); + } + + @Override + public void insertRow() throws SQLException { + throwReadOnlyException(); + } + + @Override + public void updateRow() throws SQLException { + throwReadOnlyException(); + } + + @Override + public void deleteRow() throws SQLException { + throwReadOnlyException(); + } + + @Override + public void refreshRow() throws SQLException { + throw new SQLFeatureNotSupportedException("refreshRow is not supported on ResultSet produced from Array object"); + } + + @Override + public void cancelRowUpdates() throws SQLException { + + } + + @Override + public void moveToInsertRow() throws SQLException { + throwReadOnlyException(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + throwReadOnlyException(); + } + + @Override + public Statement getStatement() throws SQLException { + return null; // null as it is produced from an Array object + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + return null; + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return null; + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + return null; + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + return null; + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + return null; + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return null; + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + return null; + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + return null; + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as date"); + } + return null; + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return getDate(getColumnIndex(columnLabel), cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + if (columnIndex == 1) { + throw new SQLException("INDEX column cannot be get as time"); + } + return null; + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return getTime(getColumnIndex(columnLabel), cal); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return getTimestamp(getColumnIndex(columnLabel), cal); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + return (URL) getValueAsObject(columnIndex, URL.class, null); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + return getURL(getColumnIndex(columnLabel)); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + return null; + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + return null; + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + + } + + @Override + public int getHoldability() throws SQLException { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return null; + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return null; + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + return null; + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + return null; + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + + } + + @Override + public String getNString(int columnIndex) throws SQLException { + return ""; + } + + @Override + public String getNString(String columnLabel) throws SQLException { + return ""; + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return null; + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return null; + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + if (columnIndex == 1) { + if (Number.class.isAssignableFrom(type)) { + return (T) pos; + } else if (String.class.isAssignableFrom(type)) { + return (T) String.valueOf(pos); + } else { + throw new SQLException("INDEX column cannot be converted to non-number value"); + } + } + return null; + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return getObject(getColumnIndex(columnLabel), type); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } +} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java index a97e4d492..23e8104af 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java @@ -360,4 +360,29 @@ public void testGetMetadata() throws SQLException { } } } + + @Test + public void testGetResultSetFromArray() throws Exception { + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("select [1, 2, 3, 4]::Array(UInt16) as v")) { + assertTrue(rs.next()); + + Array array = rs.getArray("v"); + Assert.assertNotNull(array); + Assert.assertEquals(array.getBaseType(), Types.INTEGER); + Assert.assertEquals(array.getBaseTypeName(), "UInt16"); + + Integer[] array2 = (Integer[]) array.getArray(); + + ResultSet rs2 = array.getResultSet(); + Assert.assertTrue(rs2.isBeforeFirst()); + Assert.assertFalse(rs2.isAfterLast()); + for (int i = 0; i < array2.length; i++) { + rs2.next(); + Assert.assertEquals(rs2.getInt(1), array2[i]); + } + } + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java new file mode 100644 index 000000000..3b607122f --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -0,0 +1,124 @@ +package com.clickhouse.jdbc.types; + +import com.clickhouse.data.ClickHouseColumn; +import org.testng.annotations.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.testng.Assert.*; + +@Test(groups = {"unit"}) +public class ArrayResultSetTest { + + @Test + void testCursorNavigation() throws SQLException { + Integer[] array = {1, 2, 3, 4, 5}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); + + assertEquals(rs.getFetchDirection(), ResultSet.FETCH_FORWARD); + rs.setFetchDirection(ResultSet.FETCH_REVERSE); + assertEquals(rs.getFetchDirection(), ResultSet.FETCH_REVERSE); + assertThrows(SQLException.class, () -> rs.setFetchDirection(123)); + assertEquals(rs.getType(), ResultSet.TYPE_SCROLL_INSENSITIVE); + assertEquals(rs.getConcurrency(), ResultSet.CONCUR_READ_ONLY); + rs.setFetchSize(10000); + assertEquals(rs.getFetchSize(), 10000); + assertEquals(rs.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + assertFalse(rs.isClosed()); + + assertTrue(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertFalse(rs.isFirst()); + assertFalse(rs.isLast()); + assertThrows(SQLException.class, () -> rs.getShort(2)); + + rs.next(); + + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertTrue(rs.isFirst()); + assertFalse(rs.isLast()); + + assertEquals(rs.getShort(1), 1); // INDEX + assertEquals(rs.getShort(2), array[0].shortValue()); // VALUE + assertEquals(rs.getInt(1), rs.getRow()); + + assertTrue(rs.relative(2)); + assertEquals(rs.getRow(), 3); // INDEX + assertEquals(rs.getShort(2), array[2].shortValue()); // VALUE + + assertTrue(rs.previous()); + assertEquals(rs.getRow(), 2); // INDEX + assertEquals(rs.getShort(2), array[1].shortValue()); // VALUE + + + assertTrue(rs.absolute(array.length)); // INDEX - last element + assertEquals(rs.getRow(), array.length); // INDEX + assertEquals(rs.getShort(2), array[array.length - 1].shortValue()); // VALUE + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertFalse(rs.isFirst()); + assertTrue(rs.isLast()); + + + assertFalse(rs.relative(2)); + assertEquals(rs.getRow(), 0); // INDEX + assertFalse(rs.isBeforeFirst()); + assertTrue(rs.isAfterLast()); + assertFalse(rs.isFirst()); + assertFalse(rs.isLast()); + + rs.first(); + assertEquals(rs.getRow(), 1); // INDEX + assertEquals(rs.getShort(2), array[0].shortValue()); // VALUE + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertTrue(rs.isFirst()); + assertFalse(rs.isLast()); + + rs.last(); + assertEquals(rs.getRow(), array.length); // INDEX + assertEquals(rs.getShort(2), array[array.length - 1].shortValue()); // VALUE + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertFalse(rs.isFirst()); + assertTrue(rs.isLast()); + + rs.beforeFirst(); + assertEquals(rs.getRow(), 0); // INDEX + assertTrue(rs.isBeforeFirst()); + assertFalse(rs.isAfterLast()); + assertFalse(rs.isFirst()); + assertFalse(rs.isLast()); + + rs.afterLast(); + assertEquals(rs.getRow(), 0); // INDEX + assertTrue(rs.isAfterLast()); + assertFalse(rs.isBeforeFirst()); + assertFalse(rs.isFirst()); + assertFalse(rs.isLast()); + + assertFalse(rs.next()); + assertThrows(SQLException.class, () -> rs.getShort(2)); + + rs.close(); + assertTrue(rs.isClosed()); + } + + @Test + void testNullValues() throws SQLException { + Integer[] array = {1, null, 3, 4, 5}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); + + rs.next(); + assertFalse(rs.wasNull()); + assertEquals(rs.getInt(2), array[0]); + assertFalse(rs.wasNull()); + + rs.next(); + assertFalse(rs.wasNull()); + assertEquals(rs.getInt(2), 0); + assertTrue(rs.wasNull()); + } +} \ No newline at end of file From 8408fdd89dd7918731df219c7b7185a0d9e14f5b Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 2 Dec 2025 12:53:18 -0800 Subject: [PATCH 02/15] added creating result set from array --- .../main/java/com/clickhouse/jdbc/types/Array.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java index fc61feebd..ac82a3fd2 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/Array.java @@ -22,6 +22,7 @@ public class Array implements java.sql.Array { private final String elementTypeName; private boolean valid; private final ClickHouseDataType baseDataType; + private ArrayResultSet arrayResultSet; public Array(ClickHouseColumn column, Object[] elements) throws SQLException { this.column = column; @@ -88,8 +89,12 @@ public Object getArray(long index, int count, Map> map) throws } @Override - public ResultSet getResultSet() throws SQLException { - throw new SQLFeatureNotSupportedException("getResultSet() is not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + public synchronized ResultSet getResultSet() throws SQLException { + ensureValid(); + if (arrayResultSet == null) { + arrayResultSet = new ArrayResultSet(array, column); + } + return arrayResultSet; } @Override @@ -99,7 +104,8 @@ public ResultSet getResultSet(Map> map) throws SQLException { @Override public ResultSet getResultSet(long index, int count) throws SQLException { - throw new SQLFeatureNotSupportedException("getResultSet(long, int) is not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + ensureValid(); + return new ArrayResultSet(getArray(index, count), column); } @Override From 5b557ff15b0a46a44bf89f177f37ba8a26158ce3 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Thu, 4 Dec 2025 16:19:34 -0800 Subject: [PATCH 03/15] Implemented conversion logic for array of primitives and nested arrays. Tested. --- .../internal/ValueConverters.java | 115 ++++++++++++++++- .../clickhouse/jdbc/internal/JdbcUtils.java | 4 +- .../clickhouse/jdbc/types/ArrayResultSet.java | 118 ++++++++++-------- .../jdbc/types/ArrayResultSetTest.java | 96 ++++++++++++++ 4 files changed, 276 insertions(+), 57 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java index 117ac0648..08c5a00d1 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -1,13 +1,115 @@ package com.clickhouse.client.api.data_formats.internal; +import com.google.common.collect.ImmutableMap; + import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; public final class ValueConverters { + + // > + private final Map, Map, Function>> classConverters; + + // + private final ImmutableMap, Function> numberConverters; + + public ValueConverters() { + + + ImmutableMap.Builder, Function> numberConvertersBuilder = ImmutableMap.builder(); + numberConvertersBuilder.put(String.class, this::convertNumberToString); + numberConvertersBuilder.put(Boolean.class, this::convertNumberToBoolean); + numberConvertersBuilder.put(byte.class, this::convertNumberToByte); + numberConvertersBuilder.put(short.class, this::convertNumberToShort); + numberConvertersBuilder.put(int.class, this::convertNumberToInt); + numberConvertersBuilder.put(long.class, this::convertNumberToLong); + numberConvertersBuilder.put(float.class, this::convertNumberToFloat); + numberConvertersBuilder.put(double.class, this::convertNumberToDouble); + numberConvertersBuilder.put(Byte.class, this::convertNumberToByte); + numberConvertersBuilder.put(Short.class, this::convertNumberToShort); + numberConvertersBuilder.put(Integer.class, this::convertNumberToInt); + numberConvertersBuilder.put(Long.class, this::convertNumberToLong); + numberConvertersBuilder.put(Float.class, this::convertNumberToFloat); + numberConvertersBuilder.put(Double.class, this::convertNumberToDouble); + numberConvertersBuilder.put(BigInteger.class, this::convertNumberToBigInteger); + numberConvertersBuilder.put(BigDecimal.class, this::convertNumberToBigDecimal); + + numberConverters = numberConvertersBuilder.build(); + + + ImmutableMap.Builder, Map, Function>> mapBuilder = ImmutableMap.builder(); + + mapBuilder.put(byte.class, numberConverters); + mapBuilder.put(short.class, numberConverters); + mapBuilder.put(int.class, numberConverters); + mapBuilder.put(long.class, numberConverters); + mapBuilder.put(float.class, numberConverters); + mapBuilder.put(double.class, numberConverters); + mapBuilder.put(Byte.class, numberConverters); + mapBuilder.put(Short.class, numberConverters); + mapBuilder.put(Integer.class, numberConverters); + mapBuilder.put(Long.class, numberConverters); + mapBuilder.put(Float.class, numberConverters); + mapBuilder.put(Double.class, numberConverters); + mapBuilder.put(BigInteger.class, numberConverters); + mapBuilder.put(BigDecimal.class, numberConverters); + + ImmutableMap.Builder, Function> booleanMapBuilder = ImmutableMap.builder(); + booleanMapBuilder.put(byte.class, this::convertBooleanToNumber); + booleanMapBuilder.put(short.class, this::convertBooleanToNumber); + booleanMapBuilder.put(int.class, this::convertBooleanToNumber); + booleanMapBuilder.put(long.class, this::convertBooleanToNumber); + booleanMapBuilder.put(float.class, this::convertBooleanToNumber); + booleanMapBuilder.put(double.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Byte.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Short.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Integer.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Long.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Float.class, this::convertBooleanToNumber); + booleanMapBuilder.put(Double.class, this::convertBooleanToNumber); + booleanMapBuilder.put(BigInteger.class, this::convertBooleanToNumber); + booleanMapBuilder.put(BigDecimal.class, this::convertBooleanToNumber); + booleanMapBuilder.put(String.class, this::convertBooleanToString); + booleanMapBuilder.put(Boolean.class, this::convertBooleanToBoolean); + booleanMapBuilder.put(boolean.class, this::convertBooleanToBoolean); + + mapBuilder.put(Boolean.class, booleanMapBuilder.build()); + mapBuilder.put(boolean.class, booleanMapBuilder.build()); + + ImmutableMap.Builder, Function> stringMapBuilder = ImmutableMap.builder(); + stringMapBuilder.put(byte.class, this::convertStringToByte); + stringMapBuilder.put(short.class, this::convertStringToShort); + stringMapBuilder.put(int.class, this::convertStringToInt); + stringMapBuilder.put(long.class, this::convertStringToLong); + stringMapBuilder.put(float.class, this::convertStringToFloat); + stringMapBuilder.put(double.class, this::convertStringToDouble); + stringMapBuilder.put(Byte.class, this::convertStringToByte); + stringMapBuilder.put(Short.class, this::convertStringToShort); + stringMapBuilder.put(Integer.class, this::convertStringToInt); + stringMapBuilder.put(Long.class, this::convertStringToLong); + stringMapBuilder.put(Float.class, this::convertStringToFloat); + stringMapBuilder.put(Double.class, this::convertStringToDouble); + stringMapBuilder.put(Boolean.class, this::convertStringToBoolean); + stringMapBuilder.put(String.class, this::convertStringToString); + stringMapBuilder.put(byte[].class, this::convertStringToBytes); + mapBuilder.put(String.class, stringMapBuilder.build()); + + classConverters = mapBuilder.build(); + } + // Boolean to any + public Boolean convertBooleanToBoolean(Object value) { + return (Boolean) value; + } + public Number convertBooleanToNumber(Object value) { - return ((Boolean)value) ? 1 : 0; + return ((Number) (((Boolean)value) ? 1 : 0)).longValue(); } public String convertBooleanToString(Object value) { @@ -91,4 +193,15 @@ public BigInteger convertNumberToBigInteger(Object value) { public BigDecimal convertNumberToBigDecimal(Object value) { return BigDecimal.valueOf(((Number) value).doubleValue()); } + + /** + * Returns the converter map for the given source type. + * Map contains target type and converter function. For example, if source type is boolean then map will contain all + * converters that support converting boolean to target type. + * @param type - source type + * @return - map of target type and converter function + */ + public Map, Function> getConvertersForType(Class type) { + return classConverters.getOrDefault(type, Collections.emptyMap()); + } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 81ceb5fa9..78a0e6121 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -108,7 +108,7 @@ private static Map> generateClassMap() { map.put(JDBCType.INTEGER, Integer.class); map.put(JDBCType.BIGINT, Long.class); map.put(JDBCType.REAL, Float.class); - map.put(JDBCType.FLOAT, Double.class); + map.put(JDBCType.FLOAT, Float.class); map.put(JDBCType.DOUBLE, Double.class); map.put(JDBCType.BINARY, byte[].class); map.put(JDBCType.VARBINARY, byte[].class); @@ -454,7 +454,7 @@ public Object getValue(int i) { } } - private static Object[] arrayToObjectArray(Object array) { + public static Object[] arrayToObjectArray(Object array) { if (array == null) { return null; } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index bf272cbc4..568e26ab4 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -9,7 +9,6 @@ import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; -import java.math.BigInteger; import java.net.URL; import java.sql.Array; import java.sql.Blob; @@ -22,6 +21,7 @@ import java.sql.RowId; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLType; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Statement; @@ -29,14 +29,13 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.Calendar; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; public class ArrayResultSet implements ResultSet { - private final Object[] array; + private final Object array; private final int length; private Integer pos; private boolean closed; @@ -47,41 +46,34 @@ public class ArrayResultSet implements ResultSet { private int fetchDirection = ResultSet.FETCH_FORWARD; private int fetchSize = 0; private boolean wasNull = false; - private final Map, Function> converterMap; + private Map, Function> converterMap; - public ArrayResultSet(Object[] array, ClickHouseColumn column) { + private final ClickHouseDataType componentDataType; + private final Class defaultClass; + private final ClickHouseColumn column; + + public ArrayResultSet(Object array, ClickHouseColumn column) { this.array = array; this.length = java.lang.reflect.Array.getLength(array); this.pos = -1; - this.converterMap = new HashMap<>(); + this.column = column; List nestedColumns = column.getNestedColumns(); - ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1? column.getArrayBaseColumn() : nestedColumns.get(0); + ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : nestedColumns.get(0); this.metadata = new ResultSetMetaDataImpl(Arrays.asList(INDEX_COLUMN, valueColumn) , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP); - - if (array.length > 1) { + this.componentDataType = valueColumn.getDataType(); + this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); + if (this.length > 1) { ValueConverters converters = new ValueConverters(); - converterMap.put(Object.class, o -> o); // default conversion - if (array[0] instanceof Number) { - converterMap.put(Byte.class, converters::convertNumberToByte); - converterMap.put(Short.class, converters::convertNumberToShort); - converterMap.put(Integer.class, converters::convertNumberToInt); - converterMap.put(Long.class, converters::convertNumberToLong); - converterMap.put(Float.class, converters::convertNumberToFloat); - converterMap.put(Double.class, converters::convertNumberToDouble); - converterMap.put(BigInteger.class, converters::convertNumberToBigInteger); - converterMap.put(BigDecimal.class, converters::convertNumberToBigDecimal); - converterMap.put(Boolean.class, converters::convertNumberToBoolean); - converterMap.put(String.class, converters::convertNumberToString); - } else if (array[0] instanceof Boolean) { - converterMap.put(Boolean.class, converters::convertBooleanToNumber); - converterMap.put(String.class, converters::convertBooleanToString); - } else if (array[0] instanceof String) { - converterMap.put(String.class, converters::convertStringToString); - converterMap.put(Boolean.class, converters::convertStringToBoolean); - converterMap.put(byte[].class, converters::convertStringToBytes); + Class itemClass = array.getClass().getComponentType(); + if (itemClass == null) { + itemClass = java.lang.reflect.Array.get(array, 0).getClass(); } + converterMap = converters.getConvertersForType(itemClass); + } else { + // empty array - no values to convert + converterMap = null; } } @@ -113,29 +105,24 @@ private Object getValueAsObject(int columnIndex, Class type, Object defaultVa if (columnIndex == 1) { return pos; } - Object value = array[pos]; - if (value != null) { + + Object value = java.lang.reflect.Array.get(array, pos); + if (value != null && type == Array.class) { + ClickHouseColumn nestedColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : column.getNestedColumns().get(0); + return new com.clickhouse.jdbc.types.Array(nestedColumn, JdbcUtils.arrayToObjectArray(value)); + } else if (value != null && type != Object.class) { + // if there is something to convert. type == Object.class means no conversion Function converter = converterMap.get(type); if (converter != null) { value = converter.apply(value); } else { - throw new SQLException("Value cannot be converted to " + type); + throw new SQLException("Value of " + value.getClass() + " cannot be converted to " + type); } } wasNull = value == null; return value == null ? defaultValue : value; } - - private String getValueAsString(int columnIndex) throws SQLException { - checkColumnIndex(columnIndex); - checkRowPosition(); - if (columnIndex == 1) { - return String.valueOf(pos); - } - return String.valueOf(array[pos]); - } - @Override public void close() throws SQLException { this.closed = true; @@ -151,7 +138,7 @@ public boolean wasNull() throws SQLException { @Override public String getString(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); - return getValueAsString(columnIndex); + return (String) getValueAsObject(columnIndex, String.class, null); } @Override @@ -171,7 +158,7 @@ public byte getByte(int columnIndex) throws SQLException { throw new SQLException("INDEX column value too big and cannot be get as byte"); } } - return (Byte) getValueAsObject(columnIndex, Byte.class, 0); + return ((Number) getValueAsObject(columnIndex, Byte.class, 0)).byteValue(); } @Override @@ -183,7 +170,7 @@ public short getShort(int columnIndex) throws SQLException { throw new SQLException("INDEX column value too big and cannot be get as short"); } } - return (Short) getValueAsObject(columnIndex, Short.class, 0); + return ((Number) getValueAsObject(columnIndex, Short.class, 0)).shortValue(); } @Override @@ -191,7 +178,7 @@ public int getInt(int columnIndex) throws SQLException { if (columnIndex == 1) { return getRow(); } - return (Integer) getValueAsObject(columnIndex, Integer.class, 0); + return ((Number) getValueAsObject(columnIndex, Integer.class, 0)).intValue(); } @Override @@ -199,7 +186,7 @@ public long getLong(int columnIndex) throws SQLException { if (columnIndex == 1) { return getRow(); } - return (Long) getValueAsObject(columnIndex, Long.class, 0L); + return ((Number) getValueAsObject(columnIndex, Long.class, 0L)).longValue(); } @Override @@ -207,7 +194,7 @@ public float getFloat(int columnIndex) throws SQLException { if (columnIndex == 1) { return (float) getRow(); } - return (Float) getValueAsObject(columnIndex, Float.class, 0.0f); + return ((Number) getValueAsObject(columnIndex, Float.class, 0.0f)).floatValue(); } @Override @@ -215,7 +202,7 @@ public double getDouble(int columnIndex) throws SQLException { if (columnIndex == 1) { return getRow(); } - return (Double) getValueAsObject(columnIndex, Double.class, 0.0d); + return ((Number) getValueAsObject(columnIndex, Double.class, 0.0d)).doubleValue(); } @Override @@ -231,7 +218,7 @@ public byte[] getBytes(int columnIndex) throws SQLException { if (columnIndex == 1) { throw new SQLException("INDEX column cannot be get as bytes"); } - return (byte[])getValueAsObject(columnIndex, byte[].class, null); + return (byte[]) getValueAsObject(columnIndex, byte[].class, null); } @Override @@ -259,7 +246,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { if (columnIndex == 1) { throw new SQLException("INDEX column cannot be get as timestamp"); } - return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); + return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); } @Override @@ -405,7 +392,7 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public Object getObject(int columnIndex) throws SQLException { - return getValueAsObject(columnIndex, Object.class, null); + return getObject(columnIndex, defaultClass); } @Override @@ -798,7 +785,29 @@ public Statement getStatement() throws SQLException { @Override public Object getObject(int columnIndex, Map> map) throws SQLException { - return null; + Class type = map.get(componentDataType.getName()); + if (type == null) { + SQLType sqlType = JdbcUtils.CLICKHOUSE_TO_SQL_TYPE_MAP.get(componentDataType); + if (sqlType != null) { + type = map.get(sqlType.getName()); + } + + if (type == null) { + // try to find by alias + for (String alias : componentDataType.getAliases()) { + type = map.get(alias); + if (type != null) { + break; + } + } + } + + if (type == null) { + type = defaultClass; + } + } + + return getObject(columnIndex, type); } @Override @@ -823,7 +832,7 @@ public Array getArray(int columnIndex) throws SQLException { @Override public Object getObject(String columnLabel, Map> map) throws SQLException { - return null; + return getObject(getColumnIndex(columnLabel), map); } @Override @@ -1185,7 +1194,8 @@ public T getObject(int columnIndex, Class type) throws SQLException { throw new SQLException("INDEX column cannot be converted to non-number value"); } } - return null; + + return (T) getValueAsObject(columnIndex, type, null); } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 3b607122f..730a9f5d1 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -1,8 +1,10 @@ package com.clickhouse.jdbc.types; import com.clickhouse.data.ClickHouseColumn; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.lang.reflect.Array; import java.sql.ResultSet; import java.sql.SQLException; @@ -121,4 +123,98 @@ void testNullValues() throws SQLException { assertEquals(rs.getInt(2), 0); assertTrue(rs.wasNull()); } + + @Test(dataProvider = "testPrimitiveValues") + void testPrimitiveValues(Object array, ClickHouseColumn column) throws SQLException { + ArrayResultSet rs = new ArrayResultSet(array, column); + + int len = java.lang.reflect.Array.getLength(array); + Class itemClass = array.getClass().getComponentType(); + for (int i = 0; i < len; i++) { + rs.next(); + Object value = Array.get(array, i); + Object actualValue = rs.getObject(2); + assertEquals(actualValue, value, "Actual value is " + actualValue.getClass() + " but expected " + value.getClass()); + if (itemClass.isPrimitive() && (itemClass != Boolean.class && itemClass != boolean.class)) { + assertEquals(rs.getByte(2), ((Number) value).byteValue()); + assertEquals(rs.getShort(2), ((Number) value).shortValue()); + assertEquals(rs.getInt(2), ((Number) value).intValue()); + assertEquals(rs.getLong(2), ((Number) value).longValue()); + assertEquals(rs.getFloat(2), ((Number) value).floatValue()); + assertEquals(rs.getDouble(2), ((Number) value).doubleValue()); + } else if (Number.class.isAssignableFrom(itemClass)) { + assertEquals(rs.getByte(2), ((Number) value).byteValue()); + assertEquals(rs.getShort(2), ((Number) value).shortValue()); + assertEquals(rs.getInt(2), ((Number) value).intValue()); + assertEquals(rs.getLong(2), ((Number) value).longValue()); + assertEquals(rs.getFloat(2), ((Number) value).floatValue()); + assertEquals(rs.getDouble(2), ((Number) value).doubleValue()); + } else if (itemClass == Boolean.class || itemClass == boolean.class) { + Number number = ((Boolean) value) ? 1 : 0; + assertEquals(rs.getByte(2), number.byteValue()); + assertEquals(rs.getShort(2), number.shortValue()); + assertEquals(rs.getInt(2), number.intValue()); + assertEquals(rs.getLong(2), number.longValue()); + assertEquals(rs.getFloat(2), number.floatValue()); + assertEquals(rs.getDouble(2), number.doubleValue()); + } + } + } + + @DataProvider + static Object[][] testPrimitiveValues() { + return new Object[][]{ + {(Object) new boolean[]{true, false, true}, ClickHouseColumn.parse("v Array(Bool)").get(0)}, + {(Object) new byte[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int8)").get(0)}, + {(Object) new short[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int16)").get(0)}, + {(Object) new int[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int32)").get(0)}, + {(Object) new long[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int64)").get(0)}, + {(Object) new double[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Float64)").get(0)}, + {(Object) new float[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Float32)").get(0)}, + {(Object) new String[]{"a", "b", "c"}, ClickHouseColumn.parse("v Array(String)").get(0)}, + {(Object) new Boolean[]{true, false, true}, ClickHouseColumn.parse("v Array(Bool)").get(0)}, + {(Object) new Byte[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int8)").get(0)}, + {(Object) new Short[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int16)").get(0)}, + {(Object) new Integer[]{1, 2, 3}, ClickHouseColumn.parse("v Array(Int32)").get(0)}, + {(Object) new Long[]{1L, 2L, 3L}, ClickHouseColumn.parse("v Array(Int64)").get(0)}, + {(Object) new Float[]{1.0F, 2.0F, 3.0F}, ClickHouseColumn.parse("v Array(Float32)").get(0)}, + {(Object) new Double[]{1.0D, 2.0D, 3.0D}, ClickHouseColumn.parse("v Array(Float64)").get(0)}, + {(Object) new String[]{"a", "b", "c"}, ClickHouseColumn.parse("v Array(String)").get(0)}, + }; + } + + @Test(dataProvider = "testPrimitiveMultiDimensionalValues") + void testPrimitiveMultiDimensionalValues(Object array, ClickHouseColumn column) throws SQLException { + ArrayResultSet rs = new ArrayResultSet(array, column); + + int len = java.lang.reflect.Array.getLength(array); + Class itemClass = array.getClass().getComponentType(); + for (int i = 0; i < len; i++) { + rs.next(); + Object value = Array.get(array, i); + java.sql.Array sqlArray = (java.sql.Array) rs.getObject(2); + assertEquals(sqlArray.getArray(), value); + + ArrayResultSet nestedRs = (ArrayResultSet) sqlArray.getResultSet(); + for (int j = 0; j < len; j++) { + nestedRs.next(); + Object nestedValue = Array.get(value, j); + assertEquals(nestedRs.getObject(2), nestedValue); + } + } + } + + @DataProvider + static Object[][] testPrimitiveMultiDimensionalValues() { + return new Object[][]{ + {(Object) new boolean[][]{new boolean[]{true, false, true}, new boolean[]{true, false, true}}, ClickHouseColumn.parse("v Array(Array(Bool))").get(0)}, + {(Object) new byte[][]{new byte[]{1, 2, 3}, new byte[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Int8))").get(0)}, + {(Object) new short[][]{new short[]{1, 2, 3}, new short[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Int16))").get(0)}, + + {(Object) new int[][]{new int[]{1, 2, 3}, new int[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Int32))").get(0)}, + {(Object) new long[][]{new long[]{1, 2, 3}, new long[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Int64))").get(0)}, + {(Object) new float[][]{new float[]{1, 2, 3}, new float[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Float32))").get(0)}, + {(Object) new double[][]{new double[]{1, 2, 3}, new double[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Float64))").get(0)}, + }; + } } \ No newline at end of file From a814fa36fbed11046892eaf45ce378b89faf6775 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 5 Dec 2025 06:07:27 -0800 Subject: [PATCH 04/15] updated tests where exception expected but now implemented --- jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java | 4 ---- jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java | 4 ++-- .../src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java | 3 ++- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java index 37b5dd1b6..9da719d0e 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java @@ -397,10 +397,6 @@ public void testCreateArray() throws SQLException { assertEquals(arrayValue.getBaseType(), JDBCType.OTHER.getVendorTypeNumber()); assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getArray(null)); assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getArray(0, 1, null)); - assertThrows(SQLFeatureNotSupportedException.class, arrayValue::getResultSet); - assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(0, 1)); - assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(null)); - assertThrows(SQLFeatureNotSupportedException.class, () -> arrayValue.getResultSet(0, 1, null)); Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(-1, 1)); Assert.expectThrows(SQLException.class, () -> arrayValue.getArray(0, -1)); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java index 0f71de053..ee6cfe788 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -820,11 +820,11 @@ public void testFloatTypes() throws SQLException { try (Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT * FROM test_floats ORDER BY order")) { assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), -3.402823E38d); + assertEquals(rs.getObject("float32"), -3.402823E38f); assertEquals(rs.getObject("float64"), Double.valueOf(-1.7976931348623157E308)); assertTrue(rs.next()); - assertEquals(rs.getObject("float32"), 3.402823E38d); + assertEquals(rs.getObject("float32"), 3.402823E38f); assertEquals(rs.getObject("float64"), Double.valueOf(1.7976931348623157E308)); assertTrue(rs.next()); diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java index 23e8104af..8177abacb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java @@ -28,6 +28,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +@Test(groups = "integration") public class ResultSetImplTest extends JdbcIntegrationTest { @Test(groups = "integration") @@ -361,7 +362,7 @@ public void testGetMetadata() throws SQLException { } } - @Test + @Test(groups = {"integration"}) public void testGetResultSetFromArray() throws Exception { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { From 52946439bead8c02cc951988533a09707e72d5c2 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 5 Dec 2025 06:48:01 -0800 Subject: [PATCH 05/15] added tests for unsupported methods. added tests for methods by labels --- .../clickhouse/jdbc/types/ArrayResultSet.java | 170 ++++++++-------- .../jdbc/types/ArrayResultSetTest.java | 190 ++++++++++++++++-- 2 files changed, 256 insertions(+), 104 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 568e26ab4..e632a2181 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -60,7 +60,8 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { List nestedColumns = column.getNestedColumns(); ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : nestedColumns.get(0); - this.metadata = new ResultSetMetaDataImpl(Arrays.asList(INDEX_COLUMN, valueColumn) + this.metadata = new ResultSetMetaDataImpl(Arrays.asList(INDEX_COLUMN, ClickHouseColumn.parse(VALUE_COLUMN + " " + + valueColumn.getOriginalTypeName()).get(0)) , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP); this.componentDataType = valueColumn.getDataType(); this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); @@ -92,7 +93,6 @@ private void checkColumnIndex(int columnIndex) throws SQLException { } } - private void checkRowPosition() throws SQLException { if (pos < 0 || pos >= length) { throw new SQLException("No current row"); @@ -551,192 +551,192 @@ public boolean rowDeleted() throws SQLException { @Override public void updateNull(int columnIndex) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBoolean(int columnIndex, boolean x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateByte(int columnIndex, byte x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateShort(int columnIndex, short x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateInt(int columnIndex, int x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateLong(int columnIndex, long x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateFloat(int columnIndex, float x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateDouble(int columnIndex, double x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateString(int columnIndex, String x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBytes(int columnIndex, byte[] x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateDate(int columnIndex, Date x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateTime(int columnIndex, Time x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { - + throwReadOnlyException(); } @Override public void updateObject(int columnIndex, Object x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNull(String columnLabel) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBoolean(String columnLabel, boolean x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateByte(String columnLabel, byte x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateShort(String columnLabel, short x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateInt(String columnLabel, int x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateLong(String columnLabel, long x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateFloat(String columnLabel, float x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateDouble(String columnLabel, double x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateString(String columnLabel, String x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBytes(String columnLabel, byte[] x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateDate(String columnLabel, Date x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateTime(String columnLabel, Time x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { - + throwReadOnlyException(); } @Override public void updateObject(String columnLabel, Object x) throws SQLException { - + throwReadOnlyException(); } private void throwReadOnlyException() throws SQLException { @@ -765,7 +765,7 @@ public void refreshRow() throws SQLException { @Override public void cancelRowUpdates() throws SQLException { - + throw new SQLFeatureNotSupportedException("cancelRowUpdates is not supported"); } @Override @@ -903,42 +903,42 @@ public URL getURL(String columnLabel) throws SQLException { @Override public void updateRef(int columnIndex, Ref x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateRef(String columnLabel, Ref x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(int columnIndex, Blob x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(String columnLabel, Blob x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(int columnIndex, Clob x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(String columnLabel, Clob x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateArray(int columnIndex, Array x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateArray(String columnLabel, Array x) throws SQLException { - + throwReadOnlyException(); } @Override @@ -953,12 +953,12 @@ public RowId getRowId(String columnLabel) throws SQLException { @Override public void updateRowId(int columnIndex, RowId x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateRowId(String columnLabel, RowId x) throws SQLException { - + throwReadOnlyException(); } @Override @@ -973,22 +973,22 @@ public boolean isClosed() throws SQLException { @Override public void updateNString(int columnIndex, String nString) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNString(String columnLabel, String nString) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { - + throwReadOnlyException(); } @Override @@ -1013,12 +1013,12 @@ public SQLXML getSQLXML(String columnLabel) throws SQLException { @Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { - + throwReadOnlyException(); } @Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { - + throwReadOnlyException(); } @Override @@ -1043,142 +1043,142 @@ public Reader getNCharacterStream(String columnLabel) throws SQLException { @Override public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { - + throwReadOnlyException(); } @Override public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { - + throwReadOnlyException(); } @Override public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(int columnIndex, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override public void updateClob(String columnLabel, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(int columnIndex, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override public void updateNClob(String columnLabel, Reader reader) throws SQLException { - + throwReadOnlyException(); } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 730a9f5d1..18cf023f4 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -1,14 +1,32 @@ package com.clickhouse.jdbc.types; import com.clickhouse.data.ClickHouseColumn; +import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.NClob; +import java.sql.Ref; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; -import static org.testng.Assert.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; @Test(groups = {"unit"}) public class ArrayResultSetTest { @@ -104,6 +122,10 @@ void testCursorNavigation() throws SQLException { assertFalse(rs.next()); assertThrows(SQLException.class, () -> rs.getShort(2)); + assertFalse(rs.rowDeleted()); + assertFalse(rs.rowInserted()); + assertFalse(rs.rowUpdated()); + rs.close(); assertTrue(rs.isClosed()); } @@ -128,6 +150,8 @@ void testNullValues() throws SQLException { void testPrimitiveValues(Object array, ClickHouseColumn column) throws SQLException { ArrayResultSet rs = new ArrayResultSet(array, column); + expectThrows(SQLException.class, () -> rs.findColumn("something")); + String valueColumn = rs.getMetaData().getColumnLabel(2); int len = java.lang.reflect.Array.getLength(array); Class itemClass = array.getClass().getComponentType(); for (int i = 0; i < len; i++) { @@ -136,28 +160,40 @@ void testPrimitiveValues(Object array, ClickHouseColumn column) throws SQLExcept Object actualValue = rs.getObject(2); assertEquals(actualValue, value, "Actual value is " + actualValue.getClass() + " but expected " + value.getClass()); if (itemClass.isPrimitive() && (itemClass != Boolean.class && itemClass != boolean.class)) { - assertEquals(rs.getByte(2), ((Number) value).byteValue()); - assertEquals(rs.getShort(2), ((Number) value).shortValue()); - assertEquals(rs.getInt(2), ((Number) value).intValue()); - assertEquals(rs.getLong(2), ((Number) value).longValue()); - assertEquals(rs.getFloat(2), ((Number) value).floatValue()); - assertEquals(rs.getDouble(2), ((Number) value).doubleValue()); + assertEquals(rs.getByte(valueColumn), ((Number) value).byteValue()); + assertEquals(rs.getShort(valueColumn), ((Number) value).shortValue()); + assertEquals(rs.getInt(valueColumn), ((Number) value).intValue()); + assertEquals(rs.getLong(valueColumn), ((Number) value).longValue()); + assertEquals(rs.getFloat(valueColumn), ((Number) value).floatValue()); + assertEquals(rs.getDouble(valueColumn), ((Number) value).doubleValue()); + + assertEquals(rs.getString(valueColumn), String.valueOf(value)); } else if (Number.class.isAssignableFrom(itemClass)) { - assertEquals(rs.getByte(2), ((Number) value).byteValue()); - assertEquals(rs.getShort(2), ((Number) value).shortValue()); - assertEquals(rs.getInt(2), ((Number) value).intValue()); - assertEquals(rs.getLong(2), ((Number) value).longValue()); - assertEquals(rs.getFloat(2), ((Number) value).floatValue()); - assertEquals(rs.getDouble(2), ((Number) value).doubleValue()); + assertEquals(rs.getByte(valueColumn), ((Number) value).byteValue()); + assertEquals(rs.getShort(valueColumn), ((Number) value).shortValue()); + assertEquals(rs.getInt(valueColumn), ((Number) value).intValue()); + assertEquals(rs.getLong(valueColumn), ((Number) value).longValue()); + assertEquals(rs.getFloat(valueColumn), ((Number) value).floatValue()); + assertEquals(rs.getDouble(valueColumn), ((Number) value).doubleValue()); } else if (itemClass == Boolean.class || itemClass == boolean.class) { Number number = ((Boolean) value) ? 1 : 0; - assertEquals(rs.getByte(2), number.byteValue()); - assertEquals(rs.getShort(2), number.shortValue()); - assertEquals(rs.getInt(2), number.intValue()); - assertEquals(rs.getLong(2), number.longValue()); - assertEquals(rs.getFloat(2), number.floatValue()); - assertEquals(rs.getDouble(2), number.doubleValue()); + assertEquals(rs.getBoolean(valueColumn), ((Boolean) value)); + assertEquals(rs.getByte(valueColumn), number.byteValue()); + assertEquals(rs.getShort(valueColumn), number.shortValue()); + assertEquals(rs.getInt(valueColumn), number.intValue()); + assertEquals(rs.getLong(valueColumn), number.longValue()); + assertEquals(rs.getFloat(valueColumn), number.floatValue()); + assertEquals(rs.getDouble(valueColumn), number.doubleValue()); + } + + String indexColumn = rs.getMetaData().getColumnName(1); + assertEquals(rs.getByte(indexColumn), i + 1); + assertEquals(rs.getShort(indexColumn), i + 1); + assertEquals(rs.getInt(indexColumn), i + 1); + assertEquals(rs.getLong(indexColumn), i + 1); + assertEquals(rs.getFloat(indexColumn), i + 1); + assertEquals(rs.getDouble(indexColumn), i + 1); } } @@ -217,4 +253,120 @@ static Object[][] testPrimitiveMultiDimensionalValues() { {(Object) new double[][]{new double[]{1, 2, 3}, new double[]{1, 2, 3}}, ClickHouseColumn.parse("v Array(Array(Float64))").get(0)}, }; } + + @Test + public void testReadOnlyException() throws Throwable { + Integer[] array = {1, null, 3, 4, 5}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); + + Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ + rs::moveToCurrentRow, + rs::moveToInsertRow, + rs::refreshRow, + () -> rs.updateBoolean("col1", true), + () -> rs.updateByte("col1", (byte) 1), + () -> rs.updateShort("col1", (short) 1), + () -> rs.updateInt("col1", 1), + () -> rs.updateLong("col1", 1L), + () -> rs.updateFloat("col1", 1.1f), + () -> rs.updateDouble("col1", 1.1), + () -> rs.updateBigDecimal("col1", BigDecimal.valueOf(1.1)), + () -> rs.updateString("col1", "test"), + () -> rs.updateNString("col1", "test"), + () -> rs.updateBytes("col1", new byte[1]), + () -> rs.updateDate("col1", Date.valueOf("2020-01-01")), + () -> rs.updateTime("col1", Time.valueOf("12:34:56")), + () -> rs.updateTimestamp("col1", Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob("col1", (Blob) null), + () -> rs.updateClob("col1", new StringReader("test")), + () -> rs.updateNClob("col1", new StringReader("test")), + + () -> rs.updateBoolean(1, true), + () -> rs.updateByte(1, (byte) 1), + () -> rs.updateShort(1, (short) 1), + () -> rs.updateInt(1, 1), + () -> rs.updateLong(1, 1L), + () -> rs.updateFloat(1, 1.1f), + () -> rs.updateDouble(1, 1.1), + () -> rs.updateBigDecimal(1, BigDecimal.valueOf(1.1)), + () -> rs.updateString(1, "test"), + () -> rs.updateNString(1, "test"), + () -> rs.updateBytes(1, new byte[1]), + () -> rs.updateDate(1, Date.valueOf("2020-01-01")), + () -> rs.updateTime(1, Time.valueOf("12:34:56")), + () -> rs.updateTimestamp(1, Timestamp.valueOf("2020-01-01 12:34:56.789123")), + () -> rs.updateBlob(1, (Blob) null), + () -> rs.updateClob(1, new StringReader("test")), + () -> rs.updateNClob(1, new StringReader("test")), + () -> rs.updateSQLXML(1, null), + () -> rs.updateObject(1, 1), + () -> rs.updateObject("col1", 1), + () -> rs.updateObject(1, "test", Types.INTEGER), + () -> rs.updateObject("col1", "test", Types.INTEGER), + () -> rs.updateObject(1, "test", JDBCType.INTEGER), + () -> rs.updateObject("col1", "test", JDBCType.INTEGER), + () -> rs.updateObject(1, "test", JDBCType.INTEGER, 1), + () -> rs.updateCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateCharacterStream(1, new StringReader("test")), + () -> rs.updateCharacterStream("col1", new StringReader("test")), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1), + () -> rs.updateNCharacterStream(1, new StringReader("test"), 1L), + () -> rs.updateNCharacterStream("col1", new StringReader("test"), 1L), + () -> rs.updateNCharacterStream(1, new StringReader("test")), + () -> rs.updateNCharacterStream("col1", new StringReader("test")), + () -> rs.updateBlob(1, (InputStream) null), + () -> rs.updateBlob("col1", (InputStream) null), + () -> rs.updateBlob(1, (InputStream) null, -1), + () -> rs.updateBlob("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null), + () -> rs.updateBinaryStream("col1", (InputStream) null), + () -> rs.updateBinaryStream(1, (InputStream) null, -1), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1), + () -> rs.updateBinaryStream(1, (InputStream) null, -1L), + () -> rs.updateBinaryStream("col1", (InputStream) null, -1L), + () -> rs.updateAsciiStream(1, (InputStream) null), + () -> rs.updateAsciiStream("col1", (InputStream) null), + () -> rs.updateAsciiStream(1, (InputStream) null, -1), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1), + () -> rs.updateAsciiStream(1, (InputStream) null, -1L), + () -> rs.updateAsciiStream("col1", (InputStream) null, -1L), + () -> rs.updateClob(1, (Reader) null), + () -> rs.updateClob("col1", (Reader) null), + () -> rs.updateClob(1, (Reader) null, -1), + () -> rs.updateClob("col1", (Reader) null, -1), + () -> rs.updateClob(1, (Reader) null, -1L), + () -> rs.updateClob("col1", (Reader) null, -1L), + () -> rs.updateNClob(1, (Reader) null), + () -> rs.updateNClob("col1", (Reader) null), + () -> rs.updateNClob(1, (NClob) null), + () -> rs.updateNClob("col1", (NClob) null), + () -> rs.updateNClob(1, (Reader) null, -1), + () -> rs.updateNClob("col1", (Reader) null, -1), + () -> rs.updateNClob(1, (Reader) null, -1L), + () -> rs.updateNClob("col1", (Reader) null, -1L), + () -> rs.updateRef(1, (Ref) null), + () -> rs.updateRef("col1", (Ref) null), + () -> rs.updateArray(1, (java.sql.Array) null), + () -> rs.updateArray("col1", (java.sql.Array) null), + rs::cancelRowUpdates, + () -> rs.updateNull(1), + () -> rs.updateNull("col1"), + () -> rs.updateRowId(1, null), + () -> rs.updateRowId("col1", null), + () -> rs.updateClob(1, (Clob) null), + () -> rs.updateClob("col1", (Clob) null), + rs::updateRow, + rs::insertRow, + rs::deleteRow, + }; + + for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { + Assert.assertThrows(SQLException.class, op); + } + } } \ No newline at end of file From f472b4415ee0255ecf388ff48f94c4351d9dca01 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 5 Dec 2025 11:54:57 -0800 Subject: [PATCH 06/15] added more tests --- .../clickhouse/jdbc/types/ArrayResultSet.java | 153 +++++++++++------- .../jdbc/types/ArrayResultSetTest.java | 79 ++++++--- 2 files changed, 153 insertions(+), 79 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index e632a2181..8b6b7c817 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -3,12 +3,15 @@ import com.clickhouse.client.api.data_formats.internal.ValueConverters; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; +import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.MalformedURLException; import java.net.URL; import java.sql.Array; import java.sql.Blob; @@ -123,6 +126,16 @@ private Object getValueAsObject(int columnIndex, Class type, Object defaultVa return value == null ? defaultValue : value; } + private void throwReadOnlyException() throws SQLException { + throw new SQLException("ResultSet is read-only"); + } + + private void throwUnsupportedIndexOperation(int columnIndex, String operation) throws SQLException { + if (columnIndex == 1) { + throw new SQLFeatureNotSupportedException("operation " + operation + " is not supported on INDEX column"); + } + } + @Override public void close() throws SQLException { this.closed = true; @@ -143,9 +156,7 @@ public String getString(int columnIndex) throws SQLException { @Override public boolean getBoolean(int columnIndex) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as boolean"); - } + throwUnsupportedIndexOperation(columnIndex, "getBoolean"); return (Boolean) getValueAsObject(columnIndex, Boolean.class, false); } @@ -215,17 +226,13 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException @Override public byte[] getBytes(int columnIndex) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as bytes"); - } + throwUnsupportedIndexOperation(columnIndex, "getBytes"); return (byte[]) getValueAsObject(columnIndex, byte[].class, null); } @Override public Date getDate(int columnIndex) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as date"); - } + throwUnsupportedIndexOperation(columnIndex, "getDate"); return (Date) getValueAsObject(columnIndex, Date.class, null); } @@ -233,9 +240,7 @@ public Date getDate(int columnIndex) throws SQLException { public Time getTime(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as time"); - } + throwUnsupportedIndexOperation(columnIndex, "getTime"); return (Time) getValueAsObject(columnIndex, Time.class, null); } @@ -243,9 +248,7 @@ public Time getTime(int columnIndex) throws SQLException { public Timestamp getTimestamp(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as timestamp"); - } + throwUnsupportedIndexOperation(columnIndex, "getTimestamp"); return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); } @@ -253,9 +256,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { public InputStream getAsciiStream(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as ascii stream"); - } + throwUnsupportedIndexOperation(columnIndex, "getAsciiStream"); throw new SQLFeatureNotSupportedException("getAsciiStream is not implemented"); } @@ -263,9 +264,7 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { public InputStream getUnicodeStream(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as unicode stream"); - } + throwUnsupportedIndexOperation(columnIndex, "getUnicodeStream"); throw new SQLFeatureNotSupportedException("getUnicodeStream is not implemented"); } @@ -273,9 +272,7 @@ public InputStream getUnicodeStream(int columnIndex) throws SQLException { public InputStream getBinaryStream(int columnIndex) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as binary stream"); - } + throwUnsupportedIndexOperation(columnIndex, "getBinaryStream"); throw new SQLFeatureNotSupportedException("getBinaryStream is not implemented"); } @@ -357,17 +354,17 @@ public Timestamp getTimestamp(String columnLabel) throws SQLException { @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { - return null; + return getAsciiStream(getColumnIndex(columnLabel)); } @Override public InputStream getUnicodeStream(String columnLabel) throws SQLException { - return null; + return getUnicodeStream(getColumnIndex(columnLabel)); } @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { - return null; + return getBinaryStream(getColumnIndex(columnLabel)); } @Override @@ -407,19 +404,22 @@ public int findColumn(String columnLabel) throws SQLException { @Override public Reader getCharacterStream(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getCharacterStream"); + throw new SQLFeatureNotSupportedException("getCharacterStream is not implemented"); } @Override public Reader getCharacterStream(String columnLabel) throws SQLException { - return null; + return getCharacterStream(getColumnIndex(columnLabel)); } @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as big decimal"); - } + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getBigDecimal"); return (BigDecimal) getValueAsObject(columnIndex, BigDecimal.class, null); } @@ -739,10 +739,6 @@ public void updateObject(String columnLabel, Object x) throws SQLException { throwReadOnlyException(); } - private void throwReadOnlyException() throws SQLException { - throw new SQLException("ResultSet is read-only"); - } - @Override public void insertRow() throws SQLException { throwReadOnlyException(); @@ -812,22 +808,34 @@ public Object getObject(int columnIndex, Map> map) throws SQLEx @Override public Ref getRef(int columnIndex) throws SQLException { - return null; + checkRowPosition(); + checkColumnIndex(columnIndex); + throwUnsupportedIndexOperation(columnIndex, "getRef"); + throw new SQLFeatureNotSupportedException("getRef is not implemented"); } @Override public Blob getBlob(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getBlob"); + throw new SQLFeatureNotSupportedException("getBlob is not implemented"); } @Override public Clob getClob(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getClob"); + throw new SQLFeatureNotSupportedException("getClob is not implemented"); } @Override public Array getArray(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getArray"); + return (Array) getValueAsObject(columnIndex, Array.class, null); } @Override @@ -837,29 +845,29 @@ public Object getObject(String columnLabel, Map> map) throws SQ @Override public Ref getRef(String columnLabel) throws SQLException { - return null; + return getRef(getColumnIndex(columnLabel)); } @Override public Blob getBlob(String columnLabel) throws SQLException { - return null; + return getBlob(getColumnIndex(columnLabel)); } @Override public Clob getClob(String columnLabel) throws SQLException { - return null; + return getClob(getColumnIndex(columnLabel)); } @Override public Array getArray(String columnLabel) throws SQLException { - return null; + return getArray(getColumnIndex(columnLabel)); } @Override public Date getDate(int columnIndex, Calendar cal) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as date"); - } + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getDate"); return null; } @@ -870,9 +878,9 @@ public Date getDate(String columnLabel, Calendar cal) throws SQLException { @Override public Time getTime(int columnIndex, Calendar cal) throws SQLException { - if (columnIndex == 1) { - throw new SQLException("INDEX column cannot be get as time"); - } + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getTime"); return null; } @@ -883,6 +891,9 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { @Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getTimestamp"); return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); } @@ -893,7 +904,15 @@ public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLExcept @Override public URL getURL(int columnIndex) throws SQLException { - return (URL) getValueAsObject(columnIndex, URL.class, null); + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getURL"); + String value = getString(columnIndex); + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new SQLException("Invalid URL value", ExceptionUtils.SQL_STATE_DATA_EXCEPTION, e); + } } @Override @@ -943,12 +962,15 @@ public void updateArray(String columnLabel, Array x) throws SQLException { @Override public RowId getRowId(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getRowId"); + throw new SQLFeatureNotSupportedException("getRowId is not implemented"); } @Override public RowId getRowId(String columnLabel) throws SQLException { - return null; + return getRowId(getColumnIndex(columnLabel)); } @Override @@ -993,22 +1015,28 @@ public void updateNClob(String columnLabel, NClob nClob) throws SQLException { @Override public NClob getNClob(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getNClob"); + throw new SQLFeatureNotSupportedException("getNClob is not implemented"); } @Override public NClob getNClob(String columnLabel) throws SQLException { - return null; + return getNClob(getColumnIndex(columnLabel)); } @Override public SQLXML getSQLXML(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getSQLXML"); + throw new SQLFeatureNotSupportedException("getSQLXML is not implemented"); } @Override public SQLXML getSQLXML(String columnLabel) throws SQLException { - return null; + return getSQLXML(getColumnIndex(columnLabel)); } @Override @@ -1023,22 +1051,25 @@ public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLExcepti @Override public String getNString(int columnIndex) throws SQLException { - return ""; + return getString(columnIndex); } @Override public String getNString(String columnLabel) throws SQLException { - return ""; + return getString(getColumnIndex(columnLabel)); } @Override public Reader getNCharacterStream(int columnIndex) throws SQLException { - return null; + checkColumnIndex(columnIndex); + checkRowPosition(); + throwUnsupportedIndexOperation(columnIndex, "getNCharacterStream"); + throw new SQLFeatureNotSupportedException("getNCharacterStream is not implemented"); } @Override public Reader getNCharacterStream(String columnLabel) throws SQLException { - return null; + return getNCharacterStream(getColumnIndex(columnLabel)); } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 18cf023f4..a6409722c 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -157,24 +157,26 @@ void testPrimitiveValues(Object array, ClickHouseColumn column) throws SQLExcept for (int i = 0; i < len; i++) { rs.next(); Object value = Array.get(array, i); - Object actualValue = rs.getObject(2); + Object actualValue = rs.getObject(valueColumn); assertEquals(actualValue, value, "Actual value is " + actualValue.getClass() + " but expected " + value.getClass()); if (itemClass.isPrimitive() && (itemClass != Boolean.class && itemClass != boolean.class)) { - assertEquals(rs.getByte(valueColumn), ((Number) value).byteValue()); - assertEquals(rs.getShort(valueColumn), ((Number) value).shortValue()); - assertEquals(rs.getInt(valueColumn), ((Number) value).intValue()); - assertEquals(rs.getLong(valueColumn), ((Number) value).longValue()); - assertEquals(rs.getFloat(valueColumn), ((Number) value).floatValue()); - assertEquals(rs.getDouble(valueColumn), ((Number) value).doubleValue()); - + Number number = ((Number) value); + assertEquals(rs.getByte(valueColumn), number.byteValue()); + assertEquals(rs.getShort(valueColumn), number.shortValue()); + assertEquals(rs.getInt(valueColumn), number.intValue()); + assertEquals(rs.getLong(valueColumn), number.longValue()); + assertEquals(rs.getFloat(valueColumn), number.floatValue()); + assertEquals(rs.getDouble(valueColumn), number.doubleValue()); assertEquals(rs.getString(valueColumn), String.valueOf(value)); } else if (Number.class.isAssignableFrom(itemClass)) { - assertEquals(rs.getByte(valueColumn), ((Number) value).byteValue()); - assertEquals(rs.getShort(valueColumn), ((Number) value).shortValue()); - assertEquals(rs.getInt(valueColumn), ((Number) value).intValue()); - assertEquals(rs.getLong(valueColumn), ((Number) value).longValue()); - assertEquals(rs.getFloat(valueColumn), ((Number) value).floatValue()); - assertEquals(rs.getDouble(valueColumn), ((Number) value).doubleValue()); + Number number = ((Number) value); + assertEquals(rs.getByte(valueColumn), number.byteValue()); + assertEquals(rs.getShort(valueColumn), number.shortValue()); + assertEquals(rs.getInt(valueColumn), number.intValue()); + assertEquals(rs.getLong(valueColumn), number.longValue()); + assertEquals(rs.getFloat(valueColumn), number.floatValue()); + assertEquals(rs.getDouble(valueColumn), number.doubleValue()); + assertEquals(rs.getBigDecimal(valueColumn), BigDecimal.valueOf(number.doubleValue())); } else if (itemClass == Boolean.class || itemClass == boolean.class) { Number number = ((Boolean) value) ? 1 : 0; assertEquals(rs.getBoolean(valueColumn), ((Boolean) value)); @@ -184,7 +186,6 @@ void testPrimitiveValues(Object array, ClickHouseColumn column) throws SQLExcept assertEquals(rs.getLong(valueColumn), number.longValue()); assertEquals(rs.getFloat(valueColumn), number.floatValue()); assertEquals(rs.getDouble(valueColumn), number.doubleValue()); - } String indexColumn = rs.getMetaData().getColumnName(1); @@ -224,12 +225,14 @@ void testPrimitiveMultiDimensionalValues(Object array, ClickHouseColumn column) ArrayResultSet rs = new ArrayResultSet(array, column); int len = java.lang.reflect.Array.getLength(array); - Class itemClass = array.getClass().getComponentType(); + final String valueColumn = rs.getMetaData().getColumnName(2); for (int i = 0; i < len; i++) { rs.next(); Object value = Array.get(array, i); - java.sql.Array sqlArray = (java.sql.Array) rs.getObject(2); + java.sql.Array sqlArray = (java.sql.Array) rs.getObject(valueColumn); assertEquals(sqlArray.getArray(), value); + java.sql.Array sqlArray2 = rs.getArray(valueColumn); + assertEquals(sqlArray2.getArray(), value); ArrayResultSet nestedRs = (ArrayResultSet) sqlArray.getResultSet(); for (int j = 0; j < len; j++) { @@ -254,11 +257,40 @@ static Object[][] testPrimitiveMultiDimensionalValues() { }; } + @Test + void testStringValues() throws SQLException { + String[] array = new String[] {"a", null, "c"}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Nullable(String))").get(0)); + + final String valueColumn = rs.getMetaData().getColumnLabel(2); + int len = java.lang.reflect.Array.getLength(array); + for (int i = 0; i < len; i++) { + rs.next(); + String value = array[i]; + assertEquals(rs.getString(valueColumn), value); + assertEquals(rs.getObject(valueColumn), value); + assertEquals(rs.getNString(valueColumn), value); + if (value == null) { + assertTrue(rs.wasNull()); + } else { + assertEquals(rs.getBytes(valueColumn), value.getBytes()); + } + } + } + + @Test + void testEmptyArray() throws SQLException { + ArrayResultSet rs = new ArrayResultSet(new Object[0], ClickHouseColumn.parse("v Array(Int32)").get(0)); + assertFalse(rs.next()); + } + @Test public void testReadOnlyException() throws Throwable { Integer[] array = {1, null, 3, 4, 5}; ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); + rs.next(); + final String valueColumn = rs.getMetaData().getColumnName(2); Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ rs::moveToCurrentRow, rs::moveToInsertRow, @@ -280,7 +312,6 @@ public void testReadOnlyException() throws Throwable { () -> rs.updateBlob("col1", (Blob) null), () -> rs.updateClob("col1", new StringReader("test")), () -> rs.updateNClob("col1", new StringReader("test")), - () -> rs.updateBoolean(1, true), () -> rs.updateByte(1, (byte) 1), () -> rs.updateShort(1, (short) 1), @@ -363,6 +394,18 @@ public void testReadOnlyException() throws Throwable { rs::updateRow, rs::insertRow, rs::deleteRow, + () -> rs.getCharacterStream(valueColumn), + () -> rs.getBinaryStream(valueColumn), + () -> rs.getUnicodeStream(valueColumn), + () -> rs.getAsciiStream(valueColumn), + () -> rs.getNCharacterStream(valueColumn), + () -> rs.getNClob(valueColumn), + () -> rs.getClob(valueColumn), + () -> rs.getBlob(valueColumn), + () -> rs.getSQLXML(valueColumn), + () -> rs.getRef(valueColumn), + () -> rs.getRowId(valueColumn), + () -> rs.getSQLXML(valueColumn), }; for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { From 34391ed0fa541406fd469d00f9a4ee12ae516c52 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Fri, 5 Dec 2025 17:39:47 -0800 Subject: [PATCH 07/15] done handling index column as normal value. done retreiving date/time values --- .../internal/ValueConverters.java | 43 ++++++++- .../clickhouse/jdbc/internal/JdbcUtils.java | 3 + .../clickhouse/jdbc/types/ArrayResultSet.java | 43 ++++----- .../clickhouse/jdbc/ResultSetImplTest.java | 90 ++++++++++++++++++- .../jdbc/types/ArrayResultSetTest.java | 17 ++++ 5 files changed, 167 insertions(+), 29 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java index 08c5a00d1..710cf9fc9 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -4,9 +4,11 @@ import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Arrays; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.ZonedDateTime; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.function.Function; @@ -100,6 +102,10 @@ public ValueConverters() { stringMapBuilder.put(byte[].class, this::convertStringToBytes); mapBuilder.put(String.class, stringMapBuilder.build()); + mapBuilder.put(java.sql.Date.class, ImmutableMap.of(java.sql.Date.class, this::conveSqlDateToSqlDate)); + mapBuilder.put(Time.class, ImmutableMap.of(Time.class, this::conveSqlTimeToSqlTime)); + mapBuilder.put(Timestamp.class, ImmutableMap.of(Timestamp.class, this::conveSqlTimestampToSqlTimestamp)); + classConverters = mapBuilder.build(); } @@ -194,6 +200,39 @@ public BigDecimal convertNumberToBigDecimal(Object value) { return BigDecimal.valueOf(((Number) value).doubleValue()); } + // Date & Time converters + + public java.sql.Date convertZonedDateTimeToSqlDate(Object value) { + // Date is stored without time zone information and should be treated as UTC + ZonedDateTime zonedDateTime = (ZonedDateTime) value; + Date date = Date.valueOf(zonedDateTime.toLocalDate()); + return date; + } + + public Time convertZonedDateTimeToSqlTime(Object value) { + // Time is stored without time zone information and should be treated as UTC + ZonedDateTime zonedDateTime = (ZonedDateTime) value; + Time time = Time.valueOf(zonedDateTime.toLocalTime()); + return time; + } + + public Timestamp convertZonedDateTimeToSqlTimestamp(Object value) { + ZonedDateTime zonedDateTime = (ZonedDateTime) value; + return Timestamp.valueOf(zonedDateTime.toLocalDateTime()); + } + + public java.sql.Date conveSqlDateToSqlDate(Object value) { + return (java.sql.Date) value; + } + + public Time conveSqlTimeToSqlTime(Object value) { + return (Time) value; + } + + public Timestamp conveSqlTimestampToSqlTimestamp(Object value) { + return (Timestamp) value; + } + /** * Returns the converter map for the given source type. * Map contains target type and converter function. For example, if source type is boolean then map will contain all diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 78a0e6121..a51a7467e 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -20,6 +20,7 @@ import java.sql.JDBCType; import java.sql.SQLException; import java.sql.SQLType; +import java.sql.Time; import java.sql.Types; import java.time.*; import java.time.chrono.ChronoZonedDateTime; @@ -79,6 +80,8 @@ private static Map generateTypeMap() { map.put(ClickHouseDataType.DateTime, JDBCType.TIMESTAMP); map.put(ClickHouseDataType.DateTime32, JDBCType.TIMESTAMP); map.put(ClickHouseDataType.DateTime64, JDBCType.TIMESTAMP); + map.put(ClickHouseDataType.Time, JDBCType.TIME); + map.put(ClickHouseDataType.Time64, JDBCType.TIME); map.put(ClickHouseDataType.Array, JDBCType.ARRAY); map.put(ClickHouseDataType.Nested, JDBCType.ARRAY); map.put(ClickHouseDataType.Map, JDBCType.JAVA_OBJECT); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 8b6b7c817..f78ac48c4 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -30,6 +30,7 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Calendar; import java.util.List; @@ -49,8 +50,8 @@ public class ArrayResultSet implements ResultSet { private int fetchDirection = ResultSet.FETCH_FORWARD; private int fetchSize = 0; private boolean wasNull = false; - private Map, Function> converterMap; - + private final Map, Function> converterMap; + private final Map, Function> indexConverterMap; private final ClickHouseDataType componentDataType; private final Class defaultClass; private final ClickHouseColumn column; @@ -68,8 +69,9 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP); this.componentDataType = valueColumn.getDataType(); this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); + ValueConverters converters = new ValueConverters(); + indexConverterMap = converters.getConvertersForType(Integer.class); if (this.length > 1) { - ValueConverters converters = new ValueConverters(); Class itemClass = array.getClass().getComponentType(); if (itemClass == null) { itemClass = java.lang.reflect.Array.get(array, 0).getClass(); @@ -105,17 +107,23 @@ private void checkRowPosition() throws SQLException { private Object getValueAsObject(int columnIndex, Class type, Object defaultValue) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); + + Object value; + Map, Function> valueConverterMap; if (columnIndex == 1) { - return pos; + value = pos + 1; + valueConverterMap = indexConverterMap; + } else { + value = java.lang.reflect.Array.get(array, pos); + valueConverterMap = converterMap; } - Object value = java.lang.reflect.Array.get(array, pos); - if (value != null && type == Array.class) { + if (columnIndex != 1 && value != null && type == Array.class) { ClickHouseColumn nestedColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : column.getNestedColumns().get(0); return new com.clickhouse.jdbc.types.Array(nestedColumn, JdbcUtils.arrayToObjectArray(value)); } else if (value != null && type != Object.class) { // if there is something to convert. type == Object.class means no conversion - Function converter = converterMap.get(type); + Function converter = valueConverterMap.get(type); if (converter != null) { value = converter.apply(value); } else { @@ -238,16 +246,12 @@ public Date getDate(int columnIndex) throws SQLException { @Override public Time getTime(int columnIndex) throws SQLException { - checkColumnIndex(columnIndex); - checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getTime"); return (Time) getValueAsObject(columnIndex, Time.class, null); } @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { - checkColumnIndex(columnIndex); - checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getTimestamp"); return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); } @@ -389,7 +393,8 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public Object getObject(int columnIndex) throws SQLException { - return getObject(columnIndex, defaultClass); + Class targetType = columnIndex == 1 ? Integer.class : defaultClass; + return getObject(columnIndex, targetType); } @Override @@ -832,8 +837,6 @@ public Clob getClob(int columnIndex) throws SQLException { @Override public Array getArray(int columnIndex) throws SQLException { - checkColumnIndex(columnIndex); - checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getArray"); return (Array) getValueAsObject(columnIndex, Array.class, null); } @@ -891,8 +894,6 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { @Override public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { - checkColumnIndex(columnIndex); - checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getTimestamp"); return (Timestamp) getValueAsObject(columnIndex, Timestamp.class, null); } @@ -1216,16 +1217,6 @@ public void updateNClob(String columnLabel, Reader reader) throws SQLException { public T getObject(int columnIndex, Class type) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - if (columnIndex == 1) { - if (Number.class.isAssignableFrom(type)) { - return (T) pos; - } else if (String.class.isAssignableFrom(type)) { - return (T) String.valueOf(pos); - } else { - throw new SQLException("INDEX column cannot be converted to non-number value"); - } - } - return (T) getValueAsObject(columnIndex, type, null); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java index 8177abacb..d18a925fb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java @@ -1,5 +1,7 @@ package com.clickhouse.jdbc; +import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.data.ClickHouseVersion; import org.testng.Assert; import org.testng.annotations.Test; @@ -364,7 +366,6 @@ public void testGetMetadata() throws SQLException { @Test(groups = {"integration"}) public void testGetResultSetFromArray() throws Exception { - try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("select [1, 2, 3, 4]::Array(UInt16) as v")) { assertTrue(rs.next()); @@ -386,4 +387,91 @@ public void testGetResultSetFromArray() throws Exception { } } } + + @Test(groups = {"integration"}) + public void testGetResultSetFromArrayDate() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + // date array + try (ResultSet rs = stmt.executeQuery("select [toDate('2020-01-01'), toDate('2020-01-02')] as v")) { + + assertTrue(rs.next()); + Array array = rs.getArray("v"); + Assert.assertNotNull(array); + Assert.assertEquals(array.getBaseType(), Types.DATE); + Assert.assertEquals(array.getBaseTypeName(), "Date"); + + Object[] resultArray = (Object[]) array.getArray(); + Assert.assertEquals(resultArray.length, 2); + Assert.assertEquals(resultArray[0], Date.valueOf("2020-01-01")); + Assert.assertEquals(resultArray[1], Date.valueOf("2020-01-02")); + + ResultSet rs2 = array.getResultSet(); + final String valueColumn = rs2.getMetaData().getColumnName(2); + for (int i = 0; i < resultArray.length; i++) { + rs2.next(); + Assert.assertEquals(rs2.getDate(valueColumn), resultArray[i]); + } + } + } + } + + @Test(groups = {"integration"}, enabled = false) + public void testGetResultSetFromArrayTime() throws Exception { + if (ClickHouseVersion.of(getServerVersion()).check("(,25.5]")) { + return; // Time64 introduced in 25.6 + } + + Properties properties = new Properties(); + properties.put(ClientConfigProperties.serverSetting("allow_experimental_time_time64_type"), "1"); + try (Connection conn = getJdbcConnection(properties); Statement stmt = conn.createStatement()) { + // time array + try (ResultSet rs = stmt.executeQuery("select ['14:30:25'::Time, '17:30:25'::Time] as v")) { + + assertTrue(rs.next()); + Array array = rs.getArray("v"); + Assert.assertNotNull(array); + Assert.assertEquals(array.getBaseType(), Types.TIME); + Assert.assertEquals(array.getBaseTypeName(), "Time"); + + Object[] resultArray = (Object[]) array.getArray(); + Assert.assertEquals(resultArray.length, 2); + Assert.assertEquals(resultArray[0], Time.valueOf("14:30:25")); + Assert.assertEquals(resultArray[1], Time.valueOf("17:30:25")); + + ResultSet rs2 = array.getResultSet(); + final String valueColumn = rs2.getMetaData().getColumnName(2); + for (int i = 0; i < resultArray.length; i++) { + rs2.next(); + Assert.assertEquals(rs2.getTime(valueColumn), resultArray[i]); + } + } + } + } + + @Test(groups = {"integration"}) + public void testGetResultSetFromArrayTimestamp() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + // timestamp array + try (ResultSet rs = stmt.executeQuery("select [toDateTime('2020-01-01 00:00:00'), toDateTime('2020-01-01 00:00:01')] as v")) { + + assertTrue(rs.next()); + Array array = rs.getArray("v"); + Assert.assertNotNull(array); + Assert.assertEquals(array.getBaseType(), Types.TIMESTAMP); + Assert.assertEquals(array.getBaseTypeName(), "DateTime"); + + Object[] resultArray = (Object[]) array.getArray(); + Assert.assertEquals(resultArray.length, 2); + Assert.assertEquals(resultArray[0], Timestamp.valueOf("2020-01-01 00:00:00")); + Assert.assertEquals(resultArray[1], Timestamp.valueOf("2020-01-01 00:00:01")); + + ResultSet rs2 = array.getResultSet(); + final String valueColumn = rs2.getMetaData().getColumnName(2); + for (int i = 0; i < resultArray.length; i++) { + rs2.next(); + Assert.assertEquals(rs2.getTimestamp(valueColumn), resultArray[i]); + } + } + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index a6409722c..b2e136d16 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -412,4 +412,21 @@ public void testReadOnlyException() throws Throwable { Assert.assertThrows(SQLException.class, op); } } + + @Test + void testIndexColumn() throws Exception { + Integer[] array = {1, null, 3, 4, 5}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); + + final String indexColumn = rs.getMetaData().getColumnName(1); + Assert.assertEquals(indexColumn, "INDEX"); + rs.next(); + assertEquals(rs.getObject(indexColumn), 1); + assertEquals(rs.getObject(indexColumn, String.class), "1"); + assertEquals(rs.getObject(indexColumn, Long.class), 1L); + assertEquals(rs.getObject(indexColumn, Integer.class), 1); + assertEquals(rs.getObject(indexColumn, Short.class), (short) 1); + assertEquals(rs.getObject(indexColumn, Byte.class), (byte) 1); + + } } \ No newline at end of file From 2d2ca7c0779a45222383b924acd38569f69772af Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 8 Dec 2025 16:08:50 -0800 Subject: [PATCH 08/15] added time/date/timestamp tests --- .../internal/ValueConverters.java | 53 +++++++------ .../clickhouse/jdbc/types/ArrayResultSet.java | 4 +- .../jdbc/types/ArrayResultSetTest.java | 76 +++++++++++++++++++ 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java index 710cf9fc9..2568cd72e 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -4,10 +4,11 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URL; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; -import java.time.ZonedDateTime; import java.util.Collections; import java.util.Map; import java.util.function.Function; @@ -100,11 +101,15 @@ public ValueConverters() { stringMapBuilder.put(Boolean.class, this::convertStringToBoolean); stringMapBuilder.put(String.class, this::convertStringToString); stringMapBuilder.put(byte[].class, this::convertStringToBytes); + stringMapBuilder.put(URL.class, this::convertStringToURL); mapBuilder.put(String.class, stringMapBuilder.build()); - mapBuilder.put(java.sql.Date.class, ImmutableMap.of(java.sql.Date.class, this::conveSqlDateToSqlDate)); - mapBuilder.put(Time.class, ImmutableMap.of(Time.class, this::conveSqlTimeToSqlTime)); - mapBuilder.put(Timestamp.class, ImmutableMap.of(Timestamp.class, this::conveSqlTimestampToSqlTimestamp)); + mapBuilder.put(java.sql.Date.class, ImmutableMap.of(java.sql.Date.class, this::conveSqlDateToSqlDate, + String.class, this::convertDateToString)); + mapBuilder.put(Time.class, ImmutableMap.of(Time.class, this::conveSqlTimeToSqlTime, + String.class, this::convertTimeToString)); + mapBuilder.put(Timestamp.class, ImmutableMap.of(Timestamp.class, this::conveSqlTimestampToSqlTimestamp, + String.class, this::convertTimestampToString)); classConverters = mapBuilder.build(); } @@ -159,6 +164,14 @@ public double convertStringToDouble(Object value) { return Double.parseDouble((String) value); } + public URL convertStringToURL(Object value) { + try { + return new URL((String) value); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + // Number to any public String convertNumberToString(Object value) { return String.valueOf(value); @@ -201,26 +214,6 @@ public BigDecimal convertNumberToBigDecimal(Object value) { } // Date & Time converters - - public java.sql.Date convertZonedDateTimeToSqlDate(Object value) { - // Date is stored without time zone information and should be treated as UTC - ZonedDateTime zonedDateTime = (ZonedDateTime) value; - Date date = Date.valueOf(zonedDateTime.toLocalDate()); - return date; - } - - public Time convertZonedDateTimeToSqlTime(Object value) { - // Time is stored without time zone information and should be treated as UTC - ZonedDateTime zonedDateTime = (ZonedDateTime) value; - Time time = Time.valueOf(zonedDateTime.toLocalTime()); - return time; - } - - public Timestamp convertZonedDateTimeToSqlTimestamp(Object value) { - ZonedDateTime zonedDateTime = (ZonedDateTime) value; - return Timestamp.valueOf(zonedDateTime.toLocalDateTime()); - } - public java.sql.Date conveSqlDateToSqlDate(Object value) { return (java.sql.Date) value; } @@ -233,6 +226,18 @@ public Timestamp conveSqlTimestampToSqlTimestamp(Object value) { return (Timestamp) value; } + public String convertDateToString(Object value) { + return ((Date)value).toString(); + } + + public String convertTimeToString(Object value) { + return ((Time) value).toString(); + } + + public String convertTimestampToString(Object value) { + return ((Timestamp) value).toString(); + } + /** * Returns the converter map for the given source type. * Map contains target type and converter function. For example, if source type is boolean then map will contain all diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index f78ac48c4..f22e93717 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -871,7 +871,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getDate"); - return null; + return getDate(columnIndex); } @Override @@ -884,7 +884,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); throwUnsupportedIndexOperation(columnIndex, "getTime"); - return null; + return getTime(columnIndex); } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index b2e136d16..56c6cd3b9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -10,6 +10,7 @@ import java.io.StringReader; import java.lang.reflect.Array; import java.math.BigDecimal; +import java.net.URL; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; @@ -21,6 +22,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.util.GregorianCalendar; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -427,6 +429,80 @@ void testIndexColumn() throws Exception { assertEquals(rs.getObject(indexColumn, Integer.class), 1); assertEquals(rs.getObject(indexColumn, Short.class), (short) 1); assertEquals(rs.getObject(indexColumn, Byte.class), (byte) 1); + } + + @Test + void testDateColumn() throws Exception { + Date[] array = {Date.valueOf("2020-01-01"), null, Date.valueOf("2020-01-02")}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Date)").get(0)); + + final String dateColumn = rs.getMetaData().getColumnName(2); + Assert.assertEquals(dateColumn, "VALUE"); + rs.next(); + assertEquals(rs.getObject(dateColumn), Date.valueOf("2020-01-01")); + assertEquals(rs.getObject(dateColumn, String.class), "2020-01-01"); + assertEquals(rs.getDate(dateColumn), rs.getObject(dateColumn, Date.class)); + assertEquals(rs.getDate(dateColumn, new GregorianCalendar()), rs.getObject(dateColumn, Date.class)); + } + + @Test + void testTimeColumn() throws Exception { + Time[] array = {Time.valueOf("12:34:56"), null, Time.valueOf("12:34:57")}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Time)").get(0)); + + final String timeColumn = rs.getMetaData().getColumnName(2); + Assert.assertEquals(timeColumn, "VALUE"); + rs.next(); + assertEquals(rs.getObject(timeColumn), Time.valueOf("12:34:56")); + assertEquals(rs.getObject(timeColumn, String.class), "12:34:56"); + assertEquals(rs.getTime(timeColumn), rs.getObject(timeColumn, Time.class)); + assertEquals(rs.getTime(timeColumn, new GregorianCalendar()), rs.getObject(timeColumn, Time.class)); + } + + @Test + void testTimestampColumn() throws Exception { + Timestamp[] array = {Timestamp.valueOf("2020-01-01 12:34:56.789123"), null, Timestamp.valueOf("2020-01-01 12:34:56.789124")}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Timestamp)").get(0)); + + final String timestampColumn = rs.getMetaData().getColumnName(2); + Assert.assertEquals(timestampColumn, "VALUE"); + rs.next(); + assertEquals(rs.getObject(timestampColumn), Timestamp.valueOf("2020-01-01 12:34:56.789123")); + assertEquals(rs.getObject(timestampColumn, String.class), "2020-01-01 12:34:56.789123"); + assertEquals(rs.getTimestamp(timestampColumn), rs.getObject(timestampColumn, Timestamp.class)); + assertEquals(rs.getTimestamp(timestampColumn, new GregorianCalendar()), rs.getObject(timestampColumn, Timestamp.class)); + } + + @Test + void testStringColumn() throws Exception { + String[] array = {"123", null, "456"}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(String)").get(0)); + final String stringColumn = rs.getMetaData().getColumnName(2); + Assert.assertEquals(stringColumn, "VALUE"); + rs.next(); + assertEquals(rs.getObject(stringColumn), "123"); + assertEquals(rs.getObject(stringColumn, String.class), "123"); + assertEquals(rs.getString(stringColumn), rs.getObject(stringColumn, String.class)); + assertEquals(rs.getByte(stringColumn), 123); + assertEquals(rs.getShort(stringColumn), (short) 123); + assertEquals(rs.getInt(stringColumn), 123); + assertEquals(rs.getLong(stringColumn), 123L); + assertEquals(rs.getFloat(stringColumn), 123.0f); + assertEquals(rs.getDouble(stringColumn), 123.0); + } + + @Test + void testStringToURL() throws Exception { + String[] array = {"http://test.com", null, "https://test.com"}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(String)").get(0)); + + final String stringColumn = rs.getMetaData().getColumnName(2); + Assert.assertEquals(stringColumn, "VALUE"); + rs.next(); + assertEquals(rs.getObject(stringColumn), "http://test.com"); + assertEquals(rs.getObject(stringColumn, String.class), "http://test.com"); + assertEquals(rs.getString(stringColumn), rs.getObject(stringColumn, String.class)); + assertEquals(rs.getURL(stringColumn), rs.getObject(stringColumn, URL.class)); } } \ No newline at end of file From 5ef9c403f5bfc6e59d117d4932146daa3c481e6a Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 8 Dec 2025 16:21:54 -0800 Subject: [PATCH 09/15] added test for Array(Nullable(UInt16)) --- .../java/com/clickhouse/jdbc/ResultSetImplTest.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java index d18a925fb..4c3262dd6 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ResultSetImplTest.java @@ -367,22 +367,29 @@ public void testGetMetadata() throws SQLException { @Test(groups = {"integration"}) public void testGetResultSetFromArray() throws Exception { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - try (ResultSet rs = stmt.executeQuery("select [1, 2, 3, 4]::Array(UInt16) as v")) { + try (ResultSet rs = stmt.executeQuery("select [1, 2, null, 4]::Array(Nullable(UInt16)) as v")) { assertTrue(rs.next()); Array array = rs.getArray("v"); Assert.assertNotNull(array); Assert.assertEquals(array.getBaseType(), Types.INTEGER); - Assert.assertEquals(array.getBaseTypeName(), "UInt16"); + Assert.assertEquals(array.getBaseTypeName(), "Nullable(UInt16)"); Integer[] array2 = (Integer[]) array.getArray(); ResultSet rs2 = array.getResultSet(); Assert.assertTrue(rs2.isBeforeFirst()); Assert.assertFalse(rs2.isAfterLast()); + String valueColumn = rs2.getMetaData().getColumnName(2); for (int i = 0; i < array2.length; i++) { rs2.next(); - Assert.assertEquals(rs2.getInt(1), array2[i]); + if (i == 2 ) { + rs2.getInt(valueColumn); + Assert.assertTrue(rs2.wasNull()); + } else { + Assert.assertEquals(rs2.getInt(valueColumn), array2[i]); + Assert.assertFalse(rs2.wasNull()); + } } } } From e722fd9de0b4078bddd0c9861daf194e094251f6 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Mon, 8 Dec 2025 16:36:11 -0800 Subject: [PATCH 10/15] fixed columnIndex check --- .../api/data_formats/internal/ValueConverters.java | 12 ++++++------ .../java/com/clickhouse/jdbc/internal/JdbcUtils.java | 1 - .../com/clickhouse/jdbc/types/ArrayResultSet.java | 6 ++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java index 2568cd72e..ff61ed5b8 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -104,11 +104,11 @@ public ValueConverters() { stringMapBuilder.put(URL.class, this::convertStringToURL); mapBuilder.put(String.class, stringMapBuilder.build()); - mapBuilder.put(java.sql.Date.class, ImmutableMap.of(java.sql.Date.class, this::conveSqlDateToSqlDate, + mapBuilder.put(java.sql.Date.class, ImmutableMap.of(java.sql.Date.class, this::convertSqlDateToSqlDate, String.class, this::convertDateToString)); - mapBuilder.put(Time.class, ImmutableMap.of(Time.class, this::conveSqlTimeToSqlTime, + mapBuilder.put(Time.class, ImmutableMap.of(Time.class, this::convertSqlTimeToSqlTime, String.class, this::convertTimeToString)); - mapBuilder.put(Timestamp.class, ImmutableMap.of(Timestamp.class, this::conveSqlTimestampToSqlTimestamp, + mapBuilder.put(Timestamp.class, ImmutableMap.of(Timestamp.class, this::convertSqlTimestampToSqlTimestamp, String.class, this::convertTimestampToString)); classConverters = mapBuilder.build(); @@ -214,15 +214,15 @@ public BigDecimal convertNumberToBigDecimal(Object value) { } // Date & Time converters - public java.sql.Date conveSqlDateToSqlDate(Object value) { + public java.sql.Date convertSqlDateToSqlDate(Object value) { return (java.sql.Date) value; } - public Time conveSqlTimeToSqlTime(Object value) { + public Time convertSqlTimeToSqlTime(Object value) { return (Time) value; } - public Timestamp conveSqlTimestampToSqlTimestamp(Object value) { + public Timestamp convertSqlTimestampToSqlTimestamp(Object value) { return (Timestamp) value; } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 3f02ebb42..cbc07d28c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -18,7 +18,6 @@ import java.sql.SQLException; import java.sql.SQLType; import java.sql.Time; -import java.sql.Time; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index f22e93717..750e4cb0c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -55,12 +55,14 @@ public class ArrayResultSet implements ResultSet { private final ClickHouseDataType componentDataType; private final Class defaultClass; private final ClickHouseColumn column; + private final int columnCount; public ArrayResultSet(Object array, ClickHouseColumn column) { this.array = array; this.length = java.lang.reflect.Array.getLength(array); this.pos = -1; this.column = column; + this.columnCount = 2; // INDEX, VALUE List nestedColumns = column.getNestedColumns(); ClickHouseColumn valueColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : nestedColumns.get(0); @@ -71,7 +73,7 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); ValueConverters converters = new ValueConverters(); indexConverterMap = converters.getConvertersForType(Integer.class); - if (this.length > 1) { + if (this.length > 0) { Class itemClass = array.getClass().getComponentType(); if (itemClass == null) { itemClass = java.lang.reflect.Array.get(array, 0).getClass(); @@ -93,7 +95,7 @@ public boolean next() throws SQLException { } private void checkColumnIndex(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > length) { + if (columnIndex < 1 || columnIndex > columnCount) { throw new SQLException("Invalid column index: " + columnIndex); } } From 4785167510ffe0c814f16f9a6e767530f94325ed Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 9 Dec 2025 08:40:38 -0800 Subject: [PATCH 11/15] moved ValueConverters instance to static init of ArrayResultSet. Added wrapping try-catch for exception while convertions --- .../com/clickhouse/jdbc/types/ArrayResultSet.java | 14 ++++++++++---- .../clickhouse/jdbc/types/ArrayResultSetTest.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 750e4cb0c..7aa65270b 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -47,6 +47,8 @@ public class ArrayResultSet implements ResultSet { private static final ClickHouseColumn INDEX_COLUMN = ClickHouseColumn.of("INDEX", ClickHouseDataType.UInt32, false, 0, 0); private static final String VALUE_COLUMN = "VALUE"; + private static final ValueConverters defaultValueConverters = new ValueConverters(); + private int fetchDirection = ResultSet.FETCH_FORWARD; private int fetchSize = 0; private boolean wasNull = false; @@ -71,14 +73,13 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { , "", "", "", JdbcUtils.DATA_TYPE_CLASS_MAP); this.componentDataType = valueColumn.getDataType(); this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); - ValueConverters converters = new ValueConverters(); - indexConverterMap = converters.getConvertersForType(Integer.class); + indexConverterMap = defaultValueConverters.getConvertersForType(Integer.class); if (this.length > 0) { Class itemClass = array.getClass().getComponentType(); if (itemClass == null) { itemClass = java.lang.reflect.Array.get(array, 0).getClass(); } - converterMap = converters.getConvertersForType(itemClass); + converterMap = defaultValueConverters.getConvertersForType(itemClass); } else { // empty array - no values to convert converterMap = null; @@ -127,7 +128,12 @@ private Object getValueAsObject(int columnIndex, Class type, Object defaultVa // if there is something to convert. type == Object.class means no conversion Function converter = valueConverterMap.get(type); if (converter != null) { - value = converter.apply(value); + try { + value = converter.apply(value); + } catch (Exception e) { + throw new SQLException("Failed to convert value of " + value.getClass() + " to " + type, + ExceptionUtils.SQL_STATE_DATA_EXCEPTION, e); + } } else { throw new SQLException("Value of " + value.getClass() + " cannot be converted to " + type); } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 56c6cd3b9..5cdb436a5 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -505,4 +505,19 @@ void testStringToURL() throws Exception { assertEquals(rs.getString(stringColumn), rs.getObject(stringColumn, String.class)); assertEquals(rs.getURL(stringColumn), rs.getObject(stringColumn, URL.class)); } + + @Test + void testInvalidStringConverts() throws Exception { + String[] array = {"abc"}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(String)").get(0)); + + final String stringColumn = rs.getMetaData().getColumnName(2); + rs.next(); + Assert.assertThrows(SQLException.class, () -> rs.getByte(stringColumn)); + Assert.assertThrows(SQLException.class, () -> rs.getShort(stringColumn)); + Assert.assertThrows(SQLException.class, () -> rs.getInt(stringColumn)); + Assert.assertThrows(SQLException.class, () -> rs.getLong(stringColumn)); + Assert.assertThrows(SQLException.class, () -> rs.getFloat(stringColumn)); + Assert.assertThrows(SQLException.class, () -> rs.getDouble(stringColumn)); + } } \ No newline at end of file From 9ed3fdc7bd66c44539eece68abf025003aedff15 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 9 Dec 2025 09:38:05 -0800 Subject: [PATCH 12/15] Extended empty array test method --- .../com/clickhouse/jdbc/types/ArrayResultSet.java | 2 +- .../clickhouse/jdbc/types/ArrayResultSetTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index 7aa65270b..e8a815519 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -458,7 +458,7 @@ public boolean isFirst() throws SQLException { @Override public boolean isLast() throws SQLException { - return pos == length - 1; + return length > 0 && pos == length - 1; } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 5cdb436a5..09c4357f1 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -283,7 +283,22 @@ void testStringValues() throws SQLException { @Test void testEmptyArray() throws SQLException { ArrayResultSet rs = new ArrayResultSet(new Object[0], ClickHouseColumn.parse("v Array(Int32)").get(0)); + + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isLast()); + Assert.assertFalse(rs.isFirst()); + assertFalse(rs.next()); + + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isLast()); + Assert.assertFalse(rs.isFirst()); + + Assert.assertThrows(SQLException.class, () -> rs.getString("col1")); + Assert.assertThrows(SQLException.class, () -> rs.getObject("col1")); + Assert.assertThrows(SQLException.class, () -> rs.getInt("col1")); } @Test From 23d1f94f18bfa0f863f2dcc5ac8f4216ea6b139e Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 9 Dec 2025 10:02:36 -0800 Subject: [PATCH 13/15] fixed wasNull method to not reset the flag and added test. Fixed boolean convertion to a number --- .../client/api/data_formats/internal/ValueConverters.java | 8 ++++---- .../java/com/clickhouse/jdbc/types/ArrayResultSet.java | 4 +--- .../com/clickhouse/jdbc/types/ArrayResultSetTest.java | 2 ++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java index ff61ed5b8..e94283cf9 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/ValueConverters.java @@ -120,7 +120,7 @@ public Boolean convertBooleanToBoolean(Object value) { } public Number convertBooleanToNumber(Object value) { - return ((Number) (((Boolean)value) ? 1 : 0)).longValue(); + return ((Boolean) value) ? 1L : 0L; } public String convertBooleanToString(Object value) { @@ -227,15 +227,15 @@ public Timestamp convertSqlTimestampToSqlTimestamp(Object value) { } public String convertDateToString(Object value) { - return ((Date)value).toString(); + return value.toString(); } public String convertTimeToString(Object value) { - return ((Time) value).toString(); + return value.toString(); } public String convertTimestampToString(Object value) { - return ((Timestamp) value).toString(); + return value.toString(); } /** diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index e8a815519..fa9de0318 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -159,9 +159,7 @@ public void close() throws SQLException { @Override public boolean wasNull() throws SQLException { - boolean tmp = wasNull; - wasNull = false; - return tmp; + return wasNull; } @Override diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 09c4357f1..0daf6a379 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -141,11 +141,13 @@ void testNullValues() throws SQLException { assertFalse(rs.wasNull()); assertEquals(rs.getInt(2), array[0]); assertFalse(rs.wasNull()); + assertFalse(rs.wasNull()); rs.next(); assertFalse(rs.wasNull()); assertEquals(rs.getInt(2), 0); assertTrue(rs.wasNull()); + assertTrue(rs.wasNull()); } @Test(dataProvider = "testPrimitiveValues") From 32bbfc89a1ef979685be2eb08507fbddf44cfce4 Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 9 Dec 2025 10:38:09 -0800 Subject: [PATCH 14/15] added lazy init of conversion map to make logic more coherent --- .../clickhouse/jdbc/types/ArrayResultSet.java | 79 ++++++++++--------- .../jdbc/types/ArrayResultSetTest.java | 27 ++++++- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index fa9de0318..ff727fb8c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -10,7 +10,6 @@ import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; -import java.math.RoundingMode; import java.net.MalformedURLException; import java.net.URL; import java.sql.Array; @@ -30,7 +29,6 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; -import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Calendar; import java.util.List; @@ -52,7 +50,7 @@ public class ArrayResultSet implements ResultSet { private int fetchDirection = ResultSet.FETCH_FORWARD; private int fetchSize = 0; private boolean wasNull = false; - private final Map, Function> converterMap; + private Map, Function> converterMap; private final Map, Function> indexConverterMap; private final ClickHouseDataType componentDataType; private final Class defaultClass; @@ -74,16 +72,6 @@ public ArrayResultSet(Object array, ClickHouseColumn column) { this.componentDataType = valueColumn.getDataType(); this.defaultClass = JdbcUtils.DATA_TYPE_CLASS_MAP.get(componentDataType); indexConverterMap = defaultValueConverters.getConvertersForType(Integer.class); - if (this.length > 0) { - Class itemClass = array.getClass().getComponentType(); - if (itemClass == null) { - itemClass = java.lang.reflect.Array.get(array, 0).getClass(); - } - converterMap = defaultValueConverters.getConvertersForType(itemClass); - } else { - // empty array - no values to convert - converterMap = null; - } } @Override @@ -107,39 +95,58 @@ private void checkRowPosition() throws SQLException { } } + private Map, Function> initValueConverterMapIfNeeded(Object nonNullValue) { + if (converterMap == null) { + if (array.getClass().getComponentType() == Object.class) { + converterMap = defaultValueConverters.getConvertersForType(nonNullValue.getClass()); + } else { + converterMap = defaultValueConverters.getConvertersForType(array.getClass().getComponentType()); + } + } + return converterMap; + } + + private Object convertValue(Object value, Class targetType, Map, Function> valueConverterMap) throws SQLException { + if (value == null || targetType == value.getClass() || targetType == Object.class) { + return value; + } + + Function converter = valueConverterMap.get(targetType); + if (converter != null) { + try { + return converter.apply(value); + } catch (Exception e) { + throw new SQLException("Failed to convert value of " + value.getClass() + " to " + targetType, + ExceptionUtils.SQL_STATE_DATA_EXCEPTION, e); + } + } else { + throw new SQLException("Value of " + value.getClass() + " cannot be converted to " + targetType); + } + } + private Object getValueAsObject(int columnIndex, Class type, Object defaultValue) throws SQLException { checkColumnIndex(columnIndex); checkRowPosition(); - Object value; - Map, Function> valueConverterMap; if (columnIndex == 1) { - value = pos + 1; - valueConverterMap = indexConverterMap; + Integer value = pos + 1; + return convertValue(value, type, indexConverterMap); } else { - value = java.lang.reflect.Array.get(array, pos); - valueConverterMap = converterMap; - } + Object value = java.lang.reflect.Array.get(array, pos); + wasNull = value == null; + if (value == null) { + return defaultValue; + } - if (columnIndex != 1 && value != null && type == Array.class) { - ClickHouseColumn nestedColumn = column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : column.getNestedColumns().get(0); - return new com.clickhouse.jdbc.types.Array(nestedColumn, JdbcUtils.arrayToObjectArray(value)); - } else if (value != null && type != Object.class) { - // if there is something to convert. type == Object.class means no conversion - Function converter = valueConverterMap.get(type); - if (converter != null) { - try { - value = converter.apply(value); - } catch (Exception e) { - throw new SQLException("Failed to convert value of " + value.getClass() + " to " + type, - ExceptionUtils.SQL_STATE_DATA_EXCEPTION, e); - } + if (type == Array.class) { + ClickHouseColumn nestedColumn = + column.getArrayNestedLevel() == 1 ? column.getArrayBaseColumn() : column.getNestedColumns().get(0); + return new com.clickhouse.jdbc.types.Array(nestedColumn, JdbcUtils.arrayToObjectArray(value)); } else { - throw new SQLException("Value of " + value.getClass() + " cannot be converted to " + type); + Map, Function> valueConverterMap = initValueConverterMapIfNeeded(value); + return convertValue(value, type, valueConverterMap); } } - wasNull = value == null; - return value == null ? defaultValue : value; } private void throwReadOnlyException() throws SQLException { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java index 0daf6a379..077b1f78d 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/types/ArrayResultSetTest.java @@ -134,12 +134,17 @@ void testCursorNavigation() throws SQLException { @Test void testNullValues() throws SQLException { - Integer[] array = {1, null, 3, 4, 5}; + Integer[] array = {null, 1, null, 3, 4, 5}; ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Int32)").get(0)); rs.next(); - assertFalse(rs.wasNull()); - assertEquals(rs.getInt(2), array[0]); + assertEquals(rs.getInt(2), 0); + assertTrue(rs.wasNull()); + assertTrue(rs.wasNull()); + + rs.next(); + assertTrue(rs.wasNull()); + assertEquals(rs.getInt(2), array[1]); assertFalse(rs.wasNull()); assertFalse(rs.wasNull()); @@ -537,4 +542,20 @@ void testInvalidStringConverts() throws Exception { Assert.assertThrows(SQLException.class, () -> rs.getFloat(stringColumn)); Assert.assertThrows(SQLException.class, () -> rs.getDouble(stringColumn)); } + + @Test + void testArrayOfObjects() throws Exception { + Object[] array = {null, 2, 3}; + ArrayResultSet rs = new ArrayResultSet(array, ClickHouseColumn.parse("v Array(Nullable(UInt32))").get(0)); + + final String valueColumn = rs.getMetaData().getColumnName(2); + rs.next(); + assertEquals(rs.getObject(valueColumn), null); + assertTrue(rs.wasNull()); + + rs.next(); + assertEquals(rs.getInt(valueColumn), 2); + assertEquals(rs.getObject(valueColumn, String.class), "2"); + assertEquals(rs.getString(valueColumn), rs.getObject(valueColumn, String.class)); + } } \ No newline at end of file From c6c2f8569bee80117789550693cdc277f0fdf2bf Mon Sep 17 00:00:00 2001 From: Sergey Chernov Date: Tue, 9 Dec 2025 10:50:56 -0800 Subject: [PATCH 15/15] fixed boundary check for index column --- .../java/com/clickhouse/jdbc/types/ArrayResultSet.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java index ff727fb8c..e36571215 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/types/ArrayResultSet.java @@ -39,7 +39,7 @@ public class ArrayResultSet implements ResultSet { private final Object array; private final int length; - private Integer pos; + private int pos; private boolean closed; private ResultSetMetaDataImpl metadata; @@ -184,10 +184,10 @@ public boolean getBoolean(int columnIndex) throws SQLException { @Override public byte getByte(int columnIndex) throws SQLException { if (columnIndex == 1) { - if (pos < Byte.MAX_VALUE) { + if (pos + 1 < Byte.MAX_VALUE) { return (byte) getRow(); } else { - throw new SQLException("INDEX column value too big and cannot be get as byte"); + throw new SQLException("INDEX column value too big and cannot be retrieved as byte"); } } return ((Number) getValueAsObject(columnIndex, Byte.class, 0)).byteValue(); @@ -196,10 +196,10 @@ public byte getByte(int columnIndex) throws SQLException { @Override public short getShort(int columnIndex) throws SQLException { if (columnIndex == 1) { - if (pos < Short.MAX_VALUE) { + if (pos + 1 < Short.MAX_VALUE) { return (short) getRow(); } else { - throw new SQLException("INDEX column value too big and cannot be get as short"); + throw new SQLException("INDEX column value too big and cannot be retrieved as short"); } } return ((Number) getValueAsObject(columnIndex, Short.class, 0)).shortValue();