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
6 changes: 6 additions & 0 deletions libsql-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.github.magnusp.libsql.jdbc.flyway;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

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

/**
* Pins the Flyway-facing gaps the smoke test found.
*
* <p>These assertions describe current behavior, not desired behavior. If a test
* starts failing, the driver improved and the workaround in the assertion message
* can be removed.
*/
class FlywayKnownGapsIT extends FlywaySqldSupport {

private static Flyway flyway() {
return Flyway.configure()
.dataSource(dataSource())
.locations("classpath:flyway/migration")
.cleanDisabled(false)
.load();
}

@BeforeEach
void emptyDatabase() throws Exception {
resetDatabase();
}

@Test
void libsqlUrlIsNotRecognizedByFlyway() {
assertThatThrownBy(() -> Flyway.configure()
.dataSource(jdbcUrl(), "", "")
.load()
.migrate())
.as("Flyway 13's SQLite plugin claims only jdbc:sqlite:, jdbc:sqldroid: and"
+ " p6spy prefixes. Use the jdbc:sqlite: alias or pass a DataSource.")
.isInstanceOf(FlywayException.class)
.hasMessageContaining("No Flyway database plugin found to handle")
.hasMessageContaining("jdbc:libsql:");
}

@Test
void sqliteAliasUrlCannotUseTheLibsqlDriver() {
assertThatThrownBy(() -> Flyway.configure()
.driver("io.github.magnusp.libsql.jdbc.LibsqlDriver")
.dataSource("jdbc:sqlite:" + httpUrl(), "", "")
.load()
.migrate())
.as("Flyway 13.12 builds its URL-based DriverDataSource eagerly and passes a"
+ " null driver class, so driver(...) is ignored for this path. Flyway"
+ " falls back to org.sqlite.JDBC, which is not on the classpath."
+ " Pass a DataSource instead.")
.isInstanceOf(FlywayException.class)
.hasMessageContaining("No JDBC driver found to handle");
}

@Test
void productNameStartsWithSqliteSoProductDetectionWorks() throws SQLException {
try (Connection connection = connect()) {
assertThat(connection.getMetaData().getDatabaseProductName())
.as("Flyway matches SQLite with startsWith(\"SQLite\"), so the suffix is safe")
.startsWith("SQLite");
assertThat(connection.getMetaData().getDatabaseMajorVersion()).isNotZero();
}
}

@Test
void cleanIsBlockedUntilMultiStatementSupportLands() throws Exception {
Flyway flyway = flyway();
flyway.migrate();

assertThat(scalarInt("PRAGMA foreign_keys"))
.as("sqld enables foreign keys on a fresh connection, unlike vanilla SQLite")
.isEqualTo(1);

// The decision is to support clean(); see #258. This pin flips when
// multi-statement PRAGMA/DROP scripts are supported.
assertThatThrownBy(flyway::clean)
.as("Flyway sees foreign keys enabled and sends"
+ " 'PRAGMA foreign_keys = OFF; DROP TABLE ...; PRAGMA foreign_keys = ON'"
+ " as one statement. sqld rejects that string, so clean cannot finish."
+ " Supporting clean() is tracked in #258.")
.isInstanceOf(FlywayException.class)
.hasMessageContaining("Unable to drop")
.hasStackTraceContaining("SQL string contains more than one statement");

assertThat(userTables())
.as("clean stopped at the first table")
.contains("author");
}

@Test
void autoCommitStatementsDoNotShareSessionState() throws SQLException {
try (Connection connection = connect(); Statement statement = connection.createStatement()) {
statement.execute("PRAGMA foreign_keys = OFF");

try (ResultSet resultSet = statement.executeQuery("PRAGMA foreign_keys")) {
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getInt(1))
.as("each auto-commit statement runs as a one-shot Hrana stream, so"
+ " connection-scoped state set by PRAGMA is gone by the next"
+ " statement. Wrap related statements in a transaction.")
.isEqualTo(1);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package io.github.magnusp.libsql.jdbc.flyway;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.Location;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.configuration.ClassicConfiguration;
import org.flywaydb.core.api.output.MigrateResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

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

/**
* End-to-end Flyway probes. Every migration below travels through the libSQL JDBC
* driver to a real sqld container.
*
* <p>Flyway owns the schema, so these tests cover the driver paths Flyway depends
* on: multi-statement migration scripts, DDL inside a transaction, the schema
* history table and its timestamp column, baseline, and repeatable migrations.
*
* <p>Two Flyway-facing limitations are pinned in {@link FlywayKnownGapsIT}.
*/
class FlywayMigrationIT extends FlywaySqldSupport {

private static Flyway flyway(String... locations) {
return Flyway.configure()
.dataSource(dataSource())
.locations(locations.length == 0 ? new String[] {"classpath:flyway/migration"} : locations)
.cleanDisabled(false)
.load();
}

@BeforeEach
void emptyDatabase() throws Exception {
resetDatabase();
}

@Test
void migratesThroughADataSource() throws Exception {
Flyway flyway = flyway();

MigrateResult result = flyway.migrate();

assertThat(result.migrationsExecuted)
.as("V1, V2, V3 and the repeatable view")
.isEqualTo(4);
assertThat(userTables()).contains("author", "book", "flyway_schema_history");

assertThat(scalarInt("SELECT count(*) FROM author")).isEqualTo(2);
assertThat(scalarInt("SELECT count(*) FROM book")).isEqualTo(2);
assertThat(scalarInt("SELECT book_count FROM author_summary WHERE name = 'Ursula K. Le Guin'"))
.isEqualTo(1);

assertThat(flyway.info().applied())
.extracting(info -> info.getVersion() == null ? "repeatable" : info.getVersion().toString())
.containsExactly("1", "2", "3", "repeatable");
assertThat(flyway.info().current().getState().isApplied()).isTrue();
assertThat(flyway.info().pending()).isEmpty();

flyway.validate();
}

@Test
void secondMigrateIsANoOp() {
Flyway flyway = flyway();
flyway.migrate();

assertThat(flyway.migrate().migrationsExecuted).isZero();
}

@Test
void schemaHistoryIsReadableThroughTheDriver() throws Exception {
flyway().migrate();

try (Connection connection = connect();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(
"SELECT installed_rank, version, description, type, success, installed_on"
+ " FROM flyway_schema_history ORDER BY installed_rank")) {
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getInt("installed_rank")).isEqualTo(1);
assertThat(resultSet.getString("version")).isEqualTo("1");
assertThat(resultSet.getString("description")).isEqualTo("create author");
assertThat(resultSet.getString("type")).isEqualTo("SQL");
assertThat(resultSet.getBoolean("success")).isTrue();
assertThat(resultSet.getTimestamp("installed_on"))
.as("installed_on is TEXT with a strftime() default")
.isNotNull();
assertThat(resultSet.wasNull()).isFalse();
}
}

@Test
void migrateWithASqliteAliasUrlAndADataSource() throws Exception {
// jdbc:sqlite: is the only prefix Flyway's SQLite plugin accepts. Passing a
// DataSource still avoids its URL-based driver pick, which hardcodes
// org.sqlite.JDBC. This proves the alias itself works through the driver.
Flyway flyway = Flyway.configure()
.dataSource(new FlywaySqldSupport.DriverManagerDataSource(
"jdbc:sqlite:" + httpUrl()))
.locations("classpath:flyway/migration")
.load();

MigrateResult result = flyway.migrate();

assertThat(result.migrationsExecuted).isEqualTo(4);
assertThat(scalarInt("SELECT count(*) FROM author")).isEqualTo(2);
}

@Test
void migrateWithUrlConfigurationAndExplicitDriver() throws Exception {
// This mirrors the Maven plugin / flyway.properties path: url(...) plus an
// explicit driver is honored, because unlike dataSource(url) it does not
// build the DriverDataSource eagerly with a null driver class. The URL
// still has to use the jdbc:sqlite: alias that Flyway's plugin claims.
ClassicConfiguration configuration = new ClassicConfiguration();
configuration.setUrl("jdbc:sqlite:" + httpUrl());
configuration.setDriver("io.github.magnusp.libsql.jdbc.LibsqlDriver");
configuration.setLocations(new Location("classpath:flyway/migration"));

Check warning on line 124 in libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/flyway/FlywayMigrationIT.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 25)

Location(java.lang.String) in org.flywaydb.core.api.Location has been deprecated

Check warning on line 124 in libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/flyway/FlywayMigrationIT.java

View workflow job for this annotation

GitHub Actions / Build & Test (Java 26)

Location(java.lang.String) in org.flywaydb.core.api.Location has been deprecated

Check warning on line 124 in libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/flyway/FlywayMigrationIT.java

View workflow job for this annotation

GitHub Actions / Xerial Control (reference probe)

Location(java.lang.String) in org.flywaydb.core.api.Location has been deprecated
Flyway flyway = new Flyway(configuration);

assertThat(flyway.migrate().migrationsExecuted).isEqualTo(4);
assertThat(scalarInt("SELECT count(*) FROM author")).isEqualTo(2);
}

@Test
void failedMigrationRollsBackItsDdl() throws Exception {
Flyway flyway = flyway("classpath:flyway/broken");

assertThatThrownBy(flyway::migrate)
.as("V2 inserts into a table that does not exist")
.hasStackTraceContaining("table_that_does_not_exist");

assertThat(tableExists("migration_probe"))
.as("V1 committed")
.isTrue();
assertThat(tableExists("partial_table"))
.as("V2's CREATE TABLE must roll back with the failed insert")
.isFalse();

assertThat(flyway.info().applied())
.extracting(info -> info.getVersion().toString())
.containsExactly("1");
}

@Test
void baselineAnExistingSchemaThenMigrate() throws Exception {
try (Connection connection = connect(); Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE author ("
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ " name TEXT NOT NULL,"
+ " country TEXT)");
statement.execute("CREATE INDEX author_name_idx ON author (name)");
}

Flyway flyway = flyway();
flyway.baseline();

MigrateResult result = flyway.migrate();

assertThat(result.migrationsExecuted)
.as("V1 is skipped by the baseline; V2, V3 and the repeatable view run")
.isEqualTo(3);
assertThat(userTables()).contains("author", "book", "flyway_schema_history");
assertThat(scalarInt("SELECT count(*) FROM author")).isEqualTo(2);

MigrationInfo baseline = flyway.info().applied()[0];
assertThat(baseline.getVersion().toString()).isEqualTo("1");
assertThat(baseline.getType().name()).isEqualTo("BASELINE");
}
}
Loading
Loading