(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!)) | |