Skip to content

Commit 9fae229

Browse files
committed
fix(jdbc): coerce text and blob values like sqlite-jdbc
1 parent 194ebdc commit 9fae229

2 files changed

Lines changed: 213 additions & 38 deletions

File tree

‎libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java‎

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,14 @@ public int getInt(int columnIndex) throws SQLException {
152152
case Value.Null n -> 0;
153153
case Value.Integer i -> (int) i.value();
154154
case Value.Float f -> (int) f.value();
155-
case Value.Text t -> {
156-
try {
157-
yield Integer.parseInt(t.value());
158-
} catch (NumberFormatException e) {
159-
throw new SQLException("Bad value for type Integer: " + t.value(), e);
160-
}
161-
}
162-
case Value.Blob b -> 0;
155+
case Value.Text t -> (int) parseSqliteLong(t.value());
156+
case Value.Blob b -> (int) parseSqliteLong(blobAsText(b));
163157
};
164158
}
165159

166160
@Override
167161
public long getLong(int columnIndex) throws SQLException {
168-
Value val = getCurrentValue(columnIndex);
169-
return switch (val) {
170-
case Value.Null n -> 0L;
171-
case Value.Integer i -> i.value();
172-
case Value.Float f -> (long) f.value();
173-
case Value.Text t -> {
174-
try {
175-
yield Long.parseLong(t.value());
176-
} catch (NumberFormatException e) {
177-
throw new SQLException("Bad value for type Long: " + t.value(), e);
178-
}
179-
}
180-
case Value.Blob b -> 0L;
181-
};
162+
return toLong(getCurrentValue(columnIndex));
182163
}
183164

184165
@Override
@@ -188,33 +169,139 @@ public float getFloat(int columnIndex) throws SQLException {
188169
case Value.Null n -> 0.0F;
189170
case Value.Integer i -> (float) i.value();
190171
case Value.Float f -> (float) f.value();
191-
case Value.Text t -> {
192-
try {
193-
yield Float.parseFloat(t.value());
194-
} catch (NumberFormatException e) {
195-
throw new SQLException("Bad value for type Float: " + t.value(), e);
196-
}
197-
}
198-
case Value.Blob b -> 0.0F;
172+
case Value.Text t -> (float) parseSqliteDouble(t.value());
173+
case Value.Blob b -> (float) parseSqliteDouble(blobAsText(b));
199174
};
200175
}
201176

