Skip to content

Commit bdff98b

Browse files
authored
fix(jdbc): make getColumnClassName follow the current row (#66)
Closes #36. Part of #54. ## What changed - `LibsqlResultSetMetaData.getColumnClassName` now reads the value at the metadata cursor through the shared `LibsqlResultSet.valueForMetadata` accessor added by #35, instead of scanning every row for a column-wide class. - A valid current row reports the current storage class: INTEGER in the int range is `java.lang.Integer`, INTEGER outside it is `java.lang.Long`, FLOAT is `java.lang.Double`, TEXT is `java.lang.String`, and BLOB/NULL are `java.lang.Object`. - Empty results and positions after the last row report `java.lang.Object`. - Removed the now-unused cross-row `integerClassName` and `isIntInRange` helpers. - Added behavior tests for an empty result and for a mixed-type column (`select 1 union all select 'a'`) before cursor movement, on each row, and after the last row, plus a current-row Integer-to-Long range check. ## Contract decision Class metadata is cursor-local, matching xerial 3.53.4.0 `JDBC3ResultSet.getColumnClassName` (`JDBC3ResultSet.java:579-597`). Empty results and end-of-results report `java.lang.Object`; otherwise the class describes the value on the current row. Parity wins under #54. One Steering detail does not match the pinned xerial behavior. Steering says positions before the first row report `java.lang.Object`. I probed xerial 3.53.4.0 directly: it preloads the first row during execute, so before the first `next()` it reports that row's class (`Integer`/`String` in a newly executed result set), not `Object`. The vendored `RSMetaDataTest.testGetColumnClassName` asserts that preloaded behavior, so returning `Object` before the first row would break the conformance suite. This PR therefore follows the actual pinned xerial behavior, which is the primary contract, and keeps conformance green. ## Evidence Failing before the fix: ```text $ ./mvnw -q -pl libsql-jdbc -am -Dtest=LibsqlResultSetMetaDataTest -Dsurefire.failIfNoSpecifiedTests=false test [ERROR] Tests run: 16, Failures: 3, Errors: 0, Skipped: 0 [ERROR] LibsqlResultSetMetaDataTest.reportsObjectClassNameForEmptyResultSet:229 [column 1 before the first row] expected: "java.lang.Object" but was: "java.lang.Integer" [ERROR] LibsqlResultSetMetaDataTest.reportsCurrentRowClassNameForMixedTypeColumn:253 [before the first row] expected: "java.lang.Integer" but was: "java.lang.String" [ERROR] LibsqlResultSetMetaDataTest.reportsClassNameForCurrentRowOutsideIntRange:282 expected: "java.lang.Integer" but was: "java.lang.Long" ``` Passing after the fix: ```text $ ./mvnw -pl libsql-jdbc -am test Tests run: 83, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` ```text $ ./mvnw -pl libsql-conformance -am verify Tests run: 115, Failures: 0, Errors: 0, Skipped: 1 BUILD SUCCESS ``` The conformance run includes the vendored xerial `RSMetaDataTest` (13 tests, all passed), confirming the before-first-row and mixed-column behavior against live sqld. ```text $ ./mvnw spotless:apply BUILD SUCCESS (no formatting changes) ``` ## Deviations None from xerial 3.53.4.0. The literal Steering sentence about `Object` before the first row is not what the pinned xerial does; see Contract decision. Following it would regress the vendored conformance test, so the PR keeps actual xerial parity instead.
1 parent 486a6f4 commit bdff98b

2 files changed

Lines changed: 89 additions & 51 deletions

File tree

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

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -342,58 +342,23 @@ public boolean isDefinitelyWritable(int column) throws SQLException {
342342
@Override
343343
public String getColumnClassName(int column) throws SQLException {
344344
getCol(column);
345-
Value sample = sampleValue(column);
346-
if (sample != null) {
347-
return switch (sample) {
348-
case Value.Integer i -> integerClassName(column);
349-
case Value.Float f -> Double.class.getName();
350-
case Value.Text t -> String.class.getName();
351-
case Value.Blob b -> Object.class.getName();
352-
case Value.Null n -> Object.class.getName();
353-
};
354-
}
355-
if (!rows.isEmpty()) {
356-
return Object.class.getName();
357-
}
358-
return switch (getColumnType(column)) {
359-
case Types.TINYINT, Types.SMALLINT, Types.INTEGER -> Integer.class.getName();
360-
case Types.BIGINT -> Long.class.getName();
361-
case Types.FLOAT, Types.REAL, Types.DOUBLE, Types.NUMERIC, Types.DECIMAL -> Double.class.getName();
362-
case Types.CHAR, Types.VARCHAR, Types.CLOB -> String.class.getName();
363-
case Types.BOOLEAN -> Boolean.class.getName();
364-
default -> Object.class.getName();
365-
};
345+
return className(runtimeValue(column));
366346
}
367347

368-
private String integerClassName(int column) {
369-
Value firstNonInteger = null;
370-
boolean outsideIntRange = false;
371-
for (Row row : rows) {
372-
List<Value> values = row.values();
373-
if (values == null || column < 1 || column > values.size()) {
374-
continue;
375-
}
376-
Value value = values.get(column - 1);
377-
if (value instanceof Value.Integer) {
378-
if (!isIntInRange(value)) {
379-
outsideIntRange = true;
380-
}
381-
} else if (value != null
382-
&& !(value instanceof Value.Null)
383-
&& firstNonInteger == null) {
384-
firstNonInteger = value;
385-
}
348+
private static String className(Value value) {
349+
if (value instanceof Value.Integer i) {
350+
long val = i.value();
351+
return val > Integer.MAX_VALUE || val < Integer.MIN_VALUE
352+
? Long.class.getName()
353+
: Integer.class.getName();
386354
}
387-
if (firstNonInteger instanceof Value.Float) {
355+
if (value instanceof Value.Float) {
388356
return Double.class.getName();
389357
}
390-
if (firstNonInteger instanceof Value.Text) {
358+
if (value instanceof Value.Text) {
391359
return String.class.getName();
392360
}
393-
if (firstNonInteger != null) {
394-
return Object.class.getName();
395-
}
396-
return outsideIntRange ? Long.class.getName() : Integer.class.getName();
361+
return Object.class.getName();
397362
}
398363

399364
private String declType(int column) throws SQLException {
@@ -458,12 +423,6 @@ private static SampleType storageType(Value value) {
458423
return SampleType.NULL;
459424
}
460425

461-
private static boolean isIntInRange(Value value) {
462-
return value instanceof Value.Integer i
463-
&& i.value() >= Integer.MIN_VALUE
464-
&& i.value() <= Integer.MAX_VALUE;
465-
}
466-
467426
private TableInfo resolveTableContext() throws SQLException {
468427
if (!tableContextResolved) {
469428
TableInfo resolved = null;

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,85 @@ void usesDeclaredTypesForEmptyResultSet() throws SQLException {
206206
assertDeclaredTypes(metadata);
207207
}
208208

209+
@Test
210+
void reportsObjectClassNameForEmptyResultSet() throws SQLException {
211+
StmtResult result = new StmtResult(
212+
List.of(
213+
new Col("i", "INTEGER"),
214+
new Col("big", "BIGINT"),
215+
new Col("txt", "TEXT"),
216+
new Col("b", "BLOB"),
217+
new Col("flag", "BOOLEAN"),
218+
new Col("real", "REAL"),
219+
new Col("untyped", null)),
220+
List.of(),
221+
0,
222+
null);
223+
LibsqlResultSet resultSet = new LibsqlResultSet(null, result);
224+
ResultSetMetaData metadata = resultSet.getMetaData();
225+
226+
for (int column = 1; column <= metadata.getColumnCount(); column++) {
227+
assertThat(metadata.getColumnClassName(column))
228+
.as("column %d before the first row", column)
229+
.isEqualTo(Object.class.getName());
230+
}
231+
232+
assertThat(resultSet.next()).isFalse();
233+
234+
for (int column = 1; column <= metadata.getColumnCount(); column++) {
235+
assertThat(metadata.getColumnClassName(column))
236+
.as("column %d after the last row", column)
237+
.isEqualTo(Object.class.getName());
238+
}
239+
}
240+
241+
@Test
242+
void reportsCurrentRowClassNameForMixedTypeColumn() throws SQLException {
243+
StmtResult result = new StmtResult(
244+
List.of(new Col("x", null)),
245+
List.of(new Row(List.of(Value.of(1L))), new Row(List.of(Value.of("a")))),
246+
0,
247+
null);
248+
LibsqlResultSet resultSet = new LibsqlResultSet(null, result);
249+
ResultSetMetaData metadata = resultSet.getMetaData();
250+
251+
assertThat(metadata.getColumnClassName(1))
252+
.as("before the first row")
253+
.isEqualTo(Integer.class.getName());
254+
255+
assertThat(resultSet.next()).isTrue();
256+
assertThat(metadata.getColumnClassName(1))
257+
.as("on the integer row")
258+
.isEqualTo(Integer.class.getName());
259+
260+
assertThat(resultSet.next()).isTrue();
261+
assertThat(metadata.getColumnClassName(1))
262+
.as("on the text row")
263+
.isEqualTo(String.class.getName());
264+
265+
assertThat(resultSet.next()).isFalse();
266+
assertThat(metadata.getColumnClassName(1))
267+
.as("after the last row")
268+
.isEqualTo(Object.class.getName());
269+
}
270+
271+
@Test
272+
void reportsClassNameForCurrentRowOutsideIntRange() throws SQLException {
273+
StmtResult result = new StmtResult(
274+
List.of(new Col("x", null)),
275+
List.of(new Row(List.of(Value.of(1L))), new Row(List.of(Value.of(5_000_000_000L)))),
276+
0,
277+
null);
278+
LibsqlResultSet resultSet = new LibsqlResultSet(null, result);
279+
ResultSetMetaData metadata = resultSet.getMetaData();
280+
281+
assertThat(resultSet.next()).isTrue();
282+
assertThat(metadata.getColumnClassName(1)).isEqualTo(Integer.class.getName());
283+
284+
assertThat(resultSet.next()).isTrue();
285+
assertThat(metadata.getColumnClassName(1)).isEqualTo(Long.class.getName());
286+
}
287+
209288
@Test
210289
void reflectsStorageClassChangesBetweenRows() throws SQLException {
211290
StmtResult result = new StmtResult(

0 commit comments

Comments
 (0)