diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java index 54a7ca2..db3b68e 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -85,6 +85,19 @@ private Value getCurrentValue(int columnIndex) throws SQLException { return val; } + Value valueForMetadata(int columnIndex) { + int rowIndex = currentRowIndex < 0 ? 0 : currentRowIndex; + if (rowIndex >= rows.size()) { + return null; + } + Row row = rows.get(rowIndex); + List values = row.values(); + if (values == null || columnIndex < 1 || columnIndex > values.size()) { + return null; + } + return values.get(columnIndex - 1); + } + @Override public boolean next() throws SQLException { checkClosed(); @@ -554,7 +567,7 @@ public Object getObject(String columnLabel) throws SQLException { public ResultSetMetaData getMetaData() throws SQLException { checkClosed(); if (metadata == null) { - metadata = new LibsqlResultSetMetaData(cols, rows, sql, connection); + metadata = new LibsqlResultSetMetaData(cols, rows, sql, connection, this); } return metadata; } 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 c2f059a..0553a65 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 @@ -27,19 +27,26 @@ public class LibsqlResultSetMetaData implements ResultSetMetaData { private final List rows; private final String sql; private final LibsqlConnection connection; + private final LibsqlResultSet resultSet; private boolean tableContextResolved; private TableInfo tableContext; public LibsqlResultSetMetaData(List cols) { - this(cols, List.of(), null, null); + this(cols, List.of(), null, null, null); } LibsqlResultSetMetaData(List cols, List rows, String sql, LibsqlConnection connection) { + this(cols, rows, sql, connection, null); + } + + LibsqlResultSetMetaData( + List cols, List rows, String sql, LibsqlConnection connection, LibsqlResultSet resultSet) { this.cols = cols != null ? cols : List.of(); this.rows = rows != null ? rows : List.of(); this.sql = sql; this.connection = connection; + this.resultSet = resultSet; } @Override @@ -204,28 +211,92 @@ public String getCatalogName(int column) throws SQLException { public int getColumnType(int column) throws SQLException { getCol(column); String typeName = getColumnTypeName(column); - Value sample = sampleValue(column); + Value value = runtimeValue(column); + SampleType valueType = storageType(value); - return switch (typeName) { - case "BOOLEAN" -> Types.BOOLEAN; - case "TINYINT" -> Types.TINYINT; - case "SMALLINT", "INT2" -> Types.SMALLINT; - case "BIGINT", "INT8", "UNSIGNED BIG INT" -> Types.BIGINT; - case "DATE" -> Types.DATE; - case "DATETIME", "TIMESTAMP" -> Types.TIMESTAMP; - case "INT", "INTEGER", "MEDIUMINT" -> integerType(column); - case "DECIMAL" -> Types.DECIMAL; - case "DOUBLE", "DOUBLE PRECISION" -> Types.DOUBLE; - case "NUMERIC" -> Types.NUMERIC; - case "REAL" -> Types.REAL; - case "FLOAT" -> Types.FLOAT; - case "CHARACTER", "NCHAR", "NATIVE CHARACTER", "CHAR" -> Types.CHAR; - case "CLOB" -> Types.CLOB; - case "VARCHAR", "VARYING CHARACTER", "NVARCHAR", "TEXT" -> Types.VARCHAR; - case "BINARY" -> Types.BINARY; - case "BLOB" -> Types.BLOB; - default -> inferTypeFromValue(sample); - }; + if (valueType == SampleType.INTEGER || valueType == SampleType.NULL) { + if ("BOOLEAN".equals(typeName)) { + return Types.BOOLEAN; + } + if ("TINYINT".equals(typeName)) { + return Types.TINYINT; + } + if ("SMALLINT".equals(typeName) || "INT2".equals(typeName)) { + return Types.SMALLINT; + } + if ("BIGINT".equals(typeName) + || "INT8".equals(typeName) + || "UNSIGNED BIG INT".equals(typeName)) { + return Types.BIGINT; + } + if ("DATE".equals(typeName)) { + return Types.DATE; + } + if ("DATETIME".equals(typeName) || "TIMESTAMP".equals(typeName)) { + return Types.TIMESTAMP; + } + if (valueType == SampleType.INTEGER + || "INT".equals(typeName) + || "INTEGER".equals(typeName) + || "MEDIUMINT".equals(typeName)) { + long val = integer(value); + return val > Integer.MAX_VALUE || val < Integer.MIN_VALUE ? Types.BIGINT : Types.INTEGER; + } + } + + if (valueType == SampleType.FLOAT || valueType == SampleType.NULL) { + if ("DECIMAL".equals(typeName)) { + return Types.DECIMAL; + } + if ("DOUBLE".equals(typeName) || "DOUBLE PRECISION".equals(typeName)) { + return Types.DOUBLE; + } + if ("NUMERIC".equals(typeName)) { + return Types.NUMERIC; + } + if ("REAL".equals(typeName)) { + return Types.REAL; + } + if (valueType == SampleType.FLOAT || "FLOAT".equals(typeName)) { + return Types.FLOAT; + } + } + + if (valueType == SampleType.TEXT || valueType == SampleType.NULL) { + if ("CHARACTER".equals(typeName) + || "NCHAR".equals(typeName) + || "NATIVE CHARACTER".equals(typeName) + || "CHAR".equals(typeName)) { + return Types.CHAR; + } + if ("CLOB".equals(typeName)) { + return Types.CLOB; + } + if ("DATE".equals(typeName)) { + return Types.DATE; + } + if ("DATETIME".equals(typeName) || "TIMESTAMP".equals(typeName)) { + return Types.TIMESTAMP; + } + if (valueType == SampleType.TEXT + || "VARCHAR".equals(typeName) + || "VARYING CHARACTER".equals(typeName) + || "NVARCHAR".equals(typeName) + || "TEXT".equals(typeName)) { + return Types.VARCHAR; + } + } + + if (valueType == SampleType.BLOB || valueType == SampleType.NULL) { + if ("BINARY".equals(typeName)) { + return Types.BINARY; + } + if (valueType == SampleType.BLOB || "BLOB".equals(typeName)) { + return Types.BLOB; + } + } + + return Types.NUMERIC; } @Override @@ -346,6 +417,13 @@ private String columnName(int column) throws SQLException { return name != null ? name.toLowerCase(Locale.ROOT) : ""; } + private Value runtimeValue(int column) { + if (resultSet != null) { + return resultSet.valueForMetadata(column); + } + return sampleValue(column); + } + private Value sampleValue(int column) { for (Row row : rows) { List values = row.values(); @@ -361,17 +439,20 @@ private Value sampleValue(int column) { } private SampleType sampleType(int column) { - Value sample = sampleValue(column); - if (sample instanceof Value.Integer) { + return storageType(sampleValue(column)); + } + + private static SampleType storageType(Value value) { + if (value instanceof Value.Integer) { return SampleType.INTEGER; } - if (sample instanceof Value.Float) { + if (value instanceof Value.Float) { return SampleType.FLOAT; } - if (sample instanceof Value.Blob) { + if (value instanceof Value.Blob) { return SampleType.BLOB; } - if (sample instanceof Value.Text) { + if (value instanceof Value.Text) { return SampleType.TEXT; } return SampleType.NULL; @@ -383,50 +464,6 @@ private static boolean isIntInRange(Value value) { && i.value() <= Integer.MAX_VALUE; } - private int integerType(int column) { - Value firstNonInteger = null; - boolean outsideIntRange = false; - for (Row row : rows) { - List values = row.values(); - if (values == null || column < 1 || column > values.size()) { - continue; - } - Value value = values.get(column - 1); - if (value == null || value instanceof Value.Null) { - continue; - } - if (value instanceof Value.Integer) { - if (!isIntInRange(value)) { - outsideIntRange = true; - } - } else if (firstNonInteger == null) { - firstNonInteger = value; - } - } - if (firstNonInteger != null) { - return inferTypeFromValue(firstNonInteger); - } - return outsideIntRange ? Types.BIGINT : Types.INTEGER; - } - - private static int inferTypeFromValue(Value sample) { - if (sample instanceof Value.Integer i) { - return i.value() >= Integer.MIN_VALUE && i.value() <= Integer.MAX_VALUE - ? Types.INTEGER - : Types.BIGINT; - } - if (sample instanceof Value.Float) { - return Types.FLOAT; - } - if (sample instanceof Value.Text) { - return Types.VARCHAR; - } - if (sample instanceof Value.Blob) { - return Types.BLOB; - } - return Types.NUMERIC; - } - private TableInfo resolveTableContext() throws SQLException { if (!tableContextResolved) { TableInfo resolved = null; 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 7adce6c..d47af8f 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 @@ -147,6 +147,101 @@ void usesRuntimeValueMappingForNonIntegerValuesInIntegerColumn() throws SQLExcep assertThat(metadata.getColumnType(1)).isEqualTo(Types.VARCHAR); } + @Test + void usesRuntimeStorageClassForDeclaredTypes() throws SQLException { + StmtResult result = new StmtResult( + List.of( + new Col("bi", "BIGINT"), + new Col("bo", "BOOLEAN"), + new Col("num", "NUMERIC"), + new Col("dec", "DECIMAL")), + List.of(new Row(List.of(Value.of(1.5), Value.of("true"), Value.of(7L), Value.of(8L)))), + 0, + null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertRuntimeTypes(metadata); + + assertThat(resultSet.next()).isTrue(); + assertRuntimeTypes(metadata); + } + + @Test + void usesDeclaredTypesWhenRuntimeValueIsNull() throws SQLException { + StmtResult result = new StmtResult( + List.of( + new Col("bi", "BIGINT"), + new Col("bo", "BOOLEAN"), + new Col("num", "NUMERIC"), + new Col("dec", "DECIMAL")), + List.of(new Row(List.of(Value.ofNull(), Value.ofNull(), Value.ofNull(), Value.ofNull()))), + 0, + null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertDeclaredTypes(metadata); + + assertThat(resultSet.next()).isTrue(); + assertDeclaredTypes(metadata); + } + + @Test + void usesDeclaredTypesForEmptyResultSet() throws SQLException { + StmtResult result = new StmtResult( + List.of( + new Col("bi", "BIGINT"), + new Col("bo", "BOOLEAN"), + new Col("num", "NUMERIC"), + new Col("dec", "DECIMAL")), + List.of(), + 0, + null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertDeclaredTypes(metadata); + assertThat(resultSet.next()).isFalse(); + assertDeclaredTypes(metadata); + } + + @Test + void reflectsStorageClassChangesBetweenRows() throws SQLException { + StmtResult result = new StmtResult( + List.of(new Col("bi", "BIGINT")), + List.of(new Row(List.of(Value.of(1.5))), new Row(List.of(Value.of(2L)))), + 0, + null); + LibsqlResultSet resultSet = new LibsqlResultSet(null, result); + ResultSetMetaData metadata = resultSet.getMetaData(); + + assertThat(metadata.getColumnType(1)).isEqualTo(Types.FLOAT); + + assertThat(resultSet.next()).isTrue(); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.FLOAT); + + assertThat(resultSet.next()).isTrue(); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT); + + assertThat(resultSet.next()).isFalse(); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT); + } + + private static void assertRuntimeTypes(ResultSetMetaData metadata) throws SQLException { + assertThat(metadata.getColumnType(1)).isEqualTo(Types.FLOAT); + assertThat(metadata.getColumnType(2)).isEqualTo(Types.VARCHAR); + assertThat(metadata.getColumnType(3)).isEqualTo(Types.INTEGER); + assertThat(metadata.getColumnType(4)).isEqualTo(Types.INTEGER); + } + + private static void assertDeclaredTypes(ResultSetMetaData metadata) throws SQLException { + assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT); + assertThat(metadata.getColumnType(2)).isEqualTo(Types.BOOLEAN); + assertThat(metadata.getColumnType(3)).isEqualTo(Types.NUMERIC); + assertThat(metadata.getColumnType(4)).isEqualTo(Types.DECIMAL); + } + @Test void leavesAmbiguousTableContextUnknown() throws SQLException { LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData(