diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index bbf037f6..ac7b5db6 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -39,13 +39,14 @@ public class SQLResultSet implements ResultSet { private CursorIterator> iterator; - private SQLResultSetMetaData metaData; + private TarantoolResultSetMetaData metaData; private Map columnByNameLookups; private boolean lastColumnWasNull; private final TarantoolStatement statement; private final int maxRows; + private final int maxFieldSize; private AtomicBoolean isClosed = new AtomicBoolean(false); @@ -59,7 +60,8 @@ public SQLResultSet(SQLResultHolder holder, TarantoolStatement ownerStatement) t scrollType = statement.getResultSetType(); concurrencyLevel = statement.getResultSetConcurrency(); holdability = statement.getResultSetHoldability(); - this.maxRows = statement.getMaxRows(); + maxRows = statement.getMaxRows(); + maxFieldSize = statement.getMaxFieldSize(); List> fetchedRows = holder.getRows(); List> rows = maxRows == 0 || maxRows >= fetchedRows.size() @@ -127,7 +129,13 @@ public boolean wasNull() throws SQLException { @Override public String getString(int columnIndex) throws SQLException { Object raw = getRaw(columnIndex); - return raw == null ? null : String.valueOf(raw); + if (raw == null) { + return null; + } + String value = String.valueOf(raw); + return (maxFieldSize > 0 && value.length() > maxFieldSize && metaData.isTrimmable(columnIndex)) + ? value.substring(0, maxFieldSize) + : value; } @Override @@ -232,7 +240,17 @@ public BigDecimal getBigDecimal(String columnLabel) throws SQLException { @Override public byte[] getBytes(int columnIndex) throws SQLException { - return (byte[]) getRaw(columnIndex); + Object raw = getRaw(columnIndex); + if (raw == null) { + return null; + } + byte[] bytes = (byte[]) raw; + if (maxFieldSize > 0 && bytes.length > maxFieldSize && metaData.isTrimmable(columnIndex)) { + byte[] trimmedBytes = new byte[maxFieldSize]; + System.arraycopy(bytes, 0, trimmedBytes, 0, maxFieldSize); + return trimmedBytes; + } + return bytes; } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java index f23b8fa5..a248bffa 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -8,7 +8,7 @@ import java.sql.SQLNonTransientException; import java.util.List; -public class SQLResultSetMetaData implements ResultSetMetaData { +public class SQLResultSetMetaData implements TarantoolResultSetMetaData { private final List sqlMetadata; private final boolean readOnly; @@ -181,7 +181,8 @@ public boolean isWrapperFor(Class type) throws SQLException { return type.isAssignableFrom(this.getClass()); } - void checkColumnIndex(int columnIndex) throws SQLException { + @Override + public void checkColumnIndex(int columnIndex) throws SQLException { if (columnIndex < 1 || columnIndex > getColumnCount()) { throw new SQLNonTransientException( String.format("Column index %d is out of range. Max index is %d", columnIndex, getColumnCount()), @@ -190,6 +191,12 @@ void checkColumnIndex(int columnIndex) throws SQLException { } } + @Override + public boolean isTrimmable(int columnIndex) throws SQLException { + checkColumnIndex(columnIndex); + return sqlMetadata.get(columnIndex - 1).getType().isTrimmable(); + } + @Override public String toString() { return "SQLResultSetMetaData{" + diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index bbb68a7a..a57b2ab2 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -51,6 +51,7 @@ public class SQLStatement implements TarantoolStatement { private final int resultSetHoldability; private int maxRows; + private int maxFieldSize; /** * Query timeout in millis. @@ -134,12 +135,18 @@ public void close() throws SQLException { @Override public int getMaxFieldSize() throws SQLException { - throw new SQLFeatureNotSupportedException(); + return maxFieldSize; } @Override - public void setMaxFieldSize(int max) throws SQLException { - throw new SQLFeatureNotSupportedException(); + public void setMaxFieldSize(int size) throws SQLException { + if (size < 0) { + throw new SQLException( + "The max field size must be positive or zero", + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + maxFieldSize = size; } @Override diff --git a/src/main/java/org/tarantool/jdbc/TarantoolResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/TarantoolResultSetMetaData.java new file mode 100644 index 00000000..505002eb --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/TarantoolResultSetMetaData.java @@ -0,0 +1,32 @@ +package org.tarantool.jdbc; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +/** + * Tarantool specific result set metadata extensions. + */ +public interface TarantoolResultSetMetaData extends ResultSetMetaData { + + /** + * Checks a column accessibility. + * + * @param columnIndex column number + * @throws SQLException if column is not accessible by the index + */ + void checkColumnIndex(int columnIndex) throws SQLException; + + /** + * Determines whether a column type can be trimmed or not. + * This status depends on JDBC types that are defined as + * trimmable such as {@literal VARCHAR} or {@literal BINARY} + * + * @param columnIndex column number + * @return {@literal true} if the column is trimmable + * @throws SQLException if column is not accessible by the index + * + * @see java.sql.Statement#setMaxFieldSize(int) + */ + boolean isTrimmable(int columnIndex) throws SQLException; + +} diff --git a/src/main/java/org/tarantool/jdbc/type/JdbcType.java b/src/main/java/org/tarantool/jdbc/type/JdbcType.java index 2c120f07..296cbbef 100644 --- a/src/main/java/org/tarantool/jdbc/type/JdbcType.java +++ b/src/main/java/org/tarantool/jdbc/type/JdbcType.java @@ -16,42 +16,44 @@ */ public enum JdbcType { - UNKNOWN(Object.class, JDBCType.OTHER), + UNKNOWN(Object.class, JDBCType.OTHER, false), - CHAR(String.class, JDBCType.CHAR), - VARCHAR(String.class, JDBCType.VARCHAR), - LONGVARCHAR(String.class, JDBCType.LONGNVARCHAR), + CHAR(String.class, JDBCType.CHAR, true), + VARCHAR(String.class, JDBCType.VARCHAR, true), + LONGVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), - NCHAR(String.class, JDBCType.NCHAR), - NVARCHAR(String.class, JDBCType.NVARCHAR), - LONGNVARCHAR(String.class, JDBCType.LONGNVARCHAR), + NCHAR(String.class, JDBCType.NCHAR, true), + NVARCHAR(String.class, JDBCType.NVARCHAR, true), + LONGNVARCHAR(String.class, JDBCType.LONGNVARCHAR, true), - BINARY(byte[].class, JDBCType.BINARY), - VARBINARY(byte[].class, JDBCType.VARBINARY), - LONGVARBINARY(byte[].class, JDBCType.LONGVARBINARY), + BINARY(byte[].class, JDBCType.BINARY, true), + VARBINARY(byte[].class, JDBCType.VARBINARY, true), + LONGVARBINARY(byte[].class, JDBCType.LONGVARBINARY, true), - BIT(Boolean.class, JDBCType.BIT), - BOOLEAN(Boolean.class, JDBCType.BOOLEAN), + BIT(Boolean.class, JDBCType.BIT, false), + BOOLEAN(Boolean.class, JDBCType.BOOLEAN, false), - REAL(Float.class, JDBCType.REAL), - FLOAT(Double.class, JDBCType.FLOAT), - DOUBLE(Double.class, JDBCType.DOUBLE), + REAL(Float.class, JDBCType.REAL, false), + FLOAT(Double.class, JDBCType.FLOAT, false), + DOUBLE(Double.class, JDBCType.DOUBLE, false), - TINYINT(Byte.class, JDBCType.TINYINT), - SMALLINT(Short.class, JDBCType.SMALLINT), - INTEGER(Integer.class, JDBCType.INTEGER), - BIGINT(Long.class, JDBCType.BIGINT), + TINYINT(Byte.class, JDBCType.TINYINT, false), + SMALLINT(Short.class, JDBCType.SMALLINT, false), + INTEGER(Integer.class, JDBCType.INTEGER, false), + BIGINT(Long.class, JDBCType.BIGINT, false), - CLOB(Clob.class, JDBCType.CLOB), - NCLOB(NClob.class, JDBCType.NCLOB), - BLOB(Blob.class, JDBCType.BLOB); + CLOB(Clob.class, JDBCType.CLOB, false), + NCLOB(NClob.class, JDBCType.NCLOB, false), + BLOB(Blob.class, JDBCType.BLOB, false); private final Class javaType; private final JDBCType targetJdbcType; + private final boolean trimmable; - JdbcType(Class javaType, JDBCType targetJdbcType) { + JdbcType(Class javaType, JDBCType targetJdbcType, boolean trimmable) { this.javaType = javaType; this.targetJdbcType = targetJdbcType; + this.trimmable = trimmable; } public Class getJavaType() { @@ -62,6 +64,10 @@ public JDBCType getTargetJdbcType() { return targetJdbcType; } + public boolean isTrimmable() { + return trimmable; + } + public int getTypeNumber() { return targetJdbcType.getVendorTypeNumber(); } diff --git a/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java b/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java index 3653a474..c582d871 100644 --- a/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java +++ b/src/main/java/org/tarantool/jdbc/type/TarantoolSqlType.java @@ -98,6 +98,10 @@ public boolean isCaseSensitive() { return tarantoolType.isCaseSensitive(); } + public boolean isTrimmable() { + return jdbcType.isTrimmable(); + } + public int getPrecision() { return tarantoolType.getPrecision(); } diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java index 8f9272d0..e9b2884b 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -10,6 +10,7 @@ import org.tarantool.ServerVersion; import org.tarantool.TarantoolTestHelper; +import org.tarantool.util.SQLStates; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -370,4 +371,70 @@ public void testTraversal() throws SQLException { assertEquals(0, resultSet.getRow()); } + @Test + public void testMaxFieldSize() throws SQLException { + assertEquals(0, stmt.getMaxFieldSize()); + + int expectedMaxSize = 256; + stmt.setMaxFieldSize(expectedMaxSize); + assertEquals(expectedMaxSize, stmt.getMaxFieldSize()); + } + + @Test + public void testNegativeMaxFieldSize() throws SQLException { + SQLException error = assertThrows(SQLException.class, () -> stmt.setMaxFieldSize(-12)); + assertEquals(SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), error.getSQLState()); + } + + @Test + public void testPositiveMaxFieldSize() throws SQLException { + testHelper.executeSql("INSERT INTO test VALUES (1, 'greater-than-ten-characters-value')"); + + stmt.setMaxFieldSize(10); + try (ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id = 1")) { + resultSet.next(); + assertEquals("greater-th", resultSet.getString(2)); + } + } + + @Test + public void testMaxFieldSizeBiggerThanValue() throws SQLException { + testHelper.executeSql("INSERT INTO test VALUES (1, 'less-than-one-hundred-characters-value')"); + + stmt.setMaxFieldSize(100); + try (ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id = 1")) { + resultSet.next(); + assertEquals("less-than-one-hundred-characters-value", resultSet.getString(2)); + } + } + + @Test + public void testMaxFieldSizeBinaryType() throws SQLException { + testHelper.executeSql("CREATE TABLE test_bin(id INT PRIMARY KEY, val SCALAR)"); + testHelper.executeSql("INSERT INTO test_bin VALUES (1, X'6c6f6e672d62696e6172792d737472696e67')"); + + stmt.setMaxFieldSize(12); + try (ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_bin WHERE id = 1")) { + resultSet.next(); + assertEquals(12, resultSet.getBytes(2).length); + } + + testHelper.executeSql("DROP TABLE test_bin"); + } + + @Test + public void testMaxFieldSizeNotTrimmableType() throws SQLException { + testHelper.executeSql("CREATE TABLE test_num(id INT PRIMARY KEY, val INT)"); + testHelper.executeSql("INSERT INTO test_num VALUES (1, 1234567890)"); + + String expectedUntrimmedValue = "1234567890"; + stmt.setMaxFieldSize(5); + try (ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_num WHERE id = 1")) { + resultSet.next(); + assertEquals(expectedUntrimmedValue, resultSet.getString(2)); + } + + testHelper.executeSql("DROP TABLE test_num"); + } + }