Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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.
*
* <p>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):
*
* <table>
* <caption>Target type by source storage class</caption>
* <tr>
* <th>Target type</th><th>INTEGER</th><th>FLOAT</th><th>TEXT</th><th>BLOB</th><th>NULL</th>
* </tr>
* <tr>
* <td>{@code Integer}, {@code Long}, {@code Float}, {@code Double}</td>
* <td>converts</td><td>converts</td><td>bad value</td><td>bad value</td><td>bad value</td>
* </tr>
* <tr>
* <td>{@code String}</td>
* <td>canonical text</td><td>canonical text</td><td>text</td><td>UTF-8 text</td>
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@code Boolean}</td>
* <td>nonzero</td><td>nonzero</td><td>integer coercion</td><td>integer coercion</td>
* <td>{@code false}</td>
* </tr>
* <tr>
* <td>{@code BigDecimal}</td>
* <td>decimal text</td><td>decimal text</td><td>decimal text</td><td>decimal text</td>
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@code byte[]}</td>
* <td>UTF-8 text</td><td>UTF-8 text</td><td>UTF-8 text</td><td>bytes</td>
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@code Date}, {@code Time}, {@code Timestamp}, {@code LocalDate}, {@code LocalTime},
* {@code LocalDateTime}</td>
* <td>text parse</td><td>text parse</td><td>text parse</td><td>text parse</td>
* <td>{@code null}</td>
* </tr>
* <tr>
* <td>{@code Short}, {@code Byte}, any other type</td>
* <td colspan="5">unsupported</td>
* </tr>
* </table>
*
* <p>{@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> T getObject(int columnIndex, Class<T> 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));
Expand All @@ -1056,24 +1040,6 @@ public <T> T getObject(int columnIndex, Class<T> 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));
}
Expand All @@ -1092,8 +1058,35 @@ public <T> T getObject(int columnIndex, Class<T> 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Object>) 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<Col> 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);
}
}
Loading
Loading