diff --git a/build.gradle.kts b/build.gradle.kts index 6de5f8c4..b273dd38 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,11 @@ allprojects { ktlint { version.set("1.3.1") + filter { + exclude { entry -> + entry.file.toString().contains("/generated/") + } + } } } diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 7efafa84..c3acbb33 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -1,5 +1,6 @@ val liquibaseVersion = rootProject.properties["liquibaseVersion"] as String val kotestVersion = rootProject.properties["kotestVersion"] as String +val slf4jVersion = rootProject.properties["slf4jVersion"] as String dependencies { // liquibase @@ -7,7 +8,7 @@ dependencies { // for liquibase-cli api("info.picocli:picocli:4.7.6") // log - api("org.slf4j:slf4j-api:2.0.16") + api("org.slf4j:slf4j-api:$slf4jVersion") // test testImplementation("io.kotest:kotest-framework-engine-jvm:$kotestVersion") diff --git a/compiled-parser/build.gradle.kts b/compiled-parser/build.gradle.kts index 622215b1..cb8a6af6 100644 --- a/compiled-parser/build.gradle.kts +++ b/compiled-parser/build.gradle.kts @@ -3,6 +3,7 @@ val kotestVersion = rootProject.properties["kotestVersion"] as String val kotlinVersion = rootProject.properties["kotlinVersion"] as String val liquibaseKotlinVersion = rootProject.properties["liquibaseKotlinVersion"] as String val classgraphVersion = rootProject.properties["classgraphVersion"] as String +val slf4jVersion = rootProject.properties["slf4jVersion"] as String dependencies { // liquibase-kotlin @@ -13,6 +14,8 @@ dependencies { api("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") // Scan classes api("io.github.classgraph:classgraph:$classgraphVersion") + // log + api("org.slf4j:slf4j-api:$slf4jVersion") // test testImplementation("io.kotest:kotest-framework-engine-jvm:$kotestVersion") diff --git a/compiled-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinCompiledLiquibaseChangeLogParser.kt b/compiled-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinCompiledLiquibaseChangeLogParser.kt index 6a54f238..bb405f03 100644 --- a/compiled-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinCompiledLiquibaseChangeLogParser.kt +++ b/compiled-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinCompiledLiquibaseChangeLogParser.kt @@ -10,18 +10,27 @@ import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor class KotlinCompiledLiquibaseChangeLogParser : ChangeLogParser { + private val log = org.slf4j.LoggerFactory.getLogger(this::class.java) + override fun parse( physicalChangeLogLocation: String, changeLogParameters: ChangeLogParameters, resourceAccessor: ResourceAccessor, ): DatabaseChangeLog { val databaseChangeLog = DatabaseChangeLog(physicalChangeLogLocation) - updateChangeLog( - databaseChangeLog = databaseChangeLog, - changeLogParameters = changeLogParameters, - resourceAccessor = resourceAccessor, + return runCatching { + updateChangeLog( + databaseChangeLog = databaseChangeLog, + changeLogParameters = changeLogParameters, + resourceAccessor = resourceAccessor, + ) + }.fold( + onSuccess = { databaseChangeLog }, + onFailure = { + log.error("error in KotlinCompiledLiquibaseChangeLogParser", it) + throw it + } ) - return databaseChangeLog } private fun updateChangeLog( diff --git a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeLogDsl.kt b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeLogDsl.kt index 29ff1e6c..82923d2e 100644 --- a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeLogDsl.kt +++ b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeLogDsl.kt @@ -1,3 +1,5 @@ +@file:Suppress("CyclomaticComplexMethod") + package momosetkn.liquibase.kotlin.dsl import liquibase.ContextExpression @@ -45,6 +47,7 @@ class ChangeLogDsl( runOrder: String? = null, runWith: String? = null, runWithSpoolFile: String? = null, + comment: String? = null, // not official args block: ChangeSetDsl.() -> Unit, ) { val enumObjectQuotingStrategy = @@ -82,6 +85,7 @@ class ChangeLogDsl( created?.also { changeSet.created = it } runOrder?.also { changeSet.runOrder = it } ignore?.also { changeSet.isIgnore = it } + comment?.also { changeSet.comments = it } val changeSetcontext = ChangeSetContext( changeSet = changeSet, diff --git a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeSetDsl.kt b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeSetDsl.kt index 25ddce27..0d399b3a 100644 --- a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeSetDsl.kt +++ b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ChangeSetDsl.kt @@ -60,6 +60,15 @@ import momosetkn.liquibase.kotlin.dsl.Expressions.tryEvalExpressions import momosetkn.liquibase.kotlin.dsl.Expressions.tryEvalExpressionsOrNull import org.intellij.lang.annotations.Language +/** + * The ChangeSetDsl class provides a domain-specific language (DSL) for constructing and managing database change sets. + * This class includes methods for defining changes to database structures such as tables, columns, indexes, views, + * procedures, sequences, and constraints. + * + * @property changeLog The change log associated with the change set. + * @property context The context in which the change set executes. + * @property changeSetSupport Support utilities for change set operations. It is made public for custom DSL. + */ @Suppress("LargeClass", "TooManyFunctions") @ChangeLogDslMarker class ChangeSetDsl( @@ -74,10 +83,28 @@ class ChangeSetDsl( inRollback = context.inRollback, ) + /** + * Adds a comment to the current context's change set. + * Can set comment in [ChangeLogDsl.changeSet] + * + * @param text The comment text. If set, that value will take precedence. + */ fun comment(text: String) { - context.changeSet.comments = text.evalExpressions(changeLog) + // NOTE: First to win in XML, last to win in groovy DSL + context.changeSet.comments = context.changeSet.comments ?: text.evalExpressions(changeLog) } + /** + * Precondition the control execute changeSet for the database state. + * [official-document](https://docs.liquibase.com/concepts/changelogs/preconditions.html) + * + * @param onError either of CONTINUE / HALT / MARK_RAN / WARN + * @param onErrorMessage provides a specific error message when an error occurs. + * @param onFail either of CONTINUE / HALT / MARK_RAN / WARN + * @param onFailMessage provides a specific failure message when a failure occurs. + * @param onSqlOutput FAIL / IGNORE / TEST. used in update-sql command. + * @param block Specify the condition of precondition + */ fun preConditions( onError: String? = null, onErrorMessage: String? = null, @@ -90,8 +117,8 @@ class ChangeSetDsl( PreconditionContainerContext( onError = onError, onFail = onFail, - onFailMessage = onFailMessage, - onErrorMessage = onErrorMessage, + onFailMessage = onFailMessage?.evalExpressions(changeLog), + onErrorMessage = onErrorMessage?.evalExpressions(changeLog), onSqlOutput = onSqlOutput, ) val dsl = PreConditionDsl.build( @@ -1292,9 +1319,21 @@ class ChangeSetDsl( } // Miscellaneous + + /** + * Applies a custom-change to the change set. + * This function requires at least one of the parameters `class`, `clazz`, or `className`. + * Class is requiring implements the [liquibase.change.custom.CustomChange] + * [official-document](https://docs.liquibase.com/change-types/custom-change.html) + * + * @param `class` specify KClass or Class<*> of CustomChange or className of CustomChange. + * @param clazz specify KClass or Class<*> of CustomChange or className of CustomChange. + * @param className className of CustomChange. + * @param block Key-value to be given to CustomChange. + */ fun customChange( @Suppress("FunctionParameterNaming") - `class`: Any?, + `class`: Any? = null, clazz: Any? = null, className: String? = null, block: (KeyValueDsl.() -> Unit)? = null, @@ -1315,20 +1354,31 @@ class ChangeSetDsl( fun empty() = Unit + /** + * Executes a shell command with the provided executable and optional parameters. + * [official-document](https://docs.liquibase.com/change-types/execute-command.html) + * + * @param executable The command or executable to run. Required. + * @param os execute codntion by os. get os by Java system property the "os.name". + * @param timeout The maximum amount of time the command is allowed to run. + * @param block arguments for executable. + */ fun executeCommand( executable: String, os: String? = null, timeout: String? = null, - block: ArgumentDsl.() -> Unit, + block: (ArgumentDsl.() -> Unit)? = null, ) { val change = changeSetSupport.createChange("executeCommand") as ExecuteShellCommandChange change.executable = executable change.setOs(os) change.timeout = timeout - val dsl = ArgumentDsl(changeLog) - val args = wrapChangeLogParseException { dsl(block) } - args.forEach { - change.args.add(it.tryEvalExpressions(changeLog).toString()) + block?.also { + val dsl = ArgumentDsl(changeLog) + val args = wrapChangeLogParseException { dsl(block) } + args.forEach { + change.addArg(it.tryEvalExpressions(changeLog).toString()) + } } changeSetSupport.addChange(change) } @@ -1348,6 +1398,13 @@ class ChangeSetDsl( // changeSetSupport.addChange(change) // } + /** + * Outputs a message to the specified target. + * [official-document](https://docs.liquibase.com/change-types/output.html) + * + * @param message message to be outputted. + * @param target output target. STDOUT, STDERR, FATAL, WARN, INFO, DEBUG. default target is "STDERR". + */ fun output( message: String? = null, target: String? = null, @@ -1362,7 +1419,7 @@ class ChangeSetDsl( dbms: String? = null, endDelimiter: String? = null, splitStatements: Boolean? = null, - stripComments: Boolean? = true, + stripComments: Boolean? = null, block: SqlBlockDsl.() -> String, ) { val change = changeSetSupport.createChange("sql") as RawSQLChange @@ -1377,13 +1434,24 @@ class ChangeSetDsl( changeSetSupport.addChange(change) } + /** + * Executes a raw SQL change. + * [official-document](https://docs.liquibase.com/change-types/sql.html) + * + * @param sql The SQL.required. + * @param dbms The type of database. [database-type](https://docs.liquibase.com/start/tutorials/home.html) + * @param endDelimiter The delimiter for the end of the SQL statement. Default is ";". + * @param splitStatements Whether to split the SQL statements. Default is true. + * @param stripComments Whether to strip comments from the SQL. Default is true. + * @param comment An optional comment for the SQL change. Default is null. + */ fun sql( @Language("sql") sql: String, - comment: String? = null, dbms: String? = null, endDelimiter: String? = null, splitStatements: Boolean? = null, - stripComments: Boolean? = true, + stripComments: Boolean? = null, + comment: String? = null, ) { val change = changeSetSupport.createChange("sql") as RawSQLChange change.dbms = dbms @@ -1395,6 +1463,18 @@ class ChangeSetDsl( changeSetSupport.addChange(change) } + /** + * Executes an SQL file as part of a change set. + * [official-document](https://docs.liquibase.com/change-types/sql-file.html) + * + * @param dbms The type of database. [database-type](https://docs.liquibase.com/start/tutorials/home.html) + * @param encoding The encoding of the SQL file. If null, the default system encoding will be used. + * @param endDelimiter The delimiter for the end of the SQL statement. Default is ";". + * @param path The path to the SQL file. + * @param relativeToChangelogFile Specifies whether the path is relative to the changelog file. Defaults is false. + * @param splitStatements Whether to split the SQL statements. Default is true. + * @param stripComments Whether to strip comments from the SQL. Default is true. + */ fun sqlFile( dbms: String? = null, encoding: String? = null, @@ -1412,16 +1492,29 @@ class ChangeSetDsl( change.isRelativeToChangelogFile = relativeToChangelogFile change.isSplitStatements = splitStatements change.isStripComments = stripComments - change.finishInitialization() // check for path in liquibase.change.core.SQLFileChange changeSetSupport.addChange(change) } + /** + * Stops the current change process and logs an optional message. + * This change is useful for debug or step update. + * [official-document](https://docs.liquibase.com/change-types/stop.html) + * + * @param message output message when stop. + */ fun stop(message: String? = null) { val change = changeSetSupport.createChange("stop") as StopChange change.message = message.evalExpressionsOrNull(changeLog) changeSetSupport.addChange(change) } + /** + * We will tag the current state. + * It will be used for rollback. + * [official-document](https://docs.liquibase.com/commands/utility/tag.html) + * + * @param tag name of tag + */ fun tagDatabase(tag: String) { val change = changeSetSupport.createChange("tagDatabase") as TagDatabaseChange change.tag = tag.evalExpressions(changeLog) diff --git a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ModifySqlDsl.kt b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ModifySqlDsl.kt index 3bba7baa..b81b117c 100644 --- a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ModifySqlDsl.kt +++ b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/ModifySqlDsl.kt @@ -10,6 +10,7 @@ import liquibase.sql.visitor.ReplaceSqlVisitor import liquibase.sql.visitor.SqlVisitor import momosetkn.liquibase.kotlin.dsl.Expressions.evalExpressions import momosetkn.liquibase.kotlin.dsl.Expressions.evalExpressionsOrNull +import momosetkn.liquibase.kotlin.dsl.util.ReflectionUtils.new import momosetkn.liquibase.kotlin.dsl.util.StringsUtil.splitAndTrim @ChangeLogDslMarker @@ -62,14 +63,7 @@ class ModifySqlDsl( private inline fun createSqlVisitor(): E { // not use liquibase.sql.visitor.SqlVisitorFactory. // because, for typesafe. - val constructor = - E::class.constructors.find { - it.parameters.isEmpty() - } - checkNotNull(constructor) { - "not supported ${E::class.qualifiedName} SqlVisitor" - } - val sqlVisitor = constructor.call() + val sqlVisitor = E::class.new() context.dbms?.let { sqlVisitor.applicableDbms = it } context.contextFilter?.let { sqlVisitor.contextFilter = it } diff --git a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/PreConditionDsl.kt b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/PreConditionDsl.kt index 86605de2..10fb2d5f 100644 --- a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/PreConditionDsl.kt +++ b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/PreConditionDsl.kt @@ -31,8 +31,8 @@ import momosetkn.liquibase.kotlin.dsl.Expressions.evalClassNameExpressions import momosetkn.liquibase.kotlin.dsl.Expressions.evalExpressions import momosetkn.liquibase.kotlin.dsl.Expressions.tryEvalExpressions import momosetkn.liquibase.kotlin.dsl.Expressions.tryEvalExpressionsOrNull +import momosetkn.liquibase.kotlin.dsl.util.ReflectionUtils.new import kotlin.reflect.KClass -import kotlin.reflect.full.primaryConstructor @ChangeLogDslMarker @Suppress("TooManyFunctions") @@ -40,15 +40,14 @@ class PreConditionDsl( private val changeLog: DatabaseChangeLog, private val buildPreconditionContainer: () -> PRECONDITION_LOGIC, ) { - @Suppress("ktlint:standard:backing-property-naming") - private val _preConditions: MutableList = mutableListOf() + private val preConditions: MutableList = mutableListOf() internal operator fun invoke( block: PreConditionDsl.() -> Unit ): PRECONDITION_LOGIC { block(this) return buildPreconditionContainer().apply { - _preConditions.forEach { + preConditions.forEach { addNestedPrecondition(it) } } @@ -56,17 +55,17 @@ class PreConditionDsl( fun and(block: PreConditionDsl.() -> Unit) { val precondition = nestedPrecondition(AndPrecondition::class, block) - _preConditions.add(precondition) + preConditions.add(precondition) } fun or(block: PreConditionDsl.() -> Unit) { val precondition = nestedPrecondition(OrPrecondition::class, block) - _preConditions.add(precondition) + preConditions.add(precondition) } fun not(block: PreConditionDsl.() -> Unit) { val precondition = nestedPrecondition(NotPrecondition::class, block) - _preConditions.add(precondition) + preConditions.add(precondition) } fun changeLogPropertyDefined( @@ -76,7 +75,7 @@ class PreConditionDsl( val precondition = ChangeLogPropertyDefinedPrecondition() precondition.property = property.evalExpressions(changeLog) value?.also { precondition.value = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun changeSetExecuted( @@ -88,7 +87,7 @@ class PreConditionDsl( precondition.id = id.evalExpressions(changeLog) precondition.author = author.evalExpressions(changeLog) precondition.changeLogFile = changeLogFile.evalExpressions(changeLog) - _preConditions.add(precondition) + preConditions.add(precondition) } fun columnExists( @@ -100,7 +99,7 @@ class PreConditionDsl( precondition.columnName = columnName.evalExpressions(changeLog) precondition.tableName = tableName.evalExpressions(changeLog) schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun dbms( @@ -108,7 +107,7 @@ class PreConditionDsl( ) { val precondition = DBMSPrecondition() precondition.type = type.evalExpressions(changeLog) - _preConditions.add(precondition) + preConditions.add(precondition) } fun expectedQuotingStrategy( @@ -116,7 +115,7 @@ class PreConditionDsl( ) { val precondition = ObjectQuotingStrategyPrecondition() precondition.setStrategy(strategy.evalExpressions(changeLog)) - _preConditions.add(precondition) + preConditions.add(precondition) } fun foreignKeyConstraintExists( @@ -128,7 +127,7 @@ class PreConditionDsl( precondition.foreignKeyName = foreignKeyName.evalExpressions(changeLog) precondition.foreignKeyTableName = foreignKeyTableName.evalExpressions(changeLog) schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun indexExists( @@ -142,7 +141,7 @@ class PreConditionDsl( columnNames?.also { precondition.columnNames = it.evalExpressions(changeLog) } tableName?.also { precondition.tableName = it.evalExpressions(changeLog) } schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun primaryKeyExists( @@ -154,7 +153,7 @@ class PreConditionDsl( primaryKeyName?.also { precondition.primaryKeyName = it.evalExpressions(changeLog) } tableName?.also { precondition.tableName = it.evalExpressions(changeLog) } schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun rowCount( @@ -164,7 +163,7 @@ class PreConditionDsl( val precondition = RowCountPrecondition() precondition.expectedRows = expectedRows precondition.tableName = tableName.evalExpressions(changeLog) - _preConditions.add(precondition) + preConditions.add(precondition) } fun runningAs( @@ -172,7 +171,7 @@ class PreConditionDsl( ) { val precondition = RunningAsPrecondition() precondition.username = username.evalExpressions(changeLog) - _preConditions.add(precondition) + preConditions.add(precondition) } fun sequenceExists( @@ -182,7 +181,7 @@ class PreConditionDsl( val precondition = SequenceExistsPrecondition() precondition.sequenceName = sequenceName.evalExpressions(changeLog) schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun sqlCheck( @@ -192,7 +191,7 @@ class PreConditionDsl( val precondition = SqlPrecondition() precondition.expectedResult = expectedResult.tryEvalExpressions(changeLog).toString() precondition.sql = block().evalExpressions(changeLog) - _preConditions.add(precondition) + preConditions.add(precondition) } fun tableExists( @@ -202,7 +201,7 @@ class PreConditionDsl( val precondition = TableExistsPrecondition() precondition.tableName = tableName.evalExpressions(changeLog) schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun tableIsEmpty( @@ -214,7 +213,7 @@ class PreConditionDsl( precondition.tableName = tableName.evalExpressions(changeLog) catalogName?.also { precondition.catalogName = it.evalExpressions(changeLog) } schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun uniqueConstraintExists( @@ -226,7 +225,7 @@ class PreConditionDsl( precondition.tableName = tableName.evalExpressions(changeLog) columnNames?.also { precondition.columnNames = it.evalExpressions(changeLog) } constraintName?.also { precondition.constraintName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun viewExists( @@ -236,7 +235,7 @@ class PreConditionDsl( val precondition = ViewExistsPrecondition() precondition.viewName = viewName.evalExpressions(changeLog) schemaName?.also { precondition.schemaName = it.evalExpressions(changeLog) } - _preConditions.add(precondition) + preConditions.add(precondition) } fun customPrecondition( @@ -259,7 +258,7 @@ class PreConditionDsl( precondition.setParam(key, expandedValue) } - _preConditions.add(precondition) + preConditions.add(precondition) } private fun nestedPrecondition( @@ -270,7 +269,7 @@ class PreConditionDsl( PreConditionDsl( changeLog = changeLog, buildPreconditionContainer = { - preconditionClass.primaryConstructor!!.call() + preconditionClass.new() }, ) return dsl(block) diff --git a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/util/ReflectionUtils.kt b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/util/ReflectionUtils.kt index 4a0f645e..ad8e658d 100644 --- a/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/util/ReflectionUtils.kt +++ b/dsl/src/main/kotlin/momosetkn/liquibase/kotlin/dsl/util/ReflectionUtils.kt @@ -4,9 +4,10 @@ import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor internal object ReflectionUtils { - inline fun KClass.new(): T { - return requireNotNull(this.primaryConstructor) { - "primaryConstructor is null. class=$this" + fun KClass.new(): T { + val constructor = this.primaryConstructor ?: this.constructors.find { it.parameters.isEmpty() } + return requireNotNull(constructor) { + "constructor is not found. class=$this" }.call() } diff --git a/gradle.properties b/gradle.properties index 6032bcb5..05d58878 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ liquibaseVersion=4.29.2 testcontainersVersion=1.20.1 artifactIdPrefix=liquibase-kotlin classgraphVersion=4.8.176 +slf4jVersion=2.0.16 liquibaseKotlinVersion=0.5.0 diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts index fda03ff5..41219087 100644 --- a/integration-test/build.gradle.kts +++ b/integration-test/build.gradle.kts @@ -1,8 +1,14 @@ val liquibaseVersion = rootProject.properties["liquibaseVersion"] as String val kotestVersion = rootProject.properties["kotestVersion"] as String val testcontainersVersion = rootProject.properties["testcontainersVersion"] as String +val slf4jVersion = rootProject.properties["slf4jVersion"] as String val komapperVersion = "3.0.0" +plugins { + id("com.google.devtools.ksp") version "2.0.20-1.0.25" + id("org.komapper.gradle") version "3.0.0" +} + dependencies { implementation(project(":dsl")) implementation(project(":script-serializer")) @@ -13,13 +19,19 @@ dependencies { implementation(project(":custom-komapper-jdbc-change")) // log - implementation("org.slf4j:slf4j-api:2.0.16") + implementation("org.slf4j:slf4j-api:$slf4jVersion") implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.0") implementation("org.apache.logging.log4j:log4j-api-kotlin:1.5.0") // komapper implementation("org.komapper:komapper-core:$komapperVersion") implementation("org.komapper:komapper-jdbc:$komapperVersion") + implementation("org.komapper:komapper-annotation:$komapperVersion") + platform("org.komapper:komapper-platform:$komapperVersion").let { + implementation(it) + ksp(it) + } + ksp("org.komapper:komapper-processor") implementation("org.komapper:komapper-dialect-postgresql-jdbc:$komapperVersion") // test @@ -41,3 +53,11 @@ tasks.test { useJUnitPlatform() systemProperty("kotest.framework.classpath.scanning.config.disable", "true") } + +tasks { + withType().configureEach { + compilerOptions { + freeCompilerArgs.add("-opt-in=org.komapper.annotation.KomapperExperimentalAssociation") + } + } +} diff --git a/integration-test/src/main/kotlin/komapper/entities.kt b/integration-test/src/main/kotlin/komapper/entities.kt new file mode 100644 index 00000000..5a188892 --- /dev/null +++ b/integration-test/src/main/kotlin/komapper/entities.kt @@ -0,0 +1,37 @@ +@file:Suppress("ktlint:standard:filename") + +package komapper + +import org.komapper.annotation.KomapperColumn +import org.komapper.annotation.KomapperEntity +import org.komapper.annotation.KomapperId +import org.komapper.annotation.KomapperTable +import java.time.LocalDateTime + +@KomapperEntity +@KomapperTable(name = "databasechangelog") +data class Databasechangelog( + @KomapperId @KomapperColumn(name = "id") val id: String, + @KomapperColumn(name = "author") val author: String, + @KomapperColumn(name = "filename") val filename: String, + @KomapperColumn(name = "dateexecuted") val dateexecuted: LocalDateTime, + @KomapperColumn(name = "orderexecuted") val orderexecuted: Int, + @KomapperColumn(name = "exectype") val exectype: String, + @KomapperColumn(name = "md5sum") val md5sum: String?, + @KomapperColumn(name = "description") val description: String?, + @KomapperColumn(name = "comments") val comments: String?, + @KomapperColumn(name = "tag") val tag: String?, + @KomapperColumn(name = "liquibase") val liquibase: String?, + @KomapperColumn(name = "contexts") val contexts: String?, + @KomapperColumn(name = "labels") val labels: String?, + @KomapperColumn(name = "deployment_id") val deploymentId: String? +) + +@KomapperEntity +@KomapperTable(name = "databasechangeloglock") +data class Databasechangeloglock( + @KomapperId @KomapperColumn(name = "id") val id: Int, + @KomapperColumn(name = "locked") val locked: Boolean, + @KomapperColumn(name = "lockgranted") val lockgranted: LocalDateTime?, + @KomapperColumn(name = "lockedby") val lockedby: String? +) diff --git a/integration-test/src/main/kotlin/komapper/liquibaseEntities.kt b/integration-test/src/main/kotlin/komapper/liquibaseEntities.kt new file mode 100644 index 00000000..27131a9f --- /dev/null +++ b/integration-test/src/main/kotlin/komapper/liquibaseEntities.kt @@ -0,0 +1,16 @@ +@file:Suppress("ktlint:standard:filename", "MatchingDeclarationName") + +package komapper + +import org.komapper.annotation.KomapperColumn +import org.komapper.annotation.KomapperEntity +import org.komapper.annotation.KomapperId +import org.komapper.annotation.KomapperTable +import java.util.UUID + +@KomapperEntity +@KomapperTable(name = "created_by_komapper") +data class CreatedByKomapper( + @KomapperId @KomapperColumn(name = "id") val id: UUID, + @KomapperColumn(name = "name") val name: String, +) diff --git a/integration-test/src/main/resources/ChangeSetSpec/sqlFile.sql b/integration-test/src/main/resources/ChangeSetSpec/sqlFile.sql new file mode 100644 index 00000000..bdf1d840 --- /dev/null +++ b/integration-test/src/main/resources/ChangeSetSpec/sqlFile.sql @@ -0,0 +1,6 @@ +-- this file is encoded "Shift_JIS" +create table "寿司" +( + "id" int primary key, + "寿司ネタの名前" varchar(255) +) diff --git a/integration-test/src/main/resources/log4j2.xml b/integration-test/src/main/resources/log4j2.xml index 9b7fcd17..3ded6284 100644 --- a/integration-test/src/main/resources/log4j2.xml +++ b/integration-test/src/main/resources/log4j2.xml @@ -11,5 +11,7 @@ + + diff --git a/integration-test/src/test/kotlin/momosetkn/ChangeSetSpec.kt b/integration-test/src/test/kotlin/momosetkn/ChangeSetSpec.kt new file mode 100644 index 00000000..e3b87ed1 --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/ChangeSetSpec.kt @@ -0,0 +1,336 @@ +package momosetkn + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import komapper.databasechangelog +import momosetkn.liquibase.client.LiquibaseClient +import momosetkn.liquibase.kotlin.dsl.ChangeSetDsl +import momosetkn.utils.DDLUtils.shouldBeEqualDdl +import momosetkn.utils.Database +import momosetkn.utils.DatabaseKomapperExtensions.komapperDb +import momosetkn.utils.InterchangeableChangeLog +import momosetkn.utils.set +import org.komapper.core.dsl.Meta +import org.komapper.core.dsl.QueryDsl +import org.komapper.core.dsl.query.single + +class ChangeSetSpec : FunSpec({ + beforeEach { + Database.start() + } + afterEach { + Database.stop() + } + val client = LiquibaseClient { + globalArgs { + general { + showBanner = false + } + } + } + fun subject() { + val container = Database.startedContainer + client.update( + driver = "org.postgresql.Driver", + url = container.jdbcUrl, + username = container.username, + password = container.password, + changelogFile = InterchangeableChangeLog::class.qualifiedName!!, + ) + } + + context("comment") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + comment("comment_123") // precedence this + comment("comment_456") + } + } + test("can migrate") { + subject() + val db = Database.komapperDb() + val d = Meta.databasechangelog + val result = db.runQuery { + QueryDsl.from(d).single() + } + result.comments shouldBe "comment_123" + } + } + context("preConditions") { + fun databaseUsername() = Database.startedContainer.username + context("postgresql and ") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + preConditions( + onFail = "MARK_RAN", + ) { + dbms(type = "postgresql") + runningAs(username = databaseUsername()) + } + createCompanyTable() + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.company ( + id uuid NOT NULL, + name character varying(256) + ); + ALTER TABLE public.company OWNER TO test; + ALTER TABLE ONLY public.company + ADD CONSTRAINT company_pkey PRIMARY KEY (id); + """.trimIndent() + ) + val db = Database.komapperDb() + val d = Meta.databasechangelog + val result = db.runQuery { + QueryDsl.from(d).single() + } + result.id shouldBe "100" + } + } + context("mysql and root") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + preConditions( + onFail = "MARK_RAN", + ) { + dbms(type = "mysql") + runningAs(username = "root") + } + createCompanyTable() + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl("") + val db = Database.komapperDb() + val d = Meta.databasechangelog + val result = db.runQuery { + QueryDsl.from(d).single() + } + result.exectype shouldBe "MARK_RAN" + } + } + context("(mysql and root) or (postgresql and currentUser)") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + preConditions( + onFail = "MARK_RAN", + ) { + or { + and { + dbms(type = "mysql") + runningAs(username = "root") + } + and { + dbms(type = "postgresql") + runningAs(username = databaseUsername()) + } + } + } + createCompanyTable() + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.company ( + id uuid NOT NULL, + name character varying(256) + ); + ALTER TABLE public.company OWNER TO test; + ALTER TABLE ONLY public.company + ADD CONSTRAINT company_pkey PRIMARY KEY (id); + """.trimIndent() + ) + val db = Database.komapperDb() + val d = Meta.databasechangelog + val result = db.runQuery { + QueryDsl.from(d).single() + } + result.id shouldBe "100" + } + } + } + + context("executeCommand") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + executeCommand( + executable = "docker", + timeout = "10s" + ) { + arg("ps") + } + } + } + test("can migrate") { + subject() + // confirm output docker help + } + } + context("output") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + output( + message = "output_message", + ) + } + } + test("can migrate") { + subject() + // confirm output "output_message" + } + } + + context("sql") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + sql( + """ + create table table_a ( + id int primary key, + name varchar(255) + ) + """.trimIndent() + ) + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.table_a ( + id integer NOT NULL, + name character varying(255) + ); + ALTER TABLE public.table_a OWNER TO test; + ALTER TABLE ONLY public.table_a + ADD CONSTRAINT table_a_pkey PRIMARY KEY (id); + """.trimIndent() + ) + } + } + context("sql(original)") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + sql { + """ + create table table_a ( + id int primary key, + name varchar(255) + ) + """.trimIndent() + } + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.table_a ( + id integer NOT NULL, + name character varying(255) + ); + ALTER TABLE public.table_a OWNER TO test; + ALTER TABLE ONLY public.table_a + ADD CONSTRAINT table_a_pkey PRIMARY KEY (id); + """.trimIndent() + ) + } + } + context("sqlFile") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + sqlFile( + path = "ChangeSetSpec/sqlFile.sql", + encoding = "Shift_JIS" + ) + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public."蟇ソ蜿ク" ( + "ス会ス" integer NOT NULL, + "蟇ソ蜿ク繝阪ち縺ョ蜷榊燕" character varying(255) + ); + ALTER TABLE public."蟇ソ蜿ク" OWNER TO test; + ALTER TABLE ONLY public."蟇ソ蜿ク" + ADD CONSTRAINT "蟇ソ蜿ク_pkey" PRIMARY KEY ("ス会ス"); + """.trimIndent() + ) + } + } + context("stop") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + createTable(tableName = "company") { + column(name = "id", type = "UUID") { + constraints(nullable = false, primaryKey = true) + } + column(name = "name", type = "VARCHAR(256)") + } + } + changeSet(author = "user", id = "200") { + stop("stop") + } + changeSet(author = "user", id = "300") { + // not executed + createTable(tableName = "company") { + column(name = "id", type = "UUID") { + constraints(nullable = false, primaryKey = true) + } + column(name = "name", type = "VARCHAR(256)") + } + } + } + test("throw error") { + shouldThrow { + subject() + } + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.company ( + id uuid NOT NULL, + name character varying(256) + ); + ALTER TABLE public.company OWNER TO test; + ALTER TABLE ONLY public.company + ADD CONSTRAINT company_pkey PRIMARY KEY (id); + """.trimIndent() + ) + } + } + context("tagDatabase") { + InterchangeableChangeLog.set { + changeSet(author = "user", id = "100") { + tagDatabase("example_tag1") + } + } + test("can migrate") { + subject() + val db = Database.komapperDb() + val d = Meta.databasechangelog + val result = db.runQuery { + QueryDsl.from(d).single() + } + result.tag shouldBe "example_tag1" + } + } +}) + +private fun ChangeSetDsl.createCompanyTable() { + createTable(tableName = "company") { + column(name = "id", type = "UUID") { + constraints(nullable = false, primaryKey = true) + } + column(name = "name", type = "VARCHAR(256)") + } +} diff --git a/integration-test/src/test/kotlin/momosetkn/CustomChangeMigrationSpec.kt b/integration-test/src/test/kotlin/momosetkn/CustomChangeMigrationSpec.kt index fd7618fc..16d3bf7e 100644 --- a/integration-test/src/test/kotlin/momosetkn/CustomChangeMigrationSpec.kt +++ b/integration-test/src/test/kotlin/momosetkn/CustomChangeMigrationSpec.kt @@ -2,17 +2,18 @@ package momosetkn import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import momosetkn.Constants.RESOURCE_DIR import momosetkn.liquibase.changelogs.customechannge.ExecuteCountTaskCustomChange import momosetkn.liquibase.client.LiquibaseClient import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog import momosetkn.liquibase.kotlin.serializer.KotlinCompiledChangeLogSerializer +import momosetkn.utils.Constants +import momosetkn.utils.Database import java.nio.file.Paths class CustomChangeMigrationSpec : FunSpec({ beforeSpec { Database.start() - KotlinCompiledChangeLogSerializer.sourceRootPath = Paths.get(RESOURCE_DIR) + KotlinCompiledChangeLogSerializer.sourceRootPath = Paths.get(Constants.RESOURCE_DIR) } afterSpec { Database.stop() diff --git a/integration-test/src/test/kotlin/momosetkn/CustomKomapperJdbcChangeSetSpec.kt b/integration-test/src/test/kotlin/momosetkn/CustomKomapperJdbcChangeSetSpec.kt new file mode 100644 index 00000000..05f029dc --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/CustomKomapperJdbcChangeSetSpec.kt @@ -0,0 +1,214 @@ +package momosetkn + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import komapper.CreatedByKomapper +import komapper.createdByKomapper +import momosetkn.liquibase.client.LiquibaseClient +import momosetkn.liquibase.kotlin.change.customKomapperJdbcChange +import momosetkn.utils.DDLUtils.shouldBeEqualDdl +import momosetkn.utils.Database +import momosetkn.utils.DatabaseKomapperExtensions.komapperDb +import momosetkn.utils.InterchangeableChangeLog +import momosetkn.utils.set +import org.komapper.core.dsl.Meta +import org.komapper.core.dsl.QueryDsl +import org.komapper.core.dsl.query.single +import java.util.UUID + +class CustomKomapperJdbcChangeSetSpec : FunSpec({ + beforeEach { + Database.start() + } + afterEach { + Database.stop() + } + val client = LiquibaseClient { + globalArgs { + general { + showBanner = false + } + } + } + + context("forwardOnly") { + fun subject() { + val container = Database.startedContainer + client.update( + driver = "org.postgresql.Driver", + url = container.jdbcUrl, + username = container.username, + password = container.password, + changelogFile = InterchangeableChangeLog::class.qualifiedName!!, + ) + } + val c = Meta.createdByKomapper + InterchangeableChangeLog.set { + changeSet(author = "momose", id = "100-10") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.executeScript( + """ + CREATE TABLE created_by_komapper ( + id uuid primary key, + name character varying(255) + ); + """.trimIndent() + ) + db.runQuery(query) + }, + rollback = { db -> + val query = QueryDsl.executeScript("DROP TABLE created_by_komapper") + db.runQuery(query) + }, + ) + } + changeSet(author = "momose", id = "100-20") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.insert(c).single( + CreatedByKomapper( + id = UUID.randomUUID(), + name = "CreatedByKomapper_name", + ) + ) + db.runQuery(query) + }, + ) + } + } + test("can migrate") { + subject() + Database.shouldBeEqualDdl( + """ + CREATE TABLE public.created_by_komapper ( + id uuid NOT NULL, + name character varying(255) + ); + ALTER TABLE public.created_by_komapper OWNER TO test; + ALTER TABLE ONLY public.created_by_komapper + ADD CONSTRAINT created_by_komapper_pkey PRIMARY KEY (id); + """.trimIndent() + ) + val db = Database.komapperDb() + val query = QueryDsl.from(c).single() + val item = db.runQuery(query) + item.name shouldBe "CreatedByKomapper_name" + } + } + + context("forward and rollback") { + val startedTag = "started" + fun subject() { + val container = Database.startedContainer + client.update( + driver = "org.postgresql.Driver", + url = container.jdbcUrl, + username = container.username, + password = container.password, + changelogFile = InterchangeableChangeLog::class.qualifiedName!!, + ) + client.rollback( + driver = "org.postgresql.Driver", + url = container.jdbcUrl, + username = container.username, + password = container.password, + changelogFile = InterchangeableChangeLog::class.qualifiedName!!, + tag = startedTag, + ) + } + val c = Meta.createdByKomapper + context("rollback arg is given") { + InterchangeableChangeLog.set { + changeSet(author = "momose", id = "0-10") { + tagDatabase(startedTag) + } + changeSet(author = "momose", id = "100-10") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.executeScript( + """ + CREATE TABLE created_by_komapper ( + id uuid primary key, + name character varying(255) + ); + """.trimIndent() + ) + db.runQuery(query) + }, + rollback = { db -> + val query = QueryDsl.executeScript("DROP TABLE created_by_komapper") + db.runQuery(query) + }, + ) + } + changeSet(author = "momose", id = "100-20") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.insert(c).single( + CreatedByKomapper( + id = UUID.randomUUID(), + name = "CreatedByKomapper_name", + ) + ) + db.runQuery(query) + }, + rollback = { db -> + val query = QueryDsl.delete(c).all() + db.runQuery(query) + } + ) + } + } + test("can rollback") { + subject() + Database.shouldBeEqualDdl("") + } + } + context("rollback arg is none") { + InterchangeableChangeLog.set { + changeSet(author = "momose", id = "0-10") { + tagDatabase(startedTag) + } + changeSet(author = "momose", id = "100-10") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.executeScript( + """ + CREATE TABLE created_by_komapper ( + id uuid primary key, + name character varying(255) + ); + """.trimIndent() + ) + db.runQuery(query) + }, + rollback = { db -> + val query = QueryDsl.executeScript("DROP TABLE created_by_komapper") + db.runQuery(query) + }, + ) + } + changeSet(author = "momose", id = "100-20") { + customKomapperJdbcChange( + execute = { db -> + val query = QueryDsl.insert(c).single( + CreatedByKomapper( + id = UUID.randomUUID(), + name = "CreatedByKomapper_name", + ) + ) + db.runQuery(query) + } + ) + } + } + test("throw error") { + shouldThrow { + subject() + } + } + } + } +}) diff --git a/integration-test/src/test/kotlin/momosetkn/KotlinCompiledMigrateAndSerializeSpec.kt b/integration-test/src/test/kotlin/momosetkn/KotlinCompiledMigrateAndSerializeSpec.kt index f549f19a..11b312a4 100644 --- a/integration-test/src/test/kotlin/momosetkn/KotlinCompiledMigrateAndSerializeSpec.kt +++ b/integration-test/src/test/kotlin/momosetkn/KotlinCompiledMigrateAndSerializeSpec.kt @@ -2,17 +2,18 @@ package momosetkn import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import momosetkn.Constants.RESOURCE_DIR -import momosetkn.ResourceUtils.getResourceAsString import momosetkn.liquibase.changelogs.CompiledDatabaseChangelogAll import momosetkn.liquibase.client.LiquibaseClient import momosetkn.liquibase.kotlin.serializer.KotlinCompiledChangeLogSerializer +import momosetkn.utils.Constants +import momosetkn.utils.Database +import momosetkn.utils.ResourceUtils.getResourceAsString import java.nio.file.Paths class KotlinCompiledMigrateAndSerializeSpec : FunSpec({ beforeSpec { Database.start() - KotlinCompiledChangeLogSerializer.sourceRootPath = Paths.get(RESOURCE_DIR) + KotlinCompiledChangeLogSerializer.sourceRootPath = Paths.get(Constants.RESOURCE_DIR) } afterSpec { Database.stop() @@ -51,7 +52,7 @@ class KotlinCompiledMigrateAndSerializeSpec : FunSpec({ changelogFile = PARSER_INPUT_CHANGELOG, ) val actualSerializedChangeLogFile = - Paths.get(RESOURCE_DIR, SERIALIZER_ACTUAL_CHANGELOG) + Paths.get(Constants.RESOURCE_DIR, SERIALIZER_ACTUAL_CHANGELOG) val f = actualSerializedChangeLogFile.toFile() if (f.exists()) f.delete() client.generateChangelog( @@ -78,7 +79,7 @@ class KotlinCompiledMigrateAndSerializeSpec : FunSpec({ }) { companion object { private fun getFileAsString(path: String) = - Paths.get(RESOURCE_DIR, path).toFile().readText() + Paths.get(Constants.RESOURCE_DIR, path).toFile().readText() private val changeSetRegex = Regex("""changeSet\(author = "(.+)", id = "(\d+)-(\d)"\) \{""") diff --git a/integration-test/src/test/kotlin/momosetkn/KotlinScriptMigrateAndSerializeSpec.kt b/integration-test/src/test/kotlin/momosetkn/KotlinScriptMigrateAndSerializeSpec.kt index 009e65a9..b532a575 100644 --- a/integration-test/src/test/kotlin/momosetkn/KotlinScriptMigrateAndSerializeSpec.kt +++ b/integration-test/src/test/kotlin/momosetkn/KotlinScriptMigrateAndSerializeSpec.kt @@ -2,8 +2,11 @@ package momosetkn import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import momosetkn.ResourceUtils.getResourceAsString import momosetkn.liquibase.client.LiquibaseClient +import momosetkn.utils.Constants +import momosetkn.utils.Database +import momosetkn.utils.ResourceUtils +import momosetkn.utils.ResourceUtils.getResourceAsString import java.nio.file.Paths class KotlinScriptMigrateAndSerializeSpec : FunSpec({ @@ -47,7 +50,7 @@ class KotlinScriptMigrateAndSerializeSpec : FunSpec({ changelogFile = PARSER_INPUT_CHANGELOG, ) val actualSerializedChangeLogFile = - Paths.get(RESOURCE_DIR, SERIALIZER_ACTUAL_CHANGELOG) + Paths.get(Constants.RESOURCE_DIR, SERIALIZER_ACTUAL_CHANGELOG) val f = actualSerializedChangeLogFile.toFile() if (f.exists()) f.delete() client.generateChangelog( @@ -64,7 +67,7 @@ class KotlinScriptMigrateAndSerializeSpec : FunSpec({ ddl shouldBe expectedDdl // check serializer - val actual = getFileAsString(SERIALIZER_ACTUAL_CHANGELOG) + val actual = ResourceUtils.getResourceFileAsString(SERIALIZER_ACTUAL_CHANGELOG) .maskingChangeSet() val expect = getResourceAsString(SERIALIZER_EXPECT_CHANGELOG) .maskingChangeSet() @@ -73,9 +76,6 @@ class KotlinScriptMigrateAndSerializeSpec : FunSpec({ } }) { companion object { - private fun getFileAsString(path: String) = - Paths.get(RESOURCE_DIR, path).toFile().readText() - private val changeSetRegex = Regex("""changeSet\(author = "(.+)", id = "(\d+)-(\d)"\) \{""") private fun String.maskingChangeSet() = @@ -93,6 +93,5 @@ class KotlinScriptMigrateAndSerializeSpec : FunSpec({ private const val SERIALIZER_EXPECT_CHANGELOG = "KotlinScriptMigrateAndSerializeSpec/serializer_expect/db.changelog-0_kts" private const val PARSER_EXPECT_DDL = "KotlinScriptMigrateAndSerializeSpec/parser_expect/db.ddl-0.sql" - private const val RESOURCE_DIR = "src/main/resources" } } diff --git a/integration-test/src/test/kotlin/momosetkn/ResourceUtils.kt b/integration-test/src/test/kotlin/momosetkn/ResourceUtils.kt deleted file mode 100644 index bd29466b..00000000 --- a/integration-test/src/test/kotlin/momosetkn/ResourceUtils.kt +++ /dev/null @@ -1,10 +0,0 @@ -package momosetkn - -object ResourceUtils { - internal fun getResourceAsString(path: String) = - Thread.currentThread() - .contextClassLoader - .getResourceAsStream(path)!! - .readAllBytes() - .let { String(it) } -} diff --git a/integration-test/src/test/kotlin/momosetkn/Constants.kt b/integration-test/src/test/kotlin/momosetkn/utils/Constants.kt similarity index 75% rename from integration-test/src/test/kotlin/momosetkn/Constants.kt rename to integration-test/src/test/kotlin/momosetkn/utils/Constants.kt index ef0eb8b5..9158b685 100644 --- a/integration-test/src/test/kotlin/momosetkn/Constants.kt +++ b/integration-test/src/test/kotlin/momosetkn/utils/Constants.kt @@ -1,4 +1,4 @@ -package momosetkn +package momosetkn.utils object Constants { const val RESOURCE_DIR = "src/main/resources" diff --git a/integration-test/src/test/kotlin/momosetkn/utils/DDLUtils.kt b/integration-test/src/test/kotlin/momosetkn/utils/DDLUtils.kt new file mode 100644 index 00000000..21610ce8 --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/utils/DDLUtils.kt @@ -0,0 +1,97 @@ +package momosetkn.utils + +import io.kotest.matchers.shouldBe +import org.intellij.lang.annotations.Language + +object DDLUtils { + fun String.omitComment(): String { + val commentRegex = Regex("""^--.*$""", RegexOption.MULTILINE) + return this.replace(commentRegex, "") + } + fun String.omitLiquibaseTable(): String { + return this.replace( + """ + -- + -- Name: databasechangelog; Type: TABLE; Schema: public; Owner: test + -- + + CREATE TABLE public.databasechangelog ( + id character varying(255) NOT NULL, + author character varying(255) NOT NULL, + filename character varying(255) NOT NULL, + dateexecuted timestamp without time zone NOT NULL, + orderexecuted integer NOT NULL, + exectype character varying(10) NOT NULL, + md5sum character varying(35), + description character varying(255), + comments character varying(255), + tag character varying(255), + liquibase character varying(20), + contexts character varying(255), + labels character varying(255), + deployment_id character varying(10) + ); + + + ALTER TABLE public.databasechangelog OWNER TO test; + + -- + -- Name: databasechangeloglock; Type: TABLE; Schema: public; Owner: test + -- + + CREATE TABLE public.databasechangeloglock ( + id integer NOT NULL, + locked boolean NOT NULL, + lockgranted timestamp without time zone, + lockedby character varying(255) + ); + + + ALTER TABLE public.databasechangeloglock OWNER TO test; + """.trimIndent(), + "", + ).replace( + """ + ALTER TABLE ONLY public.databasechangeloglock + ADD CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (id); + """.trimIndent(), + "" + ) + } + fun String.omitVariable(): String { + return this.replace( + """ + SET statement_timeout = 0; + SET lock_timeout = 0; + SET idle_in_transaction_session_timeout = 0; + SET client_encoding = 'UTF8'; + SET standard_conforming_strings = on; + SELECT pg_catalog.set_config('search_path', '', false); + SET check_function_bodies = false; + SET xmloption = content; + SET client_min_messages = warning; + SET row_security = off; + + SET default_tablespace = ''; + + SET default_table_access_method = heap; + """.trimIndent(), + "", + ) + } + fun String.normalize(): String { + val newlineRegex = Regex("""\n+""") + return this.replace(newlineRegex, "\n").trim() + } + + fun String.toMainDdl(): String { + return this.omitLiquibaseTable().omitVariable().omitComment().normalize() + } + + infix fun Database.shouldBeEqualDdl( + @Language("sql") s: String + ) { + val ddl = this.generateDdl()!!.toMainDdl() + ddl shouldBe s + } +} diff --git a/integration-test/src/test/kotlin/momosetkn/Database.kt b/integration-test/src/test/kotlin/momosetkn/utils/Database.kt similarity index 98% rename from integration-test/src/test/kotlin/momosetkn/Database.kt rename to integration-test/src/test/kotlin/momosetkn/utils/Database.kt index cb125f97..b02673c1 100644 --- a/integration-test/src/test/kotlin/momosetkn/Database.kt +++ b/integration-test/src/test/kotlin/momosetkn/utils/Database.kt @@ -1,4 +1,4 @@ -package momosetkn +package momosetkn.utils import org.slf4j.LoggerFactory import org.testcontainers.containers.Container diff --git a/integration-test/src/test/kotlin/momosetkn/utils/DatabaseKomapperExtensions.kt b/integration-test/src/test/kotlin/momosetkn/utils/DatabaseKomapperExtensions.kt new file mode 100644 index 00000000..12fd5693 --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/utils/DatabaseKomapperExtensions.kt @@ -0,0 +1,11 @@ +package momosetkn.utils + +import org.komapper.jdbc.JdbcDatabase + +object DatabaseKomapperExtensions { + fun Database.komapperDb() = JdbcDatabase( + url = this.startedContainer.jdbcUrl, + user = this.startedContainer.username, + password = this.startedContainer.password, + ) +} diff --git a/integration-test/src/test/kotlin/momosetkn/utils/InterchangeableChangeLog.kt b/integration-test/src/test/kotlin/momosetkn/utils/InterchangeableChangeLog.kt new file mode 100644 index 00000000..3cc90cd3 --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/utils/InterchangeableChangeLog.kt @@ -0,0 +1,17 @@ +package momosetkn.utils + +import momosetkn.liquibase.kotlin.parser.KotlinCompiledDatabaseChangeLog + +class InterchangeableChangeLog : KotlinCompiledDatabaseChangeLog({ + mutableDsl(this) +}) { + companion object +} + +private var mutableDsl: momosetkn.liquibase.kotlin.dsl.ChangeLogDsl.() -> Unit = {} + +fun InterchangeableChangeLog.Companion.set( + block: momosetkn.liquibase.kotlin.dsl.ChangeLogDsl.() -> Unit +) { + mutableDsl = block +} diff --git a/integration-test/src/test/kotlin/momosetkn/utils/LanguageUtils.kt b/integration-test/src/test/kotlin/momosetkn/utils/LanguageUtils.kt new file mode 100644 index 00000000..ffe747d3 --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/utils/LanguageUtils.kt @@ -0,0 +1,9 @@ +package momosetkn.utils + +import org.intellij.lang.annotations.Language + +object LanguageUtils { + fun sql( + @Language("sql") s: String + ) = s +} diff --git a/integration-test/src/test/kotlin/momosetkn/utils/ResourceUtils.kt b/integration-test/src/test/kotlin/momosetkn/utils/ResourceUtils.kt new file mode 100644 index 00000000..3b6836ff --- /dev/null +++ b/integration-test/src/test/kotlin/momosetkn/utils/ResourceUtils.kt @@ -0,0 +1,14 @@ +package momosetkn.utils + +import java.nio.file.Paths + +object ResourceUtils { + fun getResourceAsString(path: String) = + Thread.currentThread() + .contextClassLoader + .getResourceAsStream(path)!! + .readAllBytes() + .let { String(it) } + fun getResourceFileAsString(path: String) = + Paths.get(Constants.RESOURCE_DIR, path).toFile().readText() +} diff --git a/script-parser/build.gradle.kts b/script-parser/build.gradle.kts index e820e2a8..dd55c980 100644 --- a/script-parser/build.gradle.kts +++ b/script-parser/build.gradle.kts @@ -2,6 +2,7 @@ val liquibaseVersion = rootProject.properties["liquibaseVersion"] as String val kotestVersion = rootProject.properties["kotestVersion"] as String val kotlinVersion = rootProject.properties["kotlinVersion"] as String val liquibaseKotlinVersion = rootProject.properties["liquibaseKotlinVersion"] as String +val slf4jVersion = rootProject.properties["slf4jVersion"] as String dependencies { // liquibase-kotlin @@ -15,6 +16,8 @@ dependencies { api("org.jetbrains.kotlin:kotlin-scripting-dependencies:$kotlinVersion") api("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven:$kotlinVersion") api("org.jetbrains.kotlin:kotlin-script-util:1.8.22") + // log + api("org.slf4j:slf4j-api:$slf4jVersion") // test testImplementation("io.kotest:kotest-framework-engine-jvm:$kotestVersion") diff --git a/script-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinScriptLiquibaseChangeLogParser.kt b/script-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinScriptLiquibaseChangeLogParser.kt index e1bef3ba..8a54e2fc 100644 --- a/script-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinScriptLiquibaseChangeLogParser.kt +++ b/script-parser/src/main/kotlin/momosetkn/liquibase/kotlin/parser/KotlinScriptLiquibaseChangeLogParser.kt @@ -11,6 +11,8 @@ import java.io.InputStreamReader import java.util.ServiceLoader class KotlinScriptLiquibaseChangeLogParser : ChangeLogParser { + private val log = org.slf4j.LoggerFactory.getLogger(this::class.java) + private val imports = ServiceLoader.load(KotlinScriptParserImports::class.java) .flatMap { it.imports() } @@ -21,21 +23,27 @@ class KotlinScriptLiquibaseChangeLogParser : ChangeLogParser { ): DatabaseChangeLog { // windows path to a multi-platform path val fixedPhysicalChangeLogLocation = physicalChangeLogLocation.replace("\\\\", "/") - val changeLogScript = - getChangeLogKotlinScriptCode( + val databaseChangeLog = DatabaseChangeLog(fixedPhysicalChangeLogLocation) + return runCatching { + val changeLogScript = + getChangeLogKotlinScriptCode( + resourceAccessor = resourceAccessor, + physicalChangeLogLocation = fixedPhysicalChangeLogLocation, + ) + updateChangeLog( + filePath = fixedPhysicalChangeLogLocation, + databaseChangeLog = databaseChangeLog, + changeLogParameters = changeLogParameters, resourceAccessor = resourceAccessor, - physicalChangeLogLocation = fixedPhysicalChangeLogLocation, + changeLogScript = changeLogScript, ) - - val databaseChangeLog = DatabaseChangeLog(fixedPhysicalChangeLogLocation) - updateChangeLog( - filePath = fixedPhysicalChangeLogLocation, - databaseChangeLog = databaseChangeLog, - changeLogParameters = changeLogParameters, - resourceAccessor = resourceAccessor, - changeLogScript = changeLogScript, + }.fold( + onSuccess = { databaseChangeLog }, + onFailure = { + log.error("error in KotlinScriptLiquibaseChangeLogParser", it) + throw it + } ) - return databaseChangeLog } private fun updateChangeLog(