(ns metabase.lib.column-group (:require [medley.core :as m] [metabase.lib.card :as lib.card] [metabase.lib.join :as lib.join] [metabase.lib.join.util :as lib.join.util] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [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.util.i18n :as i18n] [metabase.util.malli :as mu])) | |
(def ^:private GroupType [:enum ;; the `:group-type/main` group includes all the columns from the source Table/Card/previous stage as well as ones ;; added in this stage. :group-type/main ;; the other two group types are for various types of joins. :group-type/join.explicit :group-type/join.implicit]) | |
(def ^:private group-type-ordering {:group-type/main 1 :group-type/join.explicit 2 :group-type/join.implicit 3}) | |
Schema for the metadata returned by [[group-columns]], and accepted by [[columns-group-columns]]. | (def ^:private ColumnGroup [:and [:map [:lib/type [:= :metadata/column-group]] [::group-type GroupType] [::columns [:sequential [:ref ::lib.schema.metadata/column]]]] [:multi {:dispatch ::group-type} [:group-type/main any?] ;; if we're in the process of BUILDING a join and using this in combination ;; with [[metabase.lib.join/join-condition-rhs-columns]], the alias won't be present yet, so group things by the ;; joinable -- either the Card we're joining, or the Table we're joining. See #32493 [:group-type/join.explicit [:and [:map [:join-alias {:optional true} [:ref ::lib.schema.common/non-blank-string]] [:table-id {:optional true} [:ref ::lib.schema.id/table]] [:card-id {:optional true} [:ref ::lib.schema.id/card]]] [:fn {:error/message ":group-type/join.explicit should only have at most one of :join-alias, :table-id, or :card-id"} (fn [m] (>= (count (keys (select-keys m [:join-alias :table-id :card-id]))) 1))]]] [:group-type/join.implicit [:map [:fk-field-id [:ref ::lib.schema.id/field]]]]]]) |
(defmethod lib.metadata.calculation/metadata-method :metadata/column-group [_query _stage-number column-group] column-group) | |
(defmulti ^:private display-info-for-group-method {:arglists '([query stage-number column-group])} (fn [_query _stage-number column-group] (::group-type column-group))) | |
(defmethod display-info-for-group-method :group-type/main [query stage-number _column-group] (merge (let [stage (lib.util/query-stage query stage-number)] (or (when-let [table (some->> (:source-table stage) (lib.metadata/table query))] (lib.metadata.calculation/display-info query stage-number table)) (when-let [card (some->> (:source-card stage) (lib.metadata/card query))] (lib.metadata.calculation/display-info query stage-number card)) ;; multi-stage queries (#30108) (when (next (:stages query)) {:display-name (i18n/tru "Summaries")}) ;; if this is a native query or something else that doesn't have a source Table or source Card then use the ;; stage display name. {:display-name (lib.metadata.calculation/display-name query stage-number stage)})) {:is-main-group true :is-from-join false :is-implicitly-joinable false})) | |
(defmethod display-info-for-group-method :group-type/join.explicit [query stage-number {:keys [join-alias table-id card-id], :as _column-group}] (merge (or (when join-alias (when-let [join (lib.join/resolve-join query stage-number join-alias)] (lib.metadata.calculation/display-info query stage-number join))) (when table-id (when-let [table (lib.metadata/table query table-id)] (lib.metadata.calculation/display-info query stage-number table))) (when card-id (if-let [card (lib.metadata/card query card-id)] (lib.metadata.calculation/display-info query stage-number card) {:display-name (lib.card/fallback-display-name card-id)}))) {:is-main-group false :is-from-join true :is-implicitly-joinable false})) | |
(defmethod display-info-for-group-method :group-type/join.implicit [query stage-number {:keys [fk-field-id], :as _column-group}] (merge (when-let [;; TODO: This is clumsy and expensive; there is likely a neater way to find the full FK column. ;; Note that using `lib.metadata/field` is out - we need to respect metadata overrides etc. in models, and ;; `lib.metadata/field` uses the field's original status. fk-column (->> (lib.util/query-stage query stage-number) (lib.metadata.calculation/visible-columns query stage-number) (m/find-first #(and (= (:id %) fk-field-id) (:fk-target-field-id %))))] (let [fk-info (lib.metadata.calculation/display-info query stage-number fk-column)] ;; Implicitly joined column pickers don't use the target table's name, they use the FK field's name with ;; "ID" dropped instead. ;; This is very intentional: one table might have several FKs to one foreign table, each with different ;; meaning (eg. ORDERS.customer_id vs. ORDERS.supplier_id both linking to a PEOPLE table). ;; See #30109 for more details. (update fk-info :display-name lib.util/strip-id))) {:is-main-group false :is-from-join false :is-implicitly-joinable true})) | |
(defmethod lib.metadata.calculation/display-info-method :metadata/column-group [query stage-number column-group] (display-info-for-group-method query stage-number column-group)) | |
(defmulti ^:private column-group-info-method {:arglists '([column-metadata])} :lib/source) | |
(defmethod column-group-info-method :source/implicitly-joinable [column-metadata] {::group-type :group-type/join.implicit, :fk-field-id (:fk-field-id column-metadata) :fk-join-alias (:fk-join-alias column-metadata)}) | |
(defmethod column-group-info-method :source/joins [{:keys [table-id], :lib/keys [card-id], :as column-metadata}] (merge {::group-type :group-type/join.explicit} ;; if we're in the process of BUILDING a join and using this in combination ;; with [[metabase.lib.join/join-condition-rhs-columns]], the alias won't be present yet, so group things by the ;; joinable -- either the Card we're joining, or the Table we're joining. Prefer `:lib/card-id` because when we ;; join a Card the Fields might have `:table-id` but we want the entire Card to appear as one group. See #32493 (or (when-let [join-alias (lib.join.util/current-join-alias column-metadata)] {:join-alias join-alias}) (when card-id {:card-id card-id}) (when table-id {:table-id table-id})))) | |
(defmethod column-group-info-method :default [_column-metadata] {::group-type :group-type/main}) | |
(mu/defn- column-group-info :- [:map [::group-type GroupType]] "The value we should use to `group-by` inside [[group-columns]]." [column-metadata :- ::lib.schema.metadata/column] (column-group-info-method column-metadata)) | |
(defn- column-group-ordering [fk-field-names {::keys [group-type] :as column-group}] (into [(group-type-ordering group-type)] (case group-type :group-type/main ["main"] ; There's only ever one main group, so no need to sort them further. :group-type/join.explicit [(:join-alias column-group)] :group-type/join.implicit [(or (:fk-join-alias column-group) ) (fk-field-names (:fk-field-id column-group) )]))) | |
(mu/defn group-columns :- [:sequential ColumnGroup] "Given a group of columns returned by a function like [[metabase.lib.order-by/orderable-columns]], group the columns by Table or equivalent (e.g. Saved Question) so that they're in an appropriate shape for showing in the Query Builder. e.g a sequence of columns like [venues.id venues.name venues.category-id ;; implicitly joinable categories.id categories.name] would get grouped into groups like [{::columns [venues.id venues.name venues.category-id]} {::columns [categories.id categories.name]}] Groups have the type `:metadata/column-group` and can be passed directly to [[metabase.lib.metadata.calculation/display-info]]. Ordered to put own columns first, then explicit joins alphabetically by join alias, then implicit joins alphabetically by FK join alias + FK field name (which is used as the table name). So if the same FK is available multiple times, they are ordered: own first, then alphabetically by the join alias for that FK." [column-metadatas :- [:sequential ::lib.schema.metadata/column]] (let [fk-field-names (into {} (comp (filter :id) (map (juxt :id :name))) column-metadatas)] (->> (group-by column-group-info column-metadatas) (map (fn [[group-info columns]] (assoc group-info :lib/type :metadata/column-group ::columns columns))) (sort-by (partial column-group-ordering fk-field-names)) vec))) | |
(mu/defn columns-group-columns :- [:sequential ::lib.schema.metadata/column] "Get the columns associated with a column group" [column-group :- ColumnGroup] (::columns column-group)) | |
(defmethod lib.metadata.calculation/display-name-method :metadata/column-group [query stage-number column-group _display-name-style] (:display-name (lib.metadata.calculation/display-info query stage-number column-group))) | |