diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index e8cd3d0ca9e31f77c9fc50b44544318f638789f9..cb837d12d4e28cb0bda9ecc746e4cbdea5d39d84 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -24,7 +24,10 @@
            liquibase.database.jvm.JdbcConnection
            liquibase.resource.ClassLoaderResourceAccessor))
 
-;; ## DB FILE, JDBC/KORMA DEFINITONS
+
+;;; +------------------------------------------------------------------------------------------------------------------------+
+;;; |                                              DB FILE & CONNECTION DETAILS                                              |
+;;; +------------------------------------------------------------------------------------------------------------------------+
 
 (def db-file
   "Path to our H2 DB file from env var or app config."
@@ -88,49 +91,148 @@
 
 (defn jdbc-details
   "Takes our own MB details map and formats them properly for connection details for JDBC."
-  [db-details]
-  {:pre [(map? db-details)]}
-  ;; TODO: it's probably a good idea to put some more validation here and be really strict about what's in `db-details`
-  (case (:type db-details)
-    :h2       (dbspec/h2       db-details)
-    :mysql    (dbspec/mysql    (assoc db-details :db (:dbname db-details)))
-    :postgres (dbspec/postgres (assoc db-details :db (:dbname db-details)))))
+  ([]
+   (jdbc-details @db-connection-details))
+  ([db-details]
+   {:pre [(map? db-details)]}
+   ;; TODO: it's probably a good idea to put some more validation here and be really strict about what's in `db-details`
+   (case (:type db-details)
+     :h2       (dbspec/h2       db-details)
+     :mysql    (dbspec/mysql    (assoc db-details :db (:dbname db-details)))
+     :postgres (dbspec/postgres (assoc db-details :db (:dbname db-details))))))
 
 
-;; ## MIGRATE
+;;; +------------------------------------------------------------------------------------------------------------------------+
+;;; |                                                        MIGRATE                                                         |
+;;; +------------------------------------------------------------------------------------------------------------------------+
 
