diff --git a/backend/junit/test/metabase/junit.clj b/backend/junit/test/metabase/junit.clj
index 03e92ad806fa2bc8bbf8241ec5dec6d395d7ea06..e7e73bf0943199895dcdbc573caded413e1b7604 100644
--- a/backend/junit/test/metabase/junit.clj
+++ b/backend/junit/test/metabase/junit.clj
@@ -3,6 +3,7 @@
   (:require [clojure
              [pprint :as pp]
              [string :as str]]
+            [metabase.util :as u]
             [pjstadig.print :as p]
             [test-report-junit-xml.core :as junit-xml]))
 
@@ -15,30 +16,31 @@
      (str "\n" message))))
 
 (defn- result-output [{:keys [expected actual diffs message], :as event}]
-  (with-out-str
-    (println (event-description event))
-    ;; this code is adapted from `pjstadig.util`
-    (p/with-pretty-writer
-      (fn []
-        (let [print-expected (fn [actual]
-                               (p/rprint "expected: ")
-                               (pp/pprint expected)
-                               (p/rprint "  actual: ")
-                               (pp/pprint actual)
-                               (p/clear))]
-          (if (seq diffs)
-            (doseq [[actual [a b]] diffs]
-              (print-expected actual)
-              (p/rprint "    diff:")
-              (if a
-                (do (p/rprint " - ")
-                    (pp/pprint a)
-                    (p/rprint "          + "))
-                (p/rprint " + "))
-              (when b
-                (pp/pprint b))
-              (p/clear))
-            (print-expected actual)))))))
+  (with-redefs [u/colorize? (constantly false)]
+    (with-out-str
+      (println (event-description event))
+      ;; this code is adapted from `pjstadig.util`
+      (p/with-pretty-writer
+        (fn []
+          (let [print-expected (fn [actual]
+                                 (p/rprint "expected: ")
+                                 (pp/pprint expected)
+                                 (p/rprint "  actual: ")
+                                 (pp/pprint actual)
+                                 (p/clear))]
+            (if (seq diffs)
+              (doseq [[actual [a b]] diffs]
+                (print-expected actual)
+                (p/rprint "    diff:")
+                (if a
+                  (do (p/rprint " - ")
+                      (pp/pprint a)
+                      (p/rprint "          + "))
+                  (p/rprint " + "))
+                (when b
+                  (pp/pprint b))
+                (p/clear))
+              (print-expected actual))))))))
 
 (defmulti format-result
   {:arglists '([event])}
diff --git a/modules/drivers/mongo/test/metabase/test/data/mongo.clj b/modules/drivers/mongo/test/metabase/test/data/mongo.clj
index dfd86d443e42c322cf731a3312e07a052388f221..24593f7fe88ec8192d7a454b1b8cd4afabbfafef 100644
--- a/modules/drivers/mongo/test/metabase/test/data/mongo.clj
+++ b/modules/drivers/mongo/test/metabase/test/data/mongo.clj
@@ -18,12 +18,12 @@
 
 (defmethod tx/dbdef->connection-details :mongo
   [_ _ dbdef]
-  {:dbname (tx/escaped-name dbdef)
+  {:dbname (tx/escaped-database-name dbdef)
    :host   "localhost"})
 
 (defn- destroy-db! [driver dbdef]
   (with-open [mongo-connection (mg/connect (tx/dbdef->connection-details driver :server dbdef))]
-    (mg/drop-db mongo-connection (tx/escaped-name dbdef))))
+    (mg/drop-db mongo-connection (tx/escaped-database-name dbdef))))
 
 (defmethod tx/create-db! :mongo
   [driver {:keys [table-definitions], :as dbdef} & {:keys [skip-drop-db?], :or {skip-drop-db? false}}]
diff --git a/modules/drivers/snowflake/test/metabase/driver/snowflake_test.clj b/modules/drivers/snowflake/test/metabase/driver/snowflake_test.clj
index 35ac91e71ba3ce2a35f223ed7233216dbf0cc632..5fd4143f82c98f4db064c6c724615d4e30dfd9a4 100644
--- a/modules/drivers/snowflake/test/metabase/driver/snowflake_test.clj
+++ b/modules/drivers/snowflake/test/metabase/driver/snowflake_test.clj
@@ -50,8 +50,8 @@
                 FOREIGN KEY (\"user_id\") REFERENCES \"v2_test-data\".\"PUBLIC\".\"users\" (\"id\");"
                "ALTER TABLE \"v2_test-data\".\"PUBLIC\".\"checkins\" ADD CONSTRAINT \"kins_venue_id_venues_621212269\"
                 FOREIGN KEY (\"venue_id\") REFERENCES \"v2_test-data\".\"PUBLIC\".\"venues\" (\"id\");"])
-             (ddl/create-db-ddl-statements :snowflake (-> (mt/get-dataset-definition dataset-defs/test-data)
-                                                          (update :database-name #(str "v2_" %)))))))))
+             (ddl/create-db-tables-ddl-statements :snowflake (-> (mt/get-dataset-definition dataset-defs/test-data)
+                                                                 (update :database-name #(str "v2_" %)))))))))
 
 ;; TODO -- disabled because these are randomly failing, will figure out when I'm back from vacation. I think it's a
 ;; bug in the JDBC driver -- Cam
diff --git a/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj b/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj
index 94a87f44ce63ad2ffbc007cd631e3fae44b62f14..34b3fb89ee57cfb13108d5d08bdd628dac478756 100644
--- a/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj
+++ b/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj
@@ -8,7 +8,7 @@
 (sql-jdbc.tx/add-test-extensions! :sqlite)
 
 (defmethod tx/dbdef->connection-details :sqlite [_ context dbdef]
-  {:db (str (tx/escaped-name dbdef) ".sqlite")})
+  {:db (str (tx/escaped-database-name dbdef) ".sqlite")})
 
 (doseq [[base-type sql-type] {:type/BigInteger "BIGINT"
                               :type/Boolean    "BOOLEAN"
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index 655474e8533f5302e5878b6f00e0292582136eac..683b2ad98b880b97476461bcb1ada506e2314455 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -248,14 +248,21 @@
    (when-let [max-pool-size (config/config-int :mb-application-db-max-connection-pool-size)]
      {"maxPoolSize" max-pool-size})))
 
+(defn quoting-style
+  "HoneySQL quoting style to use for application DBs of the given type. Note for H2 application DBs we automatically
+  uppercase all identifiers (since this is H2's default behavior) whereas in the SQL QP we stick with the case we got
+  when we synced the DB."
+  [db-type]
+  (case db-type
+    :postgres :ansi
+    :h2       :h2
+    :mysql    :mysql))
+
 (defn- create-connection-pool!
   "Create a connection pool for the application DB and set it as the default Toucan connection. This is normally called
   once during start up; calling it a second time (e.g. from the REPL) will "
   [jdbc-spec]
-  (db/set-default-quoting-style! (case (db-type)
-                                   :postgres :ansi
-                                   :h2       :h2
-                                   :mysql    :mysql))
+  (db/set-default-quoting-style! (quoting-style (db-type)))
   ;; REPL usage only: kill the old pool if one exists
   (u/ignore-exceptions
     (when-let [^PoolBackedDataSource pool (:datasource (db/connection))]
diff --git a/src/metabase/db/liquibase.clj b/src/metabase/db/liquibase.clj
index 8723a85c530f120b473dd65139b7bb773bcb3901..8b7d7f8c32804a906bc3fa1661ebcd4af83d323f 100644
--- a/src/metabase/db/liquibase.clj
+++ b/src/metabase/db/liquibase.clj
@@ -32,19 +32,18 @@
   ;; closing the `LiquibaseConnection`/`Database` closes the parent JDBC `Connection`, so only use it in combination
   ;; with `with-open` *if* we are opening a new JDBC `Connection` from a JDBC spec. If we're passed in a `Connection`,
   ;; it's safe to assume the caller is managing its lifecycle.
-  (letfn []
-    (cond
-      (instance? java.sql.Connection jdbc-spec-or-conn)
-      (f (-> jdbc-spec-or-conn liquibase-connection database liquibase))
+  (cond
+    (instance? java.sql.Connection jdbc-spec-or-conn)
+    (f (-> jdbc-spec-or-conn liquibase-connection database liquibase))
 
-      (:connection jdbc-spec-or-conn)
-      (recur (:connection jdbc-spec-or-conn) f)
+    (:connection jdbc-spec-or-conn)
+    (recur (:connection jdbc-spec-or-conn) f)
 
-      :else
-      (with-open [jdbc-conn      (jdbc/get-connection jdbc-spec-or-conn)
-                  liquibase-conn (liquibase-connection jdbc-conn)
-                  database       (database liquibase-conn)]
-        (f (liquibase database))))))
+    :else
+    (with-open [jdbc-conn      (jdbc/get-connection jdbc-spec-or-conn)
+                liquibase-conn (liquibase-connection jdbc-conn)
+                database       (database liquibase-conn)]
+      (f (liquibase database)))))
 
 (defmacro with-liquibase
   "Execute body with an instance of a `Liquibase` bound to `liquibase-binding`.
@@ -73,7 +72,7 @@
   filter out blank / comment lines. Even though this is not necessary for H2 or Postgres go ahead and do it anyway
   because it keeps the code simple and doesn't make a significant performance difference.
 
-  As of 0.31.1 this is only used for printing the migrations without running or using force migrating."
+  As of 0.31.1 this is only used for printing the migrations without running or when force migrating."
   [^Liquibase liquibase]
   (for [line  (str/split-lines (migrations-sql liquibase))
         :when (not (or (str/blank? line)
diff --git a/test/metabase/db/schema_migrations_test.clj b/test/metabase/db/schema_migrations_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..78509fbb05b1bd3ad5baf19f63a69277652631da
--- /dev/null
+++ b/test/metabase/db/schema_migrations_test.clj
@@ -0,0 +1,33 @@
+(ns metabase.db.schema-migrations-test
+  "Tests for the schema migrations defined in the Liquibase YAML files. The basic idea is:
+
+  1. Create a temporary H2/Postgres/MySQL/MariaDB database
+  2. Run all migrations up to a certain point
+  3. Load some arbitrary data
+  4. run migration(s) after that point (verify that they actually run)
+  5. verify that data looks like what we'd expect after running migration(s)
+
+  See `metabase.db.schema-migrations-test.impl` for the implementation of this functionality."
+  (:require [clojure.test :refer :all]
+            [metabase.db.schema-migrations-test.impl :as impl]
+            [metabase.models :refer [Database Field Table]]
+            [toucan.db :as db]))
+
+(deftest database-position-test
+  (testing "Migration 165: add `database_position` to Field"
+    (impl/test-migrations 165 [migrate!]
+      ;; create a Database with a Table with 2 Fields
+      (db/simple-insert! Database {:name "DB", :engine "h2", :created_at :%now, :updated_at :%now})
+      (db/simple-insert! Table {:name "Table", :db_id 1, :created_at :%now, :updated_at :%now, :active true})
+      (let [mock-field {:table_id 1, :created_at :%now, :updated_at :%now, :base_type "type/Text", :database_type "VARCHAR"}]
+        (db/simple-insert! Field (assoc mock-field :name "Field 1"))
+        (db/simple-insert! Field (assoc mock-field :name "Field 2")))
+      (testing "sanity check: Fields should not have a `:database_position` column yet"
+        (is (not (contains? (Field 1) :database_position))))
+      ;; now run migration 165
+      (migrate!)
+      (testing "Fields should get `:database_position` equal to their IDs"
+        (doseq [id [1 2]]
+          (testing (format "Field %d" id)
+            (is (= id
+                   (db/select-one-field :database_position Field :id id)))))))))
diff --git a/test/metabase/db/schema_migrations_test/impl.clj b/test/metabase/db/schema_migrations_test/impl.clj
new file mode 100644
index 0000000000000000000000000000000000000000..388cbf9c61f430fd664228bbd6651b67c6207acf
--- /dev/null
+++ b/test/metabase/db/schema_migrations_test/impl.clj
@@ -0,0 +1,162 @@
+(ns metabase.db.schema-migrations-test.impl
+  "Tests for the schema migrations defined in the Liquibase YAML files. The basic idea is:
+
+  1. Create a temporary H2/Postgres/MySQL/MariaDB database
+  2. Run all migrations up to a certain point
+  3. Load some arbitrary data
+  4. run migration(s) after that point (verify that they actually run)
+  5. verify that data looks like what we'd expect after running migration(s)
+
+  Actual tests using this code live in `metabase.db.schema-migrations-test`."
+  (:require [clojure.java.jdbc :as jdbc]
+            [clojure.test :refer :all]
+            [clojure.tools.logging :as log]
+            [metabase
+             [db :as mdb]
+             [driver :as driver]
+             [test :as mt]
+             [util :as u]]
+            [metabase.db.liquibase :as liquibase]
+            [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
+            [metabase.test.data.interface :as tx]
+            [metabase.test.initialize :as initialize]
+            [toucan.db :as db])
+  (:import [liquibase Contexts Liquibase]
+           [liquibase.changelog ChangeSet DatabaseChangeLog]))
+
+(defmulti ^:private do-with-temp-empty-app-db*
+  "Create a new completely empty app DB for `driver`, then call `(f jdbc-spec)` with a spec for that DB. Should clean up
+  before and after running `f` as needed."
+  {:arglists '([driver f])}
+  driver/dispatch-on-initialized-driver
+  :hierarchy #'driver/hierarchy)
+
+(defmethod do-with-temp-empty-app-db* :default
+  [driver f]
+  (log/debugf "Creating empty %s app db..." driver)
+  (let [dbdef {:database-name     "schema-migrations-test-db"
+               :table-definitions []}]
+    (try
+      (tx/create-db! driver dbdef)
+      (let [connection-details (tx/dbdef->connection-details driver :db dbdef)
+            jdbc-spec          (sql-jdbc.conn/connection-details->spec driver connection-details)]
+        (f jdbc-spec))
+      (finally
+        (log/debugf "Destroying empty %s app db..." driver)
+        (tx/destroy-db! driver dbdef)))))
+
+(defmethod do-with-temp-empty-app-db* :h2
+  [driver f]
+  (log/debug "Creating empty H2 app db...")
+  ;; we don't need to destroy this DB manually because it will just get shutdown immediately when the Connection
+  ;; closes because we're not setting a `DB_CLOSE_DELAY`
+  ;;
+  ;; don't use the usual implementation of `tx/dbdef->connection-details` because it creates a spec that only connects
+  ;; to with `USER=GUEST` which doesn't let us run DDL statements
+  (let [connection-details {:db "mem:schema-migrations-test-db"}
+        jdbc-spec          (binding [mdb/*allow-potentailly-unsafe-connections* true]
+                             (sql-jdbc.conn/connection-details->spec driver connection-details))]
+    (f jdbc-spec)))
+
+(defn- do-with-temp-empty-app-db [driver f]
+  (do-with-temp-empty-app-db*
+   driver
+   (fn [jdbc-spec]
+     (with-open [conn (jdbc/get-connection jdbc-spec)]
+       (binding [toucan.db/*db-connection* {:connection conn}
+                 toucan.db/*quoting-style* (mdb/quoting-style driver)]
+         (f conn))))))
+
+(defmacro ^:private with-temp-empty-app-db
+  "Create a new temporary application DB of `db-type` and execute `body` with `conn-binding` bound to a
+  `java.sql.Connection` to the database. Toucan `*db-connection*` is also bound, which means Toucan functions like
+  `select` or `update!` will operate against this database."
+  [[conn-binding db-type] & body]
+  `(do-with-temp-empty-app-db ~db-type (fn [~(vary-meta conn-binding assoc :tag 'java.sql.Connection)] ~@body)))
+
+(defn- run-migrations-in-range!
+  "Run Liquibase migrations from our migrations YAML file in the range of `start-id` -> `end-id` (inclusive) against a
+  DB with `jdbc-spec`."
+  [^java.sql.Connection conn [start-id end-id]]
+  (liquibase/with-liquibase [liquibase conn]
+    (let [change-log        (.getDatabaseChangeLog liquibase)
+          ;; create a new change log that only has the subset of migrations we want to run.
+          subset-change-log (doto (DatabaseChangeLog.)
+                              ;; we don't actually use this for anything but if we don't set it then Liquibase barfs
+                              (.setPhysicalFilePath (.getPhysicalFilePath change-log)))]
+      ;; add the relevant migrations (change sets) to our subset change log
+      (doseq [^ChangeSet change-set (.getChangeSets change-log)
+              :let                  [id (Integer/parseUnsignedInt (.getId change-set))]
+              :when                 (<= start-id id end-id)]
+        (.addChangeSet subset-change-log change-set))
+      ;; now create a new instance of Liquibase that will run just the subset change log
+      (let [subset-liquibase (Liquibase. subset-change-log (.getResourceAccessor liquibase) (.getDatabase liquibase))]
+        (when-let [unrun (not-empty (.listUnrunChangeSets subset-liquibase nil))]
+          (log/debugf "Running migrations %s...%s (inclusive)"
+                      (.getId ^ChangeSet (first unrun)) (.getId ^ChangeSet (last unrun))))
+        ;; run the migrations
+        (.update subset-liquibase (Contexts.))))))
+
+(defn- test-migrations-for-driver [driver [start-id end-id] f]
+  (log/debug (u/format-color 'yellow "Testing migrations for driver %s..." driver))
+  (with-temp-empty-app-db [conn driver]
+    ;; sanity check: make sure the DB is actually empty
+    (let [metadata (.getMetaData conn)]
+      (with-open [rs (.getTables metadata nil nil "%" (into-array String ["TABLE"]))]
+        (let [tables (jdbc/result-set-seq rs)]
+          (assert (zero? (count tables))
+                  (str "'Empty' application DB is not actually empty. Found tables:\n"
+                       (u/pprint-to-str tables))))))
+    (run-migrations-in-range! conn [1 (dec start-id)])
+    (f #(run-migrations-in-range! conn [start-id end-id])))
+  (log/debug (u/format-color 'green "Done testing migrations for driver %s." driver)))
+
+(defn test-migrations*
+  [migration-range f]
+  ;; make sure the normal Metabase application DB is set up before running the tests so things don't get confused and
+  ;; try to initialize it while the mock DB is bound
+  (initialize/initialize-if-needed! :db)
+  (let [[start-id end-id] (if (sequential? migration-range)
+                            migration-range
+                            [migration-range migration-range])]
+    (testing (format "Migrations %d-%d" start-id end-id)
+      (mt/test-drivers #{:h2 :mysql :postgres}
+        (test-migrations-for-driver driver/*driver* [start-id end-id] f)))))
+
+(defmacro test-migrations
+  "Util macro for running tests for a set of Liquibase schema migration(s).
+
+  Before invoking body, migrations up to `start-id` are automatically ran. In body, you should do the following in
+  this order:
+
+  1. Load data and check any preconditions before running migrations you're testing. Prefer `toucan.db/simple-insert!`
+     or plain SQL for loading data to avoid dependencies on the current state of the schema that may be present in
+     Toucan `pre-insert` functions and the like.
+
+  2. Call `(migrate!)` to run migrations in range of `start-id` -> `end-id` (inclusive)
+
+  3. Check any postconditions after running the migrations.
+
+  e.g.
+
+    ;; example test for migrations 100-105
+    (test-migrations [100 105] [migrate!]
+      ;; (Migrations 1-99 are ran automatically before body is invoked)
+      ;; 1. Load data
+      (create-some-users!)
+      ;; 2. Run migrations 100-105
+      (migrate!)
+      ;; 3. Do some test assertions
+      (is (= ...)))
+
+  For convenience `migration-range` can be either a range of migrations IDs to test (e.g. `[100 105]`) or just a
+  single migration ID (e.g. `100`).
+
+  These run against the current set of test `DRIVERS` (by default H2), so if you want to run against more than H2
+  either set the `DRIVERS` env var or use `mt/set-test-drivers!` from the REPL."
+  {:style/indent 2}
+  [migration-range [migrate!-binding] & body]
+  `(test-migrations*
+    ~migration-range
+    (fn [~migrate!-binding]
+      ~@body)))
diff --git a/test/metabase/test/data/h2.clj b/test/metabase/test/data/h2.clj
index 07d0bac1a8c80e86c3f0c74c29837dc87fe8ff1f..3c93c425392c0f2b969727156cfd9cc6eeb382ea 100644
--- a/test/metabase/test/data/h2.clj
+++ b/test/metabase/test/data/h2.clj
@@ -48,9 +48,10 @@
 
 (defmethod tx/dbdef->connection-details :h2
   [_ context dbdef]
-  {:db (str "mem:" (tx/escaped-name dbdef) (when (= context :db)
-                                             ;; Return details with the GUEST user added so SQL queries are allowed.
-                                             ";USER=GUEST;PASSWORD=guest"))})
+  {:db (str "mem:" (tx/escaped-database-name dbdef) (when (= context :db)
+                                                      ;; Return details with the GUEST user added so SQL queries are
+                                                      ;; allowed.
+                                                      ";USER=GUEST;PASSWORD=guest"))})
 
 (defmethod sql.tx/pk-sql-type :h2 [_] "BIGINT AUTO_INCREMENT")
 
@@ -58,7 +59,8 @@
 
 (defmethod sql.tx/drop-db-if-exists-sql :h2 [& _] nil)
 
-(defmethod sql.tx/create-db-sql :h2 [& _]
+(defmethod sql.tx/create-db-sql :h2
+  [& _]
   (str
    ;; We don't need to actually do anything to create a database here. Just disable the undo
    ;; log (i.e., transactions) for this DB session because the bulk operations to load data don't need to be atomic
diff --git a/test/metabase/test/data/impl.clj b/test/metabase/test/data/impl.clj
index 46383a1b63d4a02514d030e5a227edbf28870810..0158b829c66bc8873d76d231881963139aa0fa0f 100644
--- a/test/metabase/test/data/impl.clj
+++ b/test/metabase/test/data/impl.clj
@@ -93,10 +93,11 @@
       (tu.tz/with-system-timezone-id "UTC"
         (tx/create-db! driver database-definition)))
     ;; Add DB object to Metabase DB
-    (let [db (db/insert! Database
-               :name    database-name
-               :engine  (name driver)
-               :details (tx/dbdef->connection-details driver :db database-definition))]
+    (let [connection-details (tx/dbdef->connection-details driver :db database-definition)
+          db                 (db/insert! Database
+                               :name    database-name
+                               :engine  (name driver)
+                               :details connection-details)]
       (try
         ;; sync newly added DB
         (u/with-timeout sync-timeout-ms
@@ -112,16 +113,23 @@
         (Database (u/get-id db))
         (catch Throwable e
           (db/delete! Database :id (u/get-id db))
-          (throw e))))
+          (throw (ex-info "Failed to create test database"
+                          {:driver             driver
+                           :database-name      database-name
+                           :connection-details connection-details}
+                          e)))))
     (catch Throwable e
-      (printf "Failed to create %s '%s' test database:\n" driver database-name)
-      (println e)
-      (if config/is-test?
-        (System/exit -1)
-        (do
-          (println (u/format-color 'red "create-database! failed; destroying %s database %s" driver (pr-str database-name)))
-          (tx/destroy-db! driver database-definition)
-          (throw e))))))
+      (let [message (format "Failed to create %s '%s' test database" driver database-name)]
+        (println message "\n" e)
+        (if config/is-test?
+          (System/exit -1)
+          (do
+            (println (u/format-color 'red "create-database! failed; destroying %s database %s" driver (pr-str database-name)))
+            (tx/destroy-db! driver database-definition)
+            (throw (ex-info message
+                            {:driver        driver
+                             :database-name database-name}
+                            e))))))))
 
 (defmethod get-or-create-database! :default
   [driver dbdef]
diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj
index 6e37eea80e9163bb4c9e198d5a8ae4ae1434ab19..a41b8eabed3cff1ef9f7623f8d870e660480ae9c 100644
--- a/test/metabase/test/data/interface.clj
+++ b/test/metabase/test/data/interface.clj
@@ -170,7 +170,7 @@
   []
   (the-driver-with-test-extensions (or driver/*driver* :h2)))
 
-(defn escaped-name
+(defn escaped-database-name
   "Return escaped version of database name suitable for use as a filename / database name / etc."
   ^String [^DatabaseDefinition {:keys [database-name]}]
   {:pre [(string? database-name)]}
@@ -261,13 +261,18 @@
 
 (defmulti create-db!
   "Create a new database from `database-definition`, including adding tables, fields, and foreign key constraints,
-  and add the appropriate data. This method should drop existing databases with the same name if applicable, unless
-  the skip-drop-db? arg is true. This is to workaround a scenario where the postgres driver terminates the connection
-  before dropping the DB and causes some tests to fail. (This refers to creating the actual *DBMS* database itself,
-  *not* a Metabase `Database` object.)
+  and load the appropriate data. (This refers to creating the actual *DBMS* database itself, *not* a Metabase
+  `Database` object.)
 
   Optional `options` as third param. Currently supported options include `skip-drop-db?`. If unspecified,
-  `skip-drop-db?` should default to `false`."
+  `skip-drop-db?` should default to `false`.
+
+  This method should drop existing databases with the same name if applicable, unless the `skip-drop-db?` arg is
+  truthy. This is to work around a scenario where the Postgres driver terminates the connection before dropping the DB
+  and causes some tests to fail.
+
+  This method is not expected to return anything; use `dbdef->connection-details` to get connection details for this
+  database after you create it."
   {:arglists '([driver database-definition & {:keys [skip-drop-db?]}])}
   dispatch-on-driver-with-test-extensions
   :hierarchy #'driver/hierarchy)
diff --git a/test/metabase/test/data/postgres.clj b/test/metabase/test/data/postgres.clj
index 5e923c9c4bbf2e0ba6fc7a094b5595b0573dd8b1..2737057f5c155dbb141dcb044e9d441a9b0fd185 100644
--- a/test/metabase/test/data/postgres.clj
+++ b/test/metabase/test/data/postgres.clj
@@ -66,9 +66,10 @@
 
 (defmethod ddl/drop-db-ddl-statements :postgres
   [driver {:keys [database-name], :as dbdef} & options]
-  (assert (string? database-name)
-          (format "Expected String database name; got ^%s %s"
-                  (some-> database-name class .getCanonicalName) (pr-str database-name)))
+  (when-not (string? database-name)
+    (throw (ex-info (format "Expected String database name; got ^%s %s"
+                            (some-> database-name class .getCanonicalName) (pr-str database-name))
+                    {:driver driver, :dbdef dbdef})))
   ;; add an additional statement to the front to kill open connections to the DB before dropping
   (cons
    (kill-connections-to-db-sql database-name)
diff --git a/test/metabase/test/data/sql.clj b/test/metabase/test/data/sql.clj
index 4c4045c23e3710cdb6c03b9da7cddb4e2c019252..fae7f0874aa307f6870bba71eb20e933339af4a6 100644
--- a/test/metabase/test/data/sql.clj
+++ b/test/metabase/test/data/sql.clj
@@ -173,7 +173,6 @@
   tx/dispatch-on-driver-with-test-extensions
   :hierarchy #'driver/hierarchy)
 
-
 (defmulti create-db-sql
   "Return a `CREATE DATABASE` statement."
   {:arglists '([driver dbdef])}
@@ -183,7 +182,6 @@
 (defmethod create-db-sql :sql/test-extensions [driver {:keys [database-name]}]
   (format "CREATE DATABASE %s;" (qualify-and-quote driver database-name)))
 
-
 (defmulti drop-db-if-exists-sql
   "Return a `DROP DATABASE` statement."
   {:arglists '([driver dbdef])}
@@ -193,7 +191,6 @@
 (defmethod drop-db-if-exists-sql :sql/test-extensions [driver {:keys [database-name]}]
   (format "DROP DATABASE IF EXISTS %s;" (qualify-and-quote driver database-name)))
 
-
 (defmulti create-table-sql
   "Return a `CREATE TABLE` statement."
   {:arglists '([driver dbdef tabledef])}
diff --git a/test/metabase/test/data/sql/ddl.clj b/test/metabase/test/data/sql/ddl.clj
index e2219234a8ae48ecba719e1dbd9d98c04c44bcb6..307f610c4dadd914f88b77ce0434574d516f8634 100644
--- a/test/metabase/test/data/sql/ddl.clj
+++ b/test/metabase/test/data/sql/ddl.clj
@@ -23,18 +23,26 @@
 (defmethod drop-db-ddl-statements :sql/test-extensions
   [driver dbdef & {:keys [skip-drop-db?]}]
   (when-not skip-drop-db?
-    ;; Exec SQL for creating the DB
-    [(sql.tx/drop-db-if-exists-sql driver dbdef)
-     (sql.tx/create-db-sql driver dbdef)]))
+    (try
+      [(sql.tx/drop-db-if-exists-sql driver dbdef)]
+      (catch Throwable e
+        (throw (ex-info "Error generating DDL statements for dropping database"
+                        {:driver driver}
+                        e))))))
 
+(defn create-db-ddl-statements
+  "DDL statements to create the DB itself (does not include statements to drop the DB if it already exists)."
+  [driver dbdef]
+  [(sql.tx/create-db-sql driver dbdef)])
 
-(defmulti create-db-ddl-statements
-  "Return a default sequence of DDL statements for creating a DB (not including dropping it)."
+(defmulti create-db-tables-ddl-statements
+  "Return a default sequence of DDL statements for creating the tables/columns/etc. inside a Database. DOES NOT INCLUDE
+  STATEMENTS FOR CREATING (OR DROPPING) THE DATABASE ITSELF."
   {:arglists '([driver dbdef & {:as options}])}
   tx/dispatch-on-driver-with-test-extensions
   :hierarchy #'driver/hierarchy)
 
-(defmethod create-db-ddl-statements :sql/test-extensions
+(defmethod create-db-tables-ddl-statements :sql/test-extensions
   [driver {:keys [table-definitions], :as dbdef} & _]
   ;; Build combined statement for creating tables + FKs + comments
   (let [statements (atom [])
@@ -61,7 +69,6 @@
           (add! (sql.tx/standalone-column-comment-sql driver dbdef tabledef fielddef)))))
     @statements))
 
-
 ;; The methods below are currently only used by `:sql-jdbc` drivers, but you can use them to help implement your
 ;; `:sql` driver test extensions as well because there's nothing JDBC specific about them
 
diff --git a/test/metabase/test/data/sql_jdbc/execute.clj b/test/metabase/test/data/sql_jdbc/execute.clj
index 5fd4b9efd51f43f15953f0b7da18c1a71a34b4ae..eaf5165dad0dec5957a449290eea507e0ce984cc 100644
--- a/test/metabase/test/data/sql_jdbc/execute.clj
+++ b/test/metabase/test/data/sql_jdbc/execute.clj
@@ -12,7 +12,7 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defn- jdbc-execute! [db-spec sql]
-  (log/tracef "[execute] %s" (pr-str sql))
+  (log/tracef "[execute %s] %s" driver/*driver* (pr-str sql))
   (jdbc/execute! db-spec [sql] {:transaction? false, :multi? true}))
 
 (defn default-execute-sql! [driver context dbdef sql & {:keys [execute!]
diff --git a/test/metabase/test/data/sql_jdbc/load_data.clj b/test/metabase/test/data/sql_jdbc/load_data.clj
index c289ba20f496bfa88b5147cc65ace4f3a04609ba..a3eb6ef6ebff570b57e9ff2dd913363d71c254b8 100644
--- a/test/metabase/test/data/sql_jdbc/load_data.clj
+++ b/test/metabase/test/data/sql_jdbc/load_data.clj
@@ -198,12 +198,16 @@
   "Default implementation of `create-db!` for SQL drivers."
   {:arglists '([driver dbdef & {:keys [skip-drop-db?]}])}
   [driver {:keys [table-definitions], :as dbdef} & options]
-  ;; first execute statements to drop/create the DB if needed (this will return nothing is `skip-drop-db?` is true)
+  ;; first execute statements to drop the DB if needed (this will do nothing if `skip-drop-db?` is true)
   (doseq [statement (apply ddl/drop-db-ddl-statements driver dbdef options)]
     (execute/execute-sql! driver :server dbdef statement))
+  ;; now execute statements to create the DB
+  (doseq [statement (ddl/create-db-ddl-statements driver dbdef)]
+    (execute/execute-sql! driver :server dbdef statement))
   ;; next, get a set of statements for creating the DB & Tables
-  (let [statements (apply ddl/create-db-ddl-statements driver dbdef options)]
-    ;; exec the combined statement
+  (let [statements (apply ddl/create-db-tables-ddl-statements driver dbdef options)]
+    ;; exec the combined statement. Notice we're now executing in the `:db` context e.g. executing them for a specific
+    ;; DB rather than on `:server` (no DB in particular)
     (execute/execute-sql! driver :db dbdef (str/join ";\n" statements)))
   ;; Now load the data for each Table
   (doseq [tabledef table-definitions]
@@ -213,5 +217,10 @@
 (defn destroy-db!
   "Default impl of `destroy-db!` for SQL drivers."
   [driver dbdef]
-  (doseq [statement (apply ddl/drop-db-ddl-statements driver dbdef)]
-    (execute/execute-sql! driver :server dbdef statement)))
+  (try
+    (doseq [statement (ddl/drop-db-ddl-statements driver dbdef)]
+      (execute/execute-sql! driver :server dbdef statement))
+    (catch Throwable e
+      (throw (ex-info "Error destroying database"
+                      {:driver driver, :dbdef dbdef}
+                      e)))))