Repository navigation
feat(jdbc): accept transactionMode in the JDBC URL and connection properties - #295
Merged
Merged
Conversation
…perties The mode selects the begin SQL for the next transaction, so a write transaction can take its lock at BEGIN instead of failing a read-to-write upgrade with SQLITE_BUSY_SNAPSHOT. The value is connection behaviour, not transport config, so it travels in a ParsedConnection alongside the client config rather than in LibsqlClientConfig.
sqld answers a forbidden write with HTTP 403 and {"error":"..."}, which
the codecs read as an empty Hrana message. Treating the error field as the
message keeps the server's refusal text, and preserving the HTTP status lets
LibsqlErrorMapper report SQLState 28000 instead of HY000.
The server invalidates the baton with the refusal, so clearing it on 401/403
lets the next statement open a fresh stream rather than replay a dead one.
Performance benchmarkBase Driver-side microbenchmarks only; server and network time are excluded. Shared-runner noise is about ±10%, so only larger regressions are flagged. JDK 25.0.5, Linux amd64, 4 CPUs. Throughput
Allocation
Workloads and thresholds live in |
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Issue #294. sqld opens a deferred transaction by default. A transaction that reads before it writes then fails its snapshot upgrade with
SQLITE_BUSY_SNAPSHOTwhen another connection commits. SelectingBEGIN IMMEDIATEfrom configuration removes that failure without a wrapper that unwrapsLibsqlConnection.The same live sqld experiment that proved a read-only token boundary also exposed a transport bug: a 403 with
{"error":"..."}was mapped toHY000with an empty message.What changed
transactionModeis now a JDBC URL parameter and a connection property:deferred,immediate, orexclusive, case-insensitive, defaultdeferred. A URL value wins over a property; an unknown value fails at connect.ParsedConnection, so the publicparsesignature andLibsqlClientConfigstay unchanged.28000, and drop the baton so the connection can read again after a refused write.setReadOnly.Scope
readOnlyJDBC parameter. sqld refuses everyPRAGMA query_onlyform, so a driver-side parser would be a fake boundary. A server-enforced read-only connection uses a sqld JWT witha: ro.LibsqlClientConfigor the wire config; the mode is per-connection behaviour.Tradeoffs
Blast Radius
deferred, so behaviour is unchanged unlesstransactionModeis set.Verification
./mvnw -o -pl libsql-jdbc test: 1636 tests, 0 failures, 0 errors.TransactionModeIT(2) andAuthTokenIT(4) against sqld v0.24.33: immediate holds the write lock from the first statement; a read-only token reads, a write is refused withSQLInvalidAuthorizationSpecException/28000and the server message, and the same connection reads again../mvnw -o spotless:check: pass.libsql-clientsocket unit test cannot bind under the local nono sandbox (BindException: Permission denied); the same path is covered end to end byAuthTokenITand runs on CI.