Migrations
Migrations are defined using data in EDN files. They are based on the Ragtime format and 100% backwards compatible. So you can choose to use raw SQL if you prefer. Every migration file contains a map with an :up
and :down
key. These keys contain a vector of SQL statements.
Creating a table
Raw SQL
If you want to use raw SQL you can use the same format that Ragtime uses. You can add multiple statements in the :up
and :down
keys to run multiple SQL commands.
;; resources/migrations/000-products.edn
{:up ["CREATE TABLE products
( id BIGSERIAL PRIMARY KEY
, name TEXT NOT NULL UNIQUE
, visible BOOLEAN NOT NULL DEFAULT false
, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);"]
:down ["DROP TABLE products"]}
Clojure Data
You can use Clojure datastructures to define your migrations. The format is similar to how Malli defines maps. Generally a vector where the first value is a qualified-keyword
, second value is optionally a map with options, and then a finite amount of values.
We can recreate the same statement with the following data:
;; resources/migrations/000-products.edn
{:up [[:table/create {:table :products}
[:column/add
[:id {:primary-key true} :bigserial]
[:name {:unique true} :string]
[:visible {:default false} :boolean]
[:created-at {:default :current-timestamp} :timestamp]
[:updated-at {:default :current-timestamp} :timestamp]]]]
:down [[:table/drop :products]]}
We use the :table/create
key to create a new table and define the name using the :table
key in the options map. The following values are actions. The :column/add
action allows us to add all the columns we need with type and additional options.
If no primary-key is defined in our migration, Gungnir will by default automatically add an id
column the the BIGSERIAL
type as a primary-key. Gungnir also provides a special column action called :gungnir/timestamps
which adds a created_at
and updated_at
column. We can rewrite our previous migration to the following:
;; resources/migrations/000-products.edn
{:up [[:table/create {:table :products}
[:column/add
[:name {:unique true} :string]
[:visible {:default false} :boolean]
[:gungnir/timestamps]]]]
:down [[:table/drop :products]]}
Extending
Sometimes you might need more control for your migrations. For example you might want to add data to your database, generated with Clojure. Or Gungnir is missing a migration feature that should be implemented in a data-driven action (In that case, feel free to open a pull request!).
To create your own migration action, you need to implement the gungnir.migration/format-action
multimethod. It matches on a qualified-keyword representing the action, the first value of the vector. The second value of the vector is an optional map. The rest of the values is just a collection of input. The return value of this multimethod should be a string representing a raw SQL query.
;; 001-some-migration.edn
{:up [[:my/migration-action
{:some-option 123}
[:my/field 1]
[:my/field 2]
[:my/field 3]]]
:down [[:my/migration-reverse 1 2 3]]}
;; my-app/core.clj
(defmethod format-action :my/migration-action [[_key opts & fields]]
;; _key : :my/migration-action
;; opts : {:some-option 123}
;; fields : '([:my/field 1] [:my/field 2] [:my/field 3])
;;
;; Implement your query
"INSERT INTO account ...")
(defmethod format-action :my/migration-reverse [[_key opts & fields]]
;; _key : :my/migration-reverse
;; opts : {}
;; fields : '(1 2 3)
;;
;; Implement your query
"DELETE from account ...")
Keys
Table
key | description |
---|---|
:table/create | Create a new table |
:table/alter | Alter an existing table |
:table/drop | Drop an existing table |
Table options
key | description |
---|---|
:table | Table name to create or alter |
:primary-key | Set to false to disable automatic primary-key generation. Set to :uuid use a uuid as primary-key |
Column
key | description |
---|---|
:column/add | Create a new table |
:column/drop | Alter an existing table |
Column types
key | postgres type |
---|---|
:string | TEXT or VARCHAR(:size) |
:int | INTEGER |
:float | FLOAT(:size) (:size default 8) |
:boolean | BOOLEAN |
:serial | SERIAL |
:bigserial | BIGSERIAL |
:timestamp | TIMESTAMP |
:uuid | UUID (requires uuid-ossp extension) |
Column type options
key | description |
---|---|
:primary-key | Makes column primary key |
:size | Sets the size of the value (:string / :float) |
:default | Set the default value on creation |
:optional | Make a column optional. By default every column is required |
:unique | Column should be unique |
:references | Column is a foreign referencing a :table/column (qualified keyword e.g. :account/id) |
Running migrations
For convenience you can add the Ragtime configuration in your user.clj
. This will allow you to access it whenever you enter the REPL in the user namespace.
(ns user
(:require
[gungnir.migration]))
(def migrations (gungnir.migration/load-resources "migrations"))
Enter the REPL (e.g. lein repl
). Require the gungnir.migration
namespace and run the migrations using the gungnir.migration/migrate!
function. All pending migrations will be executed.
user=> (require '[gungnir.migration :as migration])
nil
user=> (migration/migrate! migrations)
Applying 000-uuid
Applying 001-auto-updated-at
Applying 002-account
You can also rollback migrations one by one using the gungnir.migration/rollback!
function.
nil
user=> (migration/rollback! config)
Rolling back 002-account
nil
user=> (migration/rollback! config)
Rolling back 001-auto-updated-at
nil
user=> (migration/rollback! config)
Rolling back 000-uuid
nil