diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java index ec08e4ea3..e6a9c2557 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/ClickHouseBinaryFormatReader.java @@ -11,7 +11,12 @@ import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; -import java.time.*; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; import java.util.List; import java.util.Map; diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index 3ac073e5f..d393d8503 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -135,7 +135,7 @@ public boolean readToPOJO(Map deserializers, Obje * It is still internal method and should be used with care. * Usually this method is called to read next record into internal object and affects hasNext() method. * So after calling this one: - * - hasNext(), next() should not be called + * - hasNext(), next() and get methods cannot be called * - stream should be read with readRecord() method fully * * @param record diff --git a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java index 5b08248b7..9d5ae7b93 100644 --- a/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java @@ -10,7 +10,6 @@ import com.clickhouse.client.api.ClientException; import com.clickhouse.client.api.DataTypeUtils; import com.clickhouse.client.api.ServerException; -import com.clickhouse.client.api.command.CommandResponse; import com.clickhouse.client.api.command.CommandSettings; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.data_formats.internal.BinaryStreamReader; @@ -43,7 +42,6 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import org.testng.util.Strings; import java.io.BufferedReader; import java.io.BufferedWriter; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index efcb8b7e2..a38515944 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -130,6 +130,10 @@ public void setDefaultQuerySettings(QuerySettings settings) { this.defaultQuerySettings = settings; } + public Calendar getDefaultCalendar() { + return defaultCalendar; + } + public String getServerVersion() throws SQLException { GenericRecord result = client.queryAll("SELECT version() as server_version").stream() .findFirst().orElseThrow(() -> new SQLException("Failed to retrieve server version.", ExceptionUtils.SQL_STATE_CLIENT_ERROR)); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index c4647da35..4bcc48d4c 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -39,7 +39,7 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper { private static final Logger log = LoggerFactory.getLogger(ResultSetImpl.class); - private ResultSetMetaData metaData; + private ResultSetMetaDataImpl metaData; protected ClickHouseBinaryFormatReader reader; private QueryResponse response; private boolean closed; @@ -49,9 +49,9 @@ public class ResultSetImpl implements ResultSet, JdbcV2Wrapper { private final FeatureManager featureManager; - private static final int AFTER_LAST = -1; - private static final int BEFORE_FIRST = 0; - private static final int FIRST_ROW = 1; + public static final int AFTER_LAST = -1; + public static final int BEFORE_FIRST = 0; + public static final int FIRST_ROW = 1; private int rowPos; private int fetchSize; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java new file mode 100644 index 000000000..5c13569b1 --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DetachedResultSet.java @@ -0,0 +1,1175 @@ +package com.clickhouse.jdbc.internal; + +import com.clickhouse.jdbc.JdbcV2Wrapper; +import com.clickhouse.jdbc.ResultSetImpl; + +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.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.function.Consumer; + +/** + * This class intended only for internal use because is based on internal API. Do not try using it in your application. + * We may change it at any time because it is internal class. + * This class should not be used in any other places than metadata resultsets. + * This class will close parent resultset when close on this is called. + */ +public class DetachedResultSet implements ResultSet, JdbcV2Wrapper { + + private List> records; + + private ListIterator> iterator; + + private ResultSetMetaData metaData; + + private final Calendar defaultCalendar; + + private Map record; + + private boolean wasNull; + + private int row; + + private final int lastRow; + + private boolean closed; + + private Map columnMap; + + private DetachedResultSet(List> records, ResultSetMetaData metaData, Calendar defaultCalendar) throws SQLException { + this.records = records; + this.iterator = records.listIterator(); + this.metaData = metaData; + this.defaultCalendar = defaultCalendar; + this.wasNull = false; + this.row = ResultSetImpl.BEFORE_FIRST; + this.lastRow = records.size(); + this.closed = false; + + this.columnMap = new HashMap<>(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + this.columnMap.put(metaData.getColumnName(i), i); + } + } + + public static DetachedResultSet createFromResultSet(ResultSet resultSet, Calendar defaultCalendar, Collection>> mutators) throws SQLException { + ResultSetMetaData metaData = resultSet.getMetaData(); + List> records = new ArrayList<>(); + while (resultSet.next()) { + Map record = new HashMap<>(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + record.put(metaData.getColumnLabel(i), resultSet.getObject(i)); + } + for (Consumer> mutator : mutators) { + mutator.accept(record); + } + records.add(record); + } + return new DetachedResultSet(records, metaData, defaultCalendar); + } + + @Override + public boolean next() throws SQLException { + ensureOpen(); + if (iterator.hasNext()) { + row++; + record = iterator.next(); + return true; + } + row = ResultSetImpl.AFTER_LAST; + return false; + } + + @Override + public void close() throws SQLException { + closed = true; + metaData = null; + record = null; + iterator = null; + } + + @Override + public boolean wasNull() throws SQLException { + ensureOpen(); + return wasNull; + } + + @Override + public String getString(int columnIndex) throws SQLException { + return getString(metaData.getColumnLabel(columnIndex)); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return getBoolean(metaData.getColumnLabel(columnIndex)); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return getByte(metaData.getColumnLabel(columnIndex)); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return getShort(metaData.getColumnLabel(columnIndex)); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return getInt(metaData.getColumnLabel(columnIndex)); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return getLong(metaData.getColumnLabel(columnIndex)); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return getFloat(metaData.getColumnLabel(columnIndex)); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return getDouble(metaData.getColumnLabel(columnIndex)); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + return getBigDecimal(metaData.getColumnLabel(columnIndex), scale); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + return getBytes(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return getDate(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return getTime(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return getTimestamp(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + return getAsciiStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + return getUnicodeStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + return getBinaryStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public String getString(String columnLabel) throws SQLException { + ensureOpen(); + return getObject(columnLabel, String.class); + } + + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + ensureOpen(); + return (boolean) getObjectImpl(columnLabel, Boolean.class, Boolean.FALSE); + } + + private Number getNumber(String columnLabel) throws SQLException { + ensureOpen(); + return (Number) getObjectImpl(columnLabel, Number.class, BigInteger.ZERO); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + ensureOpen(); + return getNumber(columnLabel).byteValue(); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + ensureOpen(); + return getNumber(columnLabel).shortValue(); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + ensureOpen(); + return getNumber(columnLabel).intValue(); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + ensureOpen(); + return getNumber(columnLabel).longValue(); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + ensureOpen(); + return getNumber(columnLabel).floatValue(); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getNumber(columnLabel).doubleValue(); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + ensureOpen(); + return getObject(columnLabel, BigDecimal.class); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + ensureOpen(); + return getObject(columnLabel, byte[].class); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + ensureOpen(); + return getDate(columnLabel, defaultCalendar); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + ensureOpen(); + return getTime(columnLabel, defaultCalendar); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + ensureOpen(); + return getTimestamp(columnLabel, defaultCalendar); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + ensureOpen(); + return getObject(columnLabel, InputStream.class); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + ensureOpen(); + return getObject(columnLabel, InputStream.class); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + ensureOpen(); + return getObject(columnLabel, InputStream.class); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + ensureOpen(); + return null; + } + + @Override + public void clearWarnings() throws SQLException { + ensureOpen(); + } + + @Override + public String getCursorName() throws SQLException { + ensureOpen(); + return ""; + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + ensureOpen(); + return metaData; + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(columnLabel, Object.class); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + ensureOpen(); + Integer index = columnMap.get(columnLabel); + if (index == null) { + throw new SQLException("Column not found: " + columnLabel, ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + return index; + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + return getObject(columnIndex, Reader.class); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + return getObject(columnLabel, Reader.class); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return getBigDecimal(metaData.getColumnLabel(columnIndex)); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getObject(columnLabel, BigDecimal.class); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + ensureOpen(); + return row == ResultSetImpl.BEFORE_FIRST; + } + + @Override + public boolean isAfterLast() throws SQLException { + ensureOpen(); + return row == ResultSetImpl.AFTER_LAST; + } + + @Override + public boolean isFirst() throws SQLException { + ensureOpen(); + return row == ResultSetImpl.FIRST_ROW; + } + + @Override + public boolean isLast() throws SQLException { + ensureOpen(); + return row == lastRow; + } + + @Override + public void beforeFirst() throws SQLException { + ensureOpen(); + } + + @Override + public void afterLast() throws SQLException { + ensureOpen(); + } + + @Override + public boolean first() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public boolean last() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public int getRow() throws SQLException { + ensureOpen(); + return Math.max(row, 0); + } + + @Override + public boolean absolute(int row) throws SQLException { + ensureOpen(); + return false; + } + + @Override + public boolean relative(int rows) throws SQLException { + ensureOpen(); + return false; + } + + @Override + public boolean previous() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + ensureOpen(); + if (direction != ResultSet.FETCH_FORWARD) { + throw new SQLException("This result set object is of FORWARD ONLY type. Only ResultSet.FETCH_FORWARD is allowed as fetchDirection."); + } + } + + @Override + public int getFetchDirection() throws SQLException { + ensureOpen(); + return FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + ensureOpen(); + } + + @Override + public int getFetchSize() throws SQLException { + ensureOpen(); + return 0; + } + + @Override + public int getType() throws SQLException { + ensureOpen(); + return TYPE_FORWARD_ONLY; + } + + @Override + public int getConcurrency() throws SQLException { + ensureOpen(); + return CONCUR_READ_ONLY; + } + + @Override + public boolean rowUpdated() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public boolean rowInserted() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public boolean rowDeleted() throws SQLException { + ensureOpen(); + return false; + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + ensureOpen(); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + ensureOpen(); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + ensureOpen(); + } + + @Override + public void insertRow() throws SQLException { + ensureOpen(); + } + + @Override + public void updateRow() throws SQLException { + ensureOpen(); + } + + @Override + public void deleteRow() throws SQLException { + ensureOpen(); + } + + @Override + public void refreshRow() throws SQLException { + ensureOpen(); + } + + @Override + public void cancelRowUpdates() throws SQLException { + ensureOpen(); + } + + @Override + public void moveToInsertRow() throws SQLException { + ensureOpen(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + ensureOpen(); + } + + @Override + public Statement getStatement() throws SQLException { + ensureOpen(); + return null; // should return null as it is a detached result set + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex), map); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + return getRef(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + return getBlob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + return getClob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + return getArray(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + ensureOpen(); + if (map == null) { + throw new SQLException("map must be not null", ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + int columnIndex = columnMap.getOrDefault(columnLabel, -1); + if (columnIndex == -1) { + throw new SQLException("column " + columnLabel + " doesn't exist", ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + + String typeName = metaData.getColumnTypeName(columnIndex); + return getObject(columnLabel, map.get(typeName)); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + return getObject(columnLabel, Ref.class); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + return getObject(columnLabel, Blob.class); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + return getObject(columnLabel, Clob.class); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + return getObject(columnLabel, Array.class); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return getDate(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); + try { + Date date = getObject(columnLabel, Date.class); + if (date != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalDate ld = date.toLocalDate(); + c.set(ld.getYear(), ld.getMonthValue() - 1, ld.getDayOfMonth(), 0, 0, 0); + date = new Date(c.getTimeInMillis()); + } + return date; + } catch (Exception e) { + throw new SQLException(String.format("Method: getDate(\"%s\") encountered an exception.", columnLabel), e); + } + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + return getTime(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); + try { + Time time = getObject(columnLabel, Time.class); + if (time != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalTime ld = time.toLocalTime(); + c.set(1970, Calendar.JANUARY, 1, ld.getHour(), ld.getMinute(), ld.getSecond()); + time = new Time(c.getTimeInMillis()); + } + return time; + } catch (Exception e) { + throw new SQLException(String.format("Method: getTime(\"%s\") encountered an exception.", columnLabel), e); + } + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + return getTimestamp(metaData.getColumnLabel(columnIndex), cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + ensureOpen(); + try { + Timestamp timestamp = getObject(columnLabel, Timestamp.class); + if (timestamp != null) { + Calendar c = (Calendar) (cal != null ? cal : defaultCalendar).clone(); + c.clear(); + LocalDateTime ldt = timestamp.toLocalDateTime(); + c.set(ldt.getYear(), ldt.getMonthValue() - 1, ldt.getDayOfMonth(), ldt.getHour(), ldt.getMinute(), + ldt.getSecond()); + timestamp = new Timestamp(c.getTimeInMillis()); + timestamp.setNanos(ldt.getNano()); + return timestamp; + } + return timestamp; + } catch (Exception e) { + throw new SQLException(String.format("Method: getTimestamp(\"%s\") encountered an exception.", columnLabel), e); + } + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + return getURL(metaData.getColumnLabel(columnIndex)); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + return getObject(columnLabel, URL.class); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + ensureOpen(); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + return getRowId(metaData.getColumnLabel(columnIndex)); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + return getObject(columnLabel, RowId.class); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + ensureOpen(); + } + + @Override + public int getHoldability() throws SQLException { + ensureOpen(); + return HOLD_CURSORS_OVER_COMMIT; // this result set remains open + } + + @Override + public boolean isClosed() throws SQLException { + return closed; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + ensureOpen(); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + return getNClob(metaData.getColumnLabel(columnIndex)); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + return getObject(columnLabel, NClob.class); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + return getSQLXML(metaData.getColumnLabel(columnIndex)); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + return getObject(columnLabel, SQLXML.class); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + ensureOpen(); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + ensureOpen(); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + return getNString(metaData.getColumnLabel(columnIndex)); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + return getObject(columnLabel, String.class); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + return getNCharacterStream(metaData.getColumnLabel(columnIndex)); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + return getObject(columnLabel, Reader.class); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + ensureOpen(); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + ensureOpen(); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + ensureOpen(); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + return getObject(metaData.getColumnLabel(columnIndex), type); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + ensureOpen(); + return (T) getObjectImpl(columnLabel, type, null); + } + + private Object getObjectImpl(String columnLabel, Class type, Object nullValue) throws SQLException { + Object value = record.get(columnLabel); + wasNull = value == null; + if (wasNull) { + return nullValue; + } + + return JdbcUtils.convert(value, type); + } + + private void ensureOpen() throws SQLException { + if (closed) { + throw new SQLException("ResultSet is closed.", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION); + } + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java deleted file mode 100644 index 77ee23a4e..000000000 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/MetadataResultSet.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.clickhouse.jdbc.internal; - -import com.clickhouse.client.api.metadata.TableSchema; -import com.clickhouse.data.ClickHouseColumn; -import com.clickhouse.jdbc.ResultSetImpl; -import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl; - -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.UnaryOperator; - -public class MetadataResultSet extends ResultSetImpl { - private final Map> columnTransformers = new HashMap<>(); - private final String[] cachedColumnLabels; - - private final OverridingSchemaAdaptor overridingSchemaAdaptor; - - public MetadataResultSet(ResultSetImpl resultSet) throws SQLException { - super(resultSet); - this.overridingSchemaAdaptor = new OverridingSchemaAdaptor(resultSet.getSchema()); - this.setMetaData(new ResultSetMetaDataImpl(overridingSchemaAdaptor.getColumns(), - overridingSchemaAdaptor.getDatabaseName(), "", - overridingSchemaAdaptor.getTableName(), JdbcUtils.DATA_TYPE_CLASS_MAP)); - ResultSetMetaData metaData = getMetaData(); - int count = metaData.getColumnCount(); - cachedColumnLabels = new String[count]; - for (int i = 1; i <= count; i++) { - cachedColumnLabels[i - 1] = metaData.getColumnLabel(i).toUpperCase(); - } - } - - /** - * Registers a transformer function for a given column. - * The transformer takes the original String value and returns a new String. - * - * @param columnLabel The name of the column (case-insensitive). - * @param transformer The function that transforms the value. - */ - public MetadataResultSet transform(String columnLabel, ClickHouseColumn column, UnaryOperator transformer) { - if (columnLabel != null && transformer != null) { - columnTransformers.put(columnLabel.toUpperCase(), transformer); - } - overridingSchemaAdaptor.setOverriddenColumn(overridingSchemaAdaptor.nameToIndex(columnLabel), column); - return this; - } - - @Override - public String getString(String columnLabel) throws SQLException { - String value = super.getString(columnLabel); - UnaryOperator transformer = columnTransformers.get(columnLabel.toUpperCase()); - if (transformer != null && value != null) { - return transformer.apply(value); - } - return value; - } - - @Override - public int getInt(String columnLabel) throws SQLException { - String stringValue = getString(columnLabel); - if (stringValue == null || stringValue.trim().isEmpty()) { - return 0; - } - try { - return Integer.parseInt(stringValue); - } catch (NumberFormatException e) { - throw new SQLException("Value for column '" + columnLabel + "' is not a valid integer: " + stringValue, e); - } - } - - @Override - public String getString(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > cachedColumnLabels.length) { - throw new SQLException("Invalid column index: " + columnIndex); - } - return getString(cachedColumnLabels[columnIndex - 1]); - } - - @Override - public int getInt(int columnIndex) throws SQLException { - if (columnIndex < 1 || columnIndex > cachedColumnLabels.length) { - throw new SQLException("Invalid column index: " + columnIndex); - } - return getInt(cachedColumnLabels[columnIndex - 1]); - } - - @Override - public byte getByte(int columnIndex) throws SQLException { - return (byte) getInt(columnIndex); - } - - @Override - public short getShort(int columnIndex) throws SQLException { - return (short) getInt(columnIndex); - } - - @Override - public long getLong(int columnIndex) throws SQLException { - return getInt(columnIndex); - } - - @Override - public byte getByte(String columnLabel) throws SQLException { - return (byte) getInt(columnLabel); - } - - @Override - public short getShort(String columnLabel) throws SQLException { - return (short) getInt(columnLabel); - } - - @Override - public long getLong(String columnLabel) throws SQLException { - return getInt(columnLabel); - } - - @Override - public TableSchema getSchema() { - return overridingSchemaAdaptor; - } - - private static class OverridingSchemaAdaptor extends TableSchema { - private final TableSchema originalSchema; - - private final List overriddenColumns; - - public OverridingSchemaAdaptor(TableSchema originalSchema) { - super(Collections.emptyList()); - this.originalSchema = originalSchema; - this.overriddenColumns = new ArrayList<>(originalSchema.getColumns()); - } - - public void setOverriddenColumn(int index, ClickHouseColumn column) { - if (index < 0 || index >= overriddenColumns.size()) { - throw new IndexOutOfBoundsException("Index " + index + " is out of bounds for overridden columns."); - } - overriddenColumns.set(index, column); - } - - @Override - public List getColumns() { - return overriddenColumns; - } - - @Override - public String getDatabaseName() { - return originalSchema.getDatabaseName(); - } - - @Override - public String getTableName() { - return originalSchema.getTableName(); - } - - @Override - public boolean hasDefaults() { - return originalSchema.hasDefaults(); - } - - @Override - public String getQuery() { - return originalSchema.getQuery(); - } - - @Override - public ClickHouseColumn getColumnByName(String name) { - return overriddenColumns.get(originalSchema.nameToIndex(name)); - } - - @Override - public ClickHouseColumn getColumnByIndex(int colIndex) { - return overriddenColumns.get(colIndex - 1); - } - - @Override - public String indexToName(int index) { - return originalSchema.indexToName(index); - } - - @Override - public String columnIndexToName(int index) { - return originalSchema.columnIndexToName(index); - } - - @Override - public int nameToColumnIndex(String name) { - return originalSchema.nameToColumnIndex(name); - } - - @Override - public int nameToIndex(String name) { - return originalSchema.nameToIndex(name); - } - - @Override - public String toString() { - return originalSchema.toString(); - } - } -} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index 7294afdb3..a42429548 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -11,7 +11,7 @@ import com.clickhouse.jdbc.ResultSetImpl; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; -import com.clickhouse.jdbc.internal.MetadataResultSet; +import com.clickhouse.jdbc.internal.DetachedResultSet; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; @@ -22,7 +22,14 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLType; +import java.sql.Statement; +import java.sql.Types; import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; public class DatabaseMetaDataImpl implements java.sql.DatabaseMetaData, JdbcV2Wrapper { private static final Logger log = LoggerFactory.getLogger(DatabaseMetaDataImpl.class); @@ -830,18 +837,16 @@ public ResultSet getTableTypes() throws SQLException { } } - private static final ClickHouseColumn DATA_TYPE_COL = ClickHouseColumn.of("DATA_TYPE", ClickHouseDataType.Int32.name()) ; @Override @SuppressWarnings({"squid:S2095", "squid:S2077"}) public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - //TODO: Best way to convert type to JDBC data type // TODO: handle useCatalogs == true and return schema catalog name - String sql = "SELECT " + + final String sql = "SELECT " + catalogPlaceholder + " AS TABLE_CAT, " + "database AS TABLE_SCHEM, " + "table AS TABLE_NAME, " + "name AS COLUMN_NAME, " + - "system.columns.type AS DATA_TYPE, " + + "toInt32(" + Types.OTHER + ") AS DATA_TYPE, " + "type AS TYPE_NAME, " + "toInt32(" + generateSqlTypeSizes("system.columns.type") + ") AS COLUMN_SIZE, " + "toInt32(0) AS BUFFER_LENGTH, " + @@ -866,9 +871,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa " AND table LIKE " + SQLUtils.enquoteLiteral(tableNamePattern == null ? "%" : tableNamePattern) + " AND name LIKE " + SQLUtils.enquoteLiteral(columnNamePattern == null ? "%" : columnNamePattern) + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; - try { - return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(sql)) - .transform(DATA_TYPE_COL.getColumnName(), DATA_TYPE_COL, DatabaseMetaDataImpl::columnDataTypeToSqlType); + try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery(sql)) { + return DetachedResultSet.createFromResultSet(rs, connection.getDefaultCalendar(), GET_COLUMNS_RS_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } @@ -887,18 +891,23 @@ private static String generateSqlTypeSizes(String columnName) { return sql.toString(); } - private static String columnDataTypeToSqlType(String value) { - SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(value); + + private static final Consumer> DATA_TYPE_VALUE_FUNCTION = row -> { + String typeName = (String) row.get("TYPE_NAME"); + SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(typeName); if (type == null) { try { - type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", value).getDataType()); + type = JdbcUtils.convertToSqlType(ClickHouseColumn.of("v1", typeName).getDataType()); } catch (Exception e) { - log.error("Failed to convert column data type to SQL type: {}", value, e); + log.error("Failed to convert column data type to SQL type: {}", typeName, e); type = JDBCType.OTHER; // In case of error, return SQL type 0 } } - return String.valueOf(type.getVendorTypeNumber()); - } + + row.put("DATA_TYPE", type.getVendorTypeNumber()); + }; + + private static final List>> GET_COLUMNS_RS_MUTATORS = Collections.singletonList(DATA_TYPE_VALUE_FUNCTION); @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { @@ -986,7 +995,6 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr "AND system.tables.database ILIKE '" + (schema == null ? "%" : schema) + "' " + "AND system.tables.name ILIKE '" + (table == null ? "%" : table) + "' " + "ORDER BY COLUMN_NAME"; - log.debug("getPrimaryKeys: %s", sql); return connection.createStatement().executeQuery(sql); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); @@ -1067,49 +1075,47 @@ public ResultSet getCrossReference(String parentCatalog, String parentSchema, St } } - private static final ClickHouseColumn NULLABLE_COL = ClickHouseColumn.of("NULLABLE", ClickHouseDataType.Int16.name()); @Override @SuppressWarnings({"squid:S2095"}) public ResultSet getTypeInfo() throws SQLException { - try { - return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(DATA_TYPE_INFO_SQL)) - .transform(DATA_TYPE_COL.getColumnName(), DATA_TYPE_COL, DatabaseMetaDataImpl::dataTypeToSqlTypeInt) - .transform(NULLABLE_COL.getColumnName(), NULLABLE_COL, DatabaseMetaDataImpl::dataTypeNullability); + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(DATA_TYPE_INFO_SQL)) { + return DetachedResultSet.createFromResultSet(rs, connection.getDefaultCalendar(), GET_TYPE_INFO_MUTATORS); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); } } - private static String dataTypeToSqlTypeInt(String type) { - SQLType sqlType = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(type); - return sqlType == null ? String.valueOf(JDBCType.OTHER.getVendorTypeNumber()) : - String.valueOf(sqlType.getVendorTypeNumber()); - } - - private static String dataTypeNullability(String type) { + private static final Consumer> NULLABILITY_VALUE_FUNCTION = (row) -> { + String type = (String) row.get("TYPE_NAME"); + int nullability= java.sql.DatabaseMetaData.typeNoNulls; if (type.equals(ClickHouseDataType.Nullable.name()) || type.equals(ClickHouseDataType.Dynamic.name())) { - return String.valueOf(java.sql.DatabaseMetaData.typeNullable); + nullability = java.sql.DatabaseMetaData.typeNullable; } - return String.valueOf(java.sql.DatabaseMetaData.typeNoNulls); - } + row.put("NULLABLE", nullability); + }; + + private static final List>> GET_TYPE_INFO_MUTATORS = Arrays.asList( + DATA_TYPE_VALUE_FUNCTION, + NULLABILITY_VALUE_FUNCTION + ); private static final String DATA_TYPE_INFO_SQL = getDataTypeInfoSql(); private static String getDataTypeInfoSql() { StringBuilder sql = new StringBuilder("SELECT " + "name AS TYPE_NAME, " + - "if(empty(alias_to), name, alias_to) AS DATA_TYPE, " + // passing type name or alias if exists to map then + "0::Int32 AS DATA_TYPE, " + // placeholder for data type int value "attrs.c2::Nullable(Int32) AS PRECISION, " + "NULL::Nullable(String) AS LITERAL_PREFIX, " + "NULL::Nullable(String) AS LITERAL_SUFFIX, " + "NULL::Nullable(String) AS CREATE_PARAMS, " + - "name AS NULLABLE, " + // passing type name to map for nullable + "0::Int16 AS NULLABLE, " + // placeholder for int value "not(dt.case_insensitive)::Boolean AS CASE_SENSITIVE, " + java.sql.DatabaseMetaData.typeSearchable + "::Int16 AS SEARCHABLE, " + "not(attrs.c3)::Boolean AS UNSIGNED_ATTRIBUTE, " + "false AS FIXED_PREC_SCALE, " + "false AS AUTO_INCREMENT, " + - "name AS LOCAL_TYPE_NAME, " + + "if(empty(alias_to), name, alias_to) AS LOCAL_TYPE_NAME, " + "attrs.c4::Nullable(Int16) AS MINIMUM_SCALE, " + "attrs.c5::Nullable(Int16) AS MAXIMUM_SCALE, " + "0::Nullable(Int32) AS SQL_DATA_TYPE, " + diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java index 49b968b50..ae713da39 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java @@ -5,6 +5,7 @@ import com.clickhouse.jdbc.JdbcV2Wrapper; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; +import com.google.common.collect.ImmutableList; import java.sql.SQLException; import java.util.List; @@ -24,7 +25,7 @@ public class ResultSetMetaDataImpl implements java.sql.ResultSetMetaData, JdbcV2 public ResultSetMetaDataImpl(List columns, String schema, String catalog, String tableName, Map> typeClassMap) { - this.columns = columns; + this.columns = ImmutableList.copyOf(columns); this.schema = schema; this.catalog = catalog; this.tableName = tableName; diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java new file mode 100644 index 000000000..94fee56db --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DetachedResultSetTest.java @@ -0,0 +1,528 @@ +package com.clickhouse.jdbc; + +import com.clickhouse.client.api.DataTypeUtils; +import com.clickhouse.jdbc.internal.DetachedResultSet; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +public class DetachedResultSetTest extends JdbcIntegrationTest { + + private static Calendar defaultCalendar = Calendar.getInstance(); + + @Test(groups = "integration") + public void shouldReturnColumnIndex() throws SQLException { + runQuery("CREATE TABLE detached_rs_test_data (id UInt32, val UInt8) ENGINE = MergeTree ORDER BY (id)"); + runQuery("INSERT INTO detached_rs_test_data VALUES (1, 10), (2, 20)"); + + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_data ORDER BY id")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.findColumn("id"), 1); + assertEquals(rs.getInt(1), 1); + assertEquals(rs.findColumn("val"), 2); + assertEquals(rs.getInt(2), 10); + + assertTrue(rs.next()); + assertEquals(rs.findColumn("id"), 1); + assertEquals(rs.getInt(1), 2); + assertEquals(rs.findColumn("val"), 2); + assertEquals(rs.getInt(2), 20); + } + } + } + } + + @Test(groups = {"integration"}) + public void testUnsupportedOperations() throws Throwable { + + try (Connection conn = this.getJdbcConnection(); Statement stmt = conn.createStatement(); + ResultSet srcRs = stmt.executeQuery("SELECT 1")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + Assert.ThrowingRunnable[] rsUnsupportedMethods = new Assert.ThrowingRunnable[]{ + rs::first, + rs::afterLast, + rs::beforeFirst, + () -> rs.absolute(-1), + () -> rs.relative(-1), + rs::moveToCurrentRow, + rs::moveToInsertRow, + rs::last, + rs::previous, + 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.updateObject(1, 1), + () -> rs.updateObject("col1", 1), + () -> rs.updateObject(1, "test", Types.INTEGER), + () -> rs.updateObject("col1", "test", Types.INTEGER), + + () -> 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, (Array) null), + () -> rs.updateArray("col1", (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, + rs::rowDeleted, + rs::rowInserted, + rs::rowUpdated, + rs::getCursorName, + }; + + for (Assert.ThrowingRunnable op : rsUnsupportedMethods) { + op.run(); + } + } + } + + + @Test(groups = {"integration"}) + public void testCursorPosition() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertTrue(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 1); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertTrue(rs.isLast()); + Assert.assertEquals(rs.getRow(), 2); + + rs.next(); + + Assert.assertFalse(rs.isBeforeFirst()); + Assert.assertTrue(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + } + + try (ResultSet rs = stmt.executeQuery("select 1 LIMIT 0")) { + Assert.assertTrue(rs.isBeforeFirst()); + Assert.assertFalse(rs.isAfterLast()); + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + + Assert.assertFalse(rs.next()); + + Assert.assertFalse(rs.isBeforeFirst()); // we stepped over the end + Assert.assertTrue(rs.isAfterLast()); // we stepped over the end + Assert.assertFalse(rs.isFirst()); + Assert.assertFalse(rs.isLast()); + Assert.assertEquals(rs.getRow(), 0); + } + } + } + + + @Test(groups = {"integration"}) + public void testFetchDirectionsAndSize() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + Assert.assertEquals(rs.getFetchDirection(), ResultSet.FETCH_FORWARD); + Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_REVERSE)); + Assert.expectThrows(SQLException.class, () -> rs.setFetchDirection(ResultSet.FETCH_UNKNOWN)); + rs.setFetchDirection(ResultSet.FETCH_FORWARD); + + Assert.assertEquals(rs.getFetchSize(), 0); + rs.setFetchSize(10); + Assert.assertEquals(rs.getFetchSize(), 0); + } + } + } + + @Test(groups = {"integration"}) + public void testConstants() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select number from system.numbers LIMIT 2")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + Assert.assertNull(rs.getStatement()); + Assert.assertEquals(rs.getType(), ResultSet.TYPE_FORWARD_ONLY); + Assert.assertEquals(rs.getConcurrency(), ResultSet.CONCUR_READ_ONLY); + Assert.assertEquals(rs.getHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + assertFalse(rs.isClosed()); + rs.close(); + assertTrue(rs.isClosed()); + assertThrows(SQLException.class, rs::next); + assertThrows(SQLException.class, rs::getStatement); + assertThrows(SQLException.class, rs::getMetaData); + } + } + } + + @Test(groups = {"integration"}) + public void testWasNull() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + final String sql = "select NULL::Nullable(%s) as v1"; + + try (ResultSet srcRs = stmt.executeQuery(sql.formatted("Int64"))) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + rs.next(); + Assert.assertFalse(rs.wasNull()); + + Assert.assertEquals(rs.getByte(1), (byte) 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getByte("v1"), (byte) 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getShort(1), (short) 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getShort("v1"), (short) 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getInt(1), 0); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getInt("v1"), 0); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getLong(1), 0L); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getLong("v1"), 0L); + Assert.assertTrue(rs.wasNull()); + + Assert.assertNull(rs.getBigDecimal(1)); + Assert.assertTrue(rs.wasNull()); + Assert.assertNull(rs.getBigDecimal("v1")); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getFloat(1), 0f); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getFloat("v1"), 0f); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getDouble(1), 0d); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getDouble("v1"), 0d); + Assert.assertTrue(rs.wasNull()); + + Assert.assertEquals(rs.getBoolean(1), false); + Assert.assertTrue(rs.wasNull()); + Assert.assertEquals(rs.getBoolean("v1"), false); + Assert.assertTrue(rs.wasNull()); + } + } + } + + @Test(groups = {"integration"}) + public void testGetMetadata() throws SQLException { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("select '1'::Int32 as v1, 'test' as v2 ")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + int v1ColumnIndex = rs.findColumn("v1"); + int v2ColumnIndex = rs.findColumn("v2"); + + ResultSetMetaData metaData = rs.getMetaData(); + Assert.assertEquals(metaData.getColumnCount(), 2); + Assert.assertEquals(metaData.getColumnType(v1ColumnIndex), Types.INTEGER); + Assert.assertEquals(metaData.getColumnType(v2ColumnIndex), Types.VARCHAR); + Assert.assertEquals(metaData.getColumnTypeName(v1ColumnIndex), "Int32"); + Assert.assertEquals(metaData.getColumnTypeName(v2ColumnIndex), "String"); + } + } + } + + @Test(groups = { "integration" }) + public void testDateTypes() throws SQLException { + runQuery("CREATE TABLE detached_rs_test_dates (order Int8, " + + "date Date, date32 Date32, " + + "dateTime DateTime, dateTime32 DateTime32, " + + "dateTime643 DateTime64(3), dateTime646 DateTime64(6), dateTime649 DateTime64(9)" + + ") ENGINE = MergeTree ORDER BY ()"); + + // Insert minimum values + insertData("INSERT INTO detached_rs_test_dates VALUES ( 1, '1970-01-01', '1970-01-01', " + + "'1970-01-01 00:00:00', '1970-01-01 00:00:00', " + + "'1970-01-01 00:00:00.000', '1970-01-01 00:00:00.000000', '1970-01-01 00:00:00.000000000' )"); + + // Insert maximum values + insertData("INSERT INTO detached_rs_test_dates VALUES ( 2, '2149-06-06', '2299-12-31', " + + "'2106-02-07 06:28:15', '2106-02-07 06:28:15', " + + "'2261-12-31 23:59:59.999', '2261-12-31 23:59:59.999999', '2261-12-31 23:59:59.999999999' )"); + + // Insert random (valid) values + final ZoneId zoneId = ZoneId.of("America/Los_Angeles"); + final LocalDateTime now = LocalDateTime.now(zoneId); + final Date date = Date.valueOf(now.toLocalDate()); + final Date date32 = Date.valueOf(now.toLocalDate()); + final java.sql.Timestamp dateTime = Timestamp.valueOf(now); + dateTime.setNanos(0); + final java.sql.Timestamp dateTime32 = Timestamp.valueOf(now); + dateTime32.setNanos(0); + final java.sql.Timestamp dateTime643 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime643.setNanos(333000000); + final java.sql.Timestamp dateTime646 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime646.setNanos(333333000); + final java.sql.Timestamp dateTime649 = Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Los_Angeles"))); + dateTime649.setNanos(333333333); + + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO detached_rs_test_dates VALUES ( 4, ?, ?, ?, ?, ?, ?, ?)")) { + stmt.setDate(1, date); + stmt.setDate(2, date32); + stmt.setTimestamp(3, dateTime); + stmt.setTimestamp(4, dateTime32); + stmt.setTimestamp(5, dateTime643); + stmt.setTimestamp(6, dateTime646); + stmt.setTimestamp(7, dateTime649); + stmt.executeUpdate(); + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getDate("date32"), Date.valueOf("1970-01-01")); + assertEquals(rs.getTimestamp("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getDate("date32"), Date.valueOf("2299-12-31")); + assertEquals(rs.getTimestamp("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getTimestamp("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getTimestamp("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getTimestamp("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + assertEquals(rs.getDate("date").toString(), date.toString()); + assertEquals(rs.getDate("date32").toString(), date32.toString()); + assertEquals(rs.getTimestamp("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getTimestamp("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertEquals(rs.getTimestamp("dateTime", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime.toString()); + assertEquals(rs.getTimestamp("dateTime32", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime32.toString()); + assertEquals(rs.getTimestamp("dateTime643", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime643.toString()); + assertEquals(rs.getTimestamp("dateTime646", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime646.toString()); + assertEquals(rs.getTimestamp("dateTime649", new GregorianCalendar(TimeZone.getTimeZone("UTC"))).toString(), dateTime649.toString()); + + assertFalse(rs.next()); + } + } + } + + // Check the results + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("date32"), Date.valueOf("1970-01-01")); + assertEquals(rs.getObject("dateTime").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime32").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime643").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime646").toString(), "1970-01-01 00:00:00.0"); + assertEquals(rs.getObject("dateTime649").toString(), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date"), Date.valueOf("2149-06-06")); + assertEquals(rs.getObject("date32"), Date.valueOf("2299-12-31")); + assertEquals(rs.getObject("dateTime").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime32").toString(), "2106-02-07 06:28:15.0"); + assertEquals(rs.getObject("dateTime643").toString(), "2261-12-31 23:59:59.999"); + assertEquals(rs.getObject("dateTime646").toString(), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getObject("dateTime649").toString(), "2261-12-31 23:59:59.999999999"); + + assertTrue(rs.next()); + assertEquals(rs.getObject("date").toString(), date.toString()); + assertEquals(rs.getObject("date32").toString(), date32.toString()); + + assertEquals(rs.getObject("dateTime").toString(), Timestamp.valueOf(dateTime.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime32").toString(), Timestamp.valueOf(dateTime32.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime643").toString(), Timestamp.valueOf(dateTime643.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime646").toString(), Timestamp.valueOf(dateTime646.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + assertEquals(rs.getObject("dateTime649").toString(), Timestamp.valueOf(dateTime649.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime()).toString()); + + assertFalse(rs.next()); + } + } + } + + try (Connection conn = getJdbcConnection(); + Statement stmt = conn.createStatement(); + ResultSet srcRs = stmt.executeQuery("SELECT * FROM detached_rs_test_dates ORDER BY order")) + { + ResultSet rs = DetachedResultSet.createFromResultSet(srcRs, defaultCalendar, Collections.emptyList()); + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "1970-01-01"); + assertEquals(rs.getString("date32"), "1970-01-01"); + assertEquals(rs.getString("dateTime"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime32"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime643"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime646"), "1970-01-01 00:00:00.0"); + assertEquals(rs.getString("dateTime649"), "1970-01-01 00:00:00.0"); + + assertTrue(rs.next()); + assertEquals(rs.getString("date"), "2149-06-06"); + assertEquals(rs.getString("date32"), "2299-12-31"); + assertEquals(rs.getString("dateTime"), "2106-02-07 06:28:15.0"); + assertEquals(rs.getString("dateTime32"), "2106-02-07 06:28:15.0"); + assertEquals(rs.getString("dateTime643"), "2261-12-31 23:59:59.999"); + assertEquals(rs.getString("dateTime646"), "2261-12-31 23:59:59.999999"); + assertEquals(rs.getString("dateTime649"), "2261-12-31 23:59:59.999999999"); + + ZoneId tzServer = ZoneId.of(((ConnectionImpl) conn).getClient().getServerTimeZone()); + assertTrue(rs.next()); + assertEquals( + rs.getString("date"), + Instant.ofEpochMilli(date.getTime()).atZone(tzServer).toLocalDate().toString()); + assertEquals( + rs.getString("date32"), + Instant.ofEpochMilli(date32.getTime()).atZone(tzServer).toLocalDate().toString()); + assertFalse(rs.next()); + } + } + + private int insertData(String sql) throws SQLException { + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()) { + return stmt.executeUpdate(sql); + } + } + } +} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java index 142c9596f..8b93a1555 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java @@ -15,6 +15,7 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; import java.util.Arrays; import java.util.Collections; @@ -33,11 +34,17 @@ public class DatabaseMetaDataTest extends JdbcIntegrationTest { @Test(groups = { "integration" }) public void testGetColumns() throws Exception { try (Connection conn = getJdbcConnection()) { + final String tableName = "get_columns_metadata_test"; + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("" + + "CREATE TABLE " + tableName + " (id Int32, name String, v1 Nullable(Int8)) " + + "ENGINE MergeTree ORDER BY ()"); + } + DatabaseMetaData dbmd = conn.getMetaData(); - try (ResultSet rs = dbmd.getColumns(null, ClickHouseServerForTest.getDatabase(), "system.numbers" - , null)) { + try (ResultSet rs = dbmd.getColumns(null, getDatabase(), tableName.substring(0, tableName.length() - 3) + "%", null)) { List expectedColumnNames = Arrays.asList( "TABLE_CAT", @@ -94,6 +101,30 @@ public void testGetColumns() throws Exception { ); assertProcedureColumns(rs.getMetaData(), expectedColumnNames, expectedColumnTypes); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "id"); + assertEquals(rs.getInt("DATA_TYPE"), Types.INTEGER); + assertEquals(rs.getObject("DATA_TYPE"), Types.INTEGER); + assertEquals(rs.getString("TYPE_NAME"), "Int32"); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "name"); + assertEquals(rs.getInt("DATA_TYPE"), Types.VARCHAR); + assertEquals(rs.getObject("DATA_TYPE"), Types.VARCHAR); + assertEquals(rs.getString("TYPE_NAME"), "String"); + + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_SCHEM"), getDatabase()); + assertEquals(rs.getString("TABLE_NAME"), tableName); + assertEquals(rs.getString("COLUMN_NAME"), "v1"); + assertEquals(rs.getInt("DATA_TYPE"), Types.TINYINT); + assertEquals(rs.getObject("DATA_TYPE"), Types.TINYINT); + assertEquals(rs.getString("TYPE_NAME"), "Nullable(Int8)"); } } } @@ -144,7 +175,7 @@ public void testGetColumnsWithTable() throws Exception { if (decimalDigits != null) { assertEquals(rs.getInt("DECIMAL_DIGITS"), decimalDigits.intValue()); } else { - assertEquals(0, rs.getInt("DECIMAL_DIGITS")); // should not throw exception + assertEquals(rs.getInt("DECIMAL_DIGITS"), 0); // should not throw exception assertTrue(rs.wasNull()); } Integer precisionRadix = columnRadix.get(colIndex); @@ -385,6 +416,8 @@ public void testGetTypeInfo() throws Exception { JdbcUtils.convertToSqlType(dataType).getVendorTypeNumber() + " but was " + rs.getInt("DATA_TYPE") + " for TYPE_NAME: " + rs.getString("TYPE_NAME")); + assertEquals(rs.getInt("DATA_TYPE"), rs.getObject("DATA_TYPE")); + assertEquals(rs.getInt("PRECISION"), dataType.getMaxPrecision()); assertNull(rs.getString("LITERAL_PREFIX")); assertNull(rs.getString("LITERAL_SUFFIX"));