diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc
index f2307a3187f165bc892b7c3040d8f973baf3825f..3aa20c3eeadbd484ec2d3f328d9766a400117741 100644
--- a/src/metabase/lib/aggregation.cljc
+++ b/src/metabase/lib/aggregation.cljc
@@ -2,14 +2,14 @@
   (:refer-clojure :exclude [count distinct max min])
   (:require
    [metabase.lib.common :as lib.common]
+   [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.aggregation :as lib.schema.aggregation]
    [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])
-  #?(:cljs (:require-macros [metabase.lib.aggregation])))
+   [metabase.util.malli :as mu]))
 
 (mu/defn resolve-aggregation :- ::lib.schema.aggregation/aggregation
   "Resolve an aggregation with a specific `index`."
@@ -206,3 +206,17 @@
        update :aggregation
        (fn [aggregations]
          (conj (vec aggregations) (lib.common/->op-arg query stage-number an-aggregate-clause)))))))
+
+(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)
+                         ag-ref   [:aggregation
+                                   {:lib/uuid  (str (random-uuid))
+                                    :base-type (:base_type metadata)}
+                                   i]]
+                     (assoc metadata :field_ref ag-ref, :source :aggregation)))
+                 aggregation-exprs)))
diff --git a/src/metabase/lib/breakout.cljc b/src/metabase/lib/breakout.cljc
index 23d49c4cd4aa8a6b34933c86ae769c09d3647826..edcdc2801542fecbf62a4adf486846217a7ba10e 100644
--- a/src/metabase/lib/breakout.cljc
+++ b/src/metabase/lib/breakout.cljc
@@ -1,6 +1,7 @@
 (ns metabase.lib.breakout
   (:require
    [clojure.string :as str]
+   [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.expression :as lib.schema.expression]
@@ -29,3 +30,12 @@
                 expr)]
      (lib.util/update-query-stage query stage-number update :breakout (fn [breakouts]
                                                                         (conj (vec breakouts) expr))))))
+
+(mu/defn breakouts :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
+  "Get metadata about the breakouts in a given stage of a `query`."
+  [query        :- ::lib.schema/query
+   stage-number :- :int]
+  (when-let [breakout-exprs (not-empty (:breakout (lib.util/query-stage query stage-number)))]
+    (mapv (fn [field-ref]
+            (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :source :breakout))
+          breakout-exprs)))
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index f6ecb6e39f467367ea41d22718ab7019a1ec4138..e202e9b0e9905891750d1d836139913b16d76e15 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -99,8 +99,6 @@
    rtrim
    upper
    lower]
-  [lib.field
-   with-join-alias]
   [lib.filter
    filter
    add-filter
@@ -138,7 +136,9 @@
   [lib.join
    join
    join-clause
-   joins]
+   joins
+   with-join-alias
+   with-join-fields]
   [lib.limit
    current-limit
    limit]
@@ -150,7 +150,8 @@
   [lib.order-by
    order-by
    order-by-clause
-   order-bys]
+   order-bys
+   orderable-columns]
   [lib.query
    native-query
    query
diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc
index d517a1db38e809216c30a4dbed700663c2f8ccce..556a2b5a10013222419549844aeeca6e5b10dd48 100644
--- a/src/metabase/lib/expression.cljc
+++ b/src/metabase/lib/expression.cljc
@@ -1,8 +1,11 @@
 (ns metabase.lib.expression
-  (:refer-clojure :exclude [+ - * / case coalesce abs time concat replace])
+  (:refer-clojure
+   :exclude
+   [+ - * / case coalesce abs time concat replace])
   (:require
    [clojure.string :as str]
    [metabase.lib.common :as lib.common]
+   [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.common :as lib.schema.common]
@@ -197,3 +200,14 @@
 (lib.common/defop rtrim [s])
 (lib.common/defop upper [s])
 (lib.common/defop lower [s])
+
+(mu/defn expressions :- [:sequential lib.metadata/ColumnMetadata]
+  "Get metadata about the expressions in a given stage of a `query`."
+  [query        :- ::lib.schema/query
+   stage-number :- :int]
+  (for [[expression-name expression-definition] (:expressions (lib.util/query-stage query stage-number))]
+    (let [metadata (lib.metadata.calculation/metadata query stage-number expression-definition)]
+      (merge
+       metadata
+       {:field_ref [:expression {:lib/uuid (str (random-uuid)), :base-type (:base_type metadata)} expression-name]
+        :source    :expressions}))))
diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc
index 471b4e4fd7787aafd85fe7209e733afd0aa68f2d..24e3c91ef2fd72ca4d2ad5ec00debad7e4a432db 100644
--- a/src/metabase/lib/field.cljc
+++ b/src/metabase/lib/field.cljc
@@ -148,10 +148,6 @@
   ([query stage-number x]
    (->field query stage-number x)))
 
-(defn with-join-alias
-  "Update a `field` so that it has `join-alias`."
-  [field-or-fn join-alias]
-  (if (fn? field-or-fn)
-    (fn [query stage-number]
-      (with-join-alias (field-or-fn query stage-number) join-alias))
-    (lib.options/update-options field-or-fn assoc :join-alias join-alias)))
+(defmethod lib.join/with-join-alias-method :field
+  [field-ref join-alias]
+  (lib.options/update-options field-ref assoc :join-alias join-alias))
diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc
index b0cd714795dc6e20f84ad50f93440da684a36303..2d1476a61a9c63dc3c22b15b7fd303a0f935b98f 100644
--- a/src/metabase/lib/join.cljc
+++ b/src/metabase/lib/join.cljc
@@ -141,6 +141,37 @@
    (cond-> (join-clause query stage-number x)
      condition (assoc :condition (join-condition query stage-number condition)))))
 
