(ns metabase.lib.field (:require [clojure.string :as str] [medley.core :as m] [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.binning :as lib.binning] [metabase.lib.card :as lib.card] [metabase.lib.dispatch :as lib.dispatch] [metabase.lib.equality :as lib.equality] [metabase.lib.expression :as lib.expression] [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.options :as lib.options] [metabase.lib.ref :as lib.ref] [metabase.lib.remove-replace :as lib.remove-replace] [metabase.lib.schema :as lib.schema] [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.schema.temporal-bucketing :as lib.schema.temporal-bucketing] [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.types.isa :as lib.types.isa] [metabase.lib.util :as lib.util] [metabase.util :as u] [metabase.util.humanization :as u.humanization] [metabase.util.i18n :as i18n] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr] [metabase.util.time :as u.time])) | |
(mu/defn resolve-column-name-in-metadata :- [:maybe ::lib.schema.metadata/column] "Find the column with `column-name` in a sequence of `column-metadatas`." [column-name :- ::lib.schema.common/non-blank-string column-metadatas :- [:sequential ::lib.schema.metadata/column]] (or (some (fn [k] (m/find-first #(= (get % k) column-name) column-metadatas)) [:lib/desired-column-alias :name]) (do (log/warnf "Invalid :field clause: column %s does not exist. Found: %s" (pr-str column-name) (pr-str (mapv :lib/desired-column-alias column-metadatas))) nil))) | |
Whether we're in a recursive call to [[resolve-column-name]] or not. Prevent infinite recursion (#32063) | (def ^:private ^:dynamic *recursive-column-resolution-by-name* false) |
(mu/defn- resolve-column-name :- [:maybe ::lib.schema.metadata/column] "String column name: get metadata from the previous stage, if it exists, otherwise if this is the first stage and we have a native query or a Saved Question source query or whatever get it from our results metadata." [query :- ::lib.schema/query stage-number :- :int column-name :- ::lib.schema.common/non-blank-string] (when-not *recursive-column-resolution-by-name* (binding [*recursive-column-resolution-by-name* true] (let [previous-stage-number (lib.util/previous-stage-number query stage-number) stage (if previous-stage-number (lib.util/query-stage query previous-stage-number) (lib.util/query-stage query stage-number)) ;; TODO -- it seems a little icky that the existence of `:metabase.lib.stage/cached-metadata` is leaking ;; here, we should look in to fixing this if we can. stage-columns (or (:metabase.lib.stage/cached-metadata stage) (get-in stage [:lib/stage-metadata :columns]) (when (or (:source-card stage) (:source-table stage) (:expressions stage) (:fields stage)) (lib.metadata.calculation/visible-columns query stage-number stage)) (log/warnf "Cannot resolve column %s: stage has no metadata" (pr-str column-name)))] (when-let [column (and (seq stage-columns) (resolve-column-name-in-metadata column-name stage-columns))] (cond-> column previous-stage-number (-> (dissoc :id :table-id ::binning ::temporal-unit) (lib.join/with-join-alias nil) (assoc :name (or (:lib/desired-column-alias column) (:name column))) (assoc :lib/source :source/previous-stage)))))))) | |
(mu/defn- resolve-field-metadata :- ::lib.schema.metadata/column "Resolve metadata for a `:field` ref. This is part of the implementation for [[lib.metadata.calculation/metadata-method]] a `:field` clause." [query :- ::lib.schema/query stage-number :- :int [_field {:keys [join-alias], :as opts} id-or-name, :as _field-clause] :- :mbql.clause/field] (let [metadata (merge (when-let [base-type (:base-type opts)] {:base-type base-type}) (when-let [effective-type ((some-fn :effective-type :base-type) opts)] {:effective-type effective-type}) (when-let [original-effective-type (::original-effective-type opts)] {::original-effective-type original-effective-type}) (when-let [original-temporal-unit (::original-temporal-unit opts)] {::original-temporal-unit original-temporal-unit}) ;; `:inherited-temporal-unit` is transfered from `:temoral-unit` ref option only when ;; the [[lib.metadata.calculation/*propagate-binning-and-bucketing*]] is thruthy, ie. bound. Intent ;; is to pass it from ref to column only during [[returned-columns]] call. Otherwise eg. ;; [[orderable-columns]] would contain that too. That could be problematic, because original ref that ;; contained `:temporal-unit` contains no `:inherited-temporal-unit`. If the column like this was used ;; to generate ref for eg. order by it would contain the `:inherited-temporal-unit`, while ;; the original column (eg. in breakout) would not. (let [inherited-temporal-unit-keys (cond-> (list :inherited-temporal-unit) lib.metadata.calculation/*propagate-binning-and-bucketing* (conj :temporal-unit))] (when-some [inherited-temporal-unit (some opts inherited-temporal-unit-keys)] {:inherited-temporal-unit inherited-temporal-unit})) ;; TODO -- some of the other stuff in `opts` probably ought to be merged in here as well. Also, if ;; the Field is temporally bucketed, the base-type/effective-type would probably be affected, right? ;; We should probably be taking that into consideration? (when-let [binning (:binning opts)] {::binning binning}) (let [binning-keys (cond-> (list :was-binned) lib.metadata.calculation/*propagate-binning-and-bucketing* (conj :binning))] (when-some [was-binned (some opts binning-keys)] {:was-binned (boolean was-binned)})) (when-let [unit (:temporal-unit opts)] {::temporal-unit unit}) (cond (integer? id-or-name) (or (lib.equality/resolve-field-id query stage-number id-or-name) {:lib/type :metadata/column, :name (str id-or-name)}) join-alias {:lib/type :metadata/column, :name (str id-or-name)} :else (or (resolve-column-name query stage-number id-or-name) {:lib/type :metadata/column, :name (str id-or-name)})))] (cond-> metadata join-alias (lib.join/with-join-alias join-alias)))) | |
If this is a nested column, add metadata about the parent column. | (mu/defn- add-parent-column-metadata [query :- ::lib.schema/query metadata :- ::lib.schema.metadata/column] (let [parent-metadata (lib.metadata/field query (:parent-id metadata)) {parent-name :name, parent-display-name :display-name} (cond->> parent-metadata (:parent-id parent-metadata) (add-parent-column-metadata query))] (-> metadata (assoc :lib/simple-name (:name metadata)) (update :name (fn [field-name] (str parent-name \. field-name))) (assoc ::simple-display-name (:display-name metadata)) (update :display-name (fn [display-name] (str parent-display-name ": " display-name)))))) |
Effective type of a column when taking the | (defn- column-metadata-effective-type [{::keys [temporal-unit], :as column-metadata}] (if (and temporal-unit (contains? lib.schema.temporal-bucketing/datetime-extraction-units temporal-unit)) :type/Integer ((some-fn :effective-type :base-type) column-metadata))) |
(defmethod lib.metadata.calculation/type-of-method :metadata/column [_query _stage-number column-metadata] (column-metadata-effective-type column-metadata)) | |
(defmethod lib.metadata.calculation/type-of-method :field [query stage-number [_tag {:keys [temporal-unit], :as _opts} _id-or-name :as field-ref]] (let [metadata (cond-> (resolve-field-metadata query stage-number field-ref) temporal-unit (assoc ::temporal-unit temporal-unit))] (lib.metadata.calculation/type-of query stage-number metadata))) | |
(defmethod lib.metadata.calculation/metadata-method :metadata/column [_query _stage-number {field-name :name, :as field-metadata}] (assoc field-metadata :name field-name)) | |
Extend column metadata | (defn extend-column-metadata-from-ref [query stage-number metadata [_tag {source-uuid :lib/uuid :keys [base-type binning effective-type join-alias source-field temporal-unit], :as opts} :as field-ref]] (let [metadata (merge {:lib/type :metadata/column} metadata {:display-name (or (:display-name opts) (lib.metadata.calculation/display-name query stage-number field-ref))})] (cond-> metadata source-uuid (assoc :lib/source-uuid source-uuid) base-type (assoc :base-type base-type, :effective-type base-type) effective-type (assoc :effective-type effective-type) temporal-unit (assoc ::temporal-unit temporal-unit) binning (assoc ::binning binning) source-field (assoc :fk-field-id source-field) join-alias (lib.join/with-join-alias join-alias)))) |
TODO -- effective type should be affected by | (defmethod lib.metadata.calculation/metadata-method :field [query stage-number field-ref] (let [field-metadata (resolve-field-metadata query stage-number field-ref) metadata (extend-column-metadata-from-ref query stage-number field-metadata field-ref)] (cond->> metadata (:parent-id metadata) (add-parent-column-metadata query)))) |
(defn- field-nesting-path [metadata-providerable {:keys [display-name parent-id] :as _field-metadata}] (loop [field-id parent-id, path (list display-name)] (if field-id (let [{:keys [display-name parent-id]} (lib.metadata/field metadata-providerable field-id)] (recur parent-id (conj path display-name))) path))) | |
(defn- nest-display-name [metadata-providerable field-metadata] (let [path (field-nesting-path metadata-providerable field-metadata)] (when (every? some? path) (str/join ": " path)))) | |
this lives here as opposed to [[metabase.lib.metadata]] because that namespace is more of an interface namespace and moving this there would cause circular references. | (defmethod lib.metadata.calculation/display-name-method :metadata/column [query stage-number {field-display-name :display-name field-name :name temporal-unit :unit binning ::binning join-alias :source-alias fk-field-id :fk-field-id table-id :table-id parent-id :parent-id simple-display-name ::simple-display-name hide-bin-bucket? :lib/hide-bin-bucket? :as field-metadata} style] (let [humanized-name (u.humanization/name->human-readable-name :simple field-name) field-display-name (or simple-display-name (when (and parent-id ;; check that we haven't nested yet (or (nil? field-display-name) (= field-display-name humanized-name))) (nest-display-name query field-metadata)) field-display-name (if (string? field-name) humanized-name (str field-name))) join-display-name (when (and (= style :long) ;; don't prepend a join display name if `:display-name` already contains one! ;; Legacy result metadata might include it for joined Fields, don't want to add ;; it twice. Otherwise we'll end up with display names like ;; ;; Products → Products → Category (not (str/includes? field-display-name " → "))) (or (when fk-field-id ;; 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. (if-let [field (lib.metadata/field query fk-field-id)] (-> (lib.metadata.calculation/display-info query stage-number field) :display-name lib.util/strip-id) (let [table (lib.metadata/table-or-card query table-id)] (lib.metadata.calculation/display-name query stage-number table style)))) join-alias (lib.join.util/current-join-alias field-metadata))) display-name (if join-display-name (str join-display-name " → " field-display-name) field-display-name) temporal-format #(lib.temporal-bucket/ensure-ends-with-temporal-unit % temporal-unit) bin-format #(lib.binning/ensure-ends-with-binning % binning (:semantic-type field-metadata))] ;; temporal unit and binning formatting are only applied if they haven't been applied yet (cond (and (not= style :long) hide-bin-bucket?) display-name (and temporal-unit (not= display-name (temporal-format humanized-name))) (temporal-format display-name) (and binning (not= display-name (bin-format humanized-name))) (bin-format display-name) :else display-name))) |
(defmethod lib.metadata.calculation/display-name-method :field [query stage-number [_tag {:keys [binning join-alias temporal-unit source-field], :as _opts} _id-or-name, :as field-clause] style] (if-let [field-metadata (cond-> (resolve-field-metadata query stage-number field-clause) join-alias (assoc :source-alias join-alias) temporal-unit (assoc :unit temporal-unit) binning (assoc ::binning binning) source-field (assoc :fk-field-id source-field))] (lib.metadata.calculation/display-name query stage-number field-metadata style) ;; mostly for the benefit of JS, which does not enforce the Malli schemas. (i18n/tru "[Unknown Field]"))) | |
(defmethod lib.metadata.calculation/column-name-method :metadata/column [_query _stage-number {field-name :name}] field-name) | |
(defmethod lib.metadata.calculation/column-name-method :field [query stage-number [_tag _id-or-name, :as field-clause]] (if-let [field-metadata (resolve-field-metadata query stage-number field-clause)] (lib.metadata.calculation/column-name query stage-number field-metadata) ;; mostly for the benefit of JS, which does not enforce the Malli schemas. "unknown_field")) | |
(defmethod lib.metadata.calculation/display-info-method :metadata/column [query stage-number field-metadata] (merge ((get-method lib.metadata.calculation/display-info-method :default) query stage-number field-metadata) ;; These have to be calculated even if the metadata has display-name to support nested fields ;; because the query processor doesn't produce nested display-names. {:display-name (lib.metadata.calculation/display-name query stage-number field-metadata) :long-display-name (lib.metadata.calculation/display-name query stage-number field-metadata :long)} ;; Include description and fingerprint if they're present on the column. Only proper fields or columns from a model ;; have these, not aggregations or expressions. (when-let [description (:description field-metadata)] {:description description}) (when-let [fingerprint (:fingerprint field-metadata)] {:fingerprint fingerprint}) ;; if this column comes from a source Card (Saved Question/Model/etc.) use the name of the Card as the 'table' name ;; rather than the ACTUAL table name. (when (= (:lib/source field-metadata) :source/card) (when-let [card-id (:lib/card-id field-metadata)] (when-let [card (lib.metadata/card query card-id)] {:table {:name (:name card), :display-name (:name card)}}))))) | |
---------------------------------- Temporal Bucketing ---------------------------------------- | |
TODO -- it's a little silly to make this a multimethod I think since there are exactly two implementations of it, right? Or can expression and aggregation references potentially be temporally bucketed as well? Think about whether just making this a plain function like we did for [[metabase.lib.join/with-join-alias]] makes sense or not. | |
(defmethod lib.temporal-bucket/temporal-bucket-method :field [[_tag opts _id-or-name]] (:temporal-unit opts)) | |
(defmethod lib.temporal-bucket/temporal-bucket-method :metadata/column [metadata] (::temporal-unit metadata)) | |
(defmethod lib.temporal-bucket/with-temporal-bucket-method :field [field-ref unit] (lib.temporal-bucket/add-temporal-bucket-to-ref field-ref unit)) | |
(defmethod lib.temporal-bucket/with-temporal-bucket-method :metadata/column [metadata unit] (let [original-effective-type ((some-fn ::original-effective-type :effective-type :base-type) metadata) original-temporal-unit ((some-fn ::original-temporal-unit ::temporal-unit) metadata)] (if unit (-> metadata (assoc ::temporal-unit unit ::original-effective-type original-effective-type) (m/assoc-some ::original-temporal-unit original-temporal-unit)) (cond-> (dissoc metadata ::temporal-unit ::original-effective-type) original-effective-type (assoc :effective-type original-effective-type) original-temporal-unit (assoc ::original-temporal-unit original-temporal-unit))))) | |
(defmethod lib.temporal-bucket/available-temporal-buckets-method :field [query stage-number field-ref] (lib.temporal-bucket/available-temporal-buckets query stage-number (resolve-field-metadata query stage-number field-ref))) | |
(defn- fingerprint-based-default-unit [fingerprint] (u/ignore-exceptions (when-let [{:keys [earliest latest]} (-> fingerprint :type :type/DateTime)] (let [days (u.time/day-diff (u.time/coerce-to-timestamp earliest) (u.time/coerce-to-timestamp latest))] (when-not (NaN? days) (condp > days 1 :minute 31 :day 365 :week :month)))))) | |
(defmethod lib.temporal-bucket/available-temporal-buckets-method :metadata/column [_query _stage-number field-metadata] (lib.temporal-bucket/available-temporal-buckets-for-type ((some-fn :effective-type :base-type) field-metadata) ;; `:ineherited-temporal-unit` being set means field was bucketed on former stage. For this case, make the default nil ;; for next bucketing attempt (of already bucketed) field eg. through BreakoutPopover on FE, by setting `:inherited` ;; default unit. (if (or (nil? (:inherited-temporal-unit field-metadata)) (= :default (:inherited-temporal-unit field-metadata))) (or (some-> field-metadata :fingerprint fingerprint-based-default-unit) :month) :inherited) (::temporal-unit field-metadata))) | |
---------------------------------------- Binning --------------------------------------------- | |
(defmethod lib.binning/binning-method :field [field-clause] (some-> field-clause lib.options/options :binning (assoc :lib/type ::lib.binning/binning :metadata-fn (fn [query stage-number] (resolve-field-metadata query stage-number field-clause))))) | |
(defmethod lib.binning/binning-method :metadata/column [metadata] (some-> metadata ::binning (assoc :lib/type ::lib.binning/binning :metadata-fn (constantly metadata)))) | |
(defmethod lib.binning/with-binning-method :field [field-clause binning] (lib.options/update-options field-clause u/assoc-dissoc :binning binning)) | |
(defmethod lib.binning/with-binning-method :metadata/column [metadata binning] (u/assoc-dissoc metadata ::binning binning)) | |
(defmethod lib.binning/available-binning-strategies-method :field [query stage-number field-ref] (lib.binning/available-binning-strategies query stage-number (resolve-field-metadata query stage-number field-ref))) | |
(defmethod lib.binning/available-binning-strategies-method :metadata/column [query _stage-number {:keys [effective-type fingerprint semantic-type] :as field-metadata}] (if (not= (:lib/source field-metadata) :source/expressions) (let [binning? (some-> query lib.metadata/database :features (contains? :binning)) fingerprint (get-in fingerprint [:type :type/Number]) existing (lib.binning/binning field-metadata) strategies (cond ;; Abort if the database doesn't support binning, or this column does not have a defined range. (not (and binning? (:min fingerprint) (:max fingerprint))) nil (isa? semantic-type :type/Coordinate) (lib.binning/coordinate-binning-strategies) (and (isa? effective-type :type/Number) (not (isa? semantic-type :Relation/*))) (lib.binning/numeric-binning-strategies))] ;; TODO: Include the time and date binning strategies too; see metabase.api.table/assoc-field-dimension-options. (for [strat strategies] (cond-> strat (or (:was-binned field-metadata) existing) (dissoc :default) (lib.binning/strategy= strat existing) (assoc :selected true)))) [])) | |
(defmethod lib.ref/ref-method :field [field-clause] field-clause) | |
(defn- column-metadata->field-ref [metadata] (let [inherited-column? (when-not (::lib.card/force-broken-id-refs metadata) (#{:source/card :source/native :source/previous-stage} (:lib/source metadata))) options (merge {:lib/uuid (str (random-uuid)) :base-type (:base-type metadata) :effective-type (column-metadata-effective-type metadata)} ;; This one deliberately comes first so it will be overwritten by current-join-alias. ;; We don't want both :source-field and :join-alias, though. (when-let [source-alias (and (not inherited-column?) (not (:fk-field-id metadata)) (not= :source/implicitly-joinable (:lib/source metadata)) (:source-alias metadata))] {:join-alias source-alias}) (when-let [join-alias (when-not inherited-column? (lib.join.util/current-join-alias metadata))] {:join-alias join-alias}) (when-let [temporal-unit (::temporal-unit metadata)] {:temporal-unit temporal-unit}) (when-let [original-effective-type (::original-effective-type metadata)] {::original-effective-type original-effective-type}) (when-let [original-temporal-unit (::original-temporal-unit metadata)] {::original-temporal-unit original-temporal-unit}) (when-let [inherited-temporal-unit (:inherited-temporal-unit metadata)] {:inherited-temporal-unit inherited-temporal-unit}) (when-let [binning (::binning metadata)] {:binning binning}) (when-let [was-binned (:was-binned metadata)] {:was-binned was-binned}) (when-let [source-field-id (when-not inherited-column? (:fk-field-id metadata))] {:source-field source-field-id})) id-or-name ((if inherited-column? (some-fn :lib/desired-column-alias :name) (some-fn :id :name)) metadata)] [:field options id-or-name])) | |
(defmethod lib.ref/ref-method :metadata/column [{source :lib/source, :as metadata}] (case source :source/aggregations (lib.aggregation/column-metadata->aggregation-ref metadata) :source/expressions (lib.expression/column-metadata->expression-ref metadata) ;; `:source/fields`/`:source/breakouts` can hide the true origin of the column. Since it's impossible to break out ;; by aggregation references at the current stage, we only have to check if we break out by an expression ;; reference. `:lib/expression-name` is only set for expression references, so if it's set, we have to generate an ;; expression ref, otherwise we generate a normal field ref. (:source/fields :source/breakouts) (if (:lib/expression-name metadata) (lib.expression/column-metadata->expression-ref metadata) (column-metadata->field-ref metadata)) #_else (column-metadata->field-ref metadata))) | |
Return the [[::lib.schema.metadata/column]] for all the expressions in a stage of a query. | (defn- expression-columns [query stage-number] (filter #(= (:lib/source %) :source/expressions) (lib.metadata.calculation/visible-columns query stage-number (lib.util/query-stage query stage-number) {:include-joined? false :include-expressions? true :include-implicitly-joinable? false}))) |
(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] (with-fields query stage-number xs))) ([query xs] (with-fields query -1 xs)) ([query :- ::lib.schema/query stage-number :- :int xs] (let [xs (not-empty (mapv lib.ref/ref xs)) ;; If any fields are specified, include all expressions not yet included. expr-cols (expression-columns query stage-number) ;; Set of expr-cols which are *already* included. included (into #{} (keep #(lib.equality/find-matching-column query stage-number % expr-cols)) (or xs [])) ;; Those expr-refs which must still be included. to-add (remove included expr-cols) xs (when xs (into xs (map lib.ref/ref) to-add))] (lib.util/update-query-stage query stage-number u/assoc-dissoc :fields xs)))) | |
(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 :- ::lib.schema/query stage-number :- :int] (:fields (lib.util/query-stage query stage-number)))) | |
(mu/defn fieldable-columns :- [:sequential ::lib.schema.metadata/column] "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 [visible-columns (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}) selected-fields (fields query stage-number)] (if (empty? selected-fields) (mapv (fn [col] (assoc col :selected? true)) visible-columns) (lib.equality/mark-selected-columns query stage-number visible-columns selected-fields))))) | |
Given a query and stage, sets the | (defn- populate-fields-for-stage [query stage-number] (let [defaults (lib.metadata.calculation/default-columns-for-stage query stage-number)] (lib.util/update-query-stage query stage-number assoc :fields (mapv lib.ref/ref defaults)))) |
If the given stage already has a | (defn- query-with-fields [query stage-number] (cond-> query (not (:fields (lib.util/query-stage query stage-number))) (populate-fields-for-stage stage-number))) |
(defn- include-field [query stage-number column] (let [populated (query-with-fields query stage-number) field-refs (fields populated stage-number) match-ref (lib.equality/find-matching-ref column field-refs) column-ref (lib.ref/ref column)] (if (and match-ref (or (string? (last column-ref)) (integer? (last match-ref)))) ;; If the column is already found, do nothing and return the original query. query (lib.util/update-query-stage populated stage-number update :fields conj column-ref)))) | |
(defn- add-field-to-join [query stage-number column] (let [column-ref (lib.ref/ref column) [join field] (first (for [join (lib.join/joins query stage-number) :let [joinables (lib.join/joinable-columns query stage-number join) field (lib.equality/find-matching-column query stage-number column-ref joinables)] :when field] [join field])) join-fields (lib.join/join-fields join)] ;; Nothing to do if it's already selected, or if this join already has :fields :all. ;; Otherwise, append it to the list of fields. (if (or (= join-fields :all) (and field (not= join-fields :none) (lib.equality/find-matching-ref field join-fields))) query (lib.remove-replace/replace-join query stage-number join (lib.join/with-join-fields join (if (= join-fields :none) [column] (conj join-fields column))))))) | |
(defn- native-query-fields-edit-error [] (i18n/tru "Fields cannot be adjusted on native queries. Either edit the native query, or save this question and edit the fields in a GUI question based on this one.")) | |
(defn- source-clauses-only-fields-edit-error [] (i18n/tru (str "Only source columns (those from a table, model, or saved question) can be adjusted on a query. " "Aggregations, breakouts and expressions are always returned, and must be removed from the query or " "hidden in the UI."))) | |
(mu/defn add-field :- ::lib.schema/query "Adds a given field (`ColumnMetadata`, as returned from eg. [[visible-columns]]) to the fields returned by the query. Exactly what this means depends on the source of the field: - Source table/card, previous stage of the query, custom expression, aggregation or breakout: - Add it to the `:fields` list - If `:fields` is missing, it's implicitly `:all`, so do nothing. - Implicit join: add it to the `:fields` list; query processor will do the right thing with it. - Explicit join: add it to that join's `:fields` list." [query :- ::lib.schema/query stage-number :- :int column :- lib.metadata.calculation/ColumnMetadataWithSource] (let [stage (lib.util/query-stage query stage-number) source (:lib/source column)] (-> (case source (:source/table-defaults :source/fields :source/card :source/previous-stage :source/expressions :source/aggregations :source/breakouts) (cond-> query (contains? stage :fields) (include-field stage-number column)) :source/joins (add-field-to-join query stage-number column) :source/implicitly-joinable (include-field query stage-number column) :source/native (throw (ex-info (native-query-fields-edit-error) {:query query :stage stage-number})) ;; Default case - do nothing if we don't know about the incoming value. ;; Generates a warning, as we should aim to capture all the :source/* values here. (do (log/warnf "Cannot add-field with unknown source %s" (pr-str source)) query)) ;; Then drop any redundant :fields clauses. lib.remove-replace/normalize-fields-clauses))) | |
(defn- remove-matching-ref [column refs] (let [match (lib.equality/find-matching-ref column refs)] (remove #(= % match) refs))) | |
This is called only for fields that plausibly need removing. If the stage has no | (defn- exclude-field [query stage-number column] (let [old-fields (-> (query-with-fields query stage-number) (lib.util/query-stage stage-number) :fields) new-fields (remove-matching-ref column old-fields)] (cond-> query ;; If we couldn't find the field, return the original query unchanged. (< (count new-fields) (count old-fields)) (lib.util/update-query-stage stage-number assoc :fields new-fields)))) |
(defn- remove-field-from-join [query stage-number column] (let [join (lib.join/resolve-join query stage-number (::lib.join/join-alias column)) join-fields (lib.join/join-fields join)] (if (or (nil? join-fields) (= join-fields :none)) ;; Nothing to do if there's already no join fields. query (let [resolved-join-fields (if (= join-fields :all) (map lib.ref/ref (lib.metadata.calculation/returned-columns query stage-number join)) join-fields) removed (remove-matching-ref column resolved-join-fields)] (cond-> query ;; If we actually removed a field, replace the join. Otherwise return the query unchanged. (< (count removed) (count resolved-join-fields)) (lib.remove-replace/replace-join stage-number join (lib.join/with-join-fields join removed))))))) | |
(mu/defn remove-field :- ::lib.schema/query "Removes the field (a `ColumnMetadata`, as returned from eg. [[visible-columns]]) from those fields returned by the query. Exactly what this means depends on the source of the field: - Source table/card, previous stage, custom expression, aggregations or breakouts: - If `:fields` is missing, it's implicitly `:all` - populate it with all the columns except the removed one. - Remove the target column from the `:fields` list - Implicit join: remove it from the `:fields` list; do nothing if it's not there. - (An implicit join only exists in the `:fields` clause, so if it's not there then it's not anywhere.) - Explicit join: remove it from that join's `:fields` list (handle `:fields :all` like for source tables)." [query :- ::lib.schema/query stage-number :- :int column :- lib.metadata.calculation/ColumnMetadataWithSource] (let [source (:lib/source column)] (-> (case source (:source/table-defaults :source/fields :source/card :source/previous-stage :source/expressions :source/implicitly-joinable) (exclude-field query stage-number column) :source/joins (remove-field-from-join query stage-number column) :source/native (throw (ex-info (native-query-fields-edit-error) {:query query :stage stage-number})) (:source/breakouts :source/aggregations) (throw (ex-info (source-clauses-only-fields-edit-error) {:query query :stage stage-number :source source})) ;; Default case: do nothing and return the query unchaged. ;; Generate a warning - we should aim to capture every `:source/*` value above. (do (log/warnf "Cannot remove-field with unknown source %s" (pr-str source)) query)) ;; Then drop any redundant :fields clauses. lib.remove-replace/normalize-fields-clauses))) | |
TODO: Refactor this away? The special handling for aggregations is strange. | (mu/defn find-visible-column-for-ref :- [:maybe ::lib.schema.metadata/column] "Return the visible column in `query` at `stage-number` referenced by `field-ref`. If `stage-number` is omitted, the last stage is used. This is currently only meant for use with `:field` clauses." ([query field-ref] (find-visible-column-for-ref query -1 field-ref)) ([query :- ::lib.schema/query stage-number :- :int field-ref :- some?] (let [stage (lib.util/query-stage query stage-number) ;; not 100% sure why, but [[lib.metadata.calculation/visible-columns]] doesn't seem to return aggregations, ;; so we have to use [[lib.metadata.calculation/returned-columns]] instead. columns ((if (= (lib.dispatch/dispatch-value field-ref) :aggregation) lib.metadata.calculation/returned-columns lib.metadata.calculation/visible-columns) query stage-number stage)] (lib.equality/find-matching-column query stage-number field-ref columns)))) |
Return true if field is a JSON field, false if not. | (defn json-field? [field] (some? (:nfc-path field))) |
yes, this is intentionally different from the version in | (mr/def ::field-values-search-info.has-field-values [:enum :list :search :none]) |
(mr/def ::field-values-search-info [:map [:field-id [:maybe [:ref ::lib.schema.id/field]]] [:search-field-id [:maybe [:ref ::lib.schema.id/field]]] [:has-field-values [:ref ::field-values-search-info.has-field-values]]]) | |
(mu/defn infer-has-field-values :- ::field-values-search-info.has-field-values "Determine the value of `:has-field-values` we should return for column metadata for frontend consumption to power filter search widgets, either when returned by the the REST API or in MLv2 with [[field-values-search-info]]. Note that this value is not necessarily the same as the value of `has_field_values` in the application database. `has_field_values` may be unset, in which case we will try to infer it. `:auto-list` is not currently understood by the FE filter stuff, so we will instead return `:list`; the distinction is not important to it anyway." [{:keys [has-field-values], :as field} :- [:map ;; this doesn't use `::lib.schema.metadata/column` because it's stricter ;; than we need and the REST API calls this function with optimized Field ;; maps that don't include some keys like `:name` [:base-type {:optional true} [:maybe ::lib.schema.common/base-type]] [:effective-type {:optional true} [:maybe ::lib.schema.common/base-type]] [:has-field-values {:optional true} [:maybe ::lib.schema.metadata/column.has-field-values]]]] (cond ;; if `has_field_values` is set in the DB, use that value; but if it's `auto-list`, return the value as `list` to ;; avoid confusing FE code, which can remain blissfully unaware that `auto-list` is a thing (= has-field-values :auto-list) :list has-field-values has-field-values ;; otherwise if it does not have value set in DB we will infer it (lib.types.isa/searchable? field) :search :else :none)) | |
(mu/defn- remapped-field :- [:maybe ::lib.schema.metadata/column] [metadata-providerable :- ::lib.schema.metadata/metadata-providerable column :- ::lib.schema.metadata/column] (when (lib.types.isa/foreign-key? column) (when-let [remap-field-id (get-in column [:lib/external-remap :field-id])] (lib.metadata/field metadata-providerable remap-field-id)))) | |
(mu/defn- search-field :- [:maybe ::lib.schema.metadata/column] [metadata-providerable :- ::lib.schema.metadata/metadata-providerable column :- ::lib.schema.metadata/column] (let [col (or (when (lib.types.isa/primary-key? column) (when-let [name-field (:name-field column)] (lib.metadata/field metadata-providerable (u/the-id name-field)))) (remapped-field metadata-providerable column) column)] (when (lib.types.isa/searchable? col) col))) | |
(mu/defn field-values-search-info :- ::field-values-search-info "Info about whether the column in question has FieldValues associated with it for purposes of powering a search widget in the QB filter modals." [metadata-providerable :- ::lib.schema.metadata/metadata-providerable column :- ::lib.schema.metadata/column] (when column (let [column-field-id (:id column) search-field-id (:id (search-field metadata-providerable column))] {:field-id (when (int? column-field-id) column-field-id) :search-field-id (when (int? search-field-id) search-field-id) :has-field-values (if column (infer-has-field-values column) :none)}))) | |