Skip to content

Commit 65270da

Browse files
committed
docs: publish canonical xerial conformance statement
1 parent 8fc3571 commit 65270da

1 file changed

Lines changed: 139 additions & 48 deletions

File tree

‎libsql-conformance/README.md‎

Lines changed: 139 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,145 @@ This module runs a selected subset of the xerial `sqlite-jdbc` test sources agai
44
libSQL over the Hrana protocol. It is a compatibility check for the JDBC driver, not a
55
claim that a `jdbc:sqlite:` URL is served by a local SQLite file.
66

7+
## Conformance status
8+
9+
This section is the canonical answer to "have we reached conformance with
10+
xerial?". The short answer is: yes for the selected subset below, verified
11+
against the reference probe; no for full xerial parity, and full parity is
12+
not the goal. The exclusions are deliberate compromises of running SQLite
13+
semantics remotely over Hrana, grouped below. Per-test selection, rejection,
14+
and divergence lines are recorded in
15+
[`../upstream-xerial_sqlite-jdbc.manifest`](../upstream-xerial_sqlite-jdbc.manifest);
16+
this statement summarizes the driver-contract divergences without repeating them.
17+
18+
### Selected subset and reference probe
19+
20+
The suite vendors the pinned xerial `sqlite-jdbc` 3.53.4.0 test tree and
21+
selects the tests that exercise JDBC behavior through ordinary SQL, so the
22+
Hrana transport can satisfy them unchanged. The manifest selects 13 test
23+
classes plus the shared `Utils` support class: `ResultSetTest`,
24+
`InsertQueryTest`, `FetchSizeTest`, `CachedRowSetTest`, `JSON1Test`,
25+
`MetadataLeakTest`, `PreparedStatementThreadTest`,
26+
`ResultSetWithoutResultsTest`, `RSMetaDataTest`, `DBMetaDataTest`,
27+
`StatementTest`, `PrepStmtTest`, and `SavepointTest`. The remaining 26
28+
classes in the pinned tree stay upstream for the reasons in the manifest.
29+
30+
The reference probe is the `xerial-control` Maven profile defined in
31+
[`pom.xml`](pom.xml): it runs the same vendored manifest against the pinned
32+
xerial 3.53.4.0 driver instead of the libSQL driver. Both drivers are on the test class path, so the control run removes
33+
every visible driver and registers the xerial driver explicitly, and the
34+
build rejects a hand-set `conformance.control.driver` so the probe can only
35+
be selected through the profile. Run it with:
36+
37+
```bash
38+
./mvnw -pl libsql-conformance -am -Pxerial-control verify
39+
```
40+
41+
Run the libSQL conformance suite itself, against the pinned sqld image,
42+
with:
43+
44+
```bash
45+
./mvnw -pl libsql-conformance -am verify
46+
```
47+
48+
Last green result: 272 tests, 7 skipped, with `SavepointTest` included. The
49+
parity queue (#54) closed at 268 tests, 7 skipped.
50+
51+
### Three-layer contract
52+
53+
When the layers disagree, the contract is:
54+
55+
1. Hrana 3 is the source of truth for transport, values, metadata,
56+
batching, transactions, and error reporting. The protocol is specified
57+
in [`../vendor/HRANA_3_SPEC.md`](../vendor/HRANA_3_SPEC.md); where
58+
xerial behavior would require a primitive Hrana does not expose, the
59+
Hrana behavior wins.
60+
2. `java.sql` is the source of truth for the JDBC surface. Where JDBC
61+
makes a feature optional or defines the observable behavior, the
62+
driver follows `java.sql`, even when xerial chooses differently.
63+
3. xerial is a behavioral reference and the conformance foundation, never
64+
a copy target. Its tests define the selected subset and the control
65+
probe; its driver internals, configuration surface, and native
66+
behaviors are not reproduced.
67+
68+
### Compromise categories
69+
70+
Full parity is impossible without changing what the driver is: a remote
71+
Hrana client with no local file, no shared connection, and no native
72+
library. The rejected classes in the manifest group into four compromise
73+
categories, each with a concrete example:
74+
75+
| Category | Why parity is impossible | Example |
76+
|---|---|---|
77+
| Native extension API | The test registers JVM callbacks or loads native code in-process; Hrana exposes no equivalent primitive, so this cannot be reversed without a new Hrana or native primitive. | `UDFTest` registers JVM user-defined functions through sqlite-jdbc's native extension API. |
78+
| sqlite-jdbc configuration surface | The test exercises xerial's own configuration, `DataSource`, loading, or source conventions instead of portable JDBC over SQL; libSQL provides no counterpart. | `SQLiteConfigTest` tests the `SQLiteConfig` object, not the JDBC API. |
79+
| Remote-model semantics | The test asserts local-file or in-process SQLite semantics (file locking and multi-connection waits, shared cache, native error wording and codes, or a deliberate driver-contract divergence) that a single remote sqld server over Hrana cannot reproduce; see "Hrana over HTTP" in `../vendor/HRANA_3_SPEC.md` for the baton and stream-expiry model behind this. | `TransactionTest` asserts SQLite file-locking and multi-connection wait behavior. |
80+
| Build-flag dependence | The test passes only when the server binary is built with a compile-time option that the pinned sqld image does not guarantee; this is addressable by pinning the server build, not by driver work. | `MathFunctionsTest` requires the server binary to be compiled with `SQLITE_ENABLE_MATH_FUNCTIONS`. |
81+
82+
The first three categories are permanent: the class stays upstream unless
83+
the underlying Hrana, sqld, or sqlite-jdbc surface changes. Build-flag
84+
dependence is the exception: the class stays upstream only until the sqld
85+
build is pinned to enable the flag.
86+
87+
### Driver-contract divergences
88+
89+
Hrana over HTTP has no connection. A stream is identified only by the `baton`
90+
in the pipeline request: `baton: null` creates a new stream, and the server
91+
closes streams after a short period of inactivity because there is no
92+
connection that would let it detect an abandoned stream
93+
([`../vendor/HRANA_3_SPEC.md`](../vendor/HRANA_3_SPEC.md), "Hrana over HTTP").
94+
xerial `sqlite-jdbc` owns one SQLite connection per JDBC connection, so two
95+
observable divergences follow from the remote model. Unlike the rejected
96+
categories above, these occur in selected tests and are recorded per test as
97+
`# divergence:` lines in the manifest.
98+
99+
#### Auto-commit holds no stream
100+
101+
An auto-commit statement runs as a one-shot pipeline that executes the
102+
statement and closes the stream in the same round trip. The driver keeps no
103+
session stream open between statements, so an idle connection consumes no
104+
server stream slot and holds no baton for the idle expiry to invalidate.
105+
Keeping one open only to preserve session state would reintroduce stale-baton
106+
failures on idle connections.
107+
108+
Because `StmtResult.last_insert_rowid` describes the insert on one stream and
109+
nothing else, a later `SELECT last_insert_rowid()` runs on a fresh stream and
110+
returns `0`, even when the same `Statement` inserted a row immediately before.
111+
xerial returns that insert's rowid instead. Use `Statement.getGeneratedKeys()`
112+
after the insert, which the driver builds from `StmtResult.last_insert_rowid`
113+
with no extra round trip, or run the insert and the query inside one explicit
114+
transaction so both run on the same stream. `LibsqlGeneratedKeysTest` pins the
115+
generated-keys path.
116+
117+
#### Transactions begin lazily
118+
119+
`setAutoCommit(false)` sends nothing. The next statement begins the
120+
transaction on a new stream with `BEGIN`, `BEGIN IMMEDIATE`, or
121+
`BEGIN EXCLUSIVE` according to
122+
`LibsqlConnection.setTransactionMode(TransactionMode)` (default `DEFERRED`),
123+
sent as its own pipeline so a failed begin cannot be followed by the
124+
statement running outside a transaction. `commit()` and `rollback()` close
125+
the stream and do not begin the next transaction, so an idle non-auto-commit
126+
connection likewise holds no stream.
127+
128+
The divergence is timing only: an `IMMEDIATE` or `EXCLUSIVE` transaction takes
129+
its write lock when the first statement begins it, not at
130+
`setAutoCommit(false)`. Select that mode when the lock must be held from
131+
transaction start; with the default `DEFERRED`, two readers that both upgrade
132+
can still see `SQLITE_BUSY`, as in SQLite. A transaction left open and idle
133+
longer than the server timeout can still fail on a stale baton, so keep
134+
transactions short and retry the work in a fresh one.
135+
136+
### Savepoint support
137+
138+
Savepoints were the last addressable JDBC-surface gap. The driver now
139+
implements `Connection.setSavepoint()`, `setSavepoint(String)`,
140+
`rollback(Savepoint)`, and `releaseSavepoint(Savepoint)` as SQLite
141+
`SAVEPOINT`, `RELEASE`, and `ROLLBACK TO` statements over Hrana, and
142+
`DatabaseMetaData.supportsSavepoints()` returns `true`. `SavepointTest` is
143+
selected, so no JDBC-surface gap remains. No new server primitive was
144+
needed.
145+
7146
## URL mapping
8147

9148
The conformance driver maps `jdbc:sqlite:` URLs to sqld namespaces so the vendored
@@ -35,51 +174,3 @@ namespaces are not retained.
35174
The conformance copy of an upstream test diverges from xerial where Hrana connection
36175
lifecycle makes the original behavior impossible; each divergence is recorded in
37176
`../upstream-xerial_sqlite-jdbc.manifest`.
38-
39-
## Divergences from a local SQLite connection
40-
41-
Hrana over HTTP has no connection. A stream is identified only by the `baton`
42-
in the pipeline request: `baton: null` creates a new stream, and the server
43-
closes streams after a short period of inactivity because there is no
44-
connection that would let it detect an abandoned stream
45-
(`../vendor/HRANA_3_SPEC.md`, "Hrana over HTTP"). Two observable divergences
46-
from xerial `sqlite-jdbc`, which owns one SQLite connection per JDBC
47-
connection, follow from that model. Per-test divergences in the vendored
48-
suite are recorded separately in `../upstream-xerial_sqlite-jdbc.manifest`.
49-
50-
### Auto-commit holds no stream
51-
52-
An auto-commit statement runs as a one-shot pipeline that executes the
53-
statement and closes the stream in the same round trip. The driver keeps no
54-
session stream open between statements, so an idle connection consumes no
55-
server stream slot and holds no baton for the idle expiry to invalidate.
56-
Keeping one open only to preserve session state would reintroduce
57-
stale-baton failures on idle connections.
58-
59-
Because `StmtResult.last_insert_rowid` describes the insert on one stream
60-
and nothing else, a later `SELECT last_insert_rowid()` runs on a fresh
61-
stream and returns `0`, even when the same `Statement` inserted a row
62-
immediately before. xerial returns that insert's rowid instead. Use
63-
`Statement.getGeneratedKeys()` after the insert, which the driver builds
64-
from `StmtResult.last_insert_rowid` with no extra round trip, or run the
65-
insert and the query inside one explicit transaction so both run on the
66-
same stream. `LibsqlGeneratedKeysTest` pins the generated-keys path.
67-
68-
### Transactions begin lazily
69-
70-
`setAutoCommit(false)` sends nothing. The next statement begins the
71-
transaction on a new stream with `BEGIN`, `BEGIN IMMEDIATE`, or
72-
`BEGIN EXCLUSIVE` according to
73-
`LibsqlConnection.setTransactionMode(TransactionMode)` (default `DEFERRED`),
74-
sent as its own pipeline so a failed begin cannot be followed by the
75-
statement running outside a transaction. `commit()` and `rollback()` close
76-
the stream and do not begin the next transaction, so an idle non-auto-commit
77-
connection likewise holds no stream.
78-
79-
The divergence is timing only: an `IMMEDIATE` or `EXCLUSIVE` transaction
80-
takes its write lock when the first statement begins it, not at
81-
`setAutoCommit(false)`. Select that mode when the lock must be held from
82-
transaction start; with the default `DEFERRED`, two readers that both
83-
upgrade can still see `SQLITE_BUSY`, as in SQLite. A transaction left open
84-
and idle longer than the server timeout can still fail on a stale baton,
85-
so keep transactions short and retry the work in a fresh one.

0 commit comments

Comments
 (0)