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 @@ -54,6 +54,34 @@ void resolvesTableNameNullabilityAndAutoIncrement() throws SQLException {
assertThat(metadata.isAutoIncrement(2)).isFalse();
}

@Test
void resolvesAttachedTableMetadataThroughPragmaDatabaseList() throws SQLException {
LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData(
List.of(new Col("id", "INTEGER"), new Col("name", "TEXT")),
List.of(),
"SELECT id, name FROM aux.People",
connectionForAttachedSchema("main", "temp", "Aux"));

assertThat(metadata.getTableName(1)).isEqualTo("People");
assertThat(metadata.getTableName(2)).isEqualTo("People");
assertThat(metadata.isNullable(1)).isEqualTo(ResultSetMetaData.columnNoNulls);
assertThat(metadata.isNullable(2)).isEqualTo(ResultSetMetaData.columnNoNulls);
assertThat(metadata.isAutoIncrement(1)).isTrue();
assertThat(metadata.isAutoIncrement(2)).isFalse();
}

@Test
void reportsNoTableMetadataForASchemaMissingFromPragmaDatabaseList() throws SQLException {
LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData(
List.of(new Col("id", "INTEGER")),
List.of(),
"SELECT id FROM ghost.People",
connectionForAttachedSchema("main", "temp", "Aux"));

assertThat(metadata.getTableName(1)).isEmpty();
assertThat(metadata.isNullable(1)).isEqualTo(ResultSetMetaData.columnNullableUnknown);
}

@Test
void resolvesNonAsciiTableName() throws SQLException {
LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData(
Expand Down Expand Up @@ -752,4 +780,59 @@ public StmtResult executeStatement(Stmt stmt) throws SQLException {
}
};
}

/**
* Answers {@code pragma_database_list} with the given schema names and every per-schema table
* lookup from a fixed catalogue, so the caller sees metadata only for schemas that the database
* list actually reports.
*/
private static LibsqlConnection connectionForAttachedSchema(String... databaseNames) {
Map<String, FakeTable> tables = new LinkedHashMap<>();
tables.put(
"Aux",
new FakeTable(
"People",
"CREATE TABLE People (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
List.of(new FakeColumn("id", true, true), new FakeColumn("name", true, false))));
tables.put(
"main",
new FakeTable(
"People",
"CREATE TABLE People (id INTEGER, name TEXT)",
List.of(new FakeColumn("id", false, false), new FakeColumn("name", false, false))));
tables.put(
"ghost",
new FakeTable(
"People",
"CREATE TABLE People (id INTEGER)",
List.of(new FakeColumn("id", false, false))));
List<Row> databases = new ArrayList<>();
for (String name : databaseNames) {
databases.add(new Row(List.of(Value.of(name))));
}
StmtResult databaseList = new StmtResult(List.of(), databases, 0, null);

return new LibsqlConnection(null, null) {
@Override
public StmtResult executeStatement(Stmt stmt) throws SQLException {
if (stmt.sql().contains("pragma_database_list")) {
return databaseList;
}
FakeTable table = tables.get(argument(stmt, 1));
if (table == null) {
return new StmtResult(List.of(), List.of(), 0, null);
}
List<Row> rows = new ArrayList<>();
for (FakeColumn column : table.columns()) {
rows.add(new Row(List.of(
Value.of(table.name()),
Value.of(table.sql()),
Value.of(column.name()),
Value.of(column.notNull() ? 1L : 0L),
Value.of(column.primaryKey() ? 1L : 0L))));
}
return new StmtResult(List.of(), rows, 0, null);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -276,6 +277,63 @@
assertDateTimeGetters(rs, 1_704_153_600_000L);
}

@Test
void textDateTimeWithAnOffsetSuffixIsConvertedToTheSystemZone() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("2024-03-15 10:30:00+02:00"));
assertThat(rs.next()).isTrue();

LocalDateTime expected = LocalDateTime.of(2024, 3, 15, 10, 30)
.atOffset(ZoneOffset.ofHours(2))
.atZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime();

assertThat(rs.getObject(1, LocalDateTime.class)).isEqualTo(expected);
assertThat(rs.getObject(1, LocalDate.class)).isEqualTo(expected.toLocalDate());
assertThat(rs.getObject(1, LocalTime.class)).isEqualTo(expected.toLocalTime());
}

