diff --git a/modules/drivers/druid/test/metabase/test/data/druid.clj b/modules/drivers/druid/test/metabase/test/data/druid.clj
index 94b7bfd2397f7598b5954d9f85ea9d6c07210a1d..177b269d2b4caa22bf5e1c5a7728470aaf02852e 100644
--- a/modules/drivers/druid/test/metabase/test/data/druid.clj
+++ b/modules/drivers/druid/test/metabase/test/data/druid.clj
@@ -30,13 +30,14 @@
 ;;; Generating Data File
 
 (defn- flattened-test-data []
-  (let [dbdef    (tx/flatten-dbdef defs/test-data "checkins")
+  (let [dbdef    (tx/get-dataset-definition
+                  (tx/flattened-dataset-definition defs/test-data "checkins"))
         tabledef (first (:table-definitions dbdef))]
     (->> (:rows tabledef)
          (map (partial zipmap (map :field-name (:field-definitions tabledef))))
          (map-indexed (fn [i row]
                         (assoc row :id (inc i))))
-         (sort-by (u/rpartial get "date")))))
+         (sort-by #(get % "date")))))
 
 (defn- write-dbdef-to-json [db-def filename]
   (io/delete-file filename :silently)
diff --git a/modules/drivers/mongo/test/metabase/driver/mongo_test.clj b/modules/drivers/mongo/test/metabase/driver/mongo_test.clj
index 1400797dc0c75a4cf77f83192d0a868dea20fb5f..20f8efc1d666dc477f9800d55879c6e06c6c1773 100644
--- a/modules/drivers/mongo/test/metabase/driver/mongo_test.clj
+++ b/modules/drivers/mongo/test/metabase/driver/mongo_test.clj
@@ -126,12 +126,12 @@
   (driver/describe-table :mongo (data/db) (Table (data/id :venues))))
 
 ;; Make sure that all-NULL columns work and are synced correctly (#6875)
-(tx/def-database-definition ^:private all-null-columns
+(tx/defdataset ^:private all-null-columns
   [["bird_species"
-     [{:field-name "name", :base-type :type/Text}
-      {:field-name "favorite_snack", :base-type :type/Text}]
-     [["House Finch" nil]
-      ["Mourning Dove" nil]]]])
+    [{:field-name "name", :base-type :type/Text}
+     {:field-name "favorite_snack", :base-type :type/Text}]
+    [["House Finch" nil]
+     ["Mourning Dove" nil]]]])
 
 (datasets/expect-with-driver :mongo
   [{:name "_id",            :database_type "java.lang.Long",   :base_type :type/Integer, :special_type :type/PK}
@@ -198,7 +198,7 @@
 
 
 ;;; Check that we support Mongo BSON ID and can filter by it (#1367)
-(tx/def-database-definition ^:private with-bson-ids
+(tx/defdataset ^:private with-bson-ids
   [["birds"
      [{:field-name "name", :base-type :type/Text}
       {:field-name "bird_id", :base-type :type/MongoBSONID}]
diff --git a/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj b/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj
index 9a333cdeb1530cd6229ba85d1125bde8f6265b18..909024229f5f4da3b0735cfad8aff1aee3f55fdf 100644
--- a/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj
+++ b/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj
@@ -16,16 +16,16 @@
              [util :as tu :refer [obj->json->obj]]]
             [metabase.test.data
              [datasets :as datasets]
-             [interface :as tx :refer [def-database-definition]]]))
+             [interface :as tx]]))
 
 ;;; -------------------------------------------------- VARCHAR(MAX) --------------------------------------------------
 
 ;; Make sure something long doesn't come back as some weird type like `ClobImpl`
-(def ^:private ^:const a-gene
+(def ^:private a-gene
   "Really long string representing a gene like \"GGAGCACCTCCACAAGTGCAGGCTATCCTGTCGAGTAAGGCCT...\""
   (apply str (repeatedly 1000 (partial rand-nth [\A \G \C \T]))))
 
-(def-database-definition ^:private ^:const genetic-data
+(tx/defdataset ^:private genetic-data
   [["genetic-data"
      [{:field-name "gene", :base-type {:native "VARCHAR(MAX)"}}]
      [[a-gene]]]])
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index b6de5e05e4623a7c2cea13e61c251c7298e42797..d820205527b91d1fea656d640957cfb29644a759 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -466,7 +466,7 @@
       (u/with-us-locale
         (verify-db-connection db-details)
         (run-schema-migrations! auto-migrate db-details)
-        (create-connection-pool! (du/profile (jdbc-details db-details)))
+        (create-connection-pool! (jdbc-details db-details))
         (run-data-migrations!)
         (reset! db-setup-finished? true))))
   nil)
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index 0a066fe87c80782bbc557423a88eedda53164cc8..a33eb1f99113ad4c094ce2fdb33546708cc302b3 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -233,9 +233,11 @@
 (def ^:private default-pipeline (qp-pipeline execute-query))
 
 (def ^:private QueryResponse
-  (s/cond-pre
-   ManyToManyChannel
-   {:status (s/enum :completed :failed :canceled), s/Any s/Any}))
+  (s/named
+   (s/cond-pre
+    ManyToManyChannel
+    {:status (s/enum :completed :failed :canceled), s/Any s/Any})
+   "Valid query response (core.async channel or map with :status)"))
 
 (s/defn process-query :- QueryResponse
   "Process an MBQL query. This is the main entrypoint to the magical realm of the Query Processor. Returns a
diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj
index 0864353fddc25fb7b6d5f0d7b2bb9cf49fda3f49..70c44398441938a634acc59b07bfb40d551c65c8 100644
--- a/test/metabase/api/dataset_test.clj
+++ b/test/metabase/api/dataset_test.clj
@@ -203,7 +203,7 @@
    ["3" "2014-09-15" "" "8" "56"]
    ["4" "2014-03-11" "" "5" "4"]
    ["5" "2013-05-05" "" "3" "49"]]
-  (data/with-db (data/get-or-create-database! defs/test-data-with-null-date-checkins)
+  (data/dataset defs/test-data-with-null-date-checkins
     (let [result ((test-users/user->client :rasta) :post 200 "dataset/csv" :query
                   (json/generate-string (data/mbql-query checkins)))]
       (take 5 (parse-and-sort-csv result)))))
diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj
index f898aea91f8f4491aea929778067ab0ebd3679da..7726c73d257692682408909472fd48c99d9d96a1 100644
--- a/test/metabase/api/table_test.clj
+++ b/test/metabase/api/table_test.clj
@@ -23,7 +23,6 @@
              [data :as data]
              [util :as tu :refer [match-$]]]
             [metabase.test.data
-             [dataset-definitions :as defs]
              [datasets :as datasets]
              [users :refer [user->client]]]
             [metabase.test.mock.util :as mutil]
@@ -205,24 +204,6 @@
             :fields_hash  $}))
   ((user->client :rasta) :get 200 (format "table/%d/query_metadata" (data/id :categories))))
 
-
-(def ^:private user-last-login-date-strs
-  "In an effort to be really annoying, the date strings returned by the API are different on Circle than they are
-   locally. Generate strings like '2014-01-01' at runtime so we get matching values."
-  (let [format-inst (fn [^java.util.Date inst]
-                      (format "%d-%02d-%02d"
-                              (+ (.getYear inst) 1900)
-                              (+ (.getMonth inst) 1)
-                              (.getDate inst)))]
-    (->> (defs/field-values defs/test-data-map "users" "last_login")
-         (map format-inst)
-         set
-         sort
-         vec)))
-
-(def ^:private user-full-names
-  (defs/field-values defs/test-data-map "users" "name"))
-
 ;;; GET api/table/:id/query_metadata?include_sensitive_fields
 ;; Make sure that getting the User table *does* include info about the password field, but not actual values
 ;; themselves
diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj
index 4c51b3c6aba44d14f89775a17f565f2bdfe68b9b..4c6a3132cd2dbedd23503d17d17a35e92183c6ce 100644
--- a/test/metabase/driver/mysql_test.clj
+++ b/test/metabase/driver/mysql_test.clj
@@ -17,7 +17,7 @@
              [util :as tu]]
             [metabase.test.data
              [datasets :refer [expect-with-driver]]
-             [interface :as tx :refer [def-database-definition]]]
+             [interface :as tx]]
             [metabase.test.util.timezone :as tu.tz]
             [metabase.util.date :as du]
             [toucan.db :as db]
@@ -54,7 +54,7 @@
 ;; Test how TINYINT(1) columns are interpreted. By default, they should be interpreted as integers, but with the
 ;; correct additional options, we should be able to change that -- see
 ;; https://github.com/metabase/metabase/issues/3506