+(defmulti with-join-alias-method
+  "Implementation for [[with-join-alias]]."
+  {:arglists '([x join-alias])}
+  (fn [x _join-alias]
+    (lib.dispatch/dispatch-value x)))
+
+(mu/defn with-join-alias
+  "Add a specific `join-alias` to something `x`, either a `:field` or join map. Does not recursively update other
+  references (yet; we can add this in the future)."
+  [x join-alias :- ::lib.schema.common/non-blank-string]
+  (with-join-alias-method x join-alias))
+
+(defmethod with-join-alias-method :dispatch-type/fn
+  [f join-alias]
+  (fn [query stage-number]
+    (let [x (f query stage-number)]
+      (with-join-alias-method x join-alias))))
+
+(defmethod with-join-alias-method :mbql/join
+  [join join-alias]
+  (assoc join :alias join-alias))
+
+(mu/defn with-join-fields
+  "Update a join (or a function that will return a join) to include `:fields`, either `:all`, `:none`, or a sequence of
+  references."
+  [x fields :- ::lib.schema.join/fields]
+  (if (fn? x)
+    (fn [query stage-number]
+      (with-join-fields (x query stage-number) fields))
+    (assoc x :fields fields)))
+
 (mu/defn join :- ::lib.schema/query
   "Create a join map as if by [[join-clause]] and add it to a `query`.
 
diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc
index 482bfe12eb34070130e6c7ea62716aad35be00f6..958689e89630df5b4b9956623c5b468159f80554 100644
--- a/src/metabase/lib/metadata.cljc
+++ b/src/metabase/lib/metadata.cljc
@@ -42,7 +42,7 @@
   that they are largely compatible. So they're the same for now. We can revisit this in the future if we actually want
   to differentiate between the two versions."
   [:map
-   [:lib/type [:= :metadata/field]]
+   [:lib/type [:= :metadata/field]] ; TODO -- should this be changed to `:metadata/column`?
    [:id {:optional true} ::lib.schema.id/field]
    [:name ::lib.schema.common/non-blank-string]])
 
diff --git a/src/metabase/lib/order_by.cljc b/src/metabase/lib/order_by.cljc
index 6063942568cbd1352187009fdcb22deeb8329437..c4f7814054b067359d6024556afb89cd280d69e9 100644
--- a/src/metabase/lib/order_by.cljc
+++ b/src/metabase/lib/order_by.cljc
@@ -1,11 +1,16 @@
 (ns metabase.lib.order-by
   (:require
+   [metabase.lib.aggregation :as lib.aggregation]
+   [metabase.lib.breakout :as lib.breakout]
    [metabase.lib.dispatch :as lib.dispatch]
    [metabase.lib.field :as lib.field]
+   [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.options :as lib.options]
    [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.expression :as lib.schema.expression]
    [metabase.lib.schema.order-by :as lib.schema.order-by]
+   [metabase.lib.stage :as lib.stage]
    [metabase.lib.util :as lib.util]
    [metabase.shared.util.i18n :as i18n]
    [metabase.util.malli :as mu]))
@@ -87,5 +92,42 @@
   ([query :- ::lib.schema/query]
    (order-bys query -1))
   ([query :- ::lib.schema/query
-    stage-number :- [:int]]
+    stage-number :- :int]
    (not-empty (get (lib.util/query-stage query stage-number) :order-by))))
