From 122be315a1836374c4b86d4bb3c1b598e2eae059 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Fri, 5 Apr 2019 22:53:45 +0700 Subject: [PATCH] Add support for a result set type and concurrency Provide support for FORWARD_ONLY and INSENSITIVE scroll types. Now statements as well as theirs resultSets can be built using a forward only iterator or full implemented insensitive iterator. To achieve this iteration logic was extracted into two separate classes to support two scroll types respectively. This is a cross-cutting support through SQLMetadata, SQLConnection, SQL(Prepared)Statement, SQLResultSet. Add support for READ_ONLY concurrency level for a result set. Extend SQLStates constants in scope of cursors and query execution. 0100E and 02000 for query results; 24000 for cursor iteration support. Add missed implementation of a closing for SQLStatement and SQLResultSet. Deprecate JDBCBridge. This redundant class should be removed completely in scope of another task. Mapping of field labels was moved to SQLResultSet. Closes: #85, #86 Affects: #119, #108 --- src/main/java/org/tarantool/JDBCBridge.java | 38 +-- .../java/org/tarantool/SqlProtoUtils.java | 27 +- .../java/org/tarantool/TarantoolBase.java | 21 +- .../org/tarantool/jdbc/SQLConnection.java | 66 +++- .../tarantool/jdbc/SQLDatabaseMetadata.java | 17 +- .../tarantool/jdbc/SQLPreparedStatement.java | 3 +- .../java/org/tarantool/jdbc/SQLResultSet.java | 212 ++++++------ .../tarantool/jdbc/SQLResultSetMetaData.java | 30 +- .../java/org/tarantool/jdbc/SQLStatement.java | 79 +++-- .../tarantool/jdbc/cursor/CursorIterator.java | 40 +++ .../InMemoryForwardCursorIteratorImpl.java | 137 ++++++++ .../InMemoryScrollableCursorIteratorImpl.java | 100 ++++++ .../java/org/tarantool/util/SQLStates.java | 5 +- src/test/java/org/tarantool/TestUtils.java | 16 + .../org/tarantool/jdbc/JdbcConnectionIT.java | 243 ++++++++++++-- .../jdbc/JdbcDatabaseMetaDataIT.java | 112 ++++++- .../jdbc/JdbcPreparedStatementIT.java | 6 + .../org/tarantool/jdbc/JdbcResultSetIT.java | 144 +++++++++ .../jdbc/JdbcResultSetMetaDataIT.java | 74 +++++ .../org/tarantool/jdbc/JdbcStatementIT.java | 5 + .../cursor/AbstractCursorIteratorTest.java | 144 +++++++++ ...InMemoryForwardCursorIteratorImplTest.java | 38 +++ ...emoryScrollableCursorIteratorImplTest.java | 306 ++++++++++++++++++ 23 files changed, 1635 insertions(+), 228 deletions(-) create mode 100644 src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java create mode 100644 src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java create mode 100644 src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java create mode 100644 src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java create mode 100644 src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java create mode 100644 src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java diff --git a/src/main/java/org/tarantool/JDBCBridge.java b/src/main/java/org/tarantool/JDBCBridge.java index c9a3e7cb..3ec18af4 100644 --- a/src/main/java/org/tarantool/JDBCBridge.java +++ b/src/main/java/org/tarantool/JDBCBridge.java @@ -4,30 +4,23 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.ListIterator; -import java.util.Map; +@Deprecated public class JDBCBridge { public static final JDBCBridge EMPTY = new JDBCBridge(Collections.emptyList(), Collections.emptyList()); - final List sqlMetadata; - final Map columnsByName; + final List sqlMetadata; final List> rows; protected JDBCBridge(TarantoolPacket pack) { this(SqlProtoUtils.getSQLMetadata(pack), SqlProtoUtils.getSQLData(pack)); } - protected JDBCBridge(List sqlMetadata, List> rows) { + protected JDBCBridge(List sqlMetadata, List> rows) { this.sqlMetadata = sqlMetadata; this.rows = rows; - columnsByName = new LinkedHashMap((int) Math.ceil(sqlMetadata.size() / 0.75), 0.75f); - for (int i = 0; i < sqlMetadata.size(); i++) { - columnsByName.put(sqlMetadata.get(i).getName(), i + 1); - } } public static JDBCBridge query(TarantoolConnection connection, String sql, Object... params) { @@ -48,9 +41,9 @@ public static int update(TarantoolConnection connection, String sql, Object... p * @return bridge */ public static JDBCBridge mock(List fields, List> values) { - List meta = new ArrayList<>(fields.size()); + List meta = new ArrayList<>(fields.size()); for (String field : fields) { - meta.add(new TarantoolBase.SQLMetaData(field)); + meta.add(new SqlProtoUtils.SQLMetaData(field)); } return new JDBCBridge(meta, values); } @@ -73,31 +66,18 @@ public static Object execute(TarantoolConnection connection, String sql, Object. return rowCount.intValue(); } - public String getColumnName(int columnIndex) { - return columnIndex > sqlMetadata.size() ? null : sqlMetadata.get(columnIndex - 1).getName(); - } - - public Integer getColumnIndex(String columnName) { - return columnsByName.get(columnName); - } - - public int getColumnCount() { - return columnsByName.size(); - } - - public ListIterator> iterator() { - return rows.listIterator(); + public List> getRows() { + return rows; } - public int size() { - return rows.size(); + public List getSqlMetadata() { + return sqlMetadata; } @Override public String toString() { return "JDBCBridge{" + "sqlMetadata=" + sqlMetadata + - ", columnsByName=" + columnsByName + ", rows=" + rows + '}'; } diff --git a/src/main/java/org/tarantool/SqlProtoUtils.java b/src/main/java/org/tarantool/SqlProtoUtils.java index af7d809d..23a339fd 100644 --- a/src/main/java/org/tarantool/SqlProtoUtils.java +++ b/src/main/java/org/tarantool/SqlProtoUtils.java @@ -12,7 +12,7 @@ public static List> readSqlResult(TarantoolPacket pack) { List> data = (List>) pack.getBody().get(Key.DATA.getId()); List> values = new ArrayList<>(data.size()); - List metaData = getSQLMetadata(pack); + List metaData = getSQLMetadata(pack); for (List row : data) { LinkedHashMap value = new LinkedHashMap<>(); for (int i = 0; i < row.size(); i++) { @@ -27,11 +27,11 @@ public static List> getSQLData(TarantoolPacket pack) { return (List>) pack.getBody().get(Key.DATA.getId()); } - public static List getSQLMetadata(TarantoolPacket pack) { + public static List getSQLMetadata(TarantoolPacket pack) { List> meta = (List>) pack.getBody().get(Key.SQL_METADATA.getId()); - List values = new ArrayList(meta.size()); + List values = new ArrayList<>(meta.size()); for (Map c : meta) { - values.add(new TarantoolBase.SQLMetaData((String) c.get(Key.SQL_FIELD_NAME.getId()))); + values.add(new SQLMetaData((String) c.get(Key.SQL_FIELD_NAME.getId()))); } return values; } @@ -44,4 +44,23 @@ public static Long getSqlRowCount(TarantoolPacket pack) { } return null; } + + public static class SQLMetaData { + protected String name; + + public SQLMetaData(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "SQLMetaData{" + + "name='" + name + '\'' + + '}'; + } + } } diff --git a/src/main/java/org/tarantool/TarantoolBase.java b/src/main/java/org/tarantool/TarantoolBase.java index e0507327..6856a495 100644 --- a/src/main/java/org/tarantool/TarantoolBase.java +++ b/src/main/java/org/tarantool/TarantoolBase.java @@ -32,25 +32,6 @@ public TarantoolBase(String username, String password, Socket socket) { } } - protected static class SQLMetaData { - protected String name; - - public SQLMetaData(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return "SQLMetaData{" + - "name='" + name + '\'' + - '}'; - } - } - protected TarantoolException serverError(long code, Object error) { return new TarantoolException(code, error instanceof String ? (String) error : new String((byte[]) error)); } @@ -60,7 +41,7 @@ protected void closeChannel(SocketChannel channel) { try { channel.close(); } catch (IOException ignored) { - // No-op + // no-op } } } diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index 566a8717..f89ac84f 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -41,6 +41,11 @@ import java.util.Properties; import java.util.concurrent.Executor; +/** + * Tarantool {@link Connection} implementation. + *

