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 @@ -36,6 +36,18 @@
import java.util.Locale;
import java.util.Map;

/**
* Result set over a materialized Hrana statement result.
*
* <p>Navigation matches sqlite-jdbc 3.53.4.0 rather than strict JDBC, because the driver is a
* drop-in replacement for that driver. Before the first {@link #next()} call the column getters
* read the first row and {@link #getRow()} returns 0. Column getters after the last row return
* SQL NULL (numeric getters return 0), while {@link #getRow()} keeps returning the number of the
* last row. {@link #isLast()} always throws {@link SQLFeatureNotSupportedException}, matching
* xerial. {@link #wasNull()} reports the last column read and throws while no column has been
* read in the current row, including immediately before the first row and immediately after the
* last row. When labels repeat, {@link #findColumn(String)} returns the first match.
*/
public class LibsqlResultSet implements ResultSet {

private final Statement statement;
Expand All @@ -45,7 +57,7 @@ public class LibsqlResultSet implements ResultSet {
private final LibsqlConnection connection;
private final boolean closeStatementOnClose;
private int currentRowIndex = -1;
private boolean wasNull = false;
private int lastColumnIndex = 0;
private boolean closed = false;
private LibsqlResultSetMetaData metadata;

Expand Down Expand Up @@ -73,18 +85,21 @@ private void checkClosed() throws SQLException {
}
}

private Value valueAtCursor(int columnIndex) {
int rowIndex = currentRowIndex < 0 ? 0 : currentRowIndex;
if (rowIndex >= rows.size()) {
return Value.ofNull();
}
return rows.get(rowIndex).values().get(columnIndex - 1);
}

private Value getCurrentValue(int columnIndex) throws SQLException {
checkClosed();
if (currentRowIndex < 0 || currentRowIndex >= rows.size()) {
throw new SQLException("Cursor is not pointing to a valid row");
}
if (columnIndex < 1 || columnIndex > cols.size()) {
throw new SQLException("Column index out of bounds: " + columnIndex);
}
Row row = rows.get(currentRowIndex);
Value val = row.values().get(columnIndex - 1);
this.wasNull = (val instanceof Value.Null);
return val;
lastColumnIndex = columnIndex;
return valueAtCursor(columnIndex);
}

Value valueForMetadata(int columnIndex) {
Expand All @@ -103,6 +118,10 @@ Value valueForMetadata(int columnIndex) {
@Override
public boolean next() throws SQLException {
checkClosed();
if (rows.isEmpty() || currentRowIndex >= rows.size()) {
return false;
}
lastColumnIndex = -1;
currentRowIndex++;
return currentRowIndex < rows.size();
}
Expand All @@ -123,7 +142,11 @@ public void close() throws SQLException {
@Override
public boolean wasNull() throws SQLException {
checkClosed();
return wasNull;
if (lastColumnIndex < 1 || lastColumnIndex > cols.size()) {
throw new SQLException(
"column " + lastColumnIndex + " out of bounds [1," + cols.size() + "]");
}
return valueAtCursor(lastColumnIndex) instanceof Value.Null;
}

/**
Expand Down Expand Up @@ -682,15 +705,15 @@ public int findColumn(String columnLabel) throws SQLException {
for (int i = 0; i < cols.size(); i++) {
String name = cols.get(i).name();
if (name != null) {
columnNameToIndex.put(name.toLowerCase(Locale.ROOT), i + 1);
columnNameToIndex.putIfAbsent(name.toLowerCase(Locale.ROOT), i + 1);
}
}
}
Integer idx = columnNameToIndex.get(columnLabel.toLowerCase(Locale.ROOT));
if (idx != null) {
return idx;
}
throw new SQLException("Column not found: " + columnLabel);
throw new SQLException("no such column: '" + columnLabel + "'");
}

@Override
Expand Down Expand Up @@ -801,7 +824,7 @@ public int getConcurrency() throws SQLException {

@Override
public int getRow() throws SQLException {
return currentRowIndex >= 0 && currentRowIndex < rows.size() ? currentRowIndex + 1 : 0;
return Math.max(0, Math.min(currentRowIndex + 1, rows.size()));
}

@Override
Expand All @@ -821,7 +844,7 @@ public boolean isFirst() throws SQLException {

@Override
public boolean isLast() throws SQLException {
return currentRowIndex == rows.size() - 1 && !rows.isEmpty();
throw new SQLFeatureNotSupportedException("not supported by sqlite");
}

@Override public InputStream getAsciiStream(int columnIndex) throws SQLException { throw new UnsupportedOperationException(); }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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 java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.List;

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

class LibsqlResultSetNavigationTest {

private static final StmtResult DUPLICATE_LABELS = new StmtResult(
List.of(new Col("a", null), new Col("a", null), new Col("n", null)),
List.of(new Row(List.of(Value.of(1L), Value.of(2L), Value.ofNull()))),
1,
null);

private static final StmtResult TWO_ROWS = new StmtResult(
List.of(new Col("value", null)),
List.of(new Row(List.of(Value.of(1L))), new Row(List.of(Value.of(2L)))),
2,
null);

private static final StmtResult NO_ROWS = new StmtResult(
List.of(new Col("value", null)),
List.of(),
0,
null);

@Test
void findColumnReturnsTheFirstMatchingDuplicateLabel() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);

assertThat(resultSet.findColumn("a")).isEqualTo(1);
assertThat(resultSet.findColumn("A")).isEqualTo(1);
}

@Test
void findColumnRejectsUnknownLabelsWithXerialMessage() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);

assertThatThrownBy(() -> resultSet.findColumn("missing"))
.isInstanceOf(SQLException.class)
.hasMessage("no such column: 'missing'");
}

@Test
void gettersReadTheFirstRowBeforeNext() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);

assertThat(resultSet.getInt(1)).isEqualTo(1);
assertThat(resultSet.getString(1)).isEqualTo("1");
assertThat(resultSet.getObject(2)).isEqualTo(2);
}

@Test
void gettersReturnNullAndZeroAfterTheLastRow() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.next()).isFalse();

assertThat(resultSet.getInt(1)).isZero();
assertThat(resultSet.getString(1)).isNull();
assertThat(resultSet.getObject(1)).isNull();
assertThat(resultSet.getBytes(1)).isNull();
assertThat(resultSet.getBoolean(1)).isFalse();
}

@Test
void gettersReturnNullAndZeroWhenTheResultSetHasNoRows() throws SQLException {
LibsqlResultSet resultSet = resultSet(NO_ROWS);

assertThat(resultSet.getInt(1)).isZero();
assertThat(resultSet.getString(1)).isNull();
assertThat(resultSet.getObject(1)).isNull();
}

@Test
void getRowIsZeroBeforeTheFirstRowAndStaysOnTheLastRowAfterTheEnd() throws SQLException {
LibsqlResultSet resultSet = resultSet(TWO_ROWS);

assertThat(resultSet.getRow()).isZero();
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getRow()).isEqualTo(1);
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getRow()).isEqualTo(2);
assertThat(resultSet.next()).isFalse();
assertThat(resultSet.getRow()).isEqualTo(2);
assertThat(resultSet.next()).isFalse();
assertThat(resultSet.getRow()).isEqualTo(2);
}

@Test
void getRowIsZeroForAnEmptyResultSet() throws SQLException {
LibsqlResultSet resultSet = resultSet(NO_ROWS);

assertThat(resultSet.getRow()).isZero();
assertThat(resultSet.next()).isFalse();
assertThat(resultSet.getRow()).isZero();
}

@Test
void isLastAlwaysThrowsLikeXerial() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);

assertThatThrownBy(resultSet::isLast)
.isInstanceOf(SQLFeatureNotSupportedException.class)
.hasMessage("not supported by sqlite");
assertThat(resultSet.next()).isTrue();
assertThatThrownBy(resultSet::isLast)
.isInstanceOf(SQLFeatureNotSupportedException.class)
.hasMessage("not supported by sqlite");
}

@Test
void wasNullThrowsBeforeAnyGetterAndAfterTheEndWithoutAGetter() throws SQLException {
LibsqlResultSet beforeFirst = resultSet(DUPLICATE_LABELS);
assertThatThrownBy(beforeFirst::wasNull)
.isInstanceOf(SQLException.class)
.hasMessage("column 0 out of bounds [1,3]");

LibsqlResultSet afterLast = resultSet(DUPLICATE_LABELS);
assertThat(afterLast.next()).isTrue();
assertThat(afterLast.next()).isFalse();
assertThatThrownBy(afterLast::wasNull)
.isInstanceOf(SQLException.class)
.hasMessage("column -1 out of bounds [1,3]");
}

@Test
void wasNullTracksTheLastGetterRead() throws SQLException {
LibsqlResultSet resultSet = resultSet(DUPLICATE_LABELS);

assertThat(resultSet.getInt(1)).isEqualTo(1);
assertThat(resultSet.wasNull()).isFalse();

assertThat(resultSet.getObject(3)).isNull();
assertThat(resultSet.wasNull()).isTrue();

assertThat(resultSet.next()).isTrue();
assertThat(resultSet.next()).isFalse();
assertThat(resultSet.getObject(1)).isNull();
assertThat(resultSet.wasNull()).isTrue();
}

private static LibsqlResultSet resultSet(StmtResult result) {
return new LibsqlResultSet(null, result);
}
}
Loading