documentation-website/Writerside/topics/Migrations.md
Managing database schema changes is a critical part of application development. Exposed offers several tools to help with schema migrations, allowing you to evolve your database alongside your codebase.
While Exposed provides basic migration support through SchemaUtils,
the MigrationUtils methods from either the exposed-migration-jdbc or exposed-migration-r2dbc packages
provide a more structured and production-ready way to manage schema changes. They allow you to inspect differences between the current
database state and your defined table schema and to generate or apply migration scripts accordingly.
To use the methods provided by MigrationUtils, include the following dependencies in your build script:
exposed-migration-core, containing core common functionality for database schema migrations.When you need to bring your database schema in line with your current Exposed table definitions, you have three options:
If you only need the SQL statements that create any columns that are missing from the existing
tables in the database, use the SchemaUtils.addMissingColumnsStatements() function:
{src="exposed-migrations/src/main/kotlin/org/example/App.kt" include-symbol="missingColStatements"}
This function returns a collection of string SQL statements, ensuring that any column-associated constraints are aligned. As it adds missing columns, it simultaneously adds any associated constraints such as primary keys, indexes, and foreign keys that may be absent.
For database-specific constraints, see the limitations section.
To compare your live database schema against your current Exposed table definitions and generate all statements
required to align the two, use the MigrationUtils.statementsRequiredForDatabaseMigration() function:
{src="exposed-migrations/src/main/kotlin/org/example/App.kt" include-symbol="statements"}
The returned collection of string SQL statements may include CREATE, ALTER, and DROP operations — including potentially destructive actions like DROP COLUMN
or DELETE, so review them carefully before choosing to execute them.
For database-specific constraints, see the limitations section.
To generate a migration script based on schema differences between your database and the current Exposed model, use the
MigrationUtils.generateMigrationScript() function:
{src="exposed-migrations/src/main/kotlin/org/example/GenerateMigrationScript.kt" include-lines="36-40"}
This method allows you to see what the migration script will look like before applying the migration. If a migration script with the same name already exists, its content will be overwritten.
Before applying any migrations, it's useful to validate that your Exposed schema definitions match the actual state of the database. While the primary use of schema alignment methods is to generate SQL statements and migration scripts, these same methods can also serve as pre-checks — especially when used to detect unexpected changes.
Exposed provides several low-level APIs that support schema validation and can be integrated into custom migration or deployment pipelines. These methods are also used internally by Exposed to generate migration statements, but you can also use them for more precise checks.
To determine if a specific database object is already present, use the .exists() method on a Table, Sequence, or
Schema.
To evaluate whether a table has excessive indices or foreign keys, which might indicate schema drift or duplication, use one of the following SchemaUtils methods:
SchemaUtils.checkExcessiveIndices() (JDBC,
R2DBC)SchemaUtils.checkExcessiveForeignKeyConstraints() (
JDBC,
R2DBC)To retrieve metadata from the current dialect to compare with your defined Exposed schema, use one of the following currentDialectMetadata methods:
currentDialectMetadata.tableColumns() (JDBC,
R2DBC)currentDialectMetadata.existingIndices() (JDBC,
R2DBC)currentDialectMetadata.existingPrimaryKeys() (JDBC,
R2DBC)As your schema evolves, it's common to remove or rename columns in your table definitions. However, old columns may still exist in the database unless explicitly dropped.
The MigrationUtils.dropUnmappedColumnsStatements() function helps identify columns that are no longer present in your
current table definitions and returns the SQL statements to remove them:
{src="exposed-migrations/src/main/kotlin/org/example/App.kt" include-symbol="dropStatements"}
For indices and sequences, you can use the MigrationUtils.dropUnmappedIndices() and
MigrationUtils.dropUnmappedSequences() methods.
By default, each method provided by MigrationUtils logs descriptions and the execution time of each intermediate step. These logs are emitted at the INFO
level and can be disabled by setting withLogs to false:
{src="exposed-migrations/src/main/kotlin/org/example/App.kt" include-lines="57-60"}
While Exposed's migration tools are powerful, there are some limitations:
For an example of manual execution with Flyway, see the
exposed-migrationssample project.
ALTER TABLE support, may lead to partial or failed migrations if not reviewed.DROP COLUMN or DROP SEQUENCE can be included — caution is advised.We recommend that you always manually review generated diffs or scripts before applying them to a live database.
SQLite has strict limitations around the ALTER TABLE ADD COLUMN statement. For example, it does not allow adding a new column without a
default value under certain conditions. Since Exposed cannot account for all of SQLite’s specific constraints, it will still generate the expected SQL statement.
It is up to you to review the generated SQL and avoid attempting migrations that are incompatible with SQLite’s rules. If such a statement is executed, it will
fail at runtime.
For more information on this restriction, refer to the SQLite documentation.
When running on PostgreSQL, the functions to align the database schema also check for inconsistencies between table definitions and sequences (especially those tied
to SERIAL columns on IdTable).
Sequences manually created with CREATE SEQUENCE and not linked to a table are ignored. No DROP statements are generated for such sequences.
Any detected changes to table and column constraints generally result in the generation of DROP and CREATE / ALTER statement pairs.
The type of change that generates these migration statements depends on the type of constraint:
ForeignKeyConstraint detects mismatches in name, update rule, or delete rule.Index detects mismatches in name, uniqueness, or columns involved. Differences in index type, index function, or filter conditions will not be detected.CheckConstraint detects mismatches in name only. Differences in the boolean expression or condition used by this constraint will not be detected.A table's column can have multiple defining properties that need to be evaluated by Exposed's migration tools.
Column changes are determined by ColumnDiff.
Column properties, such as nullability, autoincrement status, and comments, are compared directly and result in appropriate migration statements.
The following column properties have limitations in how changes are detected:
Renaming a column typically results in a pair of statements that add the new column and drop the old one.
Changes to a name's case sensitivity are usually ignored unless the database does not auto-fold identifiers or the name is quoted.
SQLite, for example, is a database for which an ALTER... RENAME statement will be specifically generated if a
difference in case sensitivity is found.
Only primitive default values are reliably detected. The detection of changes to default expressions or functions may not be guaranteed.
Additionally, any column marked with .databaseGenerated()
will have its default values excluded from the check to ensure that potential database-side defaults are not incorrectly removed.
Full support for column type changes is currently only available when using H2.
Detection applies only to column types that support these values, such as DECIMAL and CHAR.
It is also possible to configure a column's definition on table creation by marking it with .withDefinition(),
which accepts any combination of strings and expressions to append to the SQL column syntax.
However, these custom definitions are not used when comparing the Exposed table object with database metadata. For a more reliable migration workflow, prefer more definitive column methods whenever possible.
For example, if your database supports column comments on table creation, marking a table column using .withDefinition("COMMENT '...'") and then changing the
comment string value in the future will not trigger a migration statement. If you use the .comment("...")
method instead, the string value will be properly compared with the comment retrieved from the database.
A Gradle plugin to simplify SQL migrations is in development. A proposed design for Flyway integration has been presented and is actively being implemented. To show interest or get involved, see the YouTrack issue for creating the migration Gradle plugin.
Exposed does not currently offer a Maven plugin or Liquibase integration — share your interest to help shape future support: