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))) |