diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc
index a98506dab0cbc95094d7eb8d2bf651e5d9137af4..1d4f84daa4a9e2b809419d68c1537d314b2e7f8f 100644
--- a/src/metabase/lib/aggregation.cljc
+++ b/src/metabase/lib/aggregation.cljc
@@ -2,6 +2,7 @@
   (:refer-clojure :exclude [count distinct max min])
   (:require
    [metabase.lib.common :as lib.common]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
@@ -71,6 +72,8 @@
     (str "count_" (lib.metadata.calculation/column-name query stage-number x))
     "count"))
 
+(lib.hierarchy/derive :count ::aggregation)
+
 (defmethod lib.metadata.calculation/display-name-method :case
   [_query _stage-number _case]
   (i18n/tru "Case"))
@@ -79,85 +82,53 @@
   [_query _stage-number _case]
   "case")
 
-(defmethod lib.metadata.calculation/display-name-method :distinct
-  [query stage-number [_distinct _opts x]]
-  (i18n/tru "Distinct values of {0}"  (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :distinct
-  [query stage-number [_distinct _opts x]]
-  (str "distinct_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :avg
-  [query stage-number [_avg _opts x]]
-  (i18n/tru "Average of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :avg
-  [query stage-number [_avg _opts x]]
-  (str "avg_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :cum-count
-  [query stage-number [_cum-count _opts x]]
-  (i18n/tru "Cumulative count of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :cum-count
-  [query stage-number [_avg _opts x]]
-  (str "cum_count_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :sum
-  [query stage-number [_sum _opts x]]
-  (i18n/tru "Sum of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :sum
-  [query stage-number [_sum _opts x]]
-  (str "sum_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :cum-sum
-  [query stage-number [_cum-sum _opts x]]
-  (i18n/tru "Cumulative sum of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :cum-sum
-  [query stage-number [_avg _opts x]]
-  (str "cum_sum_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :stddev
-  [query stage-number [_stddev _opts x]]
-  (i18n/tru "Standard deviation of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :stddev
-  [query stage-number [_avg _opts x]]
-  (str "std_dev_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :min
-  [query stage-number [_min _opts x]]
-  (i18n/tru "Min of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :min
-  [query stage-number [_min _opts x]]
-  (str "min_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :max
-  [query stage-number [_max _opts x]]
-  (i18n/tru "Max of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :max
-  [query stage-number [_max _opts x]]
-  (str "max_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :var
-  [query stage-number [_var _opts x]]
-  (i18n/tru "Variance of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :var
-  [query stage-number [_var _opts x]]
-  (str "var_" (lib.metadata.calculation/column-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/display-name-method :median
-  [query stage-number [_median _opts x]]
-  (i18n/tru "Median of {0}" (lib.metadata.calculation/display-name query stage-number x)))
-
-(defmethod lib.metadata.calculation/column-name-method :median
-  [query stage-number [_median _opts x]]
-  (str "median_" (lib.metadata.calculation/column-name query stage-number x)))
+(lib.hierarchy/derive :case ::aggregation)
+
+(lib.hierarchy/derive ::unary-aggregation ::aggregation)
+
+(doseq [tag [:avg
+             :cum-count
+             :cum-sum
+             :distinct
+             :max
+             :median
+             :min
+             :stddev
+             :sum
+             :var]]
+  (lib.hierarchy/derive tag ::unary-aggregation))
+
+(defmethod lib.metadata.calculation/column-name-method ::unary-aggregation
+  [query stage-number [tag _opts arg]]
+  (let [arg (lib.metadata.calculation/column-name-method query stage-number arg)]
+    (str
+     (case tag
+       :avg       "avg_"
+       :cum-count "cum_count_"
+       :cum-sum   "cum_sum_"
+       :distinct  "distinct_"
+       :max       "max_"
+       :median    "median_"
+       :min       "min_"
+       :stddev    "std_dev_"
+       :sum       "sum_"
+       :var       "var_")
+     arg)))
+
+(defmethod lib.metadata.calculation/display-name-method ::unary-aggregation
+  [query stage-number [tag _opts arg]]
+  (let [arg (lib.metadata.calculation/display-name query stage-number arg)]
+    (case tag
+      :avg       (i18n/tru "Average of {0}"            arg)
+      :cum-count (i18n/tru "Cumulative count of {0}"   arg)
+      :cum-sum   (i18n/tru "Cumulative sum of {0}"     arg)
+      :distinct  (i18n/tru "Distinct values of {0}"    arg)
+      :max       (i18n/tru "Max of {0}"                arg)
+      :median    (i18n/tru "Median of {0}"             arg)
+      :min       (i18n/tru "Min of {0}"                arg)
+      :stddev    (i18n/tru "Standard deviation of {0}" arg)
+      :sum       (i18n/tru "Sum of {0}"                arg)
+      :var       (i18n/tru "Variance of {0}"           arg))))
 
 (defmethod lib.metadata.calculation/display-name-method :percentile
   [query stage-number [_percentile _opts x p]]
@@ -167,16 +138,22 @@
   [query stage-number [_percentile _opts x p]]
   (lib.util/format "p%d_%s" p (lib.metadata.calculation/column-name query stage-number x)))
 
-;;; we don't currently have sophisticated logic for generating nice display names for filter clauses
+(lib.hierarchy/derive :percentile ::aggregation)
+
+;;; we don't currently have sophisticated logic for generating nice display names for filter clauses.
+;;;
+;;; TODO : wait a minute, we do have that stuff now!
 
 (defmethod lib.metadata.calculation/display-name-method :sum-where
   [query stage-number [_sum-where _opts x _pred]]
   (i18n/tru "Sum of {0} matching condition" (lib.metadata.calculation/display-name query stage-number x)))
 
 (defmethod lib.metadata.calculation/column-name-method :sum-where
-  [query stage-number [_sum-where _opts x]]
+  [query stage-number [_sum-where _opts x _pred]]
   (str "sum_where_" (lib.metadata.calculation/column-name query stage-number x)))
 
+(lib.hierarchy/derive :sum-where ::aggregation)
+
 (defmethod lib.metadata.calculation/display-name-method :share
   [_query _stage-number _share]
   (i18n/tru "Share of rows matching condition"))
@@ -185,6 +162,8 @@
   [_query _stage-number _share]
   "share")
 
+(lib.hierarchy/derive :share ::aggregation)
+
 (defmethod lib.metadata.calculation/display-name-method :count-where
   [_query _stage-number _count-where]
   (i18n/tru "Count of rows matching condition"))
@@ -193,18 +172,29 @@
   [_query _stage-number _count-where]
   "count-where")
 
-(lib.common/defop count [] [x])
-(lib.common/defop avg [x])
+(lib.hierarchy/derive :count-where ::aggregation)
+
+(defmethod lib.metadata.calculation/metadata-method ::aggregation
+  [query stage-number [_tag _opts first-arg :as clause]]
+  (merge
+   ;; flow the `:options` from the field we're aggregating. This is important, for some reason.
+   ;; See [[metabase.query-processor-test.aggregation-test/field-settings-for-aggregate-fields-test]]
+   (when first-arg
+     (select-keys (lib.metadata.calculation/metadata query stage-number first-arg) [:settings]))
+   ((get-method lib.metadata.calculation/metadata-method :default) query stage-number clause)))
+
+(lib.common/defop count       [] [x])
+(lib.common/defop avg         [x])
 (lib.common/defop count-where [x y])
-(lib.common/defop distinct [x])
-(lib.common/defop max [x])
-(lib.common/defop median [x])
-(lib.common/defop min [x])
-(lib.common/defop percentile [x y])
-(lib.common/defop share [x])
-(lib.common/defop stddev [x])
-(lib.common/defop sum [x])
-(lib.common/defop sum-where [x y])
+(lib.common/defop distinct    [x])
+(lib.common/defop max         [x])
+(lib.common/defop median      [x])
+(lib.common/defop min         [x])
+(lib.common/defop percentile  [x y])
+(lib.common/defop share       [x])
+(lib.common/defop stddev      [x])
+(lib.common/defop sum         [x])
+(lib.common/defop sum-where   [x y])
 
 (mu/defn aggregate :- ::lib.schema/query
   "Adds an aggregation to query."
@@ -220,10 +210,13 @@
 
 (mu/defn aggregations :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
   "Get metadata about the aggregations in a given stage of a query."
-  [query        :- ::lib.schema/query
-   stage-number :- :int]
-  (when-let [aggregation-exprs (not-empty (:aggregation (lib.util/query-stage query stage-number)))]
-    (map-indexed (fn [i aggregation]
-                   (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation)]
-                     (assoc metadata :lib/source :source/aggregations, ::aggregation-index i)))
-                 aggregation-exprs)))
+  ([query]
+   (aggregations query -1))
+
+  ([query        :- ::lib.schema/query
+    stage-number :- :int]
+   (when-let [aggregation-exprs (not-empty (:aggregation (lib.util/query-stage query stage-number)))]
+     (map-indexed (fn [i aggregation]
+                    (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation)]
+                      (assoc metadata :lib/source :source/aggregations, ::aggregation-index i)))
+                  aggregation-exprs))))
diff --git a/src/metabase/lib/common.cljc b/src/metabase/lib/common.cljc
index cda9d9be0ab0807e574f77de58dd8a8bdad0e0d9..436467eb5acbda272a0b4de1401f8f76acfb35ea 100644
--- a/src/metabase/lib/common.cljc
+++ b/src/metabase/lib/common.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.common
   (:require
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.options :as lib.options]
    [metabase.lib.ref :as lib.ref]
    [metabase.lib.schema.common :as schema.common]
@@ -23,7 +24,8 @@
   "Ensures that clause arguments are properly unwrapped"
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod ->op-arg :default
   [_query _stage-number x]
diff --git a/src/metabase/lib/convert.cljc b/src/metabase/lib/convert.cljc
index d92cfa8c7bb2d8e6cdc5679a2f26b6bf215f98b9..261f8aa874119938852e36693801a704df6634da 100644
--- a/src/metabase/lib/convert.cljc
+++ b/src/metabase/lib/convert.cljc
@@ -61,11 +61,12 @@
   [[_tag x y]]
   (let [[id-or-name options] (if (map? x)
                                [y x]
-                               [x y])
-        options              (cond-> options
-                               (not (:lib/uuid options))
-                               (assoc :lib/uuid (str (random-uuid))))]
-    [:field options id-or-name]))
+                               [x y])]
+    (lib.options/ensure-uuid [:field options id-or-name])))
+
+(defmethod ->pMBQL :value
+  [[_tag value opts]]
+  (lib.options/ensure-uuid [:value opts value]))
 
 (defmethod ->pMBQL :aggregation-options
   [[_tag aggregation options]]
@@ -172,6 +173,12 @@
      (->legacy-MBQL id)
      (not-empty (disqualify opts))]))
 
+(defmethod ->legacy-MBQL :value
+  [[_tag opts value]]
+  (if-let [opts (not-empty (disqualify opts))]
+    [:value value opts]
+    [:value value]))
+
 (defmethod ->legacy-MBQL :mbql/join [join]
   (let [base (disqualify join)]
     (merge (-> base
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index f47949feab45c9c42170d7a0a9aedb173b48b2d7..fdf106cd52453c0888ffc67a3a7998373389b245 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -47,6 +47,7 @@
 
 (shared.ns/import-fns
   [lib.aggregation
+   aggregations
    aggregate
    count
    avg
@@ -145,7 +146,8 @@
    describe-query
    describe-top-level-key
    display-name
-   suggested-name]
+   suggested-name
+   type-of]
   [lib.native
    #?@(:cljs [->TemplateTags
               TemplateTags->])
diff --git a/src/metabase/lib/equality.cljc b/src/metabase/lib/equality.cljc
index 4fcdeb0ec24f07fbba437b1495a5852e2977a7ff..d046c5029a598487ebb47f30913d1a1e550eb143 100644
--- a/src/metabase/lib/equality.cljc
+++ b/src/metabase/lib/equality.cljc
@@ -2,7 +2,8 @@
   "Logic for determining whether two pMBQL queries are equal."
   (:refer-clojure :exclude [=])
   (:require
-   [metabase.lib.dispatch :as lib.dispatch]))
+   [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]))
 
 (defmulti =
   "Determine whether two already-normalized pMBQL maps, clauses, or other sorts of expressions are equal. The basic rule
@@ -16,7 +17,8 @@
           y-dispatch-value (lib.dispatch/dispatch-value y)]
       (if (not= x-dispatch-value y-dispatch-value)
         ::different-dispatch-values
-        x-dispatch-value))))
+        x-dispatch-value)))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod = ::different-dispatch-values
   [_x _y]
diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc
index 395f54c036eeb41c02564a5246121352638a3702..d460c3ad3b6a03389df5e7552b40d34c489acaa9 100644
--- a/src/metabase/lib/expression.cljc
+++ b/src/metabase/lib/expression.cljc
@@ -5,6 +5,7 @@
   (:require
    [clojure.string :as str]
    [metabase.lib.common :as lib.common]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
@@ -14,6 +15,7 @@
     :as lib.schema.temporal-bucketing]
    [metabase.lib.util :as lib.util]
    [metabase.shared.util.i18n :as i18n]
+   [metabase.types :as types]
    [metabase.util.malli :as mu]))
 
 (mu/defn column-metadata->expression-ref :- :mbql.clause/expression
@@ -38,19 +40,18 @@
                          :query           query
                          :stage-number    stage-number})))))
 
