Repository navigation
feat(jdbc): comply with JDBC 4.3 Appendix B type mapping - #247
Merged
Merged
Conversation
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.
This was referenced Oct 25, 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
The driver mapped result values by storage class, so the declared type did not control metadata or
getObject, andsetObjectignored 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 themainruleset 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
getObject.setObjecttarget type, add the missing getters and LOB wrappers, and support the offset temporal types.ARRAY,STRUCT,REF,JAVA_OBJECT, andDISTINCTN/A and reject unlisted Table B-6 conversions.Scope
The seven stacked PRs are #239, #240, #241, #242, #243, #244, and #245. No unrelated changes.
vendor/JDBC4-TYPE-MAPPING.mdis the oracle.Tradeoffs
libsql-conformance/README.md.decltypekeeps the storage fallback, documented as a profile exception.Blast Radius
Metadata and
getObjectnow follow the declared type, so aBIGINTholding 42 returnsLong, and an unlisted getter conversion throws. The default build was red from #239 through #245 by design and is green at this tip.Verification
clean verifyat0861960: 2,006 tests, 0 failures, 7 skipped, about 63 seconds.