"Return a string of SQL containing the DDL statements needed to perform unran LIQUIBASE migrations."
^String[^Liquibaseliquibase]
(let[writer(StringWriter.)]
(.updateliquibase""writer)
(.toStringwriter)))
(defn-migrations-lines
"Return a sequnce of DDL statements that should be used to perform migrations for LIQUIBASE.
MySQL gets snippy if we try to run the entire DB migration as one single string; it seems to only like it if we run one statement at a time;
Liquibase puts each DDL statement on its own line automatically so just split by lines and filter out blank / comment lines. Even though this
is not neccesary for H2 or Postgres go ahead and do it anyway because it keeps the code simple and doesn't make a significant performance difference."
[^Liquibaseliquibase]
(for[line(s/split-lines(migrations-sqlliquibase))
:when(not(or(s/blank?line)
(re-find#"^--"line)))]
line))
(defn-has-unran-migrations?
"Does LIQUIBASE have migration change sets that haven't been ran yet?
It's a good idea to Check to make sure there's actually something to do before running `(migrate :up)` because `migrations-sql` will
always contain SQL to create and release migration locks, which is both slightly dangerous and a waste of time when we won't be using them."
^Boolean[^Liquibaseliquibase]
(boolean(seq(.listUnrunChangeSetsliquibasenil))))
(defn-has-migration-lock?
"Is a migration lock in place for LIQUIBASE?"
^Boolean[^Liquibaseliquibase]
(boolean(seq(.listLocksliquibase))))
(defn-wait-for-migration-lock-to-be-cleared
"Check and make sure the database isn't locked. If it is, sleep for 2 seconds and then retry several times.
There's a chance the lock will end up clearing up so we can run migrations normally."
[^Liquibaseliquibase]
(u/auto-retry5
(when(has-migration-lock?liquibase)
(Thread/sleep2000)
(throw(Exception."Database has migration lock; cannot run migrations. You can force-release these locks by running `java -jar metabase.jar migrate release-locks`.")))))
(defn-migrate-up-if-needed
"Run any unran LIQUIBASE migrations, if needed.
This creates SQL for the migrations to be performed, then executes each DDL statement.
Running `.update` directly doesn't seem to work as we'd expect; it ends up commiting the changes made and they can't be rolled back at
the end of the transaction block. Converting the migration to SQL string and running that via `jdbc/execute!` seems to do the trick."
[conn,^Liquibaseliquibase]
(when(has-unran-migrations?liquibase)
(wait-for-migration-lock-to-be-clearedliquibase)
(doseq[line(migrations-linesliquibase)]
(jdbc/execute!conn[line]))))
(defn-force-migrate-up-if-needed
"Force migrating up. This does two things differently from `migrate-up-if-needed`:
1. This doesn't check to make sure the DB locks are cleared
2. Any DDL statements that fail are ignored
It can be used to fix situations where the database got into a weird state, as was common before the fixes made in #3295.
Each DDL statement is ran inside a nested transaction; that way if the nested transaction fails we can roll it back without rolling back the entirety of changes
that were made. (If a single statement in a transaction fails you can't do anything futher until you clear the error state by doing something like calling `.rollback`.)"
"Disable auto-commit for this transaction, that way shady queries are unable to modify the database; execute F in a try-finally block.
In the `finally`, rollback any changes made during this transaction just to be extra-double-sure JDBC doesn't try to commit them automatically for us."
"Disable auto-commit for this transaction, and make the transaction `rollback-only`, which means when the transaction finishes `.rollback` will be called instead of `.commit`.
Furthermore, execute F in a try-finally block; in the `finally`, manually call `.rollback` just to be extra-double-sure JDBC any changes made by the transaction aren't committed."
{:style/indent1}
[{^java.sql.Connectionconnection:connection},f]
(.setAutoCommitconnectionfalse)
[connf]
(jdbc/db-set-rollback-only!conn)
(.setAutoCommit(jdbc/get-connectionconn)false)
;; TODO - it would be nice if we could also `.setReadOnly` on the transaction as well, but that breaks setting the timezone. Is there some way we can have our cake and eat it too?