diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..b5bac53 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7714520..869f814 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ UTF-8 + 2.2.0 @@ -24,6 +25,70 @@ 21 + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + src/main/java + target/generated-sources/annotations + + + + + test-compile + test-compile + + test-compile + + + + src/test/java + target/generated-test-sources/test-annotations + + + + + + 1.8 + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + @@ -69,6 +134,17 @@ 2.0.16 + + org.jetbrains.kotlin + kotlin-scripting-jvm + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-scripting-common + ${kotlin.version} + + @@ -78,5 +154,24 @@ 5.13.4 test + + + + com.h2database + h2 + 2.3.230 + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + diff --git a/src/main/java/dev/mduchene/App.java b/src/main/java/dev/mduchene/App.java index c3fcc3d..b780084 100644 --- a/src/main/java/dev/mduchene/App.java +++ b/src/main/java/dev/mduchene/App.java @@ -22,6 +22,7 @@ public class App { .build(); db.init(); + Migration.of(db).getPendingBaseMigrations().forEach(f -> System.out.println(f.getName())); var app = Javalin.create(cnf -> {}); app.before( diff --git a/src/main/java/dev/mduchene/Db.java b/src/main/java/dev/mduchene/Db.java index 55738d2..0591cc4 100644 --- a/src/main/java/dev/mduchene/Db.java +++ b/src/main/java/dev/mduchene/Db.java @@ -2,63 +2,95 @@ package dev.mduchene; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; +import java.util.List; + +import org.jooq.SQLDialect; +import org.jooq.conf.RenderQuotedNames; +import org.jooq.conf.Settings; +import org.jooq.SQLDialect; public class Db { - private HikariDataSource dataSource; - private Db() {} + private HikariDataSource dataSource; + private org.jooq.DSLContext dsl; - public void init() { + private Db() {} + public void init() { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + } catch (Exception e) { + // Ignore if the table already exists + } + } + + public static class Builder { + private String url; + private String user; + private String password; + + private Builder() {} + + public static Builder create() { + return new Builder(); } - public static class Builder { - private String url; - private String user; - private String password; - - private Builder() {} - public static Builder create() { - return new Builder(); - } - - public Builder url(String url) { - this.url = url; - return this; - } - - public Builder user(String user) { - this.user = user; - return this; - } - - public Builder password(String password) { - this.password = password; - return this; - } - - public Db build() { - Db db = new Db(); - HikariConfig config = new HikariConfig(); - config.setJdbcUrl(url); - config.setUsername(user); - config.setPassword(password); - config.addDataSourceProperty("cachePrepStmts", "true"); - config.addDataSourceProperty("prepStmtCacheSize", "250"); - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - config.setMaximumPoolSize(64); - db.dataSource = new HikariDataSource(config); - return db; - } + public Builder url(String url) { + this.url = url; + return this; } - public Connection getConnection() throws Exception { - Connection connection = dataSource.getConnection(); - connection.setAutoCommit(false); - return connection; + public Builder user(String user) { + this.user = user; + return this; } + public Builder password(String password) { + this.password = password; + return this; + } + + public Db build() { + Db db = new Db(); + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(url); + config.setUsername(user); + config.setPassword(password); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.setMaximumPoolSize(64); + db.dataSource = new HikariDataSource(config); + db.dsl = + DSL.using( + db.dataSource, + SQLDialect.MARIADB, + new Settings().withRenderQuotedNames(RenderQuotedNames.NEVER)); + return db; + } + } + + public Connection getConnection() throws Exception { + Connection connection = dataSource.getConnection(); + connection.setAutoCommit(false); + return connection; + } + + public void run(String query) { + try (Connection connection = getConnection()) { + dsl.execute(query); + connection.commit(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public DSLContext dsl() { + return dsl; + } } diff --git a/src/main/java/dev/mduchene/Migration.java b/src/main/java/dev/mduchene/Migration.java new file mode 100644 index 0000000..014955f --- /dev/null +++ b/src/main/java/dev/mduchene/Migration.java @@ -0,0 +1,77 @@ +package dev.mduchene; + +import org.jooq.Meta; +import org.jooq.impl.DSL; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Migration { + + private final Db db; + + private Migration(Db db) { + this.db = db; + } + + public static Migration of(Db db) { + return new Migration(db); + } + + public List getPendingBaseMigrations() { + List ranMigrations = getRanBaseMigrations(); + List migrations = ResourceFileLister.listFilesInResourceFolder("migrations"); + return migrations.stream() + .filter(file -> !ranMigrations.contains(file.getName())) + .collect(Collectors.toList()); + } + + public boolean tableExists(String tableName) { + // Get the Meta object, which contains all database metadata + Meta meta = db.dsl().meta(); + + // Use the getTables() method with a filter + return meta.getTables(DSL.name("bolts", tableName)).stream() + .anyMatch( + table -> + table.getName().equalsIgnoreCase(tableName) + && table.getSchema().getName().equalsIgnoreCase("bolts")); + } + + public List getRanBaseMigrations() { + try (Connection connection = db.getConnection()) { + boolean migrationsExists = tableExists("_migrations"); + if (!migrationsExists) return List.of(); + + return db.dsl() + .select(DSL.field("name")) + .from(DSL.table("_migrations")) + .where(DSL.field("base").eq(true)) + .and(DSL.field("executedAt").isNull()) + .fetch(DSL.field("name"), String.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List getRanMigrations() { + try (Connection connection = db.getConnection()) { + return db.dsl() + .select(DSL.field("name")) + .from(DSL.table("_migrations")) + .fetch(DSL.field("name"), String.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/dev/mduchene/ResourceFileLister.java b/src/main/java/dev/mduchene/ResourceFileLister.java new file mode 100644 index 0000000..0ff12af --- /dev/null +++ b/src/main/java/dev/mduchene/ResourceFileLister.java @@ -0,0 +1,37 @@ +package dev.mduchene; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ResourceFileLister { + + public static void main(String[] args) { + listFilesInResourceFolder("data"); + } + + public static List listFilesInResourceFolder(String folderName) { + ClassLoader classLoader = ResourceFileLister.class.getClassLoader(); + URL folderUrl = classLoader.getResource(folderName); + + if (folderUrl == null) { + throw new IllegalArgumentException("Folder not found: " + folderName); + } + + try { + Path folderPath = Paths.get(folderUrl.toURI()); + try (Stream stream = Files.list(folderPath)) { + return stream.filter(Files::isRegularFile).map(Path::toFile).toList(); + } + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/migrations/20250919-create-migration.kts b/src/main/resources/migrations/20250919-create-migration.kts new file mode 100644 index 0000000..e69de29