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 84dfa9c..69a58a2 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
@@ -20,6 +20,7 @@
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Statement;
@@ -481,70 +482,6 @@ private static double toDouble(Value val) {
};
}
- private static int toStrictInt(Value val) throws SQLException {
- return switch (val) {
- case Value.Null n -> 0;
- case Value.Integer i -> (int) i.value();
- case Value.Float f -> (int) f.value();
- case Value.Text t -> {
- try {
- yield Integer.parseInt(t.value());
- } catch (NumberFormatException e) {
- throw new SQLException("Bad value for type Integer: " + t.value(), e);
- }
- }
- case Value.Blob b -> 0;
- };
- }
-
- private static long toStrictLong(Value val) throws SQLException {
- return switch (val) {
- case Value.Null n -> 0L;
- case Value.Integer i -> i.value();
- case Value.Float f -> (long) f.value();
- case Value.Text t -> {
- try {
- yield Long.parseLong(t.value());
- } catch (NumberFormatException e) {
- throw new SQLException("Bad value for type Long: " + t.value(), e);
- }
- }
- case Value.Blob b -> 0L;
- };
- }
-
- private static float toStrictFloat(Value val) throws SQLException {
- return switch (val) {
- case Value.Null n -> 0.0F;
- case Value.Integer i -> (float) i.value();
- case Value.Float f -> (float) f.value();
- case Value.Text t -> {
- try {
- yield Float.parseFloat(t.value());
- } catch (NumberFormatException e) {
- throw new SQLException("Bad value for type Float: " + t.value(), e);
- }
- }
- case Value.Blob b -> 0.0F;
- };
- }
-
- private static double toStrictDouble(Value val) throws SQLException {
- return switch (val) {
- case Value.Null n -> 0.0;
- case Value.Integer i -> (double) i.value();
- case Value.Float f -> f.value();
- case Value.Text t -> {
- try {
- yield Double.parseDouble(t.value());
- } catch (NumberFormatException e) {
- throw new SQLException("Bad value for type Double: " + t.value(), e);
- }
- }
- case Value.Blob b -> 0.0;
- };
- }
-
private static String blobAsText(Value.Blob b) {
return new String(b.bytes(), java.nio.charset.StandardCharsets.UTF_8);
}
@@ -1031,18 +968,65 @@ public boolean isLast() throws SQLException {
@Override public void updateNClob(int columnIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException(); }
@Override public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new UnsupportedOperationException(); }
+ /**
+ * Returns a column value converted to the requested Java type.
+ *
+ *
The conversion matrix matches sqlite-jdbc 3.53.4.0. Numeric wrapper targets are gated on
+ * the current value's SQLite storage class; any other storage class raises {@code Bad value
+ * for type Integer} (or the corresponding target type name):
+ *
+ *
+ * Target type by source storage class
+ *
+ * Target type INTEGER FLOAT TEXT BLOB NULL
+ *
+ *
+ * {@code Integer}, {@code Long}, {@code Float}, {@code Double}
+ * converts converts bad value bad value bad value
+ *
+ *
+ * {@code String}
+ * canonical text canonical text text UTF-8 text
+ * {@code null}
+ *
+ *
+ * {@code Boolean}
+ * nonzero nonzero integer coercion integer coercion
+ * {@code false}
+ *
+ *
+ * {@code BigDecimal}
+ * decimal text decimal text decimal text decimal text
+ * {@code null}
+ *
+ *
+ * {@code byte[]}
+ * UTF-8 text UTF-8 text UTF-8 text bytes
+ * {@code null}
+ *
+ *
+ * {@code Date}, {@code Time}, {@code Timestamp}, {@code LocalDate}, {@code LocalTime},
+ * {@code LocalDateTime}
+ * text parse text parse text parse text parse
+ * {@code null}
+ *
+ *
+ * {@code Short}, {@code Byte}, any other type
+ * unsupported
+ *
+ *
+ *
+ * {@code Short} and {@code Byte} are rejected rather than narrowed from the numeric
+ * targets, preserving the sqlite-jdbc 3.53.4.0 contract.
+ *
+ * @throws SQLException if {@code type} is {@code null}, a numeric target reads a value from
+ * any storage class other than INTEGER or FLOAT, or the value cannot be converted
+ * @throws SQLFeatureNotSupportedException if {@code type} is not a supported target type
+ */
@Override
public T getObject(int columnIndex, Class type) throws SQLException {
if (type == null) {
- throw new SQLException("Type must not be null");
- }
- Object val = getObject(columnIndex);
- if (val == null) {
- return null;
- }
- Value stored = getCurrentValue(columnIndex);
- if (type.isInstance(val)) {
- return type.cast(val);
+ throw new SQLException("requested type cannot be null");
}
if (type == String.class) {
return type.cast(getString(columnIndex));
@@ -1056,24 +1040,6 @@ public T getObject(int columnIndex, Class type) throws SQLException {
if (type == byte[].class) {
return type.cast(getBytes(columnIndex));
}
- if (type == Double.class) {
- return type.cast(toStrictDouble(stored));
- }
- if (type == Long.class) {
- return type.cast(toStrictLong(stored));
- }
- if (type == Float.class) {
- return type.cast(toStrictFloat(stored));
- }
- if (type == Integer.class) {
- return type.cast(toStrictInt(stored));
- }
- if (type == Short.class) {
- return type.cast(Short.valueOf((short) toStrictLong(stored)));
- }
- if (type == Byte.class) {
- return type.cast(Byte.valueOf((byte) toStrictLong(stored)));
- }
if (type == Date.class) {
return type.cast(getDate(columnIndex));
}
@@ -1092,8 +1058,35 @@ public T getObject(int columnIndex, Class type) throws SQLException {
if (type == LocalDateTime.class) {
return type.cast(getLocalDateTime(columnIndex));
}
- throw new SQLException(
- "Cannot convert value of type " + val.getClass() + " to " + type.getName());
+
+ Value stored = getCurrentValue(columnIndex);
+ boolean numeric = stored instanceof Value.Integer || stored instanceof Value.Float;
+ if (type == Double.class) {
+ if (numeric) {
+ return type.cast(getDouble(columnIndex));
+ }
+ throw new SQLException("Bad value for type Double");
+ }
+ if (type == Long.class) {
+ if (numeric) {
+ return type.cast(getLong(columnIndex));
+ }
+ throw new SQLException("Bad value for type Long");
+ }
+ if (type == Float.class) {
+ if (numeric) {
+ return type.cast(getFloat(columnIndex));
+ }
+ throw new SQLException("Bad value for type Float");
+ }
+ if (type == Integer.class) {
+ if (numeric) {
+ return type.cast(getInt(columnIndex));
+ }
+ throw new SQLException("Bad value for type Integer");
+ }
+
+ throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver");
}
private LocalDate getLocalDate(int columnIndex) throws SQLException {
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetGetObjectTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetGetObjectTest.java
new file mode 100644
index 0000000..74ed065
--- /dev/null
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetGetObjectTest.java
@@ -0,0 +1,164 @@
+package com.github.magnusp.libsql.jdbc;
+
+import com.github.magnusp.libsql.hrana.codec.Col;
+import com.github.magnusp.libsql.hrana.codec.Row;
+import com.github.magnusp.libsql.hrana.codec.StmtResult;
+import com.github.magnusp.libsql.hrana.codec.Value;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LibsqlResultSetGetObjectTest {
+
+ static Stream numericTargets() {
+ return Stream.of(
+ Arguments.of(Integer.class, "Integer", 7, 7),
+ Arguments.of(Long.class, "Long", 7L, 7L),
+ Arguments.of(Float.class, "Float", 7.0F, 7.5F),
+ Arguments.of(Double.class, "Double", 7.0, 7.5));
+ }
+
+ static Stream nonIntegerOrFloatStorage() {
+ return Stream.of(
+ Arguments.of("numeric text", new Value.Text("7")),
+ Arguments.of("non-numeric text", new Value.Text("abc")),
+ Arguments.of("numeric blob", new Value.Blob("123".getBytes(StandardCharsets.UTF_8))),
+ Arguments.of("non-numeric blob", new Value.Blob("abc".getBytes(StandardCharsets.UTF_8))),
+ Arguments.of("null", new Value.Null()));
+ }
+
+ @ParameterizedTest(name = "{1} from INTEGER storage")
+ @MethodSource("numericTargets")
+ void numericTargetsReadIntegerStorage(
+ Class> target, String typeName, Object fromInteger, Object fromFloat) throws SQLException {
+ LibsqlResultSet rs = resultSet(new Value.Integer(7));
+ assertThat(rs.next()).isTrue();
+
+ assertThat(rs.getObject(1, target)).isEqualTo(fromInteger);
+ }
+
+ @ParameterizedTest(name = "{1} from FLOAT storage")
+ @MethodSource("numericTargets")
+ void numericTargetsReadFloatStorage(
+ Class> target, String typeName, Object fromInteger, Object fromFloat) throws SQLException {
+ LibsqlResultSet rs = resultSet(new Value.Float(7.5));
+ assertThat(rs.next()).isTrue();
+
+ assertThat(rs.getObject(1, target)).isEqualTo(fromFloat);
+ }
+
+ @ParameterizedTest(name = "{1} rejects {0}")
+ @MethodSource("numericTargets")
+ void numericTargetsRejectTextBlobAndNullStorage(Class> target, String typeName)
+ throws SQLException {
+ for (Arguments arguments : nonIntegerOrFloatStorage().toList()) {
+ Object[] parts = arguments.get();
+ Value value = (Value) parts[1];
+
+ LibsqlResultSet rs = resultSet(value);
+ assertThat(rs.next()).isTrue();
+
+ assertThatThrownBy(() -> rs.getObject(1, target))
+ .as("%s from %s storage", typeName, parts[0])
+ .isExactlyInstanceOf(SQLException.class)
+ .hasMessage("Bad value for type " + typeName);
+ }
+ }
+
+ static Stream shortAndByteTargets() {
+ return Stream.of(Arguments.of(Short.class, "Short"), Arguments.of(Byte.class, "Byte"));
+ }
+
+ @ParameterizedTest(name = "{1} is unsupported")
+ @MethodSource("shortAndByteTargets")
+ void shortAndByteAreUnsupportedForEveryStorageClass(Class> target, String typeName)
+ throws SQLException {
+ for (Arguments arguments : nonIntegerOrFloatStorage().toList()) {
+ Object[] parts = arguments.get();
+ Value value = (Value) parts[1];
+
+ LibsqlResultSet rs = resultSet(value);
+ assertThat(rs.next()).isTrue();
+
+ assertThatThrownBy(() -> rs.getObject(1, target))
+ .as("%s from %s storage", typeName, parts[0])
+ .isExactlyInstanceOf(SQLFeatureNotSupportedException.class)
+ .hasMessage("not implemented by SQLite JDBC driver");
+ }
+
+ for (Value value : List.of(new Value.Integer(7), new Value.Float(7.5))) {
+ LibsqlResultSet rs = resultSet(value);
+ assertThat(rs.next()).isTrue();
+
+ assertThatThrownBy(() -> rs.getObject(1, target))
+ .as("%s from %s storage", typeName, value.getClass().getSimpleName())
+ .isExactlyInstanceOf(SQLFeatureNotSupportedException.class)
+ .hasMessage("not implemented by SQLite JDBC driver");
+ }
+ }
+
+ static Stream unsupportedTargets() {
+ return Stream.of(
+ Arguments.of(Object.class),
+ Arguments.of(Number.class),
+ Arguments.of(UUID.class));
+ }
+
+ @ParameterizedTest(name = "{0} is unsupported")
+ @MethodSource("unsupportedTargets")
+ void unsupportedTargetTypeUsesXerialMessage(Class> target) throws SQLException {
+ LibsqlResultSet rs = resultSet(new Value.Integer(7));
+ assertThat(rs.next()).isTrue();
+
+ assertThatThrownBy(() -> rs.getObject(1, target))
+ .isExactlyInstanceOf(SQLFeatureNotSupportedException.class)
+ .hasMessage("not implemented by SQLite JDBC driver");
+ }
+
+ @Test
+ void nullTargetTypeUsesXerialMessage() throws SQLException {
+ LibsqlResultSet rs = resultSet(new Value.Integer(7));
+ assertThat(rs.next()).isTrue();
+
+ assertThatThrownBy(() -> rs.getObject(1, (Class) null))
+ .isExactlyInstanceOf(SQLException.class)
+ .hasMessage("requested type cannot be null");
+ }
+
+ @Test
+ void nonNumericTargetsKeepTheirUnrestrictedConversions() throws SQLException {
+ LibsqlResultSet integers = resultSet(new Value.Integer(7));
+ assertThat(integers.next()).isTrue();
+ assertThat(integers.getObject(1, String.class)).isEqualTo("7");
+ assertThat(integers.getObject(1, byte[].class))
+ .isEqualTo("7".getBytes(StandardCharsets.UTF_8));
+
+ LibsqlResultSet text = resultSet(new Value.Text("abc"));
+ assertThat(text.next()).isTrue();
+ assertThat(text.getObject(1, String.class)).isEqualTo("abc");
+ assertThat(text.getObject(1, Boolean.class)).isFalse();
+
+ LibsqlResultSet nullValue = resultSet(new Value.Null());
+ assertThat(nullValue.next()).isTrue();
+ assertThat(nullValue.getObject(1, String.class)).isNull();
+ assertThat(nullValue.getObject(1, Boolean.class)).isFalse();
+ assertThat(nullValue.getObject(1, byte[].class)).isNull();
+ }
+
+ private static LibsqlResultSet resultSet(Value value) {
+ List cols = List.of(new Col("c", null));
+ StmtResult result = new StmtResult(cols, List.of(new Row(List.of(value))), 1, null);
+ return new LibsqlResultSet(null, result);
+ }
+}
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
index f2b3a5a..8e556db 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java
@@ -176,14 +176,22 @@ void getObjectNumericConversionsStayStrictForNonNumericText() throws SQLExceptio
}
@Test
- void getObjectNumericConversionsAcceptNumericText() throws SQLException {
+ void getObjectNumericConversionsRejectNumericTextLikeXerial() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("123"));
assertThat(rs.next()).isTrue();
- assertThat(rs.getObject(1, Double.class)).isEqualTo(123.0);
- assertThat(rs.getObject(1, Long.class)).isEqualTo(123L);
- assertThat(rs.getObject(1, Float.class)).isEqualTo(123.0f);
- assertThat(rs.getObject(1, Integer.class)).isEqualTo(123);
+ assertThatThrownBy(() -> rs.getObject(1, Double.class))
+ .isInstanceOf(SQLException.class)
+ .hasMessage("Bad value for type Double");
+ assertThatThrownBy(() -> rs.getObject(1, Long.class))
+ .isInstanceOf(SQLException.class)
+ .hasMessage("Bad value for type Long");
+ assertThatThrownBy(() -> rs.getObject(1, Float.class))
+ .isInstanceOf(SQLException.class)
+ .hasMessage("Bad value for type Float");
+ assertThatThrownBy(() -> rs.getObject(1, Integer.class))
+ .isInstanceOf(SQLException.class)
+ .hasMessage("Bad value for type Integer");
}
private static void assertNumeric(LibsqlResultSet rs, int column, int expectedInt, long expectedLong,