migrant-core: Semi-automatic database schema migrations

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Warnings:


[Skip to Readme]

Properties

Versions 0.1.0.0, 0.1.0.1, 0.1.0.2, 0.1.0.3, 0.1.1.1, 0.1.1.1
Change log None available
Dependencies base (>=4.12.0.0 && <5), text (>=1.2 && <3) [details]
License BSD-3-Clause
Author Tobias Dammers
Maintainer tdammers@gmail.com
Category Database
Home page https://github.com/tdammers/migrant
Uploaded by TobiasDammers at 2026-01-13T13:13:50Z

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for migrant-core-0.1.1.1

[back to package description]

Migrant

Opinionated SQL schema migration management

Introduction

Migrant instruments SQL schema migrations in a semi-automated way. Writing the actual up- and downgrade scripts is still a manual effort; but Migrant takes care of tracking which scripts have been run already and which still need to be run, runs them for you.

The SQL Schema Migration Problem

Databases are notoriously difficult to version-control.

Versioning source code is a solved problem: we write code, put it in a repository, and the source control software gives us a unique identifier for that exact version. We can now deploy the code in whichever state we want, and as long as we keep code and data separated, we can do this in a fairly brute-force manner: we just delete the old code, copy the new code where it needs to be, and restart what needs to be restarted. Easy. And because we can deploy any version of the code we want on any host we want, we can test our code on one machine (a test server), and then deploy it on another (a production server), and be reasonably sure that if it works on the test environment, it will also work in production. We can also, just as easily, revert the code to an older version, if one of our changes turns out to have introduced a fault.

But with SQL databases, this doesn't work. The schema and the data stored in the database are intertwined; we want to manage the schema, but we want to do it such that no data is lost. We cannot simply overwrite the schema: if we delete a schema, we also delete all the data in it, because in an SQL database, data cannot exist without the associated schema. Even small changes, such as changing the type of a column, can be destructive, and so there is a real risk of permanently losing data as a result of schema changes. And this means we must be more surgical about our database mutations.

There are two fundamental approaches to this, which I call "snapshot-based" and "delta-based".

The "snapshot-based" approach stores a snapshot of the schema at a given version in the source control system; to migrate the database to that version, it looks at the current schema, and infers the schema changes that are required to get the schema into the desired state (or a compatible one). For example, if there is a table products in the database that has three columns (id, name, price), and the version-controlled schema description says it should have columns id, name, price, image, then the migration code infers that the image column must be added.

The "delta-based" approach, which is what Migrant uses, stores descriptions of the steps required to arrive at the current schema. A migration run, then, figures out which migration steps have already been executed, and which ones are needed to get the schema where we want it, and executes the required steps. In our example above, there may be two upgrade steps: create-products-table, and add-product-image. The migration code detects that only the create-products-table step has been run, and decides to run the add-product-image step.

How Migrant Works

Migrant migrations are implemented using three key parts:

Using Migrant

  1. Add migrant-core to your project, and one of the backends (migrant-sqlite-simple, migrant-postgresql-simple, or migrant-hdbc).
  2. Write some glue code to make your application call migrate
  3. Write schema migrations as pairs of "up" and "down" scripts, and write a list that says in which order to run these scripts.
  4. To deploy your migrations, compile the project, and make it run the migrate function. Migrant will now look for a _migrations table in the database, creating it if necessary, and run "up" and "down" scripts as needed to get the database into the state that your application expects.

Example:

{-#LANGUAGE OverloadedStrings #-}

import qualified Database.HDBC as HDBC
import Database.Migrant
import Data.Text (Text)

runMigrations :: HDBC.ConnWrapper -> IO ()
runMigrations conn =
  migrate myMigrations migrateUp migrateDown conn

myMigrations :: [MigrationName]
myMigrations =
  [ "create-users-table"
  , "user-email"
  ]

migrateUp :: MigrationName -> HDBC.ConnWrapper -> IO ()
migrateUp name conn = case name of
  "create-users-table" ->
    HDBC.quickQuery
      conn
      "CREATE TABLE users (id INTEGER NOT NULL SERIAL, username TEXT NOT NULL, password BLOB NULL)"
      []
  "user-email" ->
    HDBC.quickQuery
      conn
      "ALTER TABLE users ADD COLUMN email TEXT NULL"
      []

migrateDown :: MigrationName -> HDBC.ConnWrapper -> IO ()
migrateDown name conn = case name of
  "create-users-table" ->
    HDBC.quickQuery
      conn
      "DROP TABLE users"
      []
  "user-email" ->
    HDBC.quickQuery
      conn
      "ALTER TABLE users DROP COLUMN email"
      []
  

Suggested Practices

Suggested Development Flow

  1. In a development environment, write your intended database change, and an "undo", as Migrant migration pair, and reference it in your migrations list.
  2. Run migrations on the dev environment.
  3. Verify that the migration does what you want. If it does, proceed; if not, remove the reference from the list, run migrations (this will run the "undo" script), and try again. If your "up" script failed, there is no need to roll it back. If the "down" script doesn't work, reset your database by cloning a production database or starting with a blank database (this will recreate the entire thing from scratch, running all the migrations one by one).
  4. Once you're happy with the migration script, commit it.
  5. When merging, resolve conflicts in the migrations list and test the merged version against a development database. Changing the order of entries in the list and re-running migrations will roll back changes up to the last common situation, and then re-apply the migrations in the order you specified.
  6. As a final test before deploying, clone a production database and run all migrations on it.
  7. To deploy, first install the application code on the server, and then make it run migrations.
  8. To undo a deployment, the procedure is exactly the same: install application code, then run migrations.