Skip to content

fix(jdbc): close the prepared-statement binding and metadata gaps - #161

Merged
magnusp merged 1 commit into
mainfrom
fix/i157-prepared-gaps
Oct 23, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/i157-prepared-gaps

Conversation

@magnusp

@magnusp magnusp commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Fixes #157

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 3.53.4.0 is the behavioral reference and conformance foundation, never a copy target.

What changed

  • LibsqlPreparedStatement derives the parameter count from the SQL placeholders through SqlTokenizer, and implements ParameterMetaData. The count is local; the type is Types.OTHER because Hrana has no describe request.
  • Unset parameters, including the ones released by clearParameters(), bind SQL NULL. The prepared path pads to the placeholder count, and the plain Statement and addBatch(String) paths pad too, because Hrana rejects a short argument list where SQLite reads an unbound parameter as NULL.
  • setBlob, setClob, setBinaryStream, setAsciiStream, setUnicodeStream, and setCharacterStream, with their length and no-length overloads, convert to a TEXT or BLOB value locally. setRef, setArray, setRowId, and setSQLXML now throw SQLFeatureNotSupportedException instead of an unchecked exception.
  • DirectHranaJsonCodec.writeString encodes a surrogate pair as one UTF-8 sequence. It previously encoded each surrogate alone, which the UTF-8 encoder turned into two ? bytes; this is the loss utf saw. A codec test pins it.
  • setDate is unchanged. It sends the normalized java.sql.Date text. See the divergence section.

Red

New cases at base 36c7201 with only the test diff applied:

./mvnw -pl libsql-hrana-codec -Dtest=DirectHranaJsonCodecTest test
Tests run: 3, Failures: 1   testSerializeSupplementaryCharactersAsUtf8

./mvnw -pl libsql-jdbc -Dtest='LibsqlPreparedStatementTest,LibsqlStatementTest' test
Tests run: 27, Failures: 3, Errors: 4

Vendored PrepStmtTest at the same base, with the triage copy selected temporarily:

./mvnw -pl libsql-conformance -am -Dit.test=PrepStmtTest verify
Tests run: 43, Failures: 7, Errors: 8
paramMetaData, clobRS, blobRS, clearParameters, utf, set,
date1, date2, getParameterTypeTest, getParameterTypeTest_when_no_parameter_set,
metaData, gh811_getMetadata_before_execution, preparedStatementShouldNotThrowIfNotAllParamsSet,
noSuchTable, noSuchCol

Green

At head f098fc4:

./mvnw -pl libsql-jdbc -Dtest='LibsqlPreparedStatementTest,LibsqlStatementTest' test
Tests run: 27, Failures: 0, Errors: 0
./mvnw -pl libsql-hrana-codec -Dtest=DirectHranaJsonCodecTest test
Tests run: 3, Failures: 0, Errors: 0
./mvnw -pl libsql-conformance -am -Dit.test=PrepStmtTest verify
Tests run: 43, Failures: 7, Errors: 3

The 10 remaining cases are classified below. The fixed cases are paramMetaData, clobRS, blobRS, clearParameters, utf, and the stream half of set.

Full lanes:

./mvnw -pl libsql-jdbc test                            Tests run: 318, Failures: 0, Errors: 0
./mvnw -pl libsql-conformance -am verify               Tests run: 123, Failures: 0, Errors: 0, Skipped: 1
./mvnw -pl libsql-conformance -am -Pxerial-control verify  Tests run: 100, Failures: 0, Errors: 0, Skipped: 1
./mvnw test                                            BUILD SUCCESS
./mvnw spotless:check                                  BUILD SUCCESS
./mvnw -pl libsql-jdbc -am package -DskipTests         BUILD SUCCESS
tools/export-upstream --verify                         verified

Perf, three interleaved runs of ./mvnw -pl libsql-jdbc test: base median 2.46 s, head median 2.50 s (+1.6%).

Logs are in /tmp/xerial-parity-i157/.

Remaining failures and divergences

The control profile runs the same 43-case triage copy against xerial 3.53.4.0 and passes all 43, so each remaining case is behavior only the reference driver has.

  • date1 and date2 are a reference divergence, consistent with StatementTest exposes statement lifecycle and conversion gaps #158. xerial stores setDate as epoch milliseconds, so getLong and getDate round-trip the original millis. java.sql defines SQL DATE without a time, and the java.sql.Date(long) constructor normalizes the time away before it reaches the database. The driver keeps the date-only value.
  • getParameterTypeTest and getParameterTypeTest_when_no_parameter_set are a protocol gap. Hrana has no describe statement, so a parameter type cannot be derived before execution. The expected BIGINT/REAL names and the No parameter has been set yet message are xerial's local bind-type inference. The driver returns Types.OTHER rather than faking the type.
  • metaData, gh811_getMetadata_before_execution, and preparedStatementShouldNotThrowIfNotAllParamsSet are a protocol gap. getMetaData() before execution needs a describe that Hrana does not have, so the driver returns null until a result set exists.
  • noSuchTable and noSuchCol are a protocol gap. prepareStatement cannot validate SQL before execution for the same reason.
  • set now fails only at line 206, the reused result set after a later execute. java.sql closes the implicit result set when the statement executes again, so the vendored copy must reassign rs = prep.executeQuery(). The issue assigns that adjustment to the selection PR; this PR does not edit the vendored test.

The later selection PR records these lines and adjusts the set copy.

Triage method

PrepStmtTest is not in the vendored tree, so the run used a temporary copy under libsql-conformance/src/test/java/org/sqlite/, removed before the commit. The copy drops four cases that drive sqlite-jdbc's own classes or configuration: pragmaGetGeneratedKeys, updateWithoutGeneratedKeys, constraintErrorCodeExecute, and constraintErrorCodeExecuteUpdate. They use SQLiteConnection.getConnectionConfig(), the jdbc.get_generated_keys URL property, SQLiteException, and SQLiteErrorCode, none of which the libSQL driver exposes.

Files

libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java, libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java, libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTokenizer.java, libsql-hrana-codec/src/main/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodec.java, libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java, libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlStatementTest.java, libsql-hrana-codec/src/test/java/com/github/magnusp/libsql/hrana/codec/DirectHranaJsonCodecTest.java.

Base 36c7201062a6f1a3937713fd4b286db9c3f94da5, head f098fc4f8c49ef47ff5b644b3092f714bb6872ac.

Derive the parameter count from the SQL placeholders and answer ParameterMetaData with Types.OTHER where Hrana has no describe. Bind SQL NULL for unset and cleared parameters on both statement paths. Convert the blob, clob, binary, ascii, unicode, and character stream setters locally. Encode surrogate pairs as one UTF-8 sequence in the direct Hrana JSON writer.
@magnusp
magnusp merged commit 3de2ad5 into main Oct 23, 2026
6 checks passed
@magnusp
magnusp deleted the fix/i157-prepared-gaps branch October 23, 2026 03:27
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.

PrepStmtTest exposes prepared-statement binding and metadata gaps

1 participant