(ns dev.migrate (:gen-class) (:require [clojure.string :as str] [metabase.db :as mdb] [metabase.db.liquibase :as liquibase] [metabase.util.malli :as mu] [toucan2.core :as t2]) (:import (liquibase Contexts Liquibase RuntimeEnvironment) (liquibase.change Change) (liquibase.changelog ChangeLogIterator ChangeSet DatabaseChangeLog) (liquibase.changelog.filter ChangeSetFilter) (liquibase.changelog.visitor ListVisitor) (liquibase.database Database) (liquibase.database.core H2Database MySQLDatabase PostgresDatabase MariaDBDatabase) (liquibase.exception RollbackImpossibleException) (liquibase.sql Sql) (liquibase.sqlgenerator SqlGeneratorFactory) (liquibase.statement SqlStatement))) | |
(set! *warn-on-reflection* true) | |
Run migrations for the Metabase application database. Possible directions are | (defn- latest-migration [] ((juxt :id :comments) (t2/query-one {:select [:id :comments] :from [(keyword (liquibase/changelog-table-name (mdb/data-source)))] :order-by [[:orderexecuted :desc]] :limit 1}))) (defn migrate! ([] (migrate! :up)) ;; do we really use this in dev? ([direction & [version]] (mdb/migrate! (mdb/data-source) direction version) #_{:clj-kondo/ignore [:discouraged-var]} (println (format "Migrated %s. Latest migration: %s" (name direction) (latest-migration))))) |
(defn- rollback-n-migrations! [^Integer n] (with-open [conn (.getConnection (mdb/data-source))] (liquibase/with-liquibase [^Liquibase liquibase conn] (liquibase/with-scope-locked liquibase (.rollback liquibase n ))))) | |
(defn- migration-since [id] (->> (t2/query-one {:select [[:%count.* :count]] :from [:databasechangelog] :where [:> :orderexecuted {:select [:orderexecuted] :from [:databasechangelog] :where [:like :id (format "%s%%" id)] :order-by [[:orderexecuted :desc]] :limit 1}] :limit 1}) :count ;; includes the selected id inc)) | |
(defn- maybe-parse-long [x] (cond-> x (string? x) parse-long)) | |
Rollback helper, can take a number of migrations to rollback or a specific migration ID(inclusive). ;; Rollback 2 migrations: (rollback! :count 2) ;; rollback to "v50.2024-03-18T16:00:00" (inclusive) (rollback! :id "v50.2024-03-18T16:00:00") | (mu/defn rollback! [k :- [:enum :id :count "id" "count"] target] (let [n (case (keyword k) :id (migration-since target) :count (maybe-parse-long target))] (rollback-n-migrations! n) #_{:clj-kondo/ignore [:discouraged-var]} (println (format "Rollbacked %d migrations. Latest migration: %s" n (latest-migration))))) |
Print the latest migration ID. | (defn migration-status [] #_{:clj-kondo/ignore [:discouraged-var]} (println "Current migration:" (latest-migration))) |
Migrations helpers Usage: clojure -M:migrate up ;; migrate up to the latest clojure -M:migrate rollback count 2 ;; rollback 2 migrations clojure -M:migrate rollback id "v40.00.001" ;; rollback to a specific migration with id clojure -M:migrate status ;; print the latest migration id | (defn -main [& args] (let [[cmd & migration-args] args] (case cmd "rollback" (apply rollback! migration-args) "up" (apply migrate! migration-args) "status" (migration-status) (throw (ex-info "Invalid command" {:command cmd :args args}))))) |
(defn- stmts-to-sql [stmts sql-generator-factory database] (str/join "\n" (for [stmt stmts sql (.generateSql ^SqlGeneratorFactory sql-generator-factory ^SqlStatement stmt ^Database database)] (.toString ^Sql sql)))) | |
(defn- change->sql [^Change change sql-generator-factory database] {:forward (stmts-to-sql (.generateStatements change database) sql-generator-factory database) :rollback (try (stmts-to-sql (.generateRollbackStatements change database) sql-generator-factory database) (catch RollbackImpossibleException e (str "Rollback impossible " e)))}) | |
(defn- liquibase-database [db-type] (case db-type :postgres (PostgresDatabase.) :mysql (MySQLDatabase.) :mariadb (MariaDBDatabase.) :h2 (H2Database.))) | |
Get the sql statements for a specific migration ID and DB type. If no DB type is provided, it will use the current application DB type. (migration-sql-by-id "v51.2024-06-12T18:53:02" :postgres) ;; => {:forward "DROP INDEX public.idxuseriddeviceid;", :rollback "CREATE INDEX idxuseriddeviceid ON public.loginhistory(sessionid, device_id);"} | (mu/defn migration-sql-by-id ([id] (migration-sql-by-id id (mdb/db-type))) ([id db-type :- [:enum :postgres :mysql :mariadb :h2]] (t2/with-connection [conn] (liquibase/with-liquibase [^Liquibase liquibase conn] (let [database (liquibase-database db-type) change-log-iterator (ChangeLogIterator. ^DatabaseChangeLog (.getDatabaseChangeLog liquibase) ^"[Lliquibase.changelog.filter.ChangeSetFilter;" (into-array ChangeSetFilter [])) list-visistor (ListVisitor.) runtime-env (RuntimeEnvironment. database (Contexts.) nil) _ (.run change-log-iterator list-visistor runtime-env) ^ChangeSet change-set (first (filter #(= id (.getId ^ChangeSet %)) (.getSeenChangeSets list-visistor))) sql-generator-factory (SqlGeneratorFactory/getInstance)] (reduce (fn [acc data] ;; merge all changes in one change set into one single :forward and :rollback (merge-with (fn [x y] (str x "\n" y)) acc data)) {} (map #(change->sql % sql-generator-factory database) (.getChanges change-set)))))))) |
(comment (rollback! :count 1) (rollback! :id "v51.2024-08-30T08:00:03") (migration-sql-by-id "v51.2024-09-05T08:00:04" :postgres) (migrate!)) | |