Skip to content

feat(jdbc): comply with JDBC 4.3 Appendix B type mapping - #247

Merged
magnusp merged 29 commits into
mainfrom
task/238-stack-integration
Oct 25, 2026
Merged

magnusp merged 29 commits into
mainfrom
task/238-stack-integration

Conversation

@magnusp

@magnusp magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Why

The driver mapped result values by storage class, so the declared type did not control metadata or getObject, and setObject ignored the target type. JDBC 4.3 Appendix B is the oracle. Seven stacked pull requests implement it. The stack cannot land link by link because the main ruleset requires the full build green at every head, and the Appendix B oracle is red until the last link. This PR lands the verified tip as one commit.

What changed

  • Add the Appendix B oracle suite and the vendor table, then resolve one JDBC type per declared column for metadata and getObject.
  • Honor the setObject target type, add the missing getters and LOB wrappers, and support the offset temporal types.
  • Mark ARRAY, STRUCT, REF, JAVA_OBJECT, and DISTINCT N/A and reject unlisted Table B-6 conversions.
  • Publish the compliance profile, quarantine the four xerial tests that conflict with the spec, and record the policy.

Scope

The seven stacked PRs are #239, #240, #241, #242, #243, #244, and #245. No unrelated changes. vendor/JDBC4-TYPE-MAPPING.md is the oracle.

Tradeoffs

  • The five unrepresentable types throw instead of emulating them.
  • The conformance quarantine is method-level. The lost checks are named in libsql-conformance/README.md.
  • An expression column with no decltype keeps the storage fallback, documented as a profile exception.

Blast Radius

Metadata and getObject now follow the declared type, so a BIGINT holding 42 returns Long, and an unlisted getter conversion throws. The default build was red from #239 through #245 by design and is green at this tip.

Verification

The suite encodes Tables B-1 through B-6 from the JDBC 4.3 specification. It fails on the current driver by design and becomes the evidence for the compliance stack.
getObject resolved the declared type on every call: it re-parsed the
Col decltype with a regular expression and hit the registry map for each
row. A million-row scan of INTEGER, REAL and VARCHAR columns fell from
about 64M rows/s to 4.3M rows/s, and an INTEGER-only scan from about
105M to 21M.

Resolve all columns once when the result set is built and read the
cached JdbcType by index. getObject now converts from the Value it has
already read instead of fetching the column again, and the metadata
reads the same cache. An expression column keeps a null entry and falls
back to the storage class of the value, which is a class test and a
switch rather than another DeclaredType parse.
The getters resolved the current row again on every call: row index
branch, rows.size(), rows.get and Row.values. A result set now keeps the
value list of the row the getters read, so next() swaps it once per row
and getCurrentValue is a single list read.

This pays back the per-column lookup the declared-type resolution added
to getObject: the three-column scan runs at trunk speed while the
declared type is still read from the per-result-set cache. The documented
quirks are kept: before the first next() the getters read the first row,
and after the last row the list is null so they return SQL NULL.
Removing the xerial column mapping dropped the SQLite spellings that
Appendix B does not name: DATETIME, INT, INT2, INT8, MEDIUMINT, UNSIGNED
BIG INT, DOUBLE PRECISION, TEXT, CHARACTER, NATIVE CHARACTER and VARYING
CHARACTER all became unknown and fell back to the storage class. Users
saw DATETIME report VARCHAR instead of TIMESTAMP, INT8 report INTEGER
instead of BIGINT, and DOUBLE PRECISION report FLOAT instead of DOUBLE.

Keep the Appendix B registry at its 27 canonical names and add an alias
table that points each SQLite spelling at the registry entry it stands
for. Aliases resolve through the declared type, so INT8 stays BIGINT for
a small value, and they share the canonical JdbcType instance, so
getObject and the metadata agree. A test pins every alias.

TEXT now resolves to VARCHAR, so the empty-result-set class name for a
TEXT column is String rather than Object.
The converter turns a Java value into the Hrana value for a target JDBC type per Appendix B Table B-5, and follows Table B-4 when no target is given. Unlisted pairs throw SQLFeatureNotSupportedException so the target is never ignored.
setObject(x, targetSqlType) now converts to the target type before binding, instead of ignoring the target and delegating to setObject(x).
Each Table B-4 and B-5 family asserts the Hrana value kind and the Boolean text format without a server.
Each wrapper holds the materialized value of one result column and
rejects every read once freed. NClob reuses the Clob text holder because
SQLite stores national and regular text identically; SQLXML and RowId are
read-only views over the same text and bytes.
getURL parses DATALINK text and throws on text that is not a URL.
getNClob, getSQLXML, and getRowId wrap the stored text or bytes, and
getObject routes NCLOB, SQLXML, ROWID, and DATALINK to the same wrappers.
getAsciiStream reads text as US-ASCII and getNCharacterStream delegates to
getNString.

SQLXML and ROWID join the declared-type registry so the metadata and
getObject agree on their object type.
setSQLXML binds the SQLXML content as text. setRowId binds the bytes as a
BLOB, which is the only representation getRowId reads back unchanged.
setArray and setRef still throw.
B-1 and B-3 now check the URL, NClob, SQLXML, RowId, ASCII, and NCHAR
getters end to end. A freed wrapper must reject every further read.
Map TIME_WITH_TIMEZONE to OffsetTime and TIMESTAMP_WITH_TIMEZONE to OffsetDateTime, parse ISO-8601 offset text, and reject unlisted getters on the new declared types.
Assert Z and +01:00 parse to OffsetTime and OffsetDateTime, malformed offset text raises SQLException, and declared offset columns return the offset classes.
Add the missing B-6 getObject cells for TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE to the server suite.
The RowId and SQLXML stubs returned null bytes and null text, so the B-2 rows bound null. Return the rowid-1 bytes and the <a/> text. Read the NClob wrapper directly so the B-5 row proves the wrapper round trip.
Four selected xerial methods assert behavior that Appendix B replaces. Exclude them from the libSQL run without editing the vendored tree. The xerial-control profile still runs them.
Give every Appendix B row a status, a reason, and the test that proves it. Record the expression fallback as an exception and the policy in AGENTS.md. Add the verification commands to README.md.
The B-5 reader returned the NClob wrapper while the expectation checked only
the type, so the stored text was no longer verified. Read the wrapper, assert
its type, and compare getSubString(1, length) with the source text in B-2,
B-4, and B-5.
The quarantine table names the conflicting assertion for each method but not
the non-conflicting checks the method-level exclusion also removes. List those
checks per method and correct the count note: 270 excludes the four
quarantines.
The profile test only exercises getObject, getArray, and getRef on declared
columns, so it cannot prove the B-2, B-4, or B-5 setter rows. Cite the
integration rows and the setObject rejection test instead, and note that the
timezone types belong to B-5 and B-6 rather than B-1 or B-3.
@magnusp
magnusp merged commit e41cac1 into main Oct 25, 2026
11 checks passed
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.

1 participant