@Test
void textDateTimeWithAZSuffixIsReadAsUtc() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("2024-03-15T10:30:00Z"));
assertThat(rs.next()).isTrue();

LocalDateTime expected = LocalDateTime.of(2024, 3, 15, 10, 30)
.atOffset(ZoneOffset.UTC)
.atZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime();

assertThat(rs.getObject(1, LocalDateTime.class)).isEqualTo(expected);
assertThat(rs.getObject(1, LocalDate.class)).isEqualTo(expected.toLocalDate());
assertThat(rs.getObject(1, LocalTime.class)).isEqualTo(expected.toLocalTime());
}

@Test
void bareTimeTextGetsTheYear2000DefaultDate() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Text("10:30:45"));
assertThat(rs.next()).isTrue();

assertThat(rs.getObject(1, LocalDateTime.class))
.isEqualTo(LocalDateTime.of(2000, 1, 1, 10, 30, 45));
assertThat(rs.getObject(1, LocalDate.class)).isEqualTo(LocalDate.of(2000, 1, 1));
assertThat(rs.getObject(1, LocalTime.class)).isEqualTo(LocalTime.of(10, 30, 45));
}

@Test
void blobRejectsDateAndTimeGettersWithTheTargetTypeName() throws SQLException {
LibsqlResultSet rs = resultSet(new Value.Blob(new byte[] {1, 2, 3}));
assertThat(rs.next()).isTrue();

assertThatThrownBy(() -> rs.getObject(1, LocalDate.class))
.isInstanceOf(SQLException.class)
.hasMessage("Bad value for type LocalDate");
assertThatThrownBy(() -> rs.getObject(1, LocalTime.class))
.isInstanceOf(SQLException.class)
.hasMessage("Bad value for type LocalTime");
assertThatThrownBy(() -> rs.getObject(1, LocalDateTime.class))
.isInstanceOf(SQLException.class)
.hasMessage("Bad value for type LocalDateTime");
}

@Test
void binaryStreamContentMatchesGetBytesByteForByte() throws Exception {
byte[] payload = {0, 1, 2, 65, 127, -128, -1};
Expand Down Expand Up @@ -378,8 +436,8 @@
List<UnsupportedCall> calls = List.of(
new UnsupportedCall("getAsciiStream(int)", () -> rs.getAsciiStream(1)),
new UnsupportedCall("getAsciiStream(String)", () -> rs.getAsciiStream("c1")),
new UnsupportedCall("getUnicodeStream(int)", () -> rs.getUnicodeStream(1)),

Check warning on line 439 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Xerial Control (reference probe)

getUnicodeStream(int) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated

Check warning on line 439 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 25)

getUnicodeStream(int) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated

Check warning on line 439 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 26)

getUnicodeStream(int) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated
new UnsupportedCall("getUnicodeStream(String)", () -> rs.getUnicodeStream("c1")),

Check warning on line 440 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Xerial Control (reference probe)

getUnicodeStream(java.lang.String) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated

Check warning on line 440 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 25)

getUnicodeStream(java.lang.String) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated

Check warning on line 440 in libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 26)

getUnicodeStream(java.lang.String) in com.github.magnusp.libsql.jdbc.LibsqlResultSet has been deprecated
new UnsupportedCall("getRef(int)", () -> rs.getRef(1)),
new UnsupportedCall("getRef(String)", () -> rs.getRef("c1")),
new UnsupportedCall("getArray(int)", () -> rs.getArray(1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,52 @@ void releasingTheOutermostSavepointInsideAnExplicitBeginKeepsTheTransaction()
"INSERT INTO sample VALUES (2)");
}