-(defmethod lib.schema.expression/type-of* :lib/external-op
-  [{:keys [operator options args] :or {options {}}}]
-  (lib.schema.expression/type-of* (into [(keyword operator) options] args)))
+(defmethod lib.metadata.calculation/type-of-method :expression
+  [query stage-number [_expression _opts expression-name, :as _expression-ref]]
+  (let [expression (resolve-expression query stage-number expression-name)]
+    (lib.metadata.calculation/type-of query stage-number expression)))
 
 (defmethod lib.metadata.calculation/metadata-method :expression
-  [query stage-number [_expression opts expression-name, :as expression-ref]]
-  (let [expression (resolve-expression query stage-number expression-name)]
-    {:lib/type     :metadata/field
-     :lib/source   :source/expressions
-     :name         expression-name
-     :display_name (lib.metadata.calculation/display-name query stage-number expression-ref)
-     :base_type    (or (:base-type opts)
-                       (lib.schema.expression/type-of expression))}))
+  [query stage-number [_expression _opts expression-name, :as expression-ref]]
+  {:lib/type     :metadata/field
+   :name         expression-name
+   :display_name (lib.metadata.calculation/display-name query stage-number expression-ref)
+   :base_type    (lib.metadata.calculation/type-of query stage-number expression-ref)
+   :lib/source   :source/expressions})
 
 (defmethod lib.metadata.calculation/display-name-method :dispatch-type/number
   [_query _stage-number n]
@@ -87,21 +88,18 @@
                (map (partial lib.metadata.calculation/display-name query stage-number)
                     args)))))
 
-(defmethod lib.metadata.calculation/display-name-method :+
-  [query stage-number [_plus _opts & args]]
-  (infix-display-name query stage-number "+" args))
-
-(defmethod lib.metadata.calculation/display-name-method :-
-  [query stage-number [_minute _opts & args]]
-  (infix-display-name query stage-number "-" args))
+(def ^:private infix-operator-display-name
+  {:+ "+"
+   :- "-"
+   :* "×"
+   :/ "÷"})
 
-(defmethod lib.metadata.calculation/display-name-method :/
-  [query stage-number [_divide _opts & args]]
-  (infix-display-name query stage-number "÷" args))
+(doseq [tag [:+ :- :/ :*]]
+  (lib.hierarchy/derive tag ::infix-operator))
 
-(defmethod lib.metadata.calculation/display-name-method :*
-  [query stage-number [_multiply _opts & args]]
-  (infix-display-name query stage-number "×" args))
+(defmethod lib.metadata.calculation/display-name-method ::infix-operator
+  [query stage-number [tag _opts & args]]
+  (infix-display-name query stage-number (get infix-operator-display-name tag) args))
 
 (defn- infix-column-name
   [query stage-number operator-str args]
@@ -109,48 +107,73 @@
             (map (partial lib.metadata.calculation/column-name query stage-number)
                  args)))
 
