diff --git a/extra/.gitignore b/extra/.gitignore new file mode 100644 index 0000000..e04714b --- /dev/null +++ b/extra/.gitignore @@ -0,0 +1,9 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port diff --git a/extra/README.md b/extra/README.md new file mode 100644 index 0000000..c7ba7a7 --- /dev/null +++ b/extra/README.md @@ -0,0 +1,6 @@ +# Zou extra components + +## Ragtime component + +If you want to use this component, you need to add ragtime as a dependency to your project.clj. +https://github.com/weavejester/ragtime diff --git a/extra/dev-resources/migrations/.keep b/extra/dev-resources/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/extra/project.clj b/extra/project.clj new file mode 100644 index 0000000..935b17f --- /dev/null +++ b/extra/project.clj @@ -0,0 +1,9 @@ +(defproject zou/extra "0.1.0-alpha5-SNAPSHOT" + :dependencies [[zou/common :version] + [zou/component :version] + [zou/lib :version] + [inflections "0.13.0"]] + :plugins [[lein-modules "0.3.11"]] + :profiles + {:dev {:dependencies [[zou/framework :version]]} + :provided {:dependencies [[ragtime "0.7.1"]]}}) diff --git a/extra/resources/zou/config/default/ragtime.edn b/extra/resources/zou/config/default/ragtime.edn new file mode 100644 index 0000000..fac78b6 --- /dev/null +++ b/extra/resources/zou/config/default/ragtime.edn @@ -0,0 +1,7 @@ +{:ragtime + {:zou/constructor zou.migrator.ragtime/new-ragtime + :zou/dependencies {:db :hikari-cp} + :migrations-path "migrations" + :style :sql ;; `:sql` or `:edn` + :strategy #resolve ragtime.strategy/raise-error + :reporter #resolve ragtime.reporter/print}} diff --git a/extra/src/zou/migrator/ragtime.clj b/extra/src/zou/migrator/ragtime.clj new file mode 100644 index 0000000..93a8bf6 --- /dev/null +++ b/extra/src/zou/migrator/ragtime.clj @@ -0,0 +1,129 @@ +(ns zou.migrator.ragtime + (:require [clojure.java.io :as io] + [clojure.string :as str] + [inflections.core :as inf] + [ragtime.core :as ragtime] + [ragtime.jdbc :as jdbc] + [zou.component :as c] + [zou.logging :as log] + [zou.task :as task]) + (:import java.time.format.DateTimeFormatter + java.time.LocalDateTime)) + +(defn validate-component-state [component] + (let [{:keys [datastore index migrations]} component] + (when-not (and datastore index migrations) + (throw (ex-info "Ragtime component has not been initialized" {:keys [:datastore :index :migrations]}))) + component)) + +(defn migrate [ragtime-component] + (let [{:keys [datastore index migrations]} (validate-component-state ragtime-component) + applied (ragtime/applied-migrations datastore index) + opts (select-keys ragtime-component [:strategy :reporter])] + (if (> (count index) (count applied)) + (do (ragtime/migrate-all datastore index migrations opts) + (log/info "Successfully migrated")) + (log/info "No migrations to apply")))) + +(defn rollback [ragtime-component options] + (let [{:keys [datastore index]} (validate-component-state ragtime-component) + applied (ragtime/applied-migrations datastore index) + {:keys [n id]} options + n (if (nil? n) 1 n) + opts (select-keys ragtime-component [:reporter])] + (if (and (seq applied) + (or (seq id) (integer? n))) + (do + (if (seq id) + (ragtime/rollback-to datastore index id opts) + (ragtime/rollback-last datastore index n opts)) + (log/info "Successfully rolled back")) + (log/info "No migrations to roll back")))) + +(defn- gen-file-name [description] + (let [f (DateTimeFormatter/ofPattern "yyyyMMddHHmmssSSS") + d (LocalDateTime/now)] + (->> (str/replace description #"\s" "_") + inf/underscore + (str "v_" (.format d f) "_")))) + +(defmulti generate-files (fn [style path description] style)) + +(defmethod generate-files :default + [style _ _] + (throw (ex-info "Invalid parameter" {:style style}))) + +(defmethod generate-files :sql + [_ path description] + (let [prefix (gen-file-name description) + up-sql (io/file path (str prefix ".up.sql")) + down-sql (io/file path (str prefix ".down.sql"))] + (spit up-sql "") + (spit down-sql "") + [up-sql down-sql])) + +(defmethod generate-files :edn + [_ path description] + (let [prefix (gen-file-name description) + edn (io/file path (str prefix ".edn"))] + (spit edn (str "{:up []" (System/lineSeparator) " :down []}")) + [edn])) + +(defn generate* [migrator description] + (let [{:keys [migrations-path style]} migrator + resource (io/resource migrations-path)] + (if (and resource (= (.getProtocol resource) "file")) + (generate-files style (-> resource io/file .getPath) description) + (throw (ex-info (format "Migrations directory `%s` %s" + migrations-path + (if resource "not a directory" "does not exist")) + {:migrations-path migrations-path}))))) + +(defn generate [ragtime-component description] + (doseq [file-name (map #(.getName %) (-> (validate-component-state ragtime-component) + (generate* description)))] + (log/info "Generate" file-name))) + +(defrecord Ragtime [db] + c/Lifecycle + (start [this] + (let [{:keys [migrations-path]} this + migrations (jdbc/load-resources migrations-path)] + (assoc this + :migrations migrations + :datastore (->> (select-keys this [:migrations-table]) + (jdbc/sql-database db)) + :index (ragtime/into-index migrations)))) + (stop [this] + (dissoc this :migrations :datastore :index)) + + task/Task + (task-name [this] :ragtime) + (spec [this] {:desc "Ragtime tasks"}) + + task/TaskContainer + (tasks [this] + [(task/task :migrate + (fn [_] + (migrate this)) + :desc "Runs migration") + + (task/task :rollback + (fn [{:keys [options]}] + (rollback this options)) + :desc "Rolls back the database" + :option-specs [["-n" "--n N" "How many changes to rollback" + :parse-fn #(Long/parseLong %) + :validate [pos? "Must be integer"]] + ["-i" "--id ID" "Which id to rollback to" + :validate [seq "Must be non-empty string"]]]) + + (task/task :generate + (fn [env] + (generate this (get-in env [:arguments :description]))) + :desc "Generate migration files" + :argument-specs [["description" + :validate [seq "Must be non-empty string"]]])])) + +(defn new-ragtime [conf] + (map->Ragtime conf)) diff --git a/extra/test/zou/migrator/ragtime_test.clj b/extra/test/zou/migrator/ragtime_test.clj new file mode 100644 index 0000000..0540417 --- /dev/null +++ b/extra/test/zou/migrator/ragtime_test.clj @@ -0,0 +1,129 @@ +(ns zou.migrator.ragtime-test + (:require [clojure.java.io :as io] + [clojure.java.jdbc :as jdbc] + [clojure.test :as t] + [zou.component :as c] + [zou.framework.bootstrap :as boot] + [zou.framework.entrypoint.proto :as ep] + [zou.logging :as log] + [zou.migrator.ragtime :as sut]) + (:import java.nio.file.Files)) + +(def empty-file-attrs (into-array java.nio.file.attribute.FileAttribute [])) + +(def h2db-file (Files/createTempFile "h2db-" ".sql" empty-file-attrs)) + +(defn log [_ op id] + (case op + :up (log/info "Applying" id) + :down (log/info "Rolling back" id))) + +(def test-conf + {:db (str "jdbc:h2:" (str h2db-file)) + :ragtime {:zou/constructor 'zou.migrator.ragtime/new-ragtime + :zou/dependencies {:db :db} + :migrations-path "migrations" + :style :edn + :strategy #'ragtime.strategy/raise-error + :reporter #'log}}) + +(defn setup [migrations] + (c/with-system [sys test-conf] + (dotimes [n 3] + (let [[edn] (sut/generate* (:ragtime sys) (str "migration_" n))] + (spit edn (format "{:up [\"create table test_%s(id integer);\"] :down [\"drop table test_%s;\"]}" n n)) + (swap! migrations conj edn))) + migrations)) + +(defn teardown [migrations] + (c/with-system [sys test-conf] + (jdbc/execute! (:db sys) "drop all objects;")) + (doseq [edn @migrations] + (io/delete-file edn))) + +(reset-meta! *ns* {}) +(t/use-fixtures :each + (fn [f] + (let [migrations (atom [])] + (setup migrations) + (f) + (teardown migrations)))) + +(defn find-test-tables [db] + (->> (jdbc/query db "select * from information_schema.tables") + (map :table_name) + (filter #(re-matches #"^TEST_\d+$" %)) + (into #{}))) + +(t/deftest validate-component-state-test + (t/testing "validate-component-state" + (c/with-system [sys test-conf] + (t/is (= (sut/validate-component-state (:ragtime sys)) + (:ragtime sys))) + + (t/is (thrown? + clojure.lang.ExceptionInfo + (= (-> (:ragtime sys) + (dissoc :index) + sut/validate-component-state) + (:ragtime sys))))))) + +(t/deftest migrate-test + (t/testing "migrate" + (c/with-system [sys test-conf] + (t/is (= (find-test-tables (:db sys)) + #{})) + + (log/with-test-logger + (sut/migrate (:ragtime sys)) + (doseq [o (map :msg (take 3 @log/*test-logger-entries*))] + (t/is (re-matches #"^Applying v_\d{17}_migration_\d+$" o)))) + + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1" "TEST_2"}))))) + +(t/deftest rollback-test + (t/testing "rollback" + (t/testing "with n option" + (c/with-system [sys test-conf] + (sut/migrate (:ragtime sys)) + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1" "TEST_2"})) + + (log/with-test-logger + (sut/rollback (:ragtime sys) {:n 2}) + (doseq [o (map :msg (take 2 @log/*test-logger-entries*))] + (t/is (re-matches #"^Rolling back v_\d{17}_migration_\d+$" o)))) + + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0"}))) + + (c/with-system [sys test-conf] + (sut/migrate (:ragtime sys)) + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1" "TEST_2"})) + + (log/with-test-logger + (sut/rollback (:ragtime sys) {:n nil}) + (log/logged? #"^Rolling back v_\d{17}_migration_\d+$")) + + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1"})))) + + + (t/testing "with id option" + (c/with-system [sys test-conf] + (sut/migrate (:ragtime sys)) + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1" "TEST_2"})) + + (let [id (-> (jdbc/query (:db sys) "select * from ragtime_migrations order by created_at") + second + :id)] + (log/with-test-logger + (sut/rollback (:ragtime sys) {:id id}) + (doseq [o (map :msg (take 1 @log/*test-logger-entries*))] + (t/is (re-matches #"^Rolling back v_\d{17}_migration_\d+$" o)))) + + (t/is (= (find-test-tables (:db sys)) + #{"TEST_0" "TEST_1"}))))))) diff --git a/project.clj b/project.clj index 442d14a..89d1df6 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(def modules ["common" "component" "lib" "framework" "web" "db" "cljs-devel" "devel"]) +(def modules ["common" "component" "lib" "framework" "web" "db" "extra" "cljs-devel" "devel"]) (def modules+tpl (conj modules "lein-template")) (def modules+tpl+parent (conj modules+tpl ".")) @@ -10,7 +10,8 @@ [zou/lib :version] [zou/framework :version] [zou/web :version :exclusions [org.apache.commons/commons-compress]] - [zou/db :version]] + [zou/db :version] + [zou/extra :version]] :plugins [[lein-modules "0.3.11"]] :profiles {:coverage {:source-paths ~(subdir "src") :test-paths ~(subdir "test") @@ -49,7 +50,8 @@ [enlive "1.1.6"] [hiccup "1.0.5"] [stencil "0.5.0"] - [selmer "1.10.7"]] + [selmer "1.10.7"] + [ragtime "0.7.1"]] :plugins [[lein-midje "3.2.1"] [lein-file-replace "0.1.0"]] :env {:zou-env "dev"}