Skip to content

kyle-neal/dbmigrate

Repository files navigation

dbmigrate

dbmigrate is an Erlang migration library that keeps schema changes as versioned Erlang modules and tracks applied versions per app and backend.

It supports these backends:

  • cassandra
  • elasticsearch
  • pgsql

What dbmigrate does

  • Generates timestamped migration modules.
  • Discovers available migrations from disk.
  • Tracks applied migrations in backend storage.
  • Plans what to apply or roll back (all, one, N, to target, specific version, app version).
  • Executes migrations inside adapter transaction hooks.

High-level architecture

  • dbmigrate: Public API facade.
  • dbmigrate_request: Normalizes API calls into request maps.
  • dbmigrate_plan: Pure planning stage, no database side effects.
  • dbmigrate_runner: Executes selected migrations and records outcomes.
  • dbmigrate_loader: Path resolution, migration discovery, compile-and-load.
  • dbmigrate_registry: Adapter-backed applied migration tracking.
  • dbmigrate_adapter: Behaviour definition for backend adapters.
  • dbmigrate_adapter_pgsql, dbmigrate_adapter_cassandra, dbmigrate_adapter_elasticsearch: Built-in adapters.

Migration layout

Default migration path:

<your_app>/priv/migrations/<backend>/

Examples:

apps/mmerl_core/priv/migrations/pgsql/
apps/mmerl_core/priv/migrations/cassandra/

Migration filenames are 14-digit timestamp prefixes:

20260314120000_add_positions.erl

Each migration module must export:

  • up/1
  • down/1

Add dbmigrate to your app

Set the app up in four steps:

  1. Add dbmigrate to your project dependencies.
  2. Ensure your target application starts dbmigrate.
  3. Configure the target application's migrate env.
  4. Place migration files under priv/migrations/<backend>/.

Example in rebar.config

{deps,
 [{dbmigrate, {git, "https://github.com/your-org/dbmigrate.git", {tag, "<version>"}}}]}. 

Include the backend dependencies you need in the same project or release.

Example in your app.src

{application, mmerl_core,
 [{description, "MMERL Core"},
	{vsn, "1.0.0"},
	{applications, [kernel, stdlib, dbmigrate]}]}. 

The migration configuration must be defined on the application being migrated, not on dbmigrate itself.

Configuration

dbmigrate reads migration config from the target application env under key migrate.

That means settings should live under your app (for example mmerl_core), not under dbmigrate.

Example in mmerl_core.app.src

{application, mmerl_core,
 [{description, "MMERL Core"},
	{vsn, "1.0.0"},
	{applications, [kernel, stdlib, dbmigrate]},
	{env,
	 [{migrate,
		 [{pgsql,
			 [{adapter, dbmigrate_adapter_pgsql},
				{host, "127.0.0.1"},
				{port, 5432},
				{username, "postgres"},
				{password, "postgres"},
				{database, "mmerl_core"},
				{migrations_table, "schema_migrations"},
				{timeout, 5000}]},
			{cassandra,
			 [{adapter, dbmigrate_adapter_cassandra},
				{host, "127.0.0.1"},
				{port, 9042},
				{keyspace, "mmerl_core"},
				{migrations_table, "schema_migrations"}]},
			{elasticsearch,
			 [{adapter, dbmigrate_adapter_elasticsearch},
				{host, "127.0.0.1"},
				{port, 9200},
				{index_name, "mmerl_core"},
				{type_name, "_doc"},
				{migrations_index, "schema_migrations"}]}]}]}]}.

Example in config/sys.config

[
 {mmerl_core,
	[{migrate,
		[{pgsql,
			[{adapter, dbmigrate_adapter_pgsql},
			 {host, "127.0.0.1"},
			 {port, 5432},
			 {username, "postgres"},
			 {password, "postgres"},
			 {database, "mmerl_core"},
			 {migrations_table, "schema_migrations"},
			 {timeout, 5000},
			 {ssl, false}]}
		]}]}
].

Notes:

  • You can override the default migration path with migrate_path in backend config.
  • PostgreSQL adapter also supports ssl, verify, cacertfile, depth, and server_name_indication.
  • PostgreSQL and Cassandra use migrations_table for the migration tracking table. The default is "schema_migrations".
  • Elasticsearch uses migrations_index for the migration tracking index. The default is "schema_migrations".

Full backend example

[
 {mmerl_core,
	[{migrate,
		[{pgsql,
			[{adapter, dbmigrate_adapter_pgsql},
			 {host, "127.0.0.1"},
			 {port, 5432},
			 {username, "postgres"},
			 {password, "postgres"},
			 {database, "mmerl_core"},
			 {migrations_table, "schema_migrations"},
			 {timeout, 5000}]},
		 {cassandra,
			[{adapter, dbmigrate_adapter_cassandra},
			 {host, "127.0.0.1"},
			 {port, 9042},
			 {keyspace, "mmerl_core"},
			 {migrations_table, "schema_migrations"}]},
		 {elasticsearch,
			[{adapter, dbmigrate_adapter_elasticsearch},
			 {host, "127.0.0.1"},
			 {port, 9200},
			 {index_name, "mmerl_core"},
			 {type_name, "_doc"},
			 {migrations_index, "schema_migrations"}]}]}]}
].

Creating migrations

From an Erlang shell:

1> dbmigrate:gen_migration(mmerl_core, pgsql, "add positions").
{ok,"apps/mmerl_core/priv/migrations/pgsql/20260314120000_add_positions.erl"}

Generated migration template example:

-module('20260314120000_add_positions').
-export([up/1, down/1]).

up(Conn) ->
		%% apply schema/data change
		ok.

down(Conn) ->
		%% rollback schema/data change
		ok.

Running migrations

Simple command form with -s

This form is ideal when arguments are atoms only.

erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys \
	-s dbmigrate migrate mmerl_core pgsql \
	-s init stop -noshell

Makefile-style example:

@erl -sname $(PROJECT_NAME)_migrate_db $(EPATH) -config config/sys -s dbmigrate migrate mmerl_core pgsql -s init stop -noshell

Advanced command form with -eval

Use -eval for modes that need non-atom values such as counts, versions, or options.

erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:migrate_n(mmerl_core, pgsql, 3), init:stop().'
erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:migrate_to(mmerl_core, pgsql, "20260314120000_add_positions"), init:stop().'
erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:migrate(mmerl_core, pgsql, [{app_version, "1.2.3"}]), init:stop().'

Rollback examples

erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:rollback_one(mmerl_core, pgsql), init:stop().'
erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:rollback_n(mmerl_core, pgsql, 2), init:stop().'
erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:rollback_to(mmerl_core, pgsql, "20260314120000_add_positions"), init:stop().'
erl -sname mmerl_migrate_db -pa _build/default/lib/*/ebin -config config/sys -noshell \
	-eval 'dbmigrate:rollback_app_version(mmerl_core, pgsql, "1.2.3"), init:stop().'

Public API reference

Core operations:

  • dbmigrate:gen_migration/3
  • dbmigrate:migrate/2
  • dbmigrate:migrate/3
  • dbmigrate:migrate_one/2
  • dbmigrate:migrate_n/3
  • dbmigrate:migrate_to/3
  • dbmigrate:migrate_specific/3
  • dbmigrate:migrate_mark_as_applied/1
  • dbmigrate:rollback_one/2
  • dbmigrate:rollback_n/3
  • dbmigrate:rollback_to/3
  • dbmigrate:rollback_app_version/3

Build and verification

Run the project checks locally:

make check

This runs eunit, common test, and dialyzer.

About

Migration tool for databases

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors