(ns metabase.xrays.domain-entities.core (:require [clojure.string :as str] [medley.core :as m] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.util.match :as lib.util.match] [metabase.util :as u] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [metabase.xrays.domain-entities.specs :refer [domain-entity-specs MBQL]] [toucan2.core :as t2])) | |
Return the most specific type of a given field. | (def ^:private ^{:arglists '([field])} field-type
(some-fn :semantic_type :base_type)) |
A reference to a | (def SourceName :string) |
(def ^:private DimensionReference :string) | |
Mapping from dimension name to the corresponding instantiated MBQL snippet | (def DimensionBindings [:map-of DimensionReference MBQL]) |
A source for a card. Can be either a table or another card. | (def SourceEntity [:or (ms/InstanceOf :model/Table) (ms/InstanceOf :model/Card)]) |
Top-level lexical context mapping source names to their corresponding entity and constituent dimensions. See also
| (def Bindings
[:map-of
SourceName
[:map
[:dimensions DimensionBindings]
[:entity {:optional true} SourceEntity]]]) |
(mu/defn- get-dimension-binding :- MBQL
[bindings :- Bindings
source :- SourceName
dimension-reference :- DimensionReference]
(let [[table-or-dimension maybe-dimension] (str/split dimension-reference #"\.")]
(if maybe-dimension
(let [field-clause (get-in bindings [table-or-dimension :dimensions maybe-dimension])]
(cond-> field-clause
(not= source table-or-dimension) (mbql.u/assoc-field-options :join-alias table-or-dimension)))
(get-in bindings [source :dimensions table-or-dimension])))) | |
Instantiate all dimension reference in given (nested) structure | (mu/defn resolve-dimension-clauses
[bindings :- Bindings
source :- SourceName
obj]
(lib.util.match/replace obj
[:dimension dimension] (->> dimension
(get-dimension-binding bindings source)
(resolve-dimension-clauses bindings source)))) |
(mu/defn mbql-reference :- MBQL
"Return MBQL clause for a given field-like object."
[{:keys [id name base_type]}]
(if id
[:field id nil]
[:field name {:base-type base_type}])) | |
(defn- has-attribute?
[entity {:keys [field _domain_entity _has_many]}]
(cond
field (some (fn [col]
(when (or (isa? (field-type col) field)
(= (:name col) (name field)))
col))
((some-fn :fields :result_metadata) entity)))) | |
Does source entity satisfies requierments of given spec? | (defn satisfies-requierments?
[entity {:keys [required_attributes]}]
(every? (partial has-attribute? entity) required_attributes)) |
(defn- best-match
[candidates]
(->> candidates
(sort-by (juxt (comp count ancestors :type) (comp count :required_attributes)))
last)) | |
(defn- instantiate-dimensions
[bindings source entities]
(into (empty entities) ; this way we don't care if we're dealing with a map or a vec
(for [entity entities
:when (every? (get-in bindings [source :dimensions])
(lib.util.match/match entity [:dimension dimension] dimension))]
(resolve-dimension-clauses bindings source entity)))) | |
(defn- instantiate-domain-entity
[table {:keys [name description metrics segments breakout_dimensions type]}]
(let [dimensions (into {} (for [field (:fields table)]
[(-> field field-type clojure.core/name) field]))
bindings {name {:entity table
:dimensions (m/map-vals mbql-reference dimensions)}}]
{:metrics (instantiate-dimensions bindings name metrics)
:segments (instantiate-dimensions bindings name segments)
:breakout_dimensions (instantiate-dimensions bindings name breakout_dimensions)
:dimensions dimensions
:type type
:description description
:source_table (u/the-id table)
:name name})) | |
Find the best fitting domain entity for given table. | (defn domain-entity-for-table
[table]
(let [table (t2/hydrate table :fields)]
(some->> @domain-entity-specs
vals
(filter (partial satisfies-requierments? table))
best-match
(instantiate-domain-entity table)))) |
Fake hydration function. | (defn with-domain-entity
[tables]
(for [table tables]
(assoc table :domain_entity (domain-entity-for-table table)))) |