Code for running a query in the context of a specific DashboardCard. | (ns metabase.query-processor.dashboard
(:require
[clojure.string :as str]
[medley.core :as m]
[metabase.api.common :as api]
[metabase.driver.common.parameters.operators :as params.ops]
[metabase.events :as events]
[metabase.legacy-mbql.normalize :as mbql.normalize]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.models.user-parameter-value :as user-parameter-value]
[metabase.query-processor.card :as qp.card]
[metabase.query-processor.error-type :as qp.error-type]
[metabase.query-processor.middleware.constraints :as qp.constraints]
[metabase.util :as u]
[metabase.util.i18n :refer [tru]]
[metabase.util.log :as log]
[metabase.util.malli :as mu]
[steffan-westcott.clj-otel.api.trace.span :as span]
^{:clj-kondo/ignore [:discouraged-namespace]}
[toucan2.core :as t2])) |
Check that the Card with | (defn- check-card-and-dashcard-are-in-dashboard
[dashboard-id card-id dashcard-id]
(api/check-404
(or (t2/exists? :model/DashboardCard
:id dashcard-id
:dashboard_id dashboard-id
:card_id card-id)
(and
(t2/exists? :model/DashboardCard
:id dashcard-id
:dashboard_id dashboard-id)
(t2/exists? :model/DashboardCardSeries
:card_id card-id
:dashboardcard_id dashcard-id))))) |
(defn- resolve-param-for-card
[card-id dashcard-id param-id->param {param-id :id, :as request-param}]
(when-not param-id
(throw (ex-info (tru "Unable to resolve invalid query parameter: parameter is missing :id")
{:type qp.error-type/invalid-parameter
:invalid-parameter request-param})))
(log/tracef "Resolving parameter %s\n%s" (pr-str param-id) (u/pprint-to-str request-param))
;; find information about this dashboard parameter by its parameter `:id`. If no parameter with this ID
;; exists, it is an error.
(let [matching-param (or (get param-id->param param-id)
(throw (ex-info (tru "Dashboard does not have a parameter with ID {0}." (pr-str param-id))
{:type qp.error-type/invalid-parameter
:status-code 400})))]
(log/tracef "Found matching Dashboard parameter\n%s" (u/pprint-to-str (update matching-param :mappings (fn [mappings]
(into #{} (map #(dissoc % :dashcard)) mappings)))))
;; now find the mapping for this specific card. If there is no mapping, we can just ignore this parameter.
(when-let [matching-mapping (or (some (fn [mapping]
(when (and (= (:card_id mapping) card-id)
(= (get-in mapping [:dashcard :id]) dashcard-id))
mapping))
(:mappings matching-param))
(log/tracef "Parameter has no mapping for Card %d; skipping" card-id))]
(log/tracef "Found matching mapping for Card %d, Dashcard %d:\n%s"
card-id dashcard-id
(u/pprint-to-str (update matching-mapping :dashcard #(select-keys % [:id :parameter_mappings]))))
;; if `request-param` specifies type, then validate that the type is allowed
(when (:type request-param)
(qp.card/check-allowed-parameter-value-type
param-id
(or (when (and (= (:type matching-param) :dimension)
(not= (:widget-type matching-param) :none))
(:widget-type matching-param))
(:type matching-param))
(:type request-param)))
;; ok, now return the merged parameter info map.
(merge
{:type (:type matching-param)}
request-param
;; if value comes in as a lone value for an operator filter type (as will be the case for embedding) wrap it in a
;; vector so the parameter handling code doesn't explode.
(let [value (:value request-param)]
(when (and (params.ops/operator? (:type matching-param))
(if (string? value)
(not (str/blank? value))
(some? value))
(not (sequential? value)))
{:value [value]}))
{:id param-id
:target (:target matching-mapping)})))) | |
DashboardCard parameter mappings can specify default values, and we need to make sure the parameters map returned by [[resolve-params-for-query]] includes entries for any default values. So we'll do this by creating a entries for all the parameters with defaults, and then merge together a map of param-id->default-entry with a map of param-id->request-entry (so the value from the request takes precedence over the default value) | |
Construct parameter entries for any parameters with default values in | (defn- dashboard-param-defaults
[dashboard-param-id->param card-id]
(into
{}
(comp (filter (fn [[_ {:keys [default]}]]
default))
(map (fn [[param-id {:keys [default mappings]}]]
[param-id {:id param-id
:default default
;; make sure we include target info so we can actually map this back to a template
;; tag/param declaration
:target (some (fn [{mapping-card-id :card_id, :keys [target]}]
(when (= mapping-card-id card-id)
target))
mappings)}]))
(filter (fn [[_ {:keys [target]}]]
target)))
dashboard-param-id->param)) |
(mu/defn- resolve-params-for-query :- [:maybe [:sequential :map]]
"Given a sequence of parameters included in a query-processing request to run the query for a Dashboard/Card, validate
that those parameters exist and have allowed types, and merge in default values and other info from the parameter
mappings."
[dashboard-id :- ::lib.schema.id/dashboard
card-id :- ::lib.schema.id/card
dashcard-id :- ::lib.schema.id/dashcard
request-params :- [:maybe [:sequential :map]]]
(log/tracef "Resolving Dashboard %d Card %d query request parameters" dashboard-id card-id)
(let [request-params (mbql.normalize/normalize-fragment [:parameters] request-params)
dashboard (-> (t2/select-one :model/Dashboard :id dashboard-id)
(t2/hydrate :resolved-params)
(api/check-404))
dashboard-param-id->param (into {}
;; remove the `:default` values from Dashboard params. We don't ACTUALLY want to
;; use these values ourselves -- the expectation is that the frontend will pass
;; them in as an actual `:value` if it wants to use them. If we leave them
;; around things get confused and it prevents us from actually doing the
;; expected `1 = 1` substitution for Field filters. See comments in #20503 for
;; more information.
(map (fn [[param-id param]]
[param-id (dissoc param :default)]))
(:resolved-params dashboard))
;; ignore default values in request params as well. (#20516)
request-param-id->param (into {} (map (juxt :id #(dissoc % :default))) request-params)
merged-parameters (vals (merge (dashboard-param-defaults dashboard-param-id->param card-id)
request-param-id->param))]
(when-let [user-id api/*current-user-id*]
(when (seq request-params)
(user-parameter-value/store! user-id dashboard-id request-params)))
(log/tracef "Dashboard parameters:\n%s\nRequest parameters:\n%s\nMerged:\n%s"
(u/pprint-to-str (update-vals dashboard-param-id->param
(fn [param]
(update param :mappings (fn [mappings]
(into #{} (map #(dissoc % :dashcard)) mappings))))))
(u/pprint-to-str request-param-id->param)
(u/pprint-to-str merged-parameters))
(u/prog1
(into [] (comp (map (partial resolve-param-for-card card-id dashcard-id dashboard-param-id->param))
(filter some?))
merged-parameters)
(log/tracef "Resolved =>\n%s" (u/pprint-to-str <>))))) | |
Like [[metabase.query-processor.card/process-query-for-card]], but runs the query for a See [[metabase.query-processor.card/process-query-for-card]] for more information about the various parameters. | (defn process-query-for-dashcard
{:arglists '([& {:keys [dashboard-id card-id dashcard-id export-format parameters ignore-cache constraints parameters middleware]}])}
[& {:keys [dashboard-id card-id dashcard-id parameters export-format]
:or {export-format :api}
:as options}]
(span/with-span! {:name "run-query-for-dashcard-async"
:attributes {:dashboard/id dashboard-id
:dashcard/id dashcard-id
:card/id card-id}}
(events/publish-event! :event/dashboard-queried {:object-id dashboard-id :user-id api/*current-user-id*})
;; make sure we can read this Dashboard. Card will get read-checked later on inside
;; [[qp.card/process-query-for-card]]
(api/read-check :model/Dashboard dashboard-id)
(check-card-and-dashcard-are-in-dashboard dashboard-id card-id dashcard-id)
(let [resolved-params (resolve-params-for-query dashboard-id card-id dashcard-id parameters)
options (merge
{:ignore-cache false
:constraints (qp.constraints/default-query-constraints)
:context :dashboard}
options
{:parameters resolved-params
:dashboard-id dashboard-id})]
(log/tracef "Running Query for Dashboard %d, Card %d, Dashcard %d with options\n%s"
dashboard-id card-id dashcard-id
(u/pprint-to-str options))
;; we've already validated our parameters, so we don't need the [[qp.card]] namespace to do it again
(binding [qp.card/*allow-arbitrary-mbql-parameters* true]
(m/mapply qp.card/process-query-for-card card-id export-format options))))) |