Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
import java.util.List;
import java.util.Locale;

/**
* Prepared statement over the Hrana transport.
*
* <p>Post-close transitions follow {@link LibsqlStatement}: {@link #addBatch()} and {@link
* #getGeneratedKeys()} throw after {@link #close()}. A prepared execute already throws once closed,
* so there is no post-close execute that could expose a result set. sqlite-jdbc 3.53.4.0 instead
* returns generated keys, and rejects {@code addBatch()} with a different message; see issue #87.
*/
public class LibsqlPreparedStatement extends LibsqlStatement implements PreparedStatement {

private final String sql;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
* working after {@link #close()}. This deliberately copies a likely xerial bug for drop-in
* compatibility; see the issue #47 contract decision. A result set produced after close is
* returned closed, so a closed statement never holds an open result set.
* <li>Every other post-close transition throws {@code SQLException("Statement is closed")}, per
* {@code java.sql}: {@link #addBatch(String)}, {@link #getGeneratedKeys()}, and {@link
* #getResultSet()}, including the {@code getResultSet} call that follows an execute on the
* closed statement. sqlite-jdbc 3.53.4.0 diverges: it accepts {@code addBatch}, returns a
* fresh generated-key result set, and reopens its internal pointer so {@code getResultSet}
* hands out a live result set. The driver follows {@code java.sql}; see issue #87.
* <li>{@link #close()} and the execute methods are safe when called concurrently. State
* transitions share one lock; when close wins a race, the execute closes and discards its
* result set instead of attaching it to the closed statement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,98 @@ void preparedStatementExecuteAfterCloseThrows() throws SQLException {
assertThat(connection.executeCalls).isZero();
}

@Test
void addBatchAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
Statement statement = connection.createStatement();
statement.close();

assertThatThrownBy(() -> statement.addBatch("insert into t(value) values (1)"))
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
assertThat(connection.executeCalls).isZero();
}

@Test
void preparedAddBatchAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "insert into t(value) values (?)");
statement.setLong(1, 1L);
statement.close();

assertThatThrownBy(statement::addBatch)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
assertThat(connection.executeCalls).isZero();
}

@Test
void getGeneratedKeysAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
Statement statement = connection.createStatement();
statement.execute("insert into t(value) values (1)");
statement.close();

assertThatThrownBy(statement::getGeneratedKeys)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void preparedGetGeneratedKeysAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "insert into t(value) values (1)");
statement.execute();
statement.close();

assertThatThrownBy(statement::getGeneratedKeys)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void getResultSetAfterCloseThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
Statement statement = connection.createStatement();
statement.execute("select value from t");
statement.close();

assertThatThrownBy(statement::getResultSet)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void getResultSetAfterPostCloseExecuteThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
Statement statement = connection.createStatement();
statement.close();

assertThat(statement.execute("select value from t")).isTrue();
assertThat(connection.executeCalls).isEqualTo(1);
assertThatThrownBy(statement::getResultSet)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
}

@Test
void preparedGetResultSetAfterPostCloseExecuteAttemptThrows() throws SQLException {
RecordingConnection connection = new RecordingConnection(ROWS);
LibsqlPreparedStatement statement =
new LibsqlPreparedStatement(connection, "select value from t");
statement.close();

assertThatThrownBy(statement::execute)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
assertThatThrownBy(statement::getResultSet)
.isInstanceOf(SQLException.class)
.hasMessage("Statement is closed");
assertThat(connection.executeCalls).isZero();
}

private static final class RecordingConnection extends LibsqlConnection {
private final StmtResult result;
private int executeCalls;
Expand Down
Loading