Skip to content

Commit 6da76a3

Browse files
committed
feat: Add Exposed Migration Gradle Plugin
Introduces the Exposed Migration Gradle Plugin to simplify SQL migration generation using Exposed table definitions. The plugin supports TestContainers for database testing, Flyway for integration, and a highly configurable extension. Includes unit tests for plugin functionality and task behavior.
1 parent 72a5945 commit 6da76a3

File tree

13 files changed

+1156
-2
lines changed

13 files changed

+1156
-2
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
public class org/jetbrains/exposed/migration/plugin/ExposedMigrationExtension {
2+
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
3+
public final fun getClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection;
4+
public final fun getDatabasePassword ()Lorg/gradle/api/provider/Property;
5+
public final fun getDatabaseUrl ()Lorg/gradle/api/provider/Property;
6+
public final fun getDatabaseUser ()Lorg/gradle/api/provider/Property;
7+
public final fun getExposedTablesPackage ()Lorg/gradle/api/provider/Property;
8+
public final fun getMigrationFileExtension ()Lorg/gradle/api/provider/Property;
9+
public final fun getMigrationFilePrefix ()Lorg/gradle/api/provider/Property;
10+
public final fun getMigrationFileSeparator ()Lorg/gradle/api/provider/Property;
11+
public final fun getMigrationsDir ()Lorg/gradle/api/file/DirectoryProperty;
12+
public final fun getTestContainersImageName ()Lorg/gradle/api/provider/Property;
13+
}
14+
15+
public final class org/jetbrains/exposed/migration/plugin/ExposedMigrationPlugin : org/gradle/api/Plugin {
16+
public fun <init> ()V
17+
public synthetic fun apply (Ljava/lang/Object;)V
18+
public fun apply (Lorg/gradle/api/Project;)V
19+
}
20+
21+
public abstract interface class org/jetbrains/exposed/migration/plugin/GenerateMigrationsParameters : org/gradle/workers/WorkParameters {
22+
public abstract fun getClasspathUrls ()Ljava/util/List;
23+
public abstract fun getDatabasePassword ()Ljava/lang/String;
24+
public abstract fun getDatabaseUrl ()Ljava/lang/String;
25+
public abstract fun getDatabaseUser ()Ljava/lang/String;
26+
public abstract fun getDebug ()Z
27+
public abstract fun getExposedTablesPackage ()Ljava/lang/String;
28+
public abstract fun getMigrationFileExtension ()Ljava/lang/String;
29+
public abstract fun getMigrationFilePrefix ()Ljava/lang/String;
30+
public abstract fun getMigrationFileSeparator ()Ljava/lang/String;
31+
public abstract fun getMigrationsDir ()Lorg/gradle/api/file/DirectoryProperty;
32+
public abstract fun getTestContainersImageName ()Ljava/lang/String;
33+
public abstract fun setClasspathUrls (Ljava/util/List;)V
34+
public abstract fun setDatabasePassword (Ljava/lang/String;)V
35+
public abstract fun setDatabaseUrl (Ljava/lang/String;)V
36+
public abstract fun setDatabaseUser (Ljava/lang/String;)V
37+
public abstract fun setDebug (Z)V
38+
public abstract fun setExposedTablesPackage (Ljava/lang/String;)V
39+
public abstract fun setMigrationFileExtension (Ljava/lang/String;)V
40+
public abstract fun setMigrationFilePrefix (Ljava/lang/String;)V
41+
public abstract fun setMigrationFileSeparator (Ljava/lang/String;)V
42+
public abstract fun setTestContainersImageName (Ljava/lang/String;)V
43+
}
44+
45+
public abstract class org/jetbrains/exposed/migration/plugin/GenerateMigrationsTask : org/gradle/api/DefaultTask {
46+
public fun <init> ()V
47+
public final fun generateMigrations ()V
48+
public abstract fun getClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection;
49+
public abstract fun getDatabasePassword ()Lorg/gradle/api/provider/Property;
50+
public abstract fun getDatabaseUrl ()Lorg/gradle/api/provider/Property;
51+
public abstract fun getDatabaseUser ()Lorg/gradle/api/provider/Property;
52+
public abstract fun getExposedTablesPackage ()Lorg/gradle/api/provider/Property;
53+
public abstract fun getMigrationFileExtension ()Lorg/gradle/api/provider/Property;
54+
public abstract fun getMigrationFilePrefix ()Lorg/gradle/api/provider/Property;
55+
public abstract fun getMigrationFileSeparator ()Lorg/gradle/api/provider/Property;
56+
public abstract fun getMigrationsDir ()Lorg/gradle/api/file/DirectoryProperty;
57+
public abstract fun getTestContainersImageName ()Lorg/gradle/api/provider/Property;
58+
public abstract fun getWorkerExecutor ()Lorg/gradle/workers/WorkerExecutor;
59+
}
60+
61+
public abstract class org/jetbrains/exposed/migration/plugin/GenerateMigrationsWorker : org/gradle/workers/WorkAction {
62+
public fun <init> ()V
63+
public final fun container (Ljava/lang/String;)Lorg/testcontainers/containers/JdbcDatabaseContainer;
64+
public fun execute ()V
65+
public final fun findHighestVersion (Ljava/io/File;)Lkotlin/jvm/functions/Function1;
66+
}
67+
68+
public final class org/jetbrains/exposed/migration/plugin/GenerateMigrationsWorker$GradleLogger : org/jetbrains/exposed/sql/SqlLogger {
69+
public fun <init> (Lorg/jetbrains/exposed/migration/plugin/GenerateMigrationsWorker;)V
70+
public fun log (Lorg/jetbrains/exposed/sql/statements/StatementContext;Lorg/jetbrains/exposed/sql/Transaction;)V
71+
}
72+
73+
public final class org/jetbrains/exposed/migration/plugin/NameGeneratorKt {
74+
public static final fun statementToFileName (Ljava/lang/String;)Ljava/lang/String;
75+
}
76+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
plugins {
2+
kotlin("jvm")
3+
alias(libs.plugins.dokka)
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
implementation(gradleApi())
12+
13+
implementation(projects.exposed.exposedJdbc)
14+
implementation(projects.exposed.exposedMigration)
15+
16+
implementation(libs.flyway.postgresql)
17+
implementation(libs.flyway.mysql)
18+
implementation(libs.flyway.sqlserver)
19+
implementation(libs.flyway.oracle)
20+
21+
implementation(libs.testcontainers.postgresql)
22+
implementation(libs.testcontainers.mysql)
23+
implementation(libs.testcontainers.mariadb)
24+
implementation(libs.testcontainers.mssqlserver)
25+
implementation(libs.testcontainers.oracle)
26+
27+
implementation(libs.h2)
28+
implementation(libs.postgre)
29+
implementation(libs.mysql)
30+
implementation(libs.maria.db3)
31+
implementation(libs.mssql)
32+
implementation(libs.oracle19)
33+
34+
testImplementation(kotlin("test"))
35+
}
36+
37+
tasks.test {
38+
useJUnitPlatform()
39+
}
40+
41+
kotlin {
42+
jvmToolchain(8)
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.jetbrains.exposed.migration.plugin
2+
3+
import org.gradle.api.file.ConfigurableFileCollection
4+
import org.gradle.api.file.DirectoryProperty
5+
import org.gradle.api.model.ObjectFactory
6+
import org.gradle.api.provider.Property
7+
import javax.inject.Inject
8+
9+
/**
10+
* Configuration extension for the Exposed Migration Plugin.
11+
*
12+
* This extension allows users to configure the behavior of the plugin
13+
* in their build.gradle.kts file.
14+
*/
15+
open class ExposedMigrationExtension @Inject constructor(objects: ObjectFactory) {
16+
17+
/**
18+
* Directory where the generated migration scripts will be stored.
19+
* Default: src/main/resources/db/migration
20+
*/
21+
val migrationsDir: DirectoryProperty = objects.directoryProperty()
22+
23+
/**
24+
* Package name where Exposed table definitions are located.
25+
* The plugin will scan this package for table definitions.
26+
*/
27+
val exposedTablesPackage: Property<String> = objects.property(String::class.java)
28+
29+
/**
30+
* Prefix for migration file names.
31+
* Default: V
32+
*/
33+
val migrationFilePrefix: Property<String> = objects.property(String::class.java).convention("V")
34+
35+
/**
36+
* Separator for migration file names.
37+
* Default: __
38+
*/
39+
val migrationFileSeparator: Property<String> = objects.property(String::class.java).convention("__")
40+
41+
/**
42+
* File extension for migration files.
43+
* Default: sql
44+
*/
45+
val migrationFileExtension: Property<String> = objects.property(String::class.java).convention(".sql")
46+
47+
/**
48+
* URL for the database connection.
49+
* This is optional if useTestContainers is true.
50+
*/
51+
val databaseUrl: Property<String> = objects.property(String::class.java)
52+
53+
/**
54+
* Username for the database connection.
55+
* This is optional if useTestContainers is true.
56+
*/
57+
val databaseUser: Property<String> = objects.property(String::class.java)
58+
59+
/**
60+
* Password for the database connection.
61+
* This is optional if useTestContainers is true.
62+
*/
63+
val databasePassword: Property<String> = objects.property(String::class.java)
64+
65+
/**
66+
* Docker image name for TestContainers.
67+
*/
68+
val testContainersImageName: Property<String> = objects.property(String::class.java)
69+
70+
/**
71+
* Classpath that is scanned for Exposed Tables
72+
*/
73+
val classpath: ConfigurableFileCollection = objects.fileCollection()
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.jetbrains.exposed.migration.plugin
2+
3+
import org.gradle.api.Plugin
4+
import org.gradle.api.Project
5+
import org.gradle.api.tasks.SourceSetContainer
6+
7+
class ExposedMigrationPlugin : Plugin<Project> {
8+
override fun apply(project: Project) {
9+
val extension =
10+
project.extensions.create("exposedMigration", ExposedMigrationExtension::class.java, project.objects)
11+
12+
// Set default Migration Dir
13+
extension.migrationsDir.convention(project.layout.projectDirectory.dir("src/main/resources/db/migration"))
14+
15+
val classpath = project
16+
.extensions
17+
.findByType(SourceSetContainer::class.java)
18+
?.findByName("main")
19+
?.runtimeClasspath
20+
21+
if (classpath != null) extension.classpath.setFrom(classpath)
22+
23+
project.tasks.register("generateMigrations", GenerateMigrationsTask::class.java) {
24+
it.description = "Generates SQL migration scripts from Exposed table definitions"
25+
it.group = "exposed"
26+
27+
it.migrationsDir.set(extension.migrationsDir)
28+
it.exposedTablesPackage.set(extension.exposedTablesPackage)
29+
it.migrationFilePrefix.set(extension.migrationFilePrefix)
30+
it.migrationFileSeparator.set(extension.migrationFileSeparator)
31+
it.migrationFileExtension.set(extension.migrationFileExtension)
32+
33+
// Database connection properties (optional if TestContainers is used)
34+
it.databaseUrl.set(extension.databaseUrl)
35+
it.databaseUser.set(extension.databaseUser)
36+
it.databasePassword.set(extension.databasePassword)
37+
38+
it.testContainersImageName.set(extension.testContainersImageName)
39+
it.classpath.setFrom(extension.classpath.toList())
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.jetbrains.exposed.migration.plugin
2+
3+
import org.gradle.api.DefaultTask
4+
import org.gradle.api.file.ConfigurableFileCollection
5+
import org.gradle.api.file.DirectoryProperty
6+
import org.gradle.api.provider.Property
7+
import org.gradle.api.tasks.Input
8+
import org.gradle.api.tasks.InputFiles
9+
import org.gradle.api.tasks.Optional
10+
import org.gradle.api.tasks.OutputDirectory
11+
import org.gradle.api.tasks.TaskAction
12+
import org.gradle.workers.WorkerExecutor
13+
import javax.inject.Inject
14+
15+
/**
16+
* Task for generating SQL migration scripts from Exposed table definitions.
17+
* Uses the Gradle Workers API for better isolation and parallel execution.
18+
*/
19+
abstract class GenerateMigrationsTask : DefaultTask() {
20+
21+
@get:OutputDirectory
22+
abstract val migrationsDir: DirectoryProperty
23+
24+
@get:Input
25+
abstract val exposedTablesPackage: Property<String>
26+
27+
@get:Input
28+
@get:Optional
29+
abstract val migrationFilePrefix: Property<String>
30+
31+
@get:Input
32+
@get:Optional
33+
abstract val migrationFileSeparator: Property<String>
34+
35+
@get:Input
36+
@get:Optional
37+
abstract val migrationFileExtension: Property<String>
38+
39+
@get:Input
40+
@get:Optional
41+
abstract val databaseUrl: Property<String>
42+
43+
@get:Input
44+
@get:Optional
45+
abstract val databaseUser: Property<String>
46+
47+
@get:Input
48+
@get:Optional
49+
abstract val databasePassword: Property<String>
50+
51+
@get:Input
52+
@get:Optional
53+
abstract val testContainersImageName: Property<String>
54+
55+
@get:InputFiles
56+
abstract val classpath: ConfigurableFileCollection
57+
58+
@get:Inject
59+
abstract val workerExecutor: WorkerExecutor
60+
61+
@TaskAction
62+
fun generateMigrations() {
63+
workerExecutor
64+
.classLoaderIsolation()
65+
.submit(GenerateMigrationsWorker::class.java) { parameters ->
66+
parameters.migrationsDir.set(migrationsDir)
67+
parameters.exposedTablesPackage = exposedTablesPackage.get()
68+
parameters.migrationFilePrefix = migrationFilePrefix.get()
69+
parameters.migrationFileSeparator = migrationFileSeparator.get()
70+
parameters.migrationFileExtension = migrationFileExtension.get()
71+
72+
if (databaseUrl.isPresent) parameters.databaseUrl = databaseUrl.get()
73+
if (databaseUser.isPresent) parameters.databaseUser = databaseUser.get()
74+
if (databasePassword.isPresent) parameters.databasePassword = databasePassword.get()
75+
if (testContainersImageName.isPresent) {
76+
parameters.testContainersImageName = testContainersImageName.get()
77+
}
78+
79+
parameters.classpathUrls = classpath.files.map { it.toURI().toURL() }
80+
81+
parameters.debug = logger.isDebugEnabled
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)