From fec8a48322d2d8856a5455da6e4b6714241db6e2 Mon Sep 17 00:00:00 2001 From: Eugen Stan Date: Tue, 31 Oct 2023 23:49:12 +0200 Subject: [PATCH] [#240] Improve cli ux and add cli example / docs * BREAKING? Enhanced Store protocol with close alias for disconnect * BREAKING? connect now returns the store (this) * create (migration) fn now resolves absolute file path for migration * Enhanced docs for Store protocol * CLI can load config from file * CLI can load config from env * CLI can load config from cli args * CLI merges configs in this order: file, env, args * Added some tests for CLI parsing * Added status command, to display migration status: - connection info - so we know which server we are connecting to - migrations directory - so we know where we get migrations from - the latest applied migration - the list of not-applied migrations - any migrations applied and not present ?! --- .gitignore | 2 +- CHANGES.md | 9 + README.md | 15 +- examples/postgres/README.md | 77 ++++ examples/postgres/deps.edn | 10 + examples/postgres/migratus.sh | 4 + .../src/migratus/examples/postgres.clj | 12 + src/migratus/cli.clj | 356 ++++++++++++++---- src/migratus/core.clj | 4 +- src/migratus/database.clj | 37 +- src/migratus/migrations.clj | 16 +- src/migratus/protocols.clj | 9 +- test/migratus/mock.clj | 3 +- test/migratus/test/cli_test.clj | 39 ++ 14 files changed, 487 insertions(+), 106 deletions(-) create mode 100644 examples/postgres/README.md create mode 100644 examples/postgres/deps.edn create mode 100755 examples/postgres/migratus.sh create mode 100644 examples/postgres/src/migratus/examples/postgres.clj create mode 100644 test/migratus/test/cli_test.clj diff --git a/.gitignore b/.gitignore index 63146cf..385e2a4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ migratus.iml .lsp/.cache .portal .cpcache -.calva/output-window/ +.calva .clj-kondo/ \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index e6efcc0..2d79a56 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +### Unreleased +* Improved CLI interface +* Improved CLI documentation +* Added example project + +### 1.5.3 +* [Improved CLI options](https://github.com/yogthos/migratus/pull/251) +* More improvements in CLI + ### 1.5.2 * [CLI options](https://github.com/yogthos/migratus/pull/244) * [logging improvements](https://github.com/yogthos/migratus/pull/245) diff --git a/README.md b/README.md index eac3879..4e54451 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,11 @@ The environment variable associated with the `database.table` key will replace ` ### Setup +Migratus can be used as a library in your project or as a CLI tool. +There is also an option (from a third party) to run a migratus as native binary - for PostgreSQL only. + +#### Using Migratus as a library in your project + - Add Migratus as a dependency to your `project.clj` ```clojure :dependencies [[migratus ]] @@ -209,6 +214,14 @@ It is possible to pass a `java.sql.Connection` or `javax.sql.DataSource` in plac (def config {:db {:datasource (hk/make-datasource datasource-options)}}) ``` +#### Using migratus as a command line (cli) tool + +Migratus exposes a CLI interface via `migratus.cli` namespace. +It uses [tools.cli](https://github.com/clojure/tools.cli) for argument parsing. + + + + #### Running as native image (Postgres only) [PGMig](https://github.com/leafclick/pgmig) is a standalone tool built with migratus that's compiled as a standalone GraalVM native image executable. @@ -386,7 +399,7 @@ This is intended for use with http://2ndquadrant.com/en/resources/pglogical/ and | `migratus.core/rollback` | Run `down` for the last migration that was run. | | `migratus.core/rollback-until-just-after` | Run `down` all migrations after `migration-id`. This only considers completed migrations, and will not migrate up. | | `migratus.core/up` | Run `up` for the specified migration ids. Will skip any migration that is already up. | - | `migratus.core/down` | Run `down` for the specified migration ids. Will skip any migration that is already down. + | `migratus.core/down` | Run `down` for the specified migration ids. Will skip any migration that is already down. | `migratus.core/reset` | Reset the database by down-ing all migrations successfully applied, then up-ing all migratinos. | `migratus.core/pending-list` | Returns a list of pending migrations. | | `migratus.core/migrate-until-just-before` | Run `up` for for any pending migrations which precede the given migration id (good for testing migrations). | diff --git a/examples/postgres/README.md b/examples/postgres/README.md new file mode 100644 index 0000000..0507943 --- /dev/null +++ b/examples/postgres/README.md @@ -0,0 +1,77 @@ +# Example using migratus with Postgres + +This is an example project that uses migratus to apply migrations to a PostgreSQL database. + +TODO: Add instructions on how to use migratus via code. + +## Using migratus via cli + +### Setup your database + +If you have an existing database, skip this step. +This guide uses docker for PostgreSQL setup, but you can setup PostgreSQL any way you like. + +Bellow is a short guide on how to manage a PostgreSQL db as a container for the purpose of the guide. + +For more information on PostgreSQL see [postgres image](https://hub.docker.com/_/postgres/) + +```shell +# Run a postgresql instance as a container named migratus-pg. +# We ask PostgreSQL to create a database named migratus_example_db +docker run --name migratus-pg --detach -p 5432:5432 \ + -e POSTGRES_PASSWORD=migrate-me \ + -e POSTGRES_DB=migratus_example_db \ + -v migratus_data:/var/lib/postgresql/data \ + postgres:latest + +# If all is well, we should see the container running +docker ps + +> CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +> c37a91d27631 postgres:latest "docker-entrypoint.s…" 23 seconds ago Up 23 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp migratus-pg + +# View the data volume for postgres +docker volume ls + +# And we can view the logs (in another terminal perhaps ?!) +docker logs -f migratus-pg + +# You can stop and start the container +docker container stop migratus-pg +docker container start migratus-pg + +# We can remove the container once you are done, or if you want to reset everything +docker container rm --force --volumes migratus-pg +``` + +### Setup migratus cli + +We use migratus cli via `deps.edn` aliases. +See the `deps.edn` file in this project for details. + +The file should look like this +```clojure +{:paths ["resources"] + :deps {io.github.yogthos/migratus {:mvn/version "RELEASE"} + org.postgresql/postgresql {:mvn/version "42.6.0"}} + :aliases + {:migratus {:jvm-opts ["-Dclojure.main.report=stderr"] + :main-opts ["-m" "migratus.cli"]}}} +``` + +If you have such a configuration, we can use `clojure` or `clj` tool to drive the CLI. +Since Migratus is a clojure library, we need to run it via clojure like this `clojure -M:migratus --help` + +There is also a bash script `migratus.sh` that does the same: `./migratus.sh --help` + + +Commands with migratus + +```shell + +# We export the configuration as env variable, but we can use cli or file as well +export MIGRATUS_CONFIG='{:store :database :db {:jdbcUrl "jdbc:postgresql://localhost:5432/migratus_example_db?user=postgres&password=migrate-me"}}' + +clojure -M:migratus status + +``` \ No newline at end of file diff --git a/examples/postgres/deps.edn b/examples/postgres/deps.edn new file mode 100644 index 0000000..037908b --- /dev/null +++ b/examples/postgres/deps.edn @@ -0,0 +1,10 @@ +{;; migration sql files will be on "resources" + :paths ["src" "resources"] + ;; we need migratus and postgresql jdbc driver on the classpath + :deps {io.github.yogthos/migratus {:local/root "../.."} + org.postgresql/postgresql {:mvn/version "42.6.0"}} + :aliases + {:migratus {:jvm-opts [;; print clojure errors to standard out instead of cli + "-Dclojure.main.report=stderr"] + ;; Run migratus.cli -main fn + :main-opts ["-m" "migratus.cli"]}}} diff --git a/examples/postgres/migratus.sh b/examples/postgres/migratus.sh new file mode 100755 index 0000000..fdd028b --- /dev/null +++ b/examples/postgres/migratus.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# Run migratus passing all args to it +clojure -J-Dclojure.main.report=stderr -M:migratus "$@" \ No newline at end of file diff --git a/examples/postgres/src/migratus/examples/postgres.clj b/examples/postgres/src/migratus/examples/postgres.clj new file mode 100644 index 0000000..b8bb7a6 --- /dev/null +++ b/examples/postgres/src/migratus/examples/postgres.clj @@ -0,0 +1,12 @@ +(ns migratus.examples.postgres + (:require [migratus.cli :as cli])) + + + +(comment + + (apply cli/-main ["list"]) + + + + ) \ No newline at end of file diff --git a/src/migratus/cli.clj b/src/migratus/cli.clj index f77ed16..1efe235 100644 --- a/src/migratus/cli.clj +++ b/src/migratus/cli.clj @@ -1,12 +1,16 @@ (ns migratus.cli - (:require [clojure.data.json :as json] - [clojure.core :as core] + (:require [clojure.core :as core] + [clojure.data.json :as json] + [clojure.edn :as edn] [clojure.java.io :as io] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] [clojure.tools.logging :as log] - [migratus.core :as migratus]) - (:import [java.time ZoneId ZoneOffset] + [migratus.core :as migratus] + [migratus.migrations :as mig] + [migratus.protocols :as proto]) + (:import [java.io IOException] + [java.time ZoneId ZoneOffset] [java.time.format DateTimeFormatter] [java.util.logging ConsoleHandler @@ -15,35 +19,170 @@ Logger SimpleFormatter])) -;; needed fro Clojure 1.10 compatibility -(defn parse-long [s] +(defn my-parse-long + "parse-long version for Clojure 1.10 compatibility" + [s] (Long/valueOf s)) +(defn my-parse-boolean + [s] + (Boolean/parseBoolean s)) + +(def app-config + "Application options to be used for output/logging. + To avoid passing them around everywhere." + (atom {:verbosity 0 + :output-format "plain"})) + +(defn deep-merge + "From https://clojuredocs.org/clojure.core/merge ." + [a & maps] + (if (map? a) + (apply merge-with deep-merge a maps) + (apply merge-with deep-merge maps))) + +(defn println-err + [& more] + (binding [*out* *err*] + (apply println more))) + +(defn env->config! + "Try to load migratus configuration options from environment. + We produce a configuration that can be merged with other configs. + Missing values are ok as configuring via env is optional. + + Looks and processes the following env vars: + + - MIGRATUS_CONFIG - read string as edn and return config. + Do not process any other migratus env var. + + - MIGRATUS_STORE - apply clojure.core/keyword fn to value + - MIGRATUS_MIGRATION_DIR - expect string, use as is + - MIGRATUS_DB_SPEC - read string as edn + - MIGRATUS_TABLE_NAME - string + - MIGRATUS_INIT_IN_TRANSACTION - parse boolean + + Return: A map representing the migratus configuration." + ([] + (env->config! (System/getenv))) + ([env] + (let [config (get env "MIGRATUS_CONFIG")] + (if config + (edn/read-string config) + ;; we don't have MIGRATUS_CONFIG - check the other vars + (let [store (get env "MIGRATUS_STORE") + migration-dir (get env "MIGRATUS_MIGRATION_DIR") + table (get env "MIGRATUS_TABLE_NAME") + init-in-transaction (get env "MIGRATUS_INIT_IN_TRANSACTION") + db (get env "MIGRATUS_DB_SPEC")] + (cond-> {} + store (assoc :store (keyword store)) + migration-dir (assoc :migration-dir migration-dir) + table (assoc :migration-table-name table) + init-in-transaction (assoc :init-in-transaction + (my-parse-boolean init-in-transaction)) + db (assoc :db (edn/read-string db)))))))) + +(defn cli-args->config + "Parse any migratus configuration options from cli args. + + Return a migratus configuration map with any values. + + We expect the args we receive to be values + processed by tools.cli parse-opts fn." + [config-edn-str] + (let [config (edn/read-string config-edn-str)] + (if (map? config) + config + {}))) + +(defn file->config! + "Read config-file as a edn. + If config-file is nil, return nil. + On IO exception print warning and return nil." + [^String config-file] + (when config-file + (let [config-path (.getAbsolutePath (io/file config-file))] + (try + (edn/read-string (slurp config-path)) + (catch IOException e + (println-err + "WARN: Error reading config" (.getMessage e) + "\nYou can use --config path_to_file to specify a path to config file")))))) + +(defn load-config! + "Load configuration and merge options. + + Options are loaded in this order. + Sbsequent values are deepmerged and replace previous ones. + + - configuration file - if it exists and we can parse it as edn + - environment variables + - command line arguments passed to the application + + Return a migratus configuration map." + [config-file config-data] + (let [config (file->config! config-file) + env (env->config!) + args (cli-args->config config-data)] + (deep-merge config env args))) + +#_(defn valid-config? + "Validate a migratus configuration for required options. + If valid, return true. + If invalid return map with reasons why validation failed." + [config] + (if (map? config) + (let [store (:store config)] + (if store + true + {:errors ["Missing :store key in configuration"]})) + {:errors ["Config is nil or not a map"]})) + +#_(defn- do-invalid-config + "We got invalid configuration. + Print error and exit" + [valid? cfg] + (println-err "Invalid configuration:" (:errors valid?) + "\nMigratus can load configuration from: file, env vars, cli args." + "\nSee documentation and/or use --help") + (println-err "Configuration is" cfg)) + (defn validate-format [s] (boolean (some (set (list s)) #{"plain" "edn" "json"}))) (def global-cli-options - [[nil "--config NAME" "Configuration file name" :default "migratus.edn"] - ["-v" nil "Verbosity level; may be specified multiple times to increase value" + [[nil "--config-file NAME" "Configuration file name"] + ["-v" nil + "Verbosity level; may be specified multiple times to increase value" :id :verbosity :default 0 :update-fn inc] + [nil "--output-format FORMAT" + "Option to print in plain text (default), edn or json" + :default "plain" + :validate [#(validate-format %) + "Unsupported format. Valid options: plain (default), edn, json."]] + [nil "--config CONFIG" "Configuration as edn"] ["-h" "--help"]]) (def migrate-cli-options - [[nil "--until-just-before MIGRATION-ID" "Run all migrations preceding migration-id. This is useful when testing that a migration behaves as expected on fixture data. This only considers uncompleted migrations, and will not migrate down."] + [[nil "--until-just-before MIGRATION-ID" + "Run all migrations preceding migration-id. + This is useful when testing that a migration behaves as expected on fixture data. + This only considers uncompleted migrations, and will not migrate down."] ["-h" "--help"]]) (def rollback-cli-options - [[nil "--until-just-after MIGRATION-ID" "Migrate down all migrations after migration-id. This only considers completed migrations, and will not migrate up."] + [[nil "--until-just-after MIGRATION-ID" + "Migrate down all migrations after migration-id. + This only considers completed migrations, and will not migrate up."] ["-h" "--help"]]) (def list-cli-options [[nil "--available" "List all migrations, applied and non applied"] [nil "--pending" "List pending migrations"] [nil "--applied" "List applied migrations"] - [nil "--format FORMAT" "Option to print in plain text (default), edn or json" :default "plain" - :validate [#(validate-format %) "Unsupported format. Valid options: plain (default), edn, json."]] ["-h" "--help"]]) (defn usage [options-summary] @@ -51,6 +190,7 @@ "" "Actions:" " init" + " status" " create" " migrate" " reset" @@ -59,39 +199,41 @@ " down" " list" "" - "options:" + "global options:" options-summary] (str/join \newline))) (defn error-msg [errors] - (binding [*out* *err*] - (println "The following errors occurred while parsing your command:\n\n" - (str/join \newline errors)))) + (println-err + "The following errors occurred while parsing your command:\n\n" + (str/join \newline errors))) (defn no-match-message "No matching clause message info" [arguments summary] - (binding [*out* *err*] - (println "Migratus API does not support this action(s) : " arguments "\n\n" - (str/join (usage summary))))) + (println-err + "Migratus API does not support this action(s) : " arguments "\n\n" + (str/join (usage summary)))) -(defn run-migrate [cfg args] - (let [{:keys [options arguments errors summary]} (parse-opts args migrate-cli-options :in-order true) +(defn run-migrate! [cfg args] + (let [cmd-opts (parse-opts args migrate-cli-options :in-order true) + {:keys [options arguments errors summary]} cmd-opts rest-args (rest arguments)] (cond errors (error-msg errors) (:until-just-before options) (do (log/debug "configuration is: \n" cfg "\n" - "arguments:" rest-args) + "arguments:" rest-args) (migratus/migrate-until-just-before cfg rest-args)) (empty? args) (do (log/debug "calling (migrate cfg)" cfg) (migratus/migrate cfg)) :else (no-match-message args summary)))) -(defn run-rollback [cfg args] - (let [{:keys [options arguments errors summary]} (parse-opts args rollback-cli-options :in-order true) +(defn run-rollback! [cfg args] + (let [cmd-opts (parse-opts args rollback-cli-options :in-order true) + {:keys [options arguments errors summary]} cmd-opts rest-args (rest arguments)] (cond @@ -99,7 +241,7 @@ (:until-just-after options) (do (log/debug "configuration is: \n" cfg "\n" - "args:" rest-args) + "args:" rest-args) (migratus/rollback-until-just-after cfg rest-args)) (empty? args) @@ -150,32 +292,32 @@ "pending" applied) fmt-str "%1$-15s | %2$-22s | %3$-20s"] - (println (core/format fmt-str, id, name, applied?)))) + (prn (core/format fmt-str, id, name, applied?)))) (defn format-pending-mig-data [m] (let [{:keys [id name]} m fmt-str "%1$-15s| %2$-22s%3$s"] - (println (core/format fmt-str, id, name, )))) + (prn (core/format fmt-str, id, name)))) (defn mig-print-fmt [data & format-opts] (let [pending? (:pending format-opts)] (if pending? - (do (println (table-line 43)) - (println (core/format "%-15s%-24s", - "MIGRATION-ID" "| NAME")) - (println (table-line 41)) + (do (prn (table-line 43)) + (prn (core/format "%-15s%-24s", + "MIGRATION-ID" "| NAME")) + (prn (table-line 41)) (doseq [d data] (format-pending-mig-data d))) - (do (println (table-line 67)) - (println (core/format "%-16s%-25s%-22s", - "MIGRATION-ID" "| NAME" "| APPLIED")) - (println (table-line 67)) + (do (prn (table-line 67)) + (prn (core/format "%-16s%-25s%-22s", + "MIGRATION-ID" "| NAME" "| APPLIED")) + (prn (table-line 67)) (doseq [d data] (format-mig-data d)))))) (defn cli-print-migs! [data f & format-opts] (case f "plain" (mig-print-fmt data format-opts) - "edn" (println data) - "json" (println (json/write-str data)) + "edn" (prn data) + "json" (prn (json/write-str data)) nil)) (defn list-pending-migrations [migs format] @@ -226,12 +368,12 @@ 1 java.util.logging.Level/FINE ;; :debug java.util.logging.Level/FINEST)) ;; :trace -(defn set-logger-format +(defn set-logger-format! "Configure JUL logger to use a custom log formatter. * formatter - instance of java.util.logging.Formatter" ([verbosity] - (set-logger-format verbosity (simple-formatter format-log-record))) + (set-logger-format! verbosity (simple-formatter format-log-record))) ([verbosity ^Formatter formatter] (let [main-logger (doto (Logger/getLogger "") (.setUseParentHandlers false) @@ -244,54 +386,108 @@ (.removeHandler main-logger h)) (.addHandler main-logger handler)))) -(defn load-config! - "Returns the content of config file as a clojure map datastructure" - [^String config] - (let [config-path (.getAbsolutePath (io/file config))] - (try - (read-string (slurp config-path)) - (catch java.io.FileNotFoundException e - (binding [*out* *err*] - (println "Missing config file" (.getMessage e) - "\nYou can use --config path_to_file to specify a path to config file")))))) - -(defn up [cfg args] +(defn run-up! [cfg args] (if (empty? args) - (binding [*out* *err*] - (println "To run action up you must provide a migration-id as a parameter: - up ")) + (println-err + "To run action up you must provide a migration-id as a parameter: + up ") (->> args - (map #(parse-long %)) + (map #(my-parse-long %)) (apply migratus/up cfg)))) -(defn down [cfg args] +(defn run-down! [cfg args] (if (empty? args) - (binding [*out* *err*] - (println "To run action down you must provide a migration-id as a parameter: - down ")) + (println-err + "To run action down you must provide a migration-id as a parameter: + down ") (->> args - (map #(parse-long %)) + (map #(my-parse-long %)) (apply migratus/down cfg)))) +(defn run-status + "Display migratus status. + - display last local migration + - display database connection string with credentials REDACTED) + - display last applied migration to database" + [cfg rest-args] + (prn "Not yet implemented")) + +(defn create-migration + "Create a new migration with the current date." + [config & [name type]] + (when-not name + (throw (ex-info "Required name for migration" {}))) + (mig/create config name (or type :sql))) + +(defn run-create-migration! + "Run migratus create command" + [config arguments] + (try + (let [name (first arguments) + file (create-migration config name)] + (println "Created migration" file)) + (catch Exception e + (println-err (ex-data e))))) + +(defn- run-init! + [cfg] + (migratus/init cfg)) + +(defn do-print-usage + ([summary] + (do-print-usage summary nil)) + ([summary errors] + (println (usage summary)) + (when errors + (println-err errors)))) + +(defn- run-reset! [cfg] + (migratus/reset cfg)) + +(defn do-store-actions + [config action action-args] + ;; make store and connect + (with-open [store (doto (proto/make-store config) + (proto/connect))] + (case action + "init" (run-init! store) + "list" (run-list store action-args) + "status" (run-status store action-args) + "up" (run-up! store action-args) + "down" (run-down! store action-args) + "migrate" (run-migrate! store action-args) + "reset" (run-reset! store) + "rollback" (run-rollback! store action-args) + (throw (IllegalArgumentException. (str "Unknown action " action)))))) + (defn -main [& args] - (let [{:keys [options arguments _errors summary]} (parse-opts args global-cli-options :in-order true) - config (:config options) - verbosity (:verbosity options) - cfg (load-config! config) - action (first arguments) - rest-args (rest arguments)] - (set-logger-format verbosity) - (cond - (:help options) (usage summary) - (nil? (:config options)) (error-msg "No config provided \n --config [file-name]>") - :else (case action - "init" (migratus/init cfg) - "create" (migratus/create cfg (second arguments)) - "migrate" (run-migrate cfg rest-args) - "rollback" (run-rollback cfg rest-args) - "reset" (migratus/reset cfg) - "up" (up cfg rest-args) - "down" (down cfg rest-args) - "list" (run-list cfg rest-args) - (no-match-message arguments summary))))) + (try + (let [parsed-opts (parse-opts args global-cli-options) + {:keys [options arguments errors summary]} parsed-opts + {:keys [config-file config verbosity output-format]} options + _ (when (<= 2 verbosity) + (prn "Parsed options:" parsed-opts)) + action (first arguments) + action (when action + (str/lower-case action)) + rest-args (rest arguments) + loaded-config (load-config! config-file config) + no-store-action? (contains? #{"create"} action)] + (swap! app-config assoc + :verbosity verbosity + :output-format output-format) + ;; (prn @app-config) + (set-logger-format! verbosity) + (cond + (:help options) (do-print-usage summary) + (nil? action) (do + (do-print-usage summary) + (println "No action supplied")) + (some? errors) (do-print-usage summary errors) + ;; do not require store + no-store-action? (case action + "create" (run-create-migration! loaded-config rest-args)) + :else (do-store-actions loaded-config action rest-args))) + (catch Exception e + (println-err "Error: " (ex-message e))))) diff --git a/src/migratus/core.clj b/src/migratus/core.clj index 655fd09..1c1efae 100644 --- a/src/migratus/core.clj +++ b/src/migratus/core.clj @@ -68,7 +68,7 @@ (let [completed? (set (proto/completed-ids store))] (filter (comp completed? proto/id) (mig/list-migrations config)))) -(defn gather-migrations +(defn gather-migrations "Returns a list of all migrations from migration dir and db with enriched data: - date and time when was applied; @@ -192,7 +192,7 @@ (proto/init (proto/make-store config))) (defn create - "Create a new migration with the current date" + "Create a new migration with the current date." [config & [name type]] (mig/create config name (or type :sql))) diff --git a/src/migratus/database.clj b/src/migratus/database.clj index a6fe772..29c26a8 100644 --- a/src/migratus/database.clj +++ b/src/migratus/database.clj @@ -288,7 +288,8 @@ proto/Store (config [this] config) (init [this] - (let [conn (connect* (assoc (:db config) :transaction? (:init-in-transaction? config)))] + (let [conn (connect* (assoc (:db config) + :transaction? (:init-in-transaction? config)))] (try (init-db! conn (utils/get-migration-dir config) @@ -303,27 +304,33 @@ (completed [this] (completed* @connection (migration-table-name config))) (migrate-up [this migration] - (log/info "Connection is " @connection - "Config is" (update config :db utils/censor-password)) - (if (proto/tx? migration :up) - (jdbc/with-transaction [t-con (connection-or-spec @connection)] - (migrate-up* t-con config migration)) - (migrate-up* (:db config) config migration))) + (log/info "Connection is " @connection + "Config is" (update config :db utils/censor-password)) + (if (proto/tx? migration :up) + (jdbc/with-transaction [t-con (connection-or-spec @connection)] + (migrate-up* t-con config migration)) + (migrate-up* (:db config) config migration))) (migrate-down [this migration] - (log/info "Connection is " @connection - "Config is" (update config :db utils/censor-password)) - (if (proto/tx? migration :down) - (jdbc/with-transaction [t-con (connection-or-spec @connection)] - (migrate-down* t-con config migration)) - (migrate-down* (:db config) config migration))) + (log/info "Connection is " @connection + "Config is" (update config :db utils/censor-password)) + (if (proto/tx? migration :down) + (jdbc/with-transaction [t-con (connection-or-spec @connection)] + (migrate-down* t-con config migration)) + (migrate-down* (:db config) config migration))) (connect [this] (reset! connection (connect* (:db config))) (init-schema! @connection (migration-table-name config) - (sql-mig/wrap-modify-sql-fn (:modify-sql-fn config)))) + (sql-mig/wrap-modify-sql-fn (:modify-sql-fn config))) + this) (disconnect [this] (disconnect* @connection) - (reset! connection nil))) + (reset! connection nil) + this) + (close [this] + (disconnect* @connection) + (reset! connection nil) + this)) (defmethod proto/make-store :database [config] diff --git a/src/migratus/migrations.clj b/src/migratus/migrations.clj index a0900ab..c98ec13 100644 --- a/src/migratus/migrations.clj +++ b/src/migratus/migrations.clj @@ -158,17 +158,25 @@ (props/load-properties config))] (make-migration config id mig)))) -(defn create [config name migration-type] +(defn create + "Create a migration file given a configuration, a name and migration type. + Resolves the absolute file name. + Returns the migration file name as string. + + Migrations are created in migration-dir. + If migration-dir does not exist, it will be created. " + [config name migration-type] (let [migration-dir (find-or-create-migration-dir (utils/get-parent-migration-dir config) (utils/get-migration-dir config)) migration-name (->kebab-case (str (timestamp) name))] (doall (for [mig-file (proto/migration-files* migration-type migration-name)] - (let [file (io/file migration-dir mig-file)] + (let [file (io/file migration-dir mig-file) + file (.getAbsoluteFile file)] (.createNewFile file) - (.getName (io/file migration-dir mig-file))))))) - + (.getName file)))))) + (defn destroy [config name] (let [migration-dir (utils/find-migration-dir (utils/get-migration-dir config)) diff --git a/src/migratus/protocols.clj b/src/migratus/protocols.clj index 0a802f3..d0d4368 100644 --- a/src/migratus/protocols.clj +++ b/src/migratus/protocols.clj @@ -36,9 +36,14 @@ (migrate-down [this migration] "Run and record a down migration") (connect [this] - "Opens resources necessary to run migrations against the store.") + "Opens resources necessary to run migrations against the store. + Returns the store on sucess so we can participate in with-open. + Throws exception on failure.") (disconnect [this] - "Frees resources necessary to run migrations against the store.")) + "Frees resources necessary to run migrations against the store.") + (close [this] + "Frees resources necessary to run migrations against the store. + Allow store to work with with-open.")) (defmulti make-store :store) diff --git a/test/migratus/mock.clj b/test/migratus/mock.clj index c731c93..532d365 100644 --- a/test/migratus/mock.clj +++ b/test/migratus/mock.clj @@ -40,7 +40,8 @@ (proto/down migration config) (swap! completed-ids disj (proto/id migration))) (connect [this]) - (disconnect [this])) + (disconnect [this]) + (close [this])) (defn make-migration [{:keys [id name ups downs]}] (MockMigration. nil id name ups downs)) diff --git a/test/migratus/test/cli_test.clj b/test/migratus/test/cli_test.clj new file mode 100644 index 0000000..9fd0802 --- /dev/null +++ b/test/migratus/test/cli_test.clj @@ -0,0 +1,39 @@ +(ns migratus.test.cli-test + (:require [clojure.test :refer :all] + [migratus.cli :as cli]) + (:import (java.util HashMap))) + +(deftest load-env-config-test + (testing "Test load config from env - load only MIGRATUS_CONFIG" + (let [env (doto (HashMap.) + (.put "MIGRATUS_CONFIG" "{:store :database :migration-dir \"my-migrations\"}") + (.put "MIGRATUS_DB_SPEC" + "{:jdbcUrl \"config-whould-be-ignored\"}")) + config (cli/env->config! env)] + (is (= {:store :database + :migration-dir "my-migrations"} + config)))) + + (testing "Test load config from env - empty when no env vars present" + (let [env (HashMap.) + config (cli/env->config! env)] + (is (= {} config)))) + + (testing "Test load migratus config from env" + (let [env (doto (HashMap.) + (.put "MIGRATUS_STORE" "database") + (.put "MIGRATUS_MIGRATION_DIR" "resources/migrations") + (.put "MIGRATUS_DB_SPEC" + "{:jdbcUrl \"jdbc:h2:local-db\"}")) + config (cli/env->config! env)] + (is (= {:store :database + :migration-dir "resources/migrations" + :db {:jdbcUrl "jdbc:h2:local-db"}} + config))))) + +(comment + + (run-test load-env-config-test) + + ) +