From 0dff0e205fed65db1157e22551f5c382378b3e51 Mon Sep 17 00:00:00 2001
From: Ngoc Khuat <qn.khuat@gmail.com>
Date: Thu, 20 Jul 2023 17:04:40 +0700
Subject: [PATCH] Fetch-values for implicit actions save QueryExecution in
 action context (#32465)

---
 resources/migrations/000_migrations.yaml      | 25 +++++++++++++++++++
 shared/src/metabase/mbql/schema.cljc          |  6 +++--
 src/metabase/actions/execution.clj            | 20 ++++++---------
 src/metabase/api/dashboard.clj                | 14 ++++++-----
 src/metabase/api/public.clj                   | 14 ++++++-----
 src/metabase/models/action.clj                |  3 +--
 src/metabase/models/dashboard_card.clj        |  7 ++++++
 .../middleware/process_userland_query.clj     | 11 ++++----
 test/metabase/actions/execution_test.clj      | 20 +++++++++++++++
 test/metabase/api/dataset_test.clj            |  1 +
 test/metabase/api/public_test.clj             | 10 ++++----
 .../process_userland_query_test.clj           |  2 ++
 12 files changed, 95 insertions(+), 38 deletions(-)
 create mode 100644 test/metabase/actions/execution_test.clj

diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index ec6a440b741..6af0e535811 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -15948,6 +15948,31 @@ databaseChangeLog:
               tableName: computation_job
       rollback: # no rollback needed since this table has been unused since 2018
 
+  - changeSet:
+      id: v48.00-005
+      author: qnkhuat
+      comment: Added 0.48.0 - Add query_execution.action_id
+      changes:
+        - addColumn:
+            tableName: query_execution
+            columns:
+              - column:
+                  name: action_id
+                  type: integer
+                  remarks: 'The ID of the action associated with this query execution, if any.'
+
+  - changeSet:
+      id: v48.00-006
+      author: qnkhuat
+      comment: Added 0.48.0 - Index query_execution.action_id
+      changes:
+        - createIndex:
+            tableName: query_execution
+            indexName: idx_query_execution_action_id
+            columns:
+              column:
+                name: action_id
+
   # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
 
 ########################################################################################################################
diff --git a/shared/src/metabase/mbql/schema.cljc b/shared/src/metabase/mbql/schema.cljc
index c70614f6455..b924ddb660b 100644
--- a/shared/src/metabase/mbql/schema.cljc
+++ b/shared/src/metabase/mbql/schema.cljc
@@ -1684,7 +1684,8 @@
 
 (def Context
   "Schema for `info.context`; used for informational purposes to record how a query was executed."
-  (s/enum :ad-hoc
+  (s/enum :action
+          :ad-hoc
           :collection
           :csv-download
           :dashboard
@@ -1708,6 +1709,7 @@
    ;; used for permissions checking or anything like that so don't try to be sneaky
    (s/optional-key :context)                   (s/maybe Context)
    (s/optional-key :executed-by)               (s/maybe helpers/IntGreaterThanZero)
+   (s/optional-key :action-id)                 (s/maybe helpers/IntGreaterThanZero)
    (s/optional-key :card-id)                   (s/maybe helpers/IntGreaterThanZero)
    (s/optional-key :card-name)                 (s/maybe helpers/NonBlankString)
    (s/optional-key :dashboard-id)              (s/maybe helpers/IntGreaterThanZero)
@@ -1720,7 +1722,7 @@
    ;; these in yourself. In fact, I would like this a lot better if we could take these keys out of `:info` entirely
    ;; and have the code that saves QueryExceutions figure out their values when it goes to save them
    (s/optional-key :query-hash)                (s/maybe #?(:clj (Class/forName "[B")
-                                            :cljs s/Any))})
+                                                           :cljs s/Any))})
 
 
 ;;; --------------------------------------------- Metabase [Outer] Query ---------------------------------------------
