Skip to content

fix(jdbc): match xerial result set navigation boundaries - #72

Merged
magnusp merged 1 commit into
mainfrom
fix/45-resultset-navigation
Oct 21, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/45-resultset-navigation

Conversation

@magnusp

@magnusp magnusp commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Closes #45. Part of #54.

What changed

  • LibsqlResultSet.findColumn now returns the first match for duplicate labels (case-insensitive), matching xerial, and reports unknown labels as no such column: '<label>'.
  • Column getters before next() read the first row instead of throwing.
  • Column getters after the last row return SQL NULL: strings, objects, bytes, decimals, and dates are null, numeric getters return 0, and getBoolean returns false. Typed numeric getObject(Class) still throws Bad value for type X, as xerial does.
  • getRow() returns 0 before the first row, the 1-based current row while positioned, and the last row number after the end, including after repeated next() calls.
  • isLast() now always throws SQLFeatureNotSupportedException("not supported by sqlite").
  • wasNull() now tracks the last column read and the current cursor position, including xerial's boundary exceptions (see the table below).
  • Added class-level Javadoc that documents the navigation contract.
  • Added LibsqlResultSetNavigationTest covering duplicate labels and every cursor boundary in the acceptance criteria.

Contract decision

Parity with xerial 3.53.4.0 at the pinned tag, as required by #54. Every claim was re-probed against the published org.xerial:sqlite-jdbc:3.53.4.0 jar (and cross-checked with the vendored tag sources) before pinning tests.

Method Chosen target (xerial 3.53.4.0)
findColumn First duplicate label wins; case-insensitive; unknown label throws SQLException("no such column: '<label>'").
Getters before next() Read the first row (xerial's synthetic row before next()).
Getters after the end SQL NULL: null for text/object/bytes/decimal/date getters, 0 for numeric getters, false for getBoolean; typed numeric getObject(Class) throws Bad value for type X.
getRow 0 before the first row, 1-based current row while positioned, and the last row number after the end (stays there on repeated next() returns of false).
isLast Always throws SQLFeatureNotSupportedException("not supported by sqlite").
wasNull Reflects the last column read at the current cursor position. Throws SQLException while no column has been read in the current row: column 0 out of bounds [1,N] before the first next(), and column -1 out of bounds [1,N] immediately after transitioning past the last row. After a getter at the past-end position it returns true, because the pointer reports SQL NULL.

Probe corrections to the issue body: wasNull before any getter is not false; it throws. Only getRow and getInt had been re-verified in #28, so the duplicate-label, isLast, and wasNull claims were probed again here.

Evidence

Failing before the fix, on the current code (./mvnw -pl libsql-jdbc -am -Dtest=LibsqlResultSetNavigationTest -Dsurefire.failIfNoSpecifiedTests=false test):

[ERROR] Tests run: 10, Failures: 5, Errors: 4, Skipped: 0
[ERROR] LibsqlResultSetNavigationTest.findColumnReturnsTheFirstMatchingDuplicateLabel:40 expected: 1 but was: 2
[ERROR] LibsqlResultSetNavigationTest.getRowIsZeroBeforeTheFirstRowAndStaysOnTheLastRowAfterTheEnd:94 expected: 2 but was: 0
[ERROR] LibsqlResultSetNavigationTest.isLastAlwaysThrowsLikeXerial:112 Expecting code to raise a throwable.
[ERROR] LibsqlResultSetNavigationTest.wasNullThrowsBeforeAnyGetterAndAfterTheEndWithoutAGetter:124 Expecting code to raise a throwable.
[ERROR] LibsqlResultSetNavigationTest.gettersReadTheFirstRowBeforeNext:57 » SQL Cursor is not pointing to a valid row
[ERROR] LibsqlResultSetNavigationTest.gettersReturnNullAndZeroAfterTheLastRow:68 » SQL Cursor is not pointing to a valid row
[ERROR] LibsqlResultSetNavigationTest.gettersReturnNullAndZeroWhenTheResultSetHasNoRows:79 » SQL Cursor is not pointing to a valid row
[ERROR] LibsqlResultSetNavigationTest.wasNullTracksTheLastGetterRead:140 » SQL Cursor is not pointing to a valid row

Passing after the fix:

$ ./mvnw -pl libsql-jdbc -am test
[INFO] Tests run: 132, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
$ ./mvnw -pl libsql-conformance -am verify
[INFO] Tests run: 115, Failures: 0, Errors: 0, Skipped: 1
[INFO] BUILD SUCCESS
$ ./mvnw spotless:apply
$ ./mvnw spotless:check
BUILD SUCCESS

The conformance run included the selected xerial ResultSetTest (21 tests) against sqld.

Deviations

None. The change is pure parity with xerial 3.53.4.0; the Hrana transport does not prevent any of these semantics.

Out-of-scope defect noticed

isFirst() still differs from xerial after the end of a single-row result. xerial returns true there because its internal row counter stays at the last row; this implementation returns false. That method is not one of the six navigation methods in this issue's contract and was left unchanged. Likewise isBeforeFirst/isAfterLast boundary semantics were left as-is; they already matched the probed behavior for the cases covered.

@magnusp
magnusp merged commit cae0428 into main Oct 21, 2026
5 checks passed
@magnusp
magnusp deleted the fix/45-resultset-navigation branch October 21, 2026 23:11
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.

ResultSet navigation differs on duplicate labels and cursor state

1 participant