Middleware that fetches tables that will need to be joined, referred to by | (ns metabase.query-processor.middleware.resolve-joins (:refer-clojure :exclude [alias]) (:require [medley.core :as m] [metabase.legacy-mbql.schema :as mbql.s] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.metadata :as lib.metadata] [metabase.lib.util.match :as lib.util.match] [metabase.query-processor.middleware.add-implicit-clauses :as qp.add-implicit-clauses] [metabase.query-processor.store :as qp.store] [metabase.query-processor.util.add-alias-info :as add] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu])) |
Schema for a non-empty sequence of Joins. Unlike [[mbql.s/Joins]], this does not enforce the constraint that all join aliases be unique; that is handled by the [[metabase.query-processor.middleware.escape-join-aliases]] middleware. | (def ^:private Joins
[:sequential {:min 1} mbql.s/Join]) |
Schema for the parts of the query we're modifying. For use in the various intermediate transformations in the middleware. | (def ^:private UnresolvedMBQLQuery
[:map
[:joins [:sequential mbql.s/Join]]
[:fields {:optional true} mbql.s/Fields]]) |
Schema for the final results of this middleware. | (def ^:private ResolvedMBQLQuery
[:and
UnresolvedMBQLQuery
[:fn
{:error/message "Valid MBQL query where `:joins` `:fields` is sequence of Fields or removed"}
(fn [{:keys [joins]}]
(every?
(fn [{:keys [fields]}]
(or
(empty? fields)
(sequential? fields)))
joins))]]) |
+----------------------------------------------------------------------------------------------------------------+ | Resolving Tables & Fields / Saving in QP Store | +----------------------------------------------------------------------------------------------------------------+ | |
(mu/defn- resolve-fields! :- :nil
[joins :- Joins]
(lib.metadata/bulk-metadata-or-throw (qp.store/metadata-provider)
:metadata/column
(lib.util.match/match joins [:field (id :guard integer?) _] id))
nil) | |
(mu/defn- resolve-tables! :- :nil
"Add Tables referenced by `:joins` to the Query Processor Store. This is only really needed for implicit joins,
because their Table references are added after `resolve-source-tables` runs."
[joins :- Joins]
(lib.metadata/bulk-metadata-or-throw (qp.store/metadata-provider)
:metadata/table
(remove nil? (map :source-table joins)))
nil) | |
+----------------------------------------------------------------------------------------------------------------+ | :Joins Transformations | +----------------------------------------------------------------------------------------------------------------+ | |
(def ^:private default-join-alias "__join") | |
(mu/defn- merge-defaults :- mbql.s/Join
[join]
(merge {:alias default-join-alias, :strategy :left-join} join)) | |
(defn- source-metadata->fields [{:keys [alias], :as join} source-metadata]
(when-not (seq source-metadata)
(throw (ex-info (tru "Cannot use :fields :all in join against source query unless it has :source-metadata.")
{:join join})))
(let [duplicate-ids (into #{}
(keep (fn [[item freq]]
(when (> freq 1)
item)))
(frequencies (map :id source-metadata)))]
(for [{field-name :name, base-type :base_type, field-id :id} source-metadata]
(if (and field-id (not (contains? duplicate-ids field-id)))
;; field-id is a unique reference, use it
[:field field-id {:join-alias alias}]
[:field field-name {:base-type base-type, :join-alias alias}])))) | |
(mu/defn- handle-all-fields :- mbql.s/Join
"Replace `:fields :all` in a join with an appropriate list of Fields."
[{:keys [source-table source-query alias fields source-metadata], :as join} :- mbql.s/Join]
(merge
join
(when (= fields :all)
{:fields (if source-query
(source-metadata->fields join source-metadata)
(for [[_ id-or-name opts] (qp.add-implicit-clauses/sorted-implicit-fields-for-table source-table)]
[:field id-or-name (assoc opts :join-alias alias)]))}))) | |
(mu/defn- resolve-references :- Joins
[joins :- Joins]
(resolve-tables! joins)
(u/prog1 (into []
(comp (map merge-defaults)
(map handle-all-fields))
joins)
(resolve-fields! <>))) | |
(declare resolve-joins-in-mbql-query-all-levels) | |
(mu/defn- resolve-join-source-queries :- Joins
[joins :- Joins]
(for [{:keys [source-query], :as join} joins]
(cond-> join
source-query resolve-joins-in-mbql-query-all-levels))) | |
+----------------------------------------------------------------------------------------------------------------+ | MBQL-Query Transformations | +----------------------------------------------------------------------------------------------------------------+ | |
Return a flattened list of all | (defn- joins->fields
[joins]
(into []
(comp (map :fields)
(filter sequential?)
cat)
joins)) |
Should we append the | (defn- should-add-join-fields?
[{breakouts :breakout, aggregations :aggregation}]
(every? empty? [aggregations breakouts])) |
(defn- append-join-fields [fields join-fields]
(into []
(comp cat
(m/distinct-by (fn [clause]
(-> clause
;; remove namespaced options and other things that are definitely irrelevant
add/normalize-clause
;; we shouldn't consider different type info to mean two Fields are different even if
;; everything else is the same. So give everything `:base-type` of `:type/*` (it will
;; complain if we remove `:base-type` entirely from fields with a string name)
(mbql.u/update-field-options (fn [opts]
(-> opts
(assoc :base-type :type/*)
(dissoc :effective-type))))))))
[fields join-fields])) | |
Add the fields from join | (defn append-join-fields-to-fields
[inner-query join-fields]
(cond-> inner-query
(seq join-fields) (update :fields append-join-fields join-fields))) |
(mu/defn- merge-joins-fields :- UnresolvedMBQLQuery
"Append the `:fields` from `:joins` into their parent level as appropriate so joined columns appear in the final
query results, and remove the `:fields` entry for all joins.
If the parent-level query has breakouts and/or aggregations, this function won't append the joins fields to the
parent level, because we should only be returning the ones from the ags and breakouts in the final results."
[{:keys [joins], :as inner-query} :- UnresolvedMBQLQuery]
(let [join-fields (when (should-add-join-fields? inner-query)
(joins->fields joins))
;; remove remaining keyword `:fields` like `:none` from joins
inner-query (update inner-query :joins (fn [joins]
(mapv (fn [{:keys [fields], :as join}]
(cond-> join
(keyword? fields) (dissoc :fields)))
joins)))]
(append-join-fields-to-fields inner-query join-fields))) | |
(mu/defn- resolve-joins-in-mbql-query :- ResolvedMBQLQuery
[query :- mbql.s/MBQLQuery]
(-> query
(update :joins (comp resolve-join-source-queries resolve-references))
merge-joins-fields)) | |
+----------------------------------------------------------------------------------------------------------------+ | Middleware & Boring Recursive Application Stuff | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- resolve-joins-in-mbql-query-all-levels
[{:keys [joins source-query], :as query}]
(cond-> query
(seq joins) resolve-joins-in-mbql-query
source-query (update :source-query resolve-joins-in-mbql-query-all-levels))) | |
Add any Tables and Fields referenced by the | (defn resolve-joins
[{inner-query :query, :as outer-query}]
(cond-> outer-query
inner-query (update :query resolve-joins-in-mbql-query-all-levels))) |