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 b627ab1..e22b90c 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 @@ -870,7 +870,15 @@ public boolean isLast() throws SQLException { @Override public boolean absolute(int row) throws SQLException { throw new SQLException("Forward only"); } @Override public boolean relative(int rows) throws SQLException { throw new SQLException("Forward only"); } @Override public boolean previous() throws SQLException { throw new SQLException("Forward only"); } - @Override public void setFetchDirection(int direction) throws SQLException {} + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction != FETCH_FORWARD && direction != FETCH_REVERSE && direction != FETCH_UNKNOWN) { + throw new SQLException( + "Unknown fetch direction " + + direction + + ". Must be one of FETCH_FORWARD, FETCH_REVERSE, or FETCH_UNKNOWN"); + } + } @Override public int getFetchDirection() throws SQLException { return FETCH_FORWARD; } @Override public void setFetchSize(int rows) throws SQLException { @@ -1170,7 +1178,8 @@ private LocalDateTime dateTimeValue(int columnIndex, String targetType) throws S case Value.Integer i -> epochMillisToLocalDateTime(i.value(), targetType); case Value.Float f -> epochMillisToLocalDateTime(julianDayToEpochMillis(f.value()), targetType); - case Value.Text t -> parseSqliteDateTime(t.value(), targetType); + case Value.Text t -> + t.value().isBlank() ? null : parseSqliteDateTime(t.value(), targetType); case Value.Blob b -> throw new SQLException("Bad value for type " + targetType); }; } 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 271e175..1492170 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 @@ -6,6 +6,7 @@ import com.github.magnusp.libsql.hrana.codec.StmtResult; import com.github.magnusp.libsql.hrana.codec.Value; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -533,7 +534,9 @@ public void clearBatch() throws SQLException { * Executes the batch and reports one update count per step. The generated keys and the update * count are rebuilt from the step results, so neither can leak from an earlier execution. The * update count is the total affected rows across the batch; JDBC leaves {@code getUpdateCount} - * unspecified after a batch, and xerial leaves the previous value in place. + * unspecified after a batch, and xerial leaves the previous value in place. A step that returns + * a result set throws {@link BatchUpdateException} with the counts of the steps up to and + * including it, per {@code java.sql}. */ @Override public int[] executeBatch() throws SQLException { @@ -545,8 +548,14 @@ public int[] executeBatch() throws SQLException { int[] updateCounts = new int[results.size()]; long affectedRows = 0; for (int i = 0; i < results.size(); i++) { - updateCounts[i] = (int) results.get(i).affectedRowCount(); - affectedRows += results.get(i).affectedRowCount(); + StmtResult result = results.get(i); + if (result.cols() != null && !result.cols().isEmpty()) { + throw new BatchUpdateException( + "batch entry " + i + ": query returns results", + java.util.Arrays.copyOf(updateCounts, i + 1)); + } + updateCounts[i] = (int) result.affectedRowCount(); + affectedRows += result.affectedRowCount(); } synchronized (stateLock) { updateBatchGeneratedKeys(statements, results); @@ -641,7 +650,18 @@ public void setQueryTimeout(int seconds) throws SQLException { public void setCursorName(String name) throws SQLException { throw new SQLFeatureNotSupportedException("Cursor names are not supported"); } - @Override public void setFetchDirection(int direction) throws SQLException {} + @Override + public void setFetchDirection(int direction) throws SQLException { + if (direction != ResultSet.FETCH_FORWARD + && direction != ResultSet.FETCH_REVERSE + && direction != ResultSet.FETCH_UNKNOWN) { + throw new SQLException( + "Unknown fetch direction " + + direction + + ". Must be one of FETCH_FORWARD, FETCH_REVERSE, or FETCH_UNKNOWN"); + } + } + @Override public int getFetchDirection() throws SQLException { return ResultSet.FETCH_FORWARD; } @Override public void setFetchSize(int rows) throws SQLException { diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java index 680e28d..43fcc5e 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java @@ -247,6 +247,19 @@ void textDateTimeFeedsEveryDateAndTimeGetter() throws SQLException { assertDateTimeGetters(rs, epochMillis); } + @Test + void emptyTextDateAndTimeGettersReturnNull() throws SQLException { + LibsqlResultSet rs = resultSet(new Value.Text("")); + assertThat(rs.next()).isTrue(); + + assertThat(rs.getDate(1)).isNull(); + assertThat(rs.getTime(1)).isNull(); + assertThat(rs.getTimestamp(1)).isNull(); + assertThat(rs.getObject(1, LocalDate.class)).isNull(); + assertThat(rs.getObject(1, LocalTime.class)).isNull(); + assertThat(rs.getObject(1, LocalDateTime.class)).isNull(); + } + @Test void integerEpochMillisFeedsEveryDateAndTimeGetter() throws SQLException { LibsqlResultSet rs = resultSet(new Value.Integer(1_700_000_000_000L)); diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java index bf313b8..64306a3 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java @@ -7,6 +7,7 @@ import com.github.magnusp.libsql.hrana.codec.Value; import org.junit.jupiter.api.Test; +import java.sql.BatchUpdateException; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -141,13 +142,58 @@ void setCursorNameIsNotSupported() { .hasMessage("Cursor names are not supported"); } + @Test + void setFetchDirectionAcceptsTheJdbcDirections() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(new RecordingConnection(THREE_ROWS)); + + statement.setFetchDirection(ResultSet.FETCH_FORWARD); + statement.setFetchDirection(ResultSet.FETCH_REVERSE); + statement.setFetchDirection(ResultSet.FETCH_UNKNOWN); + } + + @Test + void setFetchDirectionRejectsAnUnknownDirection() { + LibsqlStatement statement = new LibsqlStatement(new RecordingConnection(THREE_ROWS)); + + assertThatThrownBy(() -> statement.setFetchDirection(999)) + .isInstanceOf(SQLException.class) + .hasMessageContaining("999"); + } + + @Test + void batchThatReturnsResultsThrowsBatchUpdateException() throws SQLException { + StmtResult select = new StmtResult( + List.of(new Col("value", null)), + List.of(new Row(List.of(Value.ofNull()))), + 0, + null); + StmtResult insert = new StmtResult(List.of(), List.of(), 1, 1L); + LibsqlStatement statement = + new LibsqlStatement(new RecordingConnection(THREE_ROWS, List.of(insert, select))); + + statement.addBatch("insert into t(value) values (1)"); + statement.addBatch("select null"); + + assertThatThrownBy(statement::executeBatch) + .isInstanceOf(BatchUpdateException.class) + .hasMessageContaining("query returns results") + .satisfies(thrown -> assertThat(((BatchUpdateException) thrown).getUpdateCounts()) + .containsExactly(1, 0)); + } + private static final class RecordingConnection extends LibsqlConnection { private final StmtResult result; + private final List batchResults; private final List executedSql = new ArrayList<>(); private RecordingConnection(StmtResult result) { + this(result, List.of()); + } + + private RecordingConnection(StmtResult result, List batchResults) { super(null, null); this.result = result; + this.batchResults = batchResults; } @Override @@ -155,5 +201,10 @@ public StmtResult executeStatement(Stmt stmt) { executedSql.add(stmt.sql()); return result; } + + @Override + public List executeBatchResults(List statements) { + return batchResults; + } } }