Skip to content

refactor(jdbc): extract numeric text parsing and collapse numeric getters - #218

Merged
magnusp merged 5 commits into
mainfrom
refactor/i200-numeric-text
Oct 24, 2026
Merged

magnusp merged 5 commits into
mainfrom
refactor/i200-numeric-text

Conversation

@magnusp

@magnusp magnusp commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Extracts SQLite's text-to-number coercion from LibsqlResultSet into a package-private SqliteNumericText, then collapses the duplicated getInt/getFloat switch ladders onto the existing toLong/toDouble switches.

  • New SqliteNumericText holds parseLong(String), parseDouble(String), and blobAsText(byte[]) plus the private scanners skipSqliteWhitespace, isSqliteWhitespace, isAsciiDigit. The scanner bodies are byte-identical to the deleted ones; only parseSqliteLong/parseSqliteDouble were renamed to parseLong/parseDouble.
  • toLong(Value) and toDouble(Value) remain the single Value switches and now delegate the text and BLOB cases.
  • getInt is (int) toLong(getCurrentValue(columnIndex)); getFloat is (float) toDouble(getCurrentValue(columnIndex)).
  • No parser rule, message, saturation bound, date/time code, LOB class, or POM changed. A follow-up commit adds characterization tests for the saturation bound and all six SQLite whitespace characters.

LibsqlResultSet.java is now 912 lines (was 1029), under the 950-line acceptance bound.

Equivalence of the collapsed getters

The pre-change switch arms, the toLong/toDouble arms, and the new casts are compared arm by arm:

Value variant Pre-change getInt arm (int) toLong(v) arm Equal
Null 0 (int) 0L = 0 yes
Integer (int) i.value() (int) i.value() yes
Float (int) (long) f.value() (int) (long) f.value() yes — truncate to int64, then low-32-bit wrap
Text (int) parseSqliteLong(t.value()) (int) SqliteNumericText.parseLong(t.value()) yes — same body
Blob (int) parseSqliteLong(blobAsText(b)) (int) SqliteNumericText.parseLong(SqliteNumericText.blobAsText(b.bytes())) yes — same body and UTF-8 decode
Value variant Pre-change getFloat arm (float) toDouble(v) arm Equal
Null 0.0F (float) 0.0 = 0.0F yes
Integer (float) (double) i.value() (float) (double) i.value() yes — rounds through double, not direct long→float
Float (float) f.value() (float) f.value() yes
Text (float) parseSqliteDouble(t.value()) (float) SqliteNumericText.parseDouble(t.value()) yes — same body
Blob (float) parseSqliteDouble(blobAsText(b)) (float) SqliteNumericText.parseDouble(SqliteNumericText.blobAsText(b.bytes())) yes

A scratch differential probe (not committed) ran both ladders over the five variants: Null; boundary and wrapping Integer values (Long.MIN/MAX_VALUE, Integer.MIN/MAX_VALUE, 1L << 32, 3_000_000_000L); Float values including NaN, both infinities, -0.0, 3.9e9, Long.MAX_VALUE as double, and Math.nextDown(1.0f); 30 text and 30 BLOB inputs covering empty, whitespace-only, sign-only, numeric prefixes, trailing garbage, 3.5e / 3.5e+ (no exponent digit), 3.5e-2x, .5, and int64 overflow both ways; plus 40,000 random longs and random-bits doubles.

Result: 60,092 values, 0 mismatches (int compared by value, float by raw bits).

getBoolean still routes through getInt, so REAL 0.5 stays false, and LibsqlResultSetTest / LibsqlResultSetGetBooleanTest pass unedited.

Follow-up: characterization tests

Commit 373b875 adds caller-observable tests to LibsqlResultSetTest without touching production code.

Mutant Test that kills it Observed failure
Halve the saturation bound in parseLong largeIntegerTextBelowLongMaxIsReturnedUnchanged expected: 6000000000000000000L / but was: 9223372036854775807L
Drop '\t' from the whitespace set everySqliteWhitespaceCharacterIsSkippedBeforeTheNumericPrefix(String)[2] (the tab invocation only) expected: 42 / but was: 0

New coverage:

  • Large valid integer 6000000000000000000 (between Long.MAX_VALUE / 2 and Long.MAX_VALUE) read through getLong and getBigDecimal, unchanged.
  • Text just past Long.MAX_VALUE saturates to Long.MAX_VALUE; text just past Long.MIN_VALUE saturates to Long.MIN_VALUE.
  • Space, tab, newline, carriage return, vertical tab, and form feed each skipped before a numeric prefix, as one parameterized test.

LibsqlResultSetTest is 45 tests (was 36): 4 new methods, one parameterized with 6 invocations. The JDBC module is 392 tests (was 383).

Diff stat

 .../magnusp/libsql/jdbc/LibsqlResultSet.java       | 133 ++-------------------
 .../magnusp/libsql/jdbc/SqliteNumericText.java     | 121 +++++++++++++++++++
 .../magnusp/libsql/jdbc/LibsqlResultSetTest.java   |  36 ++++++
 3 files changed, 165 insertions(+), 125 deletions(-)

Lane results

Lane Result
./mvnw -pl libsql-jdbc -am test green — 392 tests, 0 failures, 0 errors, 0 skipped
./mvnw spotless:apply then ./mvnw spotless:check green — BUILD SUCCESS, no reformat needed
./mvnw -pl libsql-conformance -am verify green — 272 tests, 0 failures, 0 errors, 7 skipped

Fixes #200

Characterization tests for the four unpinned behaviors in the JDBC
refactor Wave 0:

- attached-schema table metadata through pragma_database_list
- date/time text tails (offset, Z, bare time) and BLOB rejection
- duplicate savepoint names and releasing the outermost savepoint of
  an auto-commit-started transaction
- executeBatch racing statement close

Tests only; no production code, pom or dependency change.
Move the two read-only Blob and Clob implementations out of
LibsqlResultSet into package-private top-level classes. Constructors
become package-private so getBlob and getClob can still build them.
Pure move: messages, checks, stream encodings, and free() semantics
are unchanged. Also drops the four imports that only the moved code
used (OutputStream, Writer, StandardCharsets, Arrays).
@magnusp

magnusp commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Root verdict: PASS at 373b875.

The extraction moved all five parser methods verbatim and both getter collapses are arm-for-arm equal, including REAL to int through int64 and integer to float through double. Independent checks found the moved parsers had no saturation test and covered one of six whitespace characters, so a fix-forward added both on this branch. 392 unit tests and 272 conformance tests green, and four mutants now die. Countersign: root, 373b875.

Base automatically changed from refactor/i199-lob-classes to main October 24, 2026 14:39
@magnusp
magnusp merged commit 672f6f0 into main Oct 24, 2026
6 checks passed
@magnusp
magnusp deleted the refactor/i200-numeric-text branch October 24, 2026 14:40
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.

Extract SqliteNumericText and collapse the duplicate numeric getters

1 participant