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(