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 @@ -148,6 +148,58 @@ public short getShort(int columnIndex) throws SQLException {
@Override
public int getInt(int columnIndex) throws SQLException {
Value val = getCurrentValue(columnIndex);
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 -> (int) parseSqliteLong(t.value());
case Value.Blob b -> (int) parseSqliteLong(blobAsText(b));
};
}

@Override
public long getLong(int columnIndex) throws SQLException {
return toLong(getCurrentValue(columnIndex));
}

@Override
public float getFloat(int columnIndex) throws SQLException {
Value val = getCurrentValue(columnIndex);
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 -> (float) parseSqliteDouble(t.value());
case Value.Blob b -> (float) parseSqliteDouble(blobAsText(b));
};
}

@Override
public double getDouble(int columnIndex) throws SQLException {
return toDouble(getCurrentValue(columnIndex));
}

private static long toLong(Value val) {
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 -> parseSqliteLong(t.value());
case Value.Blob b -> parseSqliteLong(blobAsText(b));
};
}

private static double toDouble(Value val) {
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 -> parseSqliteDouble(t.value());
case Value.Blob b -> parseSqliteDouble(blobAsText(b));
};
}

private static int toStrictInt(Value val) throws SQLException {
return switch (val) {
case Value.Null n -> 0;
case Value.Integer i -> (int) i.value();
Expand All @@ -163,9 +215,7 @@ public int getInt(int columnIndex) throws SQLException {
};
}

@Override
public long getLong(int columnIndex) throws SQLException {
Value val = getCurrentValue(columnIndex);
private static long toStrictLong(Value val) throws SQLException {
return switch (val) {
case Value.Null n -> 0L;
case Value.Integer i -> i.value();
Expand All @@ -181,9 +231,7 @@ public long getLong(int columnIndex) throws SQLException {
};
}

@Override
public float getFloat(int columnIndex) throws SQLException {
Value val = getCurrentValue(columnIndex);
private static float toStrictFloat(Value val) throws SQLException {
return switch (val) {
case Value.Null n -> 0.0F;
case Value.Integer i -> (float) i.value();
Expand All @@ -199,9 +247,7 @@ public float getFloat(int columnIndex) throws SQLException {
};
}

@Override
public double getDouble(int columnIndex) throws SQLException {
Value val = getCurrentValue(columnIndex);
private static double toStrictDouble(Value val) throws SQLException {
return switch (val) {
case Value.Null n -> 0.0;
case Value.Integer i -> (double) i.value();
Expand All @@ -217,6 +263,111 @@ public double getDouble(int columnIndex) throws SQLException {
};
}

private static String blobAsText(Value.Blob b) {
return new String(b.bytes(), java.nio.charset.StandardCharsets.UTF_8);
}

private static long parseSqliteLong(String text) {
int length = text.length();
int index = skipSqliteWhitespace(text, 0);
boolean negative = false;
if (index < length && (text.charAt(index) == '+' || text.charAt(index) == '-')) {
negative = text.charAt(index) == '-';
index++;
}

int digitsStart = index;
long value = 0L;
boolean overflow = false;
while (index < length) {
char c = text.charAt(index);
if (c < '0' || c > '9') {
break;
}
if (!overflow) {
int digit = c - '0';
if (value > (Long.MAX_VALUE - digit) / 10) {
overflow = true;
} else {
value = value * 10 + digit;
}
}
index++;
}

if (index == digitsStart) {
return 0L;
}
if (overflow) {
return negative ? Long.MIN_VALUE : Long.MAX_VALUE;
}
return negative ? -value : value;
}

private static double parseSqliteDouble(String text) {
int length = text.length();
int index = skipSqliteWhitespace(text, 0);
int start = index;
if (index < length && (text.charAt(index) == '+' || text.charAt(index) == '-')) {
index++;
}

int digits = 0;
while (index < length && isAsciiDigit(text.charAt(index))) {
index++;
digits++;
}
if (index < length && text.charAt(index) == '.') {
index++;
while (index < length && isAsciiDigit(text.charAt(index))) {
index++;
digits++;
}
}

if (digits == 0) {
return 0.0;
}

int end = index;
if (index < length && (text.charAt(index) == 'e' || text.charAt(index) == 'E')) {
int exponentIndex = index + 1;
if (exponentIndex < length
&& (text.charAt(exponentIndex) == '+' || text.charAt(exponentIndex) == '-')) {
exponentIndex++;
}
int exponentDigits = 0;
while (exponentIndex < length && isAsciiDigit(text.charAt(exponentIndex))) {
exponentIndex++;
exponentDigits++;
}
if (exponentDigits > 0) {
end = exponentIndex;
}
}

try {
return Double.parseDouble(text.substring(start, end));
} catch (NumberFormatException e) {
return 0.0;
}
}

private static int skipSqliteWhitespace(String text, int index) {
while (index < text.length() && isSqliteWhitespace(text.charAt(index))) {
index++;
}
return index;
}

private static boolean isSqliteWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u000b' || c == '\f';
}

private static boolean isAsciiDigit(char c) {
return c >= '0' && c <= '9';
}

@Override
@Deprecated
@SuppressWarnings("deprecation")
Expand Down Expand Up @@ -607,6 +758,7 @@ public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
if (val == null) {
return null;
}
Value stored = getCurrentValue(columnIndex);
if (type.isInstance(val)) {
return type.cast(val);
}
Expand All @@ -623,22 +775,22 @@ public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
return type.cast(getBytes(columnIndex));
}
if (type == Double.class) {
return type.cast(getDouble(columnIndex));
return type.cast(toStrictDouble(stored));
}
if (type == Long.class) {
return type.cast(getLong(columnIndex));
return type.cast(toStrictLong(stored));
}
if (type == Float.class) {
return type.cast(getFloat(columnIndex));
return type.cast(toStrictFloat(stored));
}
if (type == Integer.class) {
return type.cast(getInt(columnIndex));
return type.cast(toStrictInt(stored));
}
if (type == Short.class) {
return type.cast(getShort(columnIndex));
return type.cast(Short.valueOf((short) toStrictLong(stored)));
}
if (type == Byte.class) {
return type.cast(getByte(columnIndex));
return type.cast(Byte.valueOf((byte) toStrictLong(stored)));
}
if (type == Date.class) {
return type.cast(getDate(columnIndex));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import com.github.magnusp.libsql.hrana.codec.Value;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class LibsqlResultSetTest {

Expand Down Expand Up @@ -81,4 +84,121 @@ void statementCloseClosesCurrentResultSet() throws SQLException {

assertThat(resultSet.isClosed()).isTrue();
}

@Test
void nonNumericTextCoercesToZero() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("abc"));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 0, 0L, 0.0f, 0.0);
}

@Test
void numericTextCoercesToNumber() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("123"));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 123, 123L, 123.0f, 123.0);
}

@Test
void realTextTruncatesForIntegralGettersAndKeepsFractionForFloatingGetters() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("1.9"));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
}

