Skip to content

test(jdbc): pin post-close statement transitions per java.sql - #148

Merged
magnusp merged 1 commit into
mainfrom
fix/i87-post-close-jdbc
Oct 23, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/i87-post-close-jdbc

Conversation

@magnusp

@magnusp magnusp commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Fixes #87

Contract

Hrana 3 is the source of truth for transport, values, metadata, batching, transactions, and error reporting. java.sql is the source of truth for the JDBC surface. xerial/sqlite-jdbc is a behavioral reference and conformance foundation, never a copy target. This change is protocol-neutral: java.sql defines post-close behavior.

What changed

The driver already obeyed java.sql for every transition the issue names, so no runtime behavior changed. This PR enumerates the transitions, pins them in LibsqlStatementLifecycleTest (one case per transition), and records the enumeration in the class javadoc of LibsqlStatement and LibsqlPreparedStatement. The #47 execute-after-close exception is unchanged and scoped: a closed statement still lets execute reach the server for xerial drop-in compatibility, but it never exposes a result set afterwards.

# Post-close transition Pinned behavior
1 Statement.addBatch(String) SQLException("Statement is closed"), nothing queued
2 PreparedStatement.addBatch() SQLException("Statement is closed"), nothing queued
3 Statement.getGeneratedKeys() SQLException("Statement is closed")
4 PreparedStatement.getGeneratedKeys() SQLException("Statement is closed")
5 Statement.getResultSet() after close() SQLException("Statement is closed")
6 Statement.getResultSet() after a post-close re-execute SQLException("Statement is closed"); the re-execute still runs per #47, and getResultSet still rejects
7 PreparedStatement.getResultSet() after a rejected post-close execute() SQLException("Statement is closed"); the failed execute does not reopen the statement

Red and green

  • Green: LibsqlStatementLifecycleTest, 21/21 pass at head 8e2bfcd (/tmp/xerial-parity-i87/lane-1-head.log).
  • Trunk: the new cases also pass at origin/main 9bf5d0f, because the runtime behavior pre-existed; the coverage gap, not a behavior gap, was real (lane-1-trunk.log).
  • Pin proof: removing the checkClosed() guard from addBatch, addBatch(), getGeneratedKeys, and getResultSet in a throwaway trunk worktree fails exactly the 7 new cases (7 failures) and leaves the 14 pre-existing cases green (lane-1-mutation.log). So each case fails for the asserted reason when its guard is absent.

xerial reference notes

