I'm starting to learn Clojure, and would like feedback on some code I wrote to manage database migrations. Any recommendations to make it more robust, efficient, idiomatic, elegant, etc... are welcome!
(ns myapp.models.migrations
(:require [clojure.java.jdbc :as sql]
[myapp.models.database :as db]))
;;;; Manages database migrations.
;;;;
;;;; Usage:
;;;;
;;;; user=> (migrate!) ; migrate to the latest version
;;;; user=> (migrate! 20140208) ; migrate to a specific version
(let [db-spec db/spec]
;; WARNING: Only works with PostgreSQL!
;;
;; TODO: Can this be made generic to all databases? Look into using the
;; JDBC database metadata to determine if a table exists.
(defn table-exists? [table-name]
(-> (sql/query db-spec
["select count(*) from pg_tables where tablename = ?" table-name])
first :count pos?))
;;; The migrations to apply
;;;
;;; The order in which migrations are apply is determined by the :version property.
;;; Each migration must have :apply and :remove functions so we can migrate up or down.
(def migration-0 {:version 0
:description "Starting point. Does nothing, but allows us to remove all other migrations if we want to."
:apply (fn [] nil)
:remove (fn [] nil)})
(def migration-20140208 {:version 20140208
:description "Create the articles table."
:apply (fn []
(when (not (table-exists? "articles"))
(sql/db-do-commands db-spec (sql/create-table-ddl :articles
[:title "varchar(32)"]
[:content "text"]))))
:remove (fn []
(when (table-exists? "articles")
(sql/db-do-commands db-spec (sql/drop-table-ddl :articles))))})
(def db-migrations [ migration-0
migration-20140208 ])
;;; Forms for processing the migrations.
(defn create-migrations-table! []
(when (not (table-exists? "migrations"))
(sql/db-do-commands db-spec
(sql/create-table-ddl :migrations [:version :int]))))
(defn drop-migrations-table! []
(when (table-exists? "migrations")
(sql/db-do-commands db-spec
(sql/drop-table-ddl :migrations))))
(defn migration-recorded? [migration]
(create-migrations-table!)
(-> (sql/query db-spec ["select count(*) from migrations where version = ?" (:version migration)])
first :count pos?))
(defn record-migration! [migration]
(create-migrations-table!)
(when (not (migration-recorded? migration))
(sql/insert! db-spec :migrations {:version (:version migration)})))
(defn erase-migration! [migration]
(create-migrations-table!)
(when (migration-recorded? migration)
(sql/delete! db-spec :migrations ["version = ?" (:version migration)])))
(defn migrate-up! [to-version]
(let [filtered-migrations (sort-by :version (filter #(<= (:version %) to-version) db-migrations))]
(doseq [m filtered-migrations]
(when (not (migration-recorded? m))
((:apply m))
(record-migration! m)))))
(defn migrate-down! [to-version]
(let [filtered-migrations (reverse (sort-by :version (filter #(> (:version %) to-version) db-migrations)))]
(doseq [m filtered-migrations]
(when (migration-recorded? m)
((:remove m))
(erase-migration! m)))))
(defn migrate!
([]
(let [last-migration (last (sort-by :version db-migrations))]
(when last-migration (migrate! (:version last-migration)))))
([to-version]
(let [version (or to-version 0)
migration-exists (not (nil? (some #(= (:version %) version) db-migrations)))
already-applied (migration-recorded? {:version version})]
(cond
(not migration-exists)
(println (format "migration %s was not found" version))
already-applied
(migrate-down! version)
:else
(migrate-up! version))))))