The Query Processor Store caches resolved Tables and Fields for the duration of a query execution. Certain middleware handles resolving things like the query's source Table and any Fields that are referenced in a query, and saves the referenced objects in the store; other middleware and driver-specific query processor implementations use functions in the store to fetch those objects as needed. For example, a driver might be converting a Field ID clause (e.g. (qp.store/field 10) ;; get Field 10 Of course, it would be entirely possible to call | (ns metabase.query-processor.store (:require [medley.core :as m] [metabase.lib.core :as lib] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.jvm :as lib.metadata.jvm] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.query-processor.error-type :as qp.error-type] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr])) |
(set! *warn-on-reflection* true) | |
(def ^:private uninitialized-store
(reify
clojure.lang.IDeref
(deref [_this]
(throw (ex-info "Error: Query Processor store is not initialized. Initialize it with qp.store/with-metadata-provider"
{}))))) | |
Dynamic var used as the QP store for a given query execution. | (def ^:private ^:dynamic *store* uninitialized-store) |
This is only for tests! When enabled, [[with-metadata-provider]] can completely replace the current metadata provider (and cache) with a new one. This is reset to false after the QP store is replaced the first time. | (def ^:dynamic *TESTS-ONLY-allow-replacing-metadata-provider* false) |
Is the QP store currently initialized? TODO -- rename this to something like | (defn initialized? [] (not (identical? *store* uninitialized-store))) |
Store a miscellaneous value in a the cache. Persists for the life of this QP invocation, including for recursive calls. | (mu/defn store-miscellaneous-value! [ks :- [:sequential :any] v] (swap! *store* assoc-in ks v)) |
Fetch a miscellaneous value from the cache. Unlike other Store functions, does not throw if value is not found. | (mu/defn miscellaneous-value
([ks]
(miscellaneous-value ks nil))
([ks :- [:sequential :any]
not-found]
(get-in @*store* ks not-found))) |
Attempt to fetch a miscellaneous value from the cache using key sequence See also | (defn cached-fn
[ks thunk]
(let [cached-value (miscellaneous-value ks ::not-found)]
(if-not (= cached-value ::not-found)
cached-value
(let [v (thunk)]
(store-miscellaneous-value! ks v)
v)))) |
Cache the value of Note that each use of ;; cache lookups of Card.dataset_query (qp.store/cached card-id (t2/select-one-fn :dataset_query Card :id card-id)) | (defmacro cached
{:style/indent 1}
[k-or-ks & body]
;; for the unique key use a gensym prefixed by the namespace to make for easier store debugging if needed
(let [ks (into [(list 'quote (gensym (str (name (ns-name *ns*)) "/misc-cache-")))] (u/one-or-many k-or-ks))]
`(cached-fn ~ks (fn [] ~@body)))) |
(mu/defn metadata-provider :- ::lib.schema.metadata/metadata-provider
"Get the [[metabase.lib.metadata.protocols/MetadataProvider]] that should be used inside the QP. "
[]
(or (miscellaneous-value [::metadata-provider])
(throw (ex-info "QP Store Metadata Provider is not initialized yet; initialize it with `qp.store/with-metadata-provider`."
{})))) | |
(mr/def ::database-id-or-metadata-providerable [:or ::lib.schema.id/database ::lib.schema.metadata/metadata-providerable]) | |
(mu/defn- ->metadata-provider :- ::lib.schema.metadata/metadata-provider
[database-id-or-metadata-providerable :- ::database-id-or-metadata-providerable]
(if (integer? database-id-or-metadata-providerable)
(lib.metadata.jvm/application-database-metadata-provider database-id-or-metadata-providerable)
database-id-or-metadata-providerable)) | |
Impl for [[with-metadata-provider]]; if there's already a provider, just make sure we're not trying to change the Database. We don't need to replace it. | (mu/defn- validate-existing-provider
[database-id-or-metadata-providerable :- ::database-id-or-metadata-providerable]
(let [old-provider (miscellaneous-value [::metadata-provider])]
(when-not (identical? old-provider database-id-or-metadata-providerable)
(let [new-database-id (if (integer? database-id-or-metadata-providerable)
database-id-or-metadata-providerable
(throw (ex-info "Cannot replace MetadataProvider with another one after it has been bound"
{:old-provider old-provider, :new-provider database-id-or-metadata-providerable})))
existing-database-id (u/the-id (lib.metadata/database old-provider))]
(when-not (= new-database-id existing-database-id)
(throw (ex-info (tru "Attempting to initialize metadata provider with new Database {0}. Queries can only reference one Database. Already referencing: {1}"
(pr-str new-database-id)
(pr-str existing-database-id))
{:existing-id existing-database-id
:new-id new-database-id
:type qp.error-type/invalid-query}))))))) |
Create a new metadata provider and save it. | (mu/defn- set-metadata-provider!
[database-id-or-metadata-providerable :- ::database-id-or-metadata-providerable]
(let [new-provider (->metadata-provider database-id-or-metadata-providerable)]
;; validate the new provider.
(try
(lib.metadata/database new-provider)
(catch Throwable e
(throw (ex-info (format "Invalid MetadataProvider, failed to return valid Database: %s" (ex-message e))
{:metadata-provider new-provider}
e))))
(store-miscellaneous-value! [::metadata-provider] new-provider))) |
Implementation for [[with-metadata-provider]]. | (mu/defn do-with-metadata-provider
[database-id-or-metadata-providerable :- ::database-id-or-metadata-providerable
thunk :- [:=> [:cat] :any]]
(cond
(or (not (initialized?))
*TESTS-ONLY-allow-replacing-metadata-provider*)
(binding [*store* (atom {})
*TESTS-ONLY-allow-replacing-metadata-provider* false]
(do-with-metadata-provider database-id-or-metadata-providerable thunk))
;; existing provider
(miscellaneous-value [::metadata-provider])
(do
(validate-existing-provider database-id-or-metadata-providerable)
(thunk))
:else
(do
(set-metadata-provider! database-id-or-metadata-providerable)
(thunk)))) |
Execute If a MetadataProvider is already bound, this is a no-op. | (defmacro with-metadata-provider
{:style/indent [:defn]}
[database-id-or-metadata-providerable & body]
`(do-with-metadata-provider ~database-id-or-metadata-providerable (^:once fn* [] ~@body))) |
DEPRECATED STUFF | |
For compatibility: convert MLv2-style metadata as returned by [[metabase.lib.metadata.protocols]]
or [[metabase.lib.metadata]] functions
(with Try to avoid using this, we would like to remove this in the near future. | (defn ->legacy-metadata
{:deprecated "0.48.0"}
[mlv2-metadata]
(let [model (case (:lib/type mlv2-metadata)
:metadata/database :model/Database
:metadata/table :model/Table
:metadata/column :model/Field)]
(-> mlv2-metadata
(dissoc :lib/type)
(update-keys u/->snake_case_en)
(vary-meta assoc :type model)
;; TODO: This is converting a freestanding field ref into legacy form. Then again, the `:field_ref` on MLv2
;; metadata is already in legacy form.
(m/update-existing :field_ref lib/->legacy-MBQL)))) |