@Test
void releasingTheLaterOfTwoSavepointsWithTheSameNameKeepsTheEarlierOneActive()
throws SQLException {
ScriptedClient client = successClient();
LibsqlConnection connection = newConnection(client);
Savepoint first = connection.setSavepoint("dup");
Savepoint second = connection.setSavepoint("dup");

connection.releaseSavepoint(second);
connection.executeStatement(new Stmt("INSERT INTO sample VALUES (1)"));
connection.rollback(first);
connection.executeStatement(new Stmt("INSERT INTO sample VALUES (2)"));

assertThat(client.executedStatements())
.containsExactly(
"SAVEPOINT \"dup\"",
"SAVEPOINT \"dup\"",
"RELEASE SAVEPOINT \"dup\"",
"INSERT INTO sample VALUES (1)",
"ROLLBACK TO SAVEPOINT \"dup\"",
"INSERT INTO sample VALUES (2)");
assertThat(client.requests.stream()
.flatMap(request -> request.requests().stream())
.filter(StreamRequest.Close.class::isInstance))
.isEmpty();
}

@Test
void releasingTheOnlySavepointOfAnAutoCommitTransactionLeavesCommitWithNothingToSend()
throws SQLException {
ScriptedClient client = successClient();
LibsqlConnection connection = newConnection(client);
Savepoint savepoint = connection.setSavepoint("only");

connection.releaseSavepoint(savepoint);
connection.commit();

assertThat(connection.getAutoCommit()).isFalse();
assertThat(client.executedStatements())
.containsExactly("SAVEPOINT \"only\"", "RELEASE SAVEPOINT \"only\"");
assertThat(client.requests.stream()
.flatMap(request -> request.requests().stream())
.filter(StreamRequest.Close.class::isInstance))
.hasSize(1);
}

@Test
void rollbackAndReleaseThrowInAutoCommitModeWithoutSendingAnything() throws SQLException {
ScriptedClient client = successClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.concurrent.atomic.AtomicReference;

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

class LibsqlStatementConcurrencyTest {

Expand Down Expand Up @@ -181,6 +182,32 @@ void closeDoesNotInterleaveWithExecuteAndLeavesNoStreamOpen() throws Exception {
}
}

@Test
void executeBatchThatLosesTheRaceToCloseFailsWithStatementClosed() throws Exception {
Pause pause = new Pause();
LibsqlStatement statement = new LibsqlStatement(new PausingBatchConnection(pause));
statement.addBatch("insert into t(value) values (1)");
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<int[]> batch = executor.submit(statement::executeBatch);

assertThat(pause.awaitReached()).isTrue();
AtomicReference<Throwable> closeFailure = new AtomicReference<>();
joinAndCheck(closeInThread(statement, closeFailure), closeFailure);
assertThat(statement.isClosed()).isTrue();
pause.resume();

assertThatThrownBy(() -> batch.get(5, TimeUnit.SECONDS))
.isInstanceOf(ExecutionException.class)
.cause()
.isInstanceOf(SQLException.class)
.hasMessageContaining("Statement is closed");
} finally {
pause.resume();
executor.shutdownNow();
}
}

private static Void executeRepeatedly(LibsqlStatement statement, int count) throws SQLException {
for (int i = 0; i < count; i++) {
statement.execute("SELECT " + i);
Expand Down Expand Up @@ -300,6 +327,27 @@ protected void beforeResultSetAssignment() throws SQLException {
}
}

/**
* Holds the batch inside the connection round trip so a close can land before the statement
* reaches its own cleanup.
*/
private static final class PausingBatchConnection extends LibsqlConnection {
private final Pause pause;

private PausingBatchConnection(Pause pause) {
super(null, null);
this.pause = pause;
}

@Override
public List<StmtResult> executeBatchResults(List<Stmt> statements) throws SQLException {
pause.await();
return statements.stream()
.map(statement -> new StmtResult(List.of(), List.of(), 1, null))
.toList();
}
}

/**
* Models the baton rules the connection must uphold: one open stream per connection, one
* request on a stream at a time, and a baton that is consumed by its request. Every rule break
Expand Down
Loading