Skip to content

feat(jdbc): convert setObject values to the target JDBC type - #241

Closed
magnusp wants to merge 3 commits into
task/238-pr-1-declared-typesfrom
task/238-pr-2-target-types
Closed

magnusp wants to merge 3 commits into
task/238-pr-1-declared-typesfrom
task/238-pr-2-target-types

Conversation

@magnusp

@magnusp magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Why

setObject(x, targetSqlType) ignored the target type and delegated to setObject(x). A caller who asked for Types.INTEGER could store text, and the JDBC 4.3 Appendix B Table B-5 negative suite could not pass. PR-2 makes the target type part of the binding contract.

What changed

  • Added JdbcValueConverter with toValue(Object) for the no-target Table B-4 path and toValue(Object, int) for the Table B-5 target path. The converter is a pure function of the value and the target type.
  • setObject(x, targetSqlType) and the four-argument overload now convert to the target type first, then to a Hrana Value. The four-argument overload still ignores scaleOrLength.
  • Unlisted Table B-5 pairs throw SQLFeatureNotSupportedException.
  • setNull still accepts every target type.
  • Added JdbcTypeMapping.sqlTypeName(int) so rejection messages name the JDBC type.
  • Added one unit test per target family that asserts the bound Hrana value kind without a server.

Numeric targets use Java narrowing: TINYINT to byte, SMALLINT to short, INTEGER to int, BIGINT to long, REAL to float, FLOAT and DOUBLE to double, DECIMAL and NUMERIC to BigDecimal. BIT and BOOLEAN bind integer 0 or 1. BigInteger binds an integer, and boxed Byte and Short bind integers. Clob content is read through getCharacterStream and Blob content through getBinaryStream.

Scope

  • libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcValueConverter.java (new)
  • libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java
  • libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/JdbcTypeMapping.java
  • libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatementTest.java

ARRAY, STRUCT, REF, JAVA_OBJECT, and DISTINCT still bind toString until PR-5, so their positive integration rows stay red on purpose. No read path changes.

Tradeoffs

  • The Boolean text format is true / false, not TRUE / FALSE, so a text target round trips through getBoolean. Recorded here and in the converter Javadoc.
  • DECIMAL and NUMERIC bind decimal text. Hrana has no decimal type, and this matches the existing setBigDecimal binding.
  • A String bound to DATE, TIME, or TIMESTAMP passes through unchanged. That keeps the Table B-5 acceptance contract for any string and leaves malformed text to the getter.
  • LocalDate and LocalTime are rejected as TIMESTAMP targets because Table B-5 does not list those pairs. The plan's prose bullet lists all seven temporal sources for all three temporal targets; the oracle table wins.
  • Calendar and java.util.Date to ARRAY is accepted as the table lists it, but still binds toString until PR-5.
  • TIME to TIMESTAMP uses the epoch date because the date part is implementation defined.

Blast Radius

Only the setObject binding path changes. setBoolean, setInt, setBigDecimal, the date setters, and the stream setters keep their existing bindings. The no-target path changes deliberately: boxed Byte, Short, and BigInteger now bind integers; Calendar, java.util.Date, and LocalDateTime bind SQLite timestamp text; Time and Timestamp use the same canonical SQLite text as the target path. No result-set or metadata behavior is touched.

Verification

  1. ./mvnw -q -pl libsql-jdbc -Dtest='Jdbc4AppendixBSetObjectTest' -Dsurefire.failIfNoSpecifiedTests=false test — 33 tests, 0 failures, 0 errors (was 33 tests, 31 failures).
  2. ./mvnw -q -pl libsql-jdbc -Dtest='LibsqlPreparedStatementTest' test — 39 tests, 0 failures, 0 errors.
  3. ./mvnw -q -pl libsql-jdbc test — 1,497 tests, 476 failures, 0 errors. Every failure is in Jdbc4AppendixBReadTest: B-1 7, B-3 7, B-6 462 (461 b6GetterHonoursSupportedJdbcTypes + 1 b6NumericGettersRejectNonNumericText). B-1, B-3, and B-6 are unchanged; there are no non-Appendix-B failures.
  4. ./mvnw -q spotless:check — clean.

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

magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Verdict: CLEAN at 0ea436c. Base d900f63, branch task/238-pr-2-target-types, parent PR #240.

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

Lane Check Result
1 Regression against trunk trunk stores text 42 and accepts ARRAY; head stores integer 42 and rejects ARRAY
2 String to TINYINT stored integer, getInt 42
3 BigInteger to BIGINT stored integer 7, getLong 7
4 Boolean to CHAR stored text true, getString true
5 Clob to CLOB stored text abc
6 Blob to BLOB stored blob abc
7 Calendar to TIMESTAMP 2024-01-02 03:04:05, getTimestamp epoch 1704164645000
8 java.util.Date to TIMESTAMP same instant as lane 7
9 Unlisted target SQLFeatureNotSupportedException: Cannot convert java.lang.String to ARRAY
10 setNull with target stored null, read-back null

Units are 1,497 tests, 476 failures, 0 errors. Every failure is an Appendix B read row owned by PR-3 or PR-5. The Appendix B setObject test is 33 of 33 green. Spotless is clean. The integration suite also improved from 63 to 45 failures at this head; the remaining rows are the wrapper and unsupported types owned by PR-3 and PR-5.

Perf is PASS on the end-to-end rule. A 1,000-row insert loop across numeric, text, and temporal values runs at about 940 rows/s at trunk and 920 rows/s at head, inside 5 percent. The 100,000-row insert run named in the plan is not runnable on this setup. The server sync path caps inserts near 940 rows/s and the client's 10 second request timeout kills large batches, so 100,000 rows would take about two minutes per run. The conversion cost is measured instead with a 100,000-bind lane: trunk 16.0M binds/s, head 11.4M binds/s. That is a 29 percent conversion cost, dominated by BigDecimal in the numeric narrowing path, and it is recorded here rather than hidden. It does not breach the stated end-to-end rule, and it allocates one Hrana value per bind.

CI is red only where Appendix B is red, by design. No review-bot comments to triage.

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

@magnusp

magnusp commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Verdict correction, numbers. The 1,000-row insert means were about 923 rows/s at trunk and 906 at head, not 940 and 920. The 29 percent bind cost came from the runs 5 to 9 selection in the cited artifact; the runs vary between about 29 and 36 percent. "Dominated by BigDecimal" is an inference from the code path, not a measurement. The end-to-end conclusion is unchanged and inside 5 percent.

Lane 2 in the original run used target INTEGER and getInt, not TINYINT. A rerun at 0ea436c with target TINYINT stores an integer and getByte returns 42. The transcript is /tmp/swarm-pr-2-round2/worker-2/lane-2-tinyint.txt.

@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