From 30e731e3c5766561c041802a6349764adbb802ce Mon Sep 17 00:00:00 2001 From: dpsutton <dan@dpsutton.com> Date: Tue, 21 Feb 2023 13:00:33 -0600 Subject: [PATCH] Custom migrations (#28175) * Custom migrations Current syntax: specify the migration with ``` - changeSet: id: v46.00-080 author: dpsutton comment: Uppercases all Card names changes: - customChange: class: "metabase.db.custom_migrations.ReversibleUppercaseCards" ``` and in the new namespace metabase.db.custom-migrations: ```clojure (defmigration UppercaseCards (db/execute! {:update :report_card :set {:name :%upper.name}})) (def-reversible-migration ReversibleUppercaseCards (db/execute! {:update :report_card :set {:name :%upper.name}}) (db/execute! {:update :report_card :set {:name :%lower.name}})) ``` * Use db provided by liquibase * edit docstring * set *warn-on-reflection* to fix lint error & rebase --------- Co-authored-by: Noah Moss <noahbmoss@gmail.com> Co-authored-by: Noah Moss <32746338+noahmoss@users.noreply.github.com> --- .clj-kondo/config.edn | 2 + .../src/change/strict.clj | 7 ++- .../src/change_set/strict.clj | 5 +- .../test/lint_migrations_file_test.clj | 43 +++++++++++++-- src/metabase/db/custom_migrations.clj | 54 +++++++++++++++++++ src/metabase/db/setup.clj | 1 + 6 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 src/metabase/db/custom_migrations.clj diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 0b385bfe8ed..f80335a4a88 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -399,6 +399,8 @@ metabase.api.search-test/do-test-users clojure.core/let metabase.async.api-response-test/with-response clojure.core/let metabase.dashboard-subscription-test/with-dashboard-sub-for-card clojure.core/let + metabase.db.custom-migrations/defmigration clj-kondo.lint-as/def-catch-all + metabase.db.custom-migrations/def-reversible-migration clj-kondo.lint-as/def-catch-all metabase.db.data-migrations/defmigration clojure.core/def metabase.db.liquibase/with-liquibase clojure.core/let metabase.db.schema-migrations-test.impl/with-temp-empty-app-db clojure.core/let diff --git a/bin/lint-migrations-file/src/change/strict.clj b/bin/lint-migrations-file/src/change/strict.clj index 49b40465d35..bff8cc099e6 100644 --- a/bin/lint-migrations-file/src/change/strict.clj +++ b/bin/lint-migrations-file/src/change/strict.clj @@ -38,8 +38,13 @@ (s/def ::createIndex (s/keys :req-un [::indexName])) +(s/def :custom-change/class (every-pred string? (complement str/blank?))) + +(s/def ::customChange + (s/keys :req-un [:custom-change/class])) + (s/def ::change - (s/keys :opt-un [::addColumn ::createTable ::createIndex])) + (s/keys :opt-un [::addColumn ::createTable ::createIndex ::customChange])) (s/def :change.strict.dbms-qualified-sql-change.sql/dbms string?) diff --git a/bin/lint-migrations-file/src/change_set/strict.clj b/bin/lint-migrations-file/src/change_set/strict.clj index 63794fafa9f..8003877c375 100644 --- a/bin/lint-migrations-file/src/change_set/strict.clj +++ b/bin/lint-migrations-file/src/change_set/strict.clj @@ -55,7 +55,10 @@ :renameSequence :renameTable :renameTrigger - :renameView}) + :renameView + ;; assumes all custom changes use the `def-migration` or `def-reversible-migration` in + ;; metabase.db.custom-migrations + :customChange}) (defn- major-version "Returns major version from id string, e.g. 44 from \"v44.00-034\"" diff --git a/bin/lint-migrations-file/test/lint_migrations_file_test.clj b/bin/lint-migrations-file/test/lint_migrations_file_test.clj index 19ea98ad8d6..76592fd8a6f 100644 --- a/bin/lint-migrations-file/test/lint_migrations_file_test.clj +++ b/bin/lint-migrations-file/test/lint_migrations_file_test.clj @@ -1,12 +1,15 @@ (ns lint-migrations-file-test (:require + [clojure.spec.alpha :as s] [clojure.test :refer :all] [lint-migrations-file :as lint-migrations-file])) -(defn mock-change-set [& keyvals] +(defn mock-change-set + "Returns a \"strict\" migration (id > switch to strict). If you want a non-strict migration send :id 1 in `keyvals`. " + [& keyvals] {:changeSet (merge - {:id 1 + {:id 1000 :author "camsaul" :comment "Added x.37.0" :changes [{:whatever {}}]} @@ -31,6 +34,10 @@ (lint-migrations-file/validate-migrations {:databaseChangeLog changes})) +(defn- validate-ex-info [& changes] + (try (lint-migrations-file/validate-migrations {:databaseChangeLog changes}) + (catch Exception e (ex-data e)))) + (deftest require-unique-ids-test (testing "Make sure all migration IDs are unique" (is (thrown-with-msg? @@ -70,13 +77,12 @@ (testing "[strict only] only allow one change per change set" (is (= :ok (validate - (mock-change-set :changes [(mock-add-column-changes) (mock-add-column-changes)])))) + (mock-change-set :id 1 :changes [(mock-add-column-changes) (mock-add-column-changes)])))) (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Extra input" (validate - (mock-change-set :id 200 - :changes [(mock-add-column-changes) (mock-add-column-changes)])))))) + (mock-change-set :changes [(mock-add-column-changes) (mock-add-column-changes)])))))) (deftest require-comment-test (testing "[strict only] require a comment for a change set" @@ -215,3 +221,30 @@ (validate (mock-change-set :id "v45.12-345" :changes [(mock-add-column-changes :columns [(mock-column :constraints {:deleteCascade true})])])))))) + +(deftest custom-changes-test + (let [change-set (mock-change-set + :changes + [{:customChange {:class "metabase.db.custom_migrations.ReversibleUppercaseCards"}}])] + (is (= :ok + (validate change-set)))) + (testing "missing value" + (let [change-set (mock-change-set + :changes + [{:customChange {}}]) + ex-info (validate-ex-info change-set)] + (is (not= :ok ex-info)))) + (testing "invalid values" + (doseq [bad-value [nil 3 ""]] + (let [change-set (mock-change-set + :changes + [{:customChange {:class bad-value}}]) + ex-info (validate-ex-info change-set) + specific (->> ex-info + ::s/problems + (some (fn [problem] + (when (= (:val problem) bad-value) + problem))))] + (is (not= :ok ex-info)) + (is (= (take-last 2 (:via specific)) + [:change.strict/customChange :custom-change/class])))))) diff --git a/src/metabase/db/custom_migrations.clj b/src/metabase/db/custom_migrations.clj new file mode 100644 index 00000000000..ee0fbdb421b --- /dev/null +++ b/src/metabase/db/custom_migrations.clj @@ -0,0 +1,54 @@ +(ns metabase.db.custom-migrations + (:require [metabase.util.log :as log] + [toucan2.connection :as t2.conn]) + (:import [liquibase.change.custom CustomTaskChange CustomTaskRollback] + liquibase.exception.ValidationErrors)) + +(set! *warn-on-reflection* true) + +(defmacro def-reversible-migration + "Define a reversible custom migration. Both the forward and reverse migrations are defined using the same structure, + similar to the bodies of multi-arity Clojure functions. + + The first thing in each migration body must be a one-element vector containing a binding to use for the database + object provided by Liquibase, so that migrations have access to it if needed. This should typically not be used + directly, however, because is also set automatically as the current connection for Toucan 2. + + Example: + + ```clj + (def-reversible-migration ExampleMigrationName + ([_database] + (migration-body)) + + ([_database] + (migration-body)))" + [name [[db-binding-1] & migration-body] [[db-binding-2] reverse-migration-body]] + `(defrecord ~name [] + CustomTaskChange + (execute [_# database#] + (binding [toucan2.connection/*current-connectable* (.getWrappedConnection (.getConnection database#))] + (let [~db-binding-1 database#] + ~@migration-body))) + (getConfirmationMessage [_#] + (str "Custom migration: " ~name)) + (setUp [_#]) + (validate [_# _database#] + (ValidationErrors.)) + (setFileOpener [_# _resourceAccessor#]) + + CustomTaskRollback + (rollback [_# database#] + (binding [toucan2.connection/*current-connectable* (.getWrappedConnection (.getConnection database#))] + (let [~db-binding-2 database#] + ~@reverse-migration-body))))) + +(defn no-op + "No-op logging rollback function" + [n] + (log/info "No rollback for: " n)) + +(defmacro defmigration + "Define a custom migration." + [name migration-body] + `(def-reversible-migration ~name ~migration-body ([~'_] (no-op ~(str name))))) diff --git a/src/metabase/db/setup.clj b/src/metabase/db/setup.clj index 470011cd191..a3a1b75a5ef 100644 --- a/src/metabase/db/setup.clj +++ b/src/metabase/db/setup.clj @@ -9,6 +9,7 @@ (:require [honey.sql :as sql] [metabase.db.connection :as mdb.connection] + metabase.db.custom-migrations ;; load our custom migrations [metabase.db.jdbc-protocols :as mdb.jdbc-protocols] [metabase.db.liquibase :as liquibase] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] -- GitLab