From ea9dfd1d2b7e91b604c3bc05b055a301bffde98e Mon Sep 17 00:00:00 2001
From: Case Nelson <case@metabase.com>
Date: Mon, 17 Jun 2024 12:29:57 -0600
Subject: [PATCH] Change run to make-run in process-query-for-card (#44244)

* Change run to make-run in process-query-for-card

Fixes #44160

There were two levers where a card could change how it was run.

`qp` this is basically `qp/process-query` with some optional userland
flags and constraints marked on the passed in query.
In the case of pivot tables in the
`/pivot/:card-id/query` endpoints it was `qp.pivot/process-query`

'run' this defaults to a streaming response that takes the `qp` param
and `export-format` and passes a query to qp. Most often this is
overriden to not stream the response.

Unfortunately, if `run` is overwritten, then `qp` is thrown away, and
the caller who overrides `run` must determine the correct `qp` to use.

However, when running `process-query-for-dashcard` it is difficult to properly set
`qp` for all of the dashcards without doing card lookups. So [this commit](https://github.com/metabase/metabase/blob/release-x.50.x/src/metabase/query_processor/card.clj#L230-L232) did a check to swap the runner in the presence of a pivot query that calls the **default** runner with a `qp.pivot/run-pivot-query`. The problem is that pulse needs to use a non-default runner, but there's no way for `process-query-for-card` to do both.

This commit changes `run` to `make-run` which raises the abstraction to
a function that produces a runer given a `qp`. This allows
`process-query-for-card` to change the `qp` as needed while keeping the
passed in runner.

There's really three things going on that could be teased apart
further/better.

1. `qp` - this conflates the processor (`qp/process-query` `qp.pivot/run-pivot-query`)
  and modifying query (`qp/userland-query`,
  `qp/userland-query-with-default-constraints` `(update query :info merge info)`)
2. `make-run` - How the above are processed: (streaming/realized)

* Fix tests
---
 src/metabase/api/public.clj                   |  7 ++--
 src/metabase/pulse/util.clj                   | 24 ++++++------
 src/metabase/query_processor/card.clj         | 37 +++++++++++--------
 src/metabase/query_processor/dashboard.clj    |  2 +-
 test/metabase/api/card_test.clj               |  4 +-
 test/metabase/query_processor/card_test.clj   | 25 ++++++++++---
 .../query_processor/dashboard_test.clj        |  5 ++-
 7 files changed, 63 insertions(+), 41 deletions(-)

diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj
index 9ca248b0387..cf662629c31 100644
--- a/src/metabase/api/public.clj
+++ b/src/metabase/api/public.clj
@@ -127,7 +127,7 @@
      {:error (tru "An error occurred while running the query.")})))
 
 (defn- process-query-for-card-with-id-run-fn
-  "Create the `:run` function used for [[process-query-for-card-with-id]] and [[process-query-for-dashcard]]."
+  "Create the `:make-run` function used for [[process-query-for-card-with-id]] and [[process-query-for-dashcard]]."
   [qp export-format]
   (fn run [query info]
     (qp.streaming/streaming-response [rff export-format (u/slugify (:card-name info))]
@@ -153,7 +153,8 @@
     (m/mapply qp.card/process-query-for-card card-id export-format
               :parameters parameters
               :context    :public-question
-              :run        (process-query-for-card-with-id-run-fn qp export-format)
+              :qp         qp
+              :make-run   process-query-for-card-with-id-run-fn
               options)))
 
 (defn ^:private process-query-for-card-with-public-uuid
@@ -267,7 +268,7 @@
                                    (string? parameters) (json/parse-string keyword))
                   :export-format export-format
                   :qp            qp
-                  :run           (process-query-for-card-with-id-run-fn qp export-format)})]
+                  :make-run      process-query-for-card-with-id-run-fn})]
     ;; Run this query with full superuser perms. We don't want the various perms checks failing because there are no
     ;; current user perms; if this Dashcard is public you're by definition allowed to run it without a perms check
     ;; anyway
