(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.models.card :refer [Card]] [metabase.models.table :refer [Table]] [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 Table) (ms/InstanceOf 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)))) |