# Mikrator (a Liquibase wrapper): changelogs as code, easy usage

**URL:** <https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452>\
**Category:** Uncategorized\
**Created:** [June 8, 2025, 12:26pm UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452 "2025-06-08T12:26:29Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![frozenice](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/frozenice/32/7436_2.png) [@frozenice](https://discuss.kotlinlang.org/u/frozenice)\
**Post date:** [June 8, 2025, 12:26pm UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/1 "2025-06-08T12:26:29Z")

</div>

I always wanted to integrate a database migration tool, like Liquibase, into projects. But I didn’t quite like the untyped changelog files or some APIs. I love Kotlin DSLs and static typing, so I had the idea of creating a wrapper that supplies both.

Here is the first PoC alpha version of that: [GitHub - Batix/mikrator: A Liquibase wrapper for easy usage and changelogs as code.](https://github.com/Batix/mikrator)

To save you a click, I reproduce the example code below:

```kotlin
val config = Config {
  sqlShowAtLogLevel = Level.INFO
}

val changeLog = ChangeLog {
  changeSet("1-create", "John Doe") {
    changes {
      createTable("tbl") {
        column("id", "INT")
      }
    }
  }

  changeSet("2-insert", "John Doe") {
    changes {
      insert("tbl") {
        column("id") { numericValue(1) }
      }

      tagDatabase("v1")
    }
  }

  changeSet("3-insert", "John Doe") {
    changes {
      insert("tbl") {
        column("id") { numericValue(2) }
      }
    }

    rollback {
      delete("tbl", where = "id = 2")
    }
  }
}

Mikrator(openDatabase(sqlConn), config).use { mikrator ->
  mikrator.update(changeLog)

  if (weNeedToGoBack && mikrator.tagExists("v1")) {
    mikrator.rollback(changeLog, "v1")
  }

  logger.trace { "db snapshot:\n${mikrator.snapshot().serialize(FileFormat.YAML)}" }
}

```

What do you think? How do you do database migrations?

---

<div class="post-metadata">

**Author:** ![Skater901](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/skater901/32/10163_2.png) [@Skater901](https://discuss.kotlinlang.org/u/Skater901)\
**Post date:** [June 9, 2025, 11:34am UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/2 "2025-06-09T11:34:45Z")

</div>

Looks good, but I’d like to see some type safety around specifying column types, rather than just writing “INT”.

Also, it’d be super cool if the DSL could somehow track the structure of the table, and prevent you from inserting the wrong data type. IE, in the second change set, you have `numericValue(1)`. It’d be awesome if the DSL could know whether `numericValue` is valid for that column in that table. That said, I know this would probably be a pretty big feature, and also probably kind of replicates what the database (and maybe Liquibase) does already.

---

<div class="post-metadata">

**Author:** ![frozenice](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/frozenice/32/7436_2.png) [@frozenice](https://discuss.kotlinlang.org/u/frozenice)\
**Post date:** [June 9, 2025, 3:59pm UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/3 "2025-06-09T15:59:07Z")

</div>

Thanks! Codifying the possible column types would be cool, but it’s not feasible, because those are pretty dynamic. E.g. you have different available column types for different database types / servers. You can run changesets for specific database types with the `dbms` parameter. So the possible column types you can choose depend on the database type the changelog is run against and the specified `dbms` parameter (which can also be someting like “mssql,oracle,mysql,!mariadb”). I guess there is even more complexity to it.

The `*Value` methods (or the Liquibase internals) do some auto-converting between types. So you always have a set of choices, e.g. it would also be valid to use `stringValue("1")` or `booleanValue(true)` here.

Regarding tracking table structure: tables evolve over time, that’s what a migration tool is for. 🙂 Tracking the structure is not a good fit here, and also not feasible (you would need to track that table A, even in the same changeset, is A\_1 after `createTable` and A\_2 after `addColumn` etc.).

The changelog records _changes_, not complete, wanted states. You can do something like “sync the structure of my entity classes to the database” with Exposed’s `SchemaUtils` or [`MigrationUtils`](https://www.jetbrains.com/help/exposed/migrations.html), but those have their limits (that’s why I wanted to try out a migration tool).

A Kotlin tool with those features would be cool, but I feel that would need to be created from the ground up, this is just a wrapper around Liquibase - with the implication that you need to read up on Liquibase stuff, but the benefit of having a nicer way of specifying things and using it.

---

<div class="post-metadata">

**Author:** ![Skater901](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/skater901/32/10163_2.png) [@Skater901](https://discuss.kotlinlang.org/u/Skater901)\
**Post date:** [June 10, 2025, 7:42am UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/4 "2025-06-10T07:42:05Z")

</div>

> [@frozenice](#):
>
> Thanks! Codifying the possible column types would be cool, but it’s not feasible, because those are pretty dynamic. E.g. you have different available column types for different database types / servers. You can run changesets for specific database types with the `dbms` parameter. So the possible column types you can choose depend on the database type the changelog is run against and the specified `dbms` parameter (which can also be someting like “mssql,oracle,mysql,!mariadb”). I guess there is even more complexity to it.
> 
> The `*Value` methods (or the Liquibase internals) do some auto-converting between types. So you always have a set of choices, e.g. it would also be valid to use `stringValue("1")` or `booleanValue(true)` here.

Thing is, in your original post, you said this:

> [@frozenice](#):
>
> But I didn’t quite like the untyped changelog files or some APIs.

I agree. XML/JSON is just text, and while you can have IDE plugins that can validate your Liquibase migration files for you, it’s even better if you have compile-time type checking. But what you’ve created is a static structure that mimics the XML/JSON structure, but still leaves the data types completely untyped. IMO, I’m much more likely to get a specific data type wrong than I am to get “createTable” or “changeSet” wrong.

I get that different databases have different data types. However, there’s a lot of common types, and I would say more common types than there are different types.

I would propose two changes. Firstly, create a defined set of the common types as an enum that can be passed to the `column` function. Second, potentially create one enum per database type that contains all the unique data types for that specific database, _or_ just leave the `column` function that accepts a String as the second argument. That way, for common types that are shared across most/all databases, you get type safety, but then you get the flexibility for custom types specific to certain databases, by passing in a String.

---

<div class="post-metadata">

**Author:** ![frozenice](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/frozenice/32/7436_2.png) [@frozenice](https://discuss.kotlinlang.org/u/frozenice)\
**Post date:** [June 10, 2025, 4:59pm UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/5 "2025-06-10T16:59:37Z")

</div>

The main things about “untyped changelog files” I wanted to get rid of were questions like “what changes are available? with what parameters?” and the need for file-system / classpath-resources access (via `ResourceAccessor`). Maybe there are schemas available for the Liquibase JSON / YAML format to solve the first, I don’t know, but those also have their limits. So I made this. 🙂

Regarding the typing of columns: An enum alone wouldn’t be enough, as types can have parameters, like `varchar(100)` or `INT UNSIGNED` (for specific databases only), so either something with sealed classes or methods with parameters (`varcharColumn(100)`) would be needed. Probably methods, as those are more easily discoverable.

_Aside: Kinda off-topic, but somehow IntelliJ does a poor autocompletion job lately, or is it just me? Try setting `objectQuotingStrategy` in `ChangeLog()`. It does not display the enum or its entries._ 🤷‍♂️

Regarding the typing of values: Just to surface some complexity, there are different contexts where `column() {}` can be used:

- `createTable`: inside `column() {}` (with many parameters like `autoIncrement`) there’s `constraints()`
  - and `default {}` (in which you can call the `*Value` methods)

- `addColumn`: the same (with some other parameters like `afterColumn`)
  - additionally there’s `value {}`, also with `*Value` methods (including some additional ones for current sequence value and blob / clob)

- `insert` (for `VALUES`), `delete` (for `WHERE`): here `column() {}` has no parameters (except `name`)
  - the `*Value` methods are available directly inside `column() {}`

- `update` (two times: for `SET` and for `WHERE`)
  - same

Also, I don’t really know what the common types across all databases are (a subset of [those](https://en.wikipedia.org/wiki/SQL#Predefined_data_types)?). [Liquibase Data Type Handling](https://docs.liquibase.com/concepts/data-type-handling.html) can be a starting point, but leaves much to be desired (many empty cells). What would we do, if a database is added, that doesn’t support a (up to that point) common type? Probably just keep it as-is.

Btw, getting the data type wrong is not that bad, because of the auto-conversion (either from Liquibase or the database).

I see the appeal and am always in favor of more static typing, but I feel all this is a bit too dynamic. 😕 That doesn’t mean I’m completely against it, maybe someone comes up with a nice solution.

---

<div class="post-metadata">

**Author:** ![momosetkn](https://avatars.discourse-cdn.com/v4/letter/m/47e85d/32.png) [@momosetkn](https://discuss.kotlinlang.org/u/momosetkn)\
**Post date:** [June 14, 2025, 10:27am UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/6 "2025-06-14T10:27:56Z")

</div>

I’m also developing a Kotlin wrapper for Liquibase (Kotlin DSL and ORM integration):

> **[GitHub - momosetkn/liquibase-kotlin: Liquibase kotlin(DSL, Wrapper client, ORM...](https://github.com/momosetkn/liquibase-kotlin)**
>
> Liquibase kotlin(DSL, Wrapper client, ORM integration)

Currently, database types like “INT” have to be written as strings, but what do you think about solving this by letting users define them as enums on their side and then using those enums?  
Has different data types depending on the database…

example

```kotlin
enum class Types(val sqlType: String) {
    UUID("UUID"),
    Int("Int"),
    DateTime("DATETIME"),
    Email("VARCHAR(320)"),
}
    changeSet(author = "momose", id = "100-10") {
        createTable(tableName = "company") {
            column(name = "id", type = Types.UUID.sqlType) {
                constraints(nullable = false, primaryKey = true)
            }
            column(name = "email", type = Types.Email.sqlType)
        }
    }

```

---

<div class="post-metadata">

**Author:** ![frozenice](https://sea1.discourse-cdn.com/flex019/user_avatar/discuss.kotlinlang.org/frozenice/32/7436_2.png) [@frozenice](https://discuss.kotlinlang.org/u/frozenice)\
**Post date:** [June 15, 2025, 7:39am UTC](https://discuss.kotlinlang.org/t/mikrator-a-liquibase-wrapper-changelogs-as-code-easy-usage/30452/7 "2025-06-15T07:39:27Z")

</div>

Oh hey, big project! Congrats on that.

> [@momosetkn](#):
>
> letting users define them as enums on their side and then using those enums

Sure, users are free to do what they want on the use-site. This doesn’t need a change in the library, so that’s an easy way to organize things, I guess.

One thing that sticks out to me in the example, though, is `Email("VARCHAR(320)")`. In my view, changelogs are relative, not absolute. What I mean by this:

Let’s say you started with `email` as `VARCHAR(100)` in your initial changeset and later realized it’s too small. So you extend it to `VARCHAR(320)` in another changeset. You can’t reference `Types.Email` in both changesets, because if you change the definition of that, it would change the older changeset (and its checksum, which would manifest in errors from Liquibase).

So I’d rather ‘hardcode’ most things in changesets, to preserve their original state. Another one could be: _`Int("Int")` looks different from the others, let’s change it to `Int("INT")`_, though I don’t know if that would result in a different checksum.

A possible improvement could be what I mentioned above: don’t just use an enum, but build something with sealed classes and / or methods taking parameters (for e.g. the varchar length).
