(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 (try (sql/create-table migrations-table
[:id "varchar(255)"]
[:created_at "datetime"])
(catch Exception _))))
(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))))
(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))))
(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))
(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)))))
(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))))