Implementation(s) of [[metabase.lib.metadata.protocols/MetadataProvider]] only for the JVM. | (ns metabase.lib.metadata.jvm
(:require
[clojure.core.cache :as cache]
[clojure.core.cache.wrapped :as cache.wrapped]
[clojure.string :as str]
[metabase.lib.metadata.cached-provider :as lib.metadata.cached-provider]
[metabase.lib.metadata.invocation-tracker :as lib.metadata.invocation-tracker]
[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.models.interface :as mi]
[metabase.models.serialization :as serdes]
[metabase.models.setting :as setting]
[metabase.util :as u]
[metabase.util.malli :as mu]
[metabase.util.memoize :as u.memo]
[metabase.util.snake-hating-map :as u.snake-hating-map]
[methodical.core :as methodical]
[potemkin :as p]
[pretty.core :as pretty]
^{:clj-kondo/ignore [:discouraged-namespace]}
[toucan2.core :as t2]
[toucan2.model :as t2.model]
[toucan2.pipeline :as t2.pipeline]
[toucan2.query :as t2.query])) |
(set! *warn-on-reflection* true) | |
(defn- qualified-key? [k]
(or (qualified-keyword? k)
(str/includes? k "."))) | |
Calculating the kebab-case version of a key every time is pretty slow (even with the LRU caching [[u/->kebab-case-en]] has), since the keys here are static and finite we can just memoize them forever and get a nice performance boost. | (def ^{:private true
:arglists '([k])} memoized-kebab-key
(u.memo/fast-memo u/->kebab-case-en)) |
Convert a (presumably) Toucan 2 instance of an application database model with | (defn instance->metadata
[instance metadata-type]
(-> instance
(update-keys memoized-kebab-key)
(assoc :lib/type metadata-type)
u.snake-hating-map/snake-hating-map)) |
Database | |
(derive :metadata/database :model/Database) | |
(methodical/defmethod t2.model/resolve-model :metadata/database [model] (t2/resolve-model :model/Database) ; for side-effects model) | |
(methodical/defmethod t2.pipeline/build [#_query-type :toucan.query-type/select.*
#_model :metadata/database
#_resolved-query clojure.lang.IPersistentMap]
[query-type model parsed-args honeysql]
(merge (next-method query-type model parsed-args honeysql)
{:select [:id :engine :name :dbms_version :settings :is_audit :details :timezone]})) | |
(t2/define-after-select :metadata/database
[database]
;; ignore encrypted details that we cannot decrypt, because that breaks schema
;; validation
(let [database (instance->metadata database :metadata/database)]
(cond-> database
(not (map? (:details database))) (dissoc :details)))) | |
Table | |
(derive :metadata/table :model/Table) | |
(methodical/defmethod t2.model/resolve-model :metadata/table [model] (t2/resolve-model :model/Table) model) | |
(methodical/defmethod t2.pipeline/build [#_query-type :toucan.query-type/select.*
#_model :metadata/table
#_resolved-query clojure.lang.IPersistentMap]
[query-type model parsed-args honeysql]
(merge (next-method query-type model parsed-args honeysql)
{:select [:id :db_id :name :display_name :schema :active :visibility_type]})) | |
(t2/define-after-select :metadata/table [table] (instance->metadata table :metadata/table)) | |
Field | |
(derive :metadata/column :model/Field) | |
(methodical/defmethod t2.model/resolve-model :metadata/column [model] (t2/resolve-model :model/Dimension) ; for side-effects (t2/resolve-model :model/Field) (t2/resolve-model :model/FieldValues) (t2/resolve-model :model/Table) model) | |
(methodical/defmethod t2.model/model->namespace :metadata/column
":metadata/column joins Dimension and FieldValues by default; namespace their columns so we can distinguish them from
the columns coming back from Field."
[_model]
{:model/Dimension "dimension"
:model/FieldValues "values"}) | |
(methodical/defmethod t2.query/apply-kv-arg [#_model :metadata/column
#_resolved-query clojure.lang.IPersistentMap
#_k :default]
"Qualify unqualified kv-args when fetching a `:metadata/column`."
[model honeysql k v]
(let [k (if (not (qualified-key? k))
(keyword "field" (name k))
k)]
(next-method model honeysql k v))) | |
(methodical/defmethod t2.pipeline/build [#_query-type :toucan.query-type/select.*
#_model :metadata/column
#_resolved-query clojure.lang.IPersistentMap]
[query-type model parsed-args honeysql]
(merge
(next-method query-type model parsed-args honeysql)
{:select [:field/active
:field/base_type
:field/coercion_strategy
:field/database_type
:field/description
:field/display_name
:field/effective_type
:field/fingerprint
:field/fk_target_field_id
:field/id
:field/name
:field/nfc_path
:field/parent_id
:field/position
:field/semantic_type
:field/settings
:field/table_id
:field/visibility_type
:dimension/human_readable_field_id
:dimension/id
:dimension/name
:dimension/type
:values/human_readable_values
:values/values]
:from [[(t2/table-name :model/Field) :field]]
:left-join [[(t2/table-name :model/Table) :table]
[:= :field/table_id :table/id]
[(t2/table-name :model/Dimension) :dimension]
[:and
[:= :dimension/field_id :field/id]
[:inline [:in :dimension/type ["external" "internal"]]]]
[(t2/table-name :model/FieldValues) :values]
[:and
[:= :values/field_id :field/id]
[:= :values/type [:inline "full"]]]]})) | |
(t2/define-after-select :metadata/column
[field]
(let [entity-id (serdes/backfill-entity-id field)
field (instance->metadata field :metadata/column)
dimension-type (some-> (:dimension/type field) keyword)]
(merge
(dissoc field
:table
:dimension/human-readable-field-id :dimension/id :dimension/name :dimension/type
:values/human-readable-values :values/values)
{:ident entity-id}
(when (and (= dimension-type :external)
(:dimension/human-readable-field-id field))
{:lib/external-remap {:lib/type :metadata.column.remapping/external
:id (:dimension/id field)
:name (:dimension/name field)
:field-id (:dimension/human-readable-field-id field)}})
(when (and (= dimension-type :internal)
(:values/values field)
(:values/human-readable-values field))
{:lib/internal-remap {:lib/type :metadata.column.remapping/internal
:id (:dimension/id field)
:name (:dimension/name field)
:values (mi/json-out-with-keywordization
(:values/values field))
:human-readable-values (mi/json-out-without-keywordization
(:values/human-readable-values field))}})))) | |
Card | |
(derive :metadata/card :model/Card) | |
(methodical/defmethod t2.model/resolve-model :metadata/card [model] (t2/resolve-model :model/Card) (t2/resolve-model :model/PersistedInfo) model) | |
(methodical/defmethod t2.model/model->namespace :metadata/card
[_model]
{:model/PersistedInfo "persisted"}) | |
(methodical/defmethod t2.query/apply-kv-arg [#_model :metadata/card
#_resolved-query clojure.lang.IPersistentMap
#_k :default]
[model honeysql k v]
()
(let [k (if (not (qualified-key? k))
(keyword "card" (name k))
k)]
(next-method model honeysql k v))) | |
(methodical/defmethod t2.pipeline/build [#_query-type :toucan.query-type/select.*
#_model :metadata/card
#_resolved-query clojure.lang.IPersistentMap]
[query-type model parsed-args honeysql]
(merge
(next-method query-type model parsed-args honeysql)
{:select [:card/collection_id
:card/created_at ; Needed for backfilling :entity_id on demand; see [[metabase.models.card]].
:card/database_id
:card/dataset_query
:card/id
:card/entity_id
:card/name
:card/result_metadata
:card/table_id
:card/type
:card/visualization_settings
:persisted/active
:persisted/state
:persisted/definition
:persisted/query_hash
:persisted/table_name]
:from [[(t2/table-name :model/Card) :card]]
:left-join [[(t2/table-name :model/PersistedInfo) :persisted]
[:= :persisted/card_id :card/id]]})) | |
(defn- parse-persisted-info-definition [x] ((get-in (t2/transforms :model/PersistedInfo) [:definition :out] identity) x)) | |
(t2/define-after-select :metadata/card
[card]
(let [card (instance->metadata card :metadata/card)]
(merge
(dissoc card :persisted/active :persisted/state :persisted/definition :persisted/query-hash :persisted/table-name)
(when (:persisted/definition card)
{:lib/persisted-info {:active (:persisted/active card)
:state (:persisted/state card)
:definition (parse-persisted-info-definition (:persisted/definition card))
:query-hash (:persisted/query-hash card)
:table-name (:persisted/table-name card)}})))) | |
Metric | |
(derive :metadata/metric :model/Card) | |
(t2/define-after-select :metadata/metric [metric] (instance->metadata metric :metadata/metric)) | |
Segment | |
(derive :metadata/segment :model/Segment) | |
(methodical/defmethod t2.model/resolve-model :metadata/segment [model] (t2.model/resolve-model :model/Segment) (t2.model/resolve-model :model/Table) model) | |
(methodical/defmethod t2.query/apply-kv-arg [#_model :metadata/segment
#_resolved-query clojure.lang.IPersistentMap
#_k :default]
[model honeysql k v]
(let [k (if (not (qualified-key? k))
(keyword "segment" (name k))
k)]
(next-method model honeysql k v))) | |
(methodical/defmethod t2.pipeline/build [#_query-type :toucan.query-type/select.*
#_model :metadata/segment
#_resolved-query clojure.lang.IPersistentMap]
[query-type model parsed-args honeysql]
(merge
(next-method query-type model parsed-args honeysql)
{:select [:segment/id
:segment/table_id
:segment/name
:segment/description
:segment/archived
:segment/definition]
:from [[(t2/table-name :model/Segment) :segment]]
:left-join [[(t2/table-name :model/Table) :table]
[:= :segment/table_id :table/id]]})) | |
(t2/define-after-select :metadata/segment [segment] (instance->metadata segment :metadata/segment)) | |
MetadataProvider | |
(defn- database [database-id]
(when-not database-id
(throw (ex-info (format "Cannot use %s with %s with a nil Database ID"
`lib.metadata.protocols/database
`UncachedApplicationDatabaseMetadataProvider)
{})))
(t2/select-one :metadata/database database-id)) | |
(defn- metadatas [database-id metadata-type ids]
(let [database-id-key (case metadata-type
:metadata/table :db_id
:metadata/card :database_id
:table/db_id)]
(when (seq ids)
(t2/select metadata-type
database-id-key database-id
:id [:in (set ids)])))) | |
(defn- tables [database-id]
(t2/select :metadata/table
{:where [:and
[:= :db_id database-id]
[:= :active true]
[:or
[:is :visibility_type nil]
[:not-in :visibility_type #{"hidden" "technical" "cruft"}]]]})) | |
(defn- metadatas-for-table [metadata-type table-id]
(case metadata-type
:metadata/column
(t2/select :metadata/column
:table_id table-id
:active true
:visibility_type [:not-in #{"sensitive" "retired"}])
:metadata/metric
(t2/select :metadata/metric :table_id table-id, :source_card_id [:= nil], :type :metric, :archived false)
:metadata/segment
(t2/select :metadata/segment :table_id table-id, :archived false))) | |
(defn- metadatas-for-card [metadata-type card-id]
(case metadata-type
:metadata/metric
(t2/select :metadata/metric :source_card_id card-id, :type :metric, :archived false))) | |
(p/deftype+ UncachedApplicationDatabaseMetadataProvider [database-id]
lib.metadata.protocols/MetadataProvider
(database [_this]
(database database-id))
(metadatas [_this metadata-type ids]
(metadatas database-id metadata-type ids))
(tables [_this]
(tables database-id))
(metadatas-for-table [_this metadata-type table-id]
(metadatas-for-table metadata-type table-id))
(metadatas-for-card [_this metadata-type card-id]
(metadatas-for-card metadata-type card-id))
(setting [_this setting-name]
(setting/get setting-name))
pretty/PrettyPrintable
(pretty [_this]
(list `->UncachedApplicationDatabaseMetadataProvider database-id))
Object
(equals [_this another]
(and (instance? UncachedApplicationDatabaseMetadataProvider another)
(= database-id (.database-id ^UncachedApplicationDatabaseMetadataProvider another))))) | |
Inner function that constructs a new Call [[application-database-metadata-provider]] instead, which wraps this inner function with optional, dynamically
scoped caching, to allow reuse of | (defn- application-database-metadata-provider-factory
[database-id]
(-> (->UncachedApplicationDatabaseMetadataProvider database-id)
lib.metadata.cached-provider/cached-metadata-provider
lib.metadata.invocation-tracker/invocation-tracker-provider)) |
Bind this to a This is useful for an API request, or group fo API requests like a dashboard load, to reduce appdb traffic. | (def ^:dynamic *metadata-provider-cache* nil) |
Wrapper to create a [[metadata-provider-cache]] for the duration of the If there is already a [[metadata-provider-cache]], this leaves it in place. | (defmacro with-metadata-provider-cache
[& body]
`(binding [*metadata-provider-cache* (or *metadata-provider-cache*
(atom (cache/basic-cache-factory {})))]
~@body)) |
(mu/defn application-database-metadata-provider :- ::lib.schema.metadata/metadata-provider
"An implementation of [[metabase.lib.metadata.protocols/MetadataProvider]] for the application database.
Supports caching over a dynamic scope (eg. an API request or group of API requests like a dashboard load) via
[[*metadata-provider-cache*]]. Outside such a scope, this creates a new `MetadataProvider` for each call.
On the returned `MetadataProvider`, all operations are cached. You can use the bulk operations to pre-warm the cache
if you need to."
[database-id :- ::lib.schema.id/database]
(if-let [cache-atom *metadata-provider-cache*]
(cache.wrapped/lookup-or-miss cache-atom database-id application-database-metadata-provider-factory)
(application-database-metadata-provider-factory database-id))) | |