@Test
void textWithExponentUsesIntegerPrefixForIntegralGetters() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("1.5e2"));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 1, 1L, 150.0f, 150.0);
}

@Test
void textWithLeadingWhitespaceAndTrailingGarbageUsesNumericPrefix() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text(" 42xyz"));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 42, 42L, 42.0f, 42.0);
}

@Test
void numericBlobIsInterpretedAsText() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Blob("123".getBytes(StandardCharsets.UTF_8)));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 123, 123L, 123.0f, 123.0);
}

@Test
void realTextBlobIsInterpretedAsText() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Blob("1.9".getBytes(StandardCharsets.UTF_8)));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
}

@Test
void nonNumericBlobCoercesToZero() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Blob("abc".getBytes(StandardCharsets.UTF_8)));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 0, 0L, 0.0f, 0.0);
}

@Test
void realStorageTruncatesForIntegralGettersAndKeepsFractionForFloatingGetters() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Float(1.9));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
}

@Test
void integerStorageIsReturnedUnchanged() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Integer(42));
assertThat(rs.next()).isTrue();
assertNumeric(rs, 1, 42, 42L, 42.0f, 42.0);
}

@Test
void getObjectNumericConversionsStayStrictForNonNumericText() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("abc"));
assertThat(rs.next()).isTrue();

assertNumeric(rs, 1, 0, 0L, 0.0f, 0.0);
assertThatThrownBy(() -> rs.getObject(1, Double.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("Bad value for type Double");
assertThatThrownBy(() -> rs.getObject(1, Long.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("Bad value for type Long");
assertThatThrownBy(() -> rs.getObject(1, Float.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("Bad value for type Float");
assertThatThrownBy(() -> rs.getObject(1, Integer.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("Bad value for type Integer");
}

@Test
void getObjectNumericConversionsAcceptNumericText() 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);
}

private static void assertNumeric(LibsqlResultSet rs, int column, int expectedInt, long expectedLong,
float expectedFloat, double expectedDouble) throws SQLException {
assertThat(rs.getInt(column)).isEqualTo(expectedInt);
assertThat(rs.getLong(column)).isEqualTo(expectedLong);
assertThat(rs.getFloat(column)).isEqualTo(expectedFloat);
assertThat(rs.getDouble(column)).isEqualTo(expectedDouble);
}

private static LibsqlResultSet resultSet(Value... values) {
List<Col> cols = IntStream.range(0, values.length)
.mapToObj(i -> new Col("c" + (i + 1), null))
.toList();
StmtResult result = new StmtResult(cols, List.of(new Row(List.of(values))), 1, null);
return new LibsqlResultSet(null, result);
}
}
Loading