-(def-database-definition ^:private ^:const tiny-int-ones
+(tx/defdataset ^:private tiny-int-ones
   [["number-of-cans"
      [{:field-name "thing",          :base-type :type/Text}
       {:field-name "number-of-cans", :base-type {:native "tinyint(1)"}}]
@@ -71,7 +71,7 @@
   #{{:name "number-of-cans", :base_type :type/Boolean, :special_type :type/Category}
     {:name "id",             :base_type :type/Integer, :special_type :type/PK}
     {:name "thing",          :base_type :type/Text,    :special_type :type/Category}}
-  (data/with-temp-db [db tiny-int-ones]
+  (data/with-db-for-dataset [db tiny-int-ones]
     (db->fields db)))
 
 ;; if someone says specifies `tinyInt1isBit=false`, it should come back as a number instead
@@ -79,7 +79,7 @@
   #{{:name "number-of-cans", :base_type :type/Integer, :special_type :type/Quantity}
     {:name "id",             :base_type :type/Integer, :special_type :type/PK}
     {:name "thing",          :base_type :type/Text,    :special_type :type/Category}}
-  (data/with-temp-db [db tiny-int-ones]
+  (data/with-db-for-dataset [db tiny-int-ones]
     (tt/with-temp Database [db {:engine "mysql"
                                 :details (assoc (:details db)
                                            :additional-options "tinyInt1isBit=false")}]
diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj
index 138666efa1f2c637ffda14e75403d0832eccc9b0..00a617d8029335ee984140b119723dff4e74d156 100644
--- a/test/metabase/driver/postgres_test.clj
+++ b/test/metabase/driver/postgres_test.clj
@@ -64,9 +64,9 @@
 ;; Verify that we identify JSON columns and mark metadata properly during sync
 (datasets/expect-with-driver :postgres
   :type/SerializedJSON
-  (data/with-temp-db
+  (data/with-db-for-dataset
     [_
-     (tx/create-database-definition "Postgres with a JSON Field"
+     (tx/dataset-definition "Postgres with a JSON Field"
        ["venues"
         [{:field-name "address", :base-type {:native "json"}}]
         [[(hsql/raw "to_json('{\"street\": \"431 Natoma\", \"city\": \"San Francisco\", \"state\": \"CA\", \"zip\": 94103}'::text)")]]])]
@@ -74,7 +74,7 @@
 
 
 ;;; # UUID Support
-(tx/def-database-definition ^:private with-uuid
+(tx/defdataset ^:private with-uuid
   [["users"
     [{:field-name "user_id", :base-type :type/UUID}]
     [[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027"]
@@ -125,7 +125,7 @@
 
 
 ;; Make sure that Tables / Fields with dots in their names get escaped properly
-(tx/def-database-definition ^:private dots-in-names
+(tx/defdataset ^:private dots-in-names
   [["objects.stuff"
     [{:field-name "dotted.name", :base-type :type/Text}]
     [["toucan_cage"]
@@ -143,7 +143,7 @@
 
 
 ;; Make sure that duplicate column names (e.g. caused by using a FK) still return both columns
-(tx/def-database-definition ^:private duplicate-names
+(tx/defdataset ^:private duplicate-names
   [["birds"
     [{:field-name "name", :base-type :type/Text}]
     [["Rasta"]
@@ -163,7 +163,7 @@
 
 
 ;;; Check support for `inet` columns
-(tx/def-database-definition ^:private ip-addresses
+(tx/defdataset ^:private ip-addresses
   [["addresses"
     [{:field-name "ip", :base-type {:native "inet"}}]
     [[(hsql/raw "'192.168.1.1'::inet")]
diff --git a/test/metabase/query_processor/async_test.clj b/test/metabase/query_processor/async_test.clj
index 85247ea865b145d5cec6f7fd327e4c7cf1be1f8e..4c483ba6bdc1aaa33fa61b86b4613046432b5ce1 100644
--- a/test/metabase/query_processor/async_test.clj
+++ b/test/metabase/query_processor/async_test.clj
@@ -9,20 +9,21 @@
 
 ;; running a query async should give you the same results as running that query synchronously
 (let [query
-      {:database (data/id)
-       :type     :query
-       :query    {:source-table (data/id :venues)
-                  :fields       [[:field-id (data/id :venues :name)]]
-                  :limit        5}}
+      (delay
+       {:database (data/id)
+        :type     :query
+        :query    {:source-table (data/id :venues)
+                   :fields       [[:field-id (data/id :venues :name)]]
+                   :limit        5}})
       ;; Metadata checksum might be encrypted if a encryption key is set on this system (to make it hard for bad
       ;; actors to forge one) in which case the checksums won't be equal.
       maybe-decrypt-checksum
       #(some-> % (update-in [:data :results_metadata :checksum] encrypt/maybe-decrypt))]
   (expect
     (maybe-decrypt-checksum
-     (qp/process-query query))
+     (qp/process-query @query))
     (maybe-decrypt-checksum
-     (tu.async/with-open-channels [result-chan (qp.async/process-query query)]
+     (tu.async/with-open-channels [result-chan (qp.async/process-query @query)]
        (first (a/alts!! [result-chan (a/timeout 1000)]))))))
 
 (expect
diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj
index 68e5dee50a7295dde22d22c0e2b687c92c3c43a4..1eba17927a4e18f44b9546e0be85dbd5e634e823 100644
--- a/test/metabase/query_processor_test.clj
+++ b/test/metabase/query_processor_test.clj
@@ -335,7 +335,8 @@
   [results]
   (when (= (:status results) :failed)
     (println "Error running query:" (u/pprint-to-str 'red results))
-    (throw (ex-info (:error results) results)))
+    (throw (ex-info (str (or (:error results) "Error running query"))
+             (if (map? results) results {:results results}))))
   (:data results))
 
 (defn rows
diff --git a/test/metabase/query_processor_test/breakout_test.clj b/test/metabase/query_processor_test/breakout_test.clj
index d0f270ec2e46a45a88b5d5154fc049b43130bb95..0cf331a834d9dda5dce99c6e34e7ea4c5e3e7623 100644
--- a/test/metabase/query_processor_test/breakout_test.clj
+++ b/test/metabase/query_processor_test/breakout_test.clj
@@ -14,9 +14,7 @@
             [metabase.test
              [data :as data]
              [util :as tu]]
-            [metabase.test.data
-             [dataset-definitions :as defs]
-             [datasets :as datasets]]
+            [metabase.test.data.datasets :as datasets]
             [metabase.test.util.log :as tu.log]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
@@ -107,7 +105,7 @@
    :native_form true}
   (data/with-data
     (fn []
-      (let [venue-names (defs/field-values defs/test-data-map "categories" "name")]
+      (let [venue-names (data/dataset-field-values "categories" "name")]
         [(db/insert! Dimension {:field_id (data/id :venues :category_id)
                                 :name     "Foo"
                                 :type     :internal})
diff --git a/test/metabase/query_processor_test/date_bucketing_test.clj b/test/metabase/query_processor_test/date_bucketing_test.clj
index a9f54e23742e4a639587f3f0914ac88b57e38a13..b6089c640b9c54383691bf906bd176aea751fc37 100644
--- a/test/metabase/query_processor_test/date_bucketing_test.clj
+++ b/test/metabase/query_processor_test/date_bucketing_test.clj
@@ -19,7 +19,7 @@
              [format :as tformat]]
             [metabase
              [driver :as driver]
-             [query-processor-test :refer :all]
+             [query-processor-test :as qp.test :refer :all]
              [util :as u]]
             [metabase.test
              [data :as data]
@@ -30,7 +30,7 @@
              [interface :as tx]]
             [metabase.test.util.timezone :as tu.tz]
             [metabase.util.date :as du])
-  (:import org.joda.time.DateTime))
+  (:import [org.joda.time DateTime DateTimeZone]))
 
 (defn- ->long-if-number [x]
   (if (number? x)
@@ -54,8 +54,8 @@
             {:aggregation [[:count]]
              :breakout    [[:datetime-field $timestamp unit]]
              :limit       10}))
-        rows (format-rows-by [->long-if-number int])))
-  ([unit tz]
+        rows (qp.test/format-rows-by [->long-if-number int])))
+  ([unit, ^DateTimeZone tz]
    (tu/with-temporary-setting-values [report-timezone (.getID tz)]
      (sad-toucan-incidents-with-bucketing unit))))
 
@@ -124,7 +124,7 @@
         (repeat 1)))
 
 ;; Bucket sad toucan events by their default bucketing, which is the full datetime value
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     ;; Timezone is omitted by these databases
     (= :sqlite driver/*driver*)
@@ -145,7 +145,7 @@
   (sad-toucan-incidents-with-bucketing :default pacific-tz))
 
 ;; Buckets sad toucan events like above, but uses the eastern timezone as the report timezone
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     ;; These databases are always in UTC so aren't impacted by changes in report-timezone
     (= :sqlite driver/*driver*)
@@ -170,7 +170,7 @@
 ;;
 ;; The exclusions here are databases that give incorrect answers when the JVM timezone doesn't match the databases
 ;; timezone
-(expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
+(qp.test/expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
   (cond
     (= :sqlite driver/*driver*)
     (sad-toucan-result (source-date-formatter utc-tz) result-date-formatter-without-tz)
@@ -195,7 +195,7 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 ;; This dataset doesn't have multiple events in a minute, the results are the same as the default grouping
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (sad-toucan-result (source-date-formatter utc-tz) result-date-formatter-without-tz)
@@ -211,7 +211,7 @@
   (sad-toucan-incidents-with-bucketing :minute pacific-tz))
 
 ;; Grouping by minute of hour is not affected by timezones
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[0 5]
    [1 4]
    [2 2]
@@ -258,7 +258,7 @@
 ;; formatting of the time for that given count is different depending
 ;; on whether the database supports a report timezone and what
 ;; timezone that database is in
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (results-by-hour (source-date-formatter utc-tz)
@@ -285,7 +285,7 @@
 ;; by 7 hours. These count changes can be validated by matching the
 ;; first three results of the pacific results to the last three of the
 ;; UTC results (i.e. pacific is 7 hours back of UTC at that time)
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (and (not (tz-shifted-engine-bug? driver/*driver*))
            (supports-report-timezone? driver/*driver*))
     [[0 8] [1 9] [2 7] [3 10] [4 10] [5 9] [6 6] [7 5] [8 7] [9 7]]
@@ -293,7 +293,7 @@
   (sad-toucan-incidents-with-bucketing :hour-of-day pacific-tz))
 
 ;; With all databases in UTC, the results should be the same for all DBs
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[0 13] [1 8] [2 4] [3 7] [4 5] [5 13] [6 10] [7 8] [8 9] [9 7]]
   (sad-toucan-incidents-with-bucketing :hour-of-day utc-tz))
 
@@ -305,7 +305,7 @@
 
 (defn- offset-time
   "Add to `date` offset from UTC found in `tz`"
-  [tz date]
+  [^DateTimeZone tz, ^DateTime date]
   (time/minus date
               (time/seconds
                (/ (.getOffset tz date) 1000))))
@@ -372,7 +372,7 @@
         (adjust-date source-formatter result-formatter sad-toucan-events-grouped-by-day)
         counts))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (= :sqlite driver/*driver*)
     (results-by-day date-formatter-without-time
                     date-formatter-without-time
@@ -383,7 +383,7 @@
 
   (sad-toucan-incidents-with-bucketing :day utc-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (results-by-day date-formatter-without-time
@@ -416,7 +416,7 @@
        ["01" "02" "03" "04" "05" "06" "07" "08" "09" "10"]))
 
 ;; Similar to the pacific test above, just validating eastern timezone shifts
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (results-by-day date-formatter-without-time
@@ -449,7 +449,7 @@
 ;;
 ;; The exclusions here are databases that give incorrect answers when the JVM timezone doesn't match the databases
 ;; timezone
-(expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
+(qp.test/expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
   (cond
     (= :sqlite driver/*driver*)
     (results-by-day date-formatter-without-time
@@ -480,14 +480,14 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (and (not (tz-shifted-engine-bug? driver/*driver*))
            (supports-report-timezone? driver/*driver*))
     [[1 29] [2 36] [3 33] [4 29] [5 13] [6 38] [7 22]]
     [[1 28] [2 38] [3 29] [4 27] [5 24] [6 30] [7 24]])
   (sad-toucan-incidents-with-bucketing :day-of-week pacific-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[1 28] [2 38] [3 29] [4 27] [5 24] [6 30] [7 24]]
   (sad-toucan-incidents-with-bucketing :day-of-week utc-tz))
 
@@ -497,14 +497,14 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (and (not (tz-shifted-engine-bug? driver/*driver*))
            (supports-report-timezone? driver/*driver*))
     [[1 8] [2 9] [3 9] [4 4] [5 11] [6 8] [7 6] [8 10] [9 6] [10 10]]
     [[1 6] [2 10] [3 4] [4 9] [5  9] [6 8] [7 8] [8  9] [9 7] [10  9]])
   (sad-toucan-incidents-with-bucketing :day-of-month pacific-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[1 6] [2 10] [3 4] [4 9] [5  9] [6 8] [7 8] [8  9] [9 7] [10  9]]
   (sad-toucan-incidents-with-bucketing :day-of-month utc-tz))
 
@@ -514,14 +514,14 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (and (not (tz-shifted-engine-bug? driver/*driver*))
            (supports-report-timezone? driver/*driver*))
     [[152 8] [153 9] [154 9] [155 4] [156 11] [157 8] [158 6] [159 10] [160 6] [161 10]]
     [[152 6] [153 10] [154 4] [155 9] [156  9] [157  8] [158 8] [159  9] [160 7] [161  9]])
   (sad-toucan-incidents-with-bucketing :day-of-year pacific-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[152 6] [153 10] [154 4] [155 9] [156  9] [157  8] [158 8] [159  9] [160 7] [161  9]]
   (sad-toucan-incidents-with-bucketing :day-of-year utc-tz))
 
@@ -543,7 +543,7 @@
                                                         "2015-06-28"])
         counts))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (if (= :sqlite driver/*driver*)
     (results-by-week date-formatter-without-time
                      date-formatter-without-time
@@ -578,7 +578,7 @@
 ;; Sad toucan incidents by week. Databases in UTC that don't support report timezones will be the same as the UTC test
 ;; above. Databases that support report timezone will have different counts as the week starts and ends 7 hours
 ;; earlier
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (results-by-week date-formatter-without-time
@@ -610,7 +610,7 @@
 
 ;; Tests eastern timezone grouping by week, UTC databases don't change, databases with reporting timezones need to
 ;; account for the 4-5 hour difference
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   (cond
     (= :sqlite driver/*driver*)
     (results-by-week date-formatter-without-time
@@ -639,7 +639,7 @@
 ;;
 ;; The exclusions here are databases that give incorrect answers when the JVM timezone doesn't match the databases
 ;; timezone
-(expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
+(qp.test/expect-with-non-timeseries-dbs-except #{:h2 :sqlserver :redshift :sparksql :mongo}
   (cond
     (= :sqlite driver/*driver*)
     (results-by-week date-formatter-without-time
@@ -669,7 +669,7 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   ;; Not really sure why different drivers have different opinions on these </3
   (cond
     (= :snowflake driver/*driver*)
@@ -694,7 +694,7 @@
 
 ;; All of the sad toucan events in the test data fit in June. The results are the same on all databases and the only
 ;; difference is how the beginning of hte month is represented, since we always return times with our dates
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[(cond
       (= :sqlite driver/*driver*)
       "2015-06-01"
@@ -707,7 +707,7 @@
     200]]
   (sad-toucan-incidents-with-bucketing :month pacific-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[(cond
       (= :sqlite driver/*driver*)
       "2015-06-01"
@@ -726,7 +726,7 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[6 200]]
   (sad-toucan-incidents-with-bucketing :month-of-year pacific-tz))
 
@@ -736,7 +736,7 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[(cond (= :sqlite driver/*driver*)
           "2015-04-01"
 
@@ -748,7 +748,7 @@
     200]]
   (sad-toucan-incidents-with-bucketing :quarter pacific-tz))
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[(cond (= :sqlite driver/*driver*)
           "2015-04-01"
 
@@ -766,7 +766,7 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[2 200]]
   (sad-toucan-incidents-with-bucketing :quarter-of-year pacific-tz))
 
@@ -776,7 +776,7 @@
 ;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[2015 200]]
   (sad-toucan-incidents-with-bucketing :year pacific-tz))
 
@@ -787,30 +787,37 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 ;; RELATIVE DATES
-(defn- database-def-with-timestamps [interval-seconds]
-  (tx/create-database-definition (str "a-checkin-every-" interval-seconds "-seconds")
-    ["checkins"
-     [{:field-name "timestamp"
-       :base-type  :type/DateTime}]
-     (vec (for [i (range -15 15)]
-            ;; Create timestamps using relative dates (e.g. `DATEADD(second, -195, GETUTCDATE())` instead of
-            ;; generating `java.sql.Timestamps` here so they'll be in the DB's native timezone. Some DBs refuse to use
-            ;; the same timezone we're running the tests from *cough* SQL Server *cough*
-            [(u/prog1 (driver/date-interval driver/*driver* :second (* i interval-seconds))
-               (assert <>))]))]))
-
-(def ^:private checkins:4-per-minute (partial database-def-with-timestamps 15))
-(def ^:private checkins:4-per-hour   (partial database-def-with-timestamps (* 60 15)))
-(def ^:private checkins:1-per-day    (partial database-def-with-timestamps (* 60 60 24)))
-
-(defn- count-of-grouping [db field-grouping & relative-datetime-args]
-  (-> (data/with-temp-db [_ db]
+(deftype ^:private TimestampDatasetDef [intervalSeconds])
+
+(defmethod tx/get-dataset-definition TimestampDatasetDef
+  [^TimestampDatasetDef this]
+  (let [interval-seconds (.intervalSeconds this)]
+    (tx/dataset-definition (str "checkins_interval_" interval-seconds)
+      ["checkins"
+       [{:field-name "timestamp"
+         :base-type  :type/DateTime}]
+       (vec (for [i (range -15 15)]
+              ;; Create timestamps using relative dates (e.g. `DATEADD(second, -195, GETUTCDATE())` instead of
+              ;; generating `java.sql.Timestamps` here so they'll be in the DB's native timezone. Some DBs refuse to use
+              ;; the same timezone we're running the tests from *cough* SQL Server *cough*
+              [(u/prog1 (driver/date-interval driver/*driver* :second (* i interval-seconds))
+                 (assert <>))]))])))
+
+(defn- dataset-def-with-timestamps [interval-seconds]
+  (TimestampDatasetDef. interval-seconds))
+
+(def ^:private checkins:4-per-minute (dataset-def-with-timestamps 15))
+(def ^:private checkins:4-per-hour   (dataset-def-with-timestamps (* 60 15)))
+(def ^:private checkins:1-per-day    (dataset-def-with-timestamps (* 60 60 24)))
+
+(defn- count-of-grouping [dataset field-grouping & relative-datetime-args]
+  (-> (data/with-db-for-dataset [_ dataset]
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:=
                          [:datetime-field $timestamp field-grouping]
                          (cons :relative-datetime relative-datetime-args)]}))
-      first-row first int))
+      qp.test/first-row first int))
 
 ;; HACK - Don't run these tests against BigQuery/etc. because the databases need to be loaded every time the tests are ran
 ;;        and loading data into BigQuery/etc. is mind-bogglingly slow. Don't worry, I promise these work though!
@@ -818,44 +825,44 @@
 ;; Don't run the minute tests against Oracle because the Oracle tests are kind of slow and case CI to fail randomly
 ;; when it takes so long to load the data that the times are no longer current (these tests pass locally if your
 ;; machine isn't as slow as the CircleCI ones)
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping (checkins:4-per-minute) :minute "current"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping checkins:4-per-minute :minute "current"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping (checkins:4-per-minute) :minute -1 "minute"))
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping (checkins:4-per-minute) :minute  1 "minute"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping checkins:4-per-minute :minute -1 "minute"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery :oracle} 4 (count-of-grouping checkins:4-per-minute :minute  1 "minute"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping (checkins:4-per-hour) :hour "current"))
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping (checkins:4-per-hour) :hour -1 "hour"))
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping (checkins:4-per-hour) :hour  1 "hour"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping checkins:4-per-hour :hour "current"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping checkins:4-per-hour :hour -1 "hour"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 4 (count-of-grouping checkins:4-per-hour :hour  1 "hour"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping (checkins:1-per-day) :day "current"))
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping (checkins:1-per-day) :day -1 "day"))
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping (checkins:1-per-day) :day  1 "day"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping checkins:1-per-day :day "current"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping checkins:1-per-day :day -1 "day"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 1 (count-of-grouping checkins:1-per-day :day  1 "day"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 7 (count-of-grouping (checkins:1-per-day) :week "current"))
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery} 7 (count-of-grouping checkins:1-per-day :week "current"))
 
 ;; SYNTACTIC SUGAR
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   1
-  (-> (data/with-temp-db [_ (checkins:1-per-day)]
+  (-> (data/with-db-for-dataset [_ checkins:1-per-day]
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:time-interval $timestamp :current :day]}))
-      first-row first int))
+      qp.test/first-row first int))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   7
-  (-> (data/with-temp-db [_ (checkins:1-per-day)]
+  (-> (data/with-db-for-dataset [_ checkins:1-per-day]
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:time-interval $timestamp :last :week]}))
-      first-row first int))
+      qp.test/first-row first int))
 
 ;; Make sure that when referencing the same field multiple times with different units we return the one that actually
 ;; reflects the units the results are in. eg when we breakout by one unit and filter by another, make sure the results
 ;; and the col info use the unit used by breakout
 (defn- date-bucketing-unit-when-you [& {:keys [breakout-by filter-by with-interval]
                                         :or   {with-interval :current}}]
-  (let [results (data/with-temp-db [_ (checkins:1-per-day)]
+  (let [results (data/with-db-for-dataset [_ checkins:1-per-day]
                   (data/run-mbql-query checkins
                     {:aggregation [[:count]]
                      :breakout    [[:datetime-field $timestamp breakout-by]]
@@ -864,33 +871,33 @@
                (throw (ex-info "Query failed!" results)))
      :unit (-> results :data :cols first :unit)}))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 1, :unit :day}
   (date-bucketing-unit-when-you :breakout-by "day", :filter-by "day"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 7, :unit :day}
   (date-bucketing-unit-when-you :breakout-by "day", :filter-by "week"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 1, :unit :week}
   (date-bucketing-unit-when-you :breakout-by "week", :filter-by "day"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 1, :unit :quarter}
   (date-bucketing-unit-when-you :breakout-by "quarter", :filter-by "day"))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 1, :unit :hour}
   (date-bucketing-unit-when-you :breakout-by "hour", :filter-by "day"))
 
 ;; make sure if you use a relative date bucket in the past (e.g. "past 2 months") you get the correct amount of rows
 ;; (#3910)
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 2, :unit :day}
   (date-bucketing-unit-when-you :breakout-by "day", :filter-by "day", :with-interval -2))
 
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   {:rows 2, :unit :day}
   (date-bucketing-unit-when-you :breakout-by "day", :filter-by "day", :with-interval 2))
 
@@ -904,31 +911,31 @@
 ;;
 ;; We should get count = 1 for the current day, as opposed to count = 0 if we weren't auto-bucketing
 ;; (e.g. 2018-11-19T00:00 != 2018-11-19T12:37 or whatever time the checkin is at)
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   [[1]]
-  (format-rows-by [int]
-    (rows
-      (data/with-temp-db [_ (checkins:1-per-day)]
+  (qp.test/format-rows-by [int]
+    (qp.test/rows
+      (data/with-db-for-dataset [_ checkins:1-per-day]
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:= [:field-id $timestamp] (du/format-date "yyyy-MM-dd" (du/date-trunc :day))]})))))
 
 ;; this is basically the same test as above, but using the office-checkins dataset instead of the dynamically created
 ;; checkins DBs so we can run it against Snowflake and BigQuery as well.
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[1]]
-  (format-rows-by [int]
-    (rows
+  (qp.test/format-rows-by [int]
+    (qp.test/rows
       (data/dataset office-checkins
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:= [:field-id $timestamp] "2019-01-16"]})))))
 
 ;; Check that automatic bucketing still happens when using compound filter clauses (#9127)
-(expect-with-non-timeseries-dbs
+(qp.test/expect-with-non-timeseries-dbs
   [[1]]
-  (format-rows-by [int]
-    (rows
+  (qp.test/format-rows-by [int]
+    (qp.test/rows
       (data/dataset office-checkins
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
@@ -937,14 +944,14 @@
                          [:= [:field-id $id] 6]]})))))
 
 ;; if datetime string is not yyyy-MM-dd no date bucketing should take place, and thus we should get no (exact) matches
-(expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
+(qp.test/expect-with-non-timeseries-dbs-except #{:snowflake :bigquery}
   ;; Mongo returns empty row for count = 0. We should fix that
   (case driver/*driver*
     :mongo []
     [[0]])
-  (format-rows-by [int]
-    (rows
-      (data/with-temp-db [_ (checkins:1-per-day)]
+  (qp.test/format-rows-by [int]
+    (qp.test/rows
+      (data/with-db-for-dataset [_ checkins:1-per-day]
         (data/run-mbql-query checkins
           {:aggregation [[:count]]
            :filter      [:= [:field-id $timestamp] (str (du/format-date "yyyy-MM-dd" (du/date-trunc :day))
diff --git a/test/metabase/query_processor_test/remapping_test.clj b/test/metabase/query_processor_test/remapping_test.clj
index 004bfe18b4943d350842779da905d8b8810b24d4..833165faea74d2da3e10ba4a2cd291cfcbfb666e 100644
--- a/test/metabase/query_processor_test/remapping_test.clj
+++ b/test/metabase/query_processor_test/remapping_test.clj
@@ -2,7 +2,7 @@
   "Tests for the remapping results"
   (:require [metabase
              [query-processor :as qp]
-             [query-processor-test :refer :all]]
+             [query-processor-test :as qp.test :refer :all]]
             [metabase.models
              [dimension :refer [Dimension]]
              [field :refer [Field]]]
@@ -16,10 +16,10 @@
             [toucan.db :as db]))
 
 (qp-expect-with-all-drivers
-  {:rows        [["20th Century Cafe" 12 "Café Sweets"]
-                 ["25°" 11 "Café"]
-                 ["33 Taps" 7 "Beer Garden"]
-                 ["800 Degrees Neapolitan Pizzeria" 58 "Ramen"]]
+  {:rows        [["20th Century Cafe"               12 "Café"]
+                 ["25°"                             11 "Burger"]
+                 ["33 Taps"                          7 "Bar"]
+                 ["800 Degrees Neapolitan Pizzeria" 58 "Pizza"]]
    :columns     [(data/format-name "name")
                  (data/format-name "category_id")
                  "Foo"]
@@ -33,8 +33,8 @@
            {:fields   [$name $category_id]
             :order-by [[:asc $name]]
             :limit    4})
-         booleanize-native-form
-         (format-rows-by [str int str])
+         qp.test/booleanize-native-form
+         (qp.test/format-rows-by [str int str])
          tu/round-fingerprint-cols)))
 
 (defn- select-columns
@@ -78,8 +78,8 @@
     (->> (data/run-mbql-query venues
            {:order-by [[:asc $name]]
             :limit    4})
-         booleanize-native-form
-         (format-rows-by [int str int double double int str])
+         qp.test/booleanize-native-form
+         (qp.test/format-rows-by [int str int double double int str])
          (select-columns (set (map data/format-name ["name" "price" "name_2"])))
          tu/round-fingerprint-cols
          data)))
@@ -107,8 +107,8 @@
            {:fields   [$name $price $category_id]
             :order-by [[:asc $name]]
             :limit    4})
-         booleanize-native-form
-         (format-rows-by [str int str str])
+         qp.test/booleanize-native-form
+         (qp.test/format-rows-by [str int str str])
          (select-columns (set (map data/format-name ["name" "price" "name_2"])))
          tu/round-fingerprint-cols
          :data)))
diff --git a/test/metabase/query_processor_test/time_field_test.clj b/test/metabase/query_processor_test/time_field_test.clj
index 14f706141fde4d6cd74f9488101448cd1457ded3..10e3725e3bea1cb650ede4eabcb5564042c5a347 100644
--- a/test/metabase/query_processor_test/time_field_test.clj
+++ b/test/metabase/query_processor_test/time_field_test.clj
@@ -87,10 +87,7 @@
     [[1 "Plato Yeshua" "08:30:00.000Z"]
      [4 "Simcha Yan" "08:30:00.000Z"]])
   (tu/with-temporary-setting-values [report-timezone "America/Los_Angeles"]
-    (time-query {:filter (vec (cons
-                               :between
-                               (cons
-                                $last_login_time
-                                (if (qpt/supports-report-timezone? driver/*driver*)
-                                  ["08:00:00" "09:00:00"]
-                                  ["08:00:00-00:00" "09:00:00-00:00"]))))})))
+    (time-query {:filter (into [:between $last_login_time]
+                               (if (qpt/supports-report-timezone? driver/*driver*)
+                                 ["08:00:00" "09:00:00"]
+                                 ["08:00:00-00:00" "09:00:00-00:00"]))})))
diff --git a/test/metabase/query_processor_test/timezones_test.clj b/test/metabase/query_processor_test/timezones_test.clj
index 7cbbad5d5498b66777a2f060795108476ef28048..6a1037db7e1536a3e970006631bbfa96a6697620 100644
--- a/test/metabase/query_processor_test/timezones_test.clj
+++ b/test/metabase/query_processor_test/timezones_test.clj
@@ -12,16 +12,10 @@
             [metabase.test.data
              [dataset-definitions :as defs]
              [datasets :refer [expect-with-drivers]]
-             [interface :as i]
              [sql :as sql.tx]]
             [toucan.db :as db]))
 
 (defn- call-with-timezones-db [f]
-  ;; Does the database exist?
-  (when-not (i/metabase-instance defs/test-data-with-timezones driver/*driver*)
-    ;; The database doesn't exist, so we need to create it
-    (data/get-or-create-database! defs/test-data-with-timezones))
-  ;; The database can now be used in tests
   (data/with-db (data/get-or-create-database! defs/test-data-with-timezones)
     (f)))
 
diff --git a/test/metabase/sync/sync_metadata/comments_test.clj b/test/metabase/sync/sync_metadata/comments_test.clj
index 4c788900cad18373df6c76a196a7415314bec89b..af3099044e0724c5063e5b41adc48765d5ce5cfd 100644
--- a/test/metabase/sync/sync_metadata/comments_test.clj
+++ b/test/metabase/sync/sync_metadata/comments_test.clj
@@ -21,7 +21,7 @@
     (set (map (partial into {}) (db/select ['Field :name :description] :table_id [:in table-ids])))))
 
 ;; test basic field comments sync
-(tx/def-database-definition ^:const ^:private basic-field-comments
+(tx/defdataset ^:private basic-field-comments
   [["basic_field_comments"
     [{:field-name "with_comment", :base-type :type/Text, :field-comment "comment"}
      {:field-name "no_comment", :base-type :type/Text}]
@@ -31,11 +31,11 @@
   #{{:name (data/format-name "id"), :description nil}
     {:name (data/format-name "with_comment"), :description "comment"}
     {:name (data/format-name "no_comment"), :description nil}}
-  (data/with-temp-db [db basic-field-comments]
+  (data/with-db-for-dataset [db basic-field-comments]
     (db->fields db)))
 
 ;; test changing the description in metabase db so we can check it is not overwritten by comment in source db when resyncing
-(tx/def-database-definition ^:const ^:private update-desc
+(tx/defdataset ^:private update-desc
   [["update_desc"
     [{:field-name "updated_desc", :base-type :type/Text, :field-comment "original comment"}]
     [["foo"]]]])
@@ -43,7 +43,7 @@
 (datasets/expect-with-drivers #{:h2 :postgres}
   #{{:name (data/format-name "id"), :description nil}
     {:name (data/format-name "updated_desc"), :description "updated description"}}
-  (data/with-temp-db [db update-desc]
+  (data/with-db-for-dataset [db update-desc]
     ;; change the description in metabase while the source table comment remains the same
     (db/update-where! Field {:id (data/id "update_desc" "updated_desc")}, :description "updated description")
     ;; now sync the DB again, this should NOT overwrite the manually updated description
@@ -51,7 +51,7 @@
     (db->fields db)))
 
 ;; test adding a comment to the source data that was initially empty, so we can check that the resync picks it up
-(tx/def-database-definition ^:const ^:private comment-after-sync
+(tx/defdataset ^:private comment-after-sync
   [["comment_after_sync"
     [{:field-name "comment_after_sync", :base-type :type/Text}]
     [["foo"]]]])
@@ -59,14 +59,20 @@
 (datasets/expect-with-drivers #{:h2 :postgres}
   #{{:name (data/format-name "id"), :description nil}
     {:name (data/format-name "comment_after_sync"), :description "added comment"}}
-  (data/with-temp-db [db comment-after-sync]
+  (data/with-db-for-dataset [db comment-after-sync]
     ;; modify the source DB to add the comment and resync. The easiest way to do this is just destroy the entire DB
     ;; and re-create a modified version. As such, let the SQL JDBC driver know the DB is being "modified" so it can
     ;; destroy its current connection pool
     (driver/notify-database-updated driver/*driver* db)
-    (let [modified-dbdef (assoc-in comment-after-sync
-                                   [:table-definitions 0 :field-definitions 0 :field-comment]
-                                   "added comment")]
+    (let [modified-dbdef (update
+                          comment-after-sync
+                          :table-definitions
+                          (fn [[tabledef]]
+                            [(update
+                              tabledef
+                              :field-definitions
+                              (fn [[fielddef]]
+                                [(assoc fielddef :field-comment "added comment")]))]))]
       (tx/create-db! driver/*driver* modified-dbdef))
     (sync/sync-table! (Table (data/id "comment_after_sync")))
     (db->fields db)))
@@ -87,13 +93,13 @@
 ;; test basic comments on table
 (datasets/expect-with-drivers #{:h2 :postgres}
   #{{:name (data/format-name "table_with_comment"), :description "table comment"}}
-  (data/with-temp-db [db (basic-table "table_with_comment" "table comment")]
+  (data/with-db-for-dataset [db (basic-table "table_with_comment" "table comment")]
     (db->tables db)))
 
 ;; test changing the description in metabase on table to check it is not overwritten by comment in source db when resyncing
 (datasets/expect-with-drivers #{:h2 :postgres}
   #{{:name (data/format-name "table_with_updated_desc"), :description "updated table description"}}
-  (data/with-temp-db [db (basic-table "table_with_updated_desc" "table comment")]
+  (data/with-db-for-dataset [db (basic-table "table_with_updated_desc" "table comment")]
     ;; change the description in metabase while the source table comment remains the same
     (db/update-where! Table {:id (data/id "table_with_updated_desc")}, :description "updated table description")
     ;; now sync the DB again, this should NOT overwrite the manually updated description
@@ -103,7 +109,7 @@
 ;; test adding a comment to the source table that was initially empty, so we can check that the resync picks it up
 (datasets/expect-with-drivers #{:h2 :postgres}
   #{{:name (data/format-name "table_with_comment_after_sync"), :description "added comment"}}
-  (data/with-temp-db [db (basic-table "table_with_comment_after_sync" nil)]
+  (data/with-db-for-dataset [db (basic-table "table_with_comment_after_sync" nil)]
     ;; modify the source DB to add the comment and resync
     (driver/notify-database-updated driver/*driver* db)
     (tx/create-db! driver/*driver* (basic-table "table_with_comment_after_sync" "added comment"))
diff --git a/test/metabase/sync/sync_metadata/tables_test.clj b/test/metabase/sync/sync_metadata/tables_test.clj
index 696f24d5008f472741ae76924202308c9d63a089..899a4a9ceafe71c492d6bc69fd62798ec41ec7b7 100644
--- a/test/metabase/sync/sync_metadata/tables_test.clj
+++ b/test/metabase/sync/sync_metadata/tables_test.clj
@@ -11,7 +11,7 @@
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
-(tx/def-database-definition ^:private db-with-some-cruft
+(tx/defdataset ^:private db-with-some-cruft
   [["acquired_toucans"
      [{:field-name "species",              :base-type :type/Text}
       {:field-name "cam_has_acquired_one", :base-type :type/Boolean}]
diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj
index a055b8651296771c0060d9a0a4fea222a6a500e1..2b2f210f062e68faa734f7132b85e17247d3ebe4 100644
--- a/test/metabase/test/data.clj
+++ b/test/metabase/test/data.clj
@@ -1,5 +1,38 @@
 (ns metabase.test.data
-  "Code related to creating and deleting test databases + datasets."
+  "Super-useful test utility functions.
+
+  Basic way stuff in here, which you'll see everywhere in the tests, is:
+
+  1. Get the DB you're currently testing by calling `db`. Get IDs of the DB or of its Fields and Tables in that DB by
+     calling `id`.
+
+      (data/db)                 ; -> Get current test DB
+      (data/id)                 ; -> Get ID of current test DB
+      (data/id :table)          ; -> Get ID of Table named `table` in current test DB
+      (data/id :table :field)   ; -> Get ID of Field named `field` belonging to Table `table` in current test DB
+
+     Normally this database is the `test-data` database for the current driver, and is created the first time `db` or
+     `id` is called.
+
+  2. Bind the current driver with `driver/with-driver`. Defaults to `:h2`
+
+       (driver/with-driver :postgres
+         (data/id))
+       ;; -> Get ID of Postgres `test-data` database, creating it if needed
+
+  3. Bind a different database for use with for `db` and `id` functions with `with-db`.
+
+      (data/with-db [db some-database]
+        (data/id :table :field)))
+       ;; -> Return ID of Field named `field` in Table `table` in `some-db`
+
+  4. You can use helper macros like `$ids` to replace symbols starting with `$` (for Fields) or `$$` (for Tables) with
+     calls to `id` in a form:
+
+      ($ids {:source-table $$venues, :fields [$venues.name]})
+      ;; -> {:source-table (data/id :venues), :fields [(data/id :venues :name)]}
+
+     (There are several variations of this macro; see documentation below for more details.)"
   (:require [cheshire.core :as json]
             [clojure
              [string :as str]
@@ -22,18 +55,19 @@
              [dataset-definitions :as defs]
              [interface :as tx]]
             [metabase.test.util.timezone :as tu.tz]
-            [toucan.db :as db])
-  (:import [metabase.test.data.interface DatabaseDefinition TableDefinition]))
+            [schema.core :as s]
+            [toucan.db :as db]))
 
 (declare get-or-create-database!)
 
+
 ;;; ------------------------------------------ Dataset-Independent Data Fns ------------------------------------------
 
 ;; These functions offer a generic way to get bits of info like Table + Field IDs from any of our many driver/dataset
 ;; combos.
 
 (defn get-or-create-test-data-db!
-  "Get or create the Test Data database for DRIVER, which defaults to driver/`*driver*`."
+  "Get or create the Test Data database for `driver`, which defaults to `driver/*driver*`, or `:h2` if that is unbound."
   ([]       (get-or-create-test-data-db! (tx/driver)))
   ([driver] (get-or-create-database! driver defs/test-data)))
 
@@ -54,8 +88,7 @@
     (f)))
 
 (defmacro with-db
-  "Run body with DB as the current database.
-   Calls to `db` and `id` use this value."
+  "Run body with `db` as the current database. Calls to `db` and `id` use this value."
   [db & body]
   `(do-with-db ~db (fn [] ~@body)))
 
@@ -98,23 +131,23 @@
   to `id`.
 
   Optionally wraps IDs in `:field-id` or `:fk->` clauses as appropriate; this defaults to true."
-  [table-name body & {:keys [wrap-field-ids?], :or {wrap-field-ids? true}}]
+  [table-name-or-nil body & {:keys [wrap-field-ids?], :or {wrap-field-ids? true}}]
   (walk/postwalk
    (fn [form]
      (cond
        (not (symbol? form))
        form
 
-       (= form '$$table)
-       `(id ~(keyword table-name))
+       (and table-name-or-nil (= form '$$table))
+       `(id ~(keyword table-name-or-nil))
 
        (str/starts-with? form "$$")
-       (let [table-name (str/replace form #"^\$\$" "")]
-         `(id ~(keyword table-name)))
+       (let [table-name-or-nil (str/replace form #"^\$\$" "")]
+         `(id ~(keyword table-name-or-nil)))
 
        (str/starts-with? form "$")
        (let [field-name (str/replace form #"^\$" "")]
-         (token->id-call wrap-field-ids? table-name field-name))
+         (token->id-call wrap-field-ids? table-name-or-nil field-name))
 
        :else
        form))
@@ -125,7 +158,7 @@
   With no `.` delimiters, it is assumed we're referring to a Field belonging to `table-name`, which is passed implicitly
   as the first arg. With one or more `.` delimiters, no implicit `table-name` arg is passed to `id`:
 
-    $venue_id      -> (id :sightings :venue_id) ; TABLE-NAME is implicit first arg
+    $venue_id      -> (id :sightings :venue_id) ; `table-name` is implicit first arg
     $cities.id     -> (id :cities :id)          ; specify non-default Table
 
   Use `$$table` to refer to the table itself.
@@ -141,13 +174,16 @@
     ($ids [venues {:wrap-field-ids? true}]
       $category_id->categories.name)
     ;; -> [:fk-> [:field-id (id :venues :category_id(] [:field-id (id :categories :name)]]"
-  {:arglists '([table & body] [[table {:keys [wrap-field-ids?]}] & body]), :style/indent 1}
-  [table-and-options & body]
-  (let [[table-name options] (if (sequential? table-and-options)
-                               table-and-options
-                               [table-and-options])]
-    (m/mapply $->id (keyword table-name) `(do ~@body) (merge {:wrap-field-ids? false}
-                                                             options))))
+  {:arglists '([form] [table & body] [[table {:keys [wrap-field-ids?], :or {wrap-field-ids? false}}] & body])}
+  (^{:style/indent 0} [form]
+   `($ids nil ~form))
+
+  (^{:style/indent 1} [table-and-options & body]
+   (let [[table-name options] (if (sequential? table-and-options)
+                                table-and-options
+                                [table-and-options])]
+     (m/mapply $->id (keyword table-name) `(do ~@body) (merge {:wrap-field-ids? false}
+                                                              options)))))
 
 (declare id)
 
@@ -266,14 +302,14 @@
 
 (defn- add-extra-metadata!
   "Add extra metadata like Field base-type, etc."
-  [database-definition db]
-  (doseq [^TableDefinition table-definition (:table-definitions database-definition)]
-    (let [table-name (:table-name table-definition)
-          table      (delay (or  (tx/metabase-instance table-definition db)
-                                 (throw (Exception. (format "Table '%s' not loaded from definiton:\n%s\nFound:\n%s"
-                                                            table-name
-                                                            (u/pprint-to-str (dissoc table-definition :rows))
-                                                            (u/pprint-to-str (db/select [Table :schema :name], :db_id (:id db))))))))]
+  [{:keys [table-definitions], :as database-definition} db]
+  {:pre [(seq table-definitions)]}
+  (doseq [{:keys [table-name], :as table-definition} table-definitions]
+    (let [table (delay (or (tx/metabase-instance table-definition db)
+                           (throw (Exception. (format "Table '%s' not loaded from definiton:\n%s\nFound:\n%s"
+                                                      table-name
+                                                      (u/pprint-to-str (dissoc table-definition :rows))
+                                                      (u/pprint-to-str (db/select [Table :schema :name], :db_id (:id db))))))))]
       (doseq [{:keys [field-name visibility-type special-type], :as field-definition} (:field-definitions table-definition)]
         (let [field (delay (or (tx/metabase-instance field-definition @table)
                                (throw (Exception. (format "Field '%s' not loaded from definition:\n"
@@ -286,7 +322,8 @@
             (log/debug (format "SET SPECIAL TYPE %s.%s -> %s" table-name field-name special-type))
             (db/update! Field (:id @field) :special_type (u/keyword->qualified-name special-type))))))))
 
-(defn- create-database! [{:keys [database-name], :as database-definition} driver]
+(defn- create-database! [driver {:keys [database-name], :as database-definition}]
+  {:pre [(seq database-name)]}
   ;; Create the database and load its data
   ;; ALWAYS CREATE DATABASE AND LOAD DATA AS UTC! Unless you like broken tests
   (tu.tz/with-jvm-tz "UTC"
@@ -299,10 +336,14 @@
     ;; sync newly added DB
     (sync/sync-database! db)
     ;; add extra metadata for fields
-    (add-extra-metadata! database-definition db)
+    (try
+      (add-extra-metadata! database-definition db)
+      (catch Throwable e
+        (println "Error adding extra metadata:" e)))
     ;; make sure we're returing an up-to-date copy of the DB
     (Database (u/get-id db))))
 
+
 (defonce ^:private ^{:arglists '([driver]), :doc "We'll have a very bad time if any sort of test runs that calls
   `data/db` for the first time calls it multiple times in parallel -- for example my Oracle test that runs 30 sync
   calls at the same time to make sure nothing explodes and cursors aren't leaked. To make sure this doesn't happen
@@ -318,28 +359,38 @@
            (swap! locks update driver #(or % (Object.)))
            (@locks driver)))))))
 
-(defn get-or-create-database!
-  "Create DBMS database associated with DATABASE-DEFINITION, create corresponding Metabase
-  `Databases`/`Tables`/`Fields`, and sync the `Database`. DRIVER should be an object that implements
-  `IDriverTestExtensions`; it defaults to the value returned by the method `driver` for the current
-  dataset (driver/`*driver*`), which is H2 by default."
-  ([database-definition]
-   (get-or-create-database! (tx/driver) database-definition))
-  ([driver database-definition]
-   (let [driver (tx/the-driver-with-test-extensions driver)]
+(defmulti get-or-create-database!
+  "Create DBMS database associated with `database-definition`, create corresponding Metabase Databases/Tables/Fields,
+  and sync the Database. `driver` is a keyword name of a driver that implements test extension methods (as defined in
+  the `metabase.test.data.interface` namespace); `driver` defaults to `driver/*driver*` if bound, or `:h2` if not.
+  `database-definition` is anything that implements the `tx/get-database-definition` method."
+  {:arglists '([database-definition] [driver database-definition])}
+  (fn
+    ([_]
+     (tx/dispatch-on-driver-with-test-extensions (tx/driver)))
+    ([driver _]
+     (tx/dispatch-on-driver-with-test-extensions driver)))
+  :hierarchy #'driver/hierarchy)
+
+(defmethod get-or-create-database! :default
+  ([dbdef]
+   (get-or-create-database! (tx/driver) dbdef))
+
+  ([driver dbdef]
+   (let [dbdef (tx/get-dataset-definition dbdef)]
      (or
-      (tx/metabase-instance database-definition driver)
+      (tx/metabase-instance dbdef driver)
       (locking (driver->create-database-lock driver)
         (or
-         (tx/metabase-instance database-definition driver)
-         (create-database! database-definition driver)))))))
+         (tx/metabase-instance dbdef driver)
+         (create-database! driver dbdef)))))))
 
 
-(defn do-with-temp-db
-  "Execute F with DBDEF loaded as the current dataset. F takes a single argument, the `DatabaseInstance` that was
-  loaded and synced from DBDEF."
-  [^DatabaseDefinition dbdef, f]
-  (let [dbdef  (tx/map->DatabaseDefinition dbdef)]
+(s/defn do-with-db-for-dataset
+  "Execute `f` with `dbdef` loaded as the current dataset. `f` takes a single argument, the DatabaseInstance that was
+  loaded and synced from `dbdef`."
+  [dataset-definition, f :- (s/pred fn?)]
+  (let [dbdef (tx/get-dataset-definition dataset-definition)]
     (binding [db/*disable-db-logging* true]
       (let [db (get-or-create-database! (tx/driver) dbdef)]
         (assert db)
@@ -348,11 +399,11 @@
           (f db))))))
 
 
-(defmacro with-temp-db
-  "Load and sync DATABASE-DEFINITION with DRIVER and execute BODY with the newly created `Database` bound to
-  DB-BINDING, and make it the current database for `metabase.test.data` functions like `id`.
+(defmacro with-db-for-dataset
+  "Load and sync `database-definition` with `driver` and execute `body` with the newly created Database bound to
+  `db-binding`, and make it the current database for `metabase.test.data` functions like `id`.
 
-     (with-temp-db [db tupac-sightings]
+     (with-db-for-dataset [db tupac-sightings]
        (driver/process-quiery {:database (:id db)
                                :type     :query
                                :query    {:source-table (:id &events)
@@ -361,29 +412,29 @@
 
   A given Database is only created once per run of the test suite, and is automatically destroyed at the conclusion
   of the suite."
-  [[db-binding, ^DatabaseDefinition database-definition] & body]
-  `(do-with-temp-db ~database-definition
+  [[db-binding dataset-def] & body]
+  `(do-with-db-for-dataset ~dataset-def
      (fn [~db-binding]
        ~@body)))
 
-(defn resolve-dbdef [symb]
-  @(or (resolve symb)
+(defn resolve-dbdef [namespace-symb symb]
+  @(or (ns-resolve namespace-symb symb)
        (ns-resolve 'metabase.test.data.dataset-definitions symb)
-       (throw (Exception. (format "Dataset definition not found: '%s' or 'metabase.test.data.dataset-definitions/%s'"
-                                  symb symb)))))
+       (throw (Exception. (format "Dataset definition not found: '%s/%s' or 'metabase.test.data.dataset-definitions/%s'"
+                                  namespace-symb symb symb)))))
 
 (defmacro dataset
-  "Load and sync a temporary `Database` defined by DATASET, make it the current DB (for `metabase.test.data` functions
-  like `id`), and execute BODY.
+  "Load and sync a temporary Database defined by `dataset`, make it the current DB (for `metabase.test.data` functions
+  like `id` and `db`), and execute `body`.
 
-  Like `with-temp-db`, but takes an unquoted symbol naming a `DatabaseDefinition` rather than the dbef itself.
-  DATASET is optionally namespace-qualified; if not, `metabase.test.data.dataset-definitions` is assumed.
+  Like `with-db-for-dataset`, but takes an unquoted symbol naming a DatabaseDefinition rather than the dbef itself. `dataset`
+  is optionally namespace-qualified; if not, `metabase.test.data.dataset-definitions` is assumed.
 
      (dataset sad-toucan-incidents
        ...)"
   {:style/indent 1}
   [dataset & body]
-  `(with-temp-db [~'_ (resolve-dbdef '~dataset)]
+  `(with-db-for-dataset [~'_ (resolve-dbdef '~(ns-name *ns*) '~dataset)]
      ~@body))
 
 (defn- delete-model-instance!
@@ -392,7 +443,7 @@
   (db/delete! (-> instance name symbol) :id id))
 
 (defn call-with-data
-  "Takes a thunk `DATA-LOAD-FN` that returns a seq of toucan model instances that will be deleted after `BODY-FN`
+  "Takes a thunk `data-load-fn` that returns a seq of toucan model instances that will be deleted after `body-fn`
   finishes"
   [data-load-fn body-fn]
   (let [result-instances (data-load-fn)]
@@ -402,30 +453,51 @@
         (doseq [instance result-instances]
           (delete-model-instance! instance))))))
 
-(defmacro with-data [data-load-fn & body]
+(defmacro with-data
+  "Calls `data-load-fn` to create a sequence of objects, then runs `body`; finally, deletes the objects."
+  [data-load-fn & body]
   `(call-with-data ~data-load-fn (fn [] ~@body)))
 
-(def ^:private venue-categories
-  (map vector (defs/field-values defs/test-data-map "categories" "name")))
+(defn dataset-field-values
+  "Get all the values for a field in a `dataset-definition`.
 
+    (dataset-field-values \"categories\" \"name\") ; -> [\"African\" \"American\" \"Artisan\" ...]"
+  ([table-name field-name]
+   (dataset-field-values defs/test-data table-name field-name))
+
+  ([dataset-definition table-name field-name]
+   (some
+    (fn [{:keys [field-definitions rows], :as tabledef}]
+      (when (= table-name (:table-name tabledef))
+        (some
+         (fn [[i fielddef]]
+           (when (= field-name (:field-name fielddef))
+             (map #(nth % i) rows)))
+         (m/indexed field-definitions))))
+    (:table-definitions (tx/get-dataset-definition dataset-definition)))))
+
+(def ^:private category-names
+  (delay (vec (dataset-field-values "categories" "name"))))
+
+;; TODO - you should always call these functions with the `with-data` macro. We should enforce this
 (defn create-venue-category-remapping
-  "Returns a thunk that adds an internal remapping for category_id in the venues table aliased as `REMAPPING-NAME`.
+  "Returns a thunk that adds an internal remapping for category_id in the venues table aliased as `remapping-name`.
   Can be used in a `with-data` invocation."
   [remapping-name]
   (fn []
     [(db/insert! Dimension {:field_id (id :venues :category_id)
-                            :name remapping-name
-                            :type :internal})
-     (db/insert! FieldValues {:field_id (id :venues :category_id)
-                              :values (json/generate-string (range 0 (count venue-categories)))
-                              :human_readable_values (json/generate-string (map first venue-categories))})]))
+                            :name     remapping-name
+                            :type     :internal})
+     (db/insert! FieldValues {:field_id              (id :venues :category_id)
+                              :values                (json/generate-string (range 1 (inc (count @category-names))))
+                              :human_readable_values (json/generate-string @category-names)})]))
 
 (defn create-venue-category-fk-remapping
-  "Returns a thunk that adds a FK remapping for category_id in the venues table aliased as `REMAPPING-NAME`. Can be
+  "Returns a thunk that adds a FK remapping for category_id in the venues table aliased as `remapping-name`. Can be
   used in a `with-data` invocation."
   [remapping-name]
   (fn []
-    [(db/insert! Dimension {:field_id (id :venues :category_id)
-                            :name remapping-name
-                            :type :external
+    [(db/insert! Dimension {:field_id                (id :venues :category_id)
+                            :name                    remapping-name
+                            :type                    :external
                             :human_readable_field_id (id :categories :name)})]))
diff --git a/test/metabase/test/data/dataset_definitions.clj b/test/metabase/test/data/dataset_definitions.clj
index 34526c3fab754cccda080c57c73d62384a5ef56a..d125ca09025e03c47cce5f0731fb75500d47fed5 100644
--- a/test/metabase/test/data/dataset_definitions.clj
+++ b/test/metabase/test/data/dataset_definitions.clj
@@ -1,45 +1,55 @@
 (ns metabase.test.data.dataset-definitions
-  "Definitions of various datasets for use in tests with `with-temp-db`."
-  (:require [metabase.test.data.interface :as di])
+  "Definitions of various datasets for use in tests with `data/dataset` or `data/with-db-for-dataset`."
+  (:require [medley.core :as m]
+            [metabase.test.data.interface :as tx])
   (:import java.sql.Time
            [java.util Calendar TimeZone]))
 
-;; ## Datasets
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                Various Datasets                                                |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; The O.G. "Test Database" dataset
-(di/def-database-definition-edn test-data)
+(tx/defdataset-edn test-data
+  "The O.G. \"Test Database\" dataset.")
 
-;; Times when the Toucan cried
-(di/def-database-definition-edn sad-toucan-incidents)
+ (tx/defdataset-edn sad-toucan-incidents
+   "Times when the Toucan cried")
 
-;; Places, times, and circumstances where Tupac was sighted. Sighting timestamps are UNIX Timestamps in seconds
-(di/def-database-definition-edn tupac-sightings)
+(tx/defdataset-edn tupac-sightings
+  "Places, times, and circumstances where Tupac was sighted. Sighting timestamps are UNIX Timestamps in seconds")
 
-;; Dataset with nested columns, for testing a MongoDB-style database
-(di/def-database-definition-edn geographical-tips)
+(tx/defdataset-edn geographical-tips
+  "Dataset with nested columns, for testing a MongoDB-style database")
 
-;; A very tiny dataset with a list of places and a booleans
-(di/def-database-definition-edn places-cam-likes)
+(tx/defdataset-edn places-cam-likes
+  "A very tiny dataset with a list of places and a booleans")
 
-;; A small dataset with users and a set of messages between them. Each message has *2* foreign keys to user --
-;; sender and receiver -- allowing us to test situations where multiple joins for a *single* table should occur.
-(di/def-database-definition-edn avian-singles)
+(tx/defdataset-edn avian-singles
+  "A small dataset with users and a set of messages between them. Each message has *2* foreign keys to user -- sender
+  and receiver -- allowing us to test situations where multiple joins for a *single* table should occur.")
 
-;; A small dataset that includes an integer column with some NULL and ZERO values, meant for testing things like
-;; expressions to make sure they behave correctly
-;;
-;; As an added "bonus" this dataset has a table with a name in a slash in it, so the driver will need to support that
-;; correctly in order for this to work!
-(di/def-database-definition-edn daily-bird-counts)
+(tx/defdataset-edn daily-bird-counts
+  "A small dataset that includes an integer column with some NULL and ZERO values, meant for testing things like
+  expressions to make sure they behave correctly.
+
+  As an added bonus this dataset has a table with a name in a slash in it, so the driver will need to support that
+  correctly in order for this to work!")
+
+(tx/defdataset-edn office-checkins
+  "A small dataset that includes TIMESTAMP dates. People who stopped by the Metabase office and the time they did so.")
+
+(tx/defdataset-edn bird-flocks
+  "A small dataset with birds and the flocks they belong to (a many-to-one relationship). Some birds belong to no
+  flocks, and one flock has no birds, so this is useful for testing behavior of various types of joins. (`flock_id` is
+  not explicitly marked as a foreign key, because the test dataset syntax does not yet have a way to support nullable
+  foreign keys.)")
 
-;; A small dataset that includes TIMESTAMP dates. People who stopped by the Metabase office and the time they did so.
-(di/def-database-definition-edn office-checkins)
 
 (defn- calendar-with-fields ^Calendar [date & fields]
-  (let [cal-from-date  (doto (Calendar/getInstance (TimeZone/getTimeZone "UTC"))
-                         (.setTime date))
-        blank-calendar (doto (.clone cal-from-date)
-                         .clear)]
+  (let [^Calendar cal-from-date  (doto (Calendar/getInstance (TimeZone/getTimeZone "UTC"))
+                                   (.setTime date))
+        ^Calendar blank-calendar (doto ^Calendar (.clone cal-from-date)
+                                   .clear)]
     (doseq [field fields]
       (.set blank-calendar field (.get cal-from-date field)))
     blank-calendar))
@@ -57,66 +67,72 @@
   [date]
   (Time. (.getTimeInMillis (calendar-with-fields date Calendar/HOUR_OF_DAY Calendar/MINUTE Calendar/SECOND))))
 
-(di/def-database-definition test-data-with-time
-  (di/update-table-def "users"
-                       (fn [table-def]
-                         [(first table-def)
-                          {:field-name "last_login_date", :base-type :type/Date}
-                          {:field-name "last_login_time", :base-type :type/Time}
-                          (peek table-def)])
-                       (fn [rows]
-                         (mapv (fn [[username last-login password-text]]
-                                 [username (date-only last-login) (time-only last-login) password-text])
-                               rows))
-                       (for [[table-name :as orig-def] (di/slurp-edn-table-def "test-data")
-                             :when (= table-name "users")]
-                         orig-def)))
-
-(di/def-database-definition test-data-with-null-date-checkins
-  (di/update-table-def "checkins"
-                       #(vec (concat % [{:field-name "null_only_date" :base-type :type/Date}]))
-                       (fn [rows]
-                         (mapv #(conj % nil) rows))
-                       (di/slurp-edn-table-def "test-data")))
-
-(di/def-database-definition test-data-with-timezones
-  (di/update-table-def "users"
-                       (fn [table-def]
-                         [(first table-def)
-                          {:field-name "last_login", :base-type :type/DateTimeWithTZ}
-                          (peek table-def)])
-                       identity
-                       (di/slurp-edn-table-def "test-data")))
-
-(def test-data-map
-  "Converts data from `test-data` to a map of maps like the following:
-
-   {<table-name> [{<field-name> <field value> ...}]."
-  (reduce (fn [acc {:keys [table-name field-definitions rows]}]
-            (let [field-names (mapv :field-name field-definitions)]
-              (assoc acc table-name
-                     (for [row rows]
-                       (zipmap field-names row)))))
-          {} (:table-definitions test-data)))
-
-(defn field-values
-  "Returns the field values for the given `TABLE` and `COLUMN` found
-  in the data-map `M`."
-  [m table column]
-  (mapv #(get % column) (get m table)))
-
-;; Takes the `test-data` dataset and adds a `created_by` column to the users table that is self referencing
-(di/def-database-definition test-data-self-referencing-user
-  (di/update-table-def "users"
-                       (fn [table-def]
-                         (conj table-def {:field-name "created_by", :base-type :type/Integer, :fk :users}))
-                       (fn [rows]
-                         (mapv (fn [[username last-login password-text] idx]
-                                 [username last-login password-text (if (= 1 idx)
-                                                                      idx
-                                                                      (dec idx))])
-                               rows
-                               (iterate inc 1)))
-                       (for [[table-name :as orig-def] (di/slurp-edn-table-def "test-data")
-                             :when (= table-name "users")]
-                         orig-def)))
+
+(defonce ^{:doc "The main `test-data` dataset, but only the `users` table, and with `last_login_date` and
+  `last_login_time` instead of `last_login`."}
+  test-data-with-time
+  (tx/transformed-dataset-definition "test-data-with-time" test-data
+    (tx/transform-dataset-only-tables "users")
+    (tx/transform-dataset-update-table "users"
+      :table
+      (fn [tabledef]
+        (update
+         tabledef
+         :field-definitions
+         (fn [[name-field-def _ password-field-def]]
+           [name-field-def
+            (tx/map->FieldDefinition {:field-name "last_login_date", :base-type :type/Date})
+            (tx/map->FieldDefinition {:field-name "last_login_time", :base-type :type/Time})
+            password-field-def])))
+      :rows
+      (fn [rows]
+        (for [[username last-login password-text] rows]
+          [username (date-only last-login) (time-only last-login) password-text])))))
+
+(defonce ^{:doc "The main `test-data` dataset, with an additional (all-null) `null_only_date` Field."}
+  test-data-with-null-date-checkins
+  (tx/transformed-dataset-definition "test-data-with-null-date-checkins" test-data
+    (tx/transform-dataset-update-table "checkins"
+      :table
+      (fn [tabledef]
+        (update
+         tabledef
+         :field-definitions
+         concat
+         [(tx/map->FieldDefinition {:field-name "null_only_date" :base-type :type/Date})]))
+      :rows
+      (fn [rows]
+        (for [row rows]
+          (concat row [nil]))))))
+
+(defonce ^{:doc "The main `test-data` dataset, but `last_login` has a base type of `:type/DateTimeWithTZ`."}
+  test-data-with-timezones
+  (tx/transformed-dataset-definition "test-data-with-timezones" test-data
+    (tx/transform-dataset-update-table "users"
+      :table
+      (fn [tabledef]
+        (update
+         tabledef
+         :field-definitions
+         (fn [[name-field-def _ password-field-def]]
+           [name-field-def
+            (tx/map->FieldDefinition {:field-name "last_login", :base-type :type/DateTimeWithTZ})
+            password-field-def]))))))
+
+(defonce ^{:doc "The usual `test-data` dataset, but only the `users` table; adds a `created_by` column to the users
+  table that is self referencing."}
+  test-data-self-referencing-user
+  (tx/transformed-dataset-definition "test-data-self-referencing-user" test-data
+    (tx/transform-dataset-only-tables "users")
+    (tx/transform-dataset-update-table "users"
+      :table
+      (fn [tabledef]
+        (update tabledef :field-definitions concat [(tx/map->FieldDefinition
+                                                     {:field-name "created_by", :base-type :type/Integer, :fk :users})]))
+      ;; created_by = user.id - 1, except for User 1, who was created by himself (?)
+      :rows
+      (fn [rows]
+        (for [[idx [username last-login password-text]] (m/indexed rows)]
+          [username last-login password-text (if (zero? idx)
+                                               1
+                                               idx)])))))
diff --git a/test/metabase/test/data/dataset_definitions/bird-flocks.edn b/test/metabase/test/data/dataset_definitions/bird-flocks.edn
new file mode 100644
index 0000000000000000000000000000000000000000..7d9caeb36f616805f2a84a6be138636e486e61c8
--- /dev/null
+++ b/test/metabase/test/data/dataset_definitions/bird-flocks.edn
@@ -0,0 +1,30 @@
+[["bird" [{:field-name "name"
+           :base-type  :type/Text}
+          {:field-name "flock_id"
+           :base-type  :type/Integer}]
+  [["Russell Crow"     4]   ; 1
+   ["Big Red"          5]   ; 2
+   ["Camellia Crow"    nil] ; 3
+   ["Peter Pelican"    2]   ; 4
+   ["Geoff Goose"      nil] ; 5
+   ["Greg Goose"       1]   ; 6
+   ["Callie Crow"      4]   ; 7
+   ["Patricia Pelican" nil] ; 8
+   ["Gerald Goose"     1]   ; 9
+   ["Pamela Pelican"   nil] ; 10
+   ["Oswald Owl"       nil] ; 11
+   ["Chicken Little"   5]   ; 12
+   ["Paul Pelican"     2]   ; 13
+   ["McNugget"         5]   ; 14
+   ["Orville Owl"      3]   ; 15
+   ["Carson Crow"      4]   ; 16
+   ["Olita Owl"        nil] ; 17
+   ["Oliver Owl"       3]]] ; 18
+ ["flock" [{:field-name    "name"
+            :base-type :type/Text}]
+  [["Green Street Gaggle"]      ; 1
+   ["SoMa Squadron"]            ; 2
+   ["Portrero Hill Parliament"] ; 3
+   ["Mission Street Murder"]    ; 4
+   ["Bayview Brood"]            ; 5
+   ["Fillmore Flock"]]]]        ; 6
diff --git a/test/metabase/test/data/dataset_definitions/test-data.edn b/test/metabase/test/data/dataset_definitions/test-data.edn
index 61e70d5b22f012fb4cb37cd95433fd666bb5a637..b9da7fe8011abea58e344e092c560cab6f66d9b7 100644
--- a/test/metabase/test/data/dataset_definitions/test-data.edn
+++ b/test/metabase/test/data/dataset_definitions/test-data.edn
@@ -29,98 +29,98 @@
            {:field-name "password"
             :base-type  :type/Text
             :visibility-type :sensitive}]
-  [["Plato Yeshua" #inst "2014-04-01T08:30" "4be68cda-6fd5-4ba7-944e-2b475600bda5"]
-   ["Felipinho Asklepios" #inst "2014-12-05T15:15" "5bb19ad9-f3f8-421f-9750-7d398e38428d"]
-   ["Kaneonuskatew Eiran" #inst "2014-11-06T16:15" "a329ccfe-b99c-42eb-9c93-cb9adc3eb1ab"]
-   ["Simcha Yan" #inst "2014-01-01T08:30" "a61f97c6-4484-4a63-b37e-b5e58bfa2ecb"]
-   ["Quentin Sören" #inst "2014-10-03T17:30" "10a0fea8-9bb4-48fe-a336-4d9cbbd78aa0"]
-   ["Shad Ferdynand" #inst "2014-08-02T12:30" "d35c9d78-f9cf-4f52-b1cc-cb9078eebdcb"]
-   ["Conchúr Tihomir" #inst "2014-08-02T09:30" "900335ad-e03b-4259-abc7-76aac21cedca"]
-   ["Szymon Theutrich" #inst "2014-02-01T10:15" "d6c47a54-9d88-4c4a-8054-ace76764ed0d"]
-   ["Nils Gotam" #inst "2014-04-03T09:30" "b085040c-7aa4-4e96-8c8f-420b2c99c920"]
-   ["Frans Hevel" #inst "2014-07-03T19:30" "b7a43e91-9fb9-4fe9-ab6f-ea51ab0f94e4"]
-   ["Spiros Teofil" #inst "2014-11-01T07:00" "62b9602c-27b8-44ea-adbd-2748f26537af"]
-   ["Kfir Caj" #inst "2014-07-03T01:30" "dfe21df3-f364-479d-a5e7-04bc5d85ad2b"]
-   ["Dwight Gresham" #inst "2014-08-01T10:30" "75a1ebf1-cae7-4a50-8743-32d97500f2cf"]
-   ["Broen Olujimi" #inst "2014-10-03T13:45" "f9b65c74-9f91-4cfd-9248-94a53af82866"]
-   ["Rüstem Hebel" #inst "2014-08-01T12:45" "02ad6b15-54b0-4491-bf0f-d781b0a2c4f5"]]]
+  [["Plato Yeshua"        #inst "2014-04-01T08:30" "4be68cda-6fd5-4ba7-944e-2b475600bda5"]   ; 1
+   ["Felipinho Asklepios" #inst "2014-12-05T15:15" "5bb19ad9-f3f8-421f-9750-7d398e38428d"]   ; 2
+   ["Kaneonuskatew Eiran" #inst "2014-11-06T16:15" "a329ccfe-b99c-42eb-9c93-cb9adc3eb1ab"]   ; 3
+   ["Simcha Yan"          #inst "2014-01-01T08:30" "a61f97c6-4484-4a63-b37e-b5e58bfa2ecb"]   ; 4
+   ["Quentin Sören"       #inst "2014-10-03T17:30" "10a0fea8-9bb4-48fe-a336-4d9cbbd78aa0"]   ; 5
+   ["Shad Ferdynand"      #inst "2014-08-02T12:30" "d35c9d78-f9cf-4f52-b1cc-cb9078eebdcb"]   ; 6
+   ["Conchúr Tihomir"     #inst "2014-08-02T09:30" "900335ad-e03b-4259-abc7-76aac21cedca"]   ; 7
+   ["Szymon Theutrich"    #inst "2014-02-01T10:15" "d6c47a54-9d88-4c4a-8054-ace76764ed0d"]   ; 8
+   ["Nils Gotam"          #inst "2014-04-03T09:30" "b085040c-7aa4-4e96-8c8f-420b2c99c920"]   ; 9
+   ["Frans Hevel"         #inst "2014-07-03T19:30" "b7a43e91-9fb9-4fe9-ab6f-ea51ab0f94e4"]   ; 10
+   ["Spiros Teofil"       #inst "2014-11-01T07:00" "62b9602c-27b8-44ea-adbd-2748f26537af"]   ; 11
+   ["Kfir Caj"            #inst "2014-07-03T01:30" "dfe21df3-f364-479d-a5e7-04bc5d85ad2b"]   ; 12
+   ["Dwight Gresham"      #inst "2014-08-01T10:30" "75a1ebf1-cae7-4a50-8743-32d97500f2cf"]   ; 13
+   ["Broen Olujimi"       #inst "2014-10-03T13:45" "f9b65c74-9f91-4cfd-9248-94a53af82866"]   ; 14
+   ["Rüstem Hebel"        #inst "2014-08-01T12:45" "02ad6b15-54b0-4491-bf0f-d781b0a2c4f5"]]] ; 15
  ["categories" [{:field-name "name"
                  :base-type  :type/Text}]
-  [["African"]
-   ["American"]
-   ["Artisan"]
-   ["Asian"]
-   ["BBQ"]
-   ["Bakery"]
-   ["Bar"]
-   ["Beer Garden"]
-   ["Breakfast / Brunch"]
-   ["Brewery"]
-   ["Burger"]
-   ["Café"]
-   ["Café Sweets"]
-   ["Caribbean"]
-   ["Chinese"]
-   ["Coffee Shop"]
-   ["Comedy Club"]
-   ["Deli"]
-   ["Dim Sum"]
-   ["Diner"]
-   ["Donut Shop"]
-   ["English"]
-   ["Entertainment"]
-   ["Fashion"]
-   ["Fast Food"]
-   ["Food Truck"]
-   ["French"]
-   ["Gay Bar"]
-   ["German"]
-   ["Gluten-free"]
-   ["Greek"]
-   ["Grocery"]
-   ["Health & Beauty"]
-   ["Home"]
-   ["Hostel"]
-   ["Hot Dog"]
-   ["Hotel"]
-   ["Indian"]
-   ["Italian"]
-   ["Japanese"]
-   ["Jewish"]
-   ["Juice Bar"]
-   ["Karaoke"]
-   ["Korean"]
-   ["Landmark"]
-   ["Late Dining"]
-   ["Latin American"]
-   ["Lounge"]
-   ["Mediterannian"]
-   ["Mexican"]
-   ["Middle Eastern"]
-   ["Molecular Gastronomy"]
-   ["Moroccan"]
-   ["Museum"]
-   ["Nightclub"]
-   ["Nightlife"]
-   ["Outdoors"]
-   ["Pizza"]
-   ["Ramen"]
-   ["Restaurant General"]
-   ["Scandinavian"]
-   ["Seafood"]
-   ["South Pacific"]
-   ["Southern"]
-   ["Spanish"]
-   ["Stadium"]
-   ["Steakhouse"]
-   ["Strip Club"]
-   ["Tapas"]
-   ["Tea Room"]
-   ["Thai"]
-   ["Unknown"]
-   ["Vegetarian / Vegan"]
-   ["Wine Bar"]
-   ["Winery"]]]
+  [["African"]              ; 1
+   ["American"]             ; 2
+   ["Artisan"]              ; 3
+   ["Asian"]                ; 4
+   ["BBQ"]                  ; 5
+   ["Bakery"]               ; 6
+   ["Bar"]                  ; 7
+   ["Beer Garden"]          ; 8
+   ["Breakfast / Brunch"]   ; 9
+   ["Brewery"]              ; 10
+   ["Burger"]               ; 11
+   ["Café"]                 ; 12
+   ["Café Sweets"]          ; 13
+   ["Caribbean"]            ; 14
+   ["Chinese"]              ; 15
+   ["Coffee Shop"]          ; 16
+   ["Comedy Club"]          ; 17
+   ["Deli"]                 ; 18
+   ["Dim Sum"]              ; 19
+   ["Diner"]                ; 20
+   ["Donut Shop"]           ; 21
+   ["English"]              ; 22
+   ["Entertainment"]        ; 23
+   ["Fashion"]              ; 24
+   ["Fast Food"]            ; 25
+   ["Food Truck"]           ; 26
+   ["French"]               ; 27
+   ["Gay Bar"]              ; 28
+   ["German"]               ; 29
+   ["Gluten-free"]          ; 30
+   ["Greek"]                ; 31
+   ["Grocery"]              ; 32
+   ["Health & Beauty"]      ; 33
+   ["Home"]                 ; 34
+   ["Hostel"]               ; 35
+   ["Hot Dog"]              ; 36
+   ["Hotel"]                ; 37
+   ["Indian"]               ; 38
+   ["Italian"]              ; 39
+   ["Japanese"]             ; 40
+   ["Jewish"]               ; 41
+   ["Juice Bar"]            ; 42
+   ["Karaoke"]              ; 43
+   ["Korean"]               ; 44
+   ["Landmark"]             ; 45
+   ["Late Dining"]          ; 46
+   ["Latin American"]       ; 47
+   ["Lounge"]               ; 48
+   ["Mediterannian"]        ; 49
+   ["Mexican"]              ; 50
+   ["Middle Eastern"]       ; 51
+   ["Molecular Gastronomy"] ; 52
+   ["Moroccan"]             ; 53
+   ["Museum"]               ; 54
+   ["Nightclub"]            ; 55
+   ["Nightlife"]            ; 56
+   ["Outdoors"]             ; 57
+   ["Pizza"]                ; 58
+   ["Ramen"]                ; 59
+   ["Restaurant General"]   ; 60
+   ["Scandinavian"]         ; 61
+   ["Seafood"]              ; 62
+   ["South Pacific"]        ; 63
+   ["Southern"]             ; 64
+   ["Spanish"]              ; 65
+   ["Stadium"]              ; 66
+   ["Steakhouse"]           ; 67
+   ["Strip Club"]           ; 68
+   ["Tapas"]                ; 69
+   ["Tea Room"]             ; 70
+   ["Thai"]                 ; 71
+   ["Unknown"]              ; 72
+   ["Vegetarian / Vegan"]   ; 73
+   ["Wine Bar"]             ; 74
+   ["Winery"]]]             ; 75
  ["venues" [{:field-name   "name"
              :base-type    :type/Text}
             {:field-name   "latitude"
diff --git a/test/metabase/test/data/h2.clj b/test/metabase/test/data/h2.clj
index ee62a4246e67a503269ad433ba0acfeafeef8c8a..bda97848173c907d31f7b3ea6e95858a6519b0bd 100644
--- a/test/metabase/test/data/h2.clj
+++ b/test/metabase/test/data/h2.clj
@@ -3,6 +3,8 @@
   (:require [clojure.string :as str]
             [metabase.db.spec :as dbspec]
             [metabase.driver.sql.util :as sql.u]
+            [metabase.models.database :refer [Database]]
+            [metabase.test.data :as data]
             [metabase.test.data
              [interface :as tx]
              [sql :as sql.tx]
@@ -10,10 +12,29 @@
             [metabase.test.data.sql-jdbc
              [execute :as execute]
              [load-data :as load-data]
-             [spec :as spec]]))
+             [spec :as spec]]
+            [toucan.db :as db]))
 
 (sql-jdbc.tx/add-test-extensions! :h2)
 
+(defonce ^:private h2-test-dbs-created-by-this-instance (atom #{}))
+
+;; For H2, test databases are all in-memory, which don't work if they're saved from a different REPL session or the
+;; like. So delete any 'stale' in-mem DBs from the application DB when someone calls `get-or-create-database!` as
+;; needed.
+(defmethod data/get-or-create-database! :h2
+  ([dbdef]
+   (data/get-or-create-database! :h2 dbdef))
+
+  ([driver dbdef]
+   (let [{:keys [database-name], :as dbdef} (tx/get-dataset-definition dbdef)]
+     ;; don't think we need to bother making this super-threadsafe because REPL usage and tests are more or less
+     ;; single-threaded
+     (when (not (contains? @h2-test-dbs-created-by-this-instance database-name))
+       (db/delete! Database :engine "h2", :name database-name)
+       (swap! h2-test-dbs-created-by-this-instance conj database-name))
+     ((get-method data/get-or-create-database! :default) driver dbdef))))
+
 (defmethod sql.tx/field-base-type->sql-type [:h2 :type/BigInteger] [_ _] "BIGINT")
 (defmethod sql.tx/field-base-type->sql-type [:h2 :type/Boolean]    [_ _] "BOOL")
 (defmethod sql.tx/field-base-type->sql-type [:h2 :type/Date]       [_ _] "DATE")
diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj
index 2dcb7944998f7ceba76d6da06d88175be1b2e723..8082c16c2d392077c8191a432029d0b28c40ff03 100644
--- a/test/metabase/test/data/interface.clj
+++ b/test/metabase/test/data/interface.clj
@@ -8,6 +8,7 @@
   (:require [clojure.string :as str]
             [clojure.tools.reader.edn :as edn]
             [environ.core :refer [env]]
+            [medley.core :as m]
             [metabase
              [db :as db]
              [driver :as driver]
@@ -20,14 +21,21 @@
             [metabase.test.data.env :as tx.env]
             [metabase.util
              [date :as du]
+             [pretty :as pretty]
              [schema :as su]]
             [schema.core :as s])
   (:import clojure.lang.Keyword))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                        Dataset Definition Record Types                                         |
+;;; |                                   Dataset Definition Record Types & Protocol                                   |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
+(defmulti get-dataset-definition
+  "Return a definition of a dataset, so a test database can be created from it."
+  {:arglists '([this])}
+  class)
+
+
 (s/defrecord FieldDefinition [field-name      :- su/NonBlankString
                               base-type       :- (s/cond-pre {:native su/NonBlankString}
                                                              su/FieldType)
@@ -50,6 +58,8 @@
   nil
   :load-ns true)
 
+(defmethod get-dataset-definition DatabaseDefinition [this] this)
+
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                          Registering Test Extensions                                           |
@@ -353,109 +363,208 @@
 ;;; |                                 Helper Functions for Creating New Definitions                                  |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
+(def ^:private DatasetFieldDefinition
+  "Schema for a Field in a test dataset defined by a `defdataset` form or in a dataset defnition EDN file."
+  {:field-name                       su/NonBlankString
+   :base-type                        (s/cond-pre {:native su/NonBlankString} su/FieldType)
+   (s/optional-key :special-type)    (s/maybe su/FieldType)
+   (s/optional-key :visibility-type) (s/maybe (apply s/enum field/visibility-types))
+   (s/optional-key :fk)              (s/maybe s/Keyword)
+   (s/optional-key :field-comment)   (s/maybe su/NonBlankString)})
+
+(def ^:private DatasetTableDefinition
+  "Schema for a Table in a test dataset defined by a `defdataset` form or in a dataset defnition EDN file."
+  [(s/one su/NonBlankString "table name")
+   (s/one [DatasetFieldDefinition] "fields")
+   (s/one [[s/Any]] "rows")])
+
 ;; TODO - not sure everything below belongs in this namespace
 
-(defn create-field-definition
-  "Create a new `FieldDefinition`; verify its values."
-  ^FieldDefinition [field-definition-map]
+(s/defn ^:private dataset-field-definition :- FieldDefinition
+  [field-definition-map :- DatasetFieldDefinition]
+  "Parse a Field definition (from a `defdatset` form or EDN file) and return a FieldDefinition instance for
+  comsumption by various test-data-loading methods."
   (s/validate FieldDefinition (map->FieldDefinition field-definition-map)))
 
-(defn create-table-definition
-  "Convenience for creating a `TableDefinition`."
-  ^TableDefinition [^String table-name, field-definition-maps rows]
-  (s/validate TableDefinition (map->TableDefinition
-                               {:table-name        table-name
-                                :rows              rows
-                                :field-definitions (mapv create-field-definition field-definition-maps)})))
-
-(defn create-database-definition
-  "Convenience for creating a new `DatabaseDefinition`."
+(s/defn ^:private dataset-table-definition :- TableDefinition
+  "Parse a Table definition (from a `defdatset` form or EDN file) and return a TableDefinition instance for
+  comsumption by various test-data-loading methods."
+  ([tabledef :- DatasetTableDefinition]
+   (apply dataset-table-definition tabledef))
+
+  ([table-name :- su/NonBlankString, field-definition-maps, rows]
+   (s/validate
+    TableDefinition
+    (map->TableDefinition
+     {:table-name        table-name
+      :rows              rows
+      :field-definitions (mapv dataset-field-definition field-definition-maps)}))))
+
+(s/defn dataset-definition :- DatabaseDefinition
+  "Parse a dataset definition (from a `defdatset` form or EDN file) and return a DatabaseDefinition instance for
+  comsumption by various test-data-loading methods."
   {:style/indent 1}
-  ^DatabaseDefinition [^String database-name & table-name+field-definition-maps+rows]
-  (s/validate DatabaseDefinition (map->DatabaseDefinition
-                                  {:database-name     database-name
-                                   :table-definitions (mapv (partial apply create-table-definition)
-                                                            table-name+field-definition-maps+rows)})))
+  [database-name :- su/NonBlankString, & definition]
+  (s/validate
+   DatabaseDefinition
+   (map->DatabaseDefinition
+    {:database-name     database-name
+     :table-definitions (for [table definition]
+                          (dataset-table-definition table))})))
+
+(defmacro defdataset
+  "Define a new dataset to test against."
+  ([dataset-name definition]
+   `(defdataset ~dataset-name nil ~definition))
+
+  ([dataset-name docstring definition]
+   {:pre [(symbol? dataset-name)]}
+   `(defonce ~(vary-meta dataset-name assoc :doc docstring, :tag `DatabaseDefinition)
+      (apply dataset-definition ~(name dataset-name) ~definition))))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                            EDN Dataset Definitions                                             |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 (def ^:private edn-definitions-dir "./test/metabase/test/data/dataset_definitions/")
 
-(defn slurp-edn-table-def [dbname]
-  ;; disabled for now when AOT compiling tests because this reads the entire file in which results in Method code too
-  ;; large errors
-  ;;
-  ;; The fix would be to delay reading the code until runtime
-  (when-not *compile-files*
-    (edn/read-string (slurp (str edn-definitions-dir dbname ".edn")))))
-
-(defn update-table-def
-  "Function useful for modifying a table definition before it's applied. Will invoke `UPDATE-TABLE-DEF-FN` on the vector
-  of column definitions and `UPDATE-ROWS-FN` with the vector of rows in the database definition. `TABLE-DEF` is the
-  database definition (typically used directly in a `def-database-definition` invocation)."
-  [table-name-to-update update-table-def-fn update-rows-fn table-def]
-  (vec
-   (for [[table-name table-def rows :as orig-table-def] table-def]
-     (if (= table-name table-name-to-update)
-       [table-name
-        (update-table-def-fn table-def)
-        (update-rows-fn rows)]
-       orig-table-def))))
-
-(defmacro def-database-definition
-  "Convenience for creating a new `DatabaseDefinition` named by the symbol DATASET-NAME."
-  [^clojure.lang.Symbol dataset-name table-name+field-definition-maps+rows]
-  {:pre [(symbol? dataset-name)]}
-  `(def ~(vary-meta dataset-name assoc :tag DatabaseDefinition)
-     (apply create-database-definition ~(name dataset-name) ~table-name+field-definition-maps+rows)))
-
-(defmacro def-database-definition-edn [dbname]
-  `(def-database-definition ~dbname
-     ~(slurp-edn-table-def (name dbname))))
-
-;;; ## Convenience + Helper Functions
-;; TODO - should these go here, or in `metabase.test.data`?
-
-(defn get-tabledef
-  "Return `TableDefinition` with TABLE-NAME in DBDEF."
-  [^DatabaseDefinition dbdef, ^String table-name]
-  (first (for [tabledef (:table-definitions dbdef)
-               :when    (= (:table-name tabledef) table-name)]
-           tabledef)))
-
-(defn get-fielddefs
-  "Return the `FieldDefinitions` associated with table with TABLE-NAME in DBDEF."
-  [^DatabaseDefinition dbdef, ^String table-name]
-  (:field-definitions (get-tabledef dbdef table-name)))
-
-(defn dbdef->table->id->k->v
+(deftype ^:private EDNDatasetDefinition [dataset-name def]
+  pretty/PrettyPrintable
+  (pretty [_]
+    (list 'edn-dataset-definition dataset-name)))
+
+(defmethod get-dataset-definition EDNDatasetDefinition
+  [^EDNDatasetDefinition this]
+  @(.def this))
+
+(s/defn edn-dataset-definition
+  "Define a new test dataset using the definition in an EDN file in the `test/metabase/test/data/dataset_definitions/`
+  directory. (Filename should be `dataset-name` + `.edn`.)"
+  [dataset-name :- su/NonBlankString]
+  (let [get-def (delay
+                 (apply
+                  dataset-definition
+                  dataset-name
+                  (edn/read-string
+                   (slurp
+                    (str edn-definitions-dir dataset-name ".edn")))))]
+    (EDNDatasetDefinition. dataset-name get-def)))
+
+(defmacro defdataset-edn
+  "Define a new test dataset using the definition in an EDN file in the `test/metabase/test/data/dataset_definitions/`
+  directory. (Filename should be `dataset-name` + `.edn`.)"
+  [dataset-name & [docstring]]
+  `(defonce ~(vary-meta dataset-name assoc :doc docstring, :tag `EDNDatasetDefinition)
+     (edn-dataset-definition ~(name dataset-name))))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                        Transformed Dataset Definitions                                         |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+(deftype ^:private TransformedDatasetDefinition [new-name wrapped-definition def]
+  pretty/PrettyPrintable
+  (pretty [_]
+    (list 'transformed-dataset-definition new-name (pretty/pretty wrapped-definition))))
+
+(s/defn transformed-dataset-definition
+  "Create a dataset definition that is a transformation of an some other one, seqentially applying `transform-fns` to
+  it. The results of `transform-fns` are cached."
+  {:style/indent 2}
+  [new-name :- su/NonBlankString, wrapped-definition & transform-fns :- [(s/pred fn?)]]
+  (let [transform-fn (apply comp (reverse transform-fns))
+        get-def      (delay
+                      (transform-fn
+                       (assoc (get-dataset-definition wrapped-definition)
+                         :database-name new-name)))]
+    (TransformedDatasetDefinition. new-name wrapped-definition get-def)))
+
+(defmethod get-dataset-definition TransformedDatasetDefinition
+  [^TransformedDatasetDefinition this]
+  @(.def this))
+
+(defn transform-dataset-update-tabledefs [f & args]
+  (fn [dbdef]
+    (apply update dbdef :table-definitions f args)))
+
+(s/defn transform-dataset-only-tables :- (s/pred fn?)
+  "Create a function for `transformed-dataset-definition` to only keep some subset of Tables from the original dataset
+  definition."
+  [& table-names]
+  (transform-dataset-update-tabledefs
+   (let [names (set table-names)]
+     (fn [tabledefs]
+       (filter
+        (fn [{:keys [table-name]}]
+          (contains? names table-name))
+        tabledefs)))))
+
+(defn transform-dataset-update-table
+  "Create a function to transform a single table, for use with `transformed-dataset-definition`. Pass `:table`, `:rows`
+  or both functions to transform the entire table definition, or just the rows, respectively."
+  {:style/indent 1}
+  [table-name & {:keys [table rows], :or {table identity, rows identity}}]
+  (transform-dataset-update-tabledefs
+   (fn [tabledefs]
+     (for [{this-name :table-name, :as tabledef} tabledefs]
+       (if (= this-name table-name)
+         (update (table tabledef) :rows rows)
+         tabledef)))))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                      Flattening Dataset Definitions (i.e. for timeseries DBs like Druid)                       |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+;; TODO - maybe this should go in a different namespace
+
+(s/defn ^:private tabledef-with-name :- TableDefinition
+  "Return `TableDefinition` with `table-name` in `dbdef`."
+  [{:keys [table-definitions]} :- DatabaseDefinition, table-name :- su/NonBlankString]
+  (some
+   (fn [{this-name :table-name, :as tabledef}]
+     (when (= table-name this-name)
+       tabledef))
+   table-definitions))
+
+(s/defn ^:private fielddefs-for-table-with-name :- [FieldDefinition]
+  "Return the `FieldDefinitions` associated with table with `table-name` in `dbdef`."
+  [dbdef :- DatabaseDefinition, table-name :- su/NonBlankString]
+  (:field-definitions (tabledef-with-name dbdef table-name)))
+
+(s/defn ^:private tabledef->id->row :- {su/IntGreaterThanZero {su/NonBlankString s/Any}}
+  [{:keys [field-definitions rows]} :- TableDefinition]
+  (let [field-names (map :field-name field-definitions)]
+    (into {} (for [[i values] (m/indexed rows)]
+               [(inc i) (zipmap field-names values)]))))
+
+(s/defn ^:private dbdef->table->id->row :- {su/NonBlankString {su/IntGreaterThanZero {su/NonBlankString s/Any}}}
   "Return a map of table name -> map of row ID -> map of column key -> value."
-  [^DatabaseDefinition dbdef]
-  (into {} (for [{:keys [table-name field-definitions rows]} (:table-definitions dbdef)]
-             {table-name (let [field-names (map :field-name field-definitions)]
-                           (->> rows
-                                (map (partial zipmap field-names))
-                                (map-indexed (fn [i row]
-                                               {(inc i) row}))
-                                (into {})))})))
-
-(defn- nest-fielddefs [^DatabaseDefinition dbdef, ^String table-name]
+  [{:keys [table-definitions]} :- DatabaseDefinition]
+  (into {} (for [{:keys [table-name] :as tabledef} table-definitions]
+             [table-name (tabledef->id->row tabledef)])))
+
+(s/defn ^:private nest-fielddefs
+  [dbdef :- DatabaseDefinition, table-name :- su/NonBlankString]
   (let [nest-fielddef (fn nest-fielddef [{:keys [fk field-name], :as fielddef}]
                         (if-not fk
                           [fielddef]
                           (let [fk (name fk)]
-                            (for [nested-fielddef (mapcat nest-fielddef (get-fielddefs dbdef fk))]
+                            (for [nested-fielddef (mapcat nest-fielddef (fielddefs-for-table-with-name dbdef fk))]
                               (update nested-fielddef :field-name (partial vector field-name fk))))))]
-    (mapcat nest-fielddef (get-fielddefs dbdef table-name))))
+    (mapcat nest-fielddef (fielddefs-for-table-with-name dbdef table-name))))
 
-(defn- flatten-rows [^DatabaseDefinition dbdef, ^String table-name]
+(s/defn ^:private flatten-rows [dbdef :- DatabaseDefinition, table-name :- su/NonBlankString]
   (let [nested-fielddefs (nest-fielddefs dbdef table-name)
-        table->id->k->v  (dbdef->table->id->k->v dbdef)
+        table->id->k->v  (dbdef->table->id->row dbdef)
         resolve-field    (fn resolve-field [table id field-name]
                            (if (string? field-name)
                              (get-in table->id->k->v [table id field-name])
                              (let [[fk-from-name fk-table fk-dest-name] field-name
                                    fk-id                                (get-in table->id->k->v [table id fk-from-name])]
                                (resolve-field fk-table fk-id fk-dest-name))))]
-    (for [id (range 1 (inc (count (:rows (get-tabledef dbdef table-name)))))]
+    (for [id (range 1 (inc (count (:rows (tabledef-with-name dbdef table-name)))))]
       (for [{:keys [field-name]} nested-fielddefs]
         (resolve-field table-name id field-name)))))
 
@@ -468,23 +577,33 @@
           (str/replace #"s$" "")
           (str  \_ (flatten-field-name fk-dest-name))))))
 
-(defn flatten-dbdef
-  "Create a flattened version of DBDEF by following resolving all FKs and flattening all rows into the table with
-  TABLE-NAME."
-  [^DatabaseDefinition dbdef, ^String table-name]
-  (create-database-definition (:database-name dbdef)
-    [table-name
-     (for [fielddef (nest-fielddefs dbdef table-name)]
-       (update fielddef :field-name flatten-field-name))
-     (flatten-rows dbdef table-name)]))
+(s/defn flattened-dataset-definition
+  "Create a flattened version of `dbdef` by following resolving all FKs and flattening all rows into the table with
+  `table-name`. For use with timeseries databases like Druid."
+  [dataset-definition, table-name :- su/NonBlankString]
+  (transformed-dataset-definition table-name dataset-definition
+    (fn [dbdef]
+      (assoc dbdef
+        :table-definitions
+        [(map->TableDefinition
+          {:table-name        table-name
+           :field-definitions (for [fielddef (nest-fielddefs dbdef table-name)]
+                                (update fielddef :field-name flatten-field-name))
+           :rows              (flatten-rows dbdef table-name)})]))))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                 Test Env Vars                                                  |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defn db-test-env-var
-  "Look up test environment var `:ENV-VAR` for the given `:DATABASE-NAME` containing connection related parameters.
+  "Look up test environment var `env-var` for the given `driver` containing connection related parameters.
   If no `:default` param is specified and the var isn't found, throw.
 
      (db-test-env-var :mysql :user) ; Look up `MB_MYSQL_TEST_USER`"
   ([driver env-var]
    (db-test-env-var driver env-var nil))
+
   ([driver env-var default]
    (get env
         (keyword (format "mb-%s-test-%s" (name driver) (name env-var)))
diff --git a/test/metabase/timeseries_query_processor_test/util.clj b/test/metabase/timeseries_query_processor_test/util.clj
index b806d917fa31263f759ca3c6274d2b96324f9e5f..0d7f552d7c92b2c0fa1a9b69e1ac8cb6bbb38766 100644
--- a/test/metabase/timeseries_query_processor_test/util.clj
+++ b/test/metabase/timeseries_query_processor_test/util.clj
@@ -4,16 +4,14 @@
             [metabase.test.data
              [dataset-definitions :as defs]
              [datasets :as datasets]
-             [interface :as tx]]
-            [metabase.util :as u]))
+             [interface :as tx]]))
 
 (def event-based-dbs
   #{:druid})
 
-(def flattened-db-def
-  "The normal test-data DB definition as a flattened, single-table DB definition. (This is a function rather than a
-  straight delay because clojure complains when they delay gets embedding in expanded macros)"
-  (delay (tx/flatten-dbdef defs/test-data "checkins")))
+(def ^:private flattened-db-def
+  "The normal test-data DB definition as a flattened, single-table DB definition."
+  (tx/flattened-dataset-definition defs/test-data "checkins"))
 
 ;; force loading of the flattened db definitions for the DBs that need it
 (defn- load-event-based-db-data!
@@ -21,15 +19,15 @@
   []
   (doseq [driver event-based-dbs]
     (datasets/with-driver-when-testing driver
-      (data/do-with-temp-db @flattened-db-def (constantly nil)))))
+      (data/do-with-db-for-dataset flattened-db-def (constantly nil)))))
 
 (defn do-with-flattened-dbdef
-  "Execute F with a flattened version of the test data DB as the current DB def."
+  "Execute `f` with a flattened version of the test data DB as the current DB def."
   [f]
-  (data/do-with-temp-db @flattened-db-def (u/drop-first-arg f)))
+  (data/do-with-db-for-dataset flattened-db-def (fn [_] (f))))
 
 (defmacro with-flattened-dbdef
-  "Execute BODY using the flattened test data DB definition."
+  "Execute `body` using the flattened test data DB definition."
   [& body]
   `(do-with-flattened-dbdef (fn [] ~@body)))