-(defmethod lib.metadata.calculation/column-name-method :+
-  [query stage-number [_plus _opts & args]]
-  (infix-column-name query stage-number "plus" args))
-
-(defmethod lib.metadata.calculation/column-name-method :-
-  [query stage-number [_minute _opts & args]]
-  (infix-column-name query stage-number "minus" args))
-
-(defmethod lib.metadata.calculation/column-name-method :/
-  [query stage-number [_divide _opts & args]]
-  (infix-column-name query stage-number "divided_by" args))
-
-(defmethod lib.metadata.calculation/column-name-method :*
-  [query stage-number [_multiply _opts & args]]
-  (infix-column-name query stage-number "times" args))
+(def ^:private infix-operator-column-name
+  {:+ "plus"
+   :- "minus"
+   :/ "divided_by"
+   :* "times"})
+
+(defmethod lib.metadata.calculation/column-name-method ::infix-operator
+  [query stage-number [tag _opts & args]]
+  (infix-column-name query stage-number (get infix-operator-column-name tag) args))
+
+;;; `:+`, `:-`, and `:*` all have the same logic; also used for [[metabase.lib.schema.expression/type-of]].
+;;;
+;;; `:lib.type-of/type-is-type-of-arithmetic-args` is defined in [[metabase.lib.schema.expression.arithmetic]]
+(defmethod lib.metadata.calculation/type-of-method :lib.type-of/type-is-type-of-arithmetic-args
+  [query stage-number [_tag _opts & args]]
+  ;; Okay to use reduce without an init value here since we know we have >= 2 args
+  #_{:clj-kondo/ignore [:reduce-without-init]}
+  (reduce
+   types/most-specific-common-ancestor
+   (for [arg args]
+     (lib.metadata.calculation/type-of query stage-number arg))))
+
+(defn- interval-unit-str [amount unit]
+  (clojure.core/case unit
+    :millisecond (i18n/trun "millisecond" "milliseconds" (clojure.core/abs amount))
+    :second      (i18n/trun "second"      "seconds"      (clojure.core/abs amount))
+    :minute      (i18n/trun "minute"      "minutes"      (clojure.core/abs amount))
+    :hour        (i18n/trun "hour"        "hours"        (clojure.core/abs amount))
+    :day         (i18n/trun "day"         "days"         (clojure.core/abs amount))
+    :week        (i18n/trun "week"        "weeks"        (clojure.core/abs amount))
+    :month       (i18n/trun "month"       "months"       (clojure.core/abs amount))
+    :quarter     (i18n/trun "quarter"     "quarters"     (clojure.core/abs amount))
+    :year        (i18n/trun "year"        "years"        (clojure.core/abs amount))))
 
 (mu/defn ^:private interval-display-name  :- ::lib.schema.common/non-blank-string
   "e.g. something like \"- 2 days\""
   [amount :- :int
    unit   :- ::lib.schema.temporal-bucketing/unit.date-time.interval]
   ;; TODO -- sorta duplicated with [[metabase.shared.parameters.parameters/translated-interval]], but not exactly
-  (let [unit-str (clojure.core/case unit
-                   :millisecond (i18n/trun "millisecond" "milliseconds" (clojure.core/abs amount))
-                   :second      (i18n/trun "second"      "seconds"      (clojure.core/abs amount))
-                   :minute      (i18n/trun "minute"      "minutes"      (clojure.core/abs amount))
-                   :hour        (i18n/trun "hour"        "hours"        (clojure.core/abs amount))
-                   :day         (i18n/trun "day"         "days"         (clojure.core/abs amount))
-                   :week        (i18n/trun "week"        "weeks"        (clojure.core/abs amount))
-                   :month       (i18n/trun "month"       "months"       (clojure.core/abs amount))
-                   :quarter     (i18n/trun "quarter"     "quarters"     (clojure.core/abs amount))
-                   :year        (i18n/trun "year"        "years"        (clojure.core/abs amount)))]
+  (let [unit-str (interval-unit-str amount unit)]
     (wrap-str-in-parens-if-nested
      (if (pos? amount)
-       (lib.util/format "+ %d %s" amount       unit-str)
+       (lib.util/format "+ %d %s" amount                    unit-str)
        (lib.util/format "- %d %s" (clojure.core/abs amount) unit-str)))))
 
+(mu/defn ^:private interval-column-name  :- ::lib.schema.common/non-blank-string
+  "e.g. something like `minus_2_days`"
+  [amount :- :int
+   unit   :- ::lib.schema.temporal-bucketing/unit.date-time.interval]
+  ;; TODO -- sorta duplicated with [[metabase.shared.parameters.parameters/translated-interval]], but not exactly
+  (let [unit-str (interval-unit-str amount unit)]
+    (if (pos? amount)
+      (lib.util/format "plus_%s_%s"  amount                    unit-str)
+      (lib.util/format "minus_%d_%s" (clojure.core/abs amount) unit-str))))
+
 (defmethod lib.metadata.calculation/display-name-method :datetime-add
   [query stage-number [_datetime-add _opts x amount unit]]
   (str (lib.metadata.calculation/display-name query stage-number x)
        \space
        (interval-display-name amount unit)))
 
+(defmethod lib.metadata.calculation/column-name-method :datetime-add
+  [query stage-number [_datetime-add _opts x amount unit]]
+  (str (lib.metadata.calculation/column-name query stage-number x)
+       \_
+       (interval-column-name amount unit)))
+
 ;;; for now we'll just pretend `:coalesce` isn't a present and just use the display name for the expr it wraps.
 (defmethod lib.metadata.calculation/display-name-method :coalesce
   [query stage-number [_coalesce _opts expr _null-expr]]
diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc
index dbd505dcaebc9e4169be817e123e52caccef6a67..a9497c1f0ce35c12f03d45cea66f019d54895dc0 100644
--- a/src/metabase/lib/field.cljc
+++ b/src/metabase/lib/field.cljc
@@ -81,6 +81,14 @@
     (update metadata :name (fn [field-name]
                              (str parent-name \. field-name)))))
 
+(defmethod lib.metadata.calculation/type-of-method :metadata/field
+  [_query _stage-number field-metadata]
+  ((some-fn :effective_type :base_type) field-metadata))
+
+(defmethod lib.metadata.calculation/type-of-method :field
+  [query stage-number field-ref]
+  (lib.metadata.calculation/type-of query stage-number (resolve-field-metadata query stage-number field-ref)))
+
 (defmethod lib.metadata.calculation/metadata-method :metadata/field
   [_query _stage-number field-metadata]
   field-metadata)
diff --git a/src/metabase/lib/hierarchy.cljc b/src/metabase/lib/hierarchy.cljc
new file mode 100644
index 0000000000000000000000000000000000000000..0e19deeff2204e8a4dbb4a886188d70136aa3c1c
--- /dev/null
+++ b/src/metabase/lib/hierarchy.cljc
@@ -0,0 +1,15 @@
+(ns metabase.lib.hierarchy
+  (:refer-clojure :exclude [derive isa?]))
+
+(defonce ^{:doc "Keyword hierarchy for MLv2 stuff."} hierarchy
+  (atom (make-hierarchy)))
+
+(defn derive
+  "Like [[clojure.core/derive]], but affects [[hierarchy]] rather than the global hierarchy."
+  [tag parent]
+  (swap! hierarchy clojure.core/derive tag parent))
+
+(defn isa?
+  "Like [[clojure.core/isa?]], but uses [[hierarchy]]."
+  [tag parent]
+  (clojure.core/isa? @hierarchy tag parent))
diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc
index 329cda5694fae795c494be3b4e2ee050e5a3c92e..ef5dff225f2033b15f0bf4ac01ef9d557764685d 100644
--- a/src/metabase/lib/join.cljc
+++ b/src/metabase/lib/join.cljc
@@ -2,6 +2,7 @@
   (:require
    [medley.core :as m]
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.options :as lib.options]
@@ -102,7 +103,8 @@
   "Convert something to a join clause."
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 ;; TODO -- should the default implementation call [[metabase.lib.query/query]]? That way if we implement a method to
 ;; create an MBQL query from a `Table`, then we'd also get [[join]] support for free?
@@ -141,7 +143,8 @@
 (defmulti ^:private ->join-condition
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod ->join-condition :default
   [_query _stage-number x]
diff --git a/src/metabase/lib/js/metadata.cljs b/src/metabase/lib/js/metadata.cljs
index e5ab1112051a502fc1a4c40e3fd061aa5718e073..eb52f60d4754cc5053304ede23a2de696232634c 100644
--- a/src/metabase/lib/js/metadata.cljs
+++ b/src/metabase/lib/js/metadata.cljs
@@ -40,6 +40,7 @@
            xform)
           (gobject/getKeys obj))))
 
+;;; this intentionally does not use the lib hierarchy since it's not dealing with MBQL/lib keys
 (defmulti ^:private excluded-fields
   {:arglists '([object-type])}
   keyword)
diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc
index 279827c85604a406541dd920aebca31143ab8821..d0223ad813bc43e349697b37bcc8da76ffd78009 100644
--- a/src/metabase/lib/metadata.cljc
+++ b/src/metabase/lib/metadata.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.metadata
   (:require
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata.protocols :as lib.metadata.protocols]
    [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.id :as lib.schema.id]
@@ -132,7 +133,8 @@
 
 (defmulti ^:private ->metadata-provider*
   {:arglists '([x])}
-  lib.dispatch/dispatch-value)
+  lib.dispatch/dispatch-value
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod ->metadata-provider* :default
   [x]
diff --git a/src/metabase/lib/metadata/calculation.cljc b/src/metabase/lib/metadata/calculation.cljc
index 476360ec23a5a8c96c58865f9c6d58c1bfb8506a..502c387c3a9b442f5445e5dfcf971b246c5c5dcf 100644
--- a/src/metabase/lib/metadata/calculation.cljc
+++ b/src/metabase/lib/metadata/calculation.cljc
@@ -2,6 +2,7 @@
   (:require
    [clojure.string :as str]
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.options :as lib.options]
    [metabase.lib.schema :as lib.schema]
@@ -17,13 +18,15 @@
   "Calculate a nice human-friendly display name for something."
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmulti column-name-method
   "Calculate a database-friendly name to use for something."
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (mu/defn ^:export display-name :- ::lib.schema.common/non-blank-string
   "Calculate a nice human-friendly display name for something."
@@ -47,20 +50,23 @@
 
 (mu/defn column-name :- ::lib.schema.common/non-blank-string
   "Calculate a database-friendly name to use for an expression."
-  [query        :- ::lib.schema/query
-   stage-number :- :int
-   x]
-  (or
-   ;; if this is an MBQL clause with `:name` in the options map, then use that rather than calculating a name.
-   (:name (lib.options/options x))
-   (try
-     (column-name-method query stage-number x)
-     (catch #?(:clj Throwable :cljs js/Error) e
-       (throw (ex-info (i18n/tru "Error calculating column name for {0}: {1}" (pr-str x) (ex-message e))
-                       {:x            x
-                        :query        query
-                        :stage-number stage-number}
-                       e))))))
+  ([query x]
+   (column-name query -1 x))
+
+  ([query        :- ::lib.schema/query
+    stage-number :- :int
+    x]
+   (or
+    ;; if this is an MBQL clause with `:name` in the options map, then use that rather than calculating a name.
+    (:name (lib.options/options x))
+    (try
+      (column-name-method query stage-number x)
+      (catch #?(:clj Throwable :cljs js/Error) e
+        (throw (ex-info (i18n/tru "Error calculating column name for {0}: {1}" (pr-str x) (ex-message e))
+                        {:x            x
+                         :query        query
+                         :stage-number stage-number}
+                        e)))))))
 
 (defmethod display-name-method :default
   [_query _stage-number x]
@@ -76,10 +82,14 @@
     ;; anything else: use `pr-str` representation.
     (pr-str x)))
 
+;;; TODO -- this logic is wack, we should probably be snake casing stuff and display names like
+;;;
+;;; "Sum of Products → Price"
+;;;
+;;; result in totally wacko column names like "sum_products_%E2%86%92_price", let's try to generate things that are
+;;; actually going to be allowed here.
 (defn- slugify [s]
   (-> s
-      (str/replace #"\+" (i18n/tru "plus"))
-      (str/replace #"\-" (i18n/tru "minus"))
       (str/replace #"[\(\)]" "")
       u/slugify))
 
@@ -93,7 +103,8 @@
   `:aggregation` part. Return `nil` if there is nothing to describe."
   {:arglists '([query stage-number top-level-key])}
   (fn [_query _stage-number top-level-key]
-    top-level-key))
+    top-level-key)
+  :hierarchy lib.hierarchy/hierarchy)
 
 (def ^:private TopLevelKey
   "In the interest of making this easy to use in JS-land we'll accept either strings or keywords."
@@ -109,16 +120,55 @@
     top-level-key :- TopLevelKey]
    (describe-top-level-key-method query stage-number (keyword top-level-key))))
 