diff --git a/src/metabase/actions/execution.clj b/src/metabase/actions/execution.clj
index f9b2eb826fd..219ec637d66 100644
--- a/src/metabase/actions/execution.clj
+++ b/src/metabase/actions/execution.clj
@@ -206,15 +206,15 @@
     (execute-action! action request-parameters)))
 
 (defn- fetch-implicit-action-values
-  [dashboard-id action request-parameters]
+  [action request-parameters]
   (api/check (contains? #{"row/update" "row/delete"} (:kind action))
              400
              (tru "Values can only be fetched for actions that require a Primary Key."))
   (let [implicit-action (keyword (:kind action))
         {:keys [prefetch-parameters]} (build-implicit-query action implicit-action request-parameters)
         info {:executed-by api/*current-user-id*
-              :context :question
-              :dashboard-id dashboard-id}
+              :context     :action
+              :action-id   (:id action)}
         card (t2/select-one Card :id (:model_id action))
         ;; prefilling a form with day old data would be bad
         result (binding [persisted-info/*allow-persisted-substitution* false]
@@ -234,12 +234,8 @@
 
 (defn fetch-values
   "Fetch values to pre-fill implicit action execution - custom actions will return no values.
-   Must pass in parameters of shape `{<parameter-id> <value>}` for primary keys."
-  [dashboard-id dashcard-id request-parameters]
-  (let [dashcard (api/check-404 (t2/select-one DashboardCard
-                                               :id dashcard-id
-                                               :dashboard_id dashboard-id))
-        action (api/check-404 (action/select-action :id (:action_id dashcard)))]
-    (if (= :implicit (:type action))
-      (fetch-implicit-action-values dashboard-id action request-parameters)
-      {})))
+  Must pass in parameters of shape `{<parameter-id> <value>}` for primary keys."
+  [action request-parameters]
+  (if (= :implicit (:type action))
+    (fetch-implicit-action-values action request-parameters)
+    {}))
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index a0d67589e7e..7989da0690d 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -955,15 +955,17 @@
 
 
 ;;; ---------------------------------- Executing the action associated with a Dashcard -------------------------------
-#_{:clj-kondo/ignore [:deprecated-var]}
-(api/defendpoint-schema GET "/:dashboard-id/dashcard/:dashcard-id/execute"
+
+(api/defendpoint GET "/:dashboard-id/dashcard/:dashcard-id/execute"
   "Fetches the values for filling in execution parameters. Pass PK parameters and values to select."
   [dashboard-id dashcard-id parameters]
-  {dashboard-id su/IntGreaterThanZero
-   dashcard-id su/IntGreaterThanZero
-   parameters su/JSONString}
+  {dashboard-id ms/PositiveInt
+   dashcard-id  ms/PositiveInt
+   parameters   ms/JSONString}
   (api/read-check :model/Dashboard dashboard-id)
-  (actions.execution/fetch-values dashboard-id dashcard-id (json/parse-string parameters)))
+  (actions.execution/fetch-values
+   (api/check-404 (dashboard-card/dashcard->action dashcard-id))
+   (json/parse-string parameters)))
 
 #_{:clj-kondo/ignore [:deprecated-var]}
 (api/defendpoint-schema POST "/:dashboard-id/dashcard/:dashcard-id/execute"
diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj
index 130fcd11954..6ce13d3fba7 100644
--- a/src/metabase/api/public.clj
+++ b/src/metabase/api/public.clj
@@ -20,6 +20,7 @@
    [metabase.models.action :as action]
    [metabase.models.card :as card :refer [Card]]
    [metabase.models.dashboard :refer [Dashboard]]
+   [metabase.models.dashboard-card :as dashboard-card]
    [metabase.models.dimension :refer [Dimension]]
    [metabase.models.field :refer [Field]]
    [metabase.models.interface :as mi]
@@ -298,15 +299,16 @@
      :export-format :api
      :parameters    parameters)))
 
-#_{:clj-kondo/ignore [:deprecated-var]}
-(api/defendpoint-schema GET "/dashboard/:uuid/dashcard/:dashcard-id/execute"
+(api/defendpoint GET "/dashboard/:uuid/dashcard/:dashcard-id/execute"
   "Fetches the values for filling in execution parameters. Pass PK parameters and values to select."
   [uuid dashcard-id parameters]
-  {dashcard-id su/IntGreaterThanZero
-   parameters su/JSONString}
+  {dashcard-id ms/PositiveInt
+   parameters  ms/JSONString}
   (validation/check-public-sharing-enabled)
-  (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))]
-    (actions.execution/fetch-values dashboard-id dashcard-id (json/parse-string parameters))))
+  (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid :archived false))
+  (actions.execution/fetch-values
+   (api/check-404 (dashboard-card/dashcard->action dashcard-id))
+   (json/parse-string parameters)))
 
 (def ^:private dashcard-execution-throttle (throttle/make-throttler :dashcard-id :attempts-threshold 5000))
 
diff --git a/src/metabase/models/action.clj b/src/metabase/models/action.clj
index 19db88f16d3..e6be410491e 100644
--- a/src/metabase/models/action.clj
+++ b/src/metabase/models/action.clj
@@ -3,7 +3,6 @@
    [cheshire.core :as json]
    [medley.core :as m]
    [metabase.models.card :refer [Card]]
-   [metabase.models.dashboard-card :refer [DashboardCard]]
    [metabase.models.interface :as mi]
    [metabase.models.query :as query]
    [metabase.models.serialization :as serdes]
@@ -94,7 +93,7 @@
   [{archived? :archived, id :id, model-id :model_id, :as changes}]
   (u/prog1 changes
     (if archived?
-      (t2/delete! DashboardCard :action_id id)
+      (t2/delete! :model/DashboardCard :action_id id)
       (check-model-is-not-a-saved-question model-id))))
 
 (defmethod mi/perms-objects-set :model/Action
diff --git a/src/metabase/models/dashboard_card.clj b/src/metabase/models/dashboard_card.clj
index 6bcee0ebd59..728a3ba2b4e 100644
--- a/src/metabase/models/dashboard_card.clj
+++ b/src/metabase/models/dashboard_card.clj
@@ -5,6 +5,7 @@
    [metabase.db :as mdb]
    [metabase.db.query :as mdb.query]
    [metabase.db.util :as mdb.u]
+   [metabase.models.action :as action]
    [metabase.models.card :refer [Card]]
    [metabase.models.dashboard-card-series :refer [DashboardCardSeries]]
    [metabase.models.interface :as mi]
@@ -104,6 +105,12 @@
 
 ;;; ---------------------------------------------------- CRUD FNS ----------------------------------------------------
 
+(defn dashcard->action
+  "Get the action associated with a dashcard if exists, return `nil` otherwise."
+  [dashcard-or-dashcard-id]
+  (some->> (t2/select-one-fn :action_id :model/DashboardCard :id (u/the-id dashcard-or-dashcard-id))
+           (action/select-action :id)))
+
 (s/defn retrieve-dashboard-card
   "Fetch a single DashboardCard by its ID value."
   [id :- su/IntGreaterThanZero]
diff --git a/src/metabase/query_processor/middleware/process_userland_query.clj b/src/metabase/query_processor/middleware/process_userland_query.clj
index 9bca73db5b4..e0ff5e04d0e 100644
--- a/src/metabase/query_processor/middleware/process_userland_query.clj
+++ b/src/metabase/query_processor/middleware/process_userland_query.clj
@@ -74,7 +74,7 @@
   (merge
    (-> query-execution
        add-running-time
-       (dissoc :error :hash :executor_id :card_id :dashboard_id :pulse_id :result_rows :native))
+       (dissoc :error :hash :executor_id :action_id :card_id :dashboard_id :pulse_id :result_rows :native))
    result
    {:status                 :completed
     :average_execution_time (when cached?
@@ -108,13 +108,14 @@
 (defn- query-execution-info
   "Return the info for the QueryExecution entry for this `query`."
   {:arglists '([query])}
-  [{{:keys [executed-by query-hash context card-id dashboard-id pulse-id]} :info
-    database-id                                                            :database
-    query-type                                                             :type
-    :as                                                                    query}]
+  [{{:keys [executed-by query-hash context action-id card-id dashboard-id pulse-id]} :info
+    database-id                                                                      :database
+    query-type                                                                       :type
+    :as                                                                              query}]
   {:pre [(instance? (Class/forName "[B") query-hash)]}
   {:database_id       database-id
    :executor_id       executed-by
+   :action_id         action-id
    :card_id           card-id
    :dashboard_id      dashboard-id
    :pulse_id          pulse-id
diff --git a/test/metabase/actions/execution_test.clj b/test/metabase/actions/execution_test.clj
new file mode 100644
index 00000000000..491140969e9
--- /dev/null
+++ b/test/metabase/actions/execution_test.clj
@@ -0,0 +1,20 @@
+(ns metabase.actions.execution-test
+  (:require
+   [clojure.test :refer :all]
+   [metabase.actions.execution :as actions.execution]
+   [metabase.models.action :as action]
+   [metabase.test :as mt]
+   [toucan2.core :as t2]))
+
+(set! *warn-on-reflection* true)
+
+(deftest fetch-values-save-execution-info-test
+  (testing "fetch values for implicit action will save an execution info"
+    (mt/with-actions-enabled
+      (mt/with-actions [_                   {:dataset true :dataset_query (mt/mbql-query venues {:fields [$id $name]})}
+                        {:keys [action-id]} {:type :implicit :kind "row/update"}]
+        (is (= {"id" 1 "name" "Red Medicine"}
+               (actions.execution/fetch-values (action/select-action :id action-id) {"id" 1})))
+        ;; the query execution is saved async, so we need to sleep a bit
+        (Thread/sleep 200)
+        (is (true? (t2/exists? :model/QueryExecution :action_id action-id :context :action)))))))
diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj
index 61871d55727..8d35d95b6a6 100644
--- a/test/metabase/api/dataset_test.clj
+++ b/test/metabase/api/dataset_test.clj
@@ -107,6 +107,7 @@
                   :dashboard_id nil
                   :error        nil
                   :id           true
+                  :action_id    nil
                   :cache_hit    false
                   :database_id  (mt/id)
                   :started_at   true
diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj
index 8c257b66815..613263bc184 100644
--- a/test/metabase/api/public_test.clj
+++ b/test/metabase/api/public_test.clj
@@ -397,11 +397,11 @@
           (t2/update! Card (u/the-id card) {:archived true})
           (is (= {:name true, :ordered_cards 0, :ordered_tabs 0}
                  (fetch-public-dashboard dash)))))
-            (testing "dashboard with tabs should return ordered_tabs"
-        (api.dashboard-test/with-simple-dashboard-with-tabs [{:keys [dashboard-id]}]
-          (t2/update! :model/Dashboard :id dashboard-id (shared-obj))
-          (is (= {:name true, :ordered_cards 2, :ordered_tabs 2}
-                 (fetch-public-dashboard (t2/select-one :model/Dashboard :id dashboard-id)))))))))
+      (testing "dashboard with tabs should return ordered_tabs"
+       (api.dashboard-test/with-simple-dashboard-with-tabs [{:keys [dashboard-id]}]
+         (t2/update! :model/Dashboard :id dashboard-id (shared-obj))
+         (is (= {:name true, :ordered_cards 2, :ordered_tabs 2}
+                (fetch-public-dashboard (t2/select-one :model/Dashboard :id dashboard-id)))))))))
 
 (deftest public-dashboard-with-implicit-action-only-expose-unhidden-fields
   (mt/with-temporary-setting-values [enable-public-sharing true]
diff --git a/test/metabase/query_processor/middleware/process_userland_query_test.clj b/test/metabase/query_processor/middleware/process_userland_query_test.clj
index 82eec6e5495..60fefc3a1bf 100644
--- a/test/metabase/query_processor/middleware/process_userland_query_test.clj
+++ b/test/metabase/query_processor/middleware/process_userland_query_test.clj
@@ -69,6 +69,7 @@
               :native       false
               :pulse_id     nil
               :card_id      nil
+              :action_id    nil
               :context      nil
               :running_time true
               :cache_hit    false
@@ -94,6 +95,7 @@
               :json_query   query
               :native       false
               :pulse_id     nil
+              :action_id    nil
               :card_id      nil
               :context      nil
               :running_time true
-- 
GitLab