diff --git a/src/metabase/pulse/util.clj b/src/metabase/pulse/util.clj
index 6d830cfa588..e409f8b6326 100644
--- a/src/metabase/pulse/util.clj
+++ b/src/metabase/pulse/util.clj
@@ -74,17 +74,19 @@
                                           :dashcard dashcard
                                           :type     :card
                                           :result   (qp.dashboard/process-query-for-dashcard
-                                                     :dashboard-id  dashboard-id
-                                                     :card-id       card-id
-                                                     :dashcard-id   (u/the-id dashcard)
-                                                     :context       :dashboard-subscription
-                                                     :export-format :api
-                                                     :parameters    parameters
-                                                     :middleware    {:process-viz-settings? true
-                                                                     :js-int-to-string?     false}
-                                                     :run           (^:once fn* [query info]
-                                                                     (qp/process-query
-                                                                      (qp/userland-query-with-default-constraints query info))))})
+                                                      :dashboard-id  dashboard-id
+                                                      :card-id       card-id
+                                                      :dashcard-id   (u/the-id dashcard)
+                                                      :context       :dashboard-subscription
+                                                      :export-format :api
+                                                      :parameters    parameters
+                                                      :middleware    {:process-viz-settings? true
+                                                                      :js-int-to-string?     false}
+                                                      :make-run      (fn make-run [qp _export-format]
+                                                                       (^:once fn* [query info]
+                                                                               (qp
+                                                                                 (qp/userland-query-with-default-constraints query info)
+                                                                                 nil))))})
           result                       (result-fn card-id)
           series-results               (map (comp result-fn :id) multi-cards)]
       (when-not (and (get-in dashcard [:visualization_settings :card.hide_empty])
diff --git a/src/metabase/query_processor/card.clj b/src/metabase/query_processor/card.clj
index 61b3a2c3deb..87cda35bb8f 100644
--- a/src/metabase/query_processor/card.clj
+++ b/src/metabase/query_processor/card.clj
@@ -166,11 +166,11 @@
 (mu/defn process-query-for-card-default-qp :- :some
   "Default value of the `:qp` option for [[process-query-for-card]]."
   [query :- ::qp.schema/query
-   rff   :- ::qp.schema/rff]
+   rff   :- [:maybe ::qp.schema/rff]]
   (qp/process-query (qp/userland-query query) rff))
 
-(defn- process-query-for-card-default-run-fn
-  "Create the default `:run` function for [[process-query-for-card]]."
+(defn process-query-for-card-default-run-fn
+  "Create the default `:make-run` function for [[process-query-for-card]]."
   [qp export-format]
   (^:once fn* [query info]
    (qp.streaming/streaming-response [rff export-format (u/slugify (:card-name info))]
@@ -179,10 +179,14 @@
 (mu/defn process-query-for-card
   "Run the query for Card with `parameters` and `constraints`. By default, returns results in a
   `metabase.async.streaming_response.StreamingResponse` (see [[metabase.async.streaming-response]]) that should be
-  returned as the result of an API endpoint fn, but you can return something different by passing a different `:run`
-  option. `:run` has a signature
+  returned as the result of an API endpoint fn, but you can return something different by passing a different `:make-run`
+  option. `:make-run` has a signature.
 
-    (run query info) => result
+    (make-run qp export-format) => (fn run [query info])
+
+  The produced `run` fn has a signature, it should use the qp in to produce the results.
+
+    (run query info) => results
 
   Will throw an Exception if preconditions (such as read perms) are not met *before* returning the
   `StreamingResponse`.
@@ -192,13 +196,12 @@
   options."
   [card-id :- ::lib.schema.id/card
    export-format
-   & {:keys [parameters constraints context dashboard-id dashcard-id middleware qp run ignore-cache]
+   & {:keys [parameters constraints context dashboard-id dashcard-id middleware qp make-run ignore-cache]
       :or   {constraints (qp.constraints/default-query-constraints)
              context     :question
-             qp          process-query-for-card-default-qp
-             ;; param `run` can be used to control how the query is ran, e.g. if you need to customize the `context`
+             ;; param `make-run` can be used to control how the query is ran, e.g. if you need to customize the `context`
              ;; passed to the QP
-             run         (process-query-for-card-default-run-fn qp export-format)}}]
+             make-run    process-query-for-card-default-run-fn}}]
   {:pre [(int? card-id) (u/maybe? sequential? parameters)]}
   (let [dash-viz (when (and (not= context :question)
                             dashcard-id)
@@ -207,12 +210,17 @@
                                                  :type :result_metadata :visualization_settings :display
                                                  :cache_invalidated_at]
                                                 :id card-id))
+        ;; We need to check this here because dashcards don't get selected until this point
+        qp       (if (= :pivot (:display card))
+                   qp.pivot/run-pivot-query
+                   (or qp process-query-for-card-default-qp))
+        runner   (make-run qp export-format)
         query    (-> (query-for-card card parameters constraints middleware {:dashboard-id dashboard-id})
                      (update :viz-settings (fn [viz] (merge viz dash-viz)))
                      (update :middleware (fn [middleware]
                                            (merge
-                                            {:js-int-to-string? true, :ignore-cached-results? ignore-cache}
-                                            middleware))))
+                                             {:js-int-to-string? true, :ignore-cached-results? ignore-cache}
+                                             middleware))))
         info     (cond-> {:executed-by            api/*current-user-id*
                           :context                context
                           :card-id                card-id
@@ -226,7 +234,4 @@
     (log/tracef "Running query for Card %d:\n%s" card-id
                 (u/pprint-to-str query))
     (binding [qp.perms/*card-id* card-id]
-      (let [run (if (= :pivot (:display card))
-                  (process-query-for-card-default-run-fn qp.pivot/run-pivot-query export-format)
-                  run)]
-        (run query info)))))
+      (runner query info))))
diff --git a/src/metabase/query_processor/dashboard.clj b/src/metabase/query_processor/dashboard.clj
index 568a8a47b5b..f5f00762aae 100644
--- a/src/metabase/query_processor/dashboard.clj
+++ b/src/metabase/query_processor/dashboard.clj
@@ -163,7 +163,7 @@
 (defn process-query-for-dashcard
   "Like [[metabase.query-processor.card/process-query-for-card]], but runs the query for a `DashboardCard` with
   `parameters` and `constraints`. By default, returns a `metabase.async.streaming_response.StreamingResponse` (see
-  [[metabase.async.streaming-response]]), but this may vary if you pass in a different `:run` function. Will throw an
+  [[metabase.async.streaming-response]]), but this may vary if you pass in a different `:make-run` function. Will throw an
   Exception if preconditions such as proper permissions are not met *before* returning the `StreamingResponse`.
 
   See [[metabase.query-processor.card/process-query-for-card]] for more information about the various parameters."
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index a615361ba4f..3246e565100 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -2381,8 +2381,8 @@
       (let [orig qp.card/process-query-for-card]
         (with-redefs [qp.card/process-query-for-card (fn [card-id export-format & options]
                                                        (apply orig card-id export-format
-                                                              :run (fn [{:keys [constraints]} _]
-                                                                     {:constraints constraints})
+                                                              :make-run (constantly (fn [{:keys [constraints]} _]
+                                                                                      {:constraints constraints}))
                                                               options))]
           (testing "Sanity check: this CSV download should not be subject to C O N S T R A I N T S"
             (is (= {:constraints nil}
diff --git a/test/metabase/query_processor/card_test.clj b/test/metabase/query_processor/card_test.clj
index 2cab76d5609..2db499c78e5 100644
--- a/test/metabase/query_processor/card_test.clj
+++ b/test/metabase/query_processor/card_test.clj
@@ -21,9 +21,10 @@
   ;; stuff doesn't belong in the Dashboard QP namespace
   (binding [api/*current-user-permissions-set* (atom #{"/"})]
     (qp.card/process-query-for-card
-     card-id :api
-     :run (fn [query info]
-            (qp/process-query (assoc query :info info))))))
+      card-id :api
+      :make-run (constantly
+                  (fn [query info]
+                    (qp/process-query (assoc query :info info)))))))
 
 (defn field-filter-query
   "A query with a Field Filter parameter"
@@ -170,6 +171,17 @@
       (is (= [[100]]
              (mt/rows (run-query-for-card card-id)))))))
 
+(deftest ^:parallel pivot-tables-should-not-override-the-run-function
+  (testing "Pivot tables should not override the run function (#44160)"
+    (t2.with-temp/with-temp [:model/Card {card-id :id} {:dataset_query
+                                                        (mt/mbql-query venues
+                                                                       {:aggregation [[:count]]})
+                                                        :display :pivot}]
+      (let [result (run-query-for-card card-id)]
+        (is (=? {:status :completed}
+                result))
+        (is (= [[100]] (mt/rows result)))))))
+
 (deftest nested-query-permissions-test
   (testing "Should be able to run a Card with another Card as its source query with just perms for the former (#15131)"
     (mt/with-no-data-perms-for-all-users!
@@ -190,9 +202,10 @@
             (letfn [(process-query-for-card [card]
                       (qp.card/process-query-for-card
                        (u/the-id card) :api
-                       :run (fn [query info]
-                              (let [info (assoc info :query-hash (byte-array 0))]
-                                (qp/process-query (assoc query :info info))))))]
+                       :make-run (constantly
+                                   (fn [query info]
+                                     (let [info (assoc info :query-hash (byte-array 0))]
+                                       (qp/process-query (assoc query :info info)))))))]
               (testing "Should not be able to run the parent Card"
                 (is (not (mi/can-read? disallowed-collection)))
                 (is (not (mi/can-read? parent-card)))
diff --git a/test/metabase/query_processor/dashboard_test.clj b/test/metabase/query_processor/dashboard_test.clj
index ae5d8e3da8b..3739eac91ed 100644
--- a/test/metabase/query_processor/dashboard_test.clj
+++ b/test/metabase/query_processor/dashboard_test.clj
@@ -21,8 +21,9 @@
            :dashboard-id dashboard-id
            :card-id      card-id
            :dashcard-id  dashcard-id
-           :run          (fn run [query info]
-                           (qp/process-query (assoc query :info info)))
+           :make-run     (constantly
+                           (fn run [query info]
+                             (qp/process-query (assoc query :info info))))
            options)))
 
 (deftest resolve-parameters-validation-test
-- 
GitLab