Skip to content

Commit 8e2bfcd

Browse files
committed
test(jdbc): pin post-close statement transitions per java.sql (#87)
Enumerate the post-close addBatch, getGeneratedKeys, and getResultSet transitions for plain and prepared statements, and pin each with a LibsqlStatementLifecycleTest case. The driver already follows java.sql, so the production change is the class-javadoc enumeration only.
1 parent 9bf5d0f commit 8e2bfcd

3 files changed

Lines changed: 106 additions & 0 deletions

File tree

‎libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
import java.util.List;
2929
import java.util.Locale;
3030

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

3341
private final String sql;

‎libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
* working after {@link #close()}. This deliberately copies a likely xerial bug for drop-in
4343
* compatibility; see the issue #47 contract decision. A result set produced after close is
4444
* returned closed, so a closed statement never holds an open result set.
45+
* <li>Every other post-close transition throws {@code SQLException("Statement is closed")}, per
46+
* {@code java.sql}: {@link #addBatch(String)}, {@link #getGeneratedKeys()}, and {@link
47+
* #getResultSet()}, including the {@code getResultSet} call that follows an execute on the
48+
* closed statement. sqlite-jdbc 3.53.4.0 diverges: it accepts {@code addBatch}, returns a
49+
* fresh generated-key result set, and reopens its internal pointer so {@code getResultSet}
50+
* hands out a live result set. The driver follows {@code java.sql}; see issue #87.
4551
* <li>{@link #close()} and the execute methods are safe when called concurrently. State
4652
* transitions share one lock; when close wins a race, the execute closes and discards its
4753
* result set instead of attaching it to the closed statement.

‎libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementLifecycleTest.java‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,98 @@ void preparedStatementExecuteAfterCloseThrows() throws SQLException {
219219
assertThat(connection.executeCalls).isZero();
220220
}
221221

222+
@Test
223+
void addBatchAfterCloseThrows() throws SQLException {
224+
RecordingConnection connection = new RecordingConnection(ROWS);
225+
Statement statement = connection.createStatement();
226+
statement.close();
227+
228+
assertThatThrownBy(() -> statement.addBatch("insert into t(value) values (1)"))
229+
.isInstanceOf(SQLException.class)
230+
.hasMessage("Statement is closed");
231+
assertThat(connection.executeCalls).isZero();
232+
}
233+
234+
@Test
235+
void preparedAddBatchAfterCloseThrows() throws SQLException {
236+
RecordingConnection connection = new RecordingConnection(ROWS);
237+
LibsqlPreparedStatement statement =
238+
new LibsqlPreparedStatement(connection, "insert into t(value) values (?)");
239+
statement.setLong(1, 1L);
240+
statement.close();
241+
242+
assertThatThrownBy(statement::addBatch)
243+
.isInstanceOf(SQLException.class)
244+
.hasMessage("Statement is closed");
245+
assertThat(connection.executeCalls).isZero();
246+
}
247+
248+
@Test
249+
void getGeneratedKeysAfterCloseThrows() throws SQLException {
250+
RecordingConnection connection = new RecordingConnection(ROWS);
251+
Statement statement = connection.createStatement();
252+
statement.execute("insert into t(value) values (1)");
253+
statement.close();
254+
255+
assertThatThrownBy(statement::getGeneratedKeys)
256+
.isInstanceOf(SQLException.class)
257+
.hasMessage("Statement is closed");
258+
}
259+
260+
@Test
261+
void preparedGetGeneratedKeysAfterCloseThrows() throws SQLException {
262+
RecordingConnection connection = new RecordingConnection(ROWS);
263+
LibsqlPreparedStatement statement =
264+
new LibsqlPreparedStatement(connection, "insert into t(value) values (1)");
265+
statement.execute();
266+
statement.close();
267+
268+
assertThatThrownBy(statement::getGeneratedKeys)
269+
.isInstanceOf(SQLException.class)
270+
.hasMessage("Statement is closed");
271+
}
272+
273+
@Test
274+
void getResultSetAfterCloseThrows() throws SQLException {
275+
RecordingConnection connection = new RecordingConnection(ROWS);
276+
Statement statement = connection.createStatement();
277+
statement.execute("select value from t");
278+
statement.close();
279+
280+
assertThatThrownBy(statement::getResultSet)
281+
.isInstanceOf(SQLException.class)
282+
.hasMessage("Statement is closed");
283+
}
284+
285+
@Test
286+
void getResultSetAfterPostCloseExecuteThrows() throws SQLException {
287+
RecordingConnection connection = new RecordingConnection(ROWS);
288+
Statement statement = connection.createStatement();
289+
statement.close();
290+
291+
assertThat(statement.execute("select value from t")).isTrue();
292+
assertThat(connection.executeCalls).isEqualTo(1);
293+
assertThatThrownBy(statement::getResultSet)
294+
.isInstanceOf(SQLException.class)
295+
.hasMessage("Statement is closed");
296+
}
297+
298+
@Test
299+
void preparedGetResultSetAfterPostCloseExecuteAttemptThrows() throws SQLException {
300+
RecordingConnection connection = new RecordingConnection(ROWS);
301+
LibsqlPreparedStatement statement =
302+
new LibsqlPreparedStatement(connection, "select value from t");
303+
statement.close();
304+
305+
assertThatThrownBy(statement::execute)
306+
.isInstanceOf(SQLException.class)
307+
.hasMessage("Statement is closed");
308+
assertThatThrownBy(statement::getResultSet)
309+
.isInstanceOf(SQLException.class)
310+
.hasMessage("Statement is closed");
311+
assertThat(connection.executeCalls).isZero();
312+
}
313+
222314
private static final class RecordingConnection extends LibsqlConnection {
223315
private final StmtResult result;
224316
private int executeCalls;

0 commit comments

Comments
 (0)