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