diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java index 43a704f..a20123e 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java @@ -483,18 +483,30 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe @Override public Map> getTypeMap() throws SQLException { - return Map.of(); + throw new SQLFeatureNotSupportedException("Type maps are not supported"); } @Override - public void setTypeMap(Map> map) throws SQLException {} + public void setTypeMap(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("Type maps are not supported"); + } @Override - public void setHoldability(int holdability) throws SQLException {} + public void setHoldability(int holdability) throws SQLException { + checkClosed(); + checkHoldability(holdability); + } @Override public int getHoldability() throws SQLException { - return 1; // HOLD_CURSORS_OVER_COMMIT + checkClosed(); + return ResultSet.CLOSE_CURSORS_AT_COMMIT; + } + + private static void checkHoldability(int holdability) throws SQLException { + if (holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT) { + throw new SQLException("SQLite only supports CLOSE_CURSORS_AT_COMMIT"); + } } @Override @@ -519,16 +531,22 @@ public void releaseSavepoint(Savepoint savepoint) throws SQLException { @Override public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + checkClosed(); + checkHoldability(resultSetHoldability); return createStatement(); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + checkClosed(); + checkHoldability(resultSetHoldability); return prepareStatement(sql); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + checkClosed(); + checkHoldability(resultSetHoldability); return prepareCall(sql); } diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java index 58ed213..eac9f03 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java @@ -565,8 +565,8 @@ public Connection getConnection() throws SQLException { @Override public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { throw new UnsupportedOperationException(); } @Override public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { throw new UnsupportedOperationException(); } @Override public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { throw new UnsupportedOperationException(); } - @Override public boolean supportsResultSetHoldability(int holdability) throws SQLException { return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; } - @Override public int getResultSetHoldability() throws SQLException { return ResultSet.HOLD_CURSORS_OVER_COMMIT; } + @Override public boolean supportsResultSetHoldability(int holdability) throws SQLException { return holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT; } + @Override public int getResultSetHoldability() throws SQLException { return ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public int getDatabaseMajorVersion() throws SQLException { String[] parts = resolveDatabaseProductVersion().split("\\."); try { return Integer.parseInt(parts[0]); } catch (NumberFormatException e) { return 3; } diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java index 030190b..cda4b2c 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -59,6 +59,7 @@ public class LibsqlResultSet implements ResultSet { private int currentRowIndex = -1; private int lastColumnIndex = 0; private boolean closed = false; + private int fetchSize = 0; private LibsqlResultSetMetaData metadata; // Mitigation for Bug 6: O(1) column label lookup @@ -867,8 +868,14 @@ public boolean isLast() throws SQLException { @Override public boolean previous() throws SQLException { throw new SQLException("Forward only"); } @Override public void setFetchDirection(int direction) throws SQLException {} @Override public int getFetchDirection() throws SQLException { return FETCH_FORWARD; } - @Override public void setFetchSize(int rows) throws SQLException {} - @Override public int getFetchSize() throws SQLException { return 0; } + @Override + public void setFetchSize(int rows) throws SQLException { + if (rows < 0) { + throw new SQLException("fetch size " + rows + " out of bounds 0"); + } + fetchSize = rows; + } + @Override public int getFetchSize() throws SQLException { return fetchSize; } @Override public boolean rowUpdated() throws SQLException { return false; } @Override public boolean rowInserted() throws SQLException { return false; } @Override public boolean rowDeleted() throws SQLException { return false; } @@ -947,7 +954,7 @@ public boolean isLast() throws SQLException { @Override public RowId getRowId(String columnLabel) throws SQLException { throw new UnsupportedOperationException(); } @Override public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new UnsupportedOperationException(); } @Override public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new UnsupportedOperationException(); } - @Override public int getHoldability() throws SQLException { return HOLD_CURSORS_OVER_COMMIT; } + @Override public int getHoldability() throws SQLException { return CLOSE_CURSORS_AT_COMMIT; } @Override public void updateNString(int columnIndex, String nString) throws SQLException { throw new UnsupportedOperationException(); } @Override public void updateNString(String columnLabel, String nString) throws SQLException { throw new UnsupportedOperationException(); } @Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new UnsupportedOperationException(); } diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java index 973a68c..877cc7c 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java @@ -56,6 +56,7 @@ public class LibsqlStatement implements Statement { private ResultSet generatedKeys = null; private boolean resultSetRequested = false; private boolean closing = false; + private int fetchSize = 0; public LibsqlStatement(LibsqlConnection connection) { this.connection = connection; @@ -413,8 +414,22 @@ public boolean isClosed() throws SQLException { @Override public void setCursorName(String name) throws SQLException {} @Override public void setFetchDirection(int direction) throws SQLException {} @Override public int getFetchDirection() throws SQLException { return ResultSet.FETCH_FORWARD; } - @Override public void setFetchSize(int rows) throws SQLException {} - @Override public int getFetchSize() throws SQLException { return 0; } + @Override + public void setFetchSize(int rows) throws SQLException { + if (rows < 0) { + throw new SQLException("fetch size " + rows + " out of bounds 0"); + } + synchronized (stateLock) { + fetchSize = rows; + } + } + + @Override + public int getFetchSize() throws SQLException { + synchronized (stateLock) { + return fetchSize; + } + } @Override public int getResultSetConcurrency() throws SQLException { return ResultSet.CONCUR_READ_ONLY; } @Override public int getResultSetType() throws SQLException { return ResultSet.TYPE_FORWARD_ONLY; } @Override @@ -445,7 +460,7 @@ public ResultSet getGeneratedKeys() throws SQLException { @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { return execute(sql); } @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { return execute(sql); } @Override public boolean execute(String sql, String[] columnNames) throws SQLException { return execute(sql); } - @Override public int getResultSetHoldability() throws SQLException { return ResultSet.HOLD_CURSORS_OVER_COMMIT; } + @Override public int getResultSetHoldability() throws SQLException { return ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public void setPoolable(boolean poolable) throws SQLException {} @Override public boolean isPoolable() throws SQLException { return false; } @Override diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDriverSettingsTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDriverSettingsTest.java new file mode 100644 index 0000000..a9d92bb --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDriverSettingsTest.java @@ -0,0 +1,113 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import org.junit.jupiter.api.Test; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LibsqlDriverSettingsTest { + + private static final StmtResult EMPTY = + new StmtResult(List.of(new Col("value", null)), List.of(), 0, null); + + @Test + void typeMapReadAndWriteAreRejected() throws SQLException { + LibsqlConnection connection = new LibsqlConnection(null, null); + + assertThatThrownBy(connection::getTypeMap) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> connection.setTypeMap(Map.of("x", String.class))) + .isInstanceOf(SQLFeatureNotSupportedException.class); + } + + @Test + void connectionHoldabilityIsCloseCursorsAtCommit() throws SQLException { + LibsqlConnection connection = new LibsqlConnection(null, null); + + assertThat(connection.getHoldability()).isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT); + connection.setHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT); + assertThat(connection.getHoldability()).isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT); + } + + @Test + void connectionRejectsHoldCursorsOverCommit() throws SQLException { + LibsqlConnection connection = new LibsqlConnection(null, null); + + assertThatThrownBy(() -> connection.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)) + .isInstanceOf(SQLException.class) + .hasMessageContaining("CLOSE_CURSORS_AT_COMMIT"); + } + + @Test + void statementFactoriesRejectHoldCursorsOverCommit() throws SQLException { + LibsqlConnection connection = new LibsqlConnection(null, null); + + assertThatThrownBy(() -> connection.createStatement( + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT)) + .isInstanceOf(SQLException.class) + .hasMessageContaining("CLOSE_CURSORS_AT_COMMIT"); + assertThatThrownBy(() -> connection.prepareStatement( + "select 1", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, + ResultSet.HOLD_CURSORS_OVER_COMMIT)) + .isInstanceOf(SQLException.class) + .hasMessageContaining("CLOSE_CURSORS_AT_COMMIT"); + } + + @Test + void metadataHoldabilityAgreesWithConnection() throws SQLException { + LibsqlConnection connection = new LibsqlConnection(null, null); + DatabaseMetaData metadata = connection.getMetaData(); + + assertThat(metadata.getResultSetHoldability()) + .isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT); + assertThat(metadata.supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT)) + .isTrue(); + assertThat(metadata.supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)) + .isFalse(); + } + + @Test + void statementAndResultSetReportCloseCursorsAtCommit() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + + assertThat(statement.getResultSetHoldability()) + .isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT); + assertThat(new LibsqlResultSet(statement, EMPTY).getHoldability()) + .isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT); + } + + @Test + void statementFetchSizeRoundTripsAndRejectsNegativeValues() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + + assertThat(statement.getFetchSize()).isZero(); + statement.setFetchSize(10); + assertThat(statement.getFetchSize()).isEqualTo(10); + assertThatThrownBy(() -> statement.setFetchSize(-1)) + .isInstanceOf(SQLException.class); + } + + @Test + void resultSetFetchSizeRoundTripsAndRejectsNegativeValues() throws SQLException { + LibsqlResultSet resultSet = new LibsqlResultSet(new LibsqlStatement(null), EMPTY); + + assertThat(resultSet.getFetchSize()).isZero(); + resultSet.setFetchSize(10); + assertThat(resultSet.getFetchSize()).isEqualTo(10); + assertThatThrownBy(() -> resultSet.setFetchSize(-1)) + .isInstanceOf(SQLException.class); + } +}