From 319210bf523f0336446f2a40adcf96416792f6f4 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:08:16 +0200 Subject: [PATCH] fix(jdbc): copy xerial metadata flags and type-name parsing (#46) --- .../libsql/jdbc/LibsqlResultSetMetaData.java | 57 ++++--------- .../jdbc/LibsqlResultSetMetaDataTest.java | 82 +++++++++++++++++++ 2 files changed, 97 insertions(+), 42 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 e34034f..bd0f6a7 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 @@ -19,8 +19,9 @@ public class LibsqlResultSetMetaData implements ResultSetMetaData { + private static final Pattern COLUMN_TYPENAME = Pattern.compile("([^\\(]*)"); private static final Pattern COLUMN_TYPECAST = - Pattern.compile("(?i)cast\\(.*?\\s+as\\s+(.*?)\\s*\\)"); + Pattern.compile("cast\\(.*?\\s+as\\s+(.*?)\\s*\\)"); private static final Pattern COLUMN_PRECISION = Pattern.compile(".*?\\((.*?)\\)"); private final List cols; @@ -119,18 +120,8 @@ public int isNullable(int column) throws SQLException { @Override public boolean isSigned(int column) throws SQLException { - return switch (getColumnType(column)) { - case Types.TINYINT, - Types.SMALLINT, - Types.INTEGER, - Types.BIGINT, - Types.NUMERIC, - Types.DECIMAL, - Types.REAL, - Types.FLOAT, - Types.DOUBLE -> true; - default -> false; - }; + String typeName = getColumnTypeName(column); + return "NUMERIC".equals(typeName) || "INTEGER".equals(typeName) || "REAL".equals(typeName); } @Override @@ -163,14 +154,7 @@ public int getPrecision(int column) throws SQLException { return 0; } Matcher matcher = COLUMN_PRECISION.matcher(declType); - if (!matcher.find()) { - return 0; - } - try { - return Integer.parseInt(matcher.group(1).split(",")[0].trim()); - } catch (NumberFormatException e) { - return 0; - } + return matcher.find() ? Integer.parseInt(matcher.group(1).split(",")[0].trim()) : 0; } @Override @@ -183,11 +167,7 @@ public int getScale(int column) throws SQLException { if (matcher.find()) { String[] parts = matcher.group(1).split(","); if (parts.length == 2) { - try { - return Integer.parseInt(parts[1].trim()); - } catch (NumberFormatException e) { - return 0; - } + return Integer.parseInt(parts[1].trim()); } } return 0; @@ -302,13 +282,9 @@ public String getColumnTypeName(int column) throws SQLException { getCol(column); String declType = declType(column); if (declType != null) { - int paren = declType.indexOf('('); - String name = (paren >= 0 ? declType.substring(0, paren) : declType) - .trim() - .toUpperCase(Locale.ENGLISH); - if (!name.isEmpty()) { - return name; - } + Matcher matcher = COLUMN_TYPENAME.matcher(declType); + matcher.find(); + return matcher.group(1).toUpperCase(Locale.ENGLISH); } return switch (sampleType(column)) { case INTEGER -> "INTEGER"; @@ -362,17 +338,14 @@ private static String className(Value value) { private String declType(int column) throws SQLException { Col col = getCol(column); String declType = col.decltype(); - if (declType != null && !declType.isBlank()) { - return declType; - } - String name = col.name(); - if (name != null) { - Matcher matcher = COLUMN_TYPECAST.matcher(name); - if (matcher.find()) { - return matcher.group(1); + if (declType == null) { + String name = col.name(); + if (name != null) { + Matcher matcher = COLUMN_TYPECAST.matcher(name); + declType = matcher.find() ? matcher.group(1) : null; } } - return null; + return declType; } private String columnName(int column) throws SQLException { 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 ed86094..b620833 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 @@ -17,6 +17,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class LibsqlResultSetMetaDataTest { @@ -210,6 +211,87 @@ void usesDeclaredTypesForEmptyResultSet() throws SQLException { assertDeclaredTypes(metadata); } + @Test + void isSignedMatchesXerialNarrowTypeNameSet() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of( + new Col("bi", "BIGINT"), + new Col("sm", "SMALLINT"), + new Col("dec", "DECIMAL(10,5)"), + new Col("f", "FLOAT"), + new Col("d", "DOUBLE"), + new Col("num", "NUMERIC"), + new Col("i", "INTEGER"), + new Col("r", "REAL")), + List.of(), + "SELECT 1", + null); + + assertThat(metadata.isSigned(1)).as("BIGINT").isFalse(); + assertThat(metadata.isSigned(2)).as("SMALLINT").isFalse(); + assertThat(metadata.isSigned(3)).as("DECIMAL(10,5)").isFalse(); + assertThat(metadata.isSigned(4)).as("FLOAT").isFalse(); + assertThat(metadata.isSigned(5)).as("DOUBLE").isFalse(); + assertThat(metadata.isSigned(6)).as("NUMERIC").isTrue(); + assertThat(metadata.isSigned(7)).as("INTEGER").isTrue(); + assertThat(metadata.isSigned(8)).as("REAL").isTrue(); + } + + @Test + void matchesCastOnlyInLowerCase() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("CAST(1 AS BOOLEAN)", null), new Col("cast(1 as BOOLEAN)", null)), + List.of(new Row(List.of(Value.of(1L), Value.of(1L)))), + "SELECT CAST(1 AS BOOLEAN), cast(1 as BOOLEAN)", + null); + + assertThat(metadata.getColumnTypeName(1)).as("uppercase CAST").isEqualTo("INTEGER"); + assertThat(metadata.getColumnType(1)).as("uppercase CAST").isEqualTo(Types.INTEGER); + assertThat(metadata.isSigned(1)).as("uppercase CAST").isTrue(); + + assertThat(metadata.getColumnTypeName(2)).as("lowercase cast").isEqualTo("BOOLEAN"); + assertThat(metadata.getColumnType(2)).as("lowercase cast").isEqualTo(Types.BOOLEAN); + assertThat(metadata.isSigned(2)).as("lowercase cast").isFalse(); + } + + @Test + void preservesWhitespaceInDeclaredTypeNames() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("c", "BIG INT"), new Col("g", " NUMERIC ")), + List.of(new Row(List.of(Value.of(1L), Value.of(1L)))), + "SELECT c, g FROM q", + null); + + assertThat(metadata.getColumnTypeName(1)).isEqualTo("BIG INT"); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.INTEGER); + assertThat(metadata.isSigned(1)).isFalse(); + + assertThat(metadata.getColumnTypeName(2)).isEqualTo(" NUMERIC "); + assertThat(metadata.getColumnType(2)).isEqualTo(Types.INTEGER); + assertThat(metadata.isSigned(2)).isFalse(); + } + + @Test + void throwsNumberFormatExceptionForMalformedPrecision() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("e", "DECIMAL(abc)")), List.of(), "SELECT e FROM q", null); + + assertThat(metadata.getColumnTypeName(1)).isEqualTo("DECIMAL"); + assertThatThrownBy(() -> metadata.getPrecision(1)) + .isInstanceOf(NumberFormatException.class) + .hasMessageContaining("abc"); + assertThat(metadata.getScale(1)).isZero(); + } + + @Test + void reportsPrecisionAndScaleFromParenthesizedType() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("d", "DECIMAL(10,5)")), List.of(), "SELECT d FROM m", null); + + assertThat(metadata.getPrecision(1)).isEqualTo(10); + assertThat(metadata.getScale(1)).isEqualTo(5); + } + @Test void reportsObjectClassNameForEmptyResultSet() throws SQLException { StmtResult result = new StmtResult(