Skip to content

feat(jdbc): resolve JDBC types from the declared column type - #240

Closed
magnusp wants to merge 6 commits into
task/238-pr-0-oracle-suitefrom
task/238-pr-1-declared-types
Closed

magnusp wants to merge 6 commits into
task/238-pr-0-oracle-suitefrom
task/238-pr-1-declared-types

Conversation

@magnusp

@magnusp magnusp commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

JDBC 4.3 Appendix B keys ResultSet.getObject and the column metadata on the JDBC type declared for the column, not on the magnitude or storage class of the value read. The driver instead mapped each value at read time, so a BIGINT holding 42 came back as an Integer and a NUMERIC holding 42 came back as an Integer. PR-1 makes one resolved JdbcType per column the source of truth for getColumnType, getColumnClassName, and getObject(int).

What changed

  • Added JdbcTypeMapping with the JdbcType record (sqlType, javaClass, objectClass, storage, support), a registry for the 27 declared types in the plan, and a storage-class fallback for a missing or unknown declared type. A fallback is marked Support.FALLBACK.
  • Resolved every column once when the result set is built, so getObject and the metadata share one JdbcType per column. The first cut resolved per call and lost 15 times the throughput; the regression is closed and pinned by the perf lane.
  • Added a separate alias table for the SQLite spellings Appendix B does not name, including INT, INT8, DATETIME, TEXT, and DOUBLE PRECISION. Aliases resolve through the declared type, so existing metadata does not change when XerialColumnType is deleted.
  • LibsqlResultSet.getObject(int) returns the declared object type: BIGINT -> Long even for 42, INTEGER -> Integer with SQLException when the value does not fit, NUMERIC/DECIMAL -> BigDecimal, BIT/BOOLEAN -> Boolean, REAL -> Float, DATE/TIME/TIMESTAMP -> the java.sql temporal type, and CLOB/BLOB -> Clob/Blob.
  • getBoolean parses the text values true and false and keeps integer coercion for every other storage class.
  • Deleted XerialColumnType after moving its last callers, and moved its integer helper into JdbcTypeMapping.

Scope

PR-1 touches only libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ and the unit test files named in the plan plus the existing getBoolean test. It does not change setObject/setNull, add the PR-3 wrappers, or change the Appendix B oracle assertions. Expression columns with no decltype, such as SELECT 1 + 1, keep the storage-class fallback and still return an Integer.

Tradeoffs

  • NCLOB and DATALINK resolve to NClob and URL for metadata, but their getters arrive in PR-3. Until then getObject keeps the value's storage-class representation for those two declared types, which leaves their B-3 rows failing as assertions rather than throwing.
  • The alias table is deliberately outside the Appendix B registry, so the compliance claim stays exactly the 27 canonical names while the SQLite spellings keep their old metadata.
  • A declared INTEGER now rejects a value outside the int range instead of silently truncating it.

Blast Radius

  • Metadata type and class name now come from the declared type, so callers that relied on a BIGINT column reporting Integer for small values will see Long.
  • Untyped expression columns keep their previous storage-based mapping, including the magnitude rule for integers.
  • Declared SQLite aliases keep the metadata they had under XerialColumnType, so this PR does not change those callers.
  • Non-Appendix-B unit tests remain green. The remaining unit failures in the module are confined to the two Appendix B test classes.

Verification

All commands were run in /tmp/opencode/libsql-238/pr-1 at head d900f63.

Command Result
Jdbc4AppendixBReadTest#b3GetObjectReturnsDeclaredObjectType 34 tests, 7 failures, 0 errors. Remaining failures are ARRAY, STRUCT, REF (PR-5) and DATALINK, ROWID, NCLOB, SQLXML (PR-3). Down from 18.
Jdbc4AppendixBReadTest#b1NaturalGetterReturnsDeclaredJavaType 34 tests, 7 failures, 0 errors. Same seven wrapper types. Unchanged from baseline.
Jdbc4AppendixBSetObjectTest 33 tests, 31 failures, 0 errors. Unchanged; PR-2 owns this table.
./mvnw -q -pl libsql-jdbc test 1,487 tests, 507 failures, 0 errors. Every failure is in an Appendix B test class; all other tests pass.
./mvnw -q spotless:check Passed.

Perf on a 1,000,000-row result set with INTEGER, REAL, and VARCHAR, three interleaved rounds, mean of five measured runs after five warmups: trunk 63.4M, 64.2M, 64.4M rows/s, head 64.8M, 66.2M, 67.0M rows/s. Head is 2 to 4 percent faster and inside the 5 percent rule.

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.
@magnusp

magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Verdict: CLEAN at d900f63. Base 6a9e74a, branch task/238-pr-1-declared-types, parent PR #239.

Ten lanes PASS at the verdict head. Transcripts are under /tmp/swarm-pr-1-round2/.

Lane Check Result
1 Regression against trunk trunk BIGINT 42 returns Integer; head returns Long
2 BIGINT Long
3 NUMERIC, DECIMAL BigDecimal, BigDecimal
4 BIT, BOOLEAN Boolean, Boolean
5 DATE, TIME, TIMESTAMP java.sql.Date, Time, Timestamp
6 REAL Float
7 CLOB, BLOB Clob, Blob
8 Expression fallback SELECT 1 + 1 returns Integer
9 Out-of-range INTEGER SQLException: Value 5000000000 does not fit the declared INTEGER type
10 Metadata Types.BIGINT, java.lang.Long

Perf is PASS on the specified metric. Three interleaved rounds on 1,000,000 rows with INTEGER, REAL, and VARCHAR: trunk 63.4M, 64.2M, 64.4M rows/s; head 64.8M, 66.2M, 67.0M rows/s. The INTEGER-only diagnostic lane stays about 10 percent below trunk, which is the cost of the declared-type check and is inside the stated metric. It is recorded, not hidden.

The first fix round closed two defects. The declared type is now resolved once per column per result set instead of once per getObject call, which turned a 15x regression into a small gain. SQLite alias metadata was restored, and the alias lane matches trunk for DATETIME, INT8, and DOUBLE PRECISION.

Units at the verdict head are 1,487 tests, 507 failures, 0 errors. All failures are Appendix B rows owned by PR-2 through PR-5. Spotless is clean. CI is red only where Appendix B is red, by design. No review-bot comments to triage.

STACK-READY. PR-2 branches from this head.

@magnusp

magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Verdict correction, wording. "Matches trunk" in the alias lane applies to getColumnType and getColumnTypeName only. getObject intentionally changes the object class: DATETIME moves from String to Timestamp, and INT8 from Integer to Long. The alias test describes that change.

The INTEGER-only diagnostic lane varied between about 2 and 10 percent below trunk across runs. The plan's stated metric is the three-column lane, which passed at 2 to 4 percent faster. The note stands as a diagnostic, not a gate.

@magnusp
magnusp added this pull request to stack #246 October 25, 2026 09:09
magnusp added a commit that referenced this pull request Oct 25, 2026
## 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

- `clean verify` at `0861960`: 2,006 tests, 0 failures, 7 skipped, about
63 seconds.
- Appendix B unit 1,050 of 1,050, integration 148 of 148, conformance
270 of 270 with 7 skipped.
- Clean clone at the same SHA: unit 1,510 of 1,510, integration 148 of
148.
- Each link has a verdict and ten live lanes. The verdict comments are
on #239 through #245.
@magnusp

magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #247. The verified stack tip at 0861960 landed as one commit on main (e41cac1) because the main ruleset requires green required checks at every head, and the Appendix B oracle is red until the last link. The verdict and review record on this PR are unchanged.

@magnusp magnusp closed this Oct 25, 2026
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