+(defmulti type-of-method
+  "Calculate the effective type of something. This differs from [[metabase.lib.schema.expression/type-of]] in that it is
+  called with a query/MetadataProvider and a stage number, allowing us to fully resolve information and return
+  complete, unambigous type information. Default implementation calls [[metabase.lib.schema.expression/type-of]]."
+  {:arglists '([query stage-number expr])}
+  (fn [_query _stage-number expr]
+    (lib.dispatch/dispatch-value expr))
+  :hierarchy lib.hierarchy/hierarchy)
+
+(mu/defn type-of :- ::lib.schema.common/base-type
+  "Get the effective type of an MBQL expression."
+  ([query x]
+   (type-of query -1 x))
+  ([query        :- ::lib.schema/query
+    stage-number :- :int
+    x]
+   (or ((some-fn :effective-type :base-type) (lib.options/options x))
+       (type-of-method query stage-number x))))
+
+(defmethod type-of-method :default
+  [_query _stage-number expr]
+  (lib.schema.expresssion/type-of expr))
+
+(defmethod type-of-method :dispatch-type/fn
+  [query stage-number f]
+  (type-of query stage-number (f query stage-number)))
+
+;;; for MBQL clauses whose type is the same as the type of the first arg. Also used
+;;; for [[metabase.lib.schema.expression/type-of]].
+(defmethod type-of-method :lib.type-of/type-is-type-of-first-arg
+  [query stage-number [_tag _opts expr]]
+  (type-of query stage-number expr))
+
+;;; Ugh
+(defmethod type-of-method :lib/external-op
+  [query stage-number {:keys [operator options args]}]
+  (type-of query stage-number (into [(keyword operator) options] args)))
+
 (defmulti metadata-method
   "Impl for [[metadata]]."
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod metadata-method :default
   [query stage-number x]
   {:lib/type     :metadata/field
-   :base_type    (lib.schema.expresssion/type-of x)
+   :base_type    (type-of query stage-number x)
    :name         (column-name query stage-number x)
    :display_name (display-name query stage-number x)})
 
diff --git a/src/metabase/lib/metric.cljc b/src/metabase/lib/metric.cljc
index 2a7fee07c65466b8d48025be14d3a97634e45d6e..3392f0daa29d22f4961f7c079e8048c7714a44fd 100644
--- a/src/metabase/lib/metric.cljc
+++ b/src/metabase/lib/metric.cljc
@@ -1,8 +1,44 @@
 (ns metabase.lib.metric
+  "A Metric is a saved MBQL query stage snippet with EXACTLY ONE `:aggregation` and optionally a `:filter` (boolean)
+  expression. Can be passed into the `:aggregation`s list."
   (:require
+   [metabase.lib.convert :as lib.convert]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
-   [metabase.shared.util.i18n :as i18n]))
+   [metabase.lib.schema :as lib.schema]
+   [metabase.lib.util :as lib.util]
+   [metabase.mbql.normalize :as mbql.normalize]
+   [metabase.shared.util.i18n :as i18n]
+   [metabase.util.malli :as mu]))
+
+(defn- resolve-metric [query metric-id]
+  (when (integer? metric-id)
+    (lib.metadata/metric query metric-id)))
+
+(mu/defn ^:private metric-definition :- [:maybe ::lib.schema/stage.mbql]
+  [{:keys [definition], :as _metric-metadata}]
+  (when definition
+    (if (:mbql/type definition)
+      definition
+      ;; legacy; needs conversion
+      (->
+       (lib.convert/legacy-query-from-inner-query nil definition)
+       mbql.normalize/normalize
+       lib.convert/->pMBQL
+       (lib.util/query-stage -1)))))
+
+(defmethod lib.metadata.calculation/type-of-method :metadata/metric
+  [query stage-number metric-metadata]
+  (or
+   (when-let [[aggregation] (not-empty (:aggregation (metric-definition metric-metadata)))]
+     (lib.metadata.calculation/type-of query stage-number aggregation))
+   :type/*))
+
+(defmethod lib.metadata.calculation/type-of-method :metric
+  [query stage-number [_tag _opts metric-id-or-name]]
+  (or (when-let [metric-metadata (resolve-metric query metric-id-or-name)]
+        (lib.metadata.calculation/type-of query stage-number metric-metadata))
+      :type/*))
 
 (defmethod lib.metadata.calculation/display-name-method :metadata/metric
   [_query _stage-number metric-metadata]
@@ -11,7 +47,12 @@
 
 (defmethod lib.metadata.calculation/display-name-method :metric
   [query stage-number [_tag _opts metric-id-or-name]]
-  (or (when (integer? metric-id-or-name)
-        (when-let [metric-metadata (lib.metadata/metric query metric-id-or-name)]
-          (lib.metadata.calculation/display-name query stage-number metric-metadata)))
+  (or (when-let [metric-metadata (resolve-metric query metric-id-or-name)]
+        (lib.metadata.calculation/display-name query stage-number metric-metadata))
       (i18n/tru "[Unknown Metric]")))
+
+(defmethod lib.metadata.calculation/column-name-method :metric
+  [query stage-number [_tag _opts metric-id-or-name]]
+  (or (when-let [metric-metadata (resolve-metric query metric-id-or-name)]
+        (lib.metadata.calculation/column-name query stage-number metric-metadata))
+      "metric"))
diff --git a/src/metabase/lib/normalize.cljc b/src/metabase/lib/normalize.cljc
index b0e17e42088900dbf8cd39d092456be5472282ff..f2e629870a37b9c59d8a6c984e1023b99d9992ac 100644
--- a/src/metabase/lib/normalize.cljc
+++ b/src/metabase/lib/normalize.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.normalize
   (:require
-   [metabase.lib.dispatch :as lib.dispatch]))
+   [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]))
 
 (defn- mbql-clause-type [x]
   (when (and (vector? x)
@@ -29,7 +30,8 @@
   using [[default-map-value-fns]]; for MBQL clauses, it will convert the clause name to a keyword and recursively
   normalize its options and arguments. Implement this method if you need custom behavior for something."
   {:arglists '([x])}
-  dispatch-value)
+  dispatch-value
+  :hierarchy lib.hierarchy/hierarchy)
 
 (def default-map-value-fns
   "Default normalization functions keys when doing map normalization."
diff --git a/src/metabase/lib/options.cljc b/src/metabase/lib/options.cljc
index 30ecf1c1c58f0a051487022f5185bcbed1bc6b33..e83ff49ac13e564e9ea59c294acd44748e45b09f 100644
--- a/src/metabase/lib/options.cljc
+++ b/src/metabase/lib/options.cljc
@@ -51,7 +51,7 @@
     (assoc x :lib/options new-options)
 
     (mbql-clause? x)
-    (if (map? (second x))
+    (if ((some-fn nil? map?) (second x))
       (assoc (vec x) 1 new-options)
       (into [(first x) new-options] (rest x)))
 
diff --git a/src/metabase/lib/order_by.cljc b/src/metabase/lib/order_by.cljc
index fdb8618045ed80d7d9949f7fd07c16367b33444a..8edcbc7eadc68da58032ced3d67ed3b48d034e40 100644
--- a/src/metabase/lib/order_by.cljc
+++ b/src/metabase/lib/order_by.cljc
@@ -3,6 +3,7 @@
    [metabase.lib.aggregation :as lib.aggregation]
    [metabase.lib.breakout :as lib.breakout]
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.options :as lib.options]
@@ -35,7 +36,8 @@
 (defmulti ^:private ->order-by-clause
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod ->order-by-clause :asc
   [_query _stage-number clause]
diff --git a/src/metabase/lib/query.cljc b/src/metabase/lib/query.cljc
index 778a5a75aedd288b1b6c42c1316778075023871f..45a089b503484682e6dc940571b8d65e9d097a2b 100644
--- a/src/metabase/lib/query.cljc
+++ b/src/metabase/lib/query.cljc
@@ -4,6 +4,7 @@
    [metabase.lib.common :as lib.common]
    [metabase.lib.convert :as lib.convert]
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.normalize :as lib.normalize]
@@ -96,7 +97,8 @@
   "Implementation for [[query]]."
   {:arglists '([metadata-provider x])}
   (fn [_metadata-provider x]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod ->query :dispatch-type/map
   [metadata-provider query]
diff --git a/src/metabase/lib/schema/aggregation.cljc b/src/metabase/lib/schema/aggregation.cljc
index 20e7d8be512370957891d21d3aed84e17b6b6299..d61a47f909520f2d5540fb856cc3e13c3496f0cc 100644
--- a/src/metabase/lib/schema/aggregation.cljc
+++ b/src/metabase/lib/schema/aggregation.cljc
@@ -1,5 +1,6 @@
 (ns metabase.lib.schema.aggregation
   (:require
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.schema.expression :as expression]
    [metabase.lib.schema.mbql-clause :as mbql-clause]
    [metabase.util.malli.registry :as mr]))
@@ -20,23 +21,23 @@
 (mbql-clause/define-tuple-mbql-clause :max
   [:schema [:ref ::expression/number]])
 
-(expression/register-type-of-first-arg :max)
+(lib.hierarchy/derive :max :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :median
   [:schema [:ref ::expression/number]])
 
-(expression/register-type-of-first-arg :median)
+(lib.hierarchy/derive :median :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :min
   [:schema [:ref ::expression/number]])
 
-(expression/register-type-of-first-arg :min)
+(lib.hierarchy/derive :min :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :percentile
   #_expr [:schema [:ref ::expression/number]]
   #_percentile [:schema [:ref ::expression/non-integer-real]])
 
-(expression/register-type-of-first-arg :percentile)
+(lib.hierarchy/derive :percentile :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :share :- :type/Float
   [:schema [:ref ::expression/boolean]])
@@ -47,13 +48,13 @@
 (mbql-clause/define-tuple-mbql-clause :sum
   [:schema [:ref ::expression/number]])
 
-(expression/register-type-of-first-arg :sum)
+(lib.hierarchy/derive :sum :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :sum-where
   [:schema [:ref ::expression/number]]
   [:schema [:ref ::expression/boolean]])
 
-(expression/register-type-of-first-arg :sum-where)
+(lib.hierarchy/derive :sum-where :lib.type-of/type-is-type-of-first-arg)
 
 (mr/def ::aggregation
   ;; placeholder!
diff --git a/src/metabase/lib/schema/expression.cljc b/src/metabase/lib/schema/expression.cljc
index 428d7798348efef0f4c44899dc4cf9fe2546365a..f61067a6e864977318803eec4b5dfc86a512c0bd 100644
--- a/src/metabase/lib/schema/expression.cljc
+++ b/src/metabase/lib/schema/expression.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.schema.expression
   (:require
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.schema.common :as common]
    [metabase.shared.util.i18n :as i18n]
    [metabase.types]
@@ -10,6 +11,7 @@
 
 (comment metabase.types/keep-me)
 
+;;; TODO -- rename to `type-of-method`
 (defmulti type-of*
   "Impl for [[type-of]]. Use [[type-of]], but implement [[type-of*]].
 
@@ -25,14 +27,10 @@
     ;; can implement support for some platform-specific classes like `BigDecimal` or `java.time.OffsetDateTime`, for
     ;; use inside QP code or whatever. In the future maybe we can add support for JS-specific stuff too.
     (let [dispatch-value (lib.dispatch/dispatch-value x)]
-      (if (= dispatch-value :type/*)
+      (if (= dispatch-value :dispatch-type/*)
         (type x)
-        dispatch-value))))
-
-(defmethod type-of* :default
-  [expr]
-  (throw (ex-info (i18n/tru "Don''t know how to determine the base type of {0}" (pr-str expr))
-                  {:expr expr})))
+        dispatch-value)))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defn- mbql-clause? [expr]
   (and (vector? expr)
@@ -57,6 +55,17 @@
         (:base-type (second expr)))
    (type-of* expr)))
 
+(defmethod type-of* :default
+  [expr]
+  (throw (ex-info (i18n/tru "Don''t know how to determine the type of {0}" (pr-str expr))
+                  {:expr expr})))
+
+;;; for MBQL clauses whose type is the same as the type of the first arg. Also used
+;;; for [[metabase.lib.metadata.calculation/type-of-method]].
+(defmethod type-of* :lib.type-of/type-is-type-of-first-arg
+  [[_tag _opts expr]]
+  (type-of expr))
+
 (defn- is-type? [x y]
   (cond
     (set? x)             (some #(is-type? % y) x)
@@ -72,13 +81,6 @@
             (i18n/tru "type-of {0} returned an invalid type {1}" (pr-str expr) (pr-str expr-type)))
     (is-type? expr-type base-type)))
 
-#?(:clj
-   (defmacro register-type-of-first-arg
-     "Registers [[tag]] with [[type-of*]] in terms of its first incoming [[expr]].
-      Useful for clauses that are polymorphic on their argument."
-     [tag]
-     `(defmethod type-of* ~tag [[_tag# _opts# expr#]] (type-of expr#))))
-
 (defn- expression-schema
   "Schema that matches the following rules:
 
@@ -92,7 +94,9 @@
   [:and
    [:or
     [:fn
-     {:error/message "shaped like an MBQL clause"}
+     {:error/message "valid MBQL clause"
+      :error/fn      (fn [{:keys [value]} _]
+                       (str "invalid MBQL clause: " (pr-str value)))}
      (complement mbql-clause?)]
     [:ref :metabase.lib.schema.mbql-clause/clause]]
    [:fn
@@ -149,3 +153,7 @@
    {:min 1, :error/message ":expressions definition map of expression name -> expression"}
    ::common/non-blank-string
    ::expression])
+
+(defmethod type-of* :lib/external-op
+  [{:keys [operator options args] :or {options {}}}]
+  (type-of (into [(keyword operator) options] args)))
diff --git a/src/metabase/lib/schema/expression/arithmetic.cljc b/src/metabase/lib/schema/expression/arithmetic.cljc
index 4869e8dff9cc0245fcccf70e6171552c30ed24e7..b711f01784117405f6817b6a5fccf9d12217d7b4 100644
--- a/src/metabase/lib/schema/expression/arithmetic.cljc
+++ b/src/metabase/lib/schema/expression/arithmetic.cljc
@@ -2,6 +2,7 @@
   "Arithmetic expressions like `:+`."
   (:require
    [malli.core :as mc]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.schema.common :as common]
    [metabase.lib.schema.expression :as expression]
    [metabase.lib.schema.mbql-clause :as mbql-clause]
@@ -73,21 +74,18 @@
 (mbql-clause/define-catn-mbql-clause :/ :- :type/Float
   [:args ::args.numbers])
 
-(defmethod expression/type-of* :+
-  [[_tag _opts & args]]
-  (type-of-arithmetic-args args))
-
-(defmethod expression/type-of* :-
-  [[_tag _opts & args]]
-  (type-of-arithmetic-args args))
+(doseq [tag [:+ :- :*]]
+  (lib.hierarchy/derive tag :lib.type-of/type-is-type-of-arithmetic-args))
 
-(defmethod expression/type-of* :*
+;;; `:+`, `:-`, and `:*` all have the same logic; also used for [[metabase.lib.metadata.calculation/type-of-method]]
+(defmethod expression/type-of* :lib.type-of/type-is-type-of-arithmetic-args
   [[_tag _opts & args]]
   (type-of-arithmetic-args args))
 
 (mbql-clause/define-tuple-mbql-clause :abs
   [:schema [:ref ::expression/number]])
-(expression/register-type-of-first-arg :abs)
+
+(lib.hierarchy/derive :abs :lib.type-of/type-is-type-of-first-arg)
 
 (doseq [op [:log :exp :sqrt]]
   (mbql-clause/define-tuple-mbql-clause op :- :type/Float
diff --git a/src/metabase/lib/schema/expression/string.cljc b/src/metabase/lib/schema/expression/string.cljc
index 248c15dcd063143e59b22dedaad3ba5131026769..60bfb300d4e33f6d22551a9ae59f0030f58f5840 100644
--- a/src/metabase/lib/schema/expression/string.cljc
+++ b/src/metabase/lib/schema/expression/string.cljc
@@ -26,4 +26,4 @@
   #_end [:schema [:ref ::expression/integer]])
 
 (mbql-clause/define-catn-mbql-clause :concat :- :type/Text
-  [:sequential {:min 2} [:schema [:ref ::expression/string]]])
+  [:args [:repeat {:min 2} [:schema [:ref ::expression/string]]]])
diff --git a/src/metabase/lib/schema/expression/temporal.cljc b/src/metabase/lib/schema/expression/temporal.cljc
index 6c6f67164ca4f0dc5771a28cc7aa6b6dba0783c0..68003029cb3112e2fdf2024c22baa21a79402bd1 100644
--- a/src/metabase/lib/schema/expression/temporal.cljc
+++ b/src/metabase/lib/schema/expression/temporal.cljc
@@ -1,5 +1,6 @@
 (ns metabase.lib.schema.expression.temporal
   (:require
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.schema.expression :as expression]
    [metabase.lib.schema.mbql-clause :as mbql-clause]
    [metabase.lib.schema.temporal-bucketing :as temporal-bucketing]
@@ -20,8 +21,7 @@
     #_expr   [:ref ::expression/temporal]
     #_amount :int
     #_unit   [:ref ::temporal-bucketing/unit.date-time.interval])
-
-  (expression/register-type-of-first-arg op))
+  (lib.hierarchy/derive op :lib.type-of/type-is-type-of-first-arg))
 
 (doseq [op [:get-year :get-month :get-day :get-hour :get-minute :get-second :get-quarter]]
   (mbql-clause/define-tuple-mbql-clause op :- :type/Integer
@@ -44,7 +44,7 @@
   #_:target [:string]
   #_:source [:maybe [:string]])
 
-(expression/register-type-of-first-arg :convert-timezone)
+(lib.hierarchy/derive :convert-timezone :lib.type-of/type-is-type-of-first-arg)
 
 (mbql-clause/define-tuple-mbql-clause :now :- :type/DateTimeWithTZ)
 
diff --git a/src/metabase/lib/schema/order_by.cljc b/src/metabase/lib/schema/order_by.cljc
index 18e697aea1926f118e3d51fcdca0b186fa218a0f..143ede6ac37a100cd5923a5f96290c0ee35f0580 100644
--- a/src/metabase/lib/schema/order_by.cljc
+++ b/src/metabase/lib/schema/order_by.cljc
@@ -1,30 +1,27 @@
 (ns metabase.lib.schema.order-by
   "Schemas for order-by clauses."
   (:require
-   [metabase.lib.schema.common :as common]
    [metabase.lib.schema.expression :as expression]
+   [metabase.lib.schema.mbql-clause :as mbql-clause]
    [metabase.util.malli.registry :as mr]))
 
 (mr/def ::direction
   [:enum :asc :desc])
 
-(mr/def ::asc
-  [:catn
-   [:direction [:= :asc]]
-   [:options ::common/options]
-   [:expression ::expression/orderable]])
+(mbql-clause/define-tuple-mbql-clause :asc
+  [:ref ::expression/orderable])
 
-(mr/def ::desc
-  [:catn
-   [:direction [:= :desc]]
-   [:options ::common/options]
-   [:expression ::expression/orderable]])
+(mbql-clause/define-tuple-mbql-clause :desc
+  [:ref ::expression/orderable])
 
 (mr/def ::order-by
-  [:or
-   ::asc
-   ::desc])
+  [:and
+   [:ref ::mbql-clause/clause]
+   [:fn
+    {:error/message ":asc or :desc clause"}
+    (fn [[tag]]
+      (#{:asc :desc} tag))]])
 
 ;;; TODO -- should there be a no-duplicates constraint here?
 (mr/def ::order-bys
-  [:sequential ::order-by])
+  [:sequential {:min 1} [:ref ::order-by]])
diff --git a/src/metabase/lib/schema/temporal_bucketing.cljc b/src/metabase/lib/schema/temporal_bucketing.cljc
index b51d7b4c5d3b6d51a4f9778701b3e9d932f1bfab..73d5ea324204d58bdb949810226c9c01406c66d9 100644
--- a/src/metabase/lib/schema/temporal_bucketing.cljc
+++ b/src/metabase/lib/schema/temporal_bucketing.cljc
@@ -39,6 +39,11 @@
    ::unit.time.extract
    ::unit.time.truncate])
 
+(mr/def ::unit.date-time
+  [:or
+   ::unit.date
+   ::unit.time])
+
 (mr/def ::unit
   [:or
    [:= :default]
diff --git a/src/metabase/lib/segment.cljc b/src/metabase/lib/segment.cljc
index e77cf376ec580d49368fa79e9127018ab9426af4..e74bada9fac1d8363109faab23392bd181a35e33 100644
--- a/src/metabase/lib/segment.cljc
+++ b/src/metabase/lib/segment.cljc
@@ -1,4 +1,5 @@
 (ns metabase.lib.segment
+  "A Metric is a saved MBQL query stage snippet with `:filter`. Segments are always boolean expressions."
   (:require
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
diff --git a/src/metabase/lib/temporal_bucket.cljc b/src/metabase/lib/temporal_bucket.cljc
index 37f304a705e7a8e4344791fd40f71089342d07a5..23364600d300f23b38feab029d94722aab73ac52 100644
--- a/src/metabase/lib/temporal_bucket.cljc
+++ b/src/metabase/lib/temporal_bucket.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.temporal-bucket
   (:require
    [metabase.lib.dispatch :as lib.dispatch]
+   [metabase.lib.hierarchy :as lib.hierarchy]
    [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.temporal-bucketing
     :as lib.schema.temporal-bucketing]
@@ -41,7 +42,8 @@
   particular MBQL clause."
   {:arglists '([x unit])}
   (fn [x _unit]
-    (lib.dispatch/dispatch-value x)))
+    (lib.dispatch/dispatch-value x))
+  :hierarchy lib.hierarchy/hierarchy)
 
 (defmethod temporal-bucket-method :dispatch-type/fn
   [f unit]
diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc
index b9c313aa026cde7d97348930661b7f5c31b746e1..449eb278e501f15b000a2646e9486875286175d7 100644
--- a/test/metabase/lib/aggregation_test.cljc
+++ b/test/metabase/lib/aggregation_test.cljc
@@ -6,6 +6,7 @@
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.query :as lib.query]
+   [metabase.lib.schema.expression :as lib.schema.expression]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]
    #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
@@ -194,3 +195,60 @@
                   (lib/aggregate {:operator :sum
                                   :args [(lib/ref (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]})
                   (dissoc :lib/metadata)))))))
+
+(deftest ^:parallel type-of-sum-test
+  (is (= :type/BigInteger
+         (lib.metadata.calculation/type-of
+          lib.tu/venues-query
+          [:sum
+           {:lib/uuid (str (random-uuid))}
+           [:field {:lib/uuid (str (random-uuid))} (meta/id :venues :id)]]))))
+
+(deftest ^:parallel type-of-test
+  (testing "Make sure we can calculate correct type information for an aggregation clause like"
+    (doseq [tag  [:max
+                  :median
+                  :percentile
+                  :sum
+                  :sum-where]
+            arg  (let [field [:field {:lib/uuid (str (random-uuid))} (meta/id :venues :id)]]
+                   [field
+                    [:+ {:lib/uuid (str (random-uuid))} field 1]
+                    [:- {:lib/uuid (str (random-uuid))} field 1]
+                    [:* {:lib/uuid (str (random-uuid))} field 1]])
+            :let [clause [tag
+                          {:lib/uuid (str (random-uuid))}
+                          arg]]]
+      (testing (str \newline (pr-str clause))
+        (is (= (condp = (first arg)
+                 :field :metabase.lib.schema.expression/type.unknown
+                 :type/*)
+               (lib.schema.expression/type-of clause)))
+        (is (= (condp = (first arg)
+                 :field :type/BigInteger
+                 :type/Integer)
+               (lib.metadata.calculation/type-of lib.tu/venues-query clause)))))))
+
+(deftest ^:parallel expression-ref-inside-aggregation-type-of-test
+  (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                  (lib/expression "double-price" (lib/* (lib/field (meta/id :venues :price)) 2))
+                  (lib/aggregate (lib/sum [:expression {:lib/uuid (str (random-uuid))} "double-price"])))]
+    (is (=? [{:lib/type     :metadata/field
+              :base_type    :type/Integer
+              :name         "sum_double-price"
+              :display_name "Sum of double-price"}]
+            (lib/aggregations query)))
+    (is (= :type/Integer
+           (lib/type-of query (first (lib/aggregations query)))))))
+
+(deftest ^:parallel preserve-field-settings-metadata-test
+  (testing "Aggregation metadata should return the `:settings` for the field being aggregated, for some reason."
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/aggregate (lib/sum (lib/field (meta/id :venues :price)))))]
+      (is (=? {:settings     {:is_priceless true}
+               :lib/type     :metadata/field
+               :base_type    :type/Integer
+               :name         "sum_price"
+               :display_name "Sum of Price"
+               :lib/source   :source/aggregations}
+              (lib.metadata.calculation/metadata query (first (lib/aggregations query -1))))))))
diff --git a/test/metabase/lib/convert_test.cljc b/test/metabase/lib/convert_test.cljc
index 1671e91b7ed3b64ba15a9b671e4ae50280017ac4..b6f5497987e86632cc1b509918da4723623dd210 100644
--- a/test/metabase/lib/convert_test.cljc
+++ b/test/metabase/lib/convert_test.cljc
@@ -141,4 +141,8 @@
                                 :metabase.query-processor.util.add-alias-info/position 1
                                 :metabase.query-processor.util.add-alias-info/source-alias "avg"}]]
                 :source-table 224}
-        :type :query}))
+        :type :query}
+
+       [:value nil {:base_type :type/Number}]
+
+       [:case [[[:< [:field 1 nil] 10] [:value nil {:base_type :type/Number}]] [[:> [:field 2 nil] 2] 10]]]))
diff --git a/test/metabase/lib/expression_test.cljc b/test/metabase/lib/expression_test.cljc
index 6be081bec9465889bb9951c60ab5255aa48fafa5..d30c00c02e492f7ff398f4a7bb52a6ba1dfee126 100644
--- a/test/metabase/lib/expression_test.cljc
+++ b/test/metabase/lib/expression_test.cljc
@@ -1,6 +1,5 @@
 (ns metabase.lib.expression-test
   (:require
-   #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))
    [clojure.test :refer [deftest is testing]]
    [malli.core :as mc]
    [metabase.lib.core :as lib]
@@ -8,21 +7,22 @@
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.common :as schema.common]
-   [metabase.lib.schema.expression :as schema.expression]
+   [metabase.lib.schema.expression :as lib.schema.expression]
    [metabase.lib.test-metadata :as meta]
-   [metabase.lib.test-util :as lib.tu]))
+   [metabase.lib.test-util :as lib.tu]
+   #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
 
 (comment lib/keep-me)
 
 #?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
 
 (deftest ^:parallel expression-test
-  (is (=? {:lib/type :mbql/query,
-           :database (meta/id) ,
-           :type :pipeline,
-           :stages [{:lib/type :mbql.stage/mbql,
-                     :source-table (meta/id :venues) ,
-                     :lib/options {:lib/uuid string?},
+  (is (=? {:lib/type :mbql/query
+           :database (meta/id)
+           :type :pipeline
+           :stages [{:lib/type :mbql.stage/mbql
+                     :source-table (meta/id :venues)
+                     :lib/options {:lib/uuid string?}
                      :expressions {"myadd" [:+ {:lib/uuid string?}
                                             1
                                             [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]]}}]}
@@ -84,7 +84,7 @@
               resolved (lib.expression/resolve-expression query 0 "myexpr")]
           (is (mc/validate ::lib.schema/query query))
           (is (mc/validate ::schema.common/external-op resolved))
-          (is (= typ (schema.expression/type-of resolved))))))))
+          (is (= typ (lib.schema.expression/type-of resolved))))))))
 
 (deftest ^:parallel col-info-expression-ref-test
   (is (=? {:base_type    :type/Integer
@@ -221,6 +221,29 @@
           -1
           [:expression {:lib/uuid (str (random-uuid))} "double-price"])))))
 
+(deftest ^:parallel arithmetic-expression-type-of-test
+  (testing "Make sure we can calculate correct type information for arithmetic expression"
+    (let [field [:field {:lib/uuid (str (random-uuid))} (meta/id :venues :id)]]
+      (testing "+, -, and * should return common ancestor type of all args")
+      (doseq [tag   [:+ :- :*]
+              arg-2 [1 1.0]
+              :let  [clause [tag {:lib/uuid (str (random-uuid))} field arg-2]]]
+        (testing (str \newline (pr-str clause))
+          (is (= :type/*
+                 (lib.schema.expression/type-of clause)))
+          (is (= (condp = arg-2
+                   1   :type/Integer
+                   1.0 :type/Number)
+                 (lib.metadata.calculation/type-of lib.tu/venues-query clause)))))
+      (testing "/ should always return type/Float"
+        (doseq [arg-2 [1 1.0]
+                :let  [clause [:/ {:lib/uuid (str (random-uuid))} field arg-2]]]
+          (testing (str \newline (pr-str clause))
+            (is (= :type/Float
+                   (lib.schema.expression/type-of clause)))
+            (is (= :type/Float
+                   (lib.metadata.calculation/type-of lib.tu/venues-query clause)))))))))
+
 (deftest ^:parallel expressions-names-test
   (testing "expressions should include the original expression name"
     (is (=? [{:name         "expr"
diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc
index d653d60b88988c6e92ea9c42222510910bcb72fe..edf344a5be8b83d893de9b7f3a5baec7d95c3d39 100644
--- a/test/metabase/lib/field_test.cljc
+++ b/test/metabase/lib/field_test.cljc
@@ -4,6 +4,7 @@
    [metabase.lib.core :as lib]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
+   [metabase.lib.schema.expression :as lib.schema.expression]
    [metabase.lib.temporal-bucket :as lib.temporal-bucket]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]
@@ -149,3 +150,11 @@
               (lib.temporal-bucket/current-temporal-bucket (lib.metadata.calculation/metadata query -1 field))))
       (is (= "Date (year)"
              (lib.metadata.calculation/display-name query -1 field))))))
+
+(deftest ^:parallel field-ref-type-of-test
+  (testing "Make sure we can calculate field ref type information correctly"
+    (let [clause [:field {:lib/uuid (str (random-uuid))} (meta/id :venues :id)]]
+      (is (= ::lib.schema.expression/type.unknown
+             (lib.schema.expression/type-of clause)))
+      (is (= :type/BigInteger
+             (lib.metadata.calculation/type-of lib.tu/venues-query clause))))))
diff --git a/test/metabase/lib/metric_test.cljc b/test/metabase/lib/metric_test.cljc
index 42e03d44f688e2af86a0a5d837e54c737a5d5b01..990fe4a9237064cb55b79dd9fe8da77ce7e5dccb 100644
--- a/test/metabase/lib/metric_test.cljc
+++ b/test/metabase/lib/metric_test.cljc
@@ -1,21 +1,44 @@
 (ns metabase.lib.metric-test
   (:require
-   [clojure.test :refer [deftest is]]
+   [clojure.test :refer [deftest is testing]]
    [metabase.lib.core :as lib]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.test-metadata :as meta]
-   [metabase.lib.test-util :as lib.tu]))
+   [metabase.lib.test-util :as lib.tu]
+   #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
+
+#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
+
+(defn- mock-metadata-provider []
+  (lib.tu/mock-metadata-provider
+   {:database meta/metadata
+    :tables   [(meta/table-metadata :venues)]
+    :fields   [(meta/field-metadata :venues :price)]
+    :metrics  [{:id         100
+                :name       "My Metric"
+                :definition {:aggregation [[:sum [:field (meta/id :venues :price) nil]]]
+                             :filter      [:= [:field (meta/id :venues :price) nil] 4]}}]}))
 
 (deftest ^:parallel metric-display-name-test
-  (let [metadata (lib.tu/mock-metadata-provider
-                  {:database meta/metadata
-                   :tables   [(meta/table-metadata :venues)]
-                   :fields   [(meta/field-metadata :venues :price)]
-                   :metrics  [{:id         100
-                               :name       "My Metric"
-                               :definition {:database (meta/id)
-                                            :query    {:filter [:= [:field (meta/id :venues :price) nil] 4]}}}]})
+  (let [metadata (mock-metadata-provider)
         query    (-> (lib/query-for-table-name metadata "VENUES")
                      (lib/aggregate [:metric {:lib/uuid (str (random-uuid))} 100]))]
     (is (= "Venues, My Metric"
            (lib.metadata.calculation/suggested-name query)))))
+
+(deftest ^:parallel metric-type-of-test
+  (let [metadata (mock-metadata-provider)
+        query    (-> (lib/query-for-table-name metadata "VENUES")
+                     (lib/aggregate [:metric {:lib/uuid (str (random-uuid))} 100]))]
+    (is (= :type/Integer
+           (lib.metadata.calculation/type-of query [:metric {:lib/uuid (str (random-uuid))} 100])))))
+
+(deftest ^:parallel ga-metric-metadata-test
+  (testing "Make sure we can calculate metadata for FAKE Google Analytics metric clauses"
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/aggregate [:metric {:lib/uuid (str (random-uuid))} "ga:totalEvents"]))]
+      (is (=? [{:lib/type     :metadata/field
+                :name         "metric"
+                :display_name "[Unknown Metric]"
+                :base_type    :type/*}]
+              (lib.metadata.calculation/metadata query -1 query))))))
diff --git a/test/metabase/lib/schema/expression/string_test.cljc b/test/metabase/lib/schema/expression/string_test.cljc
new file mode 100644
index 0000000000000000000000000000000000000000..327194b13db1e5102a162c44e7e468550b615a7e
--- /dev/null
+++ b/test/metabase/lib/schema/expression/string_test.cljc
@@ -0,0 +1,18 @@
+(ns metabase.lib.schema.expression.string-test
+  (:require
+   [clojure.test :refer [are deftest]]
+   [malli.core :as mc]
+   [malli.error :as me]
+   [metabase.lib.schema]))
+
+(comment metabase.lib.schema/keep-me)
+
+(deftest ^:parallel concat-test
+  (are [clause] (not (me/humanize (mc/explain :mbql.clause/concat clause)))
+    [:concat {:lib/uuid "47cac41f-6240-4623-9a73-448addfdc735"} "1" "2"]
+    [:concat {:lib/uuid "47cac41f-6240-4623-9a73-448addfdc735"} "1" "2" "3"]
+
+    [:concat
+     {:lib/uuid "47cac41f-6240-4623-9a73-448addfdc735"}
+     [:field {:lib/uuid "e47d33bc-c89c-48af-bffe-842c815f930b"} 1]
+     "2"]))
diff --git a/test/metabase/lib/schema/literal/jvm_test.clj b/test/metabase/lib/schema/literal/jvm_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..f5acd09b1fe2e50b06a8cb805c7e34ef0322e134
--- /dev/null
+++ b/test/metabase/lib/schema/literal/jvm_test.clj
@@ -0,0 +1,11 @@
+(ns metabase.lib.schema.literal.jvm-test
+  (:require
+   [clojure.test :refer [deftest is]]
+   [metabase.lib.schema]
+   [metabase.lib.schema.expression :as expression]))
+
+(comment metabase.lib.schema/keep-me)
+
+(deftest ^:parallel temporal-literal-type-of-test
+  (is (= :type/DateTimeWithZoneID
+         (expression/type-of* #t "2019-01-01T00:00Z[UTC]"))))
diff --git a/test/metabase/lib/test_metadata.cljc b/test/metabase/lib/test_metadata.cljc
index 451dd6d07c854cd00edd5af9f5a7145be573f43c..3ed284f6a8b6f1c9ef1bba2233cdd0c2a5fc5390 100644
--- a/test/metabase/lib/test_metadata.cljc
+++ b/test/metabase/lib/test_metadata.cljc
@@ -677,7 +677,7 @@
    :name                "PRICE"
    :fingerprint_version 5
    :has_field_values    :list
-   :settings            nil
+   :settings            {:is_priceless true}
    :caveats             nil
    :fk_target_field_id  nil
    :custom_position     0
diff --git a/test/metabase/query_processor/middleware/annotate_test.clj b/test/metabase/query_processor/middleware/annotate_test.clj
index 111e1c4b9fb1e7bb40bc6afaf992517b0422fab9..8ca414cd3a8045418c5cab3eafc7f6ef5ee04c07 100644
--- a/test/metabase/query_processor/middleware/annotate_test.clj
+++ b/test/metabase/query_processor/middleware/annotate_test.clj
@@ -23,7 +23,7 @@
 ;;; |                                             column-info (:native)                                              |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(deftest native-column-info-test
+(deftest ^:parallel native-column-info-test
   (testing "native column info"
     (testing "should still infer types even if the initial value(s) are `nil` (#4256, #6924)"
       (is (= [:type/Integer]
@@ -62,7 +62,7 @@
   ([table-key field-key]
    (info-for-field (mt/id table-key field-key))))
 
-(deftest col-info-field-ids-test
+(deftest ^:parallel col-info-field-ids-test
   (testing {:base-type "make sure columns are comming back the way we'd expect for :field clauses"}
     (mt/with-everything-store
       (mt/$ids venues
@@ -74,7 +74,7 @@
                  {:type :query, :query {:fields [$price]}}
                  {:columns [:price]}))))))))
 
-(deftest col-info-for-fks-and-joins-test
+(deftest ^:parallel col-info-for-fks-and-joins-test
   (mt/with-everything-store
     (mt/$ids venues
       (testing (str "when a `:field` with `:source-field` (implicit join) is used, we should add in `:fk_field_id` "
@@ -125,7 +125,7 @@
                                       :strategy     :left-join}]}}
                    {:columns [:name]})))))))))
 
-(deftest col-info-for-field-with-temporal-unit-test
+(deftest ^:parallel col-info-for-field-with-temporal-unit-test
   (mt/with-everything-store
     (mt/$ids venues
       (testing "when a `:field` with `:temporal-unit` is used, we should add in info about the `:unit`"
@@ -181,7 +181,7 @@
                           :limit           1}}
                  nil))))))))
 
-(deftest col-info-for-binning-strategy-test
+(deftest ^:parallel col-info-for-binning-strategy-test
   (testing "when binning strategy is used, include `:binning_info`"
     (is (= [{:name         "price"
              :base_type    :type/Number
@@ -196,17 +196,16 @@
                                                             :bin-width 5
                                                             :min-value -100
                                                             :max-value 100}}]}]
-           (doall
-            (annotate/column-info
-             {:type  :query
-              :query {:fields [[:field "price" {:base-type     :type/Number
-                                                :temporal-unit :month
-                                                :binning       {:strategy  :num-bins
-                                                                :num-bins  10
-                                                                :bin-width 5
-                                                                :min-value -100
-                                                                :max-value 100}}]]}}
-             {:columns [:price]}))))))
+           (annotate/column-info
+            {:type  :query
+             :query {:fields [[:field "price" {:base-type     :type/Number
+                                               :temporal-unit :month
+                                               :binning       {:strategy  :num-bins
+                                                               :num-bins  10
+                                                               :bin-width 5
+                                                               :min-value -100
+                                                               :max-value 100}}]]}}
+            {:columns [:price]})))))
 
 (deftest col-info-combine-parent-field-names-test
   (testing "For fields with parents we should return them with a combined name including parent's name"
@@ -255,7 +254,7 @@
                 :base_type       :type/Text}
                (into {} (#'annotate/col-info-for-field-clause {} [:field (u/the-id child) nil]))))))))
 
-(deftest col-info-field-literals-test
+(deftest ^:parallel col-info-field-literals-test
   (testing "field literals should get the information from the matching `:source-metadata` if it was supplied"
     (mt/with-everything-store
       (is (= {:name          "sum"
@@ -269,7 +268,7 @@
                 {:name "sum", :display_name "sum of User ID", :base_type :type/Integer, :semantic_type :type/FK}]}
               [:field "sum" {:base-type :type/Integer}]))))))
 
-(deftest col-info-expressions-test
+(deftest ^:parallel col-info-expressions-test
   (mt/with-everything-store
     (testing "col info for an `expression` should work as expected"
       (is (= {:base_type       :type/Float
@@ -331,7 +330,7 @@
        {:name         (annotate/aggregation-name ag-clause)
         :display_name (annotate/aggregation-display-name inner-query ag-clause)}))))
 
-(deftest aggregation-names-test
+(deftest ^:parallel aggregation-names-test
   (testing "basic aggregations"
     (testing ":count"
       (is (= {:name "count", :display_name "Count"}
@@ -398,7 +397,7 @@
    (binding [driver/*driver* :h2]
      (#'annotate/col-info-for-aggregation-clause inner-query clause))))
 
-(deftest col-info-for-aggregation-clause-test
+(deftest ^:parallel col-info-for-aggregation-clause-test
   (mt/with-everything-store
     (testing "basic aggregation clauses"
       (testing "`:count` (no field)"
@@ -472,7 +471,7 @@
       first
       (select-keys [:base_type :semantic_type])))
 
-(deftest computed-columns-inference
+(deftest ^:parallel computed-columns-inference
   (letfn [(infer [expr] (-> (mt/mbql-query venues
                                            {:expressions {"expr" expr}
                                             :fields [[:expression "expr"]]
@@ -541,7 +540,7 @@
                                                     :year]])))
   (is (not (#'annotate/datetime-arithmetics? [:+ [:field (mt/id :checkins :date) nil] 3]))))
 
-(deftest temporal-extract-test
+(deftest ^:parallel temporal-extract-test
   (is (= {:base_type :type/DateTime}
          (infered-col-type [:datetime-add [:field (mt/id :checkins :date) nil] 2 :month])))
   (is (= {:base_type :type/DateTime}
@@ -549,7 +548,7 @@
   (is (= {:base_type :type/DateTime}
          (infered-col-type [:datetime-add [:field (mt/id :users :last_login) nil] 2 :month]))))
 
-(deftest test-string-extracts
+(deftest ^:parallel test-string-extracts
   (is (= {:base_type :type/Text}
          (infered-col-type  [:trim "foo"])))
   (is (= {:base_type :type/Text}
@@ -576,7 +575,7 @@
           :semantic_type :type/Name}
          (infered-col-type  [:coalesce [:field (mt/id :venues :name) nil] "bar"]))))
 
-(deftest unique-name-key-test
+(deftest ^:parallel unique-name-key-test
   (testing "Make sure `:cols` always come back with a unique `:name` key (#8759)"
     (is (= {:cols
             [{:base_type     :type/Number
@@ -617,7 +616,7 @@
                     {:name "count", :display_name "count", :base_type :type/Number}
                     {:name "count_2", :display_name "count_2", :base_type :type/Number}]})))))
 
-(deftest expressions-keys-test
+(deftest ^:parallel expressions-keys-test
   (testing "make sure expressions come back with the right set of keys, including `:expression_name` (#8854)"
     (is (= {:name            "discount_price"
             :display_name    "discount_price"
@@ -634,7 +633,7 @@
                :cols
                second)))))
 
-(deftest deduplicate-expression-names-test
+(deftest ^:parallel deduplicate-expression-names-test
   (testing "make sure multiple expressions come back with deduplicated names"
     (testing "expressions in aggregations"
       (is (= [{:base_type :type/Float, :name "expression", :display_name "0.9 * Average of Price", :source :aggregation, :field_ref [:aggregation 0]}
@@ -712,7 +711,7 @@
                   :cols
                   (map :display_name)))))))))
 
-(deftest inception-test
+(deftest ^:parallel inception-test
   (testing "Should return correct metadata for an 'inception-style' nesting of source > source > source with a join (#14745)"
     (mt/dataset sample-dataset
       ;; these tests look at the metadata for just one column so it's easier to spot the differences.
@@ -798,7 +797,7 @@
                                         :columns second :display_name))
               "Results metadata cols has wrong display name"))))))
 
-(deftest preserve-original-join-alias-test
+(deftest ^:parallel preserve-original-join-alias-test
   (testing "The join alias for the `:field_ref` in results metadata should match the one originally specified (#27464)"
     (mt/test-drivers (mt/normal-drivers-with-feature :left-join)
       (mt/dataset sample-dataset