From 7864433c187e3b95f2aba17bf303765e15c1f8fb Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:03:24 +0200 Subject: [PATCH] fix(jdbc): derive column type name from decltype (#76) Expression columns have no declared type, so getColumnTypeName returns the documented unknown value NUMERIC at every cursor position instead of sampling the first non-null row value. Remove sampleType and its row scan. Keep isSigned xerial-compatible for expression columns by reading the current row's storage class. --- .../libsql/jdbc/LibsqlResultSetMetaData.java | 26 +++++----- .../jdbc/LibsqlResultSetMetaDataTest.java | 51 ++++++++++++++++++- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java index f0d3114..e766dbf 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java @@ -120,8 +120,18 @@ public int isNullable(int column) throws SQLException { @Override public boolean isSigned(int column) throws SQLException { - String typeName = getColumnTypeName(column); - return "NUMERIC".equals(typeName) || "INTEGER".equals(typeName) || "REAL".equals(typeName); + if (declType(column) != null) { + return switch (getColumnTypeName(column)) { + case "NUMERIC", "INTEGER", "REAL" -> true; + default -> false; + }; + } + // An expression column has no declared type, so signedness follows the + // storage class of the current value, as xerial does. + return switch (storageType(runtimeValue(column))) { + case INTEGER, FLOAT, NULL -> true; + case TEXT, BLOB -> false; + }; } @Override @@ -286,13 +296,7 @@ public String getColumnTypeName(int column) throws SQLException { matcher.find(); return matcher.group(1).toUpperCase(Locale.ENGLISH); } - return switch (sampleType(column)) { - case INTEGER -> "INTEGER"; - case FLOAT -> "FLOAT"; - case BLOB -> "BLOB"; - case TEXT -> "TEXT"; - default -> "NUMERIC"; - }; + return "NUMERIC"; } @Override @@ -374,10 +378,6 @@ private Value sampleValue(int column) { return null; } - private SampleType sampleType(int column) { - return storageType(sampleValue(column)); - } - private static SampleType storageType(Value value) { if (value instanceof Value.Integer) { return SampleType.INTEGER; diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java index 2414888..bf0c0d4 100644 --- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java @@ -269,7 +269,7 @@ void matchesCastOnlyInLowerCase() throws SQLException { "SELECT CAST(1 AS BOOLEAN), cast(1 as BOOLEAN)", null); - assertThat(metadata.getColumnTypeName(1)).as("uppercase CAST").isEqualTo("INTEGER"); + assertThat(metadata.getColumnTypeName(1)).as("uppercase CAST").isEqualTo("NUMERIC"); assertThat(metadata.getColumnType(1)).as("uppercase CAST").isEqualTo(Types.INTEGER); assertThat(metadata.isSigned(1)).as("uppercase CAST").isTrue(); @@ -378,6 +378,55 @@ void reportsCurrentRowClassNameForMixedTypeColumn() throws SQLException { .isEqualTo(Object.class.getName()); } + @Test + void reportsUnknownTypeNameForExpressionColumnAtEveryCursorPosition() throws SQLException { + StmtResult result = new StmtResult( + List.of(new Col("x", null)), List.of(new Row(List.of(Value.of(1L)))), 0, null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertThat(metadata.getColumnTypeName(1)) + .as("before the first row") + .isEqualTo("NUMERIC"); + + assertThat(resultSet.next()).isTrue(); + assertThat(metadata.getColumnTypeName(1)).as("on a row").isEqualTo("NUMERIC"); + + assertThat(resultSet.next()).isFalse(); + assertThat(metadata.getColumnTypeName(1)) + .as("after the last row") + .isEqualTo("NUMERIC"); + } + + @Test + void reportsUnknownTypeNameForMixedTypeColumn() throws SQLException { + StmtResult result = new StmtResult( + List.of(new Col("x", null)), + List.of(new Row(List.of(Value.of(1L))), new Row(List.of(Value.of("a")))), + 0, + null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertThat(resultSet.next()).isTrue(); + assertThat(metadata.getColumnTypeName(1)) + .as("on the integer row") + .isEqualTo("NUMERIC"); + assertThat(metadata.isSigned(1)).as("on the integer row").isTrue(); + + assertThat(resultSet.next()).isTrue(); + assertThat(metadata.getColumnTypeName(1)) + .as("on the text row") + .isEqualTo("NUMERIC"); + assertThat(metadata.isSigned(1)).as("on the text row").isFalse(); + + assertThat(resultSet.next()).isFalse(); + assertThat(metadata.getColumnTypeName(1)) + .as("after the last row") + .isEqualTo("NUMERIC"); + assertThat(metadata.isSigned(1)).as("after the last row").isTrue(); + } + @Test void reportsClassNameForCurrentRowOutsideIntRange() throws SQLException { StmtResult result = new StmtResult(