Middlware for adding an implicit | (ns metabase.query-processor.middleware.add-implicit-clauses (:require [clojure.walk :as walk] [metabase.legacy-mbql.schema :as mbql.s] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.metadata :as lib.metadata] [metabase.lib.schema.id :as lib.schema.id] [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 :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu])) |
+----------------------------------------------------------------------------------------------------------------+ | Add Implicit Fields | +----------------------------------------------------------------------------------------------------------------+ | |
Return a sequence of all Fields for table that we'd normally include in the equivalent of a | (defn- table->sorted-fields [table-id] (->> (lib.metadata/fields (qp.store/metadata-provider) table-id) (remove :parent-id) (remove #(#{:sensitive :retired} (:visibility-type %))) (sort-by (juxt :position (comp u/lower-case-en :name))))) |
(mu/defn sorted-implicit-fields-for-table :- mbql.s/Fields "For use when adding implicit Field IDs to a query. Return a sequence of field clauses, sorted by the rules listed in [[metabase.query-processor.sort]], for all the Fields in a given Table." [table-id :- ::lib.schema.id/table] (let [fields (table->sorted-fields table-id)] (when (empty? fields) (throw (ex-info (tru "No fields found for table {0}." (pr-str (:name (lib.metadata/table (qp.store/metadata-provider) table-id)))) {:table-id table-id :type qp.error-type/invalid-query}))) (mapv (fn [field] ;; implicit datetime Fields get bucketing of `:default`. This is so other middleware doesn't try to give it ;; default bucketing of `:day` [:field (u/the-id field) nil]) fields))) | |
(defn- multiply-bucketed-field-refs [source-metadata] (->> source-metadata (map :field_ref) (group-by #(some-> % (mbql.u/update-field-options dissoc :binning :temporal-unit :original-temporal-unit))) (reduce-kv (fn [duplicates ref-key field-refs] (cond-> duplicates (and ref-key (next field-refs)) (into (filter (comp (some-fn :binning :temporal-unit) #(get % 2))) field-refs))) #{}))) | |
(mu/defn- source-metadata->fields :- mbql.s/Fields "Get implicit Fields for a query with a `:source-query` that has `source-metadata`." [source-metadata :- [:sequential {:min 1} mbql.s/SourceQueryMetadata]] ;; We want to allow columns to be bucketed or binned in several different ways. ;; Such columns would be collapsed into a single column if referenced by ID, ;; so we make sure that they get a reference by name, which is unique. (let [multiply-bucketed-refs (multiply-bucketed-field-refs source-metadata)] (distinct (for [{field-name :name base-type :base_type field-id :id [ref-type :as field-ref] :field_ref coercion-strategy :coercion_strategy} source-metadata] ;; return field-ref directly if it's a `:field` clause already. It might include important info such as ;; `:join-alias` or `:source-field`. Remove binning/temporal bucketing info. The Field should already be getting ;; bucketed in the source query; don't need to apply bucketing again in the parent query. Mark the field as ;; `qp/ignore-coercion` here so that it doesn't happen again in the parent query. (let [not-multiply-bracketed? (not (contains? multiply-bucketed-refs field-ref))] (or (and not-multiply-bracketed? (some-> (lib.util.match/match-one field-ref :field) (mbql.u/update-field-options dissoc :binning :temporal-unit) (cond-> coercion-strategy (mbql.u/assoc-field-options :qp/ignore-coercion true)))) ;; otherwise construct a field reference that can be used to refer to this Field. ;; Force string id field if expression contains just field. See issue #28451. (if (and (not= ref-type :expression) not-multiply-bracketed? field-id) ;; If we have a Field ID, return a `:field` (id) clause [:field field-id (cond-> nil coercion-strategy (assoc :qp/ignore-coercion true))] ;; otherwise return a `:field` (name) clause, e.g. for a Field that's the result of an aggregation or ;; expression. We don't need to mark as ignore-coercion here because these won't grab the field metadata [:field field-name {:base-type base-type}]))))))) | |
Whether we should add implicit Fields to this query. True if all of the following are true:
| (mu/defn- should-add-implicit-fields? [{:keys [fields source-table source-query source-metadata] breakouts :breakout aggregations :aggregation} :- mbql.s/MBQLQuery] ;; if someone is trying to include an explicit `source-query` but isn't specifiying `source-metadata` warn that ;; there's nothing we can do to help them (when (and source-query (empty? source-metadata) (qp.store/initialized?)) ;; by 'caching' this result, this log message will only be shown once for a given QP run. (qp.store/cached [::should-add-implicit-fields-warning] (log/warn (str "Warning: cannot determine fields for an explicit `source-query` unless you also include" " `source-metadata`.\n" (format "Query: %s" (u/pprint-to-str source-query)))))) ;; Determine whether we can add the implicit `:fields` (and (or source-table (and source-query (seq source-metadata))) (every? empty? [aggregations breakouts fields]))) |
For MBQL queries with no aggregation, add a | (mu/defn- add-implicit-fields [{source-table-id :source-table, :keys [expressions source-metadata], :as inner-query}] (if-not (should-add-implicit-fields? inner-query) inner-query (let [fields (if source-table-id (sorted-implicit-fields-for-table source-table-id) (source-metadata->fields source-metadata)) ;; generate a new expression ref clause for each expression defined in the query. expressions (for [[expression-name] expressions] ;; TODO - we need to wrap this in `u/qualified-name` because `:expressions` uses ;; keywords as keys. We can remove this call once we fix that. [:expression (u/qualified-name expression-name)])] ;; if the Table has no Fields, throw an Exception, because there is no way for us to proceed (when-not (seq fields) (throw (ex-info (tru "Table ''{0}'' has no Fields associated with it." (:name (lib.metadata/table (qp.store/metadata-provider) source-table-id))) {:type qp.error-type/invalid-query}))) ;; add the fields & expressions under the `:fields` clause (assoc inner-query :fields (vec (concat fields expressions)))))) |
+----------------------------------------------------------------------------------------------------------------+ | Add Implicit Breakout Order Bys | +----------------------------------------------------------------------------------------------------------------+ | |
This function transforms top level integer field refs in order by to corresponding string field refs from breakout if present. ContextIn current situation, ie. model as a source, then aggregation and breakout, and finally order by a breakout field, [[metabase.lib.order-by/orderable-columns]] returns field ref with integer id, while reference to same field, but with string id is present in breakout. Then, [[add-implicit-breakout-order-by]] adds the string ref to order by. Resulting query would contain both references, while integral is transformed differently -- it contains no casting. As that is not part of group by, the query would fail. Reference: https://github.com/metabase/metabase/issues/44653. | (defn- fix-order-by-field-refs [{:keys [breakout order-by] :as inner-query}] (if (or (empty? breakout) (empty? order-by)) inner-query (let [name->breakout (into {} (keep (fn [[tag id-or-name :as clause]] (when (and (= :field tag) (string? id-or-name)) [id-or-name clause]))) breakout) ref->maybe-field-name (fn [[tag id-or-name]] (when (and (= :field tag) (integer? id-or-name)) ((some-fn :lib/desired-column-alias :name) (lib.metadata/field (qp.store/metadata-provider) id-or-name)))) maybe-convert-order-by-ref (fn [[dir ref :as order-by-elm]] (if-some [breakout (-> ref ref->maybe-field-name name->breakout)] [dir breakout] order-by-elm))] (update inner-query :order-by (partial mapv maybe-convert-order-by-ref))))) |
(defn- has-window-function-aggregations? [inner-query] (or (lib.util.match/match (mapcat inner-query [:aggregation :expressions]) #{:cum-sum :cum-count :offset} true) (when-let [source-query (:source-query inner-query)] (has-window-function-aggregations? source-query)))) | |
(mu/defn- add-implicit-breakout-order-by :- mbql.s/MBQLQuery "Fields specified in `breakout` should add an implicit ascending `order-by` subclause *unless* that Field is already *explicitly* referenced in `order-by`." [inner-query :- mbql.s/MBQLQuery] ;; Add a new [:asc <breakout-field>] clause for each breakout. The cool thing is `add-order-by-clause` will ;; automatically ignore new ones that are reference Fields already in the order-by clause (let [{breakouts :breakout, :as inner-query} (fix-order-by-field-refs inner-query)] (reduce mbql.u/add-order-by-clause inner-query (when-not (has-window-function-aggregations? inner-query) (for [breakout breakouts] [:asc breakout]))))) | |
+----------------------------------------------------------------------------------------------------------------+ | Middleware | +----------------------------------------------------------------------------------------------------------------+ | |
Add implicit clauses such as | (defn add-implicit-mbql-clauses [form] (walk/postwalk (fn [form] ;; add implicit clauses to any 'inner query', except for joins themselves (we should still add implicit clauses ;; like `:fields` to source queries *inside* joins) (if (and (map? form) ((some-fn :source-table :source-query) form) (not (:condition form))) (-> form add-implicit-breakout-order-by add-implicit-fields) form)) form)) |
Add an implicit | (defn add-implicit-clauses [{query-type :type, :as query}] (if (= query-type :native) query (update query :query add-implicit-mbql-clauses))) |