diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b1d8e..13fe135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased -### Changed -- Deprecate the parameter `version` (CQL version) in JDBC URL because this one is purely informational and has no - effect. This will be removed in the next release. +### Added - Handle additional types and conversions in the methods `CassandraPreparedStatement.setObject()`: - - JDBC types `BLOB`, `CLOB`, `NCLOB` and Java types `java.sql.Blob`, `java.sql.Clob`, and `java.sql.NClob` handled as + - JDBC types `BLOB`, `CLOB`, `NCLOB` and Java types `java.sql.Blob`, `java.sql.Clob`, and `java.sql.NClob` handled as arrays of bytes (CQL type `blob`) - - JDBC types `LONGVARCHAR`, `NCHAR`, `NVARCHAR`, `LONGNVARCHAR` and `DATALINK` and Java type `java.net.URL` handled + - JDBC types `LONGVARCHAR`, `NCHAR`, `NVARCHAR`, `LONGNVARCHAR` and `DATALINK` and Java type `java.net.URL` handled as string (CQL types `text`, `varchar` and `ascii`) - - JDBC type `TIME_WITH_TIMEZONE` and Java types `java.time.OffsetTime` and `java.time.LocalTime` handled as + - JDBC type `TIME_WITH_TIMEZONE` and Java types `java.time.OffsetTime` and `java.time.LocalTime` handled as `LocalTime` (CQL type `time`) - JDBC type `TIMESTAMP_WITH_TIMEZONE` and Java types `java.util.OffsetDateTime`, `java.time.LocalDateTime`, `java.util.Date` and `java.util.Calendar` handled as `Instant` (CQL type `timestamp`) @@ -21,6 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - JDBC type `BIT` handled as boolean (CQL type `boolean`) - JDBC type `NUMERIC` handled as `BigDecimal` (CQL type `decimal`) - JDBC type `REAL` handled as float number (CQL type `float`) +- Handle `java.util.Calendar` in the methods `CassandraResultSet.getObject(int | String, Class)`. +- Implement the following methods in `CassandraResultSet`: `getAsciiStream(int | String)`, + `getCharacterStream(int | String)`, `getClob(int | String)`, `getNClob(int | String)`. +### Changed +- Deprecate the parameter `version` (CQL version) in JDBC URL because this one is purely informational and has no + effect. This will be removed in the next release. - Update Apache Commons IO to version 2.15.0. - Update Jackson dependencies to version 2.15.3. - Use Apache Cassandra® 5.0 image to run tests. diff --git a/src/main/java/com/ing/data/cassandra/jdbc/AbstractResultSet.java b/src/main/java/com/ing/data/cassandra/jdbc/AbstractResultSet.java index 13f81ab..e5a09eb 100644 --- a/src/main/java/com/ing/data/cassandra/jdbc/AbstractResultSet.java +++ b/src/main/java/com/ing/data/cassandra/jdbc/AbstractResultSet.java @@ -89,6 +89,10 @@ boolean isCqlType(final String columnLabel, @Nonnull final DataTypeEnum type) { */ abstract DataType getCqlDataType(String columnLabel); + public boolean absolute(final int row) throws SQLException { + throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + } + public void cancelRowUpdates() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } @@ -97,6 +101,10 @@ public void deleteRow() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } + public boolean first() throws SQLException { + throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + } + public Array getArray(final int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } @@ -165,11 +173,11 @@ public Object getObject(final String columnLabel, final Map> ma throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } - public T getObject(final String columnLabel, final Class type) throws SQLException { + public T getObject(final int columnIndex, final Class type) throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } - public T getObject(final int columnIndex, final Class type) throws SQLException { + public T getObject(final String columnLabel, final Class type) throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } @@ -201,6 +209,10 @@ public void insertRow() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } + public boolean last() throws SQLException { + throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + } + public void moveToCurrentRow() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } @@ -209,10 +221,18 @@ public void moveToInsertRow() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } + public boolean previous() throws SQLException { + throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + } + public void refreshRow() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } + public boolean relative(final int arg0) throws SQLException { + throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + } + public boolean rowDeleted() throws SQLException { throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); } diff --git a/src/main/java/com/ing/data/cassandra/jdbc/CassandraMetadataResultSet.java b/src/main/java/com/ing/data/cassandra/jdbc/CassandraMetadataResultSet.java index a9211cb..1973c5d 100644 --- a/src/main/java/com/ing/data/cassandra/jdbc/CassandraMetadataResultSet.java +++ b/src/main/java/com/ing/data/cassandra/jdbc/CassandraMetadataResultSet.java @@ -203,11 +203,6 @@ DataType getCqlDataType(final String columnLabel) { return this.currentRow.getColumnDefinitions().getType(columnLabel); } - @Override - public boolean absolute(final int row) throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public void afterLast() throws SQLException { if (this.resultSetType == TYPE_FORWARD_ONLY) { @@ -290,11 +285,6 @@ public int findColumn(final String columnLabel) throws SQLException { throw new SQLSyntaxErrorException(String.format(VALID_LABELS, columnLabel)); } - @Override - public boolean first() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public BigDecimal getBigDecimal(final int columnIndex) throws SQLException { checkIndex(columnIndex); @@ -1015,11 +1005,6 @@ public boolean isLast() throws SQLException { return !this.rowsIterator.hasNext(); } - @Override - public boolean last() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public synchronized boolean next() { if (hasMoreRows()) { @@ -1034,16 +1019,6 @@ public synchronized boolean next() { return false; } - @Override - public boolean previous() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - - @Override - public boolean relative(final int arg0) throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public boolean wasNull() { return this.wasNull; diff --git a/src/main/java/com/ing/data/cassandra/jdbc/CassandraResultSet.java b/src/main/java/com/ing/data/cassandra/jdbc/CassandraResultSet.java index 3366232..cfdd35a 100644 --- a/src/main/java/com/ing/data/cassandra/jdbc/CassandraResultSet.java +++ b/src/main/java/com/ing/data/cassandra/jdbc/CassandraResultSet.java @@ -35,12 +35,16 @@ import com.ing.data.cassandra.jdbc.types.DataTypeEnum; import com.ing.data.cassandra.jdbc.types.TypesMap; import org.apache.commons.collections4.IteratorUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -48,6 +52,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; @@ -71,6 +76,7 @@ import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; @@ -79,7 +85,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -94,6 +99,7 @@ import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.MUST_BE_POSITIVE; import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.NOT_SUPPORTED; import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.NO_INTERFACE; +import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.UNABLE_TO_READ_VALUE; import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.UNSUPPORTED_JSON_TYPE_CONVERSION; import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.UNSUPPORTED_TYPE_CONVERSION; import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.VALID_LABELS; @@ -272,11 +278,6 @@ DataType getCqlDataType(final String columnLabel) { return this.currentRow.getColumnDefinitions().get(columnLabel).getType(); } - @Override - public boolean absolute(final int row) throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public void afterLast() throws SQLException { if (this.resultSetType == TYPE_FORWARD_ONLY) { @@ -355,8 +356,25 @@ public int findColumn(final String columnLabel) throws SQLException { } @Override - public boolean first() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); + public InputStream getAsciiStream(final int columnIndex) throws SQLException { + checkIndex(columnIndex); + final String s = this.currentRow.getString(columnIndex - 1); + if (s != null) { + return new ByteArrayInputStream(s.getBytes(StandardCharsets.US_ASCII)); + } else { + return null; + } + } + + @Override + public InputStream getAsciiStream(final String columnLabel) throws SQLException { + checkName(columnLabel); + final String s = this.currentRow.getString(columnLabel); + if (s != null) { + return new ByteArrayInputStream(s.getBytes(StandardCharsets.US_ASCII)); + } else { + return null; + } } @Override @@ -505,6 +523,70 @@ public byte[] getBytes(final String columnLabel) throws SQLException { return null; } + @Override + public Reader getCharacterStream(final int columnIndex) throws SQLException { + checkIndex(columnIndex); + final byte[] byteArray = this.getBytes(columnIndex); + if (byteArray != null) { + final InputStream inputStream = new ByteArrayInputStream(byteArray); + try { + return new CharArrayReader(IOUtils.toCharArray(inputStream, StandardCharsets.UTF_8)); + } catch (final IOException e) { + throw new SQLException(String.format(UNABLE_TO_READ_VALUE, Reader.class.getSimpleName()), e); + } + } else { + return null; + } + } + + @Override + public Reader getCharacterStream(final String columnLabel) throws SQLException { + checkName(columnLabel); + final byte[] byteArray = this.getBytes(columnLabel); + if (byteArray != null) { + final InputStream inputStream = new ByteArrayInputStream(byteArray); + try { + return new CharArrayReader(IOUtils.toCharArray(inputStream, StandardCharsets.UTF_8)); + } catch (final IOException e) { + throw new SQLException(String.format(UNABLE_TO_READ_VALUE, Reader.class.getSimpleName()), e); + } + } else { + return null; + } + } + + @Override + public Clob getClob(final int columnIndex) throws SQLException { + checkIndex(columnIndex); + final byte[] byteArray = getBytes(columnIndex); + if (byteArray != null) { + final InputStream inputStream = new ByteArrayInputStream(byteArray); + try { + return new javax.sql.rowset.serial.SerialClob(IOUtils.toCharArray(inputStream, StandardCharsets.UTF_8)); + } catch (final IOException e) { + throw new SQLException(String.format(UNABLE_TO_READ_VALUE, Clob.class.getSimpleName()), e); + } + } else { + return null; + } + } + + @Override + public Clob getClob(final String columnLabel) throws SQLException { + checkName(columnLabel); + final byte[] byteArray = getBytes(columnLabel); + if (byteArray != null) { + final InputStream inputStream = new ByteArrayInputStream(byteArray); + try { + return new javax.sql.rowset.serial.SerialClob(IOUtils.toCharArray(inputStream, StandardCharsets.UTF_8)); + } catch (final IOException e) { + throw new SQLException(String.format(UNABLE_TO_READ_VALUE, Clob.class.getSimpleName()), e); + } + } else { + return null; + } + } + @Override public int getConcurrency() throws SQLException { checkNotClosed(); @@ -757,6 +839,16 @@ public ResultSetMetaData getMetaData() { return this.metadata; } + @Override + public NClob getNClob(final int columnIndex) throws SQLException { + return (NClob) getClob(columnIndex); + } + + @Override + public NClob getNClob(final String columnLabel) throws SQLException { + return (NClob) getClob(columnLabel); + } + @Override public Object getObject(final int columnIndex) throws SQLException { checkIndex(columnIndex); @@ -1065,16 +1157,18 @@ public T getObject(final int columnIndex, final Class type) throws SQLExc returnValue = getTimestamp(columnIndex); } else if (type == LocalDate.class) { returnValue = getLocalDate(columnIndex); - } else if (type == LocalDateTime.class || type == LocalTime.class) { - final Timestamp timestamp = getTimestamp(columnIndex, Calendar.getInstance(TimeZone.getTimeZone("UTC"))); + } else if (type == LocalDateTime.class || type == LocalTime.class || type == Calendar.class) { + final Timestamp timestamp = getTimestamp(columnIndex, Calendar.getInstance()); if (timestamp == null) { returnValue = null; } else { final LocalDateTime ldt = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")); if (type == java.time.LocalDateTime.class) { returnValue = ldt; - } else { + } else if (type == java.time.LocalTime.class) { returnValue = ldt.toLocalTime(); + } else { + returnValue = new Calendar.Builder().setInstant(ldt.toEpochSecond(ZoneOffset.UTC)).build(); } } } else if (type == java.time.OffsetDateTime.class) { @@ -1463,11 +1557,6 @@ public boolean isLast() throws SQLException { return !this.rowsIterator.hasNext(); } - @Override - public boolean last() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - @Override public synchronized boolean next() { if (hasMoreRows()) { @@ -1482,16 +1571,6 @@ public synchronized boolean next() { return false; } - @Override - public boolean previous() throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - - @Override - public boolean relative(final int arg0) throws SQLException { - throw new SQLFeatureNotSupportedException(NOT_SUPPORTED); - } - /** * Gets whether a column was a null value. * diff --git a/src/main/java/com/ing/data/cassandra/jdbc/utils/ErrorConstants.java b/src/main/java/com/ing/data/cassandra/jdbc/utils/ErrorConstants.java index 252dd94..4998fc4 100644 --- a/src/main/java/com/ing/data/cassandra/jdbc/utils/ErrorConstants.java +++ b/src/main/java/com/ing/data/cassandra/jdbc/utils/ErrorConstants.java @@ -243,6 +243,12 @@ public final class ErrorConstants { */ public static final String UNSUPPORTED_TYPE_CONVERSION = "Conversion to type %s not supported."; + /** + * Error message used in any SQL exception thrown when the conversion to a specific type in a getter method of + * {@link CassandraResultSet} failed. + */ + public static final String UNABLE_TO_READ_VALUE = "Unable to read value as %s."; + /** * Error message used in any SQL exception thrown when the conversion to the specified type in the methods * {@link CassandraResultSet#getObjectFromJson(int, Class)}, diff --git a/src/test/java/com/ing/data/cassandra/jdbc/ResultSetUnitTest.java b/src/test/java/com/ing/data/cassandra/jdbc/ResultSetUnitTest.java index 1d5e5e4..2052ea4 100644 --- a/src/test/java/com/ing/data/cassandra/jdbc/ResultSetUnitTest.java +++ b/src/test/java/com/ing/data/cassandra/jdbc/ResultSetUnitTest.java @@ -14,18 +14,24 @@ package com.ing.data.cassandra.jdbc; import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; +import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.Calendar; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -92,4 +98,46 @@ void givenSelectStatementGeneratingWarning_whenGetWarnings_returnExpectedWarning assertEquals("Second warning message", nextWarning.getMessage()); } + @Test + void givenResultSetWithRows_whenGetObjectAsCalendar_returnExpectedValue() throws Exception { + final String cql = "SELECT col_ts FROM tbl_test_timestamps WHERE keyname = 'key1'"; + final Statement statement = sqlConnection.createStatement(); + final ResultSet rs = statement.executeQuery(cql); + assertTrue(rs.next()); + assertEquals(new Calendar.Builder() + .setInstant(OffsetDateTime.parse("2023-11-01T11:30:25.789+01:00").toEpochSecond()) + .build(), rs.getObject("col_ts", Calendar.class)); + } + + @Test + void givenResultSetWithRows_whenGetClob_returnExpectedValue() throws Exception { + final String cql = "SELECT col_blob FROM tbl_test_blobs WHERE keyname = 'key1'"; + final Statement statement = sqlConnection.createStatement(); + final ResultSet rs = statement.executeQuery(cql); + assertTrue(rs.next()); + final byte[] byteArray = IOUtils.toByteArray(rs.getClob("col_blob").getCharacterStream(), + StandardCharsets.UTF_8); + assertArrayEquals("testValueAsClobInUtf8 with accents: Äîéè".getBytes(StandardCharsets.UTF_8), byteArray); + } + + @Test + void givenResultSetWithRows_whenGetAsciiStream_returnExpectedValue() throws Exception { + final String cql = "SELECT col_ascii FROM tbl_test_texts WHERE keyname = 'key1'"; + final Statement statement = sqlConnection.createStatement(); + final ResultSet rs = statement.executeQuery(cql); + assertTrue(rs.next()); + final byte[] byteArray = IOUtils.toByteArray(rs.getAsciiStream("col_ascii")); + assertArrayEquals("testValueAscii".getBytes(StandardCharsets.US_ASCII), byteArray); + } + + @Test + void givenResultSetWithRows_whenGetCharacterStream_returnExpectedValue() throws Exception { + final String cql = "SELECT col_blob FROM tbl_test_blobs WHERE keyname = 'key1'"; + final Statement statement = sqlConnection.createStatement(); + final ResultSet rs = statement.executeQuery(cql); + assertTrue(rs.next()); + final byte[] byteArray = IOUtils.toByteArray(rs.getCharacterStream("col_blob"), StandardCharsets.UTF_8); + assertArrayEquals("testValueAsClobInUtf8 with accents: Äîéè".getBytes(StandardCharsets.UTF_8), byteArray); + } + } diff --git a/src/test/resources/initEmbeddedCassandra.cql b/src/test/resources/initEmbeddedCassandra.cql index 70ade7b..cd0a9a9 100644 --- a/src/test/resources/initEmbeddedCassandra.cql +++ b/src/test/resources/initEmbeddedCassandra.cql @@ -27,6 +27,24 @@ t3iValue int, PRIMARY KEY(keyname, t3iValue)) WITH comment = 'Third table in the keyspace'; +CREATE TABLE tbl_test_timestamps ( +keyname text PRIMARY KEY, +col_ts timestamp); + +INSERT INTO tbl_test_timestamps (keyname, col_ts) VALUES('key1', '2023-11-01T11:30:25.789+0100'); + +CREATE TABLE tbl_test_blobs ( +keyname text PRIMARY KEY, +col_blob blob); + +INSERT INTO tbl_test_blobs (keyname, col_blob) VALUES('key1', textAsBlob('testValueAsClobInUtf8 with accents: Äîéè')); + +CREATE TABLE tbl_test_texts ( +keyname text PRIMARY KEY, +col_ascii ascii); + +INSERT INTO tbl_test_texts (keyname, col_ascii) VALUES('key1', 'testValueAscii'); + CREATE TYPE CustomType1 ( key1 int, value1 text,