diff --git a/.gitignore b/.gitignore
index 865a72f456cc15508767d346bf8316785519b551..fb81b89169037202c88c95e88cf9001f7291cce2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 *.class
-*.cljc.rej
+*.*.rej
 *.iml
 *.jar
 *.log
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index b76dcec345912c1961e03988062f44964b741904..2f05d03e5923d69f24eb5d0434d4940307efd2ea 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -389,7 +389,7 @@
   "Create a `(defroutes routes ...)` form that automatically includes all functions created with `defendpoint` in the
   current namespace. Optionally specify middleware that will apply to all of the endpoints in the current namespace.
 
-     (api/define-routes api/+check-superuser) ; all API endpoints in this namespace will require superuser access"
+    (api/define-routes api/+check-superuser) ; all API endpoints in this namespace will require superuser access"
   {:style/indent 0}
   [& middleware]
   (let [api-route-fns (namespace->api-route-fns *ns*)
@@ -404,7 +404,7 @@
 (defn +check-superuser
   "Wrap a Ring handler to make sure the current user is a superuser before handling any requests.
 
-     (api/+check-superuser routes)"
+    (api/+check-superuser routes)"
   [handler]
   (fn
     ([request]
@@ -444,9 +444,9 @@
    (read-check (apply t2/select-one entity :id id other-conditions))))
 
 (defn write-check
-  "Check whether we can write an existing OBJ, or ENTITY with ID.
-   If the object doesn't exist, throw a 404; if we don't have proper permissions, throw a 403.
-   This will fetch the object if it was not already fetched, and returns OBJ if the check is successful."
+  "Check whether we can write an existing `obj`, or `entity` with `id`. If the object doesn't exist, throw a 404; if we
+  don't have proper permissions, throw a 403. This will fetch the object if it was not already fetched, and returns
+  `obj` if the check is successful."
   {:style/indent 2}
   ([obj]
    (check-404 obj)
diff --git a/src/metabase/lib/card.cljc b/src/metabase/lib/card.cljc
index 07d220743396a396f7411071f8cac60c2b0e80a0..4f9df7f7b92ae9bf4fca31e1499ea85ad2168cc0 100644
--- a/src/metabase/lib/card.cljc
+++ b/src/metabase/lib/card.cljc
@@ -1,6 +1,5 @@
 (ns metabase.lib.card
   (:require
-   [metabase.lib.convert :as lib.convert]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.query :as lib.query]
@@ -50,7 +49,7 @@
   [metadata-providerable :- lib.metadata/MetadataProviderable
    card-query            :- :map]
   (when (some? card-query)
-    (lib.metadata.calculation/returned-columns (lib.query/query metadata-providerable (lib.convert/->pMBQL card-query)))))
+    (lib.metadata.calculation/returned-columns (lib.query/query metadata-providerable card-query))))
 
 (def ^:private Card
   [:map
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index 4c988f823b224743712f52c25db94b17825ffe2b..949f68592e88e7d6efaec6768e0e79aac3c5ee92 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -248,18 +248,20 @@
  [lib.metric
   available-metrics]
  [lib.native
+  engine
+  extract-template-tags
+  has-write-permission
+  native-extras
   native-query
   raw-native-query
-  with-native-query
-  template-tags
-  engine
-  with-template-tags
   required-native-extras
-  native-extras
-  with-native-extras
+  template-tag-card-ids
+  template-tags-referenced-cards
+  template-tags
   with-different-database
-  has-write-permission
-  extract-template-tags]
+  with-native-extras
+  with-native-query
+  with-template-tags]
  [lib.order-by
   change-direction
   order-by
diff --git a/src/metabase/lib/limit.cljc b/src/metabase/lib/limit.cljc
index ae0c31ba55ce5eeab308707ba2840842ba045a4d..36eb165a5f94bbc196697b6d0a7dcd0a55400de1 100644
--- a/src/metabase/lib/limit.cljc
+++ b/src/metabase/lib/limit.cljc
@@ -2,7 +2,6 @@
   (:require
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
-   [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.util :as lib.util]
    [metabase.shared.util.i18n :as i18n]
    [metabase.util.malli :as mu]))
@@ -19,13 +18,13 @@
 
   ([query        :- ::lib.schema/query
     stage-number :- :int
-    n            :- [:maybe ::lib.schema.common/positive-int]]
+    n            :- [:maybe pos-int?]]
    (lib.util/update-query-stage query stage-number (fn [stage]
                                                      (if n
                                                        (assoc stage :limit n)
                                                        (dissoc stage :limit))))))
 
-(mu/defn ^:export current-limit :- [:maybe ::lib.schema.common/positive-int]
+(mu/defn ^:export current-limit :- [:maybe pos-int?]
   "Get the maximum number of rows to be returned by a stage of a query. `nil` indicates there is no limit"
   ([query :- ::lib.schema/query]
    (current-limit query -1))
diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc
index dbfd70d89f07d7aa03e4ef173302a4f3a8a05b06..79fdeaf0a194822ab27228720d1170c11eb17af9 100644
--- a/src/metabase/lib/metadata.cljc
+++ b/src/metabase/lib/metadata.cljc
@@ -6,6 +6,7 @@
    [metabase.lib.schema.id :as lib.schema.id]
    [metabase.lib.schema.metadata :as lib.schema.metadata]
    [metabase.lib.util :as lib.util]
+   [metabase.shared.util.i18n :as i18n]
    [metabase.util.malli :as mu]))
 
 ;;; TODO -- deprecate all the schemas below, and just use the versions in [[lib.schema.metadata]] instead.
@@ -110,39 +111,7 @@
 
 ;;;; Stage metadata
 
-(def StageMetadata
-  "Metadata about the columns returned by a particular stage of a pMBQL query. For example a single-stage native query
-  like
-
-    {:database 1
-     :lib/type :mbql/query
-     :stages   [{:lib/type :mbql.stage/mbql
-                 :native   \"SELECT id, name FROM VENUES;\"}]}
-
-  might have stage metadata like
-
-    {:columns [{:name \"id\", :base-type :type/Integer}
-               {:name \"name\", :base-type :type/Text}]}
-
-  associated with the query's lone stage.
-
-  At some point in the near future we will hopefully attach this metadata directly to each stage in a query, so a
-  multi-stage query will have `:lib/stage-metadata` for each stage. The main goal is to facilitate things like
-  returning lists of visible or filterable columns for a given stage of a query. This is TBD, see #28717 for a WIP
-  implementation of this idea.
-
-  This is the same format as the results metadata returned with QP results in `data.results_metadata`. The `:columns`
-  portion of this (`data.results_metadata.columns`) is also saved as `Card.result_metadata` for Saved Questions.
-
-  Note that queries currently actually come back with both `data.results_metadata` AND `data.cols`; it looks like the
-  Frontend actually *merges* these together -- see `applyMetadataDiff` in
-  `frontend/src/metabase/query_builder/selectors.js` -- but this is ridiculous. Let's try to merge anything missing in
-  `results_metadata` into `cols` going forward so things don't need to be manually merged in the future."
-  [:map
-   [:lib/type [:= :metadata/results]]
-   [:columns [:sequential ColumnMetadata]]])
-
-(mu/defn stage :- [:maybe StageMetadata]
+(mu/defn stage :- [:maybe ::lib.schema.metadata/stage]
   "Get metadata associated with a particular `stage-number` of the query, if any. `stage-number` can be a negative
   index.
 
@@ -227,3 +196,74 @@
                         ;; Couldn't import and use `lib.native/has-write-permissions` here due to a circular dependency
                         ;; TODO Find a way to unify has-write-permissions and this function?
                         (= :write (:native-permissions (database query)))))))))
+
+(mu/defn fetch-bulk-metadata-with-non-bulk-provider :- [:maybe [:sequential :map]]
+  "Adapter to use a non-BulkMetadataProvider like one by calling the single-instance methods repeatedly. This is
+  mostly useful for mock metadata providers and the like; the only metadata provider where the performance boost
+  from [[bulk-metadata]] is important, the application database MetadataProvider, implements `BulkMetadata` natively."
+  [provider      :- ::lib.schema.metadata/metadata-provider
+   metadata-type :- [:enum :metadata/card :metadata/column :metadata/metric :metadata/segment :metadata/table]
+   ids           :- [:maybe
+                     [:or
+                      [:set pos-int?]
+                      [:sequential pos-int?]]]]
+  (let [f (case metadata-type
+            :metadata/card    lib.metadata.protocols/card
+            :metadata/column  lib.metadata.protocols/field
+            :metadata/metric  lib.metadata.protocols/metric
+            :metadata/segment lib.metadata.protocols/segment
+            :metadata/table   lib.metadata.protocols/table)]
+    (into []
+          (keep (fn [id]
+                  (f provider id)))
+          ids)))
+
+(mu/defn bulk-metadata :- [:maybe [:sequential [:map
+                                                [:lib/type :keyword]
+                                                [:id pos-int?]]]]
+  "Fetch multiple objects in bulk. If our metadata provider is a bulk provider (e.g., the application database
+  metadata provider), does a single fetch with [[lib.metadata.protocols/bulk-metadata]] if not (i.e., if this is a
+  mock provider), fetches them with repeated calls to the appropriate single-object method,
+  e.g. [[lib.metadata.protocols/field]].
+
+  The order of the returned objects will match the order of `ids`, but does check that all objects are returned. If
+  you want that behavior, use [[bulk-metadata-or-throw]] instead.
+
+  This can also be called for side-effects to warm the cache."
+  [metadata-providerable :- ::lib.schema.metadata/metadata-providerable
+   metadata-type         :- [:enum :metadata/card :metadata/column :metadata/metric :metadata/segment :metadata/table]
+   ids                   :- [:maybe [:or [:sequential pos-int?] [:set pos-int?]]]]
+  (when-let [ids (not-empty (cond-> ids
+                              (not (set? ids)) distinct))] ; remove duplicates but preserve order.
+    (let [provider   (->metadata-provider metadata-providerable)
+          f          (if (satisfies? lib.metadata.protocols/BulkMetadataProvider provider)
+                       lib.metadata.protocols/bulk-metadata
+                       fetch-bulk-metadata-with-non-bulk-provider)
+          results    (f provider metadata-type ids)
+          id->result (into {} (map (juxt :id identity)) results)]
+      (into []
+            (comp (map id->result)
+                  (filter some?))
+            ids))))
+
+(defn- missing-bulk-metadata-error [metadata-type id]
+  (ex-info (i18n/tru "Failed to fetch {0} {1}: either it does not exist, or it belongs to a different Database"
+                     (pr-str metadata-type)
+                     (pr-str id))
+           {:status-code   400
+            :metadata-type metadata-type
+            :id            id}))
+
+(mu/defn bulk-metadata-or-throw :- [:maybe [:sequential [:map
+                                                         [:lib/type :keyword]
+                                                         [:id pos-int?]]]]
+  "Like [[bulk-metadata]], but verifies that all the requested objects were returned; throws an Exception otherwise."
+  [metadata-providerable :- ::lib.schema.metadata/metadata-providerable
+   metadata-type         :- [:enum :metadata/card :metadata/column :metadata/metric :metadata/segment :metadata/table]
+   ids                   :- [:maybe [:or [:sequential pos-int?] [:set pos-int?]]]]
+  (let [results     (bulk-metadata metadata-providerable metadata-type ids)
+        fetched-ids (into #{} (keep :id) results)]
+    (doseq [id ids]
+      (when-not (contains? fetched-ids id)
+        (throw (missing-bulk-metadata-error metadata-type id))))
+    results))
diff --git a/src/metabase/lib/metadata/cached_provider.cljc b/src/metabase/lib/metadata/cached_provider.cljc
index 1ae579871a17be43a63302741301edfc33ce36ab..d0c14933ee29184fa16a827983d9aee858f611d1 100644
--- a/src/metabase/lib/metadata/cached_provider.cljc
+++ b/src/metabase/lib/metadata/cached_provider.cljc
@@ -3,7 +3,6 @@
    [clojure.set :as set]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
-   [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.metadata :as lib.schema.metadata]
    [metabase.util :as u]
    [metabase.util.log :as log]
@@ -34,7 +33,7 @@
 (mu/defn ^:private store-metadata!
   [cache
    metadata-type :- [:enum :metadata/database :metadata/table :metadata/column :metadata/card :metadata/metric :metadata/segment]
-   id            :- ::lib.schema.common/positive-int
+   id            :- pos-int?
    metadata      :- [:multi
                      {:dispatch :lib/type}
                      [:metadata/database lib.metadata/DatabaseMetadata]
@@ -65,8 +64,11 @@
         ;; TODO -- we should probably store `::nil` markers for things we tried to fetch that didn't exist
         (doseq [instance (lib.metadata.protocols/bulk-metadata uncached-provider metadata-type missing-ids)]
           (store-in-cache! cache [metadata-type (:id instance)] instance))))
-    (for [id ids]
-      (get-in-cache cache [metadata-type id]))))
+    (into []
+          (comp (map (fn [id]
+                       (get-in-cache cache [metadata-type id])))
+                (filter some?))
+          ids)))
 
 (defn- tables [metadata-provider cache]
   (let [fetched-tables #(lib.metadata.protocols/tables metadata-provider)]
diff --git a/src/metabase/lib/metadata/composed_provider.cljc b/src/metabase/lib/metadata/composed_provider.cljc
index aebae90a6355be7f1c1553187484da9d94aa0953..f3059547983028d6aeb2524839f4da29d30a9a83 100644
--- a/src/metabase/lib/metadata/composed_provider.cljc
+++ b/src/metabase/lib/metadata/composed_provider.cljc
@@ -5,10 +5,8 @@
    [clojure.datafy :as datafy]
    [clojure.set :as set]
    [medley.core :as m]
-   [metabase.lib.metadata.protocols :as metadata.protocols]
-   [metabase.lib.schema.common :as lib.schema.common]
-   [metabase.lib.schema.metadata :as lib.schema.metadata]
-   [metabase.util.malli :as mu]))
+   [metabase.lib.metadata :as lib.metadata]
+   [metabase.lib.metadata.protocols :as metadata.protocols]))
 
 (defn- cached-providers [providers]
   (filter #(satisfies? metadata.protocols/CachedMetadataProvider %)
@@ -27,24 +25,7 @@
          (m/distinct-by :id))
         metadata-providers))
 
-(mu/defn fetch-bulk-metadata-with-non-bulk-provider :- [:maybe [:sequential :map]]
-  "Call a non-bulk metadata provider repeatedly to fetch multiple metadatas of the same type."
-  [provider      :- ::lib.schema.metadata/metadata-provider
-   metadata-type :- [:enum :metadata/card :metadata/column :metadata/metric :metadata/segment :metadata/table]
-   ids           :- [:maybe
-                     [:or
-                      [:set ::lib.schema.common/positive-int]
-                      [:sequential ::lib.schema.common/positive-int]]]]
-  (let [f (case metadata-type
-            :metadata/card    metadata.protocols/card
-            :metadata/column  metadata.protocols/field
-            :metadata/metric  metadata.protocols/metric
-            :metadata/segment metadata.protocols/segment
-            :metadata/table   metadata.protocols/table)]
-    (into []
-          (keep (fn [id]
-                  (f provider id)))
-          ids)))
+
 
 (defn- bulk-metadata [providers metadata-type ids]
   (loop [[provider & more-providers] providers, unfetched-ids (set ids), fetched []]
@@ -56,9 +37,7 @@
       fetched
 
       :else
-      (let [newly-fetched     (if (satisfies? metadata.protocols/BulkMetadataProvider provider)
-                                (metadata.protocols/bulk-metadata provider metadata-type unfetched-ids)
-                                (fetch-bulk-metadata-with-non-bulk-provider provider metadata-type unfetched-ids))
+      (let [newly-fetched     (lib.metadata/bulk-metadata provider metadata-type unfetched-ids)
             newly-fetched-ids (into #{} (map :id) newly-fetched)
             unfetched-ids     (set/difference unfetched-ids newly-fetched-ids)]
         (recur more-providers
diff --git a/src/metabase/lib/native.cljc b/src/metabase/lib/native.cljc
index 8375b4447ec6b3f0f58904f9cc74f4baf0557e6b..b0f77112c2ddfd29ac433d6532da4507cf4c51f0 100644
--- a/src/metabase/lib/native.cljc
+++ b/src/metabase/lib/native.cljc
@@ -8,6 +8,8 @@
    [metabase.lib.query :as lib.query]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.common :as common]
+   [metabase.lib.schema.id :as lib.schema.id]
+   [metabase.lib.schema.metadata :as lib.schema.metadata]
    [metabase.lib.schema.template-tag :as lib.schema.template-tag]
    [metabase.lib.util :as lib.util]
    [metabase.shared.util.i18n :as i18n]
@@ -125,7 +127,7 @@
 (mu/defn required-native-extras :- set?
   "Returns the extra keys that are required for this database's native queries, for example `:collection` name is
   needed for MongoDB queries."
-  [metadata-provider :- lib.metadata/MetadataProviderable]
+  [metadata-provider :- ::lib.schema.metadata/metadata-providerable]
   (let [db (lib.metadata/database metadata-provider)]
    (cond-> #{}
     (get-in db [:features :native-requires-specified-collection])
@@ -154,13 +156,13 @@
   "Create a new native query.
 
   Native in this sense means a pMBQL query with a first stage that is a native query."
-  ([metadata-providerable :- lib.metadata/MetadataProviderable
+  ([metadata-providerable :- ::lib.schema.metadata/metadata-providerable
     inner-query :- ::common/non-blank-string]
    (native-query metadata-providerable inner-query nil nil))
 
-  ([metadata-providerable :- lib.metadata/MetadataProviderable
+  ([metadata-providerable :- ::lib.schema.metadata/metadata-providerable
     inner-query :- ::common/non-blank-string
-    results-metadata :- [:maybe lib.metadata/StageMetadata]
+    results-metadata :- [:maybe ::lib.schema.metadata/stage]
     native-extras :- [:maybe ::native-extras]]
    (let [tags (extract-template-tags inner-query)]
      (-> (lib.query/query-with-stages metadata-providerable
@@ -174,10 +176,10 @@
   "Changes the database for this query. The first stage must be a native type.
    Native extras must be provided if the new database requires it."
   ([query :- ::lib.schema/query
-    metadata-provider :- lib.metadata/MetadataProviderable]
+    metadata-provider :- ::lib.schema.metadata/metadata-providerable]
    (with-different-database query metadata-provider nil))
   ([query :- ::lib.schema/query
-    metadata-provider :- lib.metadata/MetadataProviderable
+    metadata-provider :- ::lib.schema.metadata/metadata-providerable
     native-extras :- [:maybe ::native-extras]]
    (assert-native-query! (lib.util/query-stage query 0))
    ;; Changing the database should also clean up template tags, see #31926
@@ -219,11 +221,24 @@
   [query :- ::lib.schema/query]
   (:native (lib.util/query-stage query 0)))
 
-(mu/defn template-tags :- ::lib.schema.template-tag/template-tag-map
+(mu/defn template-tags :- [:maybe ::lib.schema.template-tag/template-tag-map]
   "Returns the native query's template tags"
   [query :- ::lib.schema/query]
   (:template-tags (lib.util/query-stage query 0)))
 
+(mu/defn template-tag-card-ids :- [:maybe [:set {:min 1} ::lib.schema.id/card]]
+  "Returns the card IDs from the template tags of the native query of `query`."
+  [query :- ::lib.schema/query]
+  (not-empty (into #{} (keep (fn [[_k m]] (:card-id m))) (template-tags query))))
+
+(mu/defn template-tags-referenced-cards :- [:maybe [:sequential ::lib.schema.metadata/card]]
+  "Returns Card instances referenced by the given native `query`."
+  [query :- ::lib.schema/query]
+  (mapv
+   (fn [card-id]
+     (lib.metadata/card query card-id))
+   (template-tag-card-ids query)))
+
 (mu/defn has-write-permission :- :boolean
   "Returns whether the database has native write permissions.
    This is only filled in by [[metabase.api.database/add-native-perms-info]]
diff --git a/src/metabase/lib/schema/binning.cljc b/src/metabase/lib/schema/binning.cljc
index 41eea935d96d3e51e9029c0371ad6488b2c8c7d3..f427acd3ac61ee369c1763e463eb972e3ddf98aa 100644
--- a/src/metabase/lib/schema/binning.cljc
+++ b/src/metabase/lib/schema/binning.cljc
@@ -12,7 +12,7 @@
   [:enum :bin-width :default :num-bins])
 
 (mr/def ::num-bins
-  ::lib.schema.common/positive-int)
+  pos-int?)
 
 (mr/def ::bin-width
   ::lib.schema.common/positive-number)
diff --git a/src/metabase/lib/schema/common.cljc b/src/metabase/lib/schema/common.cljc
index c6ca30a77b50992ce61cfbf7e1bab5d5e2293e51..986ed1c50ec9688ea57269a9295b19cb6fdbae97 100644
--- a/src/metabase/lib/schema/common.cljc
+++ b/src/metabase/lib/schema/common.cljc
@@ -18,9 +18,6 @@
 (mr/def ::int-greater-than-or-equal-to-zero
   [:int {:min 0}])
 
-(mr/def ::positive-int
-  pos-int?)
-
 (mr/def ::positive-number
   [:fn
    {:error/message "positive number"}
diff --git a/src/metabase/lib/schema/filter.cljc b/src/metabase/lib/schema/filter.cljc
index 735cdbb3d4a240d129c045086841437b16b1215f..67077e9c2820f6d8275427ca80f50a9424130832 100644
--- a/src/metabase/lib/schema/filter.cljc
+++ b/src/metabase/lib/schema/filter.cljc
@@ -117,7 +117,7 @@
   [:tuple
    [:= :segment]
    ::common/options
-   [:or ::common/positive-int ::common/non-blank-string]])
+   [:or pos-int? ::common/non-blank-string]])
 
 (mr/def ::operator
   [:map
diff --git a/src/metabase/lib/schema/id.cljc b/src/metabase/lib/schema/id.cljc
index a6ea94377dc2c5f36b5fb22309fb1e53ba67ad23..78f72841dd3cec79d30136f7a2741843a99fe90f 100644
--- a/src/metabase/lib/schema/id.cljc
+++ b/src/metabase/lib/schema/id.cljc
@@ -1,13 +1,12 @@
 (ns metabase.lib.schema.id
   (:require
-   [metabase.lib.schema.common :as common]
    [metabase.util.malli.registry :as mr]))
 
 ;;; these aren't anything special right now, but maybe in the future we can do something special/intelligent with
 ;;; them, e.g. when we start working on the generative stuff
 
 (mr/def ::database
-  ::common/positive-int)
+  pos-int?)
 
 (def saved-questions-virtual-database-id
   "The ID used to signify that a database is 'virtual' rather than physical.
@@ -30,22 +29,22 @@
   [:= saved-questions-virtual-database-id])
 
 (mr/def ::table
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::field
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::card
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::segment
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::metric
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::snippet
-  ::common/positive-int)
+  pos-int?)
 
 (mr/def ::dimension
-  ::common/positive-int)
+  pos-int?)
diff --git a/src/metabase/lib/schema/metadata.cljc b/src/metabase/lib/schema/metadata.cljc
index 181088a00c136176f3114d19f366515fc37e3e2c..797bdceb5d84fa95d8fbb216c3b6a13aef928814 100644
--- a/src/metabase/lib/schema/metadata.cljc
+++ b/src/metabase/lib/schema/metadata.cljc
@@ -300,3 +300,36 @@
    [:map
     {:error/message "map with a MetadataProvider in the key :lib/metadata (i.e. a query)"}
     [:lib/metadata [:ref ::metadata-provider]]]])
+
+;;; Metadata about the columns returned by a particular stage of a pMBQL query. For example a single-stage native
+;;; query like
+;;;
+;;;    {:database 1
+;;;     :lib/type :mbql/query
+;;;     :stages   [{:lib/type :mbql.stage/mbql
+;;;                 :native   "SELECT id, name FROM VENUES;"}]}
+;;;
+;;; might have stage metadata like
+;;;
+;;;    {:columns [{:name "id", :base-type :type/Integer}
+;;;               {:name "name", :base-type :type/Text}]}
+;;;
+;;; associated with the query's lone stage.
+;;;
+;;; At some point in the near future we will hopefully attach this metadata directly to each stage in a query, so a
+;;; multi-stage query will have `:lib/stage-metadata` for each stage. The main goal is to facilitate things like
+;;; returning lists of visible or filterable columns for a given stage of a query. This is TBD, see #28717 for a WIP
+;;; implementation of this idea.
+;;;
+;;; This is the same format as the results metadata returned with QP results in `data.results_metadata`. The
+;;; `:columns` portion of this (`data.results_metadata.columns`) is also saved as `Card.result_metadata` for Saved
+;;; Questions.
+;;;
+;;; Note that queries currently actually come back with both `data.results_metadata` AND `data.cols`; it looks like
+;;; the Frontend actually *merges* these together -- see `applyMetadataDiff` in
+;;; `frontend/src/metabase/query_builder/selectors.js` -- but this is ridiculous. Let's try to merge anything missing
+;;; in `results_metadata` into `cols` going forward so things don't need to be manually merged in the future.
+(mr/def ::stage
+  [:map
+   [:lib/type [:= :metadata/results]]
+   [:columns [:sequential ::column]]])
diff --git a/src/metabase/mbql/schema.cljc b/src/metabase/mbql/schema.cljc
index a987f6c46d10ee62656de2551d2ed27abdf13dba..ba877419b890aafefb3596235fc132fef8c3b53f 100644
--- a/src/metabase/mbql/schema.cljc
+++ b/src/metabase/mbql/schema.cljc
@@ -42,7 +42,7 @@
   [:ref ::lib.schema.common/semantic-or-relation-type])
 
 (def ^:private PositiveInt
-  [:ref ::lib.schema.common/positive-int])
+  pos-int?)
 
 (def ^:private IntGreaterThanOrEqualToZero
   [:ref ::lib.schema.common/int-greater-than-or-equal-to-zero])
diff --git a/src/metabase/mbql/util.cljc b/src/metabase/mbql/util.cljc
index 496b55d18cad808473befa4c817652650e71c9e0..30ce299d3551edbd918eea188bc89b51dc75d038 100644
--- a/src/metabase/mbql/util.cljc
+++ b/src/metabase/mbql/util.cljc
@@ -349,7 +349,7 @@
   [filter-clause :- mbql.s/Filter]
   (-> filter-clause desugar-filter-clause negate* simplify-compound-filter))
 
-(mu/defn query->source-table-id :- [:maybe ::lib.schema.common/positive-int]
+(mu/defn query->source-table-id :- [:maybe pos-int?]
   "Return the source Table ID associated with `query`, if applicable; handles nested queries as well. If `query` is
   `nil`, returns `nil`.
 
@@ -382,7 +382,7 @@
     :else
     source-table-id))
 
-(mu/defn join->source-table-id :- [:maybe ::lib.schema.common/positive-int]
+(mu/defn join->source-table-id :- [:maybe pos-int?]
   "Like `query->source-table-id`, but for a join."
   [join]
   (query->source-table-id {:type :query, :query join}))
diff --git a/src/metabase/models/data_permissions.clj b/src/metabase/models/data_permissions.clj
index bd41439f2ee3f0016c6cf6e605af14df11e2a290..c514b91c10f884939a96a39fe83999617c7fbb23 100644
--- a/src/metabase/models/data_permissions.clj
+++ b/src/metabase/models/data_permissions.clj
@@ -579,7 +579,7 @@
   being a table-level permission."
   [group-or-id :- TheIdable
    db-or-id    :- TheIdable
-   perm-type   :- :keyword
+   perm-type   :- PermissionType
    value       :- :keyword]
   (t2/with-transaction [_conn]
     (let [group-id (u/the-id group-or-id)
@@ -604,7 +604,7 @@
   is removed and table-level rows are are added for all of its tables. Similarly, if setting a table-level permission to a value
   that results in all of the database's tables having the same permission, it is replaced with a single database-level row."
   [group-or-id :- TheIdable
-   perm-type   :- :keyword
+   perm-type   :- PermissionType
    table-perms :- [:map-of TheIdable :keyword]]
   (when (not= :model/Table (model-by-perm-type perm-type))
     (throw (ex-info (tru "Permission type {0} cannot be set on tables." perm-type)
@@ -684,7 +684,7 @@
   "Sets permissions for a single table to the specified value for a given group."
   [group-or-id :- TheIdable
    table-or-id :- TheIdable
-   perm-type   :- :keyword
+   perm-type   :- PermissionType
    value       :- :keyword]
   (set-table-permissions! group-or-id perm-type {table-or-id value}))
 
@@ -711,7 +711,7 @@
   Otherwise, the new table permission is added with the provided value."
   [groups-or-ids :- [:sequential TheIdable]
    table-or-id   :- TheIdable
-   perm-type     :- :keyword
+   perm-type     :- PermissionType
    value         :- :keyword]
   (when (not= :model/Table (model-by-perm-type perm-type))
     (throw (ex-info (tru "Permission type {0} cannot be set on tables." perm-type)
diff --git a/src/metabase/models/params/chain_filter.clj b/src/metabase/models/params/chain_filter.clj
index 5f2838ab9aaa95ddd4ece513f61e6ae895496cff..9e1cfa5b6fbc4018fe99313a17a84f80574439e3 100644
--- a/src/metabase/models/params/chain_filter.clj
+++ b/src/metabase/models/params/chain_filter.clj
@@ -80,6 +80,7 @@
    [metabase.query-processor :as qp]
    [metabase.query-processor.middleware.permissions :as qp.perms]
    [metabase.query-processor.preprocess :as qp.preprocess]
+   [metabase.query-processor.setup :as qp.setup]
    [metabase.types :as types]
    [metabase.util :as u]
    [metabase.util.i18n :refer [tru]]
@@ -526,9 +527,11 @@
 (defn- check-field-value-query-permissions
   "Check query permissions against the chain-filter-mbql-query (private #196)"
   [field-id constraints options]
-  (->> (chain-filter-mbql-query field-id constraints options)
-       qp.preprocess/preprocess
-       qp.perms/check-query-permissions*))
+  (let [query (chain-filter-mbql-query field-id constraints options)]
+    (qp.setup/with-qp-setup [query query]
+      (->> query
+           qp.preprocess/preprocess
+           qp.perms/check-query-permissions*))))
 
 (defn- cached-field-values [field-id constraints {:keys [limit]}]
   ;; TODO: why don't we remap the human readable values here?
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index ecf1ef8d17bd9806429367857ae3c5579703eb70..e516f2b5126254cc7b435e314cdf741f93031d75 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -267,7 +267,11 @@
                                               [:id (when (premium-features/enable-advanced-permissions?)
                                                      :is_group_manager)]))]
       (for [user users]
-        (assoc user :user_group_memberships (map membership->group (user-id->memberships (u/the-id user))))))))
+        (assoc user :user_group_memberships (->> (user-id->memberships (u/the-id user))
+                                                 (map membership->group)
+                                                 ;; sort these so the id returned is consistent so our tests don't
+                                                 ;; randomly fail
+                                                 (sort-by :id)))))))
 
 (mi/define-batched-hydration-method add-group-ids
   :group_ids
diff --git a/src/metabase/query_processor/middleware/convert_to_legacy.clj b/src/metabase/query_processor/middleware/convert_to_legacy.clj
deleted file mode 100644
index b3ef95a8a418971e72345dce60172517f10d64e5..0000000000000000000000000000000000000000
--- a/src/metabase/query_processor/middleware/convert_to_legacy.clj
+++ /dev/null
@@ -1,12 +0,0 @@
-(ns metabase.query-processor.middleware.convert-to-legacy
-  (:require
-   [metabase.lib.convert :as lib.convert]
-   [metabase.mbql.schema :as mbql.s]
-   [metabase.util.malli :as mu]))
-
-(mu/defn convert-to-legacy :- mbql.s/Query
-  "Middleware that converts and MLv2 query back to a legacy query. This is temporary until we concert the entire QP to
-  use MLv2 everywhere."
-  [query :- :map]
-  (cond-> query
-    (= (:lib/type query) :mbql/query) lib.convert/->legacy-MBQL))
diff --git a/src/metabase/query_processor/middleware/expand_macros.clj b/src/metabase/query_processor/middleware/expand_macros.clj
index 47e282be0e826ac59052e44696d296e3787b8dcd..83408dd66e136eda93e40f407df8544758149c0b 100644
--- a/src/metabase/query_processor/middleware/expand_macros.clj
+++ b/src/metabase/query_processor/middleware/expand_macros.clj
@@ -1,206 +1,194 @@
 (ns metabase.query-processor.middleware.expand-macros
-  "Middleware for expanding `:metric` and `:segment` 'macros' in *unexpanded* MBQL queries.
+  "Middleware for expanding LEGACY `:metric` and `:segment` 'macros' in *unexpanded* MBQL queries.
 
   (`:metric` forms are expanded into aggregations and sometimes filter clauses, while `:segment` forms are expanded
-  into filter clauses.)
-
-   TODO - this namespace is ancient and written with MBQL '95 in mind, e.g. it is case-sensitive.
-   At some point this ought to be reworked to be case-insensitive and cleaned up."
+  into filter clauses.)"
   (:require
-   [malli.core :as mc]
-   [malli.error :as me]
-   [metabase.mbql.schema :as mbql.s]
-   [metabase.mbql.schema.helpers :as helpers]
+   [metabase.lib.convert :as lib.convert]
+   [metabase.lib.filter :as lib.filter]
+   [metabase.lib.metadata :as lib.metadata]
+   [metabase.lib.options :as lib.options]
+   [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.aggregation :as lib.schema.aggregation]
+   [metabase.lib.schema.expression :as lib.schema.expression]
+   [metabase.lib.schema.metadata :as lib.schema.metadata]
+   [metabase.lib.util :as lib.util]
+   [metabase.lib.walk :as lib.walk]
    [metabase.mbql.util :as mbql.u]
    [metabase.query-processor.error-type :as qp.error-type]
-   [metabase.query-processor.store :as qp.store]
-   [metabase.util.i18n :refer [trs tru]]
+   [metabase.util :as u]
+   [metabase.util.i18n :refer [tru]]
    [metabase.util.log :as log]
    [metabase.util.malli :as mu]
-   [metabase.util.malli.schema :as ms]))
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                    SEGMENTS                                                    |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-(defn- segment-clauses->id->definition [segment-clauses]
-  (when-let [segment-ids (not-empty (into #{}
-                                          (comp (map second)
-                                                (filter integer?))
-                                          segment-clauses))]
-    (into {}
-          (map (juxt :id :definition))
-          (qp.store/bulk-metadata :metadata/segment segment-ids))))
-
-(defn- replace-segment-clauses [outer-query segment-id->definition]
-  (mbql.u/replace-in outer-query [:query]
-    [:segment (segment-id :guard (complement mbql.u/ga-id?))]
-    (or (:filter (segment-id->definition segment-id))
-        (throw (IllegalArgumentException. (tru "Segment {0} does not exist, or is invalid." segment-id))))))
-
-(mu/defn ^:private expand-segments :- mbql.s/Query
-  "Recursively expand segments in the `query`."
-  [query :- mbql.s/Query]
-  (loop [{inner-query :query :as outer-query} query
-         depth 0]
-    (if-let [segments (mbql.u/match inner-query [:segment (_ :guard (complement mbql.u/ga-id?))])]
-      (let [segment-id->definition (segment-clauses->id->definition segments)
-            expanded-query (replace-segment-clauses outer-query segment-id->definition)]
-        ;; Following line is in place to avoid infinite recursion caused by mutually recursive
-        ;; segment definitions or other unforseen circumstances. Number 41 is arbitrary.
-        (if (or (= expanded-query outer-query) (= depth 41))
-          (throw (ex-info (tru "Segment expansion failed. Check mutually recursive segment definitions.")
-                          {:type qp.error-type/invalid-query
-                           :original-query query
-                           :expanded-query expanded-query
-                           :segment-id->definition segment-id->definition
-                           :depth depth}))
-          (recur expanded-query (inc depth))))
-      outer-query)))
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                    METRICS                                                     |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-(defn- metrics
-  "Return a sequence of any (non-GA) `:metric` MBQL clauses in `query`."
-  [query]
-  ;; metrics won't be in a native query but they could be in source-query or aggregation clause
-  (mbql.u/match query [:metric (_ :guard (complement mbql.u/ga-id?))]))
-
-(def ^:private MetricInfo
-  [:map
-   [:id         ms/PositiveInt]
-   [:name       ms/NonBlankString]
-   [:definition [:map
-                 [:aggregation [:tuple mbql.s/Aggregation]]
-                 [:filter {:optional true} [:maybe mbql.s/Filter]]]]])
-
-(defn- metric-info-validation-errors [metric-info]
-  (me/humanize (mc/explain MetricInfo metric-info)))
-
-(mu/defn ^:private metric-clauses->id->info :- [:map-of ms/PositiveInt MetricInfo]
-  [metric-clauses :- [:sequential mbql.s/metric]]
-  (when-let [metric-ids (not-empty (into #{} (map second) metric-clauses))]
-    (into {}
-          (comp (remove (fn [metric]
-                          (when-let [errors (metric-info-validation-errors metric)]
-                            (log/warn (trs "Invalid metric: {0} reason: {1}" metric errors))
-                            errors)))
-                (map (juxt :id #(select-keys % [:id :name :definition]))))
-          (qp.store/bulk-metadata :metadata/metric metric-ids))))
-
-(mu/defn ^:private add-metrics-filters-this-level :- mbql.s/MBQLQuery
-  [inner-query                :- mbql.s/MBQLQuery
-   this-level-metric-id->info :- [:map-of ms/PositiveInt MetricInfo]]
-  (let [filters (for [{{filter-clause :filter} :definition} (vals this-level-metric-id->info)
-                      :when filter-clause]
-                  filter-clause)]
-    (reduce mbql.u/add-filter-clause-to-inner-query inner-query filters)))
-
-(mu/defn ^:private metric-info->ag-clause :- mbql.s/Aggregation
-  "Return an appropriate aggregation clause from `metric-info`."
-  [{{[aggregation] :aggregation} :definition, metric-name :name} :- MetricInfo
-   {:keys [use-metric-name-as-display-name?]}                    :- [:map [:use-metric-name-as-display-name? :boolean]]]
-  (if-not use-metric-name-as-display-name?
-    aggregation
-    ;; try to give the resulting aggregation the name of the Metric it came from, unless it already has a display
-    ;; name in which case keep that name
-    (mbql.u/match-one aggregation
-      [:aggregation-options _ (_ :guard :display-name)]
-      &match
-
-      [:aggregation-options ag options]
-      [:aggregation-options ag (assoc options :display-name metric-name)]
-
-      _
-      [:aggregation-options &match {:display-name metric-name}])))
-
-(mu/defn ^:private replace-metrics-aggregations-this-level :- mbql.s/MBQLQuery
-  [inner-query                :- mbql.s/MBQLQuery
-   this-level-metric-id->info :- [:map-of ms/PositiveInt MetricInfo]]
-  (letfn [(metric [metric-id]
-            (or (get this-level-metric-id->info metric-id)
-                (throw (ex-info (tru "Metric {0} does not exist, or is invalid." metric-id)
-                                {:type   :invalid-query
-                                 :metric metric-id
-                                 :query  inner-query}))))]
-    (mbql.u/replace-in inner-query [:aggregation]
-      ;; if metric is wrapped in aggregation options that give it a display name, expand the metric but do not name it
-      [:aggregation-options [:metric (metric-id :guard (complement mbql.u/ga-id?))] (options :guard :display-name)]
-      [:aggregation-options
-       (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? false})
-       options]
-
-      ;; if metric is wrapped in aggregation options that *do not* give it a display name, expand the metric and then
-      ;; merge the options
-      [:aggregation-options [:metric (metric-id :guard (complement mbql.u/ga-id?))] options]
-      (let [[_ ag ag-options] (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? true})]
-        [:aggregation-options ag (merge ag-options options)])
-
-      ;; otherwise for unwrapped metrics expand them in-place
-      [:metric (metric-id :guard (complement mbql.u/ga-id?))]
-      (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? true}))))
-
-(mu/defn ^:private metric-ids-this-level :- [:maybe [:set ms/PositiveInt]]
-  [inner-query]
-  (when (map? inner-query)
-    (when-let [aggregations (:aggregation inner-query)]
-      (not-empty
-       (set
-        (mbql.u/match aggregations
-          [:metric (metric-id :guard (complement mbql.u/ga-id?))]
-          metric-id))))))
-
-(mu/defn ^:private expand-metrics-clauses-this-level :- [:and
-                                                         mbql.s/MBQLQuery
-                                                         [:fn
-                                                          {:error/message "Inner MBQL query with no :metric clauses at this level"}
-                                                          (complement metric-ids-this-level)]]
-  [inner-query     :- mbql.s/MBQLQuery
-   metric-id->info :- [:map-of ms/PositiveInt MetricInfo]]
-  (let [this-level-metric-ids      (metric-ids-this-level inner-query)
-        this-level-metric-id->info (select-keys metric-id->info this-level-metric-ids)]
-    (-> inner-query
-        (add-metrics-filters-this-level this-level-metric-id->info)
-        (replace-metrics-aggregations-this-level this-level-metric-id->info))))
-
-(mu/defn ^:private expand-metrics-clauses :- ms/Map
-  "Add appropriate `filter` and `aggregation` clauses for a sequence of Metrics.
-
-    (expand-metrics-clauses {:query {}} [[:metric 10]])
-    ;; -> {:query {:aggregation [[:count]], :filter [:= [:field-id 10] 20]}}"
-  [query :- ms/Map metric-id->info :- (helpers/non-empty [:map-of ms/PositiveInt MetricInfo])]
-  (mbql.u/replace query
-    (m :guard metric-ids-this-level)
-    (-> m
-        ;; expand this this level...
-        (expand-metrics-clauses-this-level metric-id->info)
-        ;; then recursively expand things at any other levels.
-        (expand-metrics-clauses metric-id->info))))
-
-(mu/defn ^:private expand-metrics :- mbql.s/Query
-  [query :- mbql.s/Query]
-  (if-let [metrics (metrics query)]
-    (expand-metrics-clauses query (metric-clauses->id->info metrics))
+   [metabase.util.malli.registry :as mr]))
+
+;;; "legacy macro" as used below means EITHER a legacy Metric or a legacy Segment.
+(mr/def ::legacy-macro
+  [:or
+   ::lib.schema.metadata/metric
+   ::lib.schema.metadata/segment])
+
+(mr/def ::macro-type
+  [:enum :metric :segment])
+
+(mu/defn unresolved-legacy-macro-ids :- [:maybe [:set {:min 1} pos-int?]]
+  "Find all the unresolved legacy :metric and :segment references in `query`.
+
+  :metric references only appear in aggregations; :segment references can appear anywhere a boolean expression is
+  allowed, including `:filters`, join conditions, expression aggregations like `:sum-where`, etc."
+  [macro-type :- ::macro-type
+   query      :- ::lib.schema/query]
+  (let [ids (transient #{})]
+    (lib.walk/walk-stages
+     query
+     (fn [_query _path stage]
+       (mbql.u/match stage
+         [macro-type _opts (id :guard pos-int?)]
+         (conj! ids id))))
+    (not-empty (persistent! ids))))
+
+;;; a legacy Metric has exactly one aggregation clause, and possibly one or more filter clauses as well.
+;;;
+;;; a legacy Segment has one or more filter clauses.
+
+(defn- legacy-macro-definition->pMBQL
+  "Get the definition of a legacy Metric as a pMBQL stage."
+  [definition]
+  (log/tracef "Converting legacy MBQL for macro definition from\n%s" (u/pprint-to-str definition))
+  (u/prog1 (-> (lib.convert/->pMBQL {:type  :query
+                                     :query definition})
+               (lib.util/query-stage -1))
+    (log/tracef "to pMBQL\n%s" (u/pprint-to-str <>))))
+
+(mu/defn ^:private legacy-metric-aggregation :- ::lib.schema.aggregation/aggregation
+  "Get the aggregation associated with a legacy Metric."
+  [legacy-metric :- ::lib.schema.metadata/metric]
+  (-> (or (first (get-in legacy-metric [:definition :aggregation]))
+          (throw (ex-info (tru "Invalid legacy Metric: missing aggregation")
+                          {:type qp.error-type/invalid-query, :legacy-metric legacy-metric})))
+      ;; make sure aggregation has a display-name: keep the one attached directly to the aggregation if there is one;
+      ;; otherwise use the Metric's name
+      (lib.options/update-options update :display-name #(or % (:name legacy-metric)))
+      ;; make sure it has fresh UUIDs in case we need to add it to the query more than once (multiple Metric references
+      ;; are possible if the query joins the same source query twice for example)
+      lib.util/fresh-uuids))
+
+(mu/defn ^:private legacy-macro-filters :- [:maybe [:sequential ::lib.schema.expression/boolean]]
+  "Get the filter(s) associated with a legacy Metric or Segment."
+  [legacy-macro :- ::legacy-macro]
+  (mapv lib.util/fresh-uuids
+        (get-in legacy-macro [:definition :filters])))
+
+(mr/def ::id->legacy-macro
+  [:map-of pos-int? ::legacy-macro])
+
+(mu/defn ^:private fetch-legacy-macros :- ::id->legacy-macro
+  [macro-type            :- ::macro-type
+   metadata-providerable :- ::lib.schema.metadata/metadata-providerable
+   legacy-macro-ids      :- [:maybe [:set {:min 1} pos-int?]]]
+  (let [metadata-type     (case macro-type
+                            :metric  :metadata/metric
+                            :segment :metadata/segment)]
+    (u/prog1 (into {}
+                   (map (juxt :id (fn [legacy-macro]
+                                    (update legacy-macro :definition legacy-macro-definition->pMBQL))))
+                   (lib.metadata/bulk-metadata metadata-providerable metadata-type legacy-macro-ids))
+      ;; make sure all the IDs exist.
+      (doseq [id legacy-macro-ids]
+        (or (get <> id)
+            (throw (ex-info (tru "Legacy Metric/Segment {0} does not exist, belongs to a different Database, or is invalid."
+                                 id)
+                            {:type qp.error-type/invalid-query, :macro-type macro-type, :id id})))))))
+
+(defmulti ^:private resolve-legacy-macros-in-stage
+  {:arglists '([macro-type stage id->legacy-macro])}
+  (fn [macro-type _stage _id->legacy-macro]
+    macro-type))
+
+(mu/defmethod resolve-legacy-macros-in-stage :metric :- ::lib.schema/stage
+  [_macro-type       :- [:= :metric]
+   stage             :- ::lib.schema/stage
+   id->legacy-metric :- ::id->legacy-macro]
+  (let [new-filters (atom [])
+        stage'      (mbql.u/replace-in stage [:aggregation]
+                      [:metric opts-from-ref (id :guard pos-int?)]
+                      (let [legacy-metric  (get id->legacy-metric id)
+                            aggregation    (-> (legacy-metric-aggregation legacy-metric)
+                                               ;; preserve the `:name` and `:display-name` from the `:metric` ref itself
+                                               ;; if there are any. Very important! Preserve `:lib/uuid` so anything
+                                               ;; `:aggregation` references referring to the Metric will still be valid
+                                               ;; after macroexpansion.
+                                               (lib.options/update-options merge (select-keys opts-from-ref
+                                                                                              [:name :display-name :lib/uuid])))
+                            filters        (legacy-macro-filters legacy-metric)]
+                        (log/debugf "Expanding legacy Metric macro\n%s" (u/pprint-to-str &match))
+                        (log/tracef "Adding aggregation clause for legacy Metric %d:\n%s" id (u/pprint-to-str aggregation))
+                        (doseq [filter-clause filters]
+                          (log/tracef "Adding filter clause for legacy Metric %d:\n%s" id (u/pprint-to-str filter-clause)))
+                        (swap! new-filters concat filters)
+                        aggregation))
+        new-filters @new-filters]
+    (cond-> stage'
+      (seq new-filters) (lib.filter/add-filters-to-stage new-filters))))
+
+(mu/defmethod resolve-legacy-macros-in-stage :segment :- ::lib.schema/stage
+  [_macro-type        :- [:= :segment]
+   stage              :- ::lib.schema/stage
+   id->legacy-segment :- ::id->legacy-macro]
+  (-> (mbql.u/replace stage
+        [:segment _opts (id :guard pos-int?)]
+        (let [legacy-segment (get id->legacy-segment id)
+              filter-clauses (legacy-macro-filters legacy-segment)]
+          (log/debugf "Expanding legacy Segment macro\n%s" (u/pprint-to-str &match))
+          (doseq [filter-clause filter-clauses]
+            (log/tracef "Adding filter clause for legacy Segment %d:\n%s" id (u/pprint-to-str filter-clause)))
+          ;; replace a single segment with a single filter, wrapping them in `:and` if needed... we will unwrap once
+          ;; we've expanded all of the :segment refs.
+          (if (> (count filter-clauses) 1)
+            (apply lib.filter/and filter-clauses)
+            (first filter-clauses))))
+      lib.filter/flatten-compound-filters-in-stage
+      lib.filter/remove-duplicate-filters-in-stage))
+
+(mu/defn ^:private resolve-legacy-macros :- ::lib.schema/query
+  [macro-type       :- ::macro-type
+   query            :- ::lib.schema/query
+   legacy-macro-ids :- [:maybe [:set {:min 1} pos-int?]]]
+  (log/debugf "Resolving legacy %s macros with IDs %s" macro-type legacy-macro-ids)
+  (let [id->legacy-macro (fetch-legacy-macros macro-type query legacy-macro-ids)]
+    (lib.walk/walk-stages
+     query
+     (fn [_query _path stage]
+       (resolve-legacy-macros-in-stage macro-type stage id->legacy-macro)))))
+
+(mu/defn ^:private expand-legacy-macros :- ::lib.schema/query
+  [macro-type :- ::macro-type
+   query      :- ::lib.schema/query]
+  (if-let [legacy-macro-ids (not-empty (unresolved-legacy-macro-ids macro-type query))]
+    (resolve-legacy-macros macro-type query legacy-macro-ids)
     query))
 
+(def ^:private max-recursion-depth
+  "Detect infinite recursion for macro expansion."
+  50)
 
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                   MIDDLEWARE                                                   |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-(mu/defn ^:private expand-metrics-and-segments  :- mbql.s/Query
-  "Expand the macros (`segment`, `metric`) in a `query`."
-  [query  :- mbql.s/Query]
-  (-> query
-      expand-metrics
-      expand-segments))
-
-(defn expand-macros
+(mu/defn expand-macros
   "Middleware that looks for `:metric` and `:segment` macros in an unexpanded MBQL query and substitute the macros for
   their contents."
-  [{query-type :type, :as query}]
-  (if-not (= query-type :query)
-    query
-    (expand-metrics-and-segments query)))
+  ([query  :- ::lib.schema/query]
+   (expand-macros query 0))
+
+  ([query recursion-depth]
+   (when (> recursion-depth max-recursion-depth)
+     (throw (ex-info (tru "Metric/Segment expansion failed. Check mutually recursive segment definitions.")
+                     {:type qp.error-type/invalid-query, :query query})))
+   (let [query' (->> query
+                     (expand-legacy-macros :metric)
+                     (expand-legacy-macros :segment))]
+     ;; if we expanded anything, we need to recursively try expanding again until nothing is left to expand, in case a
+     ;; legacy Metric or Segment references another legacy Metric or Segment.
+     (if-not (= query' query)
+       (recur query' (inc recursion-depth))
+       (do
+         (log/tracef "No more legacy Metrics/Segments to expand.")
+         query')))))
diff --git a/src/metabase/query_processor/middleware/fetch_source_query.clj b/src/metabase/query_processor/middleware/fetch_source_query.clj
index 49fc047c982bb33ae365dd9891931e99e28f9a1b..555548bbc8290c4675dd0cd7fc6b49e0c4e05b75 100644
--- a/src/metabase/query_processor/middleware/fetch_source_query.clj
+++ b/src/metabase/query_processor/middleware/fetch_source_query.clj
@@ -21,6 +21,8 @@
    [metabase.util.malli.schema :as ms]
    [weavejester.dependency :as dep]))
 
+;;; TODO -- consider whether [[normalize-card-query]] should be moved into [[metabase.lib.card]], seems like it would
+;;; make sense but it would involve teasing out some QP-specific stuff to make it work.
 (defn- fix-mongodb-first-stage
   "MongoDB native queries consist of a collection and a pipelne (query).
 
@@ -38,9 +40,10 @@
                                            :query       x}))))]
     (cons first-stage more)))
 
-(defn- normalize-card-query
+(mu/defn normalize-card-query :- ::lib.schema.metadata/card
   "Convert Card's query (`:datasaet-query`) to pMBQL as needed; splice in stage metadata and some extra keys."
-  [metadata-providerable {card-id :id, :as card}]
+  [metadata-providerable   :- ::lib.schema.metadata/metadata-providerable
+   {card-id :id, :as card} :- ::lib.schema.metadata/card]
   (let [persisted-info (:lib/persisted-info card)
         persisted?     (qp.persisted/can-substitute? card persisted-info)]
     (when persisted?
@@ -92,7 +95,7 @@
 (mu/defn ^:private resolve-source-cards-in-stage :- [:maybe ::lib.schema/stages]
   [query     :- ::lib.schema/query
    stage     :- ::lib.schema/stage
-   dep-graph :- (ms/InstanceOfClass clojure.lang.Atom)]
+   dep-graph :- (ms/InstanceOfClass clojure.lang.Volatile)]
   (when (and (= (:lib/type stage) :mbql.stage/mbql)
              (:source-card stage))
     ;; make sure nested queries are enabled before resolving them.
@@ -103,10 +106,10 @@
     ;; dependency of the previously-resolved source card on the one we're about to resolve. We can check for circular
     ;; dependencies this way.
     (when (:qp/stage-is-from-source-card stage)
-      (u/prog1 (swap! dep-graph
-                      dep/depend
-                      (tru "Card {0}" (:qp/stage-is-from-source-card stage))
-                      (tru "Card {0}" (:source-card stage)))
+      (u/prog1 (vswap! dep-graph
+                       dep/depend
+                       (tru "Card {0}" (:qp/stage-is-from-source-card stage))
+                       (tru "Card {0}" (:source-card stage)))
         ;; This will throw if there's a cycle
         (dep/topo-sort <>)))
     (let [card         (card query (:source-card stage))
@@ -149,7 +152,7 @@
   "If a stage has a `:source-card`, fetch the Card and prepend its underlying stages to the pipeline."
   [query :- ::lib.schema/query]
   (let [query (dissoc query :source-card-id :qp/source-card-id)] ; `:source-card-id` was the old key
-    (resolve-source-cards* query 0 (atom (dep/graph)))))
+    (resolve-source-cards* query 0 (volatile! (dep/graph)))))
 
 (defn add-dataset-info
   "Post-processing middleware that adds `:model` and `:dataset` (for historic reasons) `true` or `false` to queries with
diff --git a/src/metabase/query_processor/middleware/fetch_source_query_legacy.clj b/src/metabase/query_processor/middleware/fetch_source_query_legacy.clj
index 396f01ac6d05ba58546ba42a9dada72bd090916c..37f80c3c33f97ea62cedc86650ba44053b8d1be7 100644
--- a/src/metabase/query_processor/middleware/fetch_source_query_legacy.clj
+++ b/src/metabase/query_processor/middleware/fetch_source_query_legacy.clj
@@ -2,25 +2,20 @@
   "LEGACY IMPLEMENTATION of [[metabase.query-processor.middleware.fetch-source-query]], will be removed soon."
   (:require
    [clojure.set :as set]
-   [medley.core :as m]
    [metabase.driver.ddl.interface :as ddl.i]
    [metabase.driver.util :as driver.u]
    [metabase.lib.convert :as lib.convert]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.schema.id :as lib.schema.id]
-   [metabase.lib.util :as lib.util]
    [metabase.mbql.normalize :as mbql.normalize]
    [metabase.mbql.schema :as mbql.s]
-   [metabase.mbql.util :as mbql.u]
    [metabase.public-settings :as public-settings]
    [metabase.query-processor.store :as qp.store]
    [metabase.query-processor.util.persisted-cache :as qp.persisted]
    [metabase.util :as u]
    [metabase.util.i18n :refer [trs tru]]
    [metabase.util.log :as log]
-   [metabase.util.malli :as mu]
-   [metabase.util.malli.schema :as ms]
-   [weavejester.dependency :as dep]))
+   [metabase.util.malli :as mu]))
 
 (set! *warn-on-reflection* true)
 
@@ -34,44 +29,6 @@
    [:source-query/model?   {:optional true} :boolean]
    [:persisted-info/native {:optional true} :string]])
 
-(def ^:private MapWithResolvedSourceQuery
-  [:and
-   [:map
-    [:database        mbql.s/DatabaseID]
-    [:source-metadata [:maybe [:sequential mbql.s/SourceQueryMetadata]]]
-    [:source-query    mbql.s/SourceQuery]
-    [:source-card-id  ms/PositiveInt]]
-   [:fn
-    {:error/message "`:source-table` should be removed"}
-    (complement :source-table)]])
-
-(defn- query-has-unresolved-card-id-source-tables? [{inner-mbql-query :query}]
-  (when inner-mbql-query
-    (mbql.u/match-one inner-mbql-query
-      (&match :guard (every-pred map? (comp string? :source-table))))))
-
-(defn- query-has-resolved-database-id? [{:keys [database]}]
-  ((every-pred integer? pos?) database))
-
-(def ^:private FullyResolvedQuery
-  "Schema for a MBQL query where all `card__id` `:source-tables` have been removes and appropriate `:source-query`s have
-  been added instead, and where the top-level `:database` ID, if it was the 'source query placeholder`, is replaced by
-  the actual database ID of the source query.
-
-  This schema represents the way the query should look after this middleware finishes preprocessing it."
-  [:and
-   mbql.s/Query
-   [:fn
-    {:error/message "Query where all card__id :source-tables are fully resolved"}
-    (complement query-has-unresolved-card-id-source-tables?)]
-   [:fn
-    {:error/message "Query where source-query virtual `:database` has been replaced with actual Database ID"}
-    query-has-resolved-database-id?]])
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                       Resolving card__id -> source query                                       |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
 (defn- source-query
   "Get the query to be run from the card"
   [{dataset-query :dataset-query, card-id :id, :as card}]
@@ -129,135 +86,3 @@
               :database        database-id
               :source-metadata (seq (map mbql.normalize/normalize-source-metadata result-metadata))}
        (= card-type :model) (assoc :source-query/model? true)))))
-
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                         Logic for traversing the query                                         |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-(def ^:private ^{:arglists '([x])} map-with-card-id-source-table?
-  "Is `x` a map with a \"card__id\" `:source-table`, i.e., something this middleware needs to resolve?"
-  (every-pred
-   map?
-   (comp string? :source-table)
-   (comp (partial re-matches mbql.s/source-table-card-id-regex) :source-table)))
-
-(mu/defn ^:private resolve-one :- MapWithResolvedSourceQuery
-  [{:keys [source-table], :as m} :- [:map [:source-table mbql.s/source-table-card-id-regex]]]
-  (let [card-id                   (-> source-table lib.util/legacy-string-table-id->card-id)
-        source-query-and-metadata (-> card-id (card-id->source-query-and-metadata true))]
-    (merge
-     (dissoc m :source-table)
-     ;; record the `card-id` we've resolved here. We'll include it in `:info` for permissions purposes later
-     {:source-card-id card-id}
-     source-query-and-metadata)))
-
-(defn- resolve-all*
-  [m]
-  (mbql.u/replace m
-    map-with-card-id-source-table?
-    ;; if this is a map that has a Card ID `:source-table`, resolve that (replacing it with the appropriate
-    ;; `:source-query`, then recurse and resolve any nested-nested queries that need to be resolved too
-    (let [resolved (if (public-settings/enable-nested-queries)
-                     (resolve-one &match)
-                     (throw (ex-info (trs "Nested queries are disabled")
-                                     {:clause &match})))]
-      ;; wrap the recursive call in a try-catch; if the recursive resolution fails, add context about the
-      ;; resolution that were we in the process of
-      (try
-        (resolve-all* resolved)
-        (catch Throwable e
-          (throw (ex-info (tru "Error resolving source query")
-                          {:resolving &match, :resolved resolved}
-                          e)))))))
-
-(defn- check-for-circular-references
-  "Check that there are no circular dependencies among source cards. This is equivalent to
-   finding a topological sort of the dependency graph.
-   https://en.wikipedia.org/wiki/Topological_sorting"
-  ([m]
-   (check-for-circular-references (dep/graph) m)
-   m)
-  ([g m]
-   (transduce (comp (filter map-with-card-id-source-table?)
-                    (map (comp card-id->source-query-and-metadata
-                               lib.util/legacy-string-table-id->card-id
-                               :source-table)))
-              (fn
-                ([] g)
-                ([g source-query]
-                 (-> g
-                     (dep/depend m source-query)
-                     ;; Recursive call will circuit break the moment there's a cycle, so no
-                     ;; danger of unbounded recursion.
-                     (check-for-circular-references source-query)))
-                ([g]
-                 ;; This will throw if there's a cycle
-                 (dep/topo-sort g)
-                 g))
-              (tree-seq coll? identity m))))
-
-(defn- copy-source-query-database-ids
-  "If `m` has the saved questions virtual `:database` ID, (recursively) look for actual resolved Database IDs in the
-  next level down and copy it to our level."
-  [{:keys [database], :as m}]
-  (if (and database (not= database lib.schema.id/saved-questions-virtual-database-id))
-    m
-    (let [{:keys [query source-query], :as m}
-          (cond-> m
-            (:query m)        (update :query        copy-source-query-database-ids)
-            (:source-query m) (update :source-query copy-source-query-database-ids))
-
-          db-id
-          (some (fn [{:keys [database]}]
-                  (when (some-> database (not= lib.schema.id/saved-questions-virtual-database-id))
-                    database))
-                [source-query query])]
-      (cond-> m
-        db-id (assoc :database db-id)))))
-
-(defn- remove-unneeded-database-ids
-  "Remove `:database` from all levels besides the top level."
-  [m]
-  (mbql.u/replace-in m [:query]
-    (&match :guard (every-pred map? :database (comp integer? :database)))
-    (recur (dissoc &match :database))))
-
-(mu/defn ^:private extract-resolved-card-id :- [:map
-                                                [:card-id [:maybe ms/PositiveInt]]
-                                                [:query   :map]]
-  "If the ID of the Card we've resolved (`:source-card-id`) was added by a previous step, add it
-  to `:query` `:info` (so it can be included in the QueryExecution log), then return a map with the resolved
-  `:card-id` and updated `:query`."
-  [query :- :map]
-  (let [card-id (get-in query [:query :source-card-id])]
-    {:query   (cond-> query
-                card-id (update-in [:info :card-id] #(or % card-id)))
-     :card-id card-id}))
-
-(mu/defn ^:private resolve-all :- [:map
-                                   [:card-id [:maybe ms/PositiveInt]]
-                                   [:query   :map]]
-  "Recursively replace all Card ID source tables in `query` with resolved `:source-query` and `:source-metadata`. Since
-  the `:database` is only useful for top-level source queries, we'll remove it from all other levels."
-  [query :- :map]
-  ;; if a `:source-card-id` is already in the query, remove it, so we don't pull user-supplied input up into `:info`
-  ;; allowing someone to bypass permissions
-  (-> (m/dissoc-in query [:query :source-card-id])
-      check-for-circular-references
-      resolve-all*
-      copy-source-query-database-ids
-      remove-unneeded-database-ids
-      extract-resolved-card-id))
-
-(mu/defn resolve-card-id-source-tables* :- [:map
-                                            [:card-id [:maybe ms/PositiveInt]]
-                                            [:query   FullyResolvedQuery]]
-  "Resolve `card__n`-style `:source-tables` in `query`."
-  [{inner-query :query, :as outer-query} :- mbql.s/Query]
-  (if-not inner-query
-    ;; for non-MBQL queries there's nothing to do since they have nested queries
-    {:query outer-query, :card-id nil}
-    ;; Otherwise attempt to expand any source queries as needed. Pull the `:database` key up into the top-level if it
-    ;; exists
-    (resolve-all outer-query)))
diff --git a/src/metabase/query_processor/middleware/normalize_query.clj b/src/metabase/query_processor/middleware/normalize_query.clj
index 334be17d44bf8becf4d37af01fd072f6bd6c8111..80fa63dfe200045f0bb9cd6505961ade3acf69eb 100644
--- a/src/metabase/query_processor/middleware/normalize_query.clj
+++ b/src/metabase/query_processor/middleware/normalize_query.clj
@@ -55,7 +55,7 @@
                                                  (some-fn :lib/type :type)]]
   "Preprocessing middleware. Normalize a query, meaning do things like convert keys and MBQL clause tags to kebab-case
   keywords. Convert query to pMBQL if needed."
-  [query :- :map]
+  [query :- [:map [:database ::lib.schema.id/database]]]
   (try
     (u/prog1 (normalize* query)
       (log/tracef "Normalized query:\n%s\n=>\n%s" (u/pprint-to-str query) (u/pprint-to-str <>)))
diff --git a/src/metabase/query_processor/middleware/permissions.clj b/src/metabase/query_processor/middleware/permissions.clj
index 69b7bcf5dfeb23c093735c3842b0ba74564e7bdb..bf50644f8b2826e4bb8b73c1abf83711d033608a 100644
--- a/src/metabase/query_processor/middleware/permissions.clj
+++ b/src/metabase/query_processor/middleware/permissions.clj
@@ -3,6 +3,7 @@
   (:require
    [metabase.api.common
     :refer [*current-user-id* *current-user-permissions-set*]]
+   [metabase.lib.core :as lib]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
    [metabase.lib.schema.id :as lib.schema.id]
    [metabase.models.data-permissions :as data-perms]
@@ -12,7 +13,6 @@
    [metabase.public-settings.premium-features :refer [defenterprise]]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.query-processor.store :as qp.store]
-   [metabase.query-processor.util.tag-referenced-cards :as qp.u.tag-referenced-cards]
    [metabase.util :as u]
    [metabase.util.i18n :refer [tru]]
    [metabase.util.log :as log]
@@ -103,7 +103,8 @@
         (do
           (query-perms/check-data-perms outer-query required-perms :throw-exceptions? true)
           ;; check perms for any Cards referenced by this query (if it is a native query)
-          (doseq [{query :dataset-query} (qp.u.tag-referenced-cards/tags-referenced-cards outer-query)]
+          (doseq [{query :dataset-query} (lib/template-tags-referenced-cards
+                                          (lib/query (qp.store/metadata-provider) outer-query))]
             (check-query-permissions* query)))))))
 
 (defn check-query-permissions
diff --git a/src/metabase/query_processor/middleware/resolve_fields.clj b/src/metabase/query_processor/middleware/resolve_fields.clj
index a39bab7644752d2c00db8c5dde86e507142b9ea9..0331f528f26759d22c3751d1237b367d304b9b40 100644
--- a/src/metabase/query_processor/middleware/resolve_fields.clj
+++ b/src/metabase/query_processor/middleware/resolve_fields.clj
@@ -2,32 +2,59 @@
   "Middleware that resolves the Fields referenced by a query."
   (:require
    [metabase.lib.metadata :as lib.metadata]
+   [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.id :as lib.schema.id]
+   [metabase.mbql.schema :as mbql.s]
    [metabase.mbql.util :as mbql.u]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.query-processor.store :as qp.store]
    [metabase.util :as u]
-   [metabase.util.i18n :refer [tru]]))
+   [metabase.util.i18n :refer [tru]]
+   [metabase.util.malli :as mu]))
 
 (defn- resolve-fields-with-ids!
-  [field-ids]
-  (qp.store/bulk-metadata :metadata/column field-ids)
+  [metadata-providerable field-ids]
+  (lib.metadata/bulk-metadata metadata-providerable :metadata/column field-ids)
   (when-let [parent-ids (not-empty
-                         (into []
+                         (into #{}
                                (comp (map (fn [field-id]
-                                            (:parent-id (lib.metadata/field (qp.store/metadata-provider) field-id))))
+                                            (:parent-id (lib.metadata/field metadata-providerable field-id))))
                                      (filter some?))
                                field-ids))]
-    (recur parent-ids)))
+    (recur metadata-providerable parent-ids)))
+
+(defmulti ^:private field-ids
+  {:arglists '([query])}
+  (fn [query]
+    (if (:lib/type query) ::pmbql ::legacy)))
+
+(mu/defmethod field-ids ::pmbql :- [:set ::lib.schema.id/field]
+  [query :- ::lib.schema/query]
+  (into #{}
+        (mbql.u/match (:stages query)
+          [:field _opts (id :guard pos-int?)]
+          id
+
+          ;; stage metadata
+          {:lib/type :metadata/column, :id (id :guard pos-int?)}
+          id)))
+
+(mu/defmethod field-ids ::legacy :- [:set ::lib.schema.id/field]
+  [query :- ::mbql.s/Query]
+  (into (set (mbql.u/match (:query query) [:field (id :guard integer?) _] id))
+        (comp cat (keep :id))
+        (mbql.u/match (:query query) {:source-metadata source-metadata} source-metadata)))
 
 (defn resolve-fields
   "Resolve all field referenced in the `query`, and store them in the QP Store."
   [query]
-  (let [ids (into (set (mbql.u/match (:query query) [:field (id :guard integer?) _] id))
-                  (comp cat (keep :id))
-                  (mbql.u/match (:query query) {:source-metadata source-metadata} source-metadata))]
+  (let [ids (field-ids query)]
     (try
       (u/prog1 query
-        (resolve-fields-with-ids! ids))
+        (let [metadata-providerable (if (:lib/type query)
+                                      query
+                                      (qp.store/metadata-provider))]
+          (resolve-fields-with-ids! metadata-providerable ids)))
       (catch Throwable e
         (throw (ex-info (tru "Error resolving Fields in query: {0}" (ex-message e))
                         {:field-ids ids
diff --git a/src/metabase/query_processor/middleware/resolve_referenced.clj b/src/metabase/query_processor/middleware/resolve_referenced.clj
index a35ec3c734da330240d1b50532eb360524fb053d..9fc4334c1767609a0531aa92c13d8c12a27e33f8 100644
--- a/src/metabase/query_processor/middleware/resolve_referenced.clj
+++ b/src/metabase/query_processor/middleware/resolve_referenced.clj
@@ -1,76 +1,82 @@
 (ns metabase.query-processor.middleware.resolve-referenced
   (:require
-   [metabase.lib.metadata.protocols :as lib.metadata.protocols]
+   [metabase.lib.core :as lib]
+   [metabase.lib.metadata :as lib.metadata]
+   [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.id :as lib.schema.id]
-   #_{:clj-kondo/ignore [:deprecated-namespace]}
-   [metabase.query-processor.middleware.fetch-source-query-legacy
-    :as fetch-source-query-legacy]
-   [metabase.query-processor.middleware.resolve-fields
-    :as qp.resolve-fields]
-   [metabase.query-processor.middleware.resolve-source-table
-    :as qp.resolve-source-table]
-   [metabase.query-processor.store :as qp.store]
-   [metabase.query-processor.util.tag-referenced-cards
-    :as qp.u.tag-referenced-cards]
+   [metabase.lib.schema.metadata :as lib.schema.metadata]
+   [metabase.query-processor.error-type :as qp.error-type]
+   [metabase.query-processor.middleware.fetch-source-query :as qp.fetch-source-query]
+   [metabase.query-processor.middleware.resolve-fields :as qp.resolve-fields]
+   [metabase.query-processor.middleware.resolve-source-table :as qp.resolve-source-table]
    [metabase.util.i18n :refer [tru]]
    [metabase.util.malli :as mu]
    [weavejester.dependency :as dep])
   (:import
    (clojure.lang ExceptionInfo)))
 
-(defn- check-query-database-id=
-  [query database-id]
-  (when-not (= (:database query) database-id)
-    (throw (ex-info (tru "Referenced query is from a different database")
-                    {:referenced-query     query
-                     :expected-database-id database-id}))))
-
-(mu/defn ^:private resolve-referenced-card-resources* :- :map
-  [query]
-  (doseq [referenced-card (qp.u.tag-referenced-cards/tags-referenced-cards query)
-          :let            [referenced-query (:dataset-query referenced-card)
-                           resolved-query (fetch-source-query-legacy/resolve-card-id-source-tables* referenced-query)]]
-    (check-query-database-id= referenced-query (:database query))
+(mu/defn ^:private resolve-referenced-card-resources*
+  "Done for side effects; warm the MetadataProvider."
+  [query :- ::lib.schema/query]
+  (doseq [referenced-card (lib/template-tags-referenced-cards query)
+          :let            [referenced-query (->> referenced-card
+                                                 (qp.fetch-source-query/normalize-card-query query)
+                                                 :dataset-query)
+                           resolved-query   (qp.fetch-source-query/resolve-source-cards referenced-query)]]
     (qp.resolve-source-table/resolve-source-tables resolved-query)
-    (qp.resolve-fields/resolve-fields resolved-query))
-  query)
+    (qp.resolve-fields/resolve-fields resolved-query)))
 
-(defn- card-subquery-graph
-  [graph card-id]
-  (let [card-query (:dataset-query (lib.metadata.protocols/card (qp.store/metadata-provider) card-id))]
+(mu/defn ^:private card-subquery-graph
+  [metadata-providerable :- ::lib.schema.metadata/metadata-providerable
+   graph                 :- :map
+   card-id               :- ::lib.schema.id/card]
+  (let [card-query (->> (or (lib.metadata/card metadata-providerable card-id)
+                            (throw (ex-info (tru "Card {0} does not exist, or is from a different Database." (pr-str card-id))
+                                            {:type qp.error-type/invalid-query, :card-id card-id})))
+                        (qp.fetch-source-query/normalize-card-query metadata-providerable)
+                        :dataset-query)]
     (reduce
      (fn [g sub-card-id]
-       (card-subquery-graph (dep/depend g card-id sub-card-id)
+       (card-subquery-graph metadata-providerable
+                            (dep/depend g card-id sub-card-id)
                             sub-card-id))
      graph
-     (qp.u.tag-referenced-cards/query->tag-card-ids card-query))))
+     (lib/template-tag-card-ids card-query))))
 
 (mu/defn ^:private circular-ref-error :- ::lib.schema.common/non-blank-string
-  [from-card :- ::lib.schema.id/card
-   to-card   :- ::lib.schema.id/card]
-  (let [cards               (into {}
-                                  (map (juxt :id :name))
-                                  (qp.store/bulk-metadata :metadata/card #{from-card to-card}))
-        from-name           (get cards from-card)
-        to-name             (get cards to-card)]
+  [metadata-providerable :- ::lib.schema.metadata/metadata-providerable
+   from-card             :- ::lib.schema.id/card
+   to-card               :- ::lib.schema.id/card]
+  (let [cards     (into {}
+                        (map (juxt :id :name))
+                        (lib.metadata/bulk-metadata metadata-providerable :metadata/card #{from-card to-card}))
+        from-name (or (get cards from-card)
+                      (throw (ex-info (tru "Referenced query is from a different database")
+                                      {:type qp.error-type/invalid-query, :card-id from-card})))
+        to-name   (or (get cards to-card)
+                      (throw (ex-info (tru "Referenced query is from a different database")
+                                      {:type qp.error-type/invalid-query, :card-id to-card})))]
     (str
      (tru "This query has circular referencing sub-queries. ")
      (tru "These questions seem to be part of the problem: \"{0}\" and \"{1}\"." from-name to-name))))
 
-(defn- check-for-circular-references
-  [query]
+(mu/defn ^:private check-for-circular-references
+  "Done for side effects; [[card-subquery-graph]] will throw if there are circular references."
+  [query :- ::lib.schema/query]
   (try
-   ;; `card-subquery-graph` will throw if there are circular references
-   (reduce card-subquery-graph (dep/graph) (qp.u.tag-referenced-cards/query->tag-card-ids query))
-   (catch ExceptionInfo e
-     (let [{:keys [reason node dependency]} (ex-data e)]
-       (if (= reason :weavejester.dependency/circular-dependency)
-         (throw (ex-info (circular-ref-error node dependency) {:original-exception e}))
-         (throw e)))))
-  query)
+    (reduce (partial card-subquery-graph query)
+            (dep/graph)
+            (lib/template-tag-card-ids query))
+    (catch ExceptionInfo e
+      (let [{:keys [reason node dependency]} (ex-data e)]
+        (if (= reason :weavejester.dependency/circular-dependency)
+          (throw (ex-info (circular-ref-error query node dependency) {:original-exception e}))
+          (throw e))))))
 
-(defn resolve-referenced-card-resources
+(mu/defn resolve-referenced-card-resources :- ::lib.schema/query
   "Resolves tables and fields referenced in card query template tags."
-  [query]
-  (-> query check-for-circular-references resolve-referenced-card-resources*))
+  [query :- ::lib.schema/query]
+  (check-for-circular-references query)
+  (resolve-referenced-card-resources* query)
+  query)
diff --git a/src/metabase/query_processor/middleware/resolve_source_table.clj b/src/metabase/query_processor/middleware/resolve_source_table.clj
index 8e68dc0a0920613c93a09d61ea3b4258e0c89cba..8af67f5bfe38ebf536dd893c94867b2acd2950bd 100644
--- a/src/metabase/query_processor/middleware/resolve_source_table.clj
+++ b/src/metabase/query_processor/middleware/resolve_source_table.clj
@@ -1,41 +1,26 @@
 (ns metabase.query-processor.middleware.resolve-source-table
   "Fetches Tables corresponding to any `:source-table` IDs anywhere in the query."
   (:require
+   [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.schema.id :as lib.schema.id]
-   [metabase.mbql.util :as mbql.u]
-   [metabase.query-processor.store :as qp.store]
-   [metabase.util.i18n :refer [tru]]
+   [metabase.lib.walk :as lib.walk]
    [metabase.util.malli :as mu]))
 
-(defn- check-all-source-table-ids-are-valid
-  "Sanity check: Any non-positive-integer value of `:source-table` should have been resolved by now. The
-  `resolve-card-id-source-tables` middleware should have already taken care of it."
-  [query]
-  (mbql.u/match-one query
-    (m :guard (every-pred map? :source-table #(string? (:source-table %))))
-    (throw
-     (ex-info
-      (tru "Invalid :source-table ''{0}'': should be resolved to a Table ID by now." (:source-table m))
-      {:form m}))))
-
-(mu/defn ^:private query->source-table-ids :- [:maybe [:set {:min 1} ::lib.schema.id/table]]
+(mu/defn  ^:private query->source-table-ids :- [:maybe [:set {:min 1} ::lib.schema.id/table]]
   "Fetch a set of all `:source-table` IDs anywhere in `query`."
   [query]
-  (some->
-   (mbql.u/match query
-     (m :guard (every-pred map? :source-table))
-     ;; Recursively look in the rest of `m` for any other source tables
-     (cons
-      (:source-table m)
-      (filter some? (recur (dissoc m :source-table)))))
-   flatten
-   set))
+  (let [source-table-ids (volatile! #{})]
+    (lib.walk/walk-stages
+     query
+     (fn [_query _path {:keys [source-table], :as _stage}]
+       (when source-table
+         (vswap! source-table-ids conj source-table))))
+    (not-empty @source-table-ids)))
 
 (defn resolve-source-tables
   "Middleware that will take any `:source-table`s (integer IDs) anywhere in the query and fetch and save the
   corresponding Table in the Query Processor Store."
   [query]
-  (check-all-source-table-ids-are-valid query)
   ;; this is done for side effects
-  (qp.store/bulk-metadata :metadata/table (query->source-table-ids query))
+  (lib.metadata/bulk-metadata-or-throw query :metadata/table (query->source-table-ids query))
   query)
diff --git a/src/metabase/query_processor/preprocess.clj b/src/metabase/query_processor/preprocess.clj
index 6c2d2b585d7c65658c37272e52cfdb60d1830760..8353e193b76148b71870d64472378997933fc15a 100644
--- a/src/metabase/query_processor/preprocess.clj
+++ b/src/metabase/query_processor/preprocess.clj
@@ -1,6 +1,9 @@
 (ns metabase.query-processor.preprocess
   (:require
+   [metabase.lib.convert :as lib.convert]
+   [metabase.lib.query :as lib.query]
    [metabase.lib.schema.id :as lib.schema.id]
+   [metabase.mbql.schema :as mbql.s]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.query-processor.middleware.add-default-temporal-unit :as qp.add-default-temporal-unit]
    [metabase.query-processor.middleware.add-dimension-projections :as qp.add-dimension-projections]
@@ -13,7 +16,6 @@
    [metabase.query-processor.middleware.binning :as binning]
    [metabase.query-processor.middleware.check-features :as check-features]
    [metabase.query-processor.middleware.constraints :as qp.constraints]
-   [metabase.query-processor.middleware.convert-to-legacy :as qp.convert-to-legacy]
    [metabase.query-processor.middleware.cumulative-aggregations :as qp.cumulative-aggregations]
    [metabase.query-processor.middleware.desugar :as desugar]
    [metabase.query-processor.middleware.enterprise :as qp.middleware.enterprise]
@@ -39,56 +41,75 @@
    [metabase.query-processor.middleware.validate-temporal-bucketing :as validate-temporal-bucketing]
    [metabase.query-processor.middleware.wrap-value-literals :as qp.wrap-value-literals]
    [metabase.query-processor.setup :as qp.setup]
+   [metabase.query-processor.store :as qp.store]
    [metabase.util :as u]
    [metabase.util.i18n :as i18n]
    [metabase.util.log :as log]
    [metabase.util.malli :as mu]))
 
+;;; the following helper functions are temporary, to aid in the transition from a legacy MBQL QP to a pMBQL QP. Each
+;;; individual middleware function is wrapped in either [[ensure-legacy]] or [[ensure-pmbql]], and will then see the
+;;; flavor of MBQL it is written for.
+
+(mu/defn ^:private ->legacy :- mbql.s/Query
+  [query]
+  (lib.convert/->legacy-MBQL query))
+
+(defn- ^:deprecated ensure-legacy [middleware-fn]
+  (fn [query]
+    (let [query (cond-> query
+                  (:lib/type query) ->legacy)]
+      (middleware-fn query))))
+
+(defn- ensure-pmbql [middleware-fn]
+  (fn [query]
+    (let [query (cond->> query
+                  (not (:lib/type query)) (lib.query/query (qp.store/metadata-provider)))]
+      (middleware-fn query))))
+
 (def ^:private middleware
   "Pre-processing middleware. Has the form
 
     (f query) -> query"
   ;; ↓↓↓ PRE-PROCESSING ↓↓↓ happens from TOP TO BOTTOM
+  #_{:clj-kondo/ignore [:deprecated-var]}
   [#'normalize/normalize-preprocessing-middleware
-   #'qp.perms/remove-permissions-key
-   #'qp.constraints/maybe-add-default-userland-constraints
-   #'validate/validate-query
-   #'fetch-source-query/resolve-source-cards
-   ;; ↑↑↑ ALL MIDDLEWARE ABOVE THIS POINT WILL SEE MLV2 PMBQL QUERIES ↑↑↑
-   #'qp.convert-to-legacy/convert-to-legacy
-   ;; ↓↓↓ ALL MIDDLEWARE BELOW THIS POINT WILL SEE LEGACY MBQL QUERIES ↓↓↓
-   #'expand-macros/expand-macros
-   #'qp.resolve-referenced/resolve-referenced-card-resources
-   #'parameters/substitute-parameters
-   #'qp.resolve-source-table/resolve-source-tables
-   #'qp.auto-bucket-datetimes/auto-bucket-datetimes
-   #'reconcile-bucketing/reconcile-breakout-and-order-by-bucketing
-   #'qp.add-source-metadata/add-source-metadata-for-source-queries
-   #'upgrade-field-literals/upgrade-field-literals
-   #'qp.middleware.enterprise/apply-sandboxing
-   #'qp.persistence/substitute-persisted-query
-   #'qp.add-implicit-clauses/add-implicit-clauses
-   #'qp.add-dimension-projections/add-remapped-columns
-   #'qp.resolve-fields/resolve-fields
-   #'binning/update-binning-strategy
-   #'desugar/desugar
-   #'qp.add-default-temporal-unit/add-default-temporal-unit
-   #'qp.add-implicit-joins/add-implicit-joins
-   #'resolve-joins/resolve-joins
-   #'resolve-joined-fields/resolve-joined-fields
-   #'fix-bad-refs/fix-bad-references
-   #'escape-join-aliases/escape-join-aliases
+   (ensure-pmbql #'qp.perms/remove-permissions-key)
+   (ensure-pmbql #'qp.constraints/maybe-add-default-userland-constraints)
+   (ensure-pmbql #'validate/validate-query)
+   (ensure-pmbql #'fetch-source-query/resolve-source-cards)
+   (ensure-pmbql #'expand-macros/expand-macros)
+   (ensure-pmbql #'qp.resolve-referenced/resolve-referenced-card-resources)
+   (ensure-legacy #'parameters/substitute-parameters)
+   (ensure-pmbql #'qp.resolve-source-table/resolve-source-tables)
+   (ensure-legacy #'qp.auto-bucket-datetimes/auto-bucket-datetimes)
+   (ensure-legacy #'reconcile-bucketing/reconcile-breakout-and-order-by-bucketing)
+   (ensure-legacy #'qp.add-source-metadata/add-source-metadata-for-source-queries)
+   (ensure-legacy #'upgrade-field-literals/upgrade-field-literals)
+   (ensure-legacy #'qp.middleware.enterprise/apply-sandboxing)
+   (ensure-legacy #'qp.persistence/substitute-persisted-query)
+   (ensure-legacy #'qp.add-implicit-clauses/add-implicit-clauses)
+   (ensure-legacy #'qp.add-dimension-projections/add-remapped-columns)
+   (ensure-legacy #'qp.resolve-fields/resolve-fields)
+   (ensure-legacy #'binning/update-binning-strategy)
+   (ensure-legacy #'desugar/desugar)
+   (ensure-legacy #'qp.add-default-temporal-unit/add-default-temporal-unit)
+   (ensure-legacy #'qp.add-implicit-joins/add-implicit-joins)
+   (ensure-legacy #'resolve-joins/resolve-joins)
+   (ensure-legacy #'resolve-joined-fields/resolve-joined-fields)
+   (ensure-legacy #'fix-bad-refs/fix-bad-references)
+   (ensure-legacy #'escape-join-aliases/escape-join-aliases)
    ;; yes, this is called a second time, because we need to handle any joins that got added
-   #'qp.middleware.enterprise/apply-sandboxing
-   #'qp.cumulative-aggregations/rewrite-cumulative-aggregations
-   #'qp.pre-alias-aggregations/pre-alias-aggregations
-   #'qp.wrap-value-literals/wrap-value-literals
-   #'auto-parse-filter-values/auto-parse-filter-values
-   #'validate-temporal-bucketing/validate-temporal-bucketing
-   #'optimize-temporal-filters/optimize-temporal-filters
-   #'limit/add-default-limit
-   #'qp.middleware.enterprise/apply-download-limit
-   #'check-features/check-features])
+   (ensure-legacy #'qp.middleware.enterprise/apply-sandboxing)
+   (ensure-legacy #'qp.cumulative-aggregations/rewrite-cumulative-aggregations)
+   (ensure-legacy #'qp.pre-alias-aggregations/pre-alias-aggregations)
+   (ensure-legacy #'qp.wrap-value-literals/wrap-value-literals)
+   (ensure-legacy #'auto-parse-filter-values/auto-parse-filter-values)
+   (ensure-legacy #'validate-temporal-bucketing/validate-temporal-bucketing)
+   (ensure-legacy #'optimize-temporal-filters/optimize-temporal-filters)
+   (ensure-legacy #'limit/add-default-limit)
+   (ensure-legacy #'qp.middleware.enterprise/apply-download-limit)
+   (ensure-legacy #'check-features/check-features)])
 
 (mu/defn preprocess :- [:map
                         [:database ::lib.schema.id/database]]
@@ -103,7 +124,6 @@
         preprocessed)
        ([query middleware-fn]
         (try
-          (assert (ifn? middleware-fn))
           ;; make sure the middleware returns a valid query... this should be dev-facing only so no need to i18n
           (u/prog1 (middleware-fn query)
             (when-not (map? <>)
diff --git a/src/metabase/query_processor/store.clj b/src/metabase/query_processor/store.clj
index a540a1f15de2524092e916db6915d3cc3e401a94..9e91f41d766f08920dca49844e8b0ab3ed11d53f 100644
--- a/src/metabase/query_processor/store.clj
+++ b/src/metabase/query_processor/store.clj
@@ -16,17 +16,13 @@
    [medley.core :as m]
    [metabase.lib.convert :as lib.convert]
    [metabase.lib.metadata :as lib.metadata]
-   [metabase.lib.metadata.composed-provider :as lib.metadata.composed-provider]
    [metabase.lib.metadata.jvm :as lib.metadata.jvm]
-   [metabase.lib.metadata.protocols :as lib.metadata.protocols]
-   [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.id :as lib.schema.id]
    [metabase.lib.schema.metadata :as lib.schema.metadata]
    [metabase.query-processor.error-type :as qp.error-type]
    [metabase.util :as u]
    [metabase.util.i18n :refer [tru]]
-   [metabase.util.malli :as mu]
-   [metabase.util.malli.schema :as ms]))
+   [metabase.util.malli :as mu]))
 
 (set! *warn-on-reflection* true)
 
@@ -180,79 +176,22 @@
   [database-id-or-metadata-provider & body]
   `(do-with-metadata-provider ~database-id-or-metadata-provider (^:once fn* [] ~@body)))
 
-(defn- missing-bulk-metadata-error [metadata-type id]
-  (ex-info (tru "Failed to fetch {0} {1}" (pr-str metadata-type) (pr-str id))
-           {:status-code       400
-            :type              qp.error-type/invalid-query
-            :metadata-provider (metadata-provider)
-            :metadata-type     metadata-type
-            :id                id}))
-
+;;; TODO -- mark this deprecated in favor of the version in `lib.metadata`
 (mu/defn bulk-metadata :- [:maybe [:sequential [:map
                                                 [:lib/type :keyword]
-                                                [:id ::lib.schema.common/positive-int]]]]
-  "Fetch multiple objects in bulk. If our metadata provider is a bulk provider (e.g., the application database metadata
-  provider), does a single fetch with [[lib.metadata.protocols/bulk-metadata]] if not (i.e., if this is a mock
-  provider), fetches them with repeated calls to the appropriate single-object method,
-  e.g. [[lib.metadata.protocols/field]].
-
-  The order of the returned objects will match the order of `ids`, and the response is guaranteed to contain every
-  object referred to by `ids`. Throws an exception if any objects could not be fetched.
-
-  This can also be called for side-effects to warm the cache."
+                                                [:id pos-int?]]]]
+  "DEPRECATED -- prefer [[metabase.lib.metadata/bulk-metadata-or-throw]] instead."
   [metadata-type :- [:enum :metadata/card :metadata/column :metadata/metric :metadata/segment :metadata/table]
    ids           :- [:maybe
                      [:or
-                      [:set ::lib.schema.common/positive-int]
-                      [:sequential ::lib.schema.common/positive-int]]]]
-  (when-let [ids-set (not-empty (set ids))]
-    (let [provider   (metadata-provider)
-          objects    (vec (if (satisfies? lib.metadata.protocols/BulkMetadataProvider provider)
-                            (filter some? (lib.metadata.protocols/bulk-metadata provider metadata-type ids-set))
-                            (lib.metadata.composed-provider/fetch-bulk-metadata-with-non-bulk-provider
-                             provider
-                             metadata-type
-                             ids-set)))
-          id->object (m/index-by :id objects)]
-      (mapv (fn [id]
-              (or (get id->object id)
-                  (throw (missing-bulk-metadata-error metadata-type id))))
-            ids))))
+                      [:set pos-int?]
+                      [:sequential pos-int?]]]]
+  (lib.metadata/bulk-metadata-or-throw (metadata-provider) metadata-type ids))
 
 ;;;;
 ;;;; DEPRECATED STUFF
 ;;;;
 
-(def ^:private ^{:deprecated "0.48.0"} LegacyDatabaseMetadata
-  [:map
-   [:id       ::lib.schema.id/database]
-   [:engine   :keyword]
-   [:name     ms/NonBlankString]
-   [:details  :map]
-   [:settings [:maybe :map]]])
-
-(def ^:private ^{:deprecated "0.48.0"} LegacyTableMetadata
-  [:map
-   [:schema [:maybe :string]]
-   [:name   ms/NonBlankString]])
-
-(def ^:private ^{:deprecated "0.48.0"} LegacyFieldMetadata
-  [:map
-   [:name          ms/NonBlankString]
-   [:table_id      ::lib.schema.id/table]
-   [:display_name  ms/NonBlankString]
-   [:description   [:maybe :string]]
-   [:database_type ms/NonBlankString]
-   [:base_type     ms/FieldType]
-   [:semantic_type [:maybe ms/FieldSemanticOrRelationType]]
-   [:fingerprint   [:maybe :map]]
-   [:parent_id     [:maybe ::lib.schema.id/field]]
-   [:nfc_path      [:maybe [:sequential ms/NonBlankString]]]
-   ;; there's a tension as we sometimes store fields from the db, and sometimes store computed fields. ideally we
-   ;; would make everything just use base_type.
-   [:effective_type    {:optional true} [:maybe ms/FieldType]]
-   [:coercion_strategy {:optional true} [:maybe ms/CoercionStrategy]]])
-
 (defn ->legacy-metadata
   "For compatibility: convert MLv2-style metadata as returned by [[metabase.lib.metadata.protocols]]
   or [[metabase.lib.metadata]] functions
@@ -271,43 +210,3 @@
         (update-keys u/->snake_case_en)
         (vary-meta assoc :type model)
         (m/update-existing :field_ref lib.convert/->legacy-MBQL))))
-
-#_{:clj-kondo/ignore [:deprecated-var]}
-(mu/defn database :- LegacyDatabaseMetadata
-  "Fetch the Database referenced by the current query from the QP Store. Throws an Exception if valid item is not
-  returned.
-
-  Deprecated in favor of [[metabase.lib.metadata/database]] + [[metadata-provider]]."
-  {:deprecated "0.48.0"}
-  []
-  (->legacy-metadata (lib.metadata/database (metadata-provider))))
-
-#_{:clj-kondo/ignore [:deprecated-var]}
-(mu/defn ^:deprecated table :- LegacyTableMetadata
-  "Fetch Table with `table-id` from the QP Store. Throws an Exception if valid item is not returned.
-
-  Deprecated in favor of [[metabase.lib.metadata/table]] + [[metadata-provider]]."
-  {:deprecated "0.48.0"}
-  [table-id :- ::lib.schema.id/table]
-  (-> (or (lib.metadata.protocols/table (metadata-provider) table-id)
-          (throw (ex-info (tru "Failed to fetch Table {0}: Table does not exist, or belongs to a different Database."
-                               (pr-str table-id))
-                          {:status-code 404
-                           :type        qp.error-type/invalid-query
-                           :table-id    table-id})))
-      ->legacy-metadata))
-
-#_{:clj-kondo/ignore [:deprecated-var]}
-(mu/defn ^:deprecated field :- LegacyFieldMetadata
-  "Fetch Field with `field-id` from the QP Store. Throws an Exception if valid item is not returned.
-
-  Deprecated in favor of [[metabase.lib.metadata/field]] + [[metadata-provider]]."
-  {:deprecated "0.48.0"}
-  [field-id :- ::lib.schema.id/field]
-  (-> (or (lib.metadata.protocols/field (metadata-provider) field-id)
-          (throw (ex-info (tru "Failed to fetch Field {0}: Field does not exist, or belongs to a different Database."
-                               (pr-str field-id))
-                          {:status-code 404
-                           :type        qp.error-type/invalid-query
-                           :field-id    field-id})))
-      ->legacy-metadata))
diff --git a/src/metabase/query_processor/util/tag_referenced_cards.clj b/src/metabase/query_processor/util/tag_referenced_cards.clj
deleted file mode 100644
index 5ef131498bc6131c7caee2c913176515bb7df142..0000000000000000000000000000000000000000
--- a/src/metabase/query_processor/util/tag_referenced_cards.clj
+++ /dev/null
@@ -1,27 +0,0 @@
-(ns metabase.query-processor.util.tag-referenced-cards
-  (:require
-   [metabase.lib.metadata.protocols :as lib.metadata.protocols]
-   [metabase.lib.schema.metadata :as lib.schema.metadata]
-   [metabase.query-processor.store :as qp.store]
-   [metabase.util.i18n :refer [tru]]
-   [metabase.util.malli :as mu]))
-
-(defn- query->template-tags
-  [query]
-  (vals (get-in query [:native :template-tags])))
-
-(defn query->tag-card-ids
-  "Returns the card IDs from the template tags of the native query of `query`."
-  [query]
-  (keep :card-id (query->template-tags query)))
-
-(mu/defn tags-referenced-cards :- [:maybe [:sequential ::lib.schema.metadata/card]]
-  "Returns Card instances referenced by the given native `query`."
-  [query]
-  (mapv
-   (fn [card-id]
-     (if-let [card (lib.metadata.protocols/card (qp.store/metadata-provider) card-id)]
-       card
-       (throw (ex-info (tru "Referenced question #{0} could not be found" (str card-id))
-                       {:card-id card-id}))))
-   (query->tag-card-ids query)))
diff --git a/src/metabase/shared/util/i18n.clj b/src/metabase/shared/util/i18n.clj
index 1f07d0f93c9067fb485eff348dda22260c3cd0da..16b77d5d317d48a41a5b3527c9121bdfa4de098e 100644
--- a/src/metabase/shared/util/i18n.clj
+++ b/src/metabase/shared/util/i18n.clj
@@ -8,6 +8,7 @@
   Placeholders should use `gettext` format e.g. `{0}`, `{1}`, and so forth.
 
     (tru \"Number of cans: {0}\" 2)"
+  {:style/indent [:form]}
   [format-string & args]
   (macros/case
     :clj
@@ -24,6 +25,7 @@
 
   NOTE: When called from ClojureScript, this function behaves identically to `tru`. The originating JS callsite must
   temporarily override the locale used by ttag using the `withInstanceLocalization` wrapper function."
+  {:style/indent [:form]}
   [format-string & args]
   (macros/case
     :clj
@@ -38,6 +40,7 @@
   "i18n a string with both singular and plural forms, using the current user's locale. The appropriate plural form will
   be returned based on the value of `n`. `n` can be interpolated into the format strings using the `{0}`
   syntax. (Other placeholders are not supported)."
+  {:style/indent [:form]}
   [format-string format-string-pl n]
   (macros/case
     :clj
@@ -50,6 +53,7 @@
   "i18n a string with both singular and plural forms, using the site's locale. The appropriate plural form will be
   returned based on the value of `n`. `n` can be interpolated into the format strings using the `{0}` syntax. (Other
   placeholders are not supported)."
+  {:style/indent [:form]}
   [format-string format-string-pl n]
   (macros/case
     :clj
diff --git a/test/metabase/lib/metadata_test.cljc b/test/metabase/lib/metadata_test.cljc
index a897fa950e67498b7531fa4a0aee1357516cadf1..8d2cad964f2040ebad05101dd72f9fc1c9f0786e 100644
--- a/test/metabase/lib/metadata_test.cljc
+++ b/test/metabase/lib/metadata_test.cljc
@@ -63,3 +63,13 @@
      :cljs
      ;; `Integer/MAX_VALUE`, but I don't know what the Cljs way to do this
      (is (nil? (lib.metadata/table-or-card lib.tu/metadata-provider-with-card 2147483647)))))
+
+(deftest ^:parallel bulk-metadata-preserve-order-test
+  (testing "bulk-metadata should return things in the same order as the IDs passed in"
+    (are [ids expected] (= expected
+                           (map :name (lib.metadata/bulk-metadata meta/metadata-provider :metadata/table (map meta/id ids))))
+      [:venues :orders :people]
+      ["VENUES" "ORDERS" "PEOPLE"]
+
+      [:people :orders :venues]
+      ["PEOPLE" "ORDERS" "VENUES"])))
diff --git a/test/metabase/lib/native_test.cljc b/test/metabase/lib/native_test.cljc
index c35a854ea4c77931dff0875532ce53605347996d..938d8a82e144edff9cef6550ecc112aa4ac27e95 100644
--- a/test/metabase/lib/native_test.cljc
+++ b/test/metabase/lib/native_test.cljc
@@ -313,3 +313,35 @@
 
 (deftest ^:parallel engine-test
   (is (= :h2 (lib/engine lib.tu/native-query))))
+
+(deftest ^:parallel template-tag-card-ids-test
+  (let [query (lib/query lib.tu/metadata-provider-with-mock-cards
+                         {:database (meta/id)
+                          :type     :native
+                          :native   {:query         {}
+                                     :template-tags {"tag-name-not-important1" {:type         :card
+                                                                                :display-name "X"
+                                                                                :card-id      1}
+                                                     "tag-name-not-important2" {:type         :card
+                                                                                :display-name "Y"
+                                                                                :card-id      2}}}})]
+    (is (= #{1 2}
+           (lib/template-tag-card-ids query)))))
+
+(deftest ^:parallel template-tags-referenced-cards-test
+  (testing "returns Card instances from raw query"
+    (let [query (lib/query lib.tu/metadata-provider-with-mock-cards
+                  {:database (meta/id)
+                   :type     :native
+                   :native   {:query         {}
+                              :template-tags {"tag-name-not-important1" {:type         :card
+                                                                         :display-name "X"
+                                                                         :card-id      1}
+                                              "tag-name-not-important2" {:type         :card
+                                                                         :display-name "Y"
+                                                                         :card-id      2}}}})]
+      (is (=? [{:id            1
+                :dataset-query {}}
+               {:id            2
+                :dataset-query {}}]
+              (lib/template-tags-referenced-cards query))))))
diff --git a/test/metabase/query_processor/middleware/expand_macros_test.clj b/test/metabase/query_processor/middleware/expand_macros_test.clj
index e804de1e38a32cb819cebc5a1a3e6a4a3e879166..9a15ff44f01cd72756dba4388eb8468c61daa218 100644
--- a/test/metabase/query_processor/middleware/expand_macros_test.clj
+++ b/test/metabase/query_processor/middleware/expand_macros_test.clj
@@ -1,8 +1,10 @@
 (ns metabase.query-processor.middleware.expand-macros-test
   (:require
    [clojure.test :refer :all]
+   [metabase.lib.convert :as lib.convert]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.jvm :as lib.metadata.jvm]
+   [metabase.lib.query :as lib.query]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]
    [metabase.lib.test-util.macros :as lib.tu.macros]
@@ -11,15 +13,30 @@
    [metabase.test :as mt]))
 
 (defn- mbql-query [inner-query]
-  {:database 1, :type :query, :query (merge {:source-table 1}
-                                            inner-query)})
+  {:database (meta/id)
+   :type     :query
+   :query    (merge {:source-table 1}
+                    inner-query)})
+
+(defn- expand-macros
+  "If input is a legacy query, convert to pMBQL, call [[expand-macros/expand-macros]], then convert back to legacy. This
+  way we don't need to update all the tests below right away."
+  [query]
+  (if (:type query) ; legacy query
+    (let [metadata-provider (if (qp.store/initialized?)
+                              (qp.store/metadata-provider)
+                              meta/metadata-provider)]
+      (-> (lib.query/query metadata-provider query)
+          (#'expand-macros/expand-macros)
+          lib.convert/->legacy-MBQL))
+    (#'expand-macros/expand-macros query)))
 
 (deftest ^:parallel basic-expansion-test
   (testing "no Segment or Metric should yield exact same query"
     (is (= (mbql-query
             {:filter   [:> [:field 4 nil] 1]
              :breakout [[:field 17 nil]]})
-           (#'expand-macros/expand-metrics-and-segments
+           (expand-macros
             (mbql-query
              {:filter   [:> [:field 4 nil] 1]
               :breakout [[:field 17 nil]]}))))))
@@ -52,7 +69,7 @@
                          [:is-null [:field 7 nil]]
                          [:> [:field 4 nil] 1]]]
              :breakout [[:field 17 nil]]})
-           (#'expand-macros/expand-metrics-and-segments
+           (expand-macros
             (mbql-query
              {:filter   [:and
                          [:segment 1]
@@ -76,7 +93,7 @@
                  {:filter [:and
                            [:= [:field 5 nil] "abc"]
                            [:> [:field 6 nil] 1]]})
-               (#'expand-macros/expand-metrics-and-segments
+               (expand-macros
                 (lib.tu.macros/mbql-query venues
                   {:filter [:segment 2]}))))))
     ;; Next line makes temporary segment definitions mutually recursive.
@@ -90,7 +107,7 @@
           (is (thrown-with-msg?
                Exception
                #"\QSegment expansion failed. Check mutually recursive segment definitions.\E"
-               (#'expand-macros/expand-metrics-and-segments
+               (expand-macros
                 (lib.tu.macros/mbql-query venues {:filter [:segment 2]})))))))))
 
 (deftest ^:parallel metric-test
@@ -105,7 +122,7 @@
                              [:= [:field 5 nil] "abc"]]
                :breakout    [[:field 17 nil]]
                :order-by    [[:asc [:field 1 nil]]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query
                {:aggregation [[:metric 1]]
                 :filter      [:> [:field 4 nil] 1]
@@ -127,7 +144,7 @@
                :filter       [:= [:field 5 nil] "abc"]
                :breakout     [[:field 17 nil]]
                :order-by     [[:asc [:field 1 nil]]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query
                {:source-table 1000
                 :aggregation  [[:metric 1]]
@@ -147,7 +164,7 @@
                :filter      [:= [:field 5 nil] "abc"]
                :breakout    [[:field 17 nil]]
                :order-by    [[:asc [:field 1 nil]]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query
                {:aggregation [[:metric 1]]
                 :filter      [:= [:field 5 nil] "abc"]
@@ -165,17 +182,20 @@
                                                                :filter      [:and
                                                                              [:between [:field 9 nil] 0 25]
                                                                              [:segment 1]]}}]})
+      (testing "Sanity check: make sure we're overriding the old Metric 1 from mock-metadata-provider"
+        (is (=? {:name "My Metric"}
+                (lib.metadata/metric (qp.store/metadata-provider) 1))))
       (is (= (mbql-query
               {:source-table 1000
                :aggregation  [[:aggregation-options [:sum [:field 18 nil]] {:display-name "My Metric"}]]
                :filter       [:and
                               [:> [:field 4 nil] 1]
-                              [:is-null [:field 7 nil]]
-                              [:between [:field 9 nil] 0 25]
-                              [:= [:field 5 nil] "abc"]]
+                              [:is-null [:field 7 nil]]       ; from Segment 2
+                              [:between [:field 9 nil] 0 25]  ; from Metric 1
+                              [:= [:field 5 nil] "abc"]]      ; from Metric 1 => Segment 1
                :breakout     [[:field 17 nil]]
                :order-by     [[:asc [:field 1 nil]]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query
                {:source-table 1000
                 :aggregation  [[:metric 1]]
@@ -209,19 +229,19 @@
   (testing "make sure that we don't try to expand GA \"metrics\" (#6104)"
     (doseq [metric ["ga:users" "gaid:users"]]
       (is (= (mbql-query {:aggregation [[:metric metric]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query {:aggregation [[:metric metric]]}))))))
   (testing "make sure expansion works with multiple GA \"metrics\" (#7399)"
     (is (= (mbql-query {:aggregation [[:metric "ga:users"]
                                       [:metric "ga:1dayUsers"]]})
-           (#'expand-macros/expand-metrics-and-segments
+           (expand-macros
             (mbql-query {:aggregation [[:metric "ga:users"]
                                        [:metric "ga:1dayUsers"]]}))))))
 
 (deftest ^:parallel dont-expand-ga-segments-test
   (testing "make sure we don't try to expand GA 'segments'"
     (is (= (mbql-query {:filter [:segment "gaid:-11"]})
-           (#'expand-macros/expand-metrics-and-segments
+           (expand-macros
             (mbql-query {:filter [:segment "gaid:-11"]}))))))
 
 (deftest ^:parallel named-metrics-test
@@ -235,7 +255,7 @@
       (is (= (mbql-query
               {:aggregation [[:aggregation-options [:sum [:field 20 nil]] {:display-name "Named Metric"}]]
                :breakout    [[:field 10 nil]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query {:aggregation [[:aggregation-options
                                           [:metric 1] {:display-name "Named Metric"}]]
                            :breakout    [[:field 10 nil]]})))))))
@@ -254,7 +274,7 @@
                               [:sum [:field 20 nil]]
                               {:name "auto_generated_name", :display-name "Metric 1"}]]
                :breakout    [[:field 10 nil]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query {:aggregation [[:aggregation-options
                                           [:metric 1] {:name "auto_generated_name"}]]
                            :breakout    [[:field 10 nil]]})))))))
@@ -265,7 +285,7 @@
       (is (=? (mbql-query
                {:aggregation [[:aggregation-options [:sum [:field 20 nil]] {:display-name "My Cool Aggregation"}]]
                 :breakout    [[:field 10 nil]]})
-              (#'expand-macros/expand-metrics-and-segments
+              (expand-macros
                (mbql-query {:aggregation [[:metric 1]]
                             :breakout    [[:field 10 nil]]})))))))
 
@@ -284,7 +304,7 @@
                               [:sum [:field 20 nil]]
                               {:name "auto_generated_name", :display-name "Metric 1"}]]
                :breakout    [[:field 10 nil]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query {:aggregation [[:metric 1]]
                            :breakout    [[:field 10 nil]]})))))))
 
@@ -297,7 +317,7 @@
                                       [:or
                                        [:is-null [:field 7 nil]]
                                        [:> [:field 4 nil] 1]]]]]})
-             (#'expand-macros/expand-metrics-and-segments
+             (expand-macros
               (mbql-query
                {:aggregation [[:share [:and
                                        [:segment 1]
@@ -326,19 +346,19 @@
           (testing "nested 1 level"
             (is (= (lib.tu.macros/mbql-query nil
                      {:source-query after})
-                   (expand-macros/expand-macros
+                   (expand-macros
                     (lib.tu.macros/mbql-query nil
                       {:source-query before})))))
           (testing "nested 2 levels"
             (is (= (lib.tu.macros/mbql-query nil
                      {:source-query {:source-query after}})
-                   (expand-macros/expand-macros
+                   (expand-macros
                     (lib.tu.macros/mbql-query nil
                       {:source-query {:source-query before}})))))
           (testing "nested 3 levels"
             (is (= (lib.tu.macros/mbql-query nil
                      {:source-query {:source-query {:source-query after}}})
-                   (expand-macros/expand-macros
+                   (expand-macros
                     (lib.tu.macros/mbql-query nil
                       {:source-query {:source-query {:source-query before}}})))))
           (testing "nested at different levels"
@@ -346,34 +366,70 @@
                      {:source-query (-> after
                                         (dissoc :source-table)
                                         (assoc :source-query after))})
-                   (expand-macros/expand-macros
+                   (expand-macros
                     (lib.tu.macros/mbql-query nil
                       {:source-query (-> before
                                          (dissoc :source-table)
                                          (assoc :source-query before))})))))
           (testing "inside :source-query inside :joins"
             (is (= (lib.tu.macros/mbql-query checkins
-                     {:joins [{:condition    [:= 1 2]
+                     {:joins [{:condition    [:= [:field 1 nil] 2]
                                :source-query after}]})
-                   (expand-macros/expand-macros
+                   (expand-macros
                     (lib.tu.macros/mbql-query checkins
-                      {:joins [{:condition    [:= 1 2]
+                      {:joins [{:condition    [:= [:field 1 nil] 2]
                                 :source-query before}]})))))
           (when (= macro-type "Segments")
             (testing "inside join condition"
               (is (= (lib.tu.macros/mbql-query checkins
                        {:joins [{:source-table $$checkins
                                  :condition    (:filter after)}]})
-                     (expand-macros/expand-macros
+                     (expand-macros
                       (lib.tu.macros/mbql-query checkins
                         {:joins [{:source-table $$checkins
                                   :condition    (:filter before)}]}))))))
           (testing "inside :joins inside :source-query"
             (is (= (lib.tu.macros/mbql-query nil
                      {:source-query {:source-table $$checkins
-                                     :joins        [{:condition    [:= 1 2]
+                                     :joins        [{:condition    [:= [:field 1 nil] 2]
                                                      :source-query after}]}})
-                   (expand-macros/expand-macros (lib.tu.macros/mbql-query nil
+                   (expand-macros (lib.tu.macros/mbql-query nil
                                     {:source-query {:source-table $$checkins
-                                                    :joins        [{:condition    [:= 1 2]
+                                                    :joins        [{:condition    [:= [:field 1 nil] 2]
                                                                     :source-query before}]}}))))))))))
+
+(deftest ^:parallel preserve-uuids-test
+  (testing "the aggregation that replaces a :metric ref should keep the :metric's :lib/uuid, so :aggregation refs pointing to it are still valid"
+    (let [metric            {:lib/type    :metadata/metric
+                             :id          1
+                             :table-id    2
+                             :name        "Revenue"
+                             :description "Sum of orders subtotal"
+                             :archived    false
+                             :definition  {:source-table 2, :aggregation [[:sum [:field 17 nil]]]}}
+          metadata-provider (lib.tu/mock-metadata-provider
+                             meta/metadata-provider
+                             {:metrics [metric]})]
+      (is (=? {:stages                 [{:lib/type     :mbql.stage/mbql
+                                         :source-table (meta/id :orders)
+                                         :aggregation  [[:sum
+                                                         {:lib/uuid "0c586819-5288-4da9-adba-1ae904a34d5e"}
+                                                         [:field {} 17]]]
+                                         :order-by     [[:desc
+                                                         {}
+                                                         [:aggregation
+                                                          {}
+                                                          "0c586819-5288-4da9-adba-1ae904a34d5e"]]]}]
+               :database               (meta/id)}
+              (#'expand-macros/expand-macros
+               {:lib/type               :mbql/query
+                :lib/metadata           metadata-provider
+                :stages                 [{:lib/type     :mbql.stage/mbql
+                                          :source-table (meta/id :orders)
+                                          :aggregation  [[:metric {:lib/uuid "0c586819-5288-4da9-adba-1ae904a34d5e"} 1]]
+                                          :order-by     [[:desc
+                                                          {:lib/uuid "4bfcc7da-1e47-41aa-af2c-bbdf11b8d6be"}
+                                                          [:aggregation
+                                                           {:lib/uuid "7fb46618-622c-40f3-b0d4-4a779179f055"}
+                                                           "0c586819-5288-4da9-adba-1ae904a34d5e"]]]}]
+                :database               (meta/id)}))))))
diff --git a/test/metabase/query_processor/middleware/permissions_test.clj b/test/metabase/query_processor/middleware/permissions_test.clj
index a6387857138c3ff12ba4299c46da02e3ce004a26..7ac7e321ed0ba0f4d4329805d4e63e7d9021fb3b 100644
--- a/test/metabase/query_processor/middleware/permissions_test.clj
+++ b/test/metabase/query_processor/middleware/permissions_test.clj
@@ -11,6 +11,7 @@
    [metabase.query-processor :as qp]
    [metabase.query-processor.middleware.permissions :as qp.perms]
    [metabase.query-processor.pipeline :as qp.pipeline]
+   [metabase.query-processor.setup :as qp.setup]
    [metabase.query-processor.store :as qp.store]
    [metabase.test :as mt]
    [metabase.util :as u]
@@ -22,7 +23,8 @@
   (let [qp (fn [query _rff]
              (qp.pipeline/*result* query))
         qp (qp.perms/check-query-permissions qp)]
-    (qp query (constantly conj))))
+    (qp.setup/with-qp-setup [query query]
+      (qp query (constantly conj)))))
 
 (defn- check-perms-for-rasta
   "Check permissions for `query` with rasta as the current user."
@@ -31,15 +33,17 @@
 
 (def ^:private perms-error-msg #"You do not have permissions to run this query\.")
 
-(deftest ^:parallel native-query-perms-test
+(deftest native-query-perms-test
   (testing "Make sure the NATIVE query fails to run if current user doesn't have perms"
-    (is (thrown-with-msg?
-         ExceptionInfo
-         perms-error-msg
-         (check-perms-for-rasta
-          {:database 1000
-           :type     :native
-           :native   {:query "SELECT * FROM VENUES"}})))))
+    (t2.with-temp/with-temp [:model/Database db {}]
+      (data-perms/set-database-permission! (perms-group/all-users) (u/the-id db) :perms/native-query-editing :no)
+      (is (thrown-with-msg?
+           ExceptionInfo
+           perms-error-msg
+           (check-perms-for-rasta
+            {:database (u/the-id db)
+             :type     :native
+             :native   {:query "SELECT * FROM VENUES"}}))))))
 
 (deftest native-query-perms-test-2
   (testing "...but it should work if user has perms"
@@ -80,15 +84,17 @@
                :type     :query
                :query    {:source-table (u/the-id table)}}))))))
 
-(deftest ^:parallel nested-native-query-test
+(deftest nested-native-query-test
   (testing "Make sure nested native query fails to run if current user doesn't have perms"
-    (is (thrown-with-msg?
-         ExceptionInfo
-         perms-error-msg
-         (check-perms-for-rasta
-          {:database 1000
-           :type     :query
-           :query   {:source-query {:native "SELECT * FROM VENUES"}}})))))
+    (t2.with-temp/with-temp [:model/Database db {}]
+      (data-perms/set-database-permission! (perms-group/all-users) (u/the-id db) :perms/native-query-editing :no)
+      (is (thrown-with-msg?
+           ExceptionInfo
+           perms-error-msg
+           (check-perms-for-rasta
+            {:database (u/the-id db)
+             :type     :query
+             :query   {:source-query {:native "SELECT * FROM VENUES"}}}))))))
 
 (deftest nested-native-query-test-2
   (testing "...but it should work if user has perms [nested native queries]"
@@ -156,23 +162,29 @@
                    Table    _       {:db_id (u/the-id db)}
                    Table    table-2 {:db_id (u/the-id db)}
                    Card     card    {:dataset_query {:database (u/the-id db), :type :query,
-                                                               :query {:source-table (u/the-id table-2)}}}]
+                                                     :query    {:source-table (u/the-id table-2)}}}]
       (let [card-id   (:id card)
             tag-name  (str "#" card-id)
             query-sql (format "SELECT * FROM {{%s}} AS x" tag-name)]
         ;; query should be returned by middleware unchanged
         (is (= {:database (u/the-id db)
-                :type :native
-                :native {:query         query-sql,
-                         :template-tags {tag-name {:id tag-name, :name tag-name, :display-name tag-name, :type "card",
-                                                   :card card-id}}}}
+                :type     :native
+                :native   {:query         query-sql
+                           :template-tags {tag-name {:id           tag-name
+                                                     :name         tag-name
+                                                     :display-name tag-name
+                                                     :type         "card"
+                                                     :card-id      card-id}}}}
                (check-perms-for-rasta
                 {:database (u/the-id db)
                  :type     :native
                  :native   {:query         query-sql
                             :template-tags {tag-name
-                                            {:id tag-name, :name tag-name, :display-name tag-name,
-                                             :type "card", :card card-id}}}})))))))
+                                            {:id           tag-name
+                                             :name         tag-name
+                                             :display-name tag-name
+                                             :type         "card"
+                                             :card-id      card-id}}}})))))))
 
 (deftest template-tags-referenced-queries-test-3
   (testing "Fails for native query referenced in template tag, when user has no perms to referenced query"
@@ -200,23 +212,29 @@
     (mt/with-temp [Database db   {}
                    Card     card {:dataset_query
                                   {:database (u/the-id db), :type :native,
-                                   :native {:query "SELECT 1 AS \"foo\", 2 AS \"bar\", 3 AS \"baz\""}}}]
-      (let [card-id  (:id card)
-            tag-name (str "#" card-id)
+                                   :native   {:query "SELECT 1 AS \"foo\", 2 AS \"bar\", 3 AS \"baz\""}}}]
+      (let [card-id   (:id card)
+            tag-name  (str "#" card-id)
             query-sql (format "SELECT * FROM {{%s}} AS x" tag-name)]
         ;; query should be returned by middleware unchanged
         (is (= {:database (u/the-id db)
                 :type     :native
-                :native   {:query query-sql
-                           :template-tags {tag-name {:id tag-name, :name tag-name, :display-name tag-name, :type "card",
-                                                     :card card-id}}}}
+                :native   {:query         query-sql
+                           :template-tags {tag-name {:id           tag-name
+                                                     :name         tag-name
+                                                     :display-name tag-name
+                                                     :type         "card"
+                                                     :card-id      card-id}}}}
                (check-perms-for-rasta
                 {:database (u/the-id db)
                  :type     :native
                  :native   {:query         query-sql
                             :template-tags {tag-name
-                                            {:id tag-name, :name tag-name, :display-name tag-name,
-                                             :type "card", :card card-id}}}})))))))
+                                            {:id           tag-name
+                                             :name         tag-name
+                                             :display-name tag-name
+                                             :type         "card"
+                                             :card-id      card-id}}}})))))))
 
 (deftest query-action-permissions-test
   (testing "Query action permissions"
diff --git a/test/metabase/query_processor/middleware/resolve_fields_test.clj b/test/metabase/query_processor/middleware/resolve_fields_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..87d8a9209153b38a525db25382dc7cbeabd9d6fb
--- /dev/null
+++ b/test/metabase/query_processor/middleware/resolve_fields_test.clj
@@ -0,0 +1,15 @@
+(ns metabase.query-processor.middleware.resolve-fields-test
+  (:require
+   [clojure.test :refer :all]
+   [metabase.query-processor.middleware.resolve-fields :as qp.resolve-fields]))
+
+(deftest ^:parallel field-ids-test
+  (testing "Field IDs in metadata should get picked up correctly"
+    (is (= #{1}
+           (#'qp.resolve-fields/field-ids
+            {:lib/type :mbql/query
+             :database 1
+             :stages   [{:lib/type           :mbql.stage/mbql
+                         :lib/stage-metadata {:columns [{:lib/type :metadata/column, :id 1}
+                                                        {:lib/type :metadata/column, :id 0}]}
+                         :source-table       1}]})))))
diff --git a/test/metabase/query_processor/middleware/resolve_referenced_test.clj b/test/metabase/query_processor/middleware/resolve_referenced_test.clj
index daf6dbaf2a8843cf037ca6a081ca60ac7c30df85..b24e3532c2f5f46306899c29b1f2552b50640b0f 100644
--- a/test/metabase/query_processor/middleware/resolve_referenced_test.clj
+++ b/test/metabase/query_processor/middleware/resolve_referenced_test.clj
@@ -1,17 +1,14 @@
 (ns metabase.query-processor.middleware.resolve-referenced-test
   (:require
    [clojure.test :refer :all]
+   [metabase.lib.core :as lib]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.jvm :as lib.metadata.jvm]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]
-   [metabase.lib.test-util.macros :as lib.tu.macros]
-   [metabase.query-processor.middleware.parameters-test
-    :refer [card-template-tags]]
-   [metabase.query-processor.middleware.resolve-referenced
-    :as qp.resolve-referenced]
-   [metabase.query-processor.store :as qp.store]
+   [metabase.query-processor.middleware.parameters-test :refer [card-template-tags]]
+   [metabase.query-processor.middleware.resolve-referenced :as qp.resolve-referenced]
    [metabase.test :as mt])
   (:import
    (clojure.lang ExceptionInfo)))
@@ -20,112 +17,73 @@
 
 (deftest ^:parallel resolve-card-resources-test
   (testing "resolve stores source table from referenced card"
-    (qp.store/with-metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
-                                      (lib.metadata.jvm/application-database-metadata-provider (mt/id))
-                                      [(mt/mbql-query venues
-                                         {:filter [:< $price 3]})])
-      (let [query {:database (mt/id)
-                   :native   {:template-tags
-                              {"tag-name-not-important1" {:type    :card
-                                                          :card-id 1}}}}]
-        (is (= query
-               (#'qp.resolve-referenced/resolve-referenced-card-resources* query)))
-        (is (some? (lib.metadata.protocols/cached-metadata
-                    (qp.store/metadata-provider)
-                    :metadata/table
-                    (mt/id :venues))))
-        (is (some? (lib.metadata.protocols/cached-metadata
-                    (qp.store/metadata-provider)
-                    :metadata/column
-                    (mt/id :venues :price))))))))
+    (let [metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
+                             (lib.metadata.jvm/application-database-metadata-provider (mt/id))
+                             [(mt/mbql-query venues
+                                {:filter [:< $price 3]})])
+          query             (lib/query
+                             metadata-provider
+                             {:database (mt/id)
+                              :type     :native
+                              :native   {:query {}
+                                         :template-tags
+                                         {"tag-name-not-important1" {:type         :card
+                                                                     :display-name "X"
+                                                                     :card-id      1}}}})]
+      (is (= query
+             (#'qp.resolve-referenced/resolve-referenced-card-resources query)))
+      (is (some? (lib.metadata.protocols/cached-metadata
+                  metadata-provider
+                  :metadata/table
+                  (mt/id :venues))))
+      (is (some? (lib.metadata.protocols/cached-metadata
+                  metadata-provider
+                  :metadata/column
+                  (mt/id :venues :price)))))))
 
 (deftest ^:parallel referenced-query-from-different-db-test
   (testing "fails on query that references a native query from a different database"
-    (qp.store/with-metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
-                                      meta/metadata-provider
-                                      [{:database (meta/id)
-                                        :type     :native
-                                        :native   {:query "SELECT 1 AS \"foo\", 2 AS \"bar\", 3 AS \"baz\""}}])
-      (let [card-query (:dataset-query (lib.metadata/card (qp.store/metadata-provider) 1))
-            tag-name   "#1"
-            query      {:database 1234
-                        :type     :native
-                        :native   {:query         (format "SELECT * FROM {{%s}} AS x" tag-name)
-                                   :template-tags {tag-name ; This tag's query is from the test db
-                                                   {:id   tag-name, :name    tag-name, :display-name tag-name,
-                                                    :type "card",   :card-id 1}}}}]
-        (is (= {:referenced-query     card-query
-                :expected-database-id 1234}
-               (try
-                 (#'qp.resolve-referenced/check-query-database-id= card-query 1234)
-                 (catch ExceptionInfo exc
-                   (ex-data exc)))))
-        (is (nil? (#'qp.resolve-referenced/check-query-database-id= card-query (meta/id))))
-        (is (thrown-with-msg?
-             clojure.lang.ExceptionInfo
-             #"\QReferenced query is from a different database\E"
-             (#'qp.resolve-referenced/resolve-referenced-card-resources* query)))
-        (is (= {:referenced-query     card-query
-                :expected-database-id 1234}
-               (try
-                 (#'qp.resolve-referenced/resolve-referenced-card-resources* query)
-                 (catch ExceptionInfo exc
-                   (ex-data exc)))))))))
-
-(deftest ^:parallel referenced-query-from-different-db-test-2
-  (testing "fails on query that references an MBQL query from a different database"
-    (qp.store/with-metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
-                                      meta/metadata-provider
-                                      [(lib.tu.macros/mbql-query venues
-                                         {:filter [:< $price 3]})])
-      (let [card-query  (:dataset-query (lib.metadata/card (qp.store/metadata-provider) 1))
-            tag-name    "#1"
-            query-db-id 1234
-            query       {:database query-db-id ; Note outer-query-db is used here
-                         :type     :native
-                         :native   {:query         (format "SELECT * FROM {{%s}} AS x" tag-name)
-                                    :template-tags {tag-name ; This tag's query is from the test db
-                                                    {:id tag-name, :name tag-name, :display-name tag-name,
-                                                     :type "card", :card-id 1}}}}]
-        (is (= {:referenced-query     card-query
-                :expected-database-id query-db-id}
-               (try
-                 (#'qp.resolve-referenced/check-query-database-id= card-query query-db-id)
-                 (catch ExceptionInfo exc
-                   (ex-data exc)))))
-        (is (nil? (#'qp.resolve-referenced/check-query-database-id= card-query (meta/id))))
-        (is (thrown-with-msg?
-             clojure.lang.ExceptionInfo
-             #"\QReferenced query is from a different database\E"
-             (#'qp.resolve-referenced/resolve-referenced-card-resources* query)))
-        (is (= {:referenced-query     card-query
-                :expected-database-id query-db-id}
-               (try
-                 (#'qp.resolve-referenced/resolve-referenced-card-resources* query)
-                 (catch ExceptionInfo exc
-                   (ex-data exc)))))))))
+    (let [metadata-provider meta/metadata-provider
+          tag-name   "#1"
+          query      (lib/query
+                      (lib.tu/mock-metadata-provider
+                       metadata-provider
+                       {:database (assoc (lib.metadata/database metadata-provider) :id 1234)})
+                      {:database 1234
+                       :type     :native
+                       :native   {:query         (format "SELECT * FROM {{%s}} AS x" tag-name)
+                                  :template-tags {tag-name ; This tag's query is from the test db
+                                                  {:id   tag-name, :name    tag-name, :display-name tag-name,
+                                                   :type "card",   :card-id 1}}}})]
+      (is (thrown-with-msg?
+           clojure.lang.ExceptionInfo
+           #"\QCard 1 does not exist, or is from a different Database.\E"
+           (qp.resolve-referenced/resolve-referenced-card-resources query))))))
 
 (deftest ^:parallel circular-referencing-tags-test
   (testing "fails on query with circular referencing sub-queries"
-    (qp.store/with-metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
-                                      meta/metadata-provider
-                                      [{:database (meta/id)
-                                        :type     :native
-                                        :native   {:query         "SELECT * FROM {{#2}} AS c2"
-                                                   :template-tags (card-template-tags [2])}}
-                                       {:database (meta/id)
-                                        :type     :native
-                                        :native   {:query         "SELECT * FROM {{#1}} AS c1"
-                                                   :template-tags (card-template-tags [1])}}])
-      (let [entrypoint-query {:database (meta/id)
+    (let [metadata-provider (lib.tu/metadata-provider-with-cards-for-queries
+                             meta/metadata-provider
+                             [{:database (meta/id)
+                               :type     :native
+                               :native   {:query         "SELECT * FROM {{#2}} AS c2"
+                                          :template-tags (card-template-tags [2])}}
+                              {:database (meta/id)
+                               :type     :native
+                               :native   {:query         "SELECT * FROM {{#1}} AS c1"
+                                          :template-tags (card-template-tags [1])}}])
+          entrypoint-query  (lib/query
+                             metadata-provider
+                             {:database (meta/id)
                               :type     :native
                               :native   {:query         "SELECT * FROM {{#1}}"
-                                         :template-tags (card-template-tags [1])}}]
-        (is (thrown?
-             ExceptionInfo
-             (#'qp.resolve-referenced/check-for-circular-references entrypoint-query)))
-        (try
-          (#'qp.resolve-referenced/check-for-circular-references entrypoint-query)
-          (catch ExceptionInfo e
-            (is (= (#'qp.resolve-referenced/circular-ref-error 2 1)
+                                         :template-tags (card-template-tags [1])}})]
+      (is (thrown?
+           ExceptionInfo
+           (#'qp.resolve-referenced/check-for-circular-references entrypoint-query)))
+      (try
+        (#'qp.resolve-referenced/check-for-circular-references entrypoint-query)
+        (catch ExceptionInfo e
+          (testing e
+            (is (= (#'qp.resolve-referenced/circular-ref-error entrypoint-query 2 1)
                    (ex-message e)))))))))
diff --git a/test/metabase/query_processor/middleware/resolve_source_table_test.clj b/test/metabase/query_processor/middleware/resolve_source_table_test.clj
index 3b8e73773467c31d214b319498765d07d5c8e641..b78d1df0ddffdc49401a29132ddb8d5d2af3371c 100644
--- a/test/metabase/query_processor/middleware/resolve_source_table_test.clj
+++ b/test/metabase/query_processor/middleware/resolve_source_table_test.clj
@@ -1,75 +1,51 @@
 (ns metabase.query-processor.middleware.resolve-source-table-test
   (:require
    [clojure.test :refer :all]
+   [metabase.lib.core :as lib]
+   [metabase.lib.metadata.jvm :as lib.metadata.jvm]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
-   [metabase.models.database :refer [Database]]
-   [metabase.models.table :refer [Table]]
-   [metabase.query-processor.middleware.resolve-source-table
-    :as qp.resolve-source-table]
-   [metabase.query-processor.store :as qp.store]
+   [metabase.query-processor.middleware.resolve-source-table :as qp.resolve-source-table]
    [metabase.test :as mt]
    [toucan2.tools.with-temp :as t2.with-temp]))
 
-(defn- store-contents
+(defn- cached-metadata
   "Fetch the names of all the objects currently in the QP Store."
-  []
-  (let [provider (qp.store/metadata-provider)
-        tables   [:venues :categories :users :checkins]]
+  [metadata-provider]
+  (let [tables [:venues :categories :users :checkins]]
     {:tables (into #{}
                    (keep (fn [table]
-                           (:name (lib.metadata.protocols/cached-metadata provider :metadata/table (mt/id table)))))
+                           (:name (lib.metadata.protocols/cached-metadata metadata-provider :metadata/table (mt/id table)))))
                    tables)}))
 
-(defn- resolve-source-tables [query]
-  (qp.resolve-source-table/resolve-source-tables query))
-
-(defn- do-with-store-contents [thunk]
-  (qp.store/with-metadata-provider (mt/id)
-    (thunk)
-    (store-contents)))
-
-(defmacro ^:private with-store-contents {:style/indent 0} [& body]
-  `(do-with-store-contents (fn [] ~@body)))
-
-(defn- resolve-and-return-store-contents [query]
-  (with-store-contents
-    (resolve-source-tables query)))
+(defn- resolve-and-return-cached-metadata [query]
+  (let [metadata-provider (lib.metadata.jvm/application-database-metadata-provider (mt/id))
+        query             (lib/query metadata-provider query)]
+    (qp.resolve-source-table/resolve-source-tables query)
+    (cached-metadata metadata-provider)))
 
 (deftest ^:parallel basic-test
   (testing "does `resolve-source-tables` resolve source tables?"
     (is (= {:tables #{"VENUES"}}
-           (resolve-and-return-store-contents (mt/mbql-query venues))))))
+           (resolve-and-return-cached-metadata (mt/mbql-query venues))))))
 
 (deftest ^:parallel validate-database-test
   (testing "If the Table does not belong to the current Database, does it throw an Exception?"
-    (t2.with-temp/with-temp [Database {database-id :id} {}
-                             Table    {table-id :id}    {:db_id database-id}]
+    (t2.with-temp/with-temp [:model/Database {database-id :id} {}
+                             :model/Table    {table-id :id}    {:db_id database-id}]
       (is (thrown-with-msg?
            clojure.lang.ExceptionInfo
-           #"\QFailed to fetch :metadata/table\E"
-           (resolve-and-return-store-contents
+           #"Failed to fetch :metadata/table \d+: either it does not exist, or it belongs to a different Database"
+           (resolve-and-return-cached-metadata
             {:database (mt/id)
              :type     :query
              :query    {:source-table table-id}}))))))
 
 (deftest ^:parallel validate-source-table-test
   (testing "Should throw an Exception if there's a `:source-table` in the query that IS NOT a positive int"
-    (is (thrown-with-msg?
-         clojure.lang.ExceptionInfo
-         #"\QInvalid :source-table 'ABC': should be resolved to a Table ID by now\E"
-         (resolve-and-return-store-contents
-          {:database (mt/id)
-           :type     :query
-           :query    {:source-table "ABC"}})))))
-
-(deftest ^:parallel validate-source-table-test-2
-  (testing "Should throw an Exception if there's a `:source-table` in the query that IS NOT a positive int"
-    ;; TODO -- a little weird that this triggers a schema validation error while the string Table ID gets a more
-    ;; useful error message
     (is (thrown-with-msg?
          clojure.lang.ExceptionInfo
          #"Invalid output:.*should be a positive int, got: 0"
-         (resolve-and-return-store-contents
+         (resolve-and-return-cached-metadata
           {:database (mt/id)
            :type     :query
            :query    {:source-table 0}})))))
@@ -77,19 +53,19 @@
 (deftest ^:parallel nested-queries-test
   (testing "Does `resolve-source-tables` resolve source tables in nested source queries?"
     (is (= {:tables #{"VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query nil
               {:source-query {:source-table $$venues}}))))
 
     (is (= {:tables #{"VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query nil
               {:source-query {:source-query {:source-table $$venues}}}))))))
 
 (deftest ^:parallel joins-test
   (testing "Does `resolve-source-tables` resolve source tables in joins?"
     (is (= {:tables #{"CATEGORIES" "VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query venues
               {:joins [{:source-table $$categories
                         :alias        "c"
@@ -98,7 +74,7 @@
 (deftest ^:parallel joins-in-nested-queries-test
   (testing "Does `resolve-source-tables` resolve source tables in joins inside nested source queries?"
     (is (= {:tables #{"CATEGORIES" "VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query venues
               {:source-query {:source-table $$venues
                               :joins        [{:source-table $$categories
@@ -108,7 +84,7 @@
 (deftest ^:parallel nested-queries-in-joins-test
   (testing "Does `resolve-source-tables` resolve source tables inside nested source queries inside joins?"
     (is (= {:tables #{"CATEGORIES" "VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query venues
               {:joins [{:source-query {:source-table $$categories}
                         :alias        "c"
@@ -118,7 +94,7 @@
   (testing (str "Does `resolve-source-tables` resolve source tables inside nested source queries inside joins inside "
                 "nested source queries?")
     (is (= {:tables #{"CATEGORIES" "VENUES"}}
-           (resolve-and-return-store-contents
+           (resolve-and-return-cached-metadata
             (mt/mbql-query venues
               {:source-query {:source-table $$venues
                               :joins        [{:source-query {:source-table $$categories}
diff --git a/test/metabase/query_processor/middleware/upgrade_field_literals_test.clj b/test/metabase/query_processor/middleware/upgrade_field_literals_test.clj
index ea2238e0b3b3675851a981239ad51b0731e2c1ac..b1d67c417b61ae17eb1d1ef21d83fd04733a6a6f 100644
--- a/test/metabase/query_processor/middleware/upgrade_field_literals_test.clj
+++ b/test/metabase/query_processor/middleware/upgrade_field_literals_test.clj
@@ -44,14 +44,14 @@
              {:aggregation     [[:count]]
               :breakout        [!week.date]
               :filter          [:between !week.date "2014-02-01T00:00:00-08:00" "2014-05-01T00:00:00-07:00"]
-              :source-query    source-query
+              :source-query    (:query source-query)
               :source-metadata source-metadata})
            (upgrade-field-literals
             (mt/mbql-query nil
               {:aggregation     [[:count]]
                :breakout        [!week.*DATE/Date]
                :filter          [:between !week.*DATE/Date "2014-02-01T00:00:00-08:00" "2014-05-01T00:00:00-07:00"]
-               :source-query    source-query
+               :source-query    (:query source-query)
                :source-metadata source-metadata}))))))
 
 (deftest support-legacy-filter-clauses-test
diff --git a/test/metabase/query_processor/util/tag_referenced_cards_test.clj b/test/metabase/query_processor/util/tag_referenced_cards_test.clj
deleted file mode 100644
index 0dd1dadb24e3231518412d652b8b251b14c97c66..0000000000000000000000000000000000000000
--- a/test/metabase/query_processor/util/tag_referenced_cards_test.clj
+++ /dev/null
@@ -1,25 +0,0 @@
-(ns metabase.query-processor.util.tag-referenced-cards-test
-  (:require
-   [clojure.test :refer :all]
-   [metabase.models.card :refer [Card]]
-   [metabase.query-processor.store :as qp.store]
-   [metabase.query-processor.util.tag-referenced-cards
-    :as qp.u.tag-referenced-cards]
-   [metabase.test :as mt]))
-
-(deftest tags-referenced-cards-lookup-test
-  (testing "returns Card instances from raw query"
-    (mt/with-temp [Card c1 {}
-                   Card c2 {}]
-      (qp.store/with-metadata-provider (mt/id)
-        (is (=? [{:id            (:id c1)
-                  :dataset-query {}}
-                 {:id            (:id c2)
-                  :dataset-query {}}]
-                (qp.u.tag-referenced-cards/tags-referenced-cards
-                 {:native
-                  {:template-tags
-                   {"tag-name-not-important1" {:type    :card
-                                               :card-id (:id c1)}
-                    "tag-name-not-important2" {:type    :card
-                                               :card-id (:id c2)}}}})))))))