(ns ragtime.lite
  (:refer-clojure :exclude [merge])
  (:require [clojure.java.jdbc :as sql]
            [clojure.java.io :as io]))

(def ^:private migrations-table "ragtime_migrations")

(def ^:private ensure-migrations-table-exists
  (delay
   ;; TODO: is there a portable way to detect table existence?
   (try (sql/create-table migrations-table
                          [:id "varchar(255)"]
                          [:created_at "datetime"])
        (catch Exception _))))

;;; Reading migrations from .sql files

(defn load-sql-file [file]
  (apply sql/do-commands (.split (slurp file) ";")))

(defn- add-sql-migration [migrations file]
  (if-let [[_ id dir] (re-find #"(.*)-(up|down).sql" (.getName file))]
    (update-in migrations [id] (fnil assoc {:id id})
               dir (partial load-sql-file file))
    migrations))

(defn sql-from-dir
  "Return a map of migration maps corresponding to .sql files in dir."
  [dir]
  (reduce add-sql-migration {} (.listFiles (io/file dir))))

;;; Reading migrations from .clj files

(defn- add-clj-migration [migrations file]
  (if-let [[_ id dir] (re-find #"(.*)-(up|down).clj" (.getName file))]
    (update-in migrations [id] (fnil assoc {:id id})
               dir (partial load-file file))
    migrations))

(defn clj-from-dir
  "Return a map of migration maps corresponding to .clj files in dir."
  [dir]
  (reduce add-clj-migration {} (.listFiles (io/file dir))))

;;; Composition

(defn- compose-up-down [result latter]
  (-> result
      (assoc :up #(sql/transaction
                   ((:up result)) ((:up latter))))
      (assoc :down #(sql/transaction
                     ((:down result)) ((:down latter))))))

(defn merge
  "Merge migration maps by wrapping each composed :up/:down fn in a transaction."
  [& migration-sets]
  (apply merge-with compose-up-down migration-sets))

;;; What needs to run?

(defn applied-migrations []
  (sql/with-query-results results
    ["SELECT id FROM ? ORDER BY created_at" migrations-table]
    (set (map :id results))))

(defn max-id []
  (sql/with-query-results results
    ["SELECT max(id) FROM ?" migrations-table]
    (first results)))

(defn up [{:keys [id up]}]
  (force ensure-migrations-table-exists)
  (sql/transaction
   (sql/insert-values migrations-table
                        [:id :created_at]
                        [(str id) (System/currentTimeMillis)])
   (up)))

(defn down [{:keys [id down]}]
  (force ensure-migrations-table-exists)
  (sql/transaction
   (sql/delete-rows migrations-table ["id = ?" id])
   (down)))

(defn migrations-for
  "Return a list of fns to be run from the given migrations to reach target.

  Accepts migrations as a seq of maps containing :up, :down, and :id or as a map
  of id to maps which will be run in order of the :ids."
  [migrations & [target]]
  (let [applied? (comp (applied-migrations) :id)
        migrations (if (map? migrations)
                     (sort-by :id (vals migrations))
                     migrations)]
    (if (or (nil? target) (< (max-id) target))
      (map :up (remove applied? migrations))
      (map :down (filter #(and (applied? %) (> (:id %) target)) migrations)))))

;;; Example usage

(defn -main [db-string dir & [target]]
  (sql/with-connection db-string
    (doseq [m (migrations-for (merge (sql-from-dir dir)
                                     (clj-from-dir dir)) target)]
      (m))))

Generated by Phil Hagelberg using scpaste at Tue Jun 26 17:14:33 2012. PDT. (original)