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
259 changes: 0 additions & 259 deletions IMPLEMENTATION_BUGS_AND_RESOLUTIONS.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static HttpClient getOrCreateHttpClient(Duration connectTimeout) {
}

public StmtResult executeOneShot(Stmt stmt) {
// Mitigation for Bug 1: pipeline [execute, close] in a single roundtrip to prevent 128-stream 10s stall
// One-shot: [execute, close] in one round trip, so no idle stream is left for the server to reap.
PipelineReqBody req = new PipelineReqBody(
null,
List.of(
Expand Down
48 changes: 48 additions & 0 deletions libsql-conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,51 @@ namespaces are not retained.
The conformance copy of an upstream test diverges from xerial where Hrana connection
lifecycle makes the original behavior impossible; each divergence is recorded in
`../upstream-xerial_sqlite-jdbc.manifest`.

## Divergences from a local SQLite connection

Hrana over HTTP has no connection. A stream is identified only by the `baton`
in the pipeline request: `baton: null` creates a new stream, and the server
closes streams after a short period of inactivity because there is no
connection that would let it detect an abandoned stream
(`../vendor/HRANA_3_SPEC.md`, "Hrana over HTTP"). Two observable divergences
from xerial `sqlite-jdbc`, which owns one SQLite connection per JDBC
connection, follow from that model. Per-test divergences in the vendored
suite are recorded separately in `../upstream-xerial_sqlite-jdbc.manifest`.

### Auto-commit holds no stream

An auto-commit statement runs as a one-shot pipeline that executes the
statement and closes the stream in the same round trip. The driver keeps no
session stream open between statements, so an idle connection consumes no
server stream slot and holds no baton for the idle expiry to invalidate.
Keeping one open only to preserve session state would reintroduce
stale-baton failures on idle connections.

Because `StmtResult.last_insert_rowid` describes the insert on one stream
and nothing else, a later `SELECT last_insert_rowid()` runs on a fresh
stream and returns `0`, even when the same `Statement` inserted a row
immediately before. xerial returns that insert's rowid instead. Use
`Statement.getGeneratedKeys()` after the insert, which the driver builds
from `StmtResult.last_insert_rowid` with no extra round trip, or run the
insert and the query inside one explicit transaction so both run on the
same stream. `LibsqlGeneratedKeysTest` pins the generated-keys path.

### Transactions begin lazily

`setAutoCommit(false)` sends nothing. The next statement begins the
transaction on a new stream with `BEGIN`, `BEGIN IMMEDIATE`, or
`BEGIN EXCLUSIVE` according to
`LibsqlConnection.setTransactionMode(TransactionMode)` (default `DEFERRED`),
sent as its own pipeline so a failed begin cannot be followed by the
statement running outside a transaction. `commit()` and `rollback()` close
the stream and do not begin the next transaction, so an idle non-auto-commit
connection likewise holds no stream.

The divergence is timing only: an `IMMEDIATE` or `EXCLUSIVE` transaction
takes its write lock when the first statement begins it, not at
`setAutoCommit(false)`. Select that mode when the lock must be held from
transaction start; with the default `DEFERRED`, two readers that both
upgrade can still see `SQLITE_BUSY`, as in SQLite. A transaction left open
and idle longer than the server timeout can still fail on a stale baton,
so keep transactions short and retry the work in a fresh one.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void testNonInteractiveOneShotSelect() {

@Test
void testAutocommitWriteBurstDoesNotStall() {
// Verifies mitigation for Bug 1: 128-stream 10-second stall
// One-shot close must keep a write burst from exhausting the server stream pool.
LibsqlClientConfig config = new LibsqlClientConfig(getHttpUrl(), null);
LibsqlHttpClient client = new LibsqlHttpClient(config);

Expand All @@ -51,7 +51,7 @@ void testAutocommitWriteBurstDoesNotStall() {
}
long duration = System.currentTimeMillis() - start;

// 150 one-shot writes should complete within < 3-4 seconds, well below the 10-second idle stall
// 150 one-shot writes should complete well within the 10-second bound
assertThat(duration).isLessThan(10000L);

StmtResult countResult = client.executeOneShot(new Stmt("SELECT COUNT(*) FROM burst_test"));
Expand All @@ -63,7 +63,7 @@ void testInteractiveTransactionLifecycleWithBaton() {
LibsqlClientConfig config = new LibsqlClientConfig(getHttpUrl(), null);
LibsqlHttpClient client = new LibsqlHttpClient(config);

// 1. BEGIN IMMEDIATE (prevents Bug 2 lock upgrade deadlocks)
// 1. BEGIN IMMEDIATE takes the write lock at begin, avoiding the deferred upgrade.
PipelineReqBody beginReq = new PipelineReqBody(null, List.of(new StreamRequest.Execute(new Stmt("BEGIN IMMEDIATE"))));
PipelineRespBody beginResp = client.sendPipeline(beginReq, null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public StmtResult executeStatement(Stmt stmt) throws SQLException {
checkClosed();
try {
if (autoCommit) {
// One-shot execution with single-roundtrip close (prevents Bug 1 128-stream 10s stall)
// One-shot: single-round-trip close, so no idle stream is left for the server to reap.
return client.executeOneShot(stmt);
} else {
ensureTransactionStarted();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class LibsqlPreparedStatement extends LibsqlStatement implements Prepared
private final String sql;
private final boolean returnsResultSet;
private final int parameterCount;
// Mitigation for Bug 7: Sequential ArrayList instead of Map to eliminate Integer autoboxing overhead
// Sequential list indexed by JDBC parameter position; no map or boxing overhead.
private final List<Value> parameters = new ArrayList<>();

public LibsqlPreparedStatement(LibsqlConnection connection, String sql) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public class LibsqlResultSet implements ResultSet {
private int fetchSize = 0;
private LibsqlResultSetMetaData metadata;

// Mitigation for Bug 6: O(1) column label lookup
// O(1) column label lookup; the map is built lazily on first use.
private Map<String, Integer> columnNameToIndex;

public LibsqlResultSet(Statement statement, StmtResult result) {
Expand Down
Loading