202177
@Override
203178
public double getDouble(int columnIndex) throws SQLException {
204-
Value val = getCurrentValue(columnIndex);
179+
return toDouble(getCurrentValue(columnIndex));
180+
}
181+
182+
private static long toLong(Value val) {
183+
return switch (val) {
184+
case Value.Null n -> 0L;
185+
case Value.Integer i -> i.value();
186+
case Value.Float f -> (long) f.value();
187+
case Value.Text t -> parseSqliteLong(t.value());
188+
case Value.Blob b -> parseSqliteLong(blobAsText(b));
189+
};
190+
}
191+
192+
private static double toDouble(Value val) {
205193
return switch (val) {
206194
case Value.Null n -> 0.0;
207195
case Value.Integer i -> (double) i.value();
208196
case Value.Float f -> f.value();
209-
case Value.Text t -> {
210-
try {
211-
yield Double.parseDouble(t.value());
212-
} catch (NumberFormatException e) {
213-
throw new SQLException("Bad value for type Double: " + t.value(), e);
197+
case Value.Text t -> parseSqliteDouble(t.value());
198+
case Value.Blob b -> parseSqliteDouble(blobAsText(b));
199+
};
200+
}
201+
202+
private static String blobAsText(Value.Blob b) {
203+
return new String(b.bytes(), java.nio.charset.StandardCharsets.UTF_8);
204+
}
205+
206+
private static long parseSqliteLong(String text) {
207+
int length = text.length();
208+
int index = skipSqliteWhitespace(text, 0);
209+
boolean negative = false;
210+
if (index < length && (text.charAt(index) == '+' || text.charAt(index) == '-')) {
211+
negative = text.charAt(index) == '-';
212+
index++;
213+
}
214+
215+
int digitsStart = index;
216+
long value = 0L;
217+
boolean overflow = false;
218+
while (index < length) {
219+
char c = text.charAt(index);
220+
if (c < '0' || c > '9') {
221+
break;
222+
}
223+
if (!overflow) {
224+
int digit = c - '0';
225+
if (value > (Long.MAX_VALUE - digit) / 10) {
226+
overflow = true;
227+
} else {
228+
value = value * 10 + digit;
214229
}
215230
}
216-
case Value.Blob b -> 0.0;
217-
};
231+
index++;
232+
}
233+
234+
if (index == digitsStart) {
235+
return 0L;
236+
}
237+
if (overflow) {
238+
return negative ? Long.MIN_VALUE : Long.MAX_VALUE;
239+
}
240+
return negative ? -value : value;
241+
}
242+
243+
private static double parseSqliteDouble(String text) {
244+
int length = text.length();
245+
int index = skipSqliteWhitespace(text, 0);
246+
int start = index;
247+
if (index < length && (text.charAt(index) == '+' || text.charAt(index) == '-')) {
248+
index++;
249+
}
250+
251+
int digits = 0;
252+
while (index < length && isAsciiDigit(text.charAt(index))) {
253+
index++;
254+
digits++;
255+
}
256+
if (index < length && text.charAt(index) == '.') {
257+
index++;
258+
while (index < length && isAsciiDigit(text.charAt(index))) {
259+
index++;
260+
digits++;
261+
}
262+
}
263+
264+
if (digits == 0) {
265+
return 0.0;
266+
}
267+
268+
int end = index;
269+
if (index < length && (text.charAt(index) == 'e' || text.charAt(index) == 'E')) {
270+
int exponentIndex = index + 1;
271+
if (exponentIndex < length
272+
&& (text.charAt(exponentIndex) == '+' || text.charAt(exponentIndex) == '-')) {
273+
exponentIndex++;
274+
}
275+
int exponentDigits = 0;
276+
while (exponentIndex < length && isAsciiDigit(text.charAt(exponentIndex))) {
277+
exponentIndex++;
278+
exponentDigits++;
279+
}
280+
if (exponentDigits > 0) {
281+
end = exponentIndex;
282+
}
283+
}
284+
285+
try {
286+
return Double.parseDouble(text.substring(start, end));
287+
} catch (NumberFormatException e) {
288+
return 0.0;
289+
}
290+
}
291+
292+
private static int skipSqliteWhitespace(String text, int index) {
293+
while (index < text.length() && isSqliteWhitespace(text.charAt(index))) {
294+
index++;
295+
}
296+
return index;
297+
}
298+
299+
private static boolean isSqliteWhitespace(char c) {
300+
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u000b' || c == '\f';
301+
}
302+
303+
private static boolean isAsciiDigit(char c) {
304+
return c >= '0' && c <= '9';
218305
}
219306

220307
@Override

‎libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import com.github.magnusp.libsql.hrana.codec.Value;
77
import org.junit.jupiter.api.Test;
88

9+
import java.nio.charset.StandardCharsets;
910
import java.sql.SQLException;
1011
import java.util.List;
12+
import java.util.stream.IntStream;
1113

1214
import static org.assertj.core.api.Assertions.assertThat;
1315

@@ -81,4 +83,90 @@ void statementCloseClosesCurrentResultSet() throws SQLException {
8183

8284
assertThat(resultSet.isClosed()).isTrue();
8385
}
86+
87+
@Test
88+
void nonNumericTextCoercesToZero() throws SQLException {
89+
LibsqlResultSet rs = resultSet(new Value.Text("abc"));
90+
assertThat(rs.next()).isTrue();
91+
assertNumeric(rs, 1, 0, 0L, 0.0f, 0.0);
92+
}
93+
94+
@Test
95+
void numericTextCoercesToNumber() throws SQLException {
96+
LibsqlResultSet rs = resultSet(new Value.Text("123"));
97+
assertThat(rs.next()).isTrue();
98+
assertNumeric(rs, 1, 123, 123L, 123.0f, 123.0);
99+
}
100+
101+
@Test
102+
void realTextTruncatesForIntegralGettersAndKeepsFractionForFloatingGetters() throws SQLException {
103+
LibsqlResultSet rs = resultSet(new Value.Text("1.9"));
104+
assertThat(rs.next()).isTrue();
105+
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
106+
}
107+
108+
@Test
109+
void textWithExponentUsesIntegerPrefixForIntegralGetters() throws SQLException {
110+
LibsqlResultSet rs = resultSet(new Value.Text("1.5e2"));
111+
assertThat(rs.next()).isTrue();
112+
assertNumeric(rs, 1, 1, 1L, 150.0f, 150.0);
113+
}
114+
115+
@Test
116+
void textWithLeadingWhitespaceAndTrailingGarbageUsesNumericPrefix() throws SQLException {
117+
LibsqlResultSet rs = resultSet(new Value.Text(" 42xyz"));
118+
assertThat(rs.next()).isTrue();
119+
assertNumeric(rs, 1, 42, 42L, 42.0f, 42.0);
120+
}
121+
122+
@Test
123+
void numericBlobIsInterpretedAsText() throws SQLException {
124+
LibsqlResultSet rs = resultSet(new Value.Blob("123".getBytes(StandardCharsets.UTF_8)));
125+
assertThat(rs.next()).isTrue();
126+
assertNumeric(rs, 1, 123, 123L, 123.0f, 123.0);
127+
}
128+
129+
@Test
130+
void realTextBlobIsInterpretedAsText() throws SQLException {
131+
LibsqlResultSet rs = resultSet(new Value.Blob("1.9".getBytes(StandardCharsets.UTF_8)));
132+
assertThat(rs.next()).isTrue();
133+
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
134+
}
135+
136+
@Test
137+
void nonNumericBlobCoercesToZero() throws SQLException {
138+
LibsqlResultSet rs = resultSet(new Value.Blob("abc".getBytes(StandardCharsets.UTF_8)));
139+
assertThat(rs.next()).isTrue();
140+
assertNumeric(rs, 1, 0, 0L, 0.0f, 0.0);
141+
}
142+
143+
@Test
144+
void realStorageTruncatesForIntegralGettersAndKeepsFractionForFloatingGetters() throws SQLException {
145+
LibsqlResultSet rs = resultSet(new Value.Float(1.9));
146+
assertThat(rs.next()).isTrue();
147+
assertNumeric(rs, 1, 1, 1L, 1.9f, 1.9);
148+
}
149+
150+
@Test
151+
void integerStorageIsReturnedUnchanged() throws SQLException {
152+
LibsqlResultSet rs = resultSet(new Value.Integer(42));
153+
assertThat(rs.next()).isTrue();
154+
assertNumeric(rs, 1, 42, 42L, 42.0f, 42.0);
155+
}
156+
157+
private static void assertNumeric(LibsqlResultSet rs, int column, int expectedInt, long expectedLong,
158+
float expectedFloat, double expectedDouble) throws SQLException {
159+
assertThat(rs.getInt(column)).isEqualTo(expectedInt);
160+
assertThat(rs.getLong(column)).isEqualTo(expectedLong);
161+
assertThat(rs.getFloat(column)).isEqualTo(expectedFloat);
162+
assertThat(rs.getDouble(column)).isEqualTo(expectedDouble);
163+
}
164+
165+
private static LibsqlResultSet resultSet(Value... values) {
166+
List<Col> cols = IntStream.range(0, values.length)
167+
.mapToObj(i -> new Col("c" + (i + 1), null))
168+
.toList();
169+
StmtResult result = new StmtResult(cols, List.of(new Row(List.of(values))), 1, null);
170+
return new LibsqlResultSet(null, result);
171+
}
84172
}

0 commit comments

Comments
 (0)