+
+(defn- orderable-column? [{base-type :base_type, :as _column-metadata}]
+  (some (fn [orderable-base-type]
+          (isa? base-type orderable-base-type))
+        lib.schema.expression/orderable-types))
+
+(mu/defn orderable-columns :- [:sequential lib.metadata/ColumnMetadata]
+  "Get column metadata for all the columns you can order by in a given `stage-number` of a `query`. Rules are as
+  follows:
+
+  1. If the stage has aggregations or breakouts, you can only order by those columns. E.g.
+
+         SELECT id, count(*) AS count FROM core_user GROUP BY id ORDER BY count ASC;
+
+     You can't ORDER BY something not in the SELECT, e.g. ORDER BY user.first_name would not make sense here.
+
+  2. If the stage has no aggregations or breakouts, you can order by any visible Field:
+
+     a. You can filter by any custom `:expressions` in this stage of the query
+
+     b. You can filter by any Field 'exported' by the previous stage of the query, if there is one; otherwise you can
+        filter by any Fields from the current `:source-table`.
+
+     c. You can filter by any Fields exported by any explicit joins
+
+     d. You can filter by and Fields in Tables that are implicitly joinable."
+  ([query :- ::lib.schema/query]
+   (orderable-columns query -1))
+
+  ([query        :- ::lib.schema/query
+    stage-number :- :int]
+   (let [breakouts    (not-empty (lib.breakout/breakouts query stage-number))
+         aggregations (not-empty (lib.aggregation/aggregations query stage-number))]
+     (filter orderable-column?
+             (if (or breakouts aggregations)
+               (concat breakouts aggregations)
+               (lib.stage/visible-columns query stage-number))))))
diff --git a/src/metabase/lib/schema/expression.cljc b/src/metabase/lib/schema/expression.cljc
index 008238342afeda15acec07e02b087db94ca4bc95..428d7798348efef0f4c44899dc4cf9fe2546365a 100644
--- a/src/metabase/lib/schema/expression.cljc
+++ b/src/metabase/lib/schema/expression.cljc
@@ -126,8 +126,12 @@
 (mr/def ::temporal
   (expression-schema :type/Temporal "expression returning a date, time, or date time"))
 
+(def orderable-types
+  "Set of base types that are orderable."
+  #{:type/Text :type/Number :type/Temporal})
+
 (mr/def ::orderable
-  (expression-schema #{:type/Text :type/Number :type/Temporal}
+  (expression-schema orderable-types
                      "an expression that can be compared with :> or :<"))
 
 (mr/def ::equality-comparable
diff --git a/src/metabase/lib/schema/join.cljc b/src/metabase/lib/schema/join.cljc
index 596eff3156aa9161e3de3c321d66232e5c9e4573..8b9a7e1d8620c5b3ce2d94f88eac30bac7532081 100644
--- a/src/metabase/lib/schema/join.cljc
+++ b/src/metabase/lib/schema/join.cljc
@@ -24,7 +24,7 @@
 (mr/def ::fields
   [:or
    [:enum :all :none]
-   ;;; TODO -- Pretty sure fields are supposed to be unique, even excluding `:lib/uuid`
+   ;; TODO -- Pretty sure fields are supposed to be unique, even excluding `:lib/uuid`
    [:sequential {:min 1} [:ref ::ref/ref]]])
 
 ;;; The name used to alias the joined table or query. This is usually generated automatically and generally looks
diff --git a/src/metabase/lib/stage.cljc b/src/metabase/lib/stage.cljc
index 69734957490139a9d21d2c9140ce086a98f1cedb..24395ec728b6a8ffe3953a2ac7beb772b87ec979 100644
--- a/src/metabase/lib/stage.cljc
+++ b/src/metabase/lib/stage.cljc
@@ -2,6 +2,10 @@
   "Method implementations for a stage of a query."
   (:require
    [clojure.string :as str]
+   [medley.core :as m]
+   [metabase.lib.aggregation :as lib.aggregation]
+   [metabase.lib.breakout :as lib.breakout]
+   [metabase.lib.expression :as lib.expression]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
@@ -16,24 +20,6 @@
 
 (declare stage-metadata)
 
-(mu/defn ^:private breakout-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
-  [query        :- ::lib.schema/query
-   stage-number :- :int]
-  (when-let [{breakouts :breakout} (lib.util/query-stage query stage-number)]
-    (mapv (fn [field-ref]
-            (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :source :breakout))
-          breakouts)))
-
-(mu/defn ^:private aggregation-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
-  [query        :- ::lib.schema/query
-   stage-number :- :int]
-  (when-let [{aggregations :aggregation} (lib.util/query-stage query stage-number)]
-    (map-indexed (fn [i aggregation]
-                   (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation)
-                         ag-ref   [:aggregation {:lib/uuid (str (random-uuid))} i]]
-                     (assoc metadata :field_ref ag-ref)))
-                 aggregations)))
-
 (mu/defn ^:private fields-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
   [query        :- ::lib.schema/query
    stage-number :- :int]
@@ -58,6 +44,14 @@
              [(or position 0) (u/lower-case-en (or field-name ""))])
            field-metadatas))
 
