Walks query and generates appropriate aliases for every selected column; and adds extra keys to the corresponding MBQL clauses with this information. Deduplicates aliases and calls [[metabase.driver/escape-alias]] with the generated aliases. Adds information about the aliases in source queries and joins that correspond to columns in the parent level. This code is currently opt-in, and is currently only used by SQL drivers ([[metabase.driver.sql.query-processor]] manually calls [[add-alias-info]] inside of [[metabase.driver.sql.query-processor/mbql->native]] and [[metabase.driver.mongo.query-processor/mbql->native]]) but at some point in the future this may become general QP middleware that can't be opted out of. [[add-alias-info]] adds some or all of the following keys to every `::source-table`String name, integer Table ID, the keyword
TODO -- consider allowing vectors of multiple qualifiers e.g. `::source-alias`String name to use to refer to this clause during compilation. `::desired-alias`If this clause is 'selected' (i.e., appears in `::position`If this clause is 'selected', this is the position the clause will appear in the results (i.e. the corresponding column index). | (ns metabase.query-processor.util.add-alias-info (:require [clojure.walk :as walk] [medley.core :as m] [metabase.driver :as driver] [metabase.driver.sql.query-processor.deprecated :as sql.qp.deprecated] [metabase.legacy-mbql.schema :as mbql.s] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.metadata :as lib.metadata] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.util :as lib.util] [metabase.lib.util.match :as lib.util.match] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.store :as qp.store] [metabase.util.i18n :refer [trs tru]] [metabase.util.malli :as mu])) |
Generate a field alias by applying | (defn- prefix-field-alias [prefix field-alias] (driver/escape-alias driver/*driver* (str prefix "__" field-alias))) |
Creates a function with the signature (unique-alias position original-alias) To return a uniquified version of | (defn- make-unique-alias-fn
[]
(let [unique-name-fn (lib.util/unique-name-generator)]
(fn unique-alias-fn [position original-alias]
(assert (string? original-alias)
(format "unique-alias-fn expected string, got: %s" (pr-str original-alias)))
(unique-name-fn position (driver/escape-alias driver/*driver* original-alias))))) |
TODO -- this should probably limit the resulting alias, and suffix a short hash as well if it gets too long. See also [[unique-alias-fn]] below. | |
(defn- remove-namespaced-options [options]
(when options
(not-empty (into {}
(remove (fn [[k _]]
(when (keyword? k)
(namespace k))))
options)))) | |
Normalize a | (defn normalize-clause
[clause]
(lib.util.match/match-one clause
;; optimization: don't need to rewrite a `:field` clause without any options
[:field _ nil]
&match
[:field id-or-name opts]
;; this doesn't use [[mbql.u/update-field-options]] because this gets called a lot and the overhead actually adds up
;; a bit
[:field id-or-name (remove-namespaced-options (cond-> (dissoc opts :source-field :effective-type)
(integer? id-or-name) (dissoc :base-type)))]
;; for `:expression` and `:aggregation` references, remove the options map if they are empty.
[:expression expression-name opts]
(if-let [opts (remove-namespaced-options opts)]
[:expression expression-name opts]
[:expression expression-name])
[:aggregation index opts]
(if-let [opts (remove-namespaced-options opts)]
[:aggregation index opts]
[:aggregation index])
_
&match)) |
Get all the clauses that are returned by this level of the query as a map of normalized-clause -> index of that column in the results. | (defn- selected-clauses
[{:keys [fields breakout aggregation], :as query}]
;; this is cached for the duration of the QP run because it's a little expensive to calculate and caching this speeds
;; up this namespace A LOT
(qp.store/cached (select-keys query [:fields :breakout :aggregation])
(into
{}
(comp cat
(map-indexed
(fn [i clause]
[(normalize-clause clause) i])))
[breakout
(map-indexed
(fn [i ag]
(lib.util.match/replace ag
;; :offset is a special case since it doesn't NEED to get wrapped in aggregation options.
[:offset _opts _expr _n]
[:aggregation i]
[:aggregation-options wrapped opts]
[:aggregation i]
;; aggregation clause should be preprocessed into an `:aggregation-options` clause by now.
_
(throw (ex-info (tru "Expected :aggregation-options clause, got {0}" (pr-str ag))
{:type qp.error-type/qp, :clause ag}))))
aggregation)
fields]))) |
Get the position (i.e., column index) | (defn- clause->position [inner-query clause] ((selected-clauses inner-query) (normalize-clause clause))) |
(defn- this-level-join-aliases [{:keys [joins]}]
(into #{} (map :alias) joins)) | |
(defn- field-is-from-join-in-this-level? [inner-query [_field _id-or-name {:keys [join-alias]}]]
(when join-alias
((this-level-join-aliases inner-query) join-alias))) | |
(mu/defn- field-instance :- [:maybe ::lib.schema.metadata/column]
[[_ id-or-name :as _field-clause] :- mbql.s/field]
(when (integer? id-or-name)
(lib.metadata/field (qp.store/metadata-provider) id-or-name))) | |
(defn- field-table-id [field-clause] (:table-id (field-instance field-clause))) | |
(mu/defn- field-source-table-alias :- [:or
::lib.schema.common/non-blank-string
::lib.schema.id/table
[:= ::source]]
"Determine the appropriate `::source-table` alias for a `field-clause`."
[{:keys [source-table source-query], :as inner-query} [_ _id-or-name {:keys [join-alias]}, :as field-clause]]
(let [table-id (field-table-id field-clause)
join-is-this-level? (field-is-from-join-in-this-level? inner-query field-clause)]
(cond
join-is-this-level? join-alias
(and table-id (= table-id source-table)) table-id
source-query ::source
:else
(throw (ex-info (trs "Cannot determine the source table or query for Field clause {0}" (pr-str field-clause))
{:type qp.error-type/invalid-query
:clause field-clause
:query inner-query}))))) | |
(defn- exports [query]
(into #{} (lib.util.match/match (dissoc query :source-query :source-metadata :joins)
[(_ :guard #{:field :expression :aggregation-options}) _ (_ :guard (every-pred map? ::position))]))) | |
(defn- join-with-alias [{:keys [joins]} join-alias]
(some (fn [join]
(when (= (:alias join) join-alias)
join))
joins)) | |
(defn- fuzzify [clause] (mbql.u/update-field-options clause dissoc :temporal-unit :binning)) | |
(defn- field-signature [field-clause] [(second field-clause) (get-in field-clause [2 :join-alias])]) | |
(defn- field-name-match [field-name all-exports source-metadata field-exports]
;; First, look for Expressions or fields from the source query stage whose `::desired-alias` matches the
;; name we're searching for.
(or (m/find-first (fn [[tag _id-or-name {::keys [desired-alias], :as _opts} :as _ref]]
(when (#{:expression :field} tag)
(= desired-alias field-name)))
all-exports)
;; Expressions by exact name.
(m/find-first (fn [[_ expression-name :as _expression-clause]]
(= expression-name field-name))
(filter (partial mbql.u/is-clause? :expression) all-exports))
;; aggregation clauses from the previous stage based on their `::desired-alias`. If THAT doesn't work,
;; then try to match based on their `::source-alias` (not 100% sure why we're checking `::source-alias` at
;; all TBH -- Cam)
(when-let [ag-clauses (seq (filter (partial mbql.u/is-clause? :aggregation-options) all-exports))]
(some (fn [k]
(m/find-first (fn [[_tag _ag-clause opts :as _aggregation-options-clause]]
(= (get opts k) field-name))
ag-clauses))
[::desired-alias ::source-alias]))
;; look for a field referenced by the name in source-metadata
(when-let [column (m/find-first #(= (:name %) field-name) source-metadata)]
(let [signature (field-signature (:field_ref column))]
(or ;; First try to match with the join alias.
(m/find-first #(= (field-signature %) signature) field-exports)
;; Then just the names, but if the match is ambiguous, warn and return nil.
(let [matches (filter #(= (second %) field-name) field-exports)]
(when (= (count matches) 1)
(first matches)))))))) | |
(defn- matching-field-in-source-query*
[source-query source-metadata field-clause & {:keys [normalize-fn]
:or {normalize-fn normalize-clause}}]
(let [normalized (normalize-fn field-clause)
all-exports (exports source-query)
field-exports (filter (partial mbql.u/is-clause? :field)
all-exports)]
;; first look for an EXACT match in the `exports`
(or (m/find-first (fn [a-clause]
(= (normalize-fn a-clause) normalized))
field-exports)
;; if there is no EXACT match, attempt a 'fuzzy' match by disregarding the `:temporal-unit` and `:binning`
(let [fuzzy-normalized (fuzzify normalized)]
(m/find-first (fn [a-clause]
(= (fuzzify (normalize-fn a-clause)) fuzzy-normalized))
field-exports))
;; if still no match try looking based for a matching Field based on ID.
(let [[_field id-or-name _opts] field-clause]
(when (integer? id-or-name)
(or (m/find-first (fn [[_field an-id-or-name _opts]]
(= an-id-or-name id-or-name))
field-exports)
;; look for a field referenced by the ID in source-metadata
(when-let [column (m/find-first #(= (:id %) id-or-name) source-metadata)]
(let [signature (field-signature (:field_ref column))]
(m/find-first #(= (field-signature %) signature) field-exports))))))
;; otherwise if this is a nominal field literal ref then look for matches based on the string name used
(when-let [field-names (let [[_ id-or-name] field-clause]
(when (string? id-or-name)
[id-or-name (some-> driver/*driver* (driver/escape-alias id-or-name))]))]
(some #(field-name-match % all-exports source-metadata field-exports) field-names))))) | |
If | (defn- matching-field-in-join-at-this-level
[inner-query [_ _ {:keys [join-alias]} :as field-clause]]
(when join-alias
(let [{:keys [source-query source-metadata]} (join-with-alias inner-query join-alias)]
(when source-query
(matching-field-in-source-query*
source-query
source-metadata
field-clause
:normalize-fn #(mbql.u/update-field-options (normalize-clause %) dissoc :join-alias)))))) |
If | (defn- field-alias-in-join-at-this-level
[inner-query field-clause]
(when-let [[_ _ {::keys [desired-alias]}] (matching-field-in-join-at-this-level inner-query field-clause)]
desired-alias)) |
(defn- matching-field-in-source-query
[{:keys [source-query source-metadata], :as inner-query} field-clause]
(when (and source-query
(= (field-source-table-alias inner-query field-clause) ::source))
(matching-field-in-source-query* source-query source-metadata field-clause))) | |
(defn- field-alias-in-source-query
[inner-query field-clause]
(when-let [[_tag _id-or-name {::keys [desired-alias]}] (matching-field-in-source-query inner-query field-clause)]
desired-alias)) | |
Generate a reference for the field instance DEPRECATED: Implement [[field-reference-mlv2]] instead, which accepts a | (defmulti ^String field-reference
{:added "0.46.0", :arglists '([driver field-inst]), :deprecated "0.48.0"}
driver/dispatch-on-initialized-driver
:hierarchy #'driver/hierarchy) |
Generate a reference for the field instance | (defmulti ^String field-reference-mlv2
{:added "0.48.0", :arglists '([driver field-inst])}
driver/dispatch-on-initialized-driver
:hierarchy #'driver/hierarchy) |
(mu/defmethod field-reference-mlv2 ::driver/driver
[driver :- :keyword
field :- ::lib.schema.metadata/column]
#_{:clj-kondo/ignore [:deprecated-var]}
(if (get-method field-reference driver)
(do
(sql.qp.deprecated/log-deprecation-warning
driver
`field-reference
"0.48.0")
(field-reference driver
#_{:clj-kondo/ignore [:deprecated-var]}
(qp.store/->legacy-metadata field)))
(:name field))) | |
Nested field components path for field, so drivers can use in identifiers. | (defn- field-nfc-path [field-clause] (some-> field-clause field-instance :nfc-path not-empty vec)) |
JSON extraction fields need to be named with their outer | (defn- field-requires-original-field-name [field-clause] (boolean (field-nfc-path field-clause))) |
Actual name of a | (defn- field-name
[_inner-query [_ id-or-name :as field-clause]]
(or (some->> field-clause
field-instance
(field-reference-mlv2 driver/*driver*))
(when (string? id-or-name)
id-or-name))) |
Calculate extra stuff about | (defn- expensive-field-info
[inner-query field-clause]
(merge
{:field-name (field-name inner-query field-clause)
:override-alias? (field-requires-original-field-name field-clause)
:join-is-this-level? (field-is-from-join-in-this-level? inner-query field-clause)
:alias-from-join (field-alias-in-join-at-this-level inner-query field-clause)
:alias-from-source-query (field-alias-in-source-query inner-query field-clause)}
(when-let [nfc-path (field-nfc-path field-clause)]
{:nfc-path nfc-path}))) |
Determine the appropriate | (defn- field-source-alias
{:arglists '([inner-query field-clause expensive-field-info])}
[{:keys [_source-table], :as _inner-query}
[_ _id-or-name {:keys [join-alias]}, :as _field-clause]
{:keys [field-name join-is-this-level? alias-from-join alias-from-source-query]}]
(cond
;; TODO -- this just recalculates the info instead of actually finding the Field in the join and getting its desired
;; alias there... this seems like a clear bug since it doesn't go thru the uniquify logic. Something will
;; potentially break by doing this. I haven't been able to reproduce it yet however.
;;
;; This will only be triggered if the join somehow exposes duplicate columns or columns that have the same escaped
;; name after going thru [[driver/escape-alias]]. I think the only way this could happen is if we escape them
;; aggressively but the escape logic produces duplicate columns (i.e., there is overlap between the unique hashes we
;; suffix to escaped identifiers.)
;;
;; We'll have to look into this more in the future. For now, it seems to work for everything we try it with.
(and join-is-this-level? alias-from-join) alias-from-join
alias-from-source-query alias-from-source-query
(and join-alias (not join-is-this-level?)) (prefix-field-alias join-alias field-name)
:else field-name)) |
Determine the appropriate | (defn- field-desired-alias
{:arglists '([inner-query field-clause expensive-field-info])}
[_inner-query
[_ _id-or-name {:keys [join-alias], ::keys [desired-alias], explicit-name :name} :as _field-clause]
{:keys [field-name alias-from-join alias-from-source-query override-alias?], :as _expensive-field-info}]
(cond
join-alias (prefix-field-alias join-alias (or alias-from-join field-name))
;; JSON fields and similar have to be aliased by the outer field name.
override-alias? field-name
explicit-name explicit-name
desired-alias desired-alias
alias-from-source-query alias-from-source-query
:else field-name)) |
(defmulti ^:private clause-alias-info
{:arglists '([inner-query unique-alias-fn clause])}
(fn [_ _ [clause-type]]
clause-type)) | |
(defmethod clause-alias-info :field
[inner-query unique-alias-fn field-clause]
(let [expensive-info (expensive-field-info inner-query field-clause)]
(merge {::source-table (field-source-table-alias inner-query field-clause)
::source-alias (field-source-alias inner-query field-clause expensive-info)}
(when-let [nfc-path (:nfc-path expensive-info)]
{::nfc-path nfc-path})
(when-let [position (clause->position inner-query field-clause)]
{::desired-alias (unique-alias-fn position (field-desired-alias inner-query field-clause expensive-info))
::position position})))) | |
(defmulti ^:private aggregation-name
{:arglists '([mbql-clause])}
(fn [x]
(when (mbql.u/mbql-clause? x)
(first x)))) | |
make sure we have an | (defmethod aggregation-name :default
[ag-clause]
(throw (ex-info (format "Expected :aggregation-options or other fully-preprocessed aggregation, got %s."
(pr-str ag-clause))
{:clause ag-clause}))) |
(mu/defmethod aggregation-name :aggregation-options :- :string [[_aggregation-options _wrapped-ag opts]] (:name opts)) | |
(mu/defmethod aggregation-name :offset :- :string [[_offset opts _expr _n]] (:name opts)) | |
(defmethod clause-alias-info :aggregation
[{aggregations :aggregation, :as inner-query} unique-alias-fn [_ index _opts :as ag-ref-clause]]
(let [position (clause->position inner-query ag-ref-clause)]
;; an aggregation is ALWAYS returned, so it HAS to have a `position`. If it does not, the aggregation reference
;; is busted.
(when-not position
(throw (ex-info (tru "Aggregation does not exist at index {0}" index)
{:type qp.error-type/invalid-query
:clause ag-ref-clause
:query inner-query})))
(let [ag-name (aggregation-name (nth aggregations index))]
{::desired-alias (unique-alias-fn position ag-name)
::position position}))) | |
(defmethod clause-alias-info :expression
[inner-query unique-alias-fn [_ expression-name :as expression-ref-clause]]
(when-let [position (clause->position inner-query expression-ref-clause)]
{::desired-alias (unique-alias-fn position expression-name)
::position position})) | |
(defn- add-info-to-aggregation-definition
[inner-query unique-alias-fn ag-clause ag-index]
(lib.util.match/replace ag-clause
[:offset opts expr n]
(let [position (clause->position inner-query [:aggregation ag-index])
original-ag-name (:name opts)
unique-alias (unique-alias-fn position original-ag-name)]
[:offset (assoc opts
:name unique-alias
::source-alias original-ag-name
::position position
::desired-alias unique-alias)
expr
n])
[:aggregation-options wrapped-ag-clause opts, :as _ag-clause]
(let [position (clause->position inner-query [:aggregation ag-index])
original-ag-name (:name opts)
unique-alias (unique-alias-fn position original-ag-name)]
[:aggregation-options wrapped-ag-clause (assoc opts
:name unique-alias
::source-alias original-ag-name
::position position
::desired-alias unique-alias)]))) | |
(defn- add-info-to-aggregation-definitions [{aggregations :aggregation, :as inner-query} unique-alias-fn]
(cond-> inner-query
(seq aggregations)
(update :aggregation (fn [aggregations]
(into
[]
(map-indexed (fn [i aggregation]
(add-info-to-aggregation-definition inner-query unique-alias-fn aggregation i)))
aggregations))))) | |
(defn- add-alias-info* [inner-query]
(assert (not (:strategy inner-query)) "add-alias-info* should not be called on a join") ; not user-facing
(let [unique-alias-fn (make-unique-alias-fn)]
(-> (lib.util.match/replace inner-query
;; don't rewrite anything inside any source queries or source metadata.
(_ :guard (constantly (some (partial contains? (set &parents))
[:source-query :source-metadata])))
&match
#{:field :aggregation :expression}
(mbql.u/update-field-options &match merge (clause-alias-info inner-query unique-alias-fn &match)))
(add-info-to-aggregation-definitions unique-alias-fn)))) | |
Add extra info to Adds some or all of the following keys: `::source-table`String name, integer Table ID, or the keyword `::source-alias`String name to use to refer to this clause during compilation. `::desired-alias`If this clause is 'selected' (i.e., appears in `::position`If this clause is 'selected', this is the position the clause will appear in the results (i.e. the corresponding column index). | (defn add-alias-info
[query-or-inner-query]
(walk/postwalk
(fn [form]
(if (and (map? form)
((some-fn :source-query :source-table) form)
(not (:strategy form)))
(vary-meta (add-alias-info* form) assoc ::transformed true)
form))
query-or-inner-query)) |