+ * Supports creating {@link Statement} and {@link PreparedStatement} instances + */ public class SQLConnection implements Connection { private static final int UNSET_HOLDABILITY = 0; @@ -171,7 +176,7 @@ public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { checkNotClosed(); - checkHoldabilitySupport(resultSetHoldability); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); return new SQLStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); } @@ -192,7 +197,7 @@ public PreparedStatement prepareStatement(String sql, int resultSetConcurrency, int resultSetHoldability) throws SQLException { checkNotClosed(); - checkHoldabilitySupport(resultSetHoldability); + checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); return new SQLPreparedStatement(this, sql, resultSetType, resultSetConcurrency, resultSetHoldability); } @@ -242,7 +247,7 @@ public String nativeSQL(String sql) throws SQLException { @Override public void setAutoCommit(boolean autoCommit) throws SQLException { - if (autoCommit == false) { + if (!autoCommit) { throw new SQLFeatureNotSupportedException(); } } @@ -589,6 +594,61 @@ private void handleException(Exception e) { } } + /** + * Checks all params required to make statements. + * + * @param resultSetType scroll type + * @param resultSetConcurrency concurrency level + * @param resultSetHoldability holdability type + * + * @throws SQLFeatureNotSupportedException if any param is not supported + * @throws SQLNonTransientException if any param has an invalid value + */ + private void checkStatementParams(int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + checkResultSetType(resultSetType); + checkResultSetConcurrency(resultSetType, resultSetConcurrency); + checkHoldabilitySupport(resultSetHoldability); + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetType param to be checked + * + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkResultSetType(int resultSetType) throws SQLException { + if (resultSetType != ResultSet.TYPE_FORWARD_ONLY && + resultSetType != ResultSet.TYPE_SCROLL_INSENSITIVE && + resultSetType != ResultSet.TYPE_SCROLL_SENSITIVE) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + if (!getMetaData().supportsResultSetType(resultSetType)) { + throw new SQLFeatureNotSupportedException(); + } + } + + /** + * Checks whether resultSetType is supported. + * + * @param resultSetConcurrency param to be checked + * + * @throws SQLFeatureNotSupportedException param is not supported + * @throws SQLNonTransientException param has invalid value + */ + private void checkResultSetConcurrency(int resultSetType, int resultSetConcurrency) throws SQLException { + if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY && + resultSetConcurrency != ResultSet.CONCUR_UPDATABLE) { + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } + if (!getMetaData().supportsResultSetConcurrency(resultSetType, resultSetConcurrency)) { + throw new SQLFeatureNotSupportedException(); + } + } + /** * Checks whether holdability is supported. * diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index 9a4738f6..1605fd86 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -36,17 +36,17 @@ public SQLNullResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws S } @Override - protected Object getRaw(int columnIndex) { - return columnIndex > getCurrentRow().size() ? null : getCurrentRow().get(columnIndex - 1); + protected Object getRaw(int columnIndex) throws SQLException { + List row = getCurrentRow(); + return columnIndex > row.size() ? null : row.get(columnIndex - 1); } @Override - protected Integer getColumnIndex(String columnLabel) { - Integer idx = super.getColumnIndex(columnLabel); - return idx == null ? Integer.MAX_VALUE : idx; + protected int findColumnIndex(String columnLabel) throws SQLException { + int index = super.findColumnIndex(columnLabel); + return index == 0 ? Integer.MAX_VALUE : index; } - } public SQLDatabaseMetadata(SQLConnection connection) { @@ -908,12 +908,13 @@ public ResultSet getIndexInfo(String catalog, String schema, String table, boole @Override public boolean supportsResultSetType(int type) throws SQLException { - return false; + return type == ResultSet.TYPE_FORWARD_ONLY || + type == ResultSet.TYPE_SCROLL_INSENSITIVE; } @Override public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { - return false; + return supportsResultSetType(type) && concurrency == ResultSet.CONCUR_READ_ONLY; } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index a85d1563..760aa569 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -239,8 +239,7 @@ private void setParameter(int parameterIndex, Object value) throws SQLException @Override public boolean execute() throws SQLException { checkNotClosed(); - discardLastResults(); - return handleResult(connection.execute(sql, getParams())); + return executeInternal(sql, getParams()); } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index e394dd72..de7a78ac 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -1,6 +1,10 @@ package org.tarantool.jdbc; import org.tarantool.JDBCBridge; +import org.tarantool.jdbc.cursor.CursorIterator; +import org.tarantool.jdbc.cursor.InMemoryForwardCursorIteratorImpl; +import org.tarantool.jdbc.cursor.InMemoryScrollableCursorIteratorImpl; +import org.tarantool.util.SQLStates; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -28,61 +32,66 @@ import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; +import java.util.LinkedHashMap; import java.util.List; -import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; -@SuppressWarnings("Since15") public class SQLResultSet implements ResultSet { - private ListIterator> iterator; - private JDBCBridge bridge; + private final CursorIterator> iterator; private final SQLResultSetMetaData metaData; + private Map columnByNameLookups; + private final Statement statement; - private int maxRows; - private List row = null; + private final int maxRows; + + private AtomicBoolean isClosed = new AtomicBoolean(false); - private final int type; + private final int scrollType; private final int concurrencyLevel; private final int holdability; public SQLResultSet(JDBCBridge bridge, SQLStatement ownerStatement) throws SQLException { - this.bridge = bridge; - iterator = bridge.iterator(); - metaData = new SQLResultSetMetaData(bridge); + metaData = new SQLResultSetMetaData(bridge.getSqlMetadata()); statement = ownerStatement; - type = statement.getResultSetType(); + scrollType = statement.getResultSetType(); concurrencyLevel = statement.getResultSetConcurrency(); holdability = statement.getResultSetHoldability(); + this.maxRows = statement.getMaxRows(); + + List> fetchedRows = bridge.getRows(); + List> rows = maxRows == 0 || maxRows >= fetchedRows.size() + ? fetchedRows + : fetchedRows.subList(0, maxRows); + + switch (scrollType) { + case ResultSet.TYPE_FORWARD_ONLY: + iterator = new InMemoryForwardCursorIteratorImpl(rows); + break; + case ResultSet.TYPE_SCROLL_INSENSITIVE: + iterator = new InMemoryScrollableCursorIteratorImpl(rows); + break; + default: + throw new SQLNonTransientException("", SQLStates.INVALID_PARAMETER_VALUE.getSqlState()); + } } public int getMaxRows() { return maxRows; } - public void setMaxRows(int maxRows) { - this.maxRows = maxRows; - } - - List getCurrentRow() { - return row; - } - - @Override - public boolean next() throws SQLException { + public List getCurrentRow() throws SQLException { checkNotClosed(); - if (iterator.hasNext() && (maxRows == 0 || iterator.nextIndex() < maxRows)) { - row = iterator.next(); - return true; - } - row = null; - return false; + return iterator.getItem(); } @Override public void close() throws SQLException { - + if (isClosed.compareAndSet(false, true)) { + iterator.close(); + } } @Override @@ -90,6 +99,13 @@ public boolean wasNull() throws SQLException { return false; } + protected Object getRaw(int columnIndex) throws SQLException { + checkNotClosed(); + metaData.checkColumnIndex(columnIndex); + List row = getCurrentRow(); + return row.get(columnIndex - 1); + } + @Override public String getString(int columnIndex) throws SQLException { Object raw = getRaw(columnIndex); @@ -98,18 +114,9 @@ public String getString(int columnIndex) throws SQLException { @Override public String getString(String columnLabel) throws SQLException { - return getString(getColumnIndex(columnLabel)); - } - - protected Object getRaw(int columnIndex) { - return row.get(columnIndex - 1); - } - - protected Integer getColumnIndex(String columnLabel) { - return bridge.getColumnIndex(columnLabel); + return getString(findColumn(columnLabel)); } - @Override public boolean getBoolean(int columnIndex) throws SQLException { return Boolean.TRUE.equals(getRaw(columnIndex)); @@ -117,7 +124,7 @@ public boolean getBoolean(int columnIndex) throws SQLException { @Override public boolean getBoolean(String columnLabel) throws SQLException { - return getBoolean(getColumnIndex(columnLabel)); + return getBoolean(findColumn(columnLabel)); } @Override @@ -127,7 +134,7 @@ public byte getByte(int columnIndex) throws SQLException { @Override public byte getByte(String columnLabel) throws SQLException { - return getByte(getColumnIndex(columnLabel)); + return getByte(findColumn(columnLabel)); } @Override @@ -137,7 +144,7 @@ public short getShort(int columnIndex) throws SQLException { @Override public short getShort(String columnLabel) throws SQLException { - return getShort(getColumnIndex(columnLabel)); + return getShort(findColumn(columnLabel)); } @Override @@ -147,10 +154,10 @@ public int getInt(int columnIndex) throws SQLException { @Override public int getInt(String columnLabel) throws SQLException { - return getInt(getColumnIndex(columnLabel)); + return getInt(findColumn(columnLabel)); } - private Number getNumber(int columnIndex) { + private Number getNumber(int columnIndex) throws SQLException { Number raw = (Number) getRaw(columnIndex); return raw == null ? 0 : raw; } @@ -162,7 +169,7 @@ public long getLong(int columnIndex) throws SQLException { @Override public long getLong(String columnLabel) throws SQLException { - return getLong(getColumnIndex(columnLabel)); + return getLong(findColumn(columnLabel)); } @Override @@ -172,7 +179,7 @@ public float getFloat(int columnIndex) throws SQLException { @Override public float getFloat(String columnLabel) throws SQLException { - return getFloat(getColumnIndex(columnLabel)); + return getFloat(findColumn(columnLabel)); } @Override @@ -182,7 +189,7 @@ public double getDouble(int columnIndex) throws SQLException { @Override public double getDouble(String columnLabel) throws SQLException { - return getDouble(getColumnIndex(columnLabel)); + return getDouble(findColumn(columnLabel)); } @Override @@ -193,7 +200,7 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException @Override public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - return getBigDecimal(getColumnIndex(columnLabel)); + return getBigDecimal(findColumn(columnLabel)); } @Override @@ -213,7 +220,7 @@ public byte[] getBytes(int columnIndex) throws SQLException { @Override public byte[] getBytes(String columnLabel) throws SQLException { - return getBytes(getColumnIndex(columnLabel)); + return getBytes(findColumn(columnLabel)); } @Override @@ -223,7 +230,7 @@ public Date getDate(int columnIndex) throws SQLException { @Override public Date getDate(String columnLabel) throws SQLException { - return getDate(getColumnIndex(columnLabel)); + return getDate(findColumn(columnLabel)); } @Override @@ -243,7 +250,7 @@ public Time getTime(int columnIndex) throws SQLException { @Override public Time getTime(String columnLabel) throws SQLException { - return getTime(getColumnIndex(columnLabel)); + return getTime(findColumn(columnLabel)); } @Override @@ -263,7 +270,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { @Override public Timestamp getTimestamp(String columnLabel) throws SQLException { - return getTimestamp(getColumnIndex(columnLabel)); + return getTimestamp(findColumn(columnLabel)); } @Override @@ -284,7 +291,7 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { @Override public InputStream getAsciiStream(String columnLabel) throws SQLException { - return getAsciiStream(getColumnIndex(columnLabel)); + return getAsciiStream(findColumn(columnLabel)); } @Override @@ -294,7 +301,7 @@ public InputStream getUnicodeStream(int columnIndex) throws SQLException { @Override public InputStream getUnicodeStream(String columnLabel) throws SQLException { - return getUnicodeStream(getColumnIndex(columnLabel)); + return getUnicodeStream(findColumn(columnLabel)); } @Override @@ -304,7 +311,7 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException { @Override public InputStream getBinaryStream(String columnLabel) throws SQLException { - return getBinaryStream(getColumnIndex(columnLabel)); + return getBinaryStream(findColumn(columnLabel)); } @Override @@ -324,7 +331,7 @@ public Object getObject(int columnIndex) throws SQLException { @Override public Object getObject(String columnLabel) throws SQLException { - return getRaw(getColumnIndex(columnLabel)); + return getRaw(findColumn(columnLabel)); } @Override @@ -344,7 +351,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { @Override public T getObject(String columnLabel, Class type) throws SQLException { - return type.cast(getRaw(getColumnIndex(columnLabel))); + return type.cast(getRaw(findColumn(columnLabel))); } @Override @@ -369,97 +376,104 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public int findColumn(String columnLabel) throws SQLException { - return getColumnIndex(columnLabel); + return findColumnIndex(columnLabel); + } + + protected int findColumnIndex(String columnLabel) throws SQLException { + if (columnByNameLookups == null) { + columnByNameLookups = new LinkedHashMap<>(); + // Spec quote: Column labels supplied to getter methods are case insensitive. + // If a select list contains the same column more than once, the first instance + // of the column will be returned. + for (int i = metaData.getColumnCount(); i > 0; i--) { + columnByNameLookups.put(metaData.getColumnLabel(i).toUpperCase(), i); + } + } + return columnByNameLookups.getOrDefault(columnLabel.toUpperCase(), 0); + } + + //region Cursor movement API + + @Override + public boolean next() throws SQLException { + checkNotClosed(); + return iterator.next(); } @Override public boolean isBeforeFirst() throws SQLException { checkNotClosed(); - return row == null && iterator.previousIndex() == -1; + return iterator.isBeforeFirst(); } @Override public boolean isAfterLast() throws SQLException { checkNotClosed(); - return iterator.nextIndex() == bridge.size() && row == null; + return iterator.isAfterLast(); } @Override public boolean isFirst() throws SQLException { checkNotClosed(); - return iterator.previousIndex() == 0; + return iterator.isFirst(); } @Override public boolean isLast() throws SQLException { checkNotClosed(); - return iterator.nextIndex() == bridge.size(); + return iterator.isLast(); } @Override public void beforeFirst() throws SQLException { checkNotClosed(); - row = null; - iterator = bridge.iterator(); + iterator.beforeFirst(); } @Override public void afterLast() throws SQLException { checkNotClosed(); - while (next()) { - } + iterator.afterLast(); } @Override public boolean first() throws SQLException { - beforeFirst(); - return next(); + checkNotClosed(); + return iterator.first(); } @Override public boolean last() throws SQLException { checkNotClosed(); - while (iterator.hasNext()) { - next(); - } - return row != null; + return iterator.last(); } @Override - public int getRow() throws SQLException { + public boolean absolute(int row) throws SQLException { checkNotClosed(); - return iterator.previousIndex() + 1; + return iterator.absolute(row); } @Override - public boolean absolute(int row) throws SQLException { - beforeFirst(); - for (int i = 0; i < row && iterator.hasNext(); i++) { - next(); - } - return !(isAfterLast() || isBeforeFirst()); - + public boolean relative(int rows) throws SQLException { + checkNotClosed(); + return iterator.relative(rows); } @Override - public boolean relative(int rows) throws SQLException { + public boolean previous() throws SQLException { checkNotClosed(); - for (int i = 0; i < rows && iterator.hasNext(); i++) { - next(); - } - return !(isAfterLast() || isBeforeFirst()); + return iterator.previous(); } @Override - public boolean previous() throws SQLException { + public int getRow() throws SQLException { checkNotClosed(); - if (iterator.hasPrevious()) { - iterator.previous(); - return true; - } - return false; + return iterator.getRow(); } + //endregion + @Override public void setFetchDirection(int direction) throws SQLException { checkNotClosed(); @@ -487,7 +501,7 @@ public int getFetchSize() throws SQLException { @Override public int getType() throws SQLException { checkNotClosed(); - return type; + return scrollType; } @Override @@ -798,7 +812,8 @@ public void moveToCurrentRow() throws SQLException { @Override public Statement getStatement() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + return statement; } @Override @@ -967,7 +982,7 @@ public int getHoldability() throws SQLException { @Override public boolean isClosed() throws SQLException { - return statement.isClosed(); + return isClosed.get() || statement.isClosed(); } @Override @@ -1096,9 +1111,12 @@ public boolean isWrapperFor(Class type) throws SQLException { @Override public String toString() { return "SQLResultSet{" + - "metaData=" + metaData + - ", row=" + row + - '}'; + "metaData=" + metaData + + ", statement=" + statement + + ", scrollType=" + scrollType + + ", concurrencyLevel=" + concurrencyLevel + + ", holdability=" + holdability + + '}'; } protected void checkNotClosed() throws SQLException { diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java index ee1f36a9..513679c9 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSetMetaData.java @@ -1,23 +1,26 @@ package org.tarantool.jdbc; -import org.tarantool.JDBCBridge; +import org.tarantool.SqlProtoUtils; +import org.tarantool.util.SQLStates; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLNonTransientException; import java.sql.Types; +import java.util.List; public class SQLResultSetMetaData implements ResultSetMetaData { - protected final JDBCBridge jdbcBridge; - public SQLResultSetMetaData(JDBCBridge jdbcBridge) { - this.jdbcBridge = jdbcBridge; + private final List sqlMetadata; + + public SQLResultSetMetaData(List sqlMetaData) { + this.sqlMetadata = sqlMetaData; } @Override public int getColumnCount() throws SQLException { - return jdbcBridge.getColumnCount(); + return sqlMetadata.size(); } @Override @@ -57,12 +60,14 @@ public int getColumnDisplaySize(int column) throws SQLException { @Override public String getColumnLabel(int column) throws SQLException { - return jdbcBridge.getColumnName(column); + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); } @Override public String getColumnName(int column) throws SQLException { - return jdbcBridge.getColumnName(column); + checkColumnIndex(column); + return sqlMetadata.get(column - 1).getName(); } @Override @@ -133,10 +138,19 @@ public boolean isWrapperFor(Class type) throws SQLException { return type.isAssignableFrom(this.getClass()); } + 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()), + SQLStates.INVALID_PARAMETER_VALUE.getSqlState() + ); + } + } + @Override public String toString() { return "SQLResultSetMetaData{" + - "bridge=" + jdbcBridge + + "sqlMetadata=" + sqlMetadata + '}'; } } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index e6e42917..a4dfdaf1 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -2,6 +2,7 @@ import org.tarantool.JDBCBridge; import org.tarantool.util.JdbcConstants; +import org.tarantool.util.SQLStates; import java.sql.Connection; import java.sql.ResultSet; @@ -10,19 +11,33 @@ import java.sql.SQLNonTransientException; import java.sql.SQLWarning; import java.sql.Statement; - +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Tarantool {@link Statement} implementation. + *

+ * Supports {@link ResultSet#TYPE_FORWARD_ONLY} and {@link ResultSet#TYPE_SCROLL_INSENSITIVE} + * types of cursors. + * Supports only {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} holdability type. + */ public class SQLStatement implements Statement { protected final SQLConnection connection; + /** + * Current result set / update count associated to this statement. + */ private SQLResultSet resultSet; + private int updateCount; + private final int resultSetType; private final int resultSetConcurrency; private final int resultSetHoldability; - private int updateCount; private int maxRows; + private final AtomicBoolean isClosed = new AtomicBoolean(false); + protected SQLStatement(SQLConnection sqlConnection) throws SQLException { this.connection = sqlConnection; this.resultSetType = ResultSet.TYPE_FORWARD_ONLY; @@ -43,15 +58,22 @@ protected SQLStatement(SQLConnection sqlConnection, @Override public ResultSet executeQuery(String sql) throws SQLException { checkNotClosed(); - discardLastResults(); - return createResultSet(connection.executeQuery(sql)); + if (!executeInternal(sql)) { + throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); + } + return resultSet; } @Override public int executeUpdate(String sql) throws SQLException { checkNotClosed(); - discardLastResults(); - return connection.executeUpdate(sql); + if (executeInternal(sql)) { + throw new SQLException( + "Result was returned but nothing was expected", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + return updateCount; } @Override @@ -76,7 +98,10 @@ public int executeUpdate(String sql, String[] columnNames) throws SQLException { @Override public void close() throws SQLException { - + if (isClosed.compareAndSet(false, true)) { + cancel(); + discardLastResults(); + } } @Override @@ -102,9 +127,6 @@ public void setMaxRows(int maxRows) throws SQLException { throw new SQLNonTransientException("Max rows parameter can't be a negative value"); } this.maxRows = maxRows; - if (resultSet != null) { - resultSet.setMaxRows(this.maxRows); - } } @Override @@ -146,7 +168,7 @@ public void setCursorName(String name) throws SQLException { public boolean execute(String sql) throws SQLException { checkNotClosed(); discardLastResults(); - return handleResult(connection.execute(sql)); + return executeInternal(sql); } @Override @@ -172,21 +194,13 @@ public boolean execute(String sql, String[] columnNames) throws SQLException { @Override public ResultSet getResultSet() throws SQLException { checkNotClosed(); - try { - return resultSet; - } finally { - resultSet = null; - } + return resultSet; } @Override public int getUpdateCount() throws SQLException { checkNotClosed(); - try { - return updateCount; - } finally { - updateCount = -1; - } + return updateCount; } @Override @@ -272,7 +286,7 @@ public int getResultSetHoldability() throws SQLException { @Override public boolean isClosed() throws SQLException { - return connection.isClosed(); + return isClosed.get() || connection.isClosed(); } @Override @@ -312,7 +326,8 @@ public boolean isWrapperFor(Class type) throws SQLException { /** * Clears the results of the most recent execution. */ - protected void discardLastResults() { + protected void discardLastResults() throws SQLException { + clearWarnings(); updateCount = -1; if (resultSet != null) { try { @@ -324,16 +339,29 @@ protected void discardLastResults() { } } + /** + * Performs query execution. + * + * @param sql query + * @param params optional params + * + * @return {@code true}, if the result is a ResultSet object; + */ + protected boolean executeInternal(String sql, Object... params) throws SQLException { + discardLastResults(); + return handleResult(connection.execute(sql, params)); + } + /** * Sets the internals according to the result of last execution. * * @param result The result of SQL statement execution. + * * @return {@code true}, if the result is a ResultSet object. */ protected boolean handleResult(Object result) throws SQLException { if (result instanceof JDBCBridge) { resultSet = createResultSet((JDBCBridge) result); - resultSet.setMaxRows(maxRows); updateCount = -1; return true; } else { @@ -347,7 +375,9 @@ protected boolean handleResult(Object result) throws SQLException { * Returns {@link ResultSet} which will be initialized by data. * * @param data predefined result to be wrapped by {@link ResultSet} + * * @return wrapped result + * * @throws SQLException if a database access error occurs or * this method is called on a closed Statement */ @@ -365,4 +395,5 @@ protected void checkNotClosed() throws SQLException { throw new SQLNonTransientException("Statement is closed."); } } + } diff --git a/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java b/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java new file mode 100644 index 00000000..7c29080a --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/CursorIterator.java @@ -0,0 +1,40 @@ +package org.tarantool.jdbc.cursor; + +import java.sql.SQLException; + +/** + * Extracted interface for a cursor traversal part of {@link java.sql.ResultSet}. + */ +public interface CursorIterator { + + boolean isBeforeFirst() throws SQLException; + + boolean isAfterLast() throws SQLException; + + boolean isFirst() throws SQLException; + + boolean isLast() throws SQLException; + + void beforeFirst() throws SQLException; + + void afterLast() throws SQLException; + + boolean first() throws SQLException; + + boolean last() throws SQLException; + + boolean absolute(int row) throws SQLException; + + boolean relative(int rows) throws SQLException; + + boolean next() throws SQLException; + + boolean previous() throws SQLException; + + int getRow() throws SQLException; + + T getItem() throws SQLException; + + void close(); + +} diff --git a/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java b/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java new file mode 100644 index 00000000..1b735968 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImpl.java @@ -0,0 +1,137 @@ +package org.tarantool.jdbc.cursor; + +import org.tarantool.util.SQLStates; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Forward only iterator to support {@link java.sql.ResultSet#TYPE_FORWARD_ONLY} + * result set semantic. + */ +public class InMemoryForwardCursorIteratorImpl implements CursorIterator> { + + protected final List> results = new ArrayList<>(); + protected int currentPosition = -1; + + public InMemoryForwardCursorIteratorImpl(List> results) { + if (results == null) { + throw new IllegalArgumentException("Results list cannot be null"); + } + this.results.addAll(results); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return hasResults() && currentPosition == -1; + } + + @Override + public boolean isAfterLast() throws SQLException { + return hasResults() && currentPosition == results.size(); + } + + @Override + public boolean isFirst() throws SQLException { + return hasResults() && currentPosition == 0; + } + + @Override + public boolean isLast() throws SQLException { + return hasResults() && currentPosition == results.size() - 1; + } + + @Override + public void beforeFirst() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public void afterLast() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean first() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean last() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean absolute(int row) throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean relative(int rows) throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public boolean next() throws SQLException { + if (!hasResults() || isAfterLast()) { + return false; + } + currentPosition++; + return !isAfterLast(); + } + + @Override + public boolean previous() throws SQLException { + throw new SQLException( + "Cannot be called on forward only cursor", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + @Override + public int getRow() throws SQLException { + return !hasResults() || isBeforeFirst() || isAfterLast() ? 0 : currentPosition + 1; + } + + @Override + public List getItem() throws SQLException { + int row = getRow(); + if (row > 0) { + return results.get(row - 1); + } + throw new SQLException( + "Cursor is out of range. Try to call next() or previous() before.", + SQLStates.INVALID_CURSOR_STATE.getSqlState() + ); + } + + protected boolean hasResults() { + return !results.isEmpty(); + } + + @Override + public void close() { + results.clear(); + currentPosition = -1; + } + +} diff --git a/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java b/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java new file mode 100644 index 00000000..0561cc37 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImpl.java @@ -0,0 +1,100 @@ +package org.tarantool.jdbc.cursor; + +import java.sql.SQLException; +import java.util.List; + +/** + * Scrollable iterator to support {@link java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE} + * result set type semantic. + */ +public class InMemoryScrollableCursorIteratorImpl extends InMemoryForwardCursorIteratorImpl { + + public InMemoryScrollableCursorIteratorImpl(List> results) { + super(results); + } + + @Override + public void beforeFirst() throws SQLException { + moveIfHasResults(-1); + } + + @Override + public void afterLast() throws SQLException { + moveIfHasResults(results.size()); + } + + @Override + public boolean first() throws SQLException { + return moveIfHasResults(0); + } + + @Override + public boolean last() throws SQLException { + return moveIfHasResults(results.size() - 1); + } + + @Override + public boolean absolute(int row) throws SQLException { + if (!hasResults()) { + return false; + } + if (row == 0) { + beforeFirst(); + return false; + } + if (row > results.size()) { + afterLast(); + return false; + } + if (row < -results.size()) { + beforeFirst(); + return false; + } + + currentPosition = (row > 0) ? row - 1 : results.size() + row; + return true; + } + + @Override + public boolean relative(int rows) throws SQLException { + if (!hasResults()) { + return false; + } + if (rows == 0) { + return !(isBeforeFirst() || isAfterLast()); + } + if (currentPosition + rows >= results.size()) { + afterLast(); + return false; + } + if (currentPosition + rows <= -1) { + beforeFirst(); + return false; + } + + return absolute(currentPosition + rows + 1); + } + + @Override + public boolean previous() throws SQLException { + if (!hasResults() || isBeforeFirst()) { + return false; + } + currentPosition--; + return !isBeforeFirst(); + } + + /** + * Moves to the target position if results is not empty. + * + * @param position target position + * @return successful operation status + */ + private boolean moveIfHasResults(int position) { + if (!hasResults()) { + return false; + } + currentPosition = position; + return true; + } +} diff --git a/src/main/java/org/tarantool/util/SQLStates.java b/src/main/java/org/tarantool/util/SQLStates.java index 48f7f332..347234fc 100644 --- a/src/main/java/org/tarantool/util/SQLStates.java +++ b/src/main/java/org/tarantool/util/SQLStates.java @@ -2,8 +2,11 @@ public enum SQLStates { + TOO_MANY_RESULTS("0100E"), + NO_DATA("02000"), + CONNECTION_DOES_NOT_EXIST("08003"), INVALID_PARAMETER_VALUE("22023"), - CONNECTION_DOES_NOT_EXIST("08003"); + INVALID_CURSOR_STATE("24000"); private final String sqlState; diff --git a/src/test/java/org/tarantool/TestUtils.java b/src/test/java/org/tarantool/TestUtils.java index a4907ec9..dfa94af4 100644 --- a/src/test/java/org/tarantool/TestUtils.java +++ b/src/test/java/org/tarantool/TestUtils.java @@ -5,11 +5,27 @@ import java.util.Map; public class TestUtils { + static final String replicationInfoRequest = "return " + "box.info.id, " + "box.info.lsn, " + "box.info.replication"; + @FunctionalInterface + public interface ThrowingAction { + void run() throws X; + } + + public static Runnable throwingWrapper(ThrowingAction action) { + return () -> { + try { + action.run(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + } + public static String makeReplicationString(String user, String pass, String... addrs) { StringBuilder sb = new StringBuilder(); for (int idx = 0; idx < addrs.length; idx++) { diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index 6bc57acb..93f8c72d 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -21,7 +21,6 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; -@SuppressWarnings("Since15") public class JdbcConnectionIT extends AbstractJdbcIT { @Test @@ -172,10 +171,10 @@ public void testCreateHoldableStatement() throws SQLException { ResultSet.HOLD_CURSORS_OVER_COMMIT ); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); + } - assertThrows(SQLException.class, () -> { - conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, Integer.MAX_VALUE); - }); + @Test + public void testCreateUnsupportedHoldableStatement() throws SQLException { assertThrows( SQLFeatureNotSupportedException.class, () -> conn.createStatement( @@ -183,12 +182,18 @@ public void testCreateHoldableStatement() throws SQLException { ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT )); + } + + @Test + public void testCreateWrongHoldableStatement() throws SQLException { + assertThrows(SQLException.class, () -> { + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, Integer.MAX_VALUE); + }); assertThrows(SQLException.class, () -> { - conn.close(); conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, - ResultSet.HOLD_CURSORS_OVER_COMMIT + -65 ); }); } @@ -209,25 +214,213 @@ public void testPrepareHoldableStatement() throws SQLException { ResultSet.HOLD_CURSORS_OVER_COMMIT ); assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); + } - assertThrows( - SQLException.class, - () -> conn.prepareStatement( - sqlString, - ResultSet.TYPE_FORWARD_ONLY, + @Test + public void testPrepareUnsupportedHoldableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, + () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.CLOSE_CURSORS_AT_COMMIT + ); + }); + } + + @Test + public void testPrepareWrongHoldableStatement() throws SQLException { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + Integer.MAX_VALUE + ); + }); + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, -190 + ); + }); + } + + @Test + public void testCreateScrollableStatement() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, statement.getResultSetType()); + + statement = conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_SCROLL_INSENSITIVE, statement.getResultSetType()); + } + + @Test + public void testCreateUnsupportedScrollableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, () -> { + conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + }); + assertThrows(SQLFeatureNotSupportedException.class, () -> { + conn.createStatement( + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY, - Integer.MAX_VALUE - )); - assertThrows( - SQLFeatureNotSupportedException.class, - () -> conn.prepareStatement( + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testCreateWrongScrollableStatement() { + assertThrows(SQLException.class, () -> { + conn.createStatement(Integer.MAX_VALUE, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + assertThrows(SQLException.class, () -> { + conn.createStatement(-47, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + } + + @Test + public void testPrepareScrollableStatement() throws SQLException { + String sqlString = "TEST"; + Statement statement = conn.prepareStatement(sqlString); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + + statement = conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + } + + @Test + public void testPrepareUnsupportedScrollableStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement(sqlString, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); + }); + assertThrows(SQLFeatureNotSupportedException.class, () -> { + String sqlString = "SELECT * FROM TEST"; + conn.prepareStatement( sqlString, - ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT - )); - assertThrows( - SQLException.class, + ); + }); + } + + @Test + public void testPrepareWrongScrollableStatement() throws SQLException { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, + () -> { + conn.prepareStatement( + sqlString, + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + Integer.MAX_VALUE + ); + }); + assertThrows(SQLException.class, () -> { + conn.prepareStatement(sqlString, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -90); + }); + } + + @Test + public void testCreateConcurrentStatement() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + + statement = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + + statement = conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + } + + @Test + public void testCreateUnsupportedConcurrentStatement() throws SQLException { + assertThrows(SQLFeatureNotSupportedException.class, () -> { + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); + }); + assertThrows(SQLFeatureNotSupportedException.class, + () -> { + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_UPDATABLE, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testCreateWrongConcurrentStatement() { + assertThrows(SQLException.class, () -> { + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, Integer.MAX_VALUE, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + assertThrows(SQLException.class, () -> { + conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, -7213, ResultSet.HOLD_CURSORS_OVER_COMMIT); + }); + } + + @Test + public void testCreateStatementWithClosedConnection() { + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); + } + + @Test + public void testPrepareStatementWithClosedConnection() { + String sqlString = "SELECT * FROM TEST"; + assertThrows(SQLException.class, () -> { conn.close(); conn.prepareStatement( @@ -237,6 +430,16 @@ public void testPrepareHoldableStatement() throws SQLException { ResultSet.HOLD_CURSORS_OVER_COMMIT ); }); + assertThrows(SQLException.class, + () -> { + conn.close(); + conn.prepareStatement( + sqlString, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT + ); + }); } @Test diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index da2e1438..99ccf284 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -20,6 +20,7 @@ import java.util.regex.Pattern; public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { + private DatabaseMetaData meta; @BeforeEach @@ -41,7 +42,7 @@ public void testGetTableTypes() throws SQLException { @Test public void testGetAllTables() throws SQLException { - ResultSet rs = meta.getTables(null, null, null, new String[] {"TABLE"}); + ResultSet rs = meta.getTables(null, null, null, new String[] { "TABLE" }); assertNotNull(rs); assertTrue(rs.next()); @@ -63,7 +64,7 @@ public void testGetAllTables() throws SQLException { @Test public void testGetTable() throws SQLException { - ResultSet rs = meta.getTables(null, null, "TEST", new String[] {"TABLE"}); + ResultSet rs = meta.getTables(null, null, "TEST", new String[] { "TABLE" }); assertNotNull(rs); assertTrue(rs.next()); assertEquals("TEST", rs.getString("TABLE_NAME")); @@ -127,7 +128,7 @@ public void testGetPrimaryKeysCompound() throws SQLException { @Test public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException { - String[] vals = new String[] {null, "", "IGNORE"}; + String[] vals = new String[] { null, "", "IGNORE" }; for (String cat : vals) { for (String schema : vals) { ResultSet rs = meta.getPrimaryKeys(cat, schema, "TEST"); @@ -143,7 +144,7 @@ public void testGetPrimaryKeysIgnoresCatalogSchema() throws SQLException { @Test public void testGetPrimaryKeysNotFound() throws SQLException { - String[] tables = new String[] {null, "", "NOSUCHTABLE"}; + String[] tables = new String[] { null, "", "NOSUCHTABLE" }; for (String t : tables) { ResultSet rs = meta.getPrimaryKeys(null, null, t); assertNotNull(rs); @@ -203,14 +204,17 @@ public void testClosedConnection() throws SQLException { @Override public void execute() throws Throwable { switch (step) { - case 0: meta.getTables(null, null, null, new String[]{"TABLE"}); - break; - case 1: meta.getColumns(null, null, "TEST", null); - break; - case 2: meta.getPrimaryKeys(null, null, "TEST"); - break; - default: - fail(); + case 0: + meta.getTables(null, null, null, new String[] { "TABLE" }); + break; + case 1: + meta.getColumns(null, null, "TEST", null); + break; + case 2: + meta.getPrimaryKeys(null, null, "TEST"); + break; + default: + fail(); } } }); @@ -283,4 +287,88 @@ public void testNullsAreSortedProperties() throws SQLException { assertFalse(meta.nullsAreSortedAtStart()); assertFalse(meta.nullsAreSortedAtEnd()); } + + @Test + public void testSupportsResultSetType() throws SQLException { + assertTrue(meta.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY)); + assertTrue(meta.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertFalse(meta.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE)); + assertFalse(meta.supportsResultSetType(Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetType(Integer.MIN_VALUE)); + assertFalse(meta.supportsResultSetType(54)); + } + + @Test + public void testSupportsResultSetConcurrency() throws SQLException { + // valid combinations + assertTrue(meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)); + assertTrue(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)); + + // everything else is invalid + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)); + assertFalse(meta.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)); + + // bad inputs are also unsupported + assertFalse(meta.supportsResultSetConcurrency(Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetConcurrency(Integer.MIN_VALUE, Integer.MAX_VALUE)); + assertFalse(meta.supportsResultSetConcurrency(54, -45)); + } + + @Test + public void testInsertDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersInsertsAreVisible(type)); + assertFalse(meta.ownInsertsAreVisible(type)); + assertFalse(meta.insertsAreDetected(type)); + } + + } + + @Test + public void testUpdateDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersUpdatesAreVisible(type)); + assertFalse(meta.ownUpdatesAreVisible(type)); + assertFalse(meta.updatesAreDetected(type)); + } + } + + @Test + public void testDeleteDetectionSupport() throws SQLException { + int[] types = new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE, + -23, + Integer.MIN_VALUE, + Integer.MAX_VALUE + }; + + for (int type : types) { + assertFalse(meta.othersDeletesAreVisible(type)); + assertFalse(meta.ownDeletesAreVisible(type)); + assertFalse(meta.deletesAreDetected(type)); + } + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index 4dbefdba..24e4a539 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -201,6 +201,12 @@ public void testSupportGeneratedKeys() throws SQLException { assertEquals(ResultSet.CONCUR_READ_ONLY, generatedKeys.getConcurrency()); } + @Test + void testStatementConnection() throws SQLException { + Statement statement = conn.prepareStatement("SELECT * FROM TEST"); + assertEquals(conn, statement.getConnection()); + } + @Test public void testSetByte() throws SQLException { makeHelper(Byte.class) diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java index 1d30cc13..944c4198 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -14,6 +14,7 @@ import java.math.BigDecimal; import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; @@ -129,6 +130,47 @@ public void testGetByteArrayColumn() throws SQLException { .testGetColumn(); } + @Test + public void testDefaultScrollType() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(stmt.getResultSetType(), resultSet.getType()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::getType); + } + + @Test + public void testSelectedScrollType() throws SQLException { + Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ResultSet resultSet = statement.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(statement.getResultSetType(), resultSet.getType()); + + statement.close(); + assertThrows(SQLException.class, resultSet::getType); + } + + @Test + public void testOwnedResultSet() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + assertEquals(stmt, resultSet.getStatement()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::getStatement); + } + + @Test + public void testResultSetMetadataAfterClose() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertNotNull(metaData); + resultSet.close(); + assertEquals(metaData, resultSet.getMetaData()); + } + @Test public void testHoldability() throws SQLException { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test WHERE id < 0"); @@ -178,4 +220,106 @@ public void testNullsSortingDesc() throws SQLException { assertFalse(resultSet.next()); } + @Test + public void testFindUniqueColumnLabels() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertEquals(1, resultSet.findColumn("f1")); + assertEquals(2, resultSet.findColumn("f2")); + } + + @Test + public void testFindDuplicatedColumnLabels() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f1 FROM test"); + assertNotNull(resultSet); + assertEquals(1, resultSet.findColumn("f1")); + } + + @Test + public void testMaxRows() throws SQLException { + stmt.setMaxRows(1); + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.next()); + assertTrue(resultSet.getInt("f1") > 0); + assertFalse(resultSet.next()); + } + + @Test + public void testForwardTraversal() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.isBeforeFirst()); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertTrue(resultSet.isFirst()); + assertEquals(1, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(2, resultSet.getRow()); + + assertTrue(resultSet.next()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertFalse(resultSet.next()); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isAfterLast()); + + stmt.close(); + assertThrows(SQLException.class, resultSet::isAfterLast); + } + + @Test + public void testTraversal() throws SQLException { + Statement statement = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); + ResultSet resultSet = statement.executeQuery("SELECT id as f1, val as f2 FROM test"); + assertNotNull(resultSet); + assertTrue(resultSet.isBeforeFirst()); + assertEquals(0, resultSet.getRow()); + + assertTrue(resultSet.last()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.first()); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + assertFalse(resultSet.relative(-1)); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isBeforeFirst()); + + assertTrue(resultSet.relative(1)); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + assertTrue(resultSet.absolute(-1)); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.absolute(1)); + assertEquals(1, resultSet.getRow()); + assertTrue(resultSet.isFirst()); + + resultSet.beforeFirst(); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isBeforeFirst()); + + resultSet.afterLast(); + assertEquals(0, resultSet.getRow()); + assertTrue(resultSet.isAfterLast()); + + assertTrue(resultSet.previous()); + assertEquals(3, resultSet.getRow()); + assertTrue(resultSet.isLast()); + + assertTrue(resultSet.first()); + assertEquals(1, resultSet.getRow()); + + assertFalse(resultSet.previous()); + assertEquals(0, resultSet.getRow()); + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java index e0a8a8c9..b5b66228 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetMetaDataIT.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.sql.ResultSet; @@ -13,8 +14,11 @@ import java.sql.SQLException; import java.sql.Statement; +@DisplayName("A resultSet metadata") public class JdbcResultSetMetaDataIT extends AbstractJdbcIT { + @Test + @DisplayName("returned correct column names") public void testColumnNames() throws SQLException { Statement stmt = conn.createStatement(); assertNotNull(stmt); @@ -36,6 +40,7 @@ public void testColumnNames() throws SQLException { } @Test + @DisplayName("unwrapped correct") public void testUnwrap() throws SQLException { try ( Statement statement = conn.createStatement(); @@ -48,6 +53,7 @@ public void testUnwrap() throws SQLException { } @Test + @DisplayName("checked as a proper wrapper") public void testIsWrapperFor() throws SQLException { try ( Statement statement = conn.createStatement(); @@ -58,4 +64,72 @@ public void testIsWrapperFor() throws SQLException { assertFalse(metaData.isWrapperFor(Integer.class)); } } + + @Test + @DisplayName("returned a correct result columns size") + public void testColumnCount() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(2, metaData.getColumnCount()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT id, val FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(2, metaData.getColumnCount()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT id FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals(1, metaData.getColumnCount()); + } + } + } + + @Test + @DisplayName("returned correct result column aliases") + public void testColumnAliases() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT id AS alias_id FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ALIAS_ID", metaData.getColumnLabel(1).toUpperCase()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT val AS alias_val FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ALIAS_VAL", metaData.getColumnLabel(1).toUpperCase()); + } + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + assertEquals("ID", metaData.getColumnLabel(1).toUpperCase()); + assertEquals("VAL", metaData.getColumnLabel(2).toUpperCase()); + } + } + } + + @Test + @DisplayName("returned an error when column index is out of range") + public void testWrongColumnAliases() throws SQLException { + try (Statement statement = conn.createStatement()) { + assertNotNull(statement); + + try (ResultSet resultSet = statement.executeQuery("SELECT * FROM test")) { + assertNotNull(resultSet); + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnsNumber = metaData.getColumnCount(); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(columnsNumber + 1)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(-5)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(Integer.MAX_VALUE)); + assertThrows(SQLException.class, () -> metaData.getColumnLabel(Integer.MIN_VALUE)); + } + } + } + } diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 21362751..cffbcfa6 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -145,4 +145,9 @@ void testUnsupportedGeneratedKeys() { } } + @Test + void testStatementConnection() throws SQLException { + Statement statement = conn.createStatement(); + assertEquals(conn, statement.getConnection()); + } } diff --git a/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java b/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java new file mode 100644 index 00000000..9738916d --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/AbstractCursorIteratorTest.java @@ -0,0 +1,144 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Minimal iterator use cases should be implemented by every {@link CursorIterator}. + */ +public abstract class AbstractCursorIteratorTest { + + protected abstract CursorIterator> getCursorIterator(List> result); + + protected List> makeSingletonListResult(Object... rows) { + List> result = new ArrayList<>(); + for (Object row : rows) { + result.add(Collections.singletonList(row)); + } + return result; + } + + @Test + @DisplayName("failed with a null result object") + void testFailIteratorWithNullResult() { + assertThrows(IllegalArgumentException.class, () -> new InMemoryForwardCursorIteratorImpl(null)); + } + + @Test + @DisplayName("iterated through an empty result") + void testIterationOverEmptyResult() throws SQLException { + List> result = makeSingletonListResult(); + CursorIterator> iterator = getCursorIterator(result); + + assertEmpty(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.next()); + assertEmpty(iterator); + } + } + + @Test + @DisplayName("iterated through the non-empty results") + void testUseCases() throws SQLException { + List> result = makeSingletonListResult("1"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2", "3"); + forwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("1", "2", "3", "4", "5", "6", "7", "8", "9"); + forwardIteratorUseCase(getCursorIterator(result), result); + } + + /** + * Tests an expected behaviour from a forward iterator. + * + * @param iterator forward iterator to be tested + * @param result result is backed by iterator + */ + protected void forwardIteratorUseCase(CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(result.isEmpty()); + + assertBeforeFirst(iterator); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.next()); + assertNthPosition(i + 1, iterator, result); + } + + assertFalse(iterator.next()); // after last + assertAfterLast(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.next()); + assertAfterLast(iterator); + } + } + + protected void assertBeforeFirst(CursorIterator> iterator) throws SQLException { + assertTrue(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + + protected void assertAfterLast(CursorIterator> iterator) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertTrue(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + + protected void assertFirst(CursorIterator> iterator, List> result) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertTrue(iterator.isFirst()); + assertFalse(iterator.isAfterLast()); + assertEquals(1, iterator.getRow()); + assertEquals(result.get(0), iterator.getItem()); + } + + protected void assertLast(CursorIterator> iterator, List> result) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertTrue(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(result.size(), iterator.getRow()); + assertEquals(result.get(result.size() - 1), iterator.getItem()); + } + + protected void assertNthPosition(int position, CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isAfterLast()); + assertEquals(position, iterator.getRow()); + assertEquals(result.get(position - 1), iterator.getItem()); + } + + protected void assertEmpty(CursorIterator> iterator) throws SQLException { + assertFalse(iterator.isBeforeFirst()); + assertFalse(iterator.isFirst()); + assertFalse(iterator.isLast()); + assertFalse(iterator.isAfterLast()); + assertEquals(0, iterator.getRow()); + assertThrows(SQLException.class, iterator::getItem); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java b/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java new file mode 100644 index 00000000..acb5babc --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/InMemoryForwardCursorIteratorImplTest.java @@ -0,0 +1,38 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +@DisplayName("A forward iterator") +class InMemoryForwardCursorIteratorImplTest extends AbstractCursorIteratorTest { + + @Override + protected CursorIterator> getCursorIterator(List> result) { + return new InMemoryForwardCursorIteratorImpl(result); + } + + @Test + @DisplayName("failed trying to use unsupported operations") + void testUnsupportedOperations() { + List> result = makeSingletonListResult("1"); + CursorIterator> iterator = getCursorIterator(result); + + assertThrows(SQLException.class, iterator::beforeFirst); + assertThrows(SQLException.class, iterator::afterLast); + assertThrows(SQLException.class, iterator::first); + assertThrows(SQLException.class, iterator::last); + assertThrows(SQLException.class, () -> iterator.absolute(0)); + assertThrows(SQLException.class, () -> iterator.absolute(Integer.MIN_VALUE)); + assertThrows(SQLException.class, () -> iterator.absolute(Integer.MAX_VALUE)); + assertThrows(SQLException.class, () -> iterator.relative(0)); + assertThrows(SQLException.class, () -> iterator.relative(Integer.MIN_VALUE)); + assertThrows(SQLException.class, () -> iterator.relative(Integer.MAX_VALUE)); + assertThrows(SQLException.class, iterator::previous); + } + +} diff --git a/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java b/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java new file mode 100644 index 00000000..e8d5435e --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/cursor/InMemoryScrollableCursorIteratorImplTest.java @@ -0,0 +1,306 @@ +package org.tarantool.jdbc.cursor; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.tarantool.TestUtils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +@DisplayName("A scrollable iterator") +class InMemoryScrollableCursorIteratorImplTest extends AbstractCursorIteratorTest { + + @Override + protected CursorIterator> getCursorIterator(List> result) { + return new InMemoryScrollableCursorIteratorImpl(result); + } + + @Test + @DisplayName("failed with a null result object") + void testFailIteratorWithNullResult() { + assertThrows(IllegalArgumentException.class, () -> new InMemoryScrollableCursorIteratorImpl(null)); + } + + @Test + @DisplayName("moved to the last position") + void testMoveLast() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.last()); + assertLast(iterator, result); + } + + @Test + @DisplayName("moved to the first position") + void testMoveFirst() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + assertTrue(iterator.first()); + assertFirst(iterator, result); + } + + @Test + @DisplayName("moved to before the first position") + void testMoveBeforeFirst() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + iterator.beforeFirst(); + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to after the last position") + void testMoveAfterLast() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + } + + @Test + @DisplayName("moved to an absolute position") + void testMoveAbsolute() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.absolute(i + 1)); + assertNthPosition(i + 1, iterator, result); + } + for (int i = result.size() + 1; i < result.size() + 10; i++) { + assertFalse(iterator.absolute(i)); + assertAfterLast(iterator); + } + } + + @Test + @DisplayName("moved to a negative absolute position (reverse traversal)") + void testMoveNegativeAbsolute() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + for (int i = 0; i < result.size(); i++) { + assertTrue(iterator.absolute(-i - 1)); // -1 -2 -3 -4 -5 + assertNthPosition(5 - i, iterator, result); // 5 4 3 2 1 + } + for (int i = -result.size() - 1; i > -result.size() - 10; i--) { + assertFalse(iterator.absolute(i)); + assertBeforeFirst(iterator); + } + } + + @Test + @DisplayName("moved to a relative position") + void testMoveRelative() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.relative(3)); // before the first -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(-2)); // 3 -> 1 (first) + assertNthPosition(1, iterator, result); + + assertTrue(iterator.relative(4)); // 1 -> 5 (last) + assertNthPosition(5, iterator, result); + + assertTrue(iterator.relative(-3)); // 5 -> 2 + assertNthPosition(2, iterator, result); + + assertFalse(iterator.relative(-2)); // 2 -> before the first + assertBeforeFirst(iterator); + + assertFalse(iterator.relative(0)); // before the first -> before the first + assertBeforeFirst(iterator); + + assertFalse(iterator.relative(-2)); // before the first -> before the first + assertBeforeFirst(iterator); + + assertTrue(iterator.relative(4)); // before the first -> 4 + assertNthPosition(4, iterator, result); + + assertFalse(iterator.relative(2)); // 4 -> after the last + assertAfterLast(iterator); + + assertTrue(iterator.relative(-3)); // after the last -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(0)); // 3 -> 3 + assertNthPosition(3, iterator, result); + + assertTrue(iterator.relative(1)); // 3 -> 4 + assertNthPosition(4, iterator, result); + + assertFalse(iterator.relative(5)); // 4 -> after last + assertAfterLast(iterator); + + assertTrue(iterator.relative(-4)); // after last -> 2 + assertNthPosition(2, iterator, result); + + assertFalse(iterator.relative(-3)); // 2 -> before first + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to before the first using absolute navigation") + void testMoveAbsoluteZero() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> iterator = getCursorIterator(result); + + assertBeforeFirst(iterator); + assertTrue(iterator.absolute(3)); + assertNthPosition(3, iterator, result); + + assertFalse(iterator.absolute(0)); // move to the before the first + assertBeforeFirst(iterator); + } + + @Test + @DisplayName("moved to the same positions using an absolute positioning") + void testMoveAbsoluteSimilarities() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> firstIterator = getCursorIterator(result); + + CursorIterator> secondIterator = getCursorIterator(result); + + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + + // absolute(1) is the same as calling first() + firstIterator.absolute(1); + secondIterator.first(); + assertFirst(firstIterator, result); + assertFirst(secondIterator, result); + + // absolute(-1) is the same as calling last() + firstIterator.absolute(-1); + secondIterator.last(); + assertLast(firstIterator, result); + assertLast(secondIterator, result); + + // absolute(0) is the same as calling beforeFirst() + firstIterator.absolute(0); + secondIterator.beforeFirst(); + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + } + + @Test + @DisplayName("moved to the same positions using an relative positioning") + void testMoveRelativeSimilarities() throws SQLException { + List> result = makeSingletonListResult("a", "b", "c", "d", "e"); + CursorIterator> firstIterator = getCursorIterator(result); + + CursorIterator> secondIterator = getCursorIterator(result); + + assertBeforeFirst(firstIterator); + assertBeforeFirst(secondIterator); + + // relative(1) is the same as calling next() + for (int i = 0; i < result.size(); i++) { + assertTrue(firstIterator.relative(1)); + assertTrue(secondIterator.next()); + assertNthPosition(i + 1, firstIterator, result); + assertNthPosition(i + 1, secondIterator, result); + } + + assertLast(firstIterator, result); + assertLast(secondIterator, result); + + // relative(-1) is the same as calling previous() + for (int i = result.size(); i > 1; i--) { + assertTrue(firstIterator.relative(-1)); + assertTrue(secondIterator.previous()); + assertNthPosition(i - 1, firstIterator, result); + assertNthPosition(i - 1, secondIterator, result); + } + + assertFirst(firstIterator, result); + assertFirst(secondIterator, result); + } + + @Test + @DisplayName("moved to edges over an empty result") + void testIterationOverEmptyResult() throws SQLException { + List> result = makeSingletonListResult(); + CursorIterator> iterator = getCursorIterator(result); + + Runnable[] actions = new Runnable[] { + TestUtils.throwingWrapper(iterator::beforeFirst), + TestUtils.throwingWrapper(iterator::afterLast), + TestUtils.throwingWrapper(iterator::first), + TestUtils.throwingWrapper(iterator::last), + TestUtils.throwingWrapper(iterator::previous), + TestUtils.throwingWrapper(() -> iterator.relative(1)), + TestUtils.throwingWrapper(() -> iterator.absolute(1)), + }; + + for (Runnable action : actions) { + assertEmpty(iterator); + action.run(); + } + } + + @Test + @DisplayName("iterated through the non-empty results") + void testUseCases() throws SQLException { + List> result = makeSingletonListResult("a"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b", "c"); + backwardIteratorUseCase(getCursorIterator(result), result); + + result = makeSingletonListResult("a", "b", "c", "d", "e", "f", "g", "h", "i"); + backwardIteratorUseCase(getCursorIterator(result), result); + } + + /** + * Tests an expected behaviour from a scrollable iterator. + * + * @param iterator scrollable iterator to be tested + * @param result result is backed by iterator + */ + protected void backwardIteratorUseCase(CursorIterator> iterator, List> result) + throws SQLException { + assertFalse(result.isEmpty()); + + assertBeforeFirst(iterator); + iterator.afterLast(); + assertAfterLast(iterator); + + for (int i = result.size() - 1; i >= 0; i--) { + assertTrue(iterator.previous()); + assertNthPosition(i + 1, iterator, result); + } + + assertFalse(iterator.previous()); // before first + assertBeforeFirst(iterator); + + for (int i = 0; i < 10; i++) { + assertFalse(iterator.previous()); + assertBeforeFirst(iterator); + } + } + +}