+(defn- ensure-field-refs [field-metadatas]
+  (for [field-metadata field-metadatas]
+    (cond-> field-metadata
+      (not (:field_ref field-metadata))
+      (assoc :field_ref [:field
+                         {:lib/uuid (str (random-uuid)), :base-type (:base_type field-metadata)}
+                         ((some-fn :id :name) field-metadata)]))))
+
 (mu/defn ^:private source-table-default-fields :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
   "Determine the Fields we'd normally return for a source Table.
   See [[metabase.query-processor.middleware.add-implicit-clauses/add-implicit-fields]]."
@@ -66,7 +60,8 @@
   (when-let [field-metadatas (lib.metadata/fields query table-id)]
     (->> field-metadatas
          remove-hidden-default-fields
-         sort-default-fields)))
+         sort-default-fields
+         ensure-field-refs)))
 
 (mu/defn ^:private default-join-alias :- ::lib.schema.common/non-blank-string
   "Generate an alias for a join that doesn't already have one."
@@ -94,7 +89,7 @@
               (not (:alias join)) (assoc :alias (unique-name-generator (default-join-alias query stage-number join)))))
           joins)))
 
-(mu/defn ^:private default-columns-added-by-join :- [:sequential lib.metadata/ColumnMetadata]
+(mu/defn ^:private default-columns-added-by-join :- [:maybe [:sequential lib.metadata/ColumnMetadata]]
   [query        :- ::lib.schema/query
    stage-number :- :int
    join         :- ::lib.schema.join/join]
@@ -109,7 +104,7 @@
            (mapcat (partial default-columns-added-by-join query stage-number))
            (ensure-all-joins-have-aliases query stage-number joins)))))
 
-(mu/defn ^:private default-columns :- [:sequential {:min 1} lib.metadata/ColumnMetadata]
+(mu/defn ^:private default-columns :- [:sequential lib.metadata/ColumnMetadata]
   "Calculate the columns to return if `:aggregations`/`:breakout`/`:fields` are unspecified.
 
   Formula for the so-called 'default' columns is
@@ -118,7 +113,9 @@
 
   1b. Default 'visible' Fields for our `:source-table`, OR
 
-  1c. `:lib/stage-metadata` if this is a `:mbql.stage/native` stage
+  1c. Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string, OR
+
+  1d. `:lib/stage-metadata` if this is a `:mbql.stage/native` stage
 
   PLUS
 
@@ -132,11 +129,22 @@
      (stage-metadata query previous-stage-number)
      ;; 1b or 1c
      (let [{:keys [source-table], :as this-stage} (lib.util/query-stage query stage-number)]
-       (if (integer? source-table)
-         ;; 1b: default visible Fields for the source Table
-         (source-table-default-fields query source-table)
-         ;; 1c: `:lib/stage-metadata` for the native query
-         (:columns (:lib/stage-metadata this-stage)))))
+       (or
+        ;; 1b: default visible Fields for the source Table
+        (when (integer? source-table)
+          (source-table-default-fields query source-table))
+        ;; 1c. Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string, OR
+        (when (string? source-table)
+          (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" source-table)]
+            (when-let [card-id (parse-long card-id-str)]
+              (when-let [result-metadata (:result_metadata (lib.metadata/card query card-id))]
+                (not-empty (for [col (:columns result-metadata)]
+                             (assoc col
+                                    :field_ref [:field
+                                                {:lib/uuid (str (random-uuid)), :base-type (:base_type col)}
+                                                (:name col)])))))))
+        ;; 1d: `:lib/stage-metadata` for the native query
+        (:columns (:lib/stage-metadata this-stage)))))
    ;; 2: columns added by joins at this stage
    (default-columns-added-by-joins query stage-number)))
 
@@ -170,8 +178,8 @@
      (or
       (not-empty (into []
                        cat
-                       [(breakout-columns query stage-number)
-                        (aggregation-columns query stage-number)
+                       [(lib.breakout/breakouts query stage-number)
+                        (lib.aggregation/aggregations query stage-number)
                         (fields-columns query stage-number)]))
       (default-columns query stage-number)))))
 
@@ -199,3 +207,45 @@
         descriptions (for [k parts]
                        (lib.metadata.calculation/describe-top-level-key query stage-number k))]
     (str/join ", " (remove str/blank? descriptions))))
