(ns metabase.query-processor.setup
(:require
[clojure.core.async :as a]
[clojure.core.async.impl.dispatch :as a.impl.dispatch]
[clojure.set :as set]
[metabase.driver :as driver]
[metabase.driver.util :as driver.u]
[metabase.lib.metadata :as lib.metadata]
[metabase.lib.metadata.protocols :as lib.metadata.protocols]
[metabase.lib.schema.id :as lib.schema.id]
[metabase.lib.schema.metadata :as lib.schema.metadata]
[metabase.lib.util :as lib.util]
[metabase.models.setting :as setting]
[metabase.query-processor.error-type :as qp.error-type]
[metabase.query-processor.pipeline :as qp.pipeline]
[metabase.query-processor.schema :as qp.schema]
[metabase.query-processor.store :as qp.store]
[metabase.util.i18n :as i18n]
[metabase.util.malli :as mu]
^{:clj-kondo/ignore [:discouraged-namespace]}
[toucan2.core :as t2])) | |
(mu/defn- query-type :- [:enum :query :native :internal :mbql/query]
[query :- ::qp.schema/query]
(or (some-> ((some-fn :lib/type :type) query) keyword)
(throw (ex-info (i18n/tru "Invalid query: missing or invalid query type (:lib/type or :type)")
{:query query, :type qp.error-type/invalid-query})))) | |
(mu/defn- source-card-id-for-pmbql-query :- [:maybe ::lib.schema.id/card] [query :- ::qp.schema/query] (-> query :stages first :source-card)) | |
(mu/defn- source-card-id-for-legacy-query :- [:maybe ::lib.schema.id/card]
[query :- ::qp.schema/query]
(let [inner-query (:query query)
deepest-inner-query (loop [inner-query inner-query]
(let [source-query (:source-query inner-query)]
(if source-query
(recur source-query)
inner-query)))
source-table (:source-table deepest-inner-query)]
(lib.util/legacy-string-table-id->card-id source-table))) | |
(defn- bootstrap-metadatas [metadata-type ids]
(when (and (seq ids)
(= metadata-type :metadata/card))
(t2/select-fn-vec
(fn [card]
{:lib/type :metadata/card
:id (:id card)
:name (format "Card #%d" (:id card))
:database-id (:database_id card)})
[:model/Card :id :database_id]
:id [:in (set ids)]))) | |
(deftype ^:private BootstrapMetadataProvider []
lib.metadata.protocols/MetadataProvider
(database [_this]
nil)
(metadatas [_this metadata-type ids]
(bootstrap-metadatas metadata-type ids))
(tables [_this]
nil)
(metadatas-for-table [_this _metadata-type _table-id]
nil)
(metadatas-for-card [_this _metadata-type _card-id]
nil)
(setting [_this _setting-key]
nil)) | |
(mu/defn- bootstrap-metadata-provider :- ::lib.schema.metadata/metadata-provider
"A super-basic metadata provider used only for resolving the database ID associated with a source Card, only for
queries that use the [[lib.schema.id/saved-questions-virtual-database-id]] e.g.
{:database -1337, :type :query, :query {:source-table \"card__1\"}}
Once the *actual* Database ID is resolved, we will create a
real [[metabase.lib.metadata.jvm/application-database-metadata-provider]]. (The App DB provider needs to be
initialized with an actual Database ID)."
[]
(if (qp.store/initialized?)
(qp.store/metadata-provider)
(->BootstrapMetadataProvider))) | |
(mu/defn- resolve-database-id-for-source-card :- ::lib.schema.id/database
[source-card-id :- ::lib.schema.id/card]
(let [card (or (lib.metadata.protocols/card (bootstrap-metadata-provider) source-card-id)
(throw (ex-info (i18n/tru "Card {0} does not exist." source-card-id)
{:card-id source-card-id, :type qp.error-type/invalid-query, :status-code 404})))]
(:database-id card))) | |
(mu/defn- source-card-id :- ::lib.schema.id/card
[query :- ::qp.schema/query]
(case (query-type query)
:mbql/query
(source-card-id-for-pmbql-query query)
(:query :native)
(source-card-id-for-legacy-query query)
#_else
(throw (ex-info (i18n/tru "Invalid query: cannot use the Saved Questions Virtual Database ID unless query has a source Card")
{:query query, :type qp.error-type/invalid-query})))) | |
(mu/defn- resolve-database-id :- [:maybe ::lib.schema.id/database]
[query :- ::qp.schema/query]
(when-not (= (query-type query) :internal)
(let [database-id (:database query)]
(cond
(pos-int? database-id)
database-id
(= database-id lib.schema.id/saved-questions-virtual-database-id)
(resolve-database-id-for-source-card (source-card-id query))
:else
(throw (ex-info (i18n/tru "Invalid query: missing or invalid Database ID (:database)")
{:query query, :type qp.error-type/invalid-query})))))) | |
(mu/defn- do-with-resolved-database :- fn?
[f :- [:=> [:cat ::qp.schema/query] :any]]
(mu/fn
[query :- ::qp.schema/query]
(let [query (set/rename-keys query {"database" :database})
database-id (resolve-database-id query)
query (cond-> query
database-id (assoc :database database-id))]
(f query)))) | |
(mu/defn- maybe-attach-metadata-provider-to-query :- ::qp.schema/query
[query :- ::qp.schema/query]
(cond-> query
(= (:lib/type query) :mbql/query) (assoc :lib/metadata (qp.store/metadata-provider)))) | |
(mu/defn- do-with-metadata-provider :- fn?
[f :- [:=> [:cat ::qp.schema/query] :any]]
(fn [query]
(cond
(qp.store/initialized?)
(f (maybe-attach-metadata-provider-to-query query))
(lib.metadata.protocols/metadata-providerable? query)
(qp.store/with-metadata-provider (lib.metadata/->metadata-provider query)
(f query))
(= (query-type query) :internal)
(f query)
:else
(qp.store/with-metadata-provider (:database query)
(f (maybe-attach-metadata-provider-to-query query)))))) | |
(mu/defn- do-with-driver :- fn?
[f :- [:=> [:cat ::qp.schema/query] :any]]
(fn [query]
(cond
driver/*driver*
(f query)
(= (query-type query) :internal)
(f query)
:else
(let [driver (driver.u/database->driver (:database query))]
(driver/with-driver driver
(f query)))))) | |
(mu/defn- do-with-database-local-settings :- fn?
[f :- [:=> [:cat ::qp.schema/query] :any]]
(fn [query]
(cond
setting/*database-local-values*
(f query)
(= (query-type query) :internal)
(f query)
:else
(let [{:keys [settings]} (lib.metadata/database (qp.store/metadata-provider))]
(binding [setting/*database-local-values* (or settings {})]
(f query)))))) | |
(mu/defn- do-with-canceled-chan :- fn?
[f :- [:=> [:cat ::qp.schema/query] :any]]
(fn [query]
(if qp.pipeline/*canceled-chan*
(f query)
(binding [qp.pipeline/*canceled-chan* (a/promise-chan)]
(f query))))) | |
Setup middleware has the signature (middleware f) => f Where f has the signature (f query) i.e. (middleware (f query)) => (f query) | (def ^:private setup-middleware [#'do-with-canceled-chan #'do-with-database-local-settings #'do-with-driver #'do-with-metadata-provider #'do-with-resolved-database]) |
↑↑↑ SETUP MIDDLEWARE ↑↑↑ happens from BOTTOM to TOP e.g. [[do-with-resolved-database]] is the first to do its thing | |
This is here so we can skip calling the setup middleware if it's already done. Not super important, since the setup middleware should all no-op, but it keeps the stacktraces tidier so we do not have a bunch of calls that don't do anything in them. | (def ^:private ^:dynamic *has-setup* false) |
Impl for [[with-qp-setup]]. | (mu/defn do-with-qp-setup
[query :- ::qp.schema/query
f :- [:=> [:cat ::qp.schema/query] :any]]
;; TODO -- think about whether we should pre-compile this middleware
(when (a.impl.dispatch/in-dispatch-thread?)
(throw (ex-info "QP calls are not allowed inside core.async dispatch pool threads."
{:type qp.error-type/qp})))
(if *has-setup*
(f query)
(let [f (reduce
(fn [f middleware]
(middleware f))
f
setup-middleware)]
(binding [*has-setup* true]
(f query))))) |
Execute This should be used at the highest level possible for all various QP entrypoints that can be called independently, e.g. [[metabase.query-processor/process-query]] or [[metabase.query-processor.preprocess/preprocess]]. This is a no-op if these things are already bound, so duplicate calls won't negatively affect things. (qp.setup/with-qp-setup [query query] ...) | (defmacro with-qp-setup
[[query-binding query] & body]
`(do-with-qp-setup
~query
(^:once fn* [~query-binding]
~@body))) |