diff --git a/frontend/src/metabase-lib/fields.ts b/frontend/src/metabase-lib/fields.ts index 305c013d2114de48668a3359da4b87650fde7bff..c8998e712e50a183c266fc58cd8d902fff85f1f0 100644 --- a/frontend/src/metabase-lib/fields.ts +++ b/frontend/src/metabase-lib/fields.ts @@ -17,3 +17,10 @@ export function withFields( ): Query { return ML.with_fields(query, stageIndex, newFields); } + +export function fieldableColumns( + query: Query, + stageIndex: number, +): ColumnMetadata[] { + return ML.fieldable_columns(query, stageIndex); +} diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc index c9f1977c4432977ff69097c9177bfd495cc81a3b..4288c3b99085d4ed98dc6bef87308e5e2995c882 100644 --- a/src/metabase/lib/aggregation.cljc +++ b/src/metabase/lib/aggregation.cljc @@ -317,17 +317,11 @@ :operator (:short aggregation-operator) :args [column]})) -(def ^:private SelectedColumnMetadata - [:merge - lib.metadata/ColumnMetadata - [:map - [:selected? {:optional true} :boolean]]]) - (def ^:private SelectedOperatorWithColumns [:merge ::lib.schema.aggregation/operator [:map - [:columns {:optional true} [:sequential SelectedColumnMetadata]] + [:columns {:optional true} [:sequential lib.metadata/ColumnMetadata]] [:selected? {:optional true} :boolean]]]) (mu/defn selected-aggregation-operators :- [:maybe [:sequential SelectedOperatorWithColumns]] @@ -346,8 +340,7 @@ (mapv (fn [col] (let [a-ref (lib.ref/ref col)] (cond-> col - (or (lib.equality/= a-ref agg-col) - (lib.equality/= a-ref (lib.util/with-default-effective-type agg-col))) + (lib.equality/ref= a-ref agg-col) (assoc :selected? true)))) cols)))))) agg-operators)))) diff --git a/src/metabase/lib/card.cljc b/src/metabase/lib/card.cljc index bbb0f57b461228afd2cb46924af81aa2a53149a0..9c86b14ebe5a05f46d45d235dea6038b9b6827f2 100644 --- a/src/metabase/lib/card.cljc +++ b/src/metabase/lib/card.cljc @@ -59,8 +59,8 @@ (when-let [card (lib.metadata/card metadata-providerable card-id)] (card-metadata-columns metadata-providerable card))) -(defmethod lib.metadata.calculation/default-columns-method :metadata/card - [query _stage-number card unique-name-fn] +(defmethod lib.metadata.calculation/visible-columns-method :metadata/card + [query _stage-number card {:keys [unique-name-fn], :as _options}] (mapv (fn [col] (assoc col :lib/desired-column-alias (unique-name-fn (:name col)))) (card-metadata-columns query card))) diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index f7316153694c48cd127ebe94e7c7aa1e4865b81a..55274b9142dd5ebabc80892ded5e300bbd84a7b4 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -136,7 +136,8 @@ lower] [lib.field fields - with-fields] + with-fields + fieldable-columns] [lib.filter filter filters diff --git a/src/metabase/lib/equality.cljc b/src/metabase/lib/equality.cljc index 2a6d6b4d60e6d7ec2c818933db545145caccc491..ea1576d7275fc05d3a1e5b95e854206eb96c7a71 100644 --- a/src/metabase/lib/equality.cljc +++ b/src/metabase/lib/equality.cljc @@ -3,7 +3,8 @@ (:refer-clojure :exclude [=]) (:require [metabase.lib.dispatch :as lib.dispatch] - [metabase.lib.hierarchy :as lib.hierarchy])) + [metabase.lib.hierarchy :as lib.hierarchy] + [metabase.lib.util :as lib.util])) (defmulti = "Determine whether two already-normalized pMBQL maps, clauses, or other sorts of expressions are equal. The basic rule @@ -85,3 +86,10 @@ (map? x) ((get-method = :dispatch-type/map) x y) (sequential? x) ((get-method = :dispatch-type/sequential) x y) :else (clojure.core/= x y))) + +(defn ref= + "Are two refs `x` and `y` equal?" + [x y] + (or (= x y) + (= (lib.util/with-default-effective-type x) + (lib.util/with-default-effective-type y)))) diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc index a5ae7b41903513ac889e39e391c4bef67b9c4495..a9e26602a10ba7cf32993edfe0c17e65a30d0986 100644 --- a/src/metabase/lib/field.cljc +++ b/src/metabase/lib/field.cljc @@ -4,6 +4,7 @@ [medley.core :as m] [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.binning :as lib.binning] + [metabase.lib.equality :as lib.equality] [metabase.lib.expression :as lib.expression] [metabase.lib.join :as lib.join] [metabase.lib.metadata :as lib.metadata] @@ -441,7 +442,7 @@ (lib.join/joined-field-desired-alias join-alias (:name field-metadata)) (:name field-metadata))) -(defn with-fields +(mu/defn with-fields :- ::lib.schema/query "Specify the `:fields` for a query. Pass `nil` or an empty sequence to remove `:fields`." ([xs] (fn [query stage-number] @@ -450,7 +451,9 @@ ([query xs] (with-fields query -1 xs)) - ([query stage-number xs] + ([query :- ::lib.schema/query + stage-number :- :int + xs] (let [xs (mapv (fn [x] (lib.ref/ref (if (fn? x) (x query stage-number) @@ -458,10 +461,42 @@ xs)] (lib.util/update-query-stage query stage-number u/assoc-dissoc :fields (not-empty xs))))) -(defn fields +(mu/defn fields :- [:maybe [:ref ::lib.schema/fields]] "Fetches the `:fields` for a query. Returns `nil` if there are no `:fields`. `:fields` should never be empty; this is enforced by the Malli schema." ([query] (fields query -1)) - ([query stage-number] + + ([query :- ::lib.schema/query + stage-number :- :int] (:fields (lib.util/query-stage query stage-number)))) + +(mu/defn fieldable-columns :- [:sequential lib.metadata/ColumnMetadata] + "Return a sequence of column metadatas for columns that you can specify in the `:fields` of a query. This is + basically just the columns returned by the source Table/Saved Question/Model or previous query stage. + + Includes a `:selected?` key letting you know this column is already in `:fields` or not; if `:fields` is + unspecified, all these columns are returned by default, so `:selected?` is true for all columns (this is a little + strange but it matches the behavior of the QB UI)." + ([query] + (fieldable-columns query -1)) + + ([query :- ::lib.schema/query + stage-number :- :int] + (let [current-fields (fields query stage-number) + selected-column? (if (empty? current-fields) + (constantly true) + (fn [column] + (let [col-ref (lib.ref/ref column)] + (boolean + (some (fn [fields-ref] + (lib.equality/ref= col-ref fields-ref)) + current-fields)))))] + (mapv (fn [col] + (assoc col :selected? (selected-column? col))) + (lib.metadata.calculation/visible-columns query + stage-number + (lib.util/query-stage query stage-number) + {:include-joined? false + :include-expressions? false + :include-implicitly-joinable? false}))))) diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc index 1249dc0bedee730cdc8aaf7a05439e288dd2a00e..3afea708e1acf6634d759e62c63a1cdffce4188a 100644 --- a/src/metabase/lib/join.cljc +++ b/src/metabase/lib/join.cljc @@ -127,15 +127,18 @@ field-name :- ::lib.schema.common/non-blank-string] (lib.util/format "%s__%s" join-alias field-name)) -(defmethod lib.metadata.calculation/default-columns-method :mbql/join - [query stage-number join unique-name-fn] +(defn- add-source-and-desired-aliases + [join unique-name-fn col] ;; should be dev-facing-only so don't need to i18n (assert (:alias join) "Join must have an alias to determine column aliases!") - (mapv (fn [col] - (assoc col - :lib/source-column-alias (:name col) - :lib/desired-column-alias (unique-name-fn (joined-field-desired-alias (:alias join) (:name col))))) - (lib.metadata.calculation/metadata query stage-number join))) + (assoc col + :lib/source-column-alias (:name col) + :lib/desired-column-alias (unique-name-fn (joined-field-desired-alias (:alias join) (:name col))))) + +(defmethod lib.metadata.calculation/visible-columns-method :mbql/join + [query stage-number join {:keys [unique-name-fn], :as _options}] + (mapv (partial add-source-and-desired-aliases join unique-name-fn) + (lib.metadata.calculation/metadata query stage-number (assoc join :fields :all)))) (def ^:private JoinsWithAliases "Schema for a sequence of joins that all have aliases." @@ -156,14 +159,30 @@ (not (:alias join)) (assoc :alias (unique-name-fn (default-join-alias query stage-number join))))) joins))) -(mu/defn all-joins-default-columns :- lib.metadata.calculation/ColumnsWithUniqueAliases - "Convenience for calling [[lib.metadata.calculation/default-columns]] on all of the joins in a query stage." +(mu/defn all-joins-visible-columns :- lib.metadata.calculation/ColumnsWithUniqueAliases + "Convenience for calling [[lib.metadata.calculation/visible-columns]] on all of the joins in a query stage." + [query :- ::lib.schema/query + stage-number :- :int + unique-name-fn :- fn?] + (into [] + (mapcat (fn [join] + (lib.metadata.calculation/visible-columns query + stage-number + join + {:unique-name-fn unique-name-fn + :include-implicitly-joinable? false}))) + (when-let [joins (:joins (lib.util/query-stage query stage-number))] + (ensure-all-joins-have-aliases query stage-number joins)))) + +(mu/defn all-joins-metadata :- lib.metadata.calculation/ColumnsWithUniqueAliases + "Convenience for calling [[lib.metadata.calculation/metadata]] on all the joins in a query stage." [query :- ::lib.schema/query stage-number :- :int unique-name-fn :- fn?] (into [] (mapcat (fn [join] - (lib.metadata.calculation/default-columns query stage-number join unique-name-fn))) + (map (partial add-source-and-desired-aliases join unique-name-fn) + (lib.metadata.calculation/metadata query stage-number join)))) (when-let [joins (:joins (lib.util/query-stage query stage-number))] (ensure-all-joins-have-aliases query stage-number joins)))) diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs index 4e941f091756badab5c38fc9d5eff90023a34d7e..4cb9b7087cc7ae18dce3a3c75d1c0bb866bf030f 100644 --- a/src/metabase/lib/js.cljs +++ b/src/metabase/lib/js.cljs @@ -402,3 +402,8 @@ (with-fields a-query -1 new-fields)) ([a-query stage-number new-fields] (lib.core/with-fields a-query stage-number new-fields))) + +(defn ^:export fieldable-columns + "Return a sequence of column metadatas for columns that you can specify in the `:fields` of a query." + [a-query stage-number] + (to-array (lib.core/fieldable-columns a-query stage-number))) diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc index a7cca573ec9d68db3a14f891e747a05598543213..ece7432fcd4c9b1850f687ffecb8986f787975da 100644 --- a/src/metabase/lib/metadata.cljc +++ b/src/metabase/lib/metadata.cljc @@ -93,7 +93,12 @@ [:lib/source-column-alias {:optional true} [:maybe ::lib.schema.common/non-blank-string]] ;; the name we should export this column as, i.e. the RHS of a `SELECT <lhs> AS <rhs>` or equivalent. This is ;; guaranteed to be unique in each stage of the query. - [:lib/desired-column-alias {:optional true} [:maybe [:string {:min 1, :max 60}]]]]) + [:lib/desired-column-alias {:optional true} [:maybe [:string {:min 1, :max 60}]]] + ;; when column metadata is returned by certain things + ;; like [[metabase.lib.aggregation/selected-aggregation-operators]] or [[metabase.lib.field/fieldable-columns]], it + ;; might include this key, which tells you whether or not that column is currently selected or not already, e.g. + ;; for [[metabase.lib.field/fieldable-columns]] it means its already present in `:fields` + [:selected? {:optional true} :boolean]]) (def ^:private CardMetadata "More or less the same as a [[metabase.models.card]], but with kebab-case keys. Note that the `:dataset-query` is not diff --git a/src/metabase/lib/metadata/calculation.cljc b/src/metabase/lib/metadata/calculation.cljc index ae5a090627fe2d6d0b8e800928cfe54259501c6a..828acba4c9e138cc8d1c66c08f8dae59cc55765a 100644 --- a/src/metabase/lib/metadata/calculation.cljc +++ b/src/metabase/lib/metadata/calculation.cljc @@ -344,26 +344,8 @@ (merge (default-display-info query stage-number table) {:is-source-table (= (lib.util/source-table query) (:id table))})) -(defmulti default-columns-method - "Impl for [[default-columns]]. This should mostly be similar to the implementation for [[metadata-method]], but needs - to include `:lib/source-column-alias` and `:lib/desired-column-alias`. `:lib/source-column-alias` should probably be - the same as `:name`; use the supplied `unique-name-fn` with the signature `(f str) => str` to ensure - `:lib/desired-column-alias` is unique." - {:arglists '([query stage-number x unique-name-fn])} - (fn [_query _stage-number x _unique-name-fn] - (lib.dispatch/dispatch-value x)) - :hierarchy lib.hierarchy/hierarchy) - -#_(defmethod default-columns-method :default - [query stage-number x unique-name-fn] - (mapv (fn [col] - (assoc col - :lib/source-column-alias (:name col) - :lib/desired-column-alias (unique-name-fn (:name col)))) - (metadata query stage-number x))) - (def ColumnsWithUniqueAliases - "Schema for column metadata that should be returned by [[default-columns]]. This is mostly used + "Schema for column metadata that should be returned by [[visible-columns]]. This is mostly used to power metadata calculation for stages (see [[metabase.lib.stage]]." [:and [:sequential @@ -383,47 +365,54 @@ (empty? columns) (apply distinct? (map (comp u/lower-case-en :lib/desired-column-alias) columns))))]]) -(mu/defn default-columns :- ColumnsWithUniqueAliases - "Return a sequence of column metadatas for columns that are returned 'by default' for a table/join/query stage. - - These columns will include `:lib/source-column-alias` and `:lib/desired-column-alias`. `:lib/desired-column-alias` - is guaranteed to be unique; `unique-name-fn` is a function with the signature - - (f column-alias-string) => unique-alias-string - - Used to generate unique names." - ([query] - (default-columns query (lib.util/query-stage query -1))) - - ([query x] - (default-columns query -1 x)) +(def VisibleColumnsOptions + "Schema for options passed to [[visible-columns]] and [[visible-columns-method]]." + [:map + ;; has the signature (f str) => str + [:unique-name-fn {:optional true} [:=> + [:cat ::lib.schema.common/non-blank-string] + ::lib.schema.common/non-blank-string]] + ;; these all default to true + [:include-joined? {:optional true} :boolean] + [:include-expressions? {:optional true} :boolean] + [:include-implicitly-joinable? {:optional true} :boolean]]) + +(mu/defn ^:private default-visible-columns-options :- VisibleColumnsOptions + [] + {:unique-name-fn (lib.util/unique-name-generator) + :include-joined? true + :include-expressions? true + :include-implicitly-joinable? true}) - ([query stage-number x] - (default-columns query stage-number x (lib.util/unique-name-generator))) +(defmulti visible-columns-method + "Impl for [[visible-columns]]. - ([query :- ::lib.schema/query - stage-number :- :int - x - unique-name-fn :- fn?] - (default-columns-method query stage-number x unique-name-fn))) + This should mostly be similar to the implementation for [[metadata-method]], but needs to include + `:lib/source-column-alias` and `:lib/desired-column-alias`. `:lib/source-column-alias` should probably be the same + as `:name`; use the supplied `:unique-name-fn` from `options` with the signature `(f str) => str` to ensure + `:lib/desired-column-alias` is unique. -(defmulti visible-columns-method - "Impl for [[visible-columns]]." - {:arglists '([query stage-number x unique-name-fn])} - (fn [_query _stage-number x _unique-name-fn] + Also, columns that aren't 'projected' should be returned as well -- in other words, ignore `:fields`, + `:aggregations`, and `:breakouts`." + {:arglists '([query stage-number x options])} + (fn [_query _stage-number x _options] (lib.dispatch/dispatch-value x)) :hierarchy lib.hierarchy/hierarchy) -(defmethod visible-columns-method :default - [query stage-number x unique-name-fn] - (default-columns query stage-number x unique-name-fn)) - (mu/defn visible-columns :- ColumnsWithUniqueAliases - "Return a sequence of columns that should be *visible* for something, e.g. a query stage or a join. Visible means - both columns that are 'returned' by the query *AND* ones that are implicitly joinable. + "Return a sequence of columns that should be visible *within* a given stage of something, e.g. a query stage or a + join query. This includes not just the columns that get returned (ones present in [[metadata]], but other columns + that are 'reachable' in this stage of the query. E.g. in a query like + + SELECT id, name + FROM table + ORDER BY position + + only `id` and `name` are 'returned' columns, but other columns such as `position` are visible in this stage as well + and would thus be returned by this function. - Default implementation is just [[default-columns]]; currently only stages have a specific implementation - for [[visible-columns-method]], but this might change in the future." + Columns from joins, expressions, and implicitly joinable columns are included automatically by default; + see [[VisibleColumnsOptions]] for the options for disabling these columns." ([query] (visible-columns query (lib.util/query-stage query -1))) @@ -431,10 +420,11 @@ (visible-columns query -1 x)) ([query stage-number x] - (visible-columns query stage-number x (lib.util/unique-name-generator))) + (visible-columns query stage-number x nil)) ([query :- ::lib.schema/query stage-number :- :int x - unique-name-fn :- fn?] - (visible-columns-method query stage-number x unique-name-fn))) + options :- [:maybe VisibleColumnsOptions]] + (let [options (merge (default-visible-columns-options) options)] + (visible-columns-method query stage-number x options)))) diff --git a/src/metabase/lib/stage.cljc b/src/metabase/lib/stage.cljc index c53fab0a0374f48824cbd707bdacf47feef4e151..fe0da548cc5bb91a7a16aee4aa90179ec6061190 100644 --- a/src/metabase/lib/stage.cljc +++ b/src/metabase/lib/stage.cljc @@ -147,7 +147,8 @@ unique-name-fn :- fn?] (when-let [card-id (lib.util/string-table-id->card-id source-table-id)] (when-let [card (lib.metadata/card query card-id)] - (lib.metadata.calculation/default-columns query stage-number card unique-name-fn)))) + (lib.metadata.calculation/visible-columns query stage-number card {:unique-name-fn unique-name-fn + :include-implicitly-joinable? false})))) (mu/defn ^:private expressions-metadata :- [:maybe lib.metadata.calculation/ColumnsWithUniqueAliases] [query :- ::lib.schema/query @@ -162,6 +163,41 @@ :lib/desired-column-alias (unique-name-fn (:name expression))) (u/assoc-default :effective-type (or base-type :type/*))))))) +(defn- implicitly-joinable-columns + "Columns that are implicitly joinable from some other columns in `column-metadatas`. To be joinable, the column has to + have appropriate FK metadata, i.e. have an `:fk-target-field-id` pointing to another Field. (I think we only include + this information for Databases that support FKs and joins, so I don't think we need to do an additional DB feature + check here.) + + This does not include columns from any Tables that are already explicitly joined, and does not include multiple + versions of a column when there are multiple pathways to it (i.e. if there is more than one FK to a Table). This + behavior matches how things currently work in MLv1, at least for order by; we can adjust as needed in the future if + it turns out we do need that stuff. + + Does not include columns that would be implicitly joinable via multiple hops." + [query stage-number column-metadatas unique-name-fn] + (let [existing-table-ids (into #{} (map :table-id) column-metadatas)] + (into [] + (comp (filter :fk-target-field-id) + (m/distinct-by :fk-target-field-id) + (map (fn [{source-field-id :id, :keys [fk-target-field-id]}] + (-> (lib.metadata/field query fk-target-field-id) + (assoc ::source-field-id source-field-id)))) + (remove #(contains? existing-table-ids (:table-id %))) + (m/distinct-by :table-id) + (mapcat (fn [{:keys [table-id], ::keys [source-field-id]}] + (let [table-metadata (lib.metadata/table query table-id) + options {:unique-name-fn unique-name-fn + :include-implicitly-joinable? false}] + (for [field (lib.metadata.calculation/visible-columns-method query stage-number table-metadata options) + :let [field (assoc field + :fk-field-id source-field-id + :lib/source :source/implicitly-joinable + :lib/source-column-alias (:name field))]] + (assoc field :lib/desired-column-alias (unique-name-fn + (lib.field/desired-alias query field)))))))) + column-metadatas))) + ;;; Calculate the columns to return if `:aggregations`/`:breakout`/`:fields` are unspecified. ;;; ;;; Formula for the so-called 'default' columns is @@ -181,35 +217,57 @@ ;;; PLUS ;;; ;;; 3. Columns added by joins at this stage -(defmethod lib.metadata.calculation/default-columns-method ::stage - [query stage-number _stage unique-name-fn] +(mu/defn ^:private previous-stage-or-source-visible-columns :- lib.metadata.calculation/ColumnsWithUniqueAliases + "Return columns from the previous query stage or source Table/Card." + [query :- ::lib.schema/query + stage-number :- :int + {:keys [unique-name-fn], :as options} :- lib.metadata.calculation/VisibleColumnsOptions] + {:pre [(fn? unique-name-fn)]} + (or + ;; 1a. columns returned by previous stage + (previous-stage-metadata query stage-number unique-name-fn) + ;; 1b or 1c + (let [{:keys [source-table], :as this-stage} (lib.util/query-stage query stage-number)] + (or + ;; 1b: default visible Fields for the source Table + (when (integer? source-table) + (let [table-metadata (lib.metadata/table query source-table)] + (lib.metadata.calculation/visible-columns query stage-number table-metadata options))) + ;; 1c. Metadata associated with a saved Question + (when (string? source-table) + (saved-question-metadata query stage-number source-table unique-name-fn)) + ;; 1d: `:lib/stage-metadata` for the (presumably native) query + (for [col (:columns (:lib/stage-metadata this-stage))] + (assoc col + :lib/source :source/native + :lib/source-column-alias (:name col) + ;; these should already be unique, but run them thru `unique-name-fn` anyway to make sure anything + ;; that gets added later gets deduplicated from these. + :lib/desired-column-alias (unique-name-fn (:name col)))))))) + +(mu/defn ^:private existing-visible-columns :- lib.metadata.calculation/ColumnsWithUniqueAliases + [query :- ::lib.schema/query + stage-number :- :int + {:keys [unique-name-fn include-joined? include-expressions?], :as options} :- lib.metadata.calculation/VisibleColumnsOptions] (concat ;; 1: columns from the previous stage, source table or query - (or - ;; 1a. columns returned by previous stage - (previous-stage-metadata query stage-number unique-name-fn) - ;; 1b or 1c - (let [{:keys [source-table], :as this-stage} (lib.util/query-stage query stage-number)] - (or - ;; 1b: default visible Fields for the source Table - (when (integer? source-table) - (let [table-metadata (lib.metadata/table query source-table)] - (lib.metadata.calculation/default-columns query stage-number table-metadata unique-name-fn))) - ;; 1c. Metadata associated with a saved Question - (when (string? source-table) - (saved-question-metadata query stage-number source-table unique-name-fn)) - ;; 1d: `:lib/stage-metadata` for the (presumably native) query - (for [col (:columns (:lib/stage-metadata this-stage))] - (assoc col - :lib/source :source/native - :lib/source-column-alias (:name col) - ;; these should already be unique, but run them thru `unique-name-fn` anyway to make sure anything - ;; that gets added later gets deduplicated from these. - :lib/desired-column-alias (unique-name-fn (:name col))))))) + (previous-stage-or-source-visible-columns query stage-number options) ;; 2: expressions (aka calculated columns) added in this stage - (expressions-metadata query stage-number unique-name-fn) + (when include-expressions? + (expressions-metadata query stage-number unique-name-fn)) ;; 3: columns added by joins at this stage - (lib.join/all-joins-default-columns query stage-number unique-name-fn))) + (when include-joined? + (lib.join/all-joins-visible-columns query stage-number unique-name-fn)))) + +(defmethod lib.metadata.calculation/visible-columns-method ::stage + [query stage-number _stage {:keys [unique-name-fn include-implicitly-joinable?], :as options}] + (let [;; query (lib.util/update-query-stage query stage-number dissoc :fields :breakout :aggregation) + existing-columns (existing-visible-columns query stage-number options)] + (concat + existing-columns + ;; add implicitly joinable columns if desired + (when include-implicitly-joinable? + (implicitly-joinable-columns query stage-number existing-columns unique-name-fn))))) (mu/defn ^:private stage-metadata :- [:maybe lib.metadata.calculation/ColumnsWithUniqueAliases] "Return results metadata about the expected columns in an MBQL query stage. If the query has @@ -224,9 +282,9 @@ unique-name-fn :- fn?] (or (existing-stage-metadata query stage-number) - (let [query (ensure-previous-stages-have-metadata query stage-number) + (let [query (ensure-previous-stages-have-metadata query stage-number) summary-cols (summary-columns query stage-number unique-name-fn) - field-cols (fields-columns query stage-number unique-name-fn)] + field-cols (fields-columns query stage-number unique-name-fn)] ;; ... then calculate metadata for this stage (cond summary-cols @@ -237,10 +295,18 @@ (into [] (m/distinct-by #(dissoc % :source_alias :lib/source :lib/source-uuid :lib/desired-column-alias)) (concat field-cols - (lib.join/all-joins-default-columns query stage-number unique-name-fn)))) + (lib.join/all-joins-metadata query stage-number unique-name-fn)))) :else - (lib.metadata.calculation/default-columns query stage-number (lib.util/query-stage query stage-number) unique-name-fn)))))) + ;; there is no `:fields` or summary columns (aggregtions or breakouts) which means we return all the visible + ;; columns from the source or previous stage plus all the expressions. We return only the `:fields` from any + ;; joins + (concat + ;; we don't want to include all visible joined columns, so calculate that separately + (previous-stage-or-source-visible-columns query stage-number {:include-implicitly-joinable? false + :unique-name-fn unique-name-fn}) + (expressions-metadata query stage-number unique-name-fn) + (lib.join/all-joins-metadata query stage-number unique-name-fn))))))) (defmethod lib.metadata.calculation/metadata-method ::stage [query stage-number _stage] @@ -271,47 +337,6 @@ (lib.util/query-stage query previous-stage-number) style)))) -(defn- implicitly-joinable-columns - "Columns that are implicitly joinable from some other columns in `column-metadatas`. To be joinable, the column has to - have appropriate FK metadata, i.e. have an `:fk-target-field-id` pointing to another Field. (I think we only include - this information for Databases that support FKs and joins, so I don't think we need to do an additional DB feature - check here.) - - This does not include columns from any Tables that are already explicitly joined, and does not include multiple - versions of a column when there are multiple pathways to it (i.e. if there is more than one FK to a Table). This - behavior matches how things currently work in MLv1, at least for order by; we can adjust as needed in the future if - it turns out we do need that stuff. - - Does not include columns that would be implicitly joinable via multiple hops." - [query stage-number column-metadatas unique-name-fn] - (let [existing-table-ids (into #{} (map :table-id) column-metadatas)] - (into [] - (comp (filter :fk-target-field-id) - (m/distinct-by :fk-target-field-id) - (map (fn [{source-field-id :id, :keys [fk-target-field-id]}] - (-> (lib.metadata/field query fk-target-field-id) - (assoc ::source-field-id source-field-id)))) - (remove #(contains? existing-table-ids (:table-id %))) - (m/distinct-by :table-id) - (mapcat (fn [{:keys [table-id], ::keys [source-field-id]}] - (let [table-metadata (lib.metadata/table query table-id)] - (for [field (lib.metadata.calculation/default-columns query stage-number table-metadata unique-name-fn) - :let [field (assoc field - :fk-field-id source-field-id - :lib/source :source/implicitly-joinable - :lib/source-column-alias (:name field))]] - (assoc field :lib/desired-column-alias (unique-name-fn - (lib.field/desired-alias query field)))))))) - column-metadatas))) - -(defmethod lib.metadata.calculation/visible-columns-method ::stage - [query stage-number stage unique-name-fn] - (let [query (lib.util/update-query-stage query stage-number dissoc :fields :breakout :aggregation) - columns (lib.metadata.calculation/default-columns query stage-number stage unique-name-fn)] - (concat - columns - (implicitly-joinable-columns query stage-number columns unique-name-fn)))) - (mu/defn append-stage :- ::lib.schema/query "Adds a new blank stage to the end of the pipeline" [query] diff --git a/src/metabase/lib/table.cljc b/src/metabase/lib/table.cljc index e953d8eed311b71c2252e91bca0cc89e8e3804d0..bb552993826633b97f4bd5dbd496e65d72bf8719 100644 --- a/src/metabase/lib/table.cljc +++ b/src/metabase/lib/table.cljc @@ -57,8 +57,8 @@ [(or position 0) (u/lower-case-en (or field-name ""))]) field-metadatas)) -(defmethod lib.metadata.calculation/default-columns-method :metadata/table - [query _stage-number table-metadata unique-name-fn] +(defmethod lib.metadata.calculation/visible-columns-method :metadata/table + [query _stage-number table-metadata {:keys [unique-name-fn], :as _options}] (when-let [field-metadatas (lib.metadata/fields query (:id table-metadata))] (->> field-metadatas remove-hidden-default-fields diff --git a/src/metabase/lib/util.cljc b/src/metabase/lib/util.cljc index 654c4980a1d9e8cdec50864799d515f3e7f5ee6b..88d1c7d3b6539d7696259a561290bef36f0930de 100644 --- a/src/metabase/lib/util.cljc +++ b/src/metabase/lib/util.cljc @@ -422,7 +422,9 @@ [query] (-> query :stages first :source-table)) -(defn unique-name-generator +(mu/defn unique-name-generator :- [:=> + [:cat ::lib.schema.common/non-blank-string] + ::lib.schema.common/non-blank-string] "Create a new function with the signature (f str) => str diff --git a/test/metabase/lib/breakout_test.cljc b/test/metabase/lib/breakout_test.cljc index e46b3c5b24bfe58cd798b8118f724dd7d2fc9b26..740e4000ff7db551e3c30bf313de9f5b563ace3c 100644 --- a/test/metabase/lib/breakout_test.cljc +++ b/test/metabase/lib/breakout_test.cljc @@ -441,3 +441,23 @@ (testing "description" (is (= "Grouped by Expr" (lib/describe-query query')))))))) + +(deftest ^:parallel breakoutable-columns-include-all-visible-columns-test + (testing "Include all visible columns, not just projected ones (#31233)" + (is (= ["ID" + "NAME" + "CATEGORY_ID" + "LATITUDE" + "LONGITUDE" + "PRICE" + "Categories__ID" ; this column is not projected, but should still be returned. + "Categories__NAME"] + (map :lib/desired-column-alias + (-> (lib/query meta/metadata-provider (meta/table-metadata :venues)) + (lib/join (-> (lib/join-clause + (meta/table-metadata :categories) + [(lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Categories"))]) + (lib/with-join-fields [(lib/with-join-alias (lib/field "CATEGORIES" "NAME") "Categories")]))) + lib/breakoutable-columns)))))) diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc index f6f0df4acf62d5a3f7a53307d860cd63521ef97e..069baac40f77fbfa0314b44c6f5649f97376039e 100644 --- a/test/metabase/lib/field_test.cljc +++ b/test/metabase/lib/field_test.cljc @@ -484,3 +484,26 @@ (is (has-fields? query) "sanity check") (is (not (has-fields? query')))))))))) + +(deftest ^:parallel fieldable-columns-test + (testing "query with no :fields" + (is (=? [{:lib/desired-column-alias "ID", :selected? true} + {:lib/desired-column-alias "NAME", :selected? true} + {:lib/desired-column-alias "CATEGORY_ID", :selected? true} + {:lib/desired-column-alias "LATITUDE", :selected? true} + {:lib/desired-column-alias "LONGITUDE", :selected? true} + {:lib/desired-column-alias "PRICE", :selected? true}] + (lib/fieldable-columns (lib/query-for-table-name meta/metadata-provider "VENUES")))))) + +(deftest ^:parallel fieldable-columns-query-with-fields-test + (testing "query with :fields" + (is (=? [{:lib/desired-column-alias "ID", :selected? true} + {:lib/desired-column-alias "NAME", :selected? true} + {:lib/desired-column-alias "CATEGORY_ID", :selected? false} + {:lib/desired-column-alias "LATITUDE", :selected? false} + {:lib/desired-column-alias "LONGITUDE", :selected? false} + {:lib/desired-column-alias "PRICE", :selected? false}] + (-> (lib/query-for-table-name meta/metadata-provider "VENUES") + (lib/with-fields [(meta/field-metadata :venues :id) + (meta/field-metadata :venues :name)]) + lib/fieldable-columns ))))) diff --git a/test/metabase/lib/metadata/calculation_test.cljc b/test/metabase/lib/metadata/calculation_test.cljc index 20ae12ef3fa65faa17b2a268a952380d46ea31f7..1d097fdeb67e14b024b35712326952c5fe22cd38 100644 --- a/test/metabase/lib/metadata/calculation_test.cljc +++ b/test/metabase/lib/metadata/calculation_test.cljc @@ -58,3 +58,23 @@ "Product → Rating" "Product → Created At"] results)))) + +(deftest ^:parallel visible-columns-test + (testing "Include all visible columns, not just projected ones (#31233)" + (is (= ["ID" + "NAME" + "CATEGORY_ID" + "LATITUDE" + "LONGITUDE" + "PRICE" + "Categories__ID" ; this column is not projected, but should still be returned. + "Categories__NAME"] + (map :lib/desired-column-alias + (-> (lib/query meta/metadata-provider (meta/table-metadata :venues)) + (lib/join (-> (lib/join-clause + (meta/table-metadata :categories) + [(lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Categories"))]) + (lib/with-join-fields [(lib/with-join-alias (lib/field "CATEGORIES" "NAME") "Categories")]))) + lib.metadata.calculation/visible-columns)))))) diff --git a/test/metabase/lib/order_by_test.cljc b/test/metabase/lib/order_by_test.cljc index a00df7f41e69979edd3dae2011ddf9ca1b1bb831..005e3d28417336ba4c965e2ab738d405c5668efb 100644 --- a/test/metabase/lib/order_by_test.cljc +++ b/test/metabase/lib/order_by_test.cljc @@ -647,6 +647,26 @@ {:display-name "Name", :lib/source :source/implicitly-joinable}] (lib/orderable-columns query'))))))) +(deftest ^:parallel orderable-columns-include-all-visible-columns-test + (testing "Include all visible columns, not just projected ones (#31233)" + (is (= ["ID" + "NAME" + "CATEGORY_ID" + "LATITUDE" + "LONGITUDE" + "PRICE" + "Categories__ID" ; this column is not projected, but should still be returned. + "Categories__NAME"] + (map :lib/desired-column-alias + (-> (lib/query meta/metadata-provider (meta/table-metadata :venues)) + (lib/join (-> (lib/join-clause + (meta/table-metadata :categories) + [(lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/with-join-alias (lib/field "CATEGORIES" "ID") "Categories"))]) + (lib/with-join-fields [(lib/with-join-alias (lib/field "CATEGORIES" "NAME") "Categories")]))) + lib/orderable-columns)))))) + (deftest ^:parallel order-by-aggregation-test (testing "Should be able to order by an aggregation (#30089)" (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")