+
+(defn- implicitly-joinable-columns
+  "Columns that are implicitly joinable from some other columns in `column-metadatas`. To be joinable, the column has to
+  have appropriate FK metadata, i.e. have an `:fk_target_field_id` pointing to another Field. (I think we only include
+  this information for Databases that support FKs and joins, so I don't think we need to do an additional DB feature
+  check here.)
+
+  This does not include columns from any Tables that are already explicitly joined, and does not include multiple
+  versions of a column when there are multiple pathways to it (i.e. if there is more than one FK to a Table). This
+  behavior matches how things currently work in MLv1, at least for order by; we can adjust as needed in the future if
+  it turns out we do need that stuff.
+
+  Does not include columns that would be implicitly joinable via multiple hops."
+  [query column-metadatas]
+  (let [existing-table-ids (into #{} (map :table_id) column-metadatas)]
+    (into []
+          (comp (filter :fk_target_field_id)
+                (m/distinct-by :fk_target_field_id)
+                (map (fn [{source-field-id :id, target-field-id :fk_target_field_id}]
+                       (-> (lib.metadata/field query target-field-id)
+                           (assoc :source-field-id source-field-id))))
+                (remove #(contains? existing-table-ids (:table_id %)))
+                (m/distinct-by :table_id)
+                (mapcat (fn [{table-id :table_id, :keys [source-field-id]}]
+                          (for [field (source-table-default-fields query table-id)]
+                            (assoc field :field_ref [:field
+                                                     {:lib/uuid     (str (random-uuid))
+                                                      :base-type    (:base_type field)
+                                                      :source-field source-field-id}
+                                                     (:id field)])))))
+          column-metadatas)))
+
+(mu/defn visible-columns :- [:sequential lib.metadata/ColumnMetadata]
+  "Columns that are visible inside a given stage of a query. Ignores `:fields`, `:breakout`, and `:aggregation`.
+  Includes columns that are implicitly joinable from other Tables."
+  [query stage-number]
+  (let [query   (lib.util/update-query-stage query stage-number dissoc :fields :breakout :aggregation)
+        columns (default-columns query stage-number)]
+    (concat
+     (lib.expression/expressions query stage-number)
+     columns
+     (implicitly-joinable-columns query columns))))
diff --git a/src/metabase/lib/util.cljc b/src/metabase/lib/util.cljc
index e2b49c9446ac36ef053f98609752492f28abcb0f..90ac25a96413db9d4b86666e86c09014e6ab42b5 100644
--- a/src/metabase/lib/util.cljc
+++ b/src/metabase/lib/util.cljc
@@ -140,7 +140,7 @@
     stage-number'))
 
 (defn previous-stage-number
-  "The index of the previous stage, if there is one."
+  "The index of the previous stage, if there is one. `nil` if there is no previous stage."
   [{:keys [stages], :as _query} stage-number]
   (let [stage-number (if (neg? stage-number)
                        (+ (count stages) stage-number)
diff --git a/test/metabase/lib/order_by_test.cljc b/test/metabase/lib/order_by_test.cljc
index eabffcb2fb1759f21250d17f9aa6d21dc392efcd..5326919b1f96151bdd9847df2153d68b4f76b9e7 100644
--- a/test/metabase/lib/order_by_test.cljc
+++ b/test/metabase/lib/order_by_test.cljc
@@ -1,12 +1,16 @@
 (ns metabase.lib.order-by-test
   (:require
    [clojure.test :refer [deftest is testing]]
+   [medley.core :as m]
    [metabase.lib.convert :as lib.convert]
    [metabase.lib.core :as lib]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.query :as lib.query]
    [metabase.lib.test-metadata :as meta]
+   [metabase.lib.test-util :as lib.tu]
+   [metabase.lib.util :as lib.util]
+   [metabase.util :as u]
    #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
 
 #?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
@@ -135,3 +139,265 @@
   ;; });
 
   )
+
+(deftest ^:parallel orderable-columns-breakouts-test
+  (testing "If query has aggregations and/or breakouts, you can only order by those."
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/aggregate (lib/sum (lib/field "VENUES" "PRICE")))
+                    (lib/aggregate (lib/avg (lib/+ (lib/field "VENUES" "PRICE") 1)))
+                    (lib/breakout (lib/field "VENUES" "CATEGORY_ID")))]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:database_type      "INTEGER"
+                  :semantic_type      :type/FK
+                  :lib/type           :metadata/field
+                  :table_id           (meta/id :venues)
+                  :name               "CATEGORY_ID"
+                  :has_field_values   :none
+                  :source             :breakout
+                  :fk_target_field_id (meta/id :categories :id)
+                  :field_ref          [:field
+                                       {:base-type :type/Integer, :lib/uuid string?}
+                                       (meta/id :venues :category-id)]
+                  :effective_type     :type/Integer
+                  :id                 (meta/id :venues :category-id)
+                  :display_name       "Category ID"
+                  :base_type          :type/Integer}
+                 {:lib/type     :metadata/field
+                  :base_type    :type/Integer
+                  :name         "sum_price"
+                  :display_name "Sum of Price"
+                  :field_ref    [:aggregation {:lib/uuid string?, :base-type :type/Integer} 0]
+                  :source       :aggregation}
+                 {:lib/type     :metadata/field
+                  :base_type    :type/Float
+                  :name         "avg_price_plus_1"
+                  :display_name "Average of Price + 1"
+                  :field_ref    [:aggregation {:lib/uuid string?, :base-type :type/Float} 1]
+                  :source       :aggregation}]
+                (lib/orderable-columns query)))))))
+
+(deftest ^:parallel orderable-columns-breakouts-with-expression-test
+  (testing "If query has aggregations and/or breakouts, you can only order by those (with an expression)"
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/expression "Category ID + 1"  (lib/+ (lib/field "VENUES" "CATEGORY_ID") 1))
+                    (lib/breakout [:expression {:lib/uuid (str (random-uuid))} "Category ID + 1"]))]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:lib/type     :metadata/field
+                  :field_ref    [:expression {:lib/uuid string?} "Category ID + 1"]
+                  :name         "Category ID + 1"
+                  :display_name "Category ID + 1"
+                  :base_type    :type/Integer
+                  :source       :breakout}]
+                (lib/orderable-columns query)))))))
+
+(deftest ^:parallel orderable-columns-test
+  (let [query (lib/query-for-table-name meta/metadata-provider "VENUES")]
+    (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+      (is (=? [{:lib/type     :metadata/field
+                :name         "ID"
+                :display_name "ID"
+                :id           (meta/id :venues :id)
+                :table_id     (meta/id :venues)
+                :base_type    :type/BigInteger
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/BigInteger} (meta/id :venues :id)]}
+               {:lib/type     :metadata/field
+                :name         "NAME"
+                :display_name "Name"
+                :id           (meta/id :venues :name)
+                :table_id     (meta/id :venues)
+                :base_type    :type/Text
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]}
+               {:lib/type     :metadata/field
+                :name         "CATEGORY_ID"
+                :display_name "Category ID"
+                :id           (meta/id :venues :category-id)
+                :table_id     (meta/id :venues)
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/Integer} (meta/id :venues :category-id)]}
+               {:lib/type     :metadata/field
+                :name         "LATITUDE"
+                :display_name "Latitude"
+                :id           (meta/id :venues :latitude)
+                :table_id     (meta/id :venues)
+                :base_type    :type/Float
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/Float} (meta/id :venues :latitude)]}
+               {:lib/type     :metadata/field
+                :name         "LONGITUDE"
+                :display_name "Longitude"
+                :id           (meta/id :venues :longitude)
+                :table_id     (meta/id :venues)
+                :base_type    :type/Float
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/Float} (meta/id :venues :longitude)]}
+               {:lib/type     :metadata/field
+                :name         "PRICE"
+                :display_name "Price"
+                :id           (meta/id :venues :price)
+                :table_id     (meta/id :venues)
+                :base_type    :type/Integer
+                :field_ref    [:field {:lib/uuid string?, :base-type :type/Integer} (meta/id :venues :price)]}
+               {:lib/type     :metadata/field
+                :name         "ID"
+                :display_name "ID"
+                :id           (meta/id :categories :id)
+                :table_id     (meta/id :categories)
+                :base_type    :type/BigInteger
+                :field_ref    [:field
+                               {:lib/uuid string?, :base-type :type/BigInteger, :source-field (meta/id :venues :category-id)}
+                               (meta/id :categories :id)]}
+               {:lib/type     :metadata/field
+                :name         "NAME"
+                :display_name "Name"
+                :id           (meta/id :categories :name)
+                :table_id     (meta/id :categories)
+                :base_type    :type/Text
+                :field_ref    [:field
+                               {:lib/uuid string?, :base-type :type/Text, :source-field (meta/id :venues :category-id)}
+                               (meta/id :categories :name)]}]
+              (lib/orderable-columns query))))))
+
+(deftest ^:parallel orderable-expressions-test
+  (testing "orderable-columns should include expressions"
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/expression "Category ID + 1"  (lib/+ (lib/field "VENUES" "CATEGORY_ID") 1)))]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:lib/type     :metadata/field
+                  :base_type    :type/Integer
+                  :name         "category_id_plus_1"
+                  :display_name "Category ID + 1"
+                  :field_ref    [:expression
+                                 {:lib/uuid string?, :base-type :type/Integer}
+                                 "Category ID + 1"]
+                  :source       :expressions}
+                 {:id (meta/id :venues :id), :name "ID"}
+                 {:id (meta/id :venues :name), :name "NAME"}
+                 {:id (meta/id :venues :category-id), :name "CATEGORY_ID"}
+                 {:id (meta/id :venues :latitude), :name "LATITUDE"}
+                 {:id (meta/id :venues :longitude), :name "LONGITUDE"}
+                 {:id (meta/id :venues :price), :name "PRICE"}
+                 {:id (meta/id :categories :id), :name "ID"}
+                 {:id (meta/id :categories :name), :name "NAME"}]
+                (lib/orderable-columns query)))))))
+
+(defn- FIXME-is-empty
+  "FIXME: Currently no way to add an `:is-empty` clause inline."
+  [expr]
+  (fn [query stage-number]
+    (lib/is-empty query stage-number expr)))
+
+(deftest ^:parallel orderable-expressions-exclude-boolean-expressions-test
+  (testing "orderable-columns should filter out boolean expressions."
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/expression "Name is empty?"  (FIXME-is-empty (lib/field "VENUES" "NAME"))))]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:id (meta/id :venues :id), :name "ID"}
+                 {:id (meta/id :venues :name), :name "NAME"}
+                 {:id (meta/id :venues :category-id), :name "CATEGORY_ID"}
+                 {:id (meta/id :venues :latitude), :name "LATITUDE"}
+                 {:id (meta/id :venues :longitude), :name "LONGITUDE"}
+                 {:id (meta/id :venues :price), :name "PRICE"}
+                 {:id (meta/id :categories :id), :name "ID"}
+                 {:id (meta/id :categories :name), :name "NAME"}]
+                (lib/orderable-columns query)))))))
+
+(defn- FIXME-=
+  "FIXME: [[metabase.lib.core]] doesn't have a test-friendly version of `:=` yet."
+  [a b]
+  (fn [query stage-number]
+    (let [a (if (fn? a)
+              (a query stage-number)
+              a)
+          b (if (fn? b)
+              (b query stage-number)
+              b)]
+      ;; This is another FIXME!
+      #_{:clj-kondo/ignore [:invalid-arity]}
+      (lib/= query stage-number a b))))
+
+(deftest ^:parallel orderable-explicit-joins-test
+  (testing "orderable-columns should include columns from explicit joins"
+    (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
+                    (lib/join (-> (lib/join-clause
+                                   (meta/table-metadata :categories)
+                                   (FIXME-=
+                                    (lib/field "VENUES" "CATEGORY_ID")
+                                    (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Cat")))
+                                  (lib/with-join-alias "Cat")
+                                  (lib/with-join-fields :all))))]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:id (meta/id :venues :id), :name "ID"}
+                 {:id (meta/id :venues :name), :name "NAME"}
+                 {:id (meta/id :venues :category-id), :name "CATEGORY_ID"}
+                 {:id (meta/id :venues :latitude), :name "LATITUDE"}
+                 {:id (meta/id :venues :longitude), :name "LONGITUDE"}
+                 {:id (meta/id :venues :price), :name "PRICE"}
+                 {:lib/type     :metadata/field
+                  :name         "ID"
+                  :display_name "Categories → ID" ; should we be using the explicit alias we gave this join?
+                  :source_alias "Cat"
+                  :id           (meta/id :categories :id)
+                  :table_id     (meta/id :categories)
+                  :base_type    :type/BigInteger
+                  :field_ref    [:field
+                                 {:lib/uuid string?, :base-type :type/BigInteger, :join-alias "Cat"}
+                                 (meta/id :categories :id)]}
+                 {:lib/type     :metadata/field
+                  :name         "NAME"
+                  :display_name "Categories → Name"
+                  :source_alias "Cat"
+                  :id           (meta/id :categories :name)
+                  :table_id     (meta/id :categories)
+                  :base_type    :type/Text
+                  :field_ref    [:field
+                                 {:lib/uuid string?, :base-type :type/Text, :join-alias "Cat"}
+                                 (meta/id :categories :name)]}]
+                (lib/orderable-columns query)))))))
+
+(deftest ^:parallel orderable-columns-source-metadata-test
+  (testing "orderable-columns should use metadata for source query."
+    (let [query (lib.tu/query-with-card-source-table)]
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (is (=? [{:name "ID"
+                  :base_type :type/BigInteger
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/BigInteger} "ID"]}
+                 {:name "NAME"
+                  :base_type :type/Text
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/Text} "NAME"]}
+                 {:name "CATEGORY_ID"
+                  :base_type :type/Integer
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} "CATEGORY_ID"]}
+                 {:name "LATITUDE"
+                  :base_type :type/Float
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/Float} "LATITUDE"]}
+                 {:name "LONGITUDE"
+                  :base_type :type/Float
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/Float} "LONGITUDE"]}
+                 {:name "PRICE"
+                  :base_type :type/Integer
+                  :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} "PRICE"]}]
+                (lib/orderable-columns query)))))))
+
+(deftest ^:parallel orderable-columns-e2e-test
+  (testing "Use the metadata returned by `orderable-columns` to add a new order-by to a query."
+    (let [query (lib/query-for-table-name meta/metadata-provider "VENUES")]
+      (is (=? {:lib/type :mbql/query
+               :database (meta/id)
+               :stages   [{:lib/type     :mbql.stage/mbql
+                           :source-table (meta/id :venues)
+                           :lib/options  {:lib/uuid string?}}]}
+              query))
+      (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query))
+        (let [orderable-columns (lib/orderable-columns query)
+              col               (m/find-first #(= (:id %) (meta/id :venues :name)) orderable-columns)
+              query'            (lib/order-by query col)]
+          (is (=? {:lib/type :mbql/query
+                   :database (meta/id)
+                   :stages   [{:lib/type     :mbql.stage/mbql
+                               :source-table (meta/id :venues)
+                               :lib/options  {:lib/uuid string?}
+                               :order-by     [[:asc
+                                               {:lib/uuid string?}
+                                               [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]]]}]}
+                  query'))
+          (is (=? [[:asc
+                    {:lib/uuid string?}
+                    [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]]]
+                  (lib/order-bys query'))))))))
diff --git a/test/metabase/lib/test_metadata.cljc b/test/metabase/lib/test_metadata.cljc
index e4ea5ec929ee5992f528b5102bfbc86c8bece7d3..9885e057d3234d33fd20ff56113f7610915dfac9 100644
--- a/test/metabase/lib/test_metadata.cljc
+++ b/test/metabase/lib/test_metadata.cljc
@@ -808,7 +808,7 @@
                :semantic_type  :type/PK
                :fingerprint    nil}
               {:lib/type       :metadata/field
-               :display_name   "NAME"
+               :display_name   "NAME" ; TODO -- these display names are icky
                :field_ref      [:field "NAME" {:base-type :type/Text}]
                :name           "NAME"
                :base_type      :type/Text
diff --git a/test/metabase/lib/test_util.cljc b/test/metabase/lib/test_util.cljc
index 16af43435348805a927d2cb7be6bfd745ba0475a..876c165dd2db89febd1101e147d7c88ed27af081 100644
--- a/test/metabase/lib/test_util.cljc
+++ b/test/metabase/lib/test_util.cljc
@@ -64,3 +64,37 @@
     (fields   [_this table-id]   (for [field fields
                                        :when (= (:table_id field) table-id)]
                                    (assoc field :lib/type :metadata/field)))))
+
+(defn composed-metadata-provider
+  "A metadata provider composed of several different `metadata-providers`. Methods try each constituent provider in
+  turn from left to right until one returns a truthy result."
+  [& metadata-providers]
+  (reify metadata.protocols/MetadataProvider
+    (database [_this]            (some metadata.protocols/database                metadata-providers))
+    (table    [_this table-id]   (some #(metadata.protocols/table   % table-id)   metadata-providers))
+    (field    [_this field-id]   (some #(metadata.protocols/field   % field-id)   metadata-providers))
+    (card     [_this card-id]    (some #(metadata.protocols/card    % card-id)    metadata-providers))
+    (metric   [_this metric-id]  (some #(metadata.protocols/metric  % metric-id)  metadata-providers))
+    (segment  [_this segment-id] (some #(metadata.protocols/segment % segment-id) metadata-providers))
+    (tables   [_this]            (some metadata.protocols/tables                  metadata-providers))
+    (fields   [_this table-id]   (some #(metadata.protocols/fields  % table-id)   metadata-providers))))
+
+(def metadata-provider-with-card
+  "[[meta/metadata-provider]], but with a Card with ID 1."
+  (composed-metadata-provider
+   meta/metadata-provider
+   (mock-metadata-provider
+    {:cards [(assoc meta/saved-question
+                    :name "My Card"
+                    :id 1)]})))
+
+(defn query-with-card-source-table
+  "A query with a `card__<id>` source Table, and a metadata provider that has that Card."
+  []
+  {:lib/type     :mbql/query
+   :lib/metadata metadata-provider-with-card
+   :type         :pipeline
+   :database     (meta/id)
+   :stages       [{:lib/type     :mbql.stage/mbql
+                   :lib/options  {:lib/uuid (str (random-uuid))}
+                   :source-table "card__1"}]})