Probed directly against the pinned xerial 3.53.4.0 jar (/tmp/xerial-parity-i87/xerial-probe.log). Every row is a difference, recorded as a reference, not a target:

  1. Plain addBatch(String) after close: xerial accepts and queues silently. Driver throws per java.sql.
  2. getGeneratedKeys() after close, plain and prepared: xerial returns a new open result set (empty or with keys). Driver throws.
  3. Plain execute + getResultSet after close: xerial reopens its internal pointer and hands out a live result set. Driver keeps the Statement lifecycle and getMoreResults argument handling differ #47 re-execute but throws from getResultSet, because the JDBC statement is still closed.
  4. Prepared execute after close: xerial throws SQLException("statement is not executing"); the driver throws SQLException("Statement is closed") and its getResultSet also throws.
  5. Wording: where xerial rejects a post-close call, its message is statement is not executing. The driver uses Statement is closed (and statement is closed for the closeOnCompletion auto-closes on ResultSet.close; xerial only tracks the flag #86 close-on-completion pair), matching its established Statement lifecycle and getMoreResults argument handling differ #47/closeOnCompletion auto-closes on ResultSet.close; xerial only tracks the flag #86 wording and describing the JDBC state rather than an internal execution pointer. Lane 9's message cases pass against the driver's wording; the xerial wording difference is this note.

No vendored xerial test covers addBatch/getGeneratedKeys/getResultSet after close, so Lane 2 used the nearest lifecycle tests (MetadataLeakTest, ResultSetWithoutResultsTest) in control mode plus the direct probe above.

Evidence

Base 9bf5d0f (origin/main), head 8e2bfcd6975924a1b51aa76367dc85632695a591. Logs in /tmp/xerial-parity-i87/.

Lane Command Result
1 lifecycle at trunk / head 21/21 at both; mutation probe red 7/7
2 -Pxerial-control verify -Dit.test=MetadataLeakTest,ResultSetWithoutResultsTest 2/2 pass, BUILD SUCCESS
3 -pl libsql-jdbc test -Dtest=LibsqlStatementLifecycleTest 21/21 pass, prepared paths identical
4 -pl libsql-jdbc test 285/285, BUILD SUCCESS
5 -pl libsql-conformance -am verify 123 tests, 1 skipped, BUILD SUCCESS
6 -pl libsql-conformance -am -Pxerial-control verify 100 tests, 1 skipped, BUILD SUCCESS
7 spotless:check BUILD SUCCESS
8 ./mvnw test reactor BUILD SUCCESS
9 -Dtest='LibsqlStatementLifecycleTest#*AfterClose*+*PostClose*' 10/10 pass; xerial wording divergence noted above
10 -pl libsql-jdbc -am package -DskipTests jar built

Perf: -pl libsql-jdbc test wall clock, three interleaved runs each side. Trunk median 2.09 s, head median 2.11 s (+0.96%, within the 10% rule). No hot-path code changed.

Note: the plan names mise exec --; this environment's brief and AGENTS.md say to run java and ./mvnw directly. Every lane above ran without mise. Docker was available for the conformance lanes.

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.
@magnusp
magnusp merged commit c3ef4de into main Oct 23, 2026
6 checks passed
@magnusp
magnusp deleted the fix/i87-post-close-jdbc branch October 23, 2026 01:39
magnusp added a commit that referenced this pull request Oct 23, 2026
Fixes #81

Contract: Hrana 3 is the source of truth for transport, values,
metadata, batching, transactions, and error reporting; `java.sql` is the
source of truth for the JDBC surface; xerial/sqlite-jdbc is a behavioral
reference and conformance foundation, never a copy target.

## What changed

`LibsqlPreparedStatement.executeCurrent()` called `checkClosed();
beginExecution(); checkClosed();`. The second check runs after the state
transition and before `connection.executeStatement`, which already
checks the connection itself. Removed it, leaving the single
`checkClosed()` before the state transition.

One file changed, one line deleted, no behavior change.

## Divergence from the plan's Files block

`LibsqlStatement.java` is in the plan's Files block but needed no edit:
its second `checkClosed()` was already removed in `4b30b46` (#85), which
matched xerial's tolerance of `execute` after `statement.close()`. That
behavior is pinned by
`LibsqlStatementLifecycleTest.executeAndGetConnectionStillWorkAfterClose`
(#148), so adding a `checkClosed()` to the statement execute path would
be a behavior change and was not done. `executeSql` reads
`beginExecution();` with the connection check inside
`LibsqlConnection.executeStatement`.

## Evidence

Red at trunk (`origin/main` `de31dc6`): `executeCurrent` at lines
231-235 reads `checkClosed(); beginExecution(); checkClosed();`. Green
at head (`2621b71`): one check. Log:
`/tmp/xerial-parity-i81/lane-3-specific.log`.

Lanes at head (logs under `/tmp/xerial-parity-i81/`):

1. Regression lane: not applicable. The plan's unit box says no new
tests, so there is no new failing case; the diff check stands in for it
(lane 3).
2. Lifecycle and prepared suites, trunk vs head: identical, 21
`LibsqlStatementLifecycleTest` + 6 `LibsqlPreparedStatementTest` = 27/27
pass on both sides (`trunk-unit.log`, `head-unit.log`).
3. No double check remains in either file (`lane-3-specific.log`).
4. `-pl libsql-jdbc test`: BUILD SUCCESS, 293/293
(`lane-4-module-suite.log`); trunk is also 293/293
(`trunk-module-suite.log`).
5. `-pl libsql-conformance -am verify`: BUILD SUCCESS, 123 tests, 0
failures, 1 skipped, no divergence line (`lane-5-conformance.log`).
6. `-Pxerial-control verify`: BUILD SUCCESS, 100 tests, 0 failures, 1
skipped; differences are the pre-existing reference notes, none
introduced here (`lane-6-control.log`).
7. `spotless:check`: BUILD SUCCESS (`lane-7-spotless.log`).
8. `./mvnw test`: BUILD SUCCESS, 293/293 in `libsql-jdbc`
(`lane-8-reactor.log`).
9. Statement surface: BUILD SUCCESS, 30/30 (`lane-9-specific.log`).
10. `-pl libsql-jdbc -am package -DskipTests`: BUILD SUCCESS, driver jar
built (`lane-10-package.log`).

Perf: three interleaved `./mvnw -pl libsql-jdbc test -q` runs per side,
trunk first. Trunk median 2.161 s, head median 2.167 s (+0.3%, within
the 10% rule). Raw values in `perf-raw.txt`.

Base `de31dc6`, head `2621b71e5c77c5a9976519942e2b8d5b2044ff29`.

No merge; stopping at merge-ready.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Post-close statement operations diverge from xerial

1 participant