Skip to content

fix(jdbc): begin non-auto-commit transactions lazily (#99) - #125

Merged
magnusp merged 1 commit into
mainfrom
fix/i99-lazy-begin
Oct 22, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/i99-lazy-begin

Conversation

@magnusp

@magnusp magnusp commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Description

setAutoCommit(false) no longer sends BEGIN, and commit() and rollback() no longer begin the next transaction. An idle non-auto-commit connection holds no Hrana stream, so sqld can no longer expire a stream under it. The next statement begins the transaction.

Protocol basis. Hrana over HTTP closes idle streams after inactivity and requires serial baton use, so a stream held from setAutoCommit(false) is a resource with a ten second lifetime, not a transaction handle.

Contract. Hrana 3 is the source of truth for transport, values, and metadata. 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.

Fixes #99

Type of Change

  • Bug fix (non-breaking change fixing an issue)

Pending-begin semantics

  • setAutoCommit(false) records the state and sends nothing. The pending-begin state is derived as !autoCommit && baton == null. A second boolean would be a second source of truth for a fact the baton already carries.
  • The next statement sends BEGIN in its own pipeline, checks the result, and only then sends the statement on the new baton. BEGIN is never pipelined with the statement, because Hrana over HTTP executes every request in a pipeline even after an earlier one fails.
  • commit() or rollback() with no work sends nothing and succeeds. Probed against xerial 3.53.4.0: commit() with no work, a repeated commit(), and rollback() with no work all succeed there.
  • commit() or rollback() after work sends the command, then closes the stream. On a server or transport error the baton stays, so the retry and rollback-on-the-same-stream behavior from Failed COMMIT or ROLLBACK leaves a stale baton, so the transaction cannot be rolled back #98 is unchanged.
  • setAutoCommit(true) and close() send nothing from a pending begin.

Divergence

  • An IMMEDIATE or EXCLUSIVE transaction takes its write lock at the first statement, not at setAutoCommit(false) or commit(). xerial begins eagerly because it owns the transaction locally. Hrana over HTTP expires an idle stream, so an eager begin cannot be held. TransactionLifecycleIT test names now state the lazy timing.
  • A commit after work costs two round trips (COMMIT then CLOSE) instead of one. Combining them into one pipeline would destroy the stream when COMMIT fails and remove the Failed COMMIT or ROLLBACK leaves a stale baton, so the transaction cannot be rolled back #98 guarantee that the caller can roll back on the same stream.

Evidence

Base 8c9381b856b9f9ba42fb3e28de5a0cd816de0509, head 2495f95f8cf06646ee5c1ae0ea11c03eb67fcf00.

Red at base:

  • ./mvnw -pl libsql-jdbc test -Dtest=LibsqlConnectionTest gives Tests run: 27, Failures: 9, Errors: 1. The failures are the eager BEGIN, the eager re-begin after commit and rollback, and requests sent from a pending begin by commit, rollback, and setAutoCommit(true).
  • ./mvnw -pl libsql-itests -am verify -Dit.test=TransactionLifecycleIT gives 2 errors, both The stream has expired due to inactivity, on aStatementAndCommitAfterAnIdlePeriodLongerThanTheStreamTimeoutSucceed and anIdleConnectionHoldsNoStreamAfterCommit.

Green at head:

  • LibsqlConnectionTest: Tests run: 27, Failures: 0, Errors: 0.
  • TransactionLifecycleIT: Tests run: 6, Failures: 0, Errors: 0.
  • ./mvnw -pl libsql-itests -am verify: 211 unit and 24 integration tests, BUILD SUCCESS.
  • ./mvnw -pl libsql-conformance -am verify: 115 tests, 1 skipped, BUILD SUCCESS.
  • ./mvnw -pl libsql-conformance -am -Pxerial-control verify: 100 tests, 1 skipped, BUILD SUCCESS.
  • ./mvnw spotless:check: BUILD SUCCESS.
  • ./mvnw test: 211 tests, BUILD SUCCESS.
  • ./mvnw -pl libsql-jdbc -am package -DskipTests: BUILD SUCCESS.
  • HikariCpIntegrationIT run on its own: 1 test, BUILD SUCCESS.

Perf

Hrana pipeline round trips for setAutoCommit(false), commit(), fifteen idle seconds, then one SELECT, counted with a scripted client:

  • Base: 1 begin, 1 commit with re-begin, 0 during idle, 1 select. Total 3.
  • Head: 0 begin, 0 commit, 0 during idle, 2 select (checked BEGIN, then SELECT). Total 2.

The head sends no request during the idle window and totals fewer round trips than the base. With one INSERT before the commit the base totals 4 and the head totals 6, from the commit close described in the divergence note.

Checklist

  • My code follows the code style of this project (./mvnw spotless:check).
  • I have added tests that prove my fix is effective or that my feature works.
  • All new and existing tests passed (./mvnw clean verify equivalent run above).
  • I have updated documentation where appropriate (the setAutoCommit contract javadoc).

@magnusp
magnusp merged commit 37a86a0 into main Oct 22, 2026
5 checks passed
@magnusp
magnusp deleted the fix/i99-lazy-begin branch October 22, 2026 20:15
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.

Non-auto-commit connections hold an idle Hrana stream that sqld expires

1 participant