-(def ^:private ^:const ^String changelog-file
-  "migrations/liquibase.json")
+(def ^:private ^:const ^String changelog-file "migrations/liquibase.json")
+
+(defn- migrations-sql
+  "Return a string of SQL containing the DDL statements needed to perform unran LIQUIBASE migrations."
+  ^String [^Liquibase liquibase]
+  (let [writer (StringWriter.)]
+    (.update liquibase "" writer)
+    (.toString writer)))
+
+(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."
+  [^Liquibase liquibase]
+  (for [line  (s/split-lines (migrations-sql liquibase))
+        :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 [^Liquibase liquibase]
+  (boolean (seq (.listUnrunChangeSets liquibase nil))))
+
+(defn- has-migration-lock?
+  "Is a migration lock in place for LIQUIBASE?"
+  ^Boolean [^Liquibase liquibase]
+  (boolean (seq (.listLocks liquibase))))
+
+(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."
+  [^Liquibase liquibase]
+  (u/auto-retry 5
+    (when (has-migration-lock? liquibase)
+      (Thread/sleep 2000)
+      (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, ^Liquibase liquibase]
+  (when (has-unran-migrations? liquibase)
+    (wait-for-migration-lock-to-be-cleared liquibase)
+    (doseq [line (migrations-lines liquibase)]
+      (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`.)"
+  [conn, ^Liquibase liquibase]
+  (when (has-unran-migrations? liquibase)
+    (doseq [line (migrations-lines liquibase)]
+      (log/info line)
+      (jdbc/with-db-transaction [nested-transaction-connection conn]
+        (try (jdbc/execute! nested-transaction-connection [line])
+             (log/info (u/format-color 'green "[SUCCESS]"))
+             (catch Throwable e
+               (.rollback (jdbc/get-connection nested-transaction-connection))
+               (log/error (u/format-color 'red "[ERROR] %s" (.getMessage e)))))))))
+
+(defn- conn->liquibase
+  "Get a `Liquibase` object from JDBC CONN."
+  (^Liquibase []
+   (conn->liquibase (jdbc-details)))
+  (^Liquibase [conn]
+   (let [^JdbcConnection liquibase-conn (JdbcConnection. (jdbc/get-connection conn))
+         ^Database       database       (.findCorrectDatabaseImplementation (DatabaseFactory/getInstance) liquibase-conn)]
+     (Liquibase. changelog-file (ClassLoaderResourceAccessor.) database))))
 
 (defn migrate
-  "Migrate the database (this can also be ran via command line like `lein run migrate down-one`):
+  "Migrate the database (this can also be ran via command line like `java -jar metabase.jar migrate up` or `lein run migrate up`):
 
    *  `:up`            - Migrate up
+   *  `:force`         - Force migrate up, ignoring locks and any DDL statements that fail.
    *  `:down`          - Rollback *all* migrations
    *  `:down-one`      - Rollback a single migration
    *  `:print`         - Just print the SQL for running the migrations, don't actually run them.
    *  `:release-locks` - Manually release migration locks left by an earlier failed migration.
-                         (This shouldn't be necessary now that we run migrations inside a transaction,
-                         but is available just in case)."
+                         (This shouldn't be necessary now that we run migrations inside a transaction, but is available just in case)."
+  ([]
+   (migrate :up))
   ([direction]
    (migrate @db-connection-details direction))
   ([db-details direction]
    (jdbc/with-db-transaction [conn (jdbc-details db-details)]
-     (let [^Database database (-> (DatabaseFactory/getInstance)
-                                  (.findCorrectDatabaseImplementation (JdbcConnection. (jdbc/get-connection conn))))
-           ^Liquibase liquibase (Liquibase. changelog-file (ClassLoaderResourceAccessor.) database)]
-       (case direction
-         :up            (.update liquibase "")
-         :down          (.rollback liquibase 10000 "")
-         :down-one      (.rollback liquibase 1 "")
-         :print         (let [writer (StringWriter.)]
-                          (.update liquibase "" writer)
-                          (.toString writer))
-         :release-locks (.forceReleaseLocks liquibase))))))
+     ;; Tell transaction to automatically `.rollback` instead of `.commit` when the transaction finishes
+     (jdbc/db-set-rollback-only! conn)
+     ;; Disable auto-commit. This should already be off but set it just to be safe
+     (.setAutoCommit (jdbc/get-connection conn) false)
+     ;; Set up liquibase and let it do its thing
+     (try
+       (let [liquibase (conn->liquibase conn)]
+         (case direction
+           :up            (migrate-up-if-needed conn liquibase)
+           :force         (force-migrate-up-if-needed conn liquibase)
+           :down          (.rollback liquibase 10000 "")
+           :down-one      (.rollback liquibase 1 "")
+           :print         (println (migrations-sql liquibase))
+           :release-locks (.forceReleaseLocks liquibase)))
+       ;; Migrations were successful; disable rollback-only so `.commit` will be called instead of `.rollback`
+       (jdbc/db-unset-rollback-only! conn)
+       :done
+       ;; If for any reason any part of the migrations fail then rollback all changes
+       (catch Throwable e
+         ;; This should already be happening as a result of `db-set-rollback-only!` but running it twice won't hurt so better safe than sorry
+         (.rollback (jdbc/get-connection conn))
+         (throw e))))))
 
 
 ;;; +------------------------------------------------------------------------------------------------------------------------+
-;;; |                                           DB CONNECTION / TRANSACTION STUFF                                            |
+;;; |                                          CONNECTION POOLS & TRANSACTION STUFF                                          |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
 (defn connection-pool
@@ -193,7 +295,7 @@
 
 
 ;;; +------------------------------------------------------------------------------------------------------------------------+
-;;; |                                                 DB SETUP & MIGRATIONS                                                  |
+;;; |                                                       DB SETUP                                                         |
 ;;; +------------------------------------------------------------------------------------------------------------------------+
 
 
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index bff34c1ee46c09acc3030fde05d1354c5c033191..4e84e8fc58bf16aa4b5bbab4ab73c85cb2724a05 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -227,14 +227,14 @@
   [sql]
   (when sql
     (-> sql
-        (s/replace #"\sFROM" "\nFROM")
+        (s/replace #"\sFROM"      "\nFROM")
         (s/replace #"\sLEFT JOIN" "\nLEFT JOIN")
-        (s/replace #"\sWHERE" "\nWHERE")
-        (s/replace #"\sGROUP BY" "\nGROUP BY")
-        (s/replace #"\sORDER BY" "\nORDER BY")
-        (s/replace #"\sLIMIT" "\nLIMIT")
-        (s/replace #"\sAND\s" "\n   AND ")
-        (s/replace #"\sOR\s" "\n    OR "))))
+        (s/replace #"\sWHERE"     "\nWHERE")
+        (s/replace #"\sGROUP BY"  "\nGROUP BY")
+        (s/replace #"\sORDER BY"  "\nORDER BY")
+        (s/replace #"\sLIMIT"     "\nLIMIT")
+        (s/replace #"\sAND\s"     "\n   AND ")
+        (s/replace #"\sOR\s"      "\n    OR "))))
 
 
 ;; TODO - make this a protocol method ?
@@ -304,13 +304,15 @@
          (throw (Exception. (exception->nice-error-message e))))))
 
 (defn- do-with-auto-commit-disabled
-  "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/indent 1}
-  [{^java.sql.Connection connection :connection}, f]
-  (.setAutoCommit connection false)
+  [conn f]
+  (jdbc/db-set-rollback-only! conn)
+  (.setAutoCommit (jdbc/get-connection conn) 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?
   (try (f)
-       (finally (.rollback connection))))
+       (finally (.rollback (jdbc/get-connection conn)))))
 
 (defn- do-in-transaction [connection f]
   (jdbc/with-db-transaction [transaction-connection connection]