Skip to content
James Reeves edited this page Nov 7, 2024 · 17 revisions

Data Stores

The ragtime.protocols/DataStore protocol describes a migratable store of data. Ragtime needs an implementation of this protocol to tell it how to record which migrations have been applied.

As an example, we'll create an in-memory data store:

(require '[ragtime.protocols :as protocols]
         '[ragtime.core :as ragtime])

(defrecord MemoryDatabase [data migrations]
  protocols/DataStore 
  (add-migration-id [_ id]
    (swap! migrations conj id))
  (remove-migration-id [_ id]
    (swap! migrations (partial filterv (complement #{id}))))
  (applied-migration-ids [_]
    (seq @migrations)))

(defn memory-database []
  (->MemoryDatabase (atom {}) (atom [])))

We can define an new instance of this database, and see that it is empty, and has no migrations:

user=> (def db (memory-database))
#'user/db
user=> (-> db :data deref)
{}
user=> (protocols/applied-migration-ids db)
nil

Migrations

The ragtime.protocols/Migration protocol describes a single migration for a particular store. A migration has an identifier, and a run-up! and run-down! function, which applies and rolls back the migration respectively.

Here's an example of one that modifies the MemoryDatabase defined earlier.

(def add-foo
  (reify protocols/Migration
    (id [_] "add-foo")
    (run-up! [_ db] (swap! (:data db) assoc :foo 1))
    (run-down! [_ db] (swap! (:data db) dissoc :foo))})

We can apply a migration to a database using the migrate function:

user=> (ragtime/migrate db add-foo)
["add-foo"]
user=> (protocols/applied-migration-ids db)
("add-foo")
user=> (-> db :data deref)
{:foo 1}

And remove a migration using rollback:

user=> (ragtime/rollback db add-foo)
[]
user=> (protocols/applied-migration-ids db)
nil
user=> (-> db :data deref)
{}

Indexes

Often we'll want to work with migrations that have already been applied to the database. For example, we may wish to roll back the latest migration. The Migration protocol provides a way of finding the IDs of the migrations applied to the database, but we need some way of associating these IDs with the migrations themselves. In other words, we need a migration index.

An index simply maps IDs to migrations. We can create an index for a collection of migrations by using the into-index function:

user=> (def idx (ragtime/into-index [add-foo]))
#'user/idx
user=> idx
{"add-foo" {:id "add-foo", :up ..., :down ...}}

Once we have an index, we can use the migrate-all function to update the database with an ordered collection of migrations:

user=> (ragtime/migrate-all db idx [add-foo])
nil
user=> (keys (ragtime/applied-migrations db idx))
("add-foo")

Or to roll back the last migrations applied to the database:

user=> (ragtime/rollback-last db idx)
nil
user=> (keys (ragtime/applied-migrations db idx))
nil

Strategies

Occasionally the list of migrations applied to the database will differ from the project's migrations, particularly during development. This results in a conflict, and how Ragtime reacts to this depends on the strategy being used.

For example, consider a database with the following migrations applied:

A B D

But the project defines migrations:

A B C D

Ragtime's default strategy is ragtime.strategy/raise-error. This will raise an error if the migrations held in the database conflict with those in the project. This strategy is most useful for production, where ideally there should be no conflicts.

However, Ragtime also provides three more strategies. ragtime.strategy/rebase will roll back all conflicting migrations, and then re-apply them in the correct order:

Rolling back D
Applying C
Applying D

This will result in the project and database migrations matching:

A B C D

Alternatively, you can use ragtime.strategy/apply-new. This will always apply new migrations, regardless of whether they conflict:

Applying C

This will result in all migrations applied, but in a different order:

A B D C

This is useful if the migrations in a project can be applied independently.

Lastly, you can use ragtime.strategy/ignore-future. This works as the ragtime.strategy/raise-error, except that it does not consider a conflict migrations that may happen in the future. In the example above, this strategy will still raise an error but in contrast to the ragtime.strategy/raise-error it will not raise an error if the database has the following migrations applied:

A B C D E

And the project defines migrations:

A B C

This will result in no migrations applied, as A B C are already there, but it will not raise an error, as D E are migrations applied by some future version of the application.

This strategy is useful if you are using blue/green deployments with a shared database. In this case, there are two versions of the application running at the same time, and it is common that the new version will have applied some new migrations that the old version does not know about.

Clone this wiki locally