From 23e97a6617730b5a7f447c3b0535a6a14cff73b1 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sat, 31 Oct 2026 00:01:44 +0100 Subject: [PATCH] fix(jdbc): route label getters through their int overloads (#250) ## Why `getArray(String)` and `getRef(String)` threw `SQLFeatureNotSupportedException` before any check. Every other label getter delegates through `findColumn`, so a closed result set, a declared N/A column, and an unknown label all reported the wrong error on these two methods. ## What changed - `LibsqlResultSet#getRef(String)` and `getArray(String)` now call `findColumn` and delegate to the int overload, matching `getBlob`, `getClob` and `getRowId`. - Added tests for the closed result set, the five N/A declared types, an unknown label, and an ordinary open column on both overloads. ## Blast radius Only error precedence changes on the two label overloads. An unknown label now raises `no such column`; a closed result set and a declared N/A column now raise the same message as the int overload. No supported read changes. ## Verification - `./mvnw -pl libsql-jdbc -Dtest=LibsqlResultSetTest test`: 54 tests, 0 failures. - `./mvnw clean verify`: 1555 unit tests, 192 itests, 271 conformance tests with 7 skipped, 0 failures. - `./mvnw spotless:check` passes. Fixes #250 --- .../magnusp/libsql/jdbc/LibsqlResultSet.java | 4 +- .../libsql/jdbc/LibsqlResultSetTest.java | 90 ++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java index 0469ac1..67edf47 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -889,10 +889,10 @@ private static Clob clobValue(Value value) { rejectTypeMap(map); return getObject(columnLabel); } - @Override public Ref getRef(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } + @Override public Ref getRef(String columnLabel) throws SQLException { return getRef(findColumn(columnLabel)); } @Override public Blob getBlob(String columnLabel) throws SQLException { return getBlob(findColumn(columnLabel)); } @Override public Clob getClob(String columnLabel) throws SQLException { return getClob(findColumn(columnLabel)); } - @Override public Array getArray(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); } + @Override public Array getArray(String columnLabel) throws SQLException { return getArray(findColumn(columnLabel)); } @Override public Date getDate(int columnIndex, Calendar cal) throws SQLException { return getDate(columnIndex); } @Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { return getDate(columnLabel); } @Override public Time getTime(int columnIndex, Calendar cal) throws SQLException { return getTime(columnIndex); } diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java index a9509ba..03521e4 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java @@ -447,6 +447,85 @@ void getObjectWithMapRejectsNonEmptyMap() throws SQLException { .hasMessageContaining("Type maps"); } + @ParameterizedTest(name = "{0} closed on {1}") + @ValueSource(strings = {"getRef", "getArray"}) + void labelAndIntGettersShareTheClosedResultSetCheck(String getter) throws SQLException { + LibsqlResultSet rs = resultSet(new Value.Integer(1)); + assertThat(rs.next()).isTrue(); + rs.close(); + + assertThatThrownBy(() -> readArrayOrRef(rs, getter, 1)) + .as("%s(int) on a closed result set", getter) + .isInstanceOf(SQLException.class) + .hasMessage("ResultSet is closed"); + assertThatThrownBy(() -> readArrayOrRef(rs, getter, "c1")) + .as("%s(String) on a closed result set", getter) + .isInstanceOf(SQLException.class) + .hasMessage("ResultSet is closed"); + } + + @ParameterizedTest(name = "{0} declared {1}") + @ValueSource(strings = {"ARRAY", "STRUCT", "REF", "DISTINCT", "JAVA_OBJECT"}) + void labelAndIntGettersShareTheDeclaredTypeCheck(String declaredType) throws SQLException { + LibsqlResultSet rs = declaredResultSet(declaredType, Value.of("42")); + assertThat(rs.next()).isTrue(); + + assertThatThrownBy(() -> rs.getArray(1)) + .as("getArray(int) on declared %s", declaredType) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Declared " + declaredType + " is not supported"); + assertThatThrownBy(() -> rs.getArray("c1")) + .as("getArray(String) on declared %s", declaredType) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Declared " + declaredType + " is not supported"); + assertThatThrownBy(() -> rs.getRef(1)) + .as("getRef(int) on declared %s", declaredType) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Declared " + declaredType + " is not supported"); + assertThatThrownBy(() -> rs.getRef("c1")) + .as("getRef(String) on declared %s", declaredType) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Declared " + declaredType + " is not supported"); + } + + @Test + void labelGettersRejectAnUnknownLabelBeforeTheUnsupportedThrow() throws SQLException { + LibsqlResultSet rs = resultSet(new Value.Integer(1)); + assertThat(rs.next()).isTrue(); + + assertThatThrownBy(() -> rs.getArray("missing")) + .isInstanceOf(SQLException.class) + .isNotInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("no such column: 'missing'"); + assertThatThrownBy(() -> rs.getRef("missing")) + .isInstanceOf(SQLException.class) + .isNotInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("no such column: 'missing'"); + } + + @Test + void labelAndIntGettersStillRejectAnOrdinaryOpenColumn() throws SQLException { + LibsqlResultSet rs = resultSet(new Value.Integer(1)); + assertThat(rs.next()).isTrue(); + + assertThatThrownBy(() -> rs.getArray(1)) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> rs.getArray("c1")) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> rs.getRef(1)) + .isInstanceOf(SQLFeatureNotSupportedException.class); + assertThatThrownBy(() -> rs.getRef("c1")) + .isInstanceOf(SQLFeatureNotSupportedException.class); + } + + private static Object readArrayOrRef(LibsqlResultSet rs, String getter, Object column) + throws SQLException { + if ("getArray".equals(getter)) { + return column instanceof Integer ? rs.getArray((Integer) column) : rs.getArray((String) column); + } + return column instanceof Integer ? rs.getRef((Integer) column) : rs.getRef((String) column); + } + @Test void findColumnRejectsNullLabel() { LibsqlResultSet rs = resultSet(new Value.Integer(1)); @@ -612,8 +691,17 @@ private static void assertNumeric(LibsqlResultSet rs, int column, int expectedIn } private static LibsqlResultSet resultSet(Value... values) { + String[] declaredTypes = new String[values.length]; + return declaredResultSet(declaredTypes, values); + } + + private static LibsqlResultSet declaredResultSet(String declaredType, Value value) { + return declaredResultSet(new String[] {declaredType}, value); + } + + private static LibsqlResultSet declaredResultSet(String[] declaredTypes, Value... values) { List cols = IntStream.range(0, values.length) - .mapToObj(i -> new Col("c" + (i + 1), null)) + .mapToObj(i -> new Col("c" + (i + 1), declaredTypes[i])) .toList(); StmtResult result = new StmtResult(cols, List.of(new Row(List.of(values))), 1, null); return new LibsqlResultSet(null, result);