diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc index aa1426984c2705a771e66dd8d30a399bc3264b20..a98506dab0cbc95094d7eb8d2bf651e5d9137af4 100644 --- a/src/metabase/lib/aggregation.cljc +++ b/src/metabase/lib/aggregation.cljc @@ -11,6 +11,15 @@ [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) +(mu/defn column-metadata->aggregation-ref :- :mbql.clause/aggregation + "Given `:metadata/field` column metadata for an aggregation, construct an `:aggregation` reference." + [metadata :- lib.metadata/ColumnMetadata] + (let [options {:lib/uuid (str (random-uuid)) + :effective-type ((some-fn :effective_type :base_type) metadata)} + index (::aggregation-index metadata)] + (assert (integer? index) "Metadata for an aggregation reference should include ::aggregation-index") + [:aggregation options index])) + (mu/defn resolve-aggregation :- ::lib.schema.aggregation/aggregation "Resolve an aggregation with a specific `index`." [query :- ::lib.schema/query @@ -32,14 +41,16 @@ (for [aggregation aggregations] (lib.metadata.calculation/display-name query stage-number aggregation))))) -(defmethod lib.metadata.calculation/metadata :aggregation - [query stage-number [_ag opts index, :as aggregation-ref]] +(defmethod lib.metadata.calculation/metadata-method :aggregation + [query stage-number [_ag opts index, :as _aggregation-ref]] (let [aggregation (resolve-aggregation query stage-number index)] (merge (lib.metadata.calculation/metadata query stage-number aggregation) - {:field_ref aggregation-ref} + {:lib/source :source/aggregations} (when (:base-type opts) - {:base_type (:base-type opts)})))) + {:base_type (:base-type opts)}) + (when (:effective-type opts) + {:effective_type opts})))) ;;; TODO -- merge this stuff into `defop` somehow. @@ -213,10 +224,6 @@ stage-number :- :int] (when-let [aggregation-exprs (not-empty (:aggregation (lib.util/query-stage query stage-number)))] (map-indexed (fn [i aggregation] - (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation) - ag-ref [:aggregation - {:lib/uuid (str (random-uuid)) - :base-type (:base_type metadata)} - i]] - (assoc metadata :field_ref ag-ref, :source :aggregation))) + (let [metadata (lib.metadata.calculation/metadata query stage-number aggregation)] + (assoc metadata :lib/source :source/aggregations, ::aggregation-index i))) aggregation-exprs))) diff --git a/src/metabase/lib/breakout.cljc b/src/metabase/lib/breakout.cljc index 2c1ac99b4ef09669b707e1e3d86389330dfc1075..5c4d84a5cbde2b774b875ef980a24a3a6b744cb5 100644 --- a/src/metabase/lib/breakout.cljc +++ b/src/metabase/lib/breakout.cljc @@ -37,5 +37,5 @@ stage-number :- :int] (when-let [breakout-exprs (not-empty (:breakout (lib.util/query-stage query stage-number)))] (mapv (fn [field-ref] - (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :source :breakout)) + (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :lib/source :source/breakouts)) breakout-exprs))) diff --git a/src/metabase/lib/common.cljc b/src/metabase/lib/common.cljc index e11b85e1760d44f1b2a8cf4f6216ce1d23116d36..cda9d9be0ab0807e574f77de58dd8a8bdad0e0d9 100644 --- a/src/metabase/lib/common.cljc +++ b/src/metabase/lib/common.cljc @@ -1,14 +1,15 @@ (ns metabase.lib.common (:require [metabase.lib.dispatch :as lib.dispatch] - [metabase.lib.field :as lib.field] - #_{:clj-kondo/ignore [:unused-namespace]} [metabase.lib.options :as lib.options] + [metabase.lib.ref :as lib.ref] [metabase.lib.schema.common :as schema.common] - #_{:clj-kondo/ignore [:unused-namespace]} [metabase.util.malli :as mu]) #?(:cljs (:require-macros [metabase.lib.common]))) +(comment lib.options/keep-me + mu/keep-me) + (mu/defn external-op :- [:maybe ::schema.common/external-op] "Convert the internal operator `clause` to the external format." [[operator options :as clause]] @@ -29,8 +30,8 @@ x) (defmethod ->op-arg :metadata/field - [query stage-number field-metadata] - (lib.field/field query stage-number field-metadata)) + [_query _stage-number field-metadata] + (lib.ref/ref field-metadata)) (defmethod ->op-arg :lib/external-op [query stage-number {:keys [operator options args] :or {options {}}}] diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index cca93928b361bafad409d34f460a958691733bd6..f47949feab45c9c42170d7a0a9aedb173b48b2d7 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -2,7 +2,7 @@ "Currently this is mostly a convenience namespace for REPL and test usage. We'll probably have a slightly different version of this for namespace for QB and QP usage in the future -- TBD." (:refer-clojure :exclude [filter remove replace and or not = < <= > ->> >= not-empty case count distinct max min - + - * / time abs concat replace]) + + - * / time abs concat replace ref]) (:require [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.breakout :as lib.breakout] @@ -18,6 +18,7 @@ [metabase.lib.normalize :as lib.normalize] [metabase.lib.order-by :as lib.order-by] [metabase.lib.query :as lib.query] + [metabase.lib.ref :as lib.ref] [metabase.lib.segment :as lib.segment] [metabase.lib.stage :as lib.stage] [metabase.lib.table :as lib.table] @@ -38,6 +39,7 @@ lib.normalize/keep-me lib.order-by/keep-me lib.query/keep-me + lib.ref/keep-me lib.segment/keep-me lib.stage/keep-me lib.table/keep-me @@ -67,6 +69,7 @@ table] [lib.expression expression + expressions + - * @@ -161,6 +164,8 @@ remove-clause replace-clause saved-question-query] + [lib.ref + ref] [lib.stage append-stage drop-stage] diff --git a/src/metabase/lib/dev.cljc b/src/metabase/lib/dev.cljc index 42b3a72a297fc3e7a1ce4802fb2a8d418e05d005..a28321ed17914bcb7bc191a8d183f0b42f104761 100644 --- a/src/metabase/lib/dev.cljc +++ b/src/metabase/lib/dev.cljc @@ -2,9 +2,9 @@ "Conveniences for usage in REPL and tests. Things in this namespace are not meant for normal usage in the FE client or in QB code." (:require - [metabase.lib.field :as lib.field] [metabase.lib.metadata :as lib.metadata] [metabase.lib.query :as lib.query] + [metabase.lib.ref :as lib.ref] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.id :as lib.schema.id] @@ -17,13 +17,11 @@ ::lib.schema.common/non-blank-string]] (if (integer? id-or-name) (let [id id-or-name] - (fn [query stage-number] - (->> (lib.metadata/field query id) - (lib.field/field query stage-number)))) + (fn [query _stage-number] + (lib.ref/ref (lib.metadata/field query id)))) (let [column-name id-or-name] (fn [query stage-number] - (->> (lib.metadata/stage-column query stage-number column-name) - (lib.field/field query stage-number)))))) + (lib.ref/ref (lib.metadata/stage-column query stage-number column-name)))))) ([table-name :- ::lib.schema.common/non-blank-string field-name :- ::lib.schema.common/non-blank-string] @@ -32,9 +30,8 @@ ([schema :- [:maybe ::lib.schema.common/non-blank-string] table-name :- ::lib.schema.common/non-blank-string field-name :- ::lib.schema.common/non-blank-string] - (fn [query stage-number] - (->> (lib.metadata/field query schema table-name field-name) - (lib.field/field query stage-number))))) + (fn [query _stage-number] + (lib.ref/ref (lib.metadata/field query schema table-name field-name))))) (mu/defn query-for-table-name :- ::lib.schema/query "Create a new query for a specific Table with a table name." diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc index e173572fefeeea4beaf2c7b06d6da8bda0729adf..aa5aa295749dac4fd8edac745996ed705743e106 100644 --- a/src/metabase/lib/expression.cljc +++ b/src/metabase/lib/expression.cljc @@ -16,6 +16,13 @@ [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) +(mu/defn column-metadata->expression-ref :- :mbql.clause/expression + "Given `:metadata/field` column metadata for an expression, construct an `:expression` reference." + [metadata :- lib.metadata/ColumnMetadata] + (let [options {:lib/uuid (str (random-uuid)) + :effective-type ((some-fn :effective_type :base_type) metadata)}] + [:expression options (:name metadata)])) + (mu/defn resolve-expression :- ::lib.schema.expression/expression "Find the expression with `expression-name` in a given stage of a `query`, or throw an Exception if it doesn't exist." @@ -34,11 +41,11 @@ [{:keys [operator options args] :or {options {}}}] (lib.schema.expression/type-of* (into [(keyword operator) options] args))) -(defmethod lib.metadata.calculation/metadata :expression +(defmethod lib.metadata.calculation/metadata-method :expression [query stage-number [_expression opts expression-name, :as expression-ref]] (let [expression (resolve-expression query stage-number expression-name)] {:lib/type :metadata/field - :field_ref expression-ref + :lib/source :source/expressions :name expression-name :display_name (lib.metadata.calculation/display-name query stage-number expression-ref) :base_type (or (:base-type opts) @@ -208,11 +215,15 @@ (mu/defn expressions :- [:sequential lib.metadata/ColumnMetadata] "Get metadata about the expressions in a given stage of a `query`." - [query :- ::lib.schema/query - stage-number :- :int] - (for [[expression-name expression-definition] (:expressions (lib.util/query-stage query stage-number))] - (let [metadata (lib.metadata.calculation/metadata query stage-number expression-definition)] - (merge - metadata - {:field_ref [:expression {:lib/uuid (str (random-uuid)), :base-type (:base_type metadata)} expression-name] - :source :expressions})))) + ([query] + (expressions query -1)) + + ([query :- ::lib.schema/query + stage-number :- :int] + (for [[expression-name expression-definition] (:expressions (lib.util/query-stage query stage-number))] + (let [metadata (lib.metadata.calculation/metadata query stage-number expression-definition)] + (merge + metadata + {:lib/source :source/expressions + :name expression-name + :display_name expression-name}))))) diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc index a6acae705c312c70f898160427d9f2b57799d0c1..dbd505dcaebc9e4169be817e123e52caccef6a67 100644 --- a/src/metabase/lib/field.cljc +++ b/src/metabase/lib/field.cljc @@ -1,12 +1,12 @@ (ns metabase.lib.field (:require - [metabase.lib.convert :as lib.convert] - [metabase.lib.dispatch :as lib.dispatch] + [metabase.lib.aggregation :as lib.aggregation] + [metabase.lib.expression :as lib.expression] [metabase.lib.join :as lib.join] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.normalize :as lib.normalize] - [metabase.lib.options :as lib.options] + [metabase.lib.ref :as lib.ref] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.id :as lib.schema.id] @@ -81,24 +81,29 @@ (update metadata :name (fn [field-name] (str parent-name \. field-name))))) -(defmethod lib.metadata.calculation/metadata :metadata/field +(defmethod lib.metadata.calculation/metadata-method :metadata/field [_query _stage-number field-metadata] field-metadata) ;;; TODO -- base type should be affected by `temporal-unit`, right? -(defmethod lib.metadata.calculation/metadata :field - [query stage-number [_tag {:keys [base-type temporal-unit], :as opts} :as field-ref]] +(defmethod lib.metadata.calculation/metadata-method :field + [query stage-number [_tag {:keys [source-field effective-type base-type temporal-unit join-alias], :as opts} :as field-ref]] (let [field-metadata (resolve-field-metadata query stage-number field-ref) metadata (merge - {:lib/type :metadata/field - :field_ref field-ref} + {:lib/type :metadata/field} field-metadata {:display_name (or (:display-name opts) (lib.metadata.calculation/display-name query stage-number field-ref))} + (when effective-type + {:effective_type effective-type}) (when base-type {:base_type base-type}) (when temporal-unit - {:unit temporal-unit}))] + {::temporal-unit temporal-unit}) + (when join-alias + {::join-alias join-alias}) + (when source-field + {:fk_field_id source-field}))] (cond->> metadata (:parent_id metadata) (add-parent-column-metadata query)))) @@ -131,47 +136,67 @@ ;; mostly for the benefit of JS, which does not enforce the Malli schemas. (i18n/tru "[Unknown Field]"))) -(defmulti ^:private ->field - {:arglists '([query stage-number field])} - (fn [_query _stage-number field] - (lib.dispatch/dispatch-value field))) +(defmethod lib.temporal-bucket/current-temporal-bucket-method :field + [[_tag opts _id-or-name]] + (:temporal-unit opts)) -(defmethod ->field :field - [_query _stage-number field-clause] - field-clause) +(defmethod lib.temporal-bucket/current-temporal-bucket-method :metadata/field + [metadata] + (::temporal-unit metadata)) -(defmethod ->field :metadata/field - [_query _stage-number {base-type :base_type, field-id :id, field-name :name, field-ref :field_ref, :as _field-metadata}] - (cond-> (or (when field-ref - (lib.convert/->pMBQL field-ref)) - [:field {} (or field-id - field-name)]) - base-type (lib.options/update-options assoc :base-type base-type) - true lib.options/ensure-uuid)) - -(defmethod ->field :dispatch-type/integer - [query _stage-number field-id] - (lib.metadata/field query field-id)) +(defmethod lib.temporal-bucket/temporal-bucket-method :field + [[_tag options id-or-name] unit] + (if unit + [:field (assoc options :temporal-unit unit) id-or-name] + [:field (dissoc options :temporal-unit) id-or-name])) -;;; Pass in a function that takes `query` and `stage-number` to support ad-hoc usage in tests etc -(defmethod ->field :dispatch-type/fn - [query stage-number f] - (f query stage-number)) +(defmethod lib.temporal-bucket/temporal-bucket-method :metadata/field + [metadata unit] + (assoc metadata ::temporal-unit unit)) -(defmethod lib.temporal-bucket/temporal-bucket* :field - [[_field options id-or-name] unit] - [:field (assoc options :temporal-unit unit) id-or-name]) +(defmethod lib.join/current-join-alias-method :field + [[_tag opts]] + (get opts :join-alias)) -(mu/defn field :- :mbql.clause/field - "Create a `:field` clause." - ([query x] - (->field query -1 x)) - ([query stage-number x] - (->field query stage-number x))) +(defmethod lib.join/current-join-alias-method :metadata/field + [metadata] + (::join-alias metadata)) (defmethod lib.join/with-join-alias-method :field - [field-ref join-alias] - (lib.options/update-options field-ref assoc :join-alias join-alias)) + [[_tag opts id-or-name] join-alias] + (if join-alias + [:field (assoc opts :join-alias join-alias) id-or-name] + [:field (dissoc opts :join-alias) id-or-name])) + +(defmethod lib.join/with-join-alias-method :metadata/field + [metadata join-alias] + (assoc metadata ::join-alias join-alias)) + +(defmethod lib.ref/ref-method :field + [field-clause] + field-clause) + +(defmethod lib.ref/ref-method :metadata/field + [metadata] + (case (:lib/source metadata) + :source/aggregation (lib.aggregation/column-metadata->aggregation-ref metadata) + :source/expressions (lib.expression/column-metadata->expression-ref metadata) + (let [options (merge + {:lib/uuid (str (random-uuid)) + :base-type (:base_type metadata) + :effective-type ((some-fn :effective_type :base_type) metadata)} + (when-let [join-alias (::join-alias metadata)] + {:join-alias join-alias}) + (when-let [temporal-unit (::temporal-unit metadata)] + {:temporal-unit temporal-unit}) + (when-let [source-field-id (:fk_field_id metadata)] + {:source-field source-field-id}) + ;; TODO -- binning options. + ) + always-use-name? (#{:source/card :source/native :source/previous-stage} (:lib/source metadata))] + [:field options (if always-use-name? + (:name metadata) + (or (:id metadata) (:name metadata)))]))) (defn fields "Specify the `:fields` for a query." @@ -183,6 +208,9 @@ (fields query -1 xs)) ([query stage-number xs] - (let [xs (mapv #(->field query stage-number %) + (let [xs (mapv (fn [x] + (lib.ref/ref (if (fn? x) + (x query stage-number) + x))) xs)] (lib.util/update-query-stage query stage-number assoc :fields xs)))) diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc index ed067364cc8db53180f46ee68b6eca5df08418af..330871592f8e9dad2913999f42aa264645dbf8a6 100644 --- a/src/metabase/lib/join.cljc +++ b/src/metabase/lib/join.cljc @@ -13,6 +13,42 @@ [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) +(defmulti with-join-alias-method + "Implementation for [[with-join-alias]]." + {:arglists '([x join-alias])} + (fn [x _join-alias] + (lib.dispatch/dispatch-value x))) + +(defmethod with-join-alias-method :dispatch-type/fn + [f join-alias] + (fn [query stage-number] + (let [x (f query stage-number)] + (with-join-alias-method x join-alias)))) + +(defmethod with-join-alias-method :mbql/join + [join join-alias] + (assoc join :alias join-alias)) + +(mu/defn with-join-alias + "Add a specific `join-alias` to something `x`, either a `:field` or join map. Does not recursively update other + references (yet; we can add this in the future)." + [x join-alias :- ::lib.schema.common/non-blank-string] + (with-join-alias-method x join-alias)) + +(defmulti current-join-alias-method + "Impl for [[current-join-alias]]." + {:arglists '([x])} + lib.dispatch/dispatch-value) + +(defmethod current-join-alias-method :default + [_x] + nil) + +(mu/defn current-join-alias :- [:maybe ::lib.schema.common/non-blank-string] + "Get the current join alias associated with something, if it has one." + [x] + (current-join-alias-method x)) + (mu/defn resolve-join :- ::lib.schema.join/join "Resolve a join with a specific `join-alias`." [query :- ::lib.schema/query @@ -35,21 +71,22 @@ (i18n/tru "Saved Question #{0}" card-id-str))) (i18n/tru "Native Query"))) -(mu/defn ^:private column-from-join-fields :- lib.metadata/ColumnMetadata +(mu/defn ^:private column-from-join-fields :- lib.metadata.calculation/ColumnMetadataWithSource "For a column that comes from a join `:fields` list, add or update metadata as needed, e.g. include join name in the display name." [query :- ::lib.schema/query stage-number :- :int column-metadata :- lib.metadata/ColumnMetadata join-alias :- ::lib.schema.common/non-blank-string] - (let [[ref-type options arg] (:field_ref column-metadata) - ref-with-join-alias [ref-type (assoc options :join-alias join-alias) arg] - column-metadata (assoc column-metadata :source_alias join-alias)] - (assoc column-metadata - :field_ref ref-with-join-alias - :display_name (lib.metadata.calculation/display-name query stage-number column-metadata)))) - -(defmethod lib.metadata.calculation/metadata :mbql/join + (let [column-metadata (assoc column-metadata :source_alias join-alias) + col (-> (assoc column-metadata + :display_name (lib.metadata.calculation/display-name query stage-number column-metadata) + :lib/source :source/fields) + (with-join-alias join-alias))] + (assert (= (current-join-alias col) join-alias)) + col)) + +(defmethod lib.metadata.calculation/metadata-method :mbql/join [query stage-number {:keys [fields stages], join-alias :alias, :or {fields :none}, :as _join}] (when-not (= fields :none) (let [field-metadatas (if (= fields :all) @@ -149,28 +186,6 @@ (cond-> (join-clause query stage-number x) condition (assoc :condition (join-condition query stage-number condition))))) -(defmulti with-join-alias-method - "Implementation for [[with-join-alias]]." - {:arglists '([x join-alias])} - (fn [x _join-alias] - (lib.dispatch/dispatch-value x))) - -(mu/defn with-join-alias - "Add a specific `join-alias` to something `x`, either a `:field` or join map. Does not recursively update other - references (yet; we can add this in the future)." - [x join-alias :- ::lib.schema.common/non-blank-string] - (with-join-alias-method x join-alias)) - -(defmethod with-join-alias-method :dispatch-type/fn - [f join-alias] - (fn [query stage-number] - (let [x (f query stage-number)] - (with-join-alias-method x join-alias)))) - -(defmethod with-join-alias-method :mbql/join - [join join-alias] - (assoc join :alias join-alias)) - (mu/defn with-join-fields "Update a join (or a function that will return a join) to include `:fields`, either `:all`, `:none`, or a sequence of references." diff --git a/src/metabase/lib/metadata.cljc b/src/metabase/lib/metadata.cljc index 72c3429ef1ec0e34f83bbed6cd5d9c7c56b6c229..8c429db1b5bbb42e012cb4864787989acc9c653e 100644 --- a/src/metabase/lib/metadata.cljc +++ b/src/metabase/lib/metadata.cljc @@ -5,7 +5,8 @@ [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.util :as lib.util] - [metabase.util.malli :as mu])) + [metabase.util.malli :as mu] + [metabase.util.malli.registry :as mr])) ;;; Column vs Field? ;;; @@ -28,6 +29,27 @@ ;;; will this affect what MLv2 needs to know or does? Not clear at this point, but we'll probably want to abstract ;;; away dealing with Dimensions in the future so the FE QB GUI doesn't need to special case them. +(mr/def ::column-source + [:enum + ;; these are for things from some sort of source other than the current stage; + ;; they must be referenced with string names rather than Field IDs + :source/card + :source/native + :source/previous-stage + ;; these are for things that were introduced by the current stage of the query; `:field` references should be + ;; referenced with Field IDs if available. + ;; + ;; default columns returned by the `:source-table` for the current stage. + :source/table-defaults + ;; specifically introduced by the corresponding top-level clauses. + :source/fields + :source/aggregations + :source/breakouts + ;; introduced by a join, not necessarily ultimately returned. + :source/joins + ;; Introduced by `:expressions`; not necessarily ultimately returned. + :source/expressions]) + (def ColumnMetadata "Malli schema for a valid map of column metadata, which can mean one of two things: @@ -42,10 +64,25 @@ that they are largely compatible. So they're the same for now. We can revisit this in the future if we actually want to differentiate between the two versions." [:map - [:lib/type [:= :metadata/field]] ; TODO -- should this be changed to `:metadata/column`? - [:id {:optional true} ::lib.schema.id/field] - [:name ::lib.schema.common/non-blank-string] - [:display_name {:optional true} [:maybe ::lib.schema.common/non-blank-string]]]) + [:lib/type [:= :metadata/field]] ; TODO -- should this be changed to `:metadata/column`? + [:name ::lib.schema.common/non-blank-string] + ;; TODO -- ignore `base_type` and make `effective_type` required; see #29707 + [:base_type ::lib.schema.common/base-type] + [:id {:optional true} ::lib.schema.id/field] + [:display_name {:optional true} [:maybe ::lib.schema.common/non-blank-string]] + [:effective_type {:optional true} [:maybe ::lib.schema.common/base-type]] + ;; if this is a field from another table (implicit join), this is the field in the current table that should be + ;; used to perform the implicit join. e.g. if current table is `VENUES` and this field is `CATEGORIES.ID`, then the + ;; `fk_field_id` would be `VENUES.CATEGORY_ID`. In a `:field` reference this is saved in the options map as + ;; `:source-field`. + [:fk_field_id {:optional true} [:maybe ::lib.schema.id/field]] + ;; Join alias of the table we're joining against, if any. Not really 100% clear why we would need this on top + ;; of [[metabase.lib.join/current-join-alias]], which stores the same info under a namespaced key. I think we can + ;; remove it. + [:source_alias {:optional true} [:maybe ::lib.schema.common/non-blank-string]] + ;; what top-level clause in the query this metadata originated from, if it is calculated (i.e., if this metadata + ;; was generated by [[metabase.lib.metadata.calculation/metadata]]) + [:lib/source {:optional true} [:ref ::column-source]]]) (def ^:private CardMetadata [:map diff --git a/src/metabase/lib/metadata/calculation.cljc b/src/metabase/lib/metadata/calculation.cljc index 70e1d8daaf97022000ae658859c85a1ea5cca007..476360ec23a5a8c96c58865f9c6d58c1bfb8506a 100644 --- a/src/metabase/lib/metadata/calculation.cljc +++ b/src/metabase/lib/metadata/calculation.cljc @@ -2,6 +2,7 @@ (:require [clojure.string :as str] [metabase.lib.dispatch :as lib.dispatch] + [metabase.lib.metadata :as lib.metadata] [metabase.lib.options :as lib.options] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.common :as lib.schema.common] @@ -108,22 +109,41 @@ top-level-key :- TopLevelKey] (describe-top-level-key-method query stage-number (keyword top-level-key)))) -(defmulti metadata - "Calculate appropriate metadata for something. What this looks like depends on what we're calculating metadata for. If - it's a reference or expression of some sort, this should return a single `:metadata/field` map (i.e., something - satisfying the [[metabase.lib.metadata/ColumnMetadata]] schema. If it's something like a stage of a query or a join - definition, it should return a sequence of metadata maps for all the columns 'returned' at that stage of the query." +(defmulti metadata-method + "Impl for [[metadata]]." {:arglists '([query stage-number x])} (fn [_query _stage-number x] (lib.dispatch/dispatch-value x))) -(defmethod metadata :default +(defmethod metadata-method :default [query stage-number x] {:lib/type :metadata/field :base_type (lib.schema.expresssion/type-of x) :name (column-name query stage-number x) :display_name (display-name query stage-number x)}) +(def ColumnMetadataWithSource + "Schema for the column metadata that should be returned by [[metadata]]." + [:merge + lib.metadata/ColumnMetadata + [:map + [:lib/source ::lib.metadata/column-source]]]) + +(mu/defn metadata :- [:or + lib.metadata/ColumnMetadata + [:sequential ColumnMetadataWithSource]] + "Calculate appropriate metadata for something. What this looks like depends on what we're calculating metadata for. + If it's a reference or expression of some sort, this should return a single `:metadata/field` map (i.e., something + satisfying the [[metabase.lib.metadata/ColumnMetadata]] schema. If it's something like a stage of a query or a join + definition, it should return a sequence of metadata maps for all the columns 'returned' at that stage of the query, + and include the `:lib/source` of where they came from." + ([query] + (metadata query -1 query)) + ([query x] + (metadata query -1 x)) + ([query stage-number x] + (metadata-method query stage-number x))) + (mu/defn describe-query :- ::lib.schema.common/non-blank-string "Convenience for calling [[display-name]] on a query to describe the results of its final stage." [query] diff --git a/src/metabase/lib/normalize.cljc b/src/metabase/lib/normalize.cljc index e292b8d429ab747b44c9f8ae209b31fbd6d1ee41..b0e17e42088900dbf8cd39d092456be5472282ff 100644 --- a/src/metabase/lib/normalize.cljc +++ b/src/metabase/lib/normalize.cljc @@ -35,9 +35,11 @@ "Default normalization functions keys when doing map normalization." {:base-type keyword :type keyword + ;; we can calculate `:field_ref` now using [[metabase.lib.ref/ref]]; `:field_ref` is wrong half of the time anyway, + ;; so ignore it. + :field_ref (constantly ::do-not-use-me) :lib/type keyword - :lib/options normalize - :field_ref normalize}) + :lib/options normalize}) (defn normalize-map "[[normalize]] a map using `key-fn` (default [[clojure.core/keyword]]) for keys and diff --git a/src/metabase/lib/options.cljc b/src/metabase/lib/options.cljc index 484a07f12ca0f9b5f8204ac77b2dd72d33bd50df..30ecf1c1c58f0a051487022f5185bcbed1bc6b33 100644 --- a/src/metabase/lib/options.cljc +++ b/src/metabase/lib/options.cljc @@ -3,6 +3,8 @@ [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) +;;; TODO -- not 100% sure we actually need all of this stuff anymore. + (defn- mbql-clause? [x] (and (vector? x) (keyword? (first x)))) diff --git a/src/metabase/lib/order_by.cljc b/src/metabase/lib/order_by.cljc index 29fdc7c93c835abe6533ebef810ed3fdfdb3bac1..d65decbde0eb42048fd5ffba2640e486a8199077 100644 --- a/src/metabase/lib/order_by.cljc +++ b/src/metabase/lib/order_by.cljc @@ -3,10 +3,10 @@ [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.breakout :as lib.breakout] [metabase.lib.dispatch :as lib.dispatch] - [metabase.lib.field :as lib.field] [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.schema :as lib.schema] [metabase.lib.schema.expression :as lib.schema.expression] [metabase.lib.schema.order-by :as lib.schema.order-by] @@ -45,11 +45,14 @@ [_query _stage-number clause] (lib.options/ensure-uuid clause)) -;;; by default, try to convert `x` to a Field clause and then order by `:asc` +(defmethod ->order-by-clause :dispatch-type/fn + [query stage-number f] + (->order-by-clause query stage-number (f query stage-number))) + +;;; by default, try to convert `x` to a ref and then order by `:asc` (defmethod ->order-by-clause :default - [query stage-number x] - (let [field-clause (lib.field/field query stage-number x)] - (lib.options/ensure-uuid [:asc field-clause]))) + [_query _stage-number x] + (lib.options/ensure-uuid [:asc (lib.ref/ref x)])) (defn order-by-clause "Create an order-by clause independently of a query, e.g. for `replace` or whatever." diff --git a/src/metabase/lib/query.cljc b/src/metabase/lib/query.cljc index f4eda4cd44d6b1b4e1858514d59b7b804f66324f..9604659e98137383d3bb3d009b0535c634e84162 100644 --- a/src/metabase/lib/query.cljc +++ b/src/metabase/lib/query.cljc @@ -60,7 +60,7 @@ {:type keyword :stages (partial mapv lib.normalize/normalize)})) -(defmethod lib.metadata.calculation/metadata :mbql/query +(defmethod lib.metadata.calculation/metadata-method :mbql/query [query stage-number x] (lib.metadata.calculation/metadata query stage-number (lib.util/query-stage x stage-number))) diff --git a/src/metabase/lib/ref.cljc b/src/metabase/lib/ref.cljc new file mode 100644 index 0000000000000000000000000000000000000000..e3960a72c7003b9895ab8ddd91e3801b3b77545b --- /dev/null +++ b/src/metabase/lib/ref.cljc @@ -0,0 +1,18 @@ +(ns metabase.lib.ref + (:refer-clojure :exclude [ref]) + (:require + [metabase.lib.dispatch :as lib.dispatch] + [metabase.lib.schema.ref :as lib.schema.ref] + [metabase.util.malli :as mu])) + +(defmulti ref-method + "Impl for [[ref]]. This should create a new ref every time it is called, i.e. it should have a fresh UUID every time + you call it." + {:arglists '([x])} + lib.dispatch/dispatch-value) + +(mu/defn ref :- ::lib.schema.ref/ref + "Create a fresh ref that can be added to a query, e.g. a `:field`, `:aggregation`, or `:expression` reference. Will + create a new UUID every time this is called." + ([x] + (ref-method x))) diff --git a/src/metabase/lib/schema/ref.cljc b/src/metabase/lib/schema/ref.cljc index 7bcde520d51ce18cd1c91d9d81ee4e07c1366d9f..fe59398578793243dde0249928bee24a1fc5a995 100644 --- a/src/metabase/lib/schema/ref.cljc +++ b/src/metabase/lib/schema/ref.cljc @@ -50,7 +50,7 @@ (defmethod expression/type-of* :field [[_tag opts _id-or-name]] - (or (:base-type opts) + (or ((some-fn :effective-type :base-type) opts) ::expression/type.unknown)) (mbql-clause/define-tuple-mbql-clause :expression @@ -58,7 +58,7 @@ (defmethod expression/type-of* :expression [[_tag opts _expression-name]] - (or (:base-type opts) + (or ((some-fn :effective-type :base-type) opts) ::expression/type.unknown)) (mr/def ::aggregation-options @@ -76,7 +76,7 @@ (defmethod expression/type-of* :aggregation [[_tag opts _index]] - (or (:base-type opts) + (or ((some-fn :effective-type :base-type) opts) ::expression/type.unknown)) (mr/def ::ref diff --git a/src/metabase/lib/stage.cljc b/src/metabase/lib/stage.cljc index 8a7e3ed7801ce597ccfe5742f3bb9ce029cf3adc..5c0b2cf0b22ac2aed647ee8b93a28edb78d8e151 100644 --- a/src/metabase/lib/stage.cljc +++ b/src/metabase/lib/stage.cljc @@ -30,13 +30,83 @@ {:aggregation (partial mapv lib.normalize/normalize) :filter lib.normalize/normalize})) -(mu/defn ^:private fields-columns :- [:maybe [:sequential lib.metadata/ColumnMetadata]] +(mu/defn ^:private ensure-previous-stages-have-metadata :- ::lib.schema/query + "Recursively calculate the metadata for the previous stages and add it to them, we'll need it for metadata + calculations for `stage-number` and we don't want to have to calculate it more than once..." + [query :- ::lib.schema/query + stage-number :- :int] + (let [previous-stage-number (lib.util/previous-stage-number query stage-number)] + (cond-> query + previous-stage-number + (lib.util/update-query-stage previous-stage-number + assoc + ::cached-metadata + {:lib/type :metadata/results + :columns (stage-metadata query previous-stage-number)})))) + +(def ^:private StageMetadataColumns + [:sequential {:min 1} lib.metadata.calculation/ColumnMetadataWithSource]) + +(def ^:private DistinctStageMetadataColumns + [:and + StageMetadataColumns + [:fn + ;; should be dev-facing only, so don't need to i18n + {:error/message "Column :names must be distinct!" + :error/fn (fn [{:keys [value]} _] + (str "Column :names must be distinct, got: " (pr-str (mapv :name value))))} + (fn [columns] + (apply distinct? (map :name columns)))]]) + +(mu/defn ^:private existing-stage-metadata :- [:maybe StageMetadataColumns] + "Return existing stage metadata attached to a stage if is already present: return it as-is, but only if this is a + native stage or a source-Card stage. if it's any other sort of stage then ignore the metadata, it's probably wrong; + we can recalculate the correct metadata anyway." + [query :- ::lib.schema/query + stage-number :- :int] + (let [{stage-type :lib/type, :keys [source-table] :as stage} (lib.util/query-stage query stage-number)] + (or (::cached-metadata stage) + (when-let [metadata (:lib/stage-metadata stage)] + (when (or (= stage-type :mbql.stage/native) + (and (string? source-table) + (str/starts-with? source-table "card__"))) + (let [source-type (case stage-type + :mbql.stage/native :source/native + :mbql.stage/mbql :source/card)] + (for [col (:columns metadata)] + (assoc col :lib/source source-type)))))))) + +(mu/defn ^:private breakouts-columns :- [:maybe StageMetadataColumns] + [query :- ::lib.schema/query + stage-number :- :int] + (not-empty + (for [breakout (lib.breakout/breakouts query stage-number)] + (assoc breakout :lib/source :source/breakouts)))) + +(mu/defn ^:private aggregations-columns :- [:maybe StageMetadataColumns] + [query :- ::lib.schema/query + stage-number :- :int] + (not-empty + (for [ag (lib.aggregation/aggregations query stage-number)] + (assoc ag :lib/source :source/aggregations)))) + +(mu/defn ^:private fields-columns :- [:maybe StageMetadataColumns] [query :- ::lib.schema/query stage-number :- :int] (when-let [{fields :fields} (lib.util/query-stage query stage-number)] - (mapv (fn [ref-clause] - (assoc (lib.metadata.calculation/metadata query stage-number ref-clause) :source :fields)) - fields))) + (not-empty + (for [field-ref fields] + (assoc (lib.metadata.calculation/metadata query stage-number field-ref) :lib/source :source/fields))))) + +(mu/defn ^:private breakout-ags-fields-columns :- [:maybe StageMetadataColumns] + [query :- ::lib.schema/query + stage-number :- :int] + (not-empty + (into [] + cat + [(breakouts-columns query stage-number) + (aggregations-columns query stage-number) + (fields-columns query stage-number)]))) (defn- remove-hidden-default-fields "Remove Fields that shouldn't be visible from the default Fields for a source Table. @@ -54,15 +124,7 @@ [(or position 0) (u/lower-case-en (or field-name ""))]) field-metadatas)) -(defn- ensure-field-refs [field-metadatas] - (for [field-metadata field-metadatas] - (cond-> field-metadata - (not (:field_ref field-metadata)) - (assoc :field_ref [:field - {:lib/uuid (str (random-uuid)), :base-type (:base_type field-metadata)} - ((some-fn :id :name) field-metadata)])))) - -(mu/defn ^:private source-table-default-fields :- [:maybe [:sequential lib.metadata/ColumnMetadata]] +(mu/defn ^:private source-table-default-fields :- [:maybe StageMetadataColumns] "Determine the Fields we'd normally return for a source Table. See [[metabase.query-processor.middleware.add-implicit-clauses/add-implicit-fields]]." [query :- ::lib.schema/query @@ -71,7 +133,8 @@ (->> field-metadatas remove-hidden-default-fields sort-default-fields - ensure-field-refs))) + (map (fn [col] + (assoc col :lib/source :source/table-defaults)))))) (mu/defn ^:private default-join-alias :- ::lib.schema.common/non-blank-string "Generate an alias for a join that doesn't already have one." @@ -99,13 +162,14 @@ (not (:alias join)) (assoc :alias (unique-name-generator (default-join-alias query stage-number join))))) joins))) -(mu/defn ^:private default-columns-added-by-join :- [:maybe [:sequential lib.metadata/ColumnMetadata]] +(mu/defn ^:private default-columns-added-by-join :- [:maybe StageMetadataColumns] [query :- ::lib.schema/query stage-number :- :int join :- ::lib.schema.join/join] - (lib.metadata.calculation/metadata query stage-number join)) + (for [col (lib.metadata.calculation/metadata query stage-number join)] + (assoc col :lib/source :source/joins))) -(mu/defn ^:private default-columns-added-by-joins :- [:maybe [:sequential lib.metadata/ColumnMetadata]] +(mu/defn ^:private default-columns-added-by-joins :- [:maybe StageMetadataColumns] [query :- ::lib.schema/query stage-number :- :int] (when-let [joins (not-empty (:joins (lib.util/query-stage query stage-number)))] @@ -114,7 +178,19 @@ (mapcat (partial default-columns-added-by-join query stage-number)) (ensure-all-joins-have-aliases query stage-number joins))))) -(mu/defn ^:private default-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn ^:private saved-question-metadata :- [:maybe StageMetadataColumns] + "Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string." + [query :- ::lib.schema/query + source-table-id] + (when (string? source-table-id) + (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" source-table-id)] + (when-let [card-id (parse-long card-id-str)] + (when-let [result-metadata (:result_metadata (lib.metadata/card query card-id))] + (when-let [cols (not-empty (:columns result-metadata))] + (for [col cols] + (assoc col :lib/source :source/card)))))))) + +(mu/defn ^:private default-columns :- StageMetadataColumns "Calculate the columns to return if `:aggregations`/`:breakout`/`:fields` are unspecified. Formula for the so-called 'default' columns is @@ -136,66 +212,46 @@ ;; 1: columns from the previous stage, source table or query (if-let [previous-stage-number (lib.util/previous-stage-number query stage-number)] ;; 1a. columns returned by previous stage - (stage-metadata query previous-stage-number) + (for [col (stage-metadata query previous-stage-number)] + (assoc col :lib/source :source/previous-stage)) ;; 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) (source-table-default-fields query source-table)) - ;; 1c. Metadata associated with a Saved Question, if `:source-table` is a `card__<id>` string, OR - (when (string? source-table) - (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" source-table)] - (when-let [card-id (parse-long card-id-str)] - (when-let [result-metadata (:result_metadata (lib.metadata/card query card-id))] - (not-empty (for [col (:columns result-metadata)] - (assoc col - :field_ref [:field - {:lib/uuid (str (random-uuid)), :base-type (:base_type col)} - (:name col)]))))))) - ;; 1d: `:lib/stage-metadata` for the native query - (:columns (:lib/stage-metadata this-stage))))) + ;; 1c. Metadata associated with a saved Question + (saved-question-metadata query source-table) + ;; 1d: `:lib/stage-metadata` for the (presumably native) query + (for [col (:columns (:lib/stage-metadata this-stage))] + (assoc col :lib/source :source/native))))) ;; 2: columns added by joins at this stage (default-columns-added-by-joins query stage-number))) -(mu/defn ^:private stage-metadata :- [:and - [:sequential {:min 1} lib.metadata/ColumnMetadata] - [:fn - ;; should be dev-facing only, so don't need to i18n - {:error/message "Column :names must be distinct!"} - (fn [columns] - (apply distinct? (map :name columns)))]] +(defn- ensure-distinct-names [metadatas] + (when (seq metadatas) + (let [f (mbql.u/unique-name-generator)] + (for [metadata metadatas] + (update metadata :name f))))) + +(mu/defn ^:private stage-metadata :- DistinctStageMetadataColumns "Return results metadata about the expected columns in an MBQL query stage. If the query has aggregations/breakouts/fields, then return THOSE. Otherwise return the defaults based on the source Table or previous stage + joins." [query :- ::lib.schema/query stage-number :- :int] - (or - ;; stage metadata is already present: return it as-is - (when-let [metadata (:lib/stage-metadata (lib.util/query-stage query stage-number))] - (:columns metadata)) - ;; otherwise recursively calculate the metadata for the previous stages and add it to them, we'll need it for - ;; calculations for this stage and we don't have to calculate it more than once... - (let [query (let [previous-stage-number (lib.util/previous-stage-number query stage-number)] - (cond-> query - previous-stage-number - (lib.util/update-query-stage previous-stage-number - assoc - :lib/stage-metadata - {:lib/type :metadata/results - :columns (stage-metadata query previous-stage-number)})))] - ;; ... then calculate metadata for this stage - (or - (not-empty (into [] - cat - [(lib.breakout/breakouts query stage-number) - (lib.aggregation/aggregations query stage-number) - (fields-columns query stage-number)])) - (default-columns query stage-number))))) + (ensure-distinct-names + (or + (existing-stage-metadata query stage-number) + (let [query (ensure-previous-stages-have-metadata query stage-number)] + ;; ... then calculate metadata for this stage + (or + (breakout-ags-fields-columns query stage-number) + (default-columns query stage-number)))))) (doseq [stage-type [:mbql.stage/mbql :mbql.stage/native]] - (defmethod lib.metadata.calculation/metadata stage-type + (defmethod lib.metadata.calculation/metadata-method stage-type [query stage-number _stage] (stage-metadata query stage-number))) @@ -236,19 +292,15 @@ (m/distinct-by :fk_target_field_id) (map (fn [{source-field-id :id, target-field-id :fk_target_field_id}] (-> (lib.metadata/field query target-field-id) - (assoc :source-field-id source-field-id)))) + (assoc ::source-field-id source-field-id)))) (remove #(contains? existing-table-ids (:table_id %))) (m/distinct-by :table_id) - (mapcat (fn [{table-id :table_id, :keys [source-field-id]}] + (mapcat (fn [{table-id :table_id, ::keys [source-field-id]}] (for [field (source-table-default-fields query table-id)] - (assoc field :field_ref [:field - {:lib/uuid (str (random-uuid)) - :base-type (:base_type field) - :source-field source-field-id} - (:id field)]))))) + (assoc field :fk_field_id source-field-id))))) column-metadatas))) -(mu/defn visible-columns :- [:sequential lib.metadata/ColumnMetadata] +(mu/defn visible-columns :- StageMetadataColumns "Columns that are visible inside a given stage of a query. Ignores `:fields`, `:breakout`, and `:aggregation`. Includes columns that are implicitly joinable from other Tables." [query stage-number] diff --git a/src/metabase/lib/temporal_bucket.cljc b/src/metabase/lib/temporal_bucket.cljc index 0c5231813030b20968604cff4772b301d1c1075d..37f304a705e7a8e4344791fd40f71089342d07a5 100644 --- a/src/metabase/lib/temporal_bucket.cljc +++ b/src/metabase/lib/temporal_bucket.cljc @@ -36,18 +36,18 @@ (i18n/tru "next {0} {1}" (pr-str n) (unit->i18n n unit)) (i18n/tru "last {0} {1}" (pr-str (abs n)) (unit->i18n (abs n) unit))))) -(defmulti temporal-bucket* +(defmulti temporal-bucket-method "Implementation for [[temporal-bucket]]. Implement this to tell [[temporal-bucket]] how to add a bucket to a particular MBQL clause." {:arglists '([x unit])} (fn [x _unit] (lib.dispatch/dispatch-value x))) -(defmethod temporal-bucket* :dispatch-type/fn +(defmethod temporal-bucket-method :dispatch-type/fn [f unit] (fn [query stage-number] (let [x (f query stage-number)] - (temporal-bucket* x unit)))) + (temporal-bucket-method x unit)))) (mu/defn temporal-bucket "Add a temporal bucketing unit, e.g. `:day` or `:day-of-year`, to an MBQL clause or something that can be converted to @@ -59,4 +59,33 @@ [:field 1 {:temporal-unit :day}]" [x unit :- ::lib.schema.temporal-bucketing/unit] - (temporal-bucket* x unit)) + (temporal-bucket-method x unit)) + +(defmulti current-temporal-bucket-method + "Implementation of [[current-temporal-bucket]]. Return the current temporal bucketing unit associated with `x`." + {:arglists '([x])} + lib.dispatch/dispatch-value) + +(defmethod current-temporal-bucket-method :default + [_x] + nil) + +(mu/defn current-temporal-bucket :- [:maybe ::lib.schema.temporal-bucketing/unit] + "Get the current temporal bucketing unit associated with something, if any." + [x] + (current-temporal-bucket-method x)) + +(defmulti available-temporal-buckets-method + "Implementation for [[available-temporal-buckets]]. Return a set of units from + `:metabase.lib.schema.temporal-bucketing/unit` that are allowed to be used with `x`." + {:arglists '([x])} + lib.dispatch/dispatch-value) + +(defmethod available-temporal-buckets-method :default + [_x] + nil) + +(mu/defn available-temporal-buckets :- [:maybe [:set {:min 1} [:ref ::lib.schema.temporal-bucketing/unit]]] + "Get a set of available temporal bucketing units for `x`. Returns nil if no units are available." + [x] + (not-empty (temporal-bucket-method x))) diff --git a/src/metabase/lib/util.cljc b/src/metabase/lib/util.cljc index dbde3000c4b2e3cc7222c09e0d40aea984ede369..40804e83c15d7d06da0f809cce2e996f3504961a 100644 --- a/src/metabase/lib/util.cljc +++ b/src/metabase/lib/util.cljc @@ -10,7 +10,6 @@ [clojure.string :as str] [metabase.lib.options :as lib.options] [metabase.lib.schema :as lib.schema] - [metabase.lib.schema.common :as lib.schema.common] [metabase.shared.util.i18n :as i18n] [metabase.util.malli :as mu])) @@ -148,7 +147,7 @@ :native (native-query->pipeline query) :query (mbql-query->pipeline query))) -(mu/defn ^:private non-negative-stage-index :- ::lib.schema.common/int-greater-than-or-equal-to-zero +(mu/defn ^:private non-negative-stage-index :- [:int {:min 0}] "If `stage-number` index is a negative number e.g. `-1` convert it to a positive index so we can use `nth` on `stages`. `-1` = the last stage, `-2` = the penultimate stage, etc." [stages :- [:sequential [:ref ::lib.schema/stage]] @@ -162,12 +161,11 @@ {:num-stages (count stages)}))) stage-number')) -(defn previous-stage-number +(mu/defn previous-stage-number :- [:maybe [:int {:min 0}]] "The index of the previous stage, if there is one. `nil` if there is no previous stage." - [{:keys [stages], :as _query} stage-number] - (let [stage-number (if (neg? stage-number) - (+ (count stages) stage-number) - stage-number)] + [{:keys [stages], :as _query} :- :map + stage-number :- :int] + (let [stage-number (non-negative-stage-index stages stage-number)] (when (pos? stage-number) (dec stage-number)))) diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc index 61efb32b310dff4d89c5ac4f2ed6e34b4d19d952..b9c313aa026cde7d97348930661b7f5c31b746e1 100644 --- a/test/metabase/lib/aggregation_test.cljc +++ b/test/metabase/lib/aggregation_test.cljc @@ -3,7 +3,6 @@ [clojure.test :refer [are deftest is testing]] [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib] - [metabase.lib.field :as lib.field] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.query :as lib.query] @@ -193,5 +192,5 @@ (is (=? result-query (-> q (lib/aggregate {:operator :sum - :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]}) + :args [(lib/ref (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]}) (dissoc :lib/metadata))))))) diff --git a/test/metabase/lib/dev_test.cljc b/test/metabase/lib/dev_test.cljc index 28819f72f887868310897ca2d60b110570ab7cda..a851e7ab82dc4b3b9a7b3cba6ba27d8f81738bbb 100644 --- a/test/metabase/lib/dev_test.cljc +++ b/test/metabase/lib/dev_test.cljc @@ -2,9 +2,6 @@ (:require [clojure.test :refer [are deftest is]] [metabase.lib.core :as lib] - [metabase.lib.field :as lib.field] - [metabase.lib.metadata :as lib.metadata] - [metabase.lib.query :as lib.query] [metabase.lib.test-metadata :as meta] #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) @@ -20,17 +17,6 @@ (is (=? [:field {:lib/uuid string?} (meta/id :venues :id)] (f {:lib/type :mbql/query, :lib/metadata meta/metadata-provider} -1))))) -(deftest ^:parallel field-from-results-metadata-test - (let [field-metadata (lib.metadata/stage-column (lib.query/saved-question-query - meta/metadata-provider - meta/saved-question) - "ID")] - (is (=? {:lib/type :metadata/field - :name "ID"} - field-metadata)) - (is (=? [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"] - (#'lib.field/->field {} -1 field-metadata))))) - (deftest ^:parallel query-for-table-name-test (is (=? {:lib/type :mbql/query :database (meta/id) diff --git a/test/metabase/lib/expression_test.cljc b/test/metabase/lib/expression_test.cljc index 17fb916f3c6314f194da5319324ae200563843bb..954a18afc499651f557f6d0c9266bad10f58e7fb 100644 --- a/test/metabase/lib/expression_test.cljc +++ b/test/metabase/lib/expression_test.cljc @@ -90,7 +90,7 @@ (is (=? {:base_type :type/Integer :name "double-price" :display_name "double-price" - :field_ref [:expression {:lib/uuid string?} "double-price"]} + :lib/source :source/expressions} (lib.metadata.calculation/metadata (lib.tu/venues-query-with-last-stage {:expressions {"double-price" [:* @@ -110,8 +110,7 @@ (is (=? [{:name "prev_month" :display_name "prev_month" :base_type :type/DateTime - :source :fields - :field_ref [:expression {:lib/uuid string?} "prev_month"]}] + :lib/source :source/fields}] (lib.metadata.calculation/metadata query -1 query))))) (deftest ^:parallel date-interval-names-test @@ -165,7 +164,6 @@ (testing "Uses the first clause" (testing "Gets the type information from the field" (is (=? {:name "expr" - :field_ref [:expression {} "expr"] :display_name "expr" :base_type :type/Text} (infer-first [:coalesce @@ -178,8 +176,7 @@ (testing "Gets the type information from the literal" (is (=? {:base_type :type/Text :name "expr" - :display_name "expr" - :field_ref [:expression {} "expr"]} + :display_name "expr"} (infer-first [:coalesce {:lib/uuid (str (random-uuid))} "bar" (lib.tu/field-clause :venues :name)]))))))) (deftest ^:parallel infer-case-test @@ -187,7 +184,6 @@ (testing "Uses first available type information" (testing "From a field" (is (=? {:name "expr" - :field_ref [:expression {} "expr"] :display_name "expr" :base_type :type/Text} (infer-first [:coalesce @@ -203,7 +199,7 @@ (is (=? {:base_type :type/DateTime :name "last-login-plus-2" :display_name "last-login-plus-2" - :field_ref [:expression {} "last-login-plus-2"]} + :lib/source :source/expressions} (lib.metadata.calculation/metadata (lib.tu/venues-query-with-last-stage {:expressions {"last-login-plus-2" [:datetime-add @@ -224,3 +220,11 @@ {:expressions {"one-hundred" 100}}) -1 [:expression {:lib/uuid (str (random-uuid))} "double-price"]))))) + +(deftest ^:parallel expressions-names-test + (testing "expressions should include the original expression name" + (is (=? [{:name "expr" + :display_name "expr"}] + (-> (lib/query-for-table-name meta/metadata-provider "VENUES") + (lib/expression "expr" (lib/absolute-datetime "2020" :month)) + lib/expressions))))) diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc index b68486656523ab052ac5573462381315dd56859d..d653d60b88988c6e92ea9c42222510910bcb72fe 100644 --- a/test/metabase/lib/field_test.cljc +++ b/test/metabase/lib/field_test.cljc @@ -2,17 +2,25 @@ (:require [clojure.test :refer [deftest is testing]] [metabase.lib.core :as lib] - [metabase.lib.field :as lib.field] + [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] + [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.test-metadata :as meta] [metabase.lib.test-util :as lib.tu] #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) #?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me)) -(deftest ^:parallel field-from-database-metadata-test - (is (=? [:field {:base-type :type/BigInteger, :lib/uuid string?} (meta/id :venues :id)] - (lib.field/field {:lib/metadata meta/metadata-provider} (meta/field-metadata :venues :id))))) +(deftest ^:parallel field-from-results-metadata-test + (let [field-metadata (lib.metadata/stage-column (lib/saved-question-query + meta/metadata-provider + meta/saved-question) + "ID")] + (is (=? {:lib/type :metadata/field + :name "ID"} + field-metadata)) + (is (=? [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"] + (lib/ref field-metadata))))) (defn- grandparent-parent-child-id [field] (+ (meta/id :venues :id) @@ -23,17 +31,20 @@ (def ^:private grandparent-parent-child-metadata-provider "A MetadataProvider for a Table that nested Fields: grandparent, parent, and child" - (let [grandparent {:lib/type :metadata/field - :name "grandparent" - :id (grandparent-parent-child-id :grandparent)} + (let [grandparent {:lib/type :metadata/field + :name "grandparent" + :id (grandparent-parent-child-id :grandparent) + :base_type :type/Text} parent {:lib/type :metadata/field :name "parent" :parent_id (grandparent-parent-child-id :grandparent) - :id (grandparent-parent-child-id :parent)} + :id (grandparent-parent-child-id :parent) + :base_type :type/Text} child {:lib/type :metadata/field :name "child" :parent_id (grandparent-parent-child-id :parent) - :id (grandparent-parent-child-id :child)}] + :id (grandparent-parent-child-id :child) + :base_type :type/Text}] (lib.tu/mock-metadata-provider {:database meta/metadata :tables [(meta/table-metadata :venues)] @@ -58,7 +69,6 @@ (testing "For fields with parents we should return them with a combined name including parent's name" (is (=? {:table_id (meta/id :venues) :name "grandparent.parent" - :field_ref [:field {} (grandparent-parent-child-id :parent)] :parent_id (grandparent-parent-child-id :grandparent) :id (grandparent-parent-child-id :parent) :visibility_type :normal} @@ -66,7 +76,6 @@ (testing "nested-nested fields should include grandparent name (etc)" (is (=? {:table_id (meta/id :venues) :name "grandparent.parent.child" - :field_ref [:field {} (grandparent-parent-child-id :child)] :parent_id (grandparent-parent-child-id :parent) :id (grandparent-parent-child-id :child) :visibility_type :normal} @@ -77,22 +86,22 @@ (is (=? {:name "sum" :display_name "sum of User ID" :base_type :type/Integer - :field_ref [:field {:base-type :type/Integer} "sum"] :semantic_type :type/FK} (lib.metadata.calculation/metadata (lib.tu/venues-query-with-last-stage - {:lib/stage-metadata - {:lib/type :metadata/results - :columns [{:lib/type :metadata/field - :name "abc" - :display_name "another Field" - :base_type :type/Integer - :semantic_type :type/FK} - {:lib/type :metadata/field - :name "sum" - :display_name "sum of User ID" - :base_type :type/Integer - :semantic_type :type/FK}]}}) + {:lib/type :mbql.stage/native + :lib/stage-metadata {:lib/type :metadata/results + :columns [{:lib/type :metadata/field + :name "abc" + :display_name "another Field" + :base_type :type/Integer + :semantic_type :type/FK} + {:lib/type :metadata/field + :name "sum" + :display_name "sum of User ID" + :base_type :type/Integer + :semantic_type :type/FK}]} + :native "SELECT whatever"}) -1 [:field {:lib/uuid (str (random-uuid)), :base-type :type/Integer} "sum"]))))) @@ -136,7 +145,7 @@ (let [field (f query -1)] (is (=? [:field {:temporal-unit :year} (meta/id :checkins :date)] field)) - (is (=? {:unit :year} - (lib.metadata.calculation/metadata query -1 field))) + (is (=? :year + (lib.temporal-bucket/current-temporal-bucket (lib.metadata.calculation/metadata query -1 field)))) (is (= "Date (year)" (lib.metadata.calculation/display-name query -1 field)))))) diff --git a/test/metabase/lib/filter_test.cljc b/test/metabase/lib/filter_test.cljc index 74a548070c48295164c7cab18917efcf8978505b..1690d3eb53ebaa84b173e2f5cc5f134a1c371553 100644 --- a/test/metabase/lib/filter_test.cljc +++ b/test/metabase/lib/filter_test.cljc @@ -2,7 +2,6 @@ (:require [clojure.test :refer [deftest is testing]] [metabase.lib.core :as lib] - [metabase.lib.field :as lib.field] [metabase.lib.metadata :as lib.metadata] [metabase.lib.test-metadata :as meta] #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) @@ -144,7 +143,7 @@ (is (=? simple-filtered-query (-> q1 (lib/filter {:operator :between - :args [(lib.field/field q1 venues-category-id-metadata) 42 100]}) + :args [(lib/ref venues-category-id-metadata) 42 100]}) (dissoc :lib/metadata))))))) (deftest ^:parallel add-filter-test @@ -190,14 +189,14 @@ (assoc-in simple-query [:stages 0 :filter] first-filter) second-add (lib/add-filter first-add {:operator "starts-with" - :args [(lib.field/field simple-query venues-name-metadata) "prefix"]}) + :args [(lib/ref venues-name-metadata) "prefix"]}) and-query (assoc-in filtered-query [:stages 0 :filter] [:and {:lib/uuid string?} first-filter second-filter]) third-add (lib/add-filter second-add {:operator :contains - :args [(lib.field/field simple-query venues-name-metadata) "part"]}) + :args [(lib/ref venues-name-metadata) "part"]}) extended-and-query (assoc-in filtered-query [:stages 0 :filter] diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc index c88c71f16c6fcf5a46d43d060620d77e86b40b6b..26dcb96586c7f574781345b928ff807d27e973da 100644 --- a/test/metabase/lib/join_test.cljc +++ b/test/metabase/lib/join_test.cljc @@ -1,8 +1,8 @@ (ns metabase.lib.join-test (:require [clojure.test :refer [deftest is testing]] + [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib] - [metabase.lib.field :as lib.field] [metabase.lib.join :as lib.join] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] @@ -46,8 +46,8 @@ (-> q (lib/join (lib/query-for-table-name meta/metadata-provider "CATEGORIES") {:operator := - :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID")) - (lib.field/field q (lib.metadata/field q nil "CATEGORIES" "ID"))]}) + :args [(lib/ref (lib.metadata/field q nil "VENUES" "CATEGORY_ID")) + (lib/ref (lib.metadata/field q nil "CATEGORIES" "ID"))]}) (dissoc :lib/metadata)))))) (deftest ^:parallel join-saved-question-test @@ -104,18 +104,21 @@ (lib/join q2 (lib/= venues-category-id-metadata categories-id-metadata)) (dissoc :lib/metadata))))))) -;;; FIXME -#_(deftest ^:parallel col-info-implicit-join-test +(deftest ^:parallel col-info-implicit-join-test (testing (str "when a `:field` with `:source-field` (implicit join) is used, we should add in `:fk_field_id` " "info about the source Field") - (is (=? [(merge (dissoc (meta/field-metadata :categories :name) :database_type) - {:fk_field_id (meta/id :venues :category-id) - :source :fields - :field_ref [:field {:fk-field-id (meta/id :venues :category-id)} (meta/id :categories :name)]})] - (stage-metadata - {:type :query - :query {:source-table (meta/id :venues) - :fields [[:field (meta/id :categories :name) {:fk-field-id (meta/id :venues :category-id)}]]}}))))) + (let [query (lib/query + meta/metadata-provider + (lib.convert/->pMBQL + {:database (meta/id) + :type :query + :query {:source-table (meta/id :venues) + :fields [[:field (meta/id :categories :name) {:source-field (meta/id :venues :category-id)}]]}}))] + (is (=? [{:name "NAME" + :id (meta/id :categories :name) + :fk_field_id (meta/id :venues :category-id) + :lib/source :source/fields}] + (lib.metadata.calculation/metadata query -1 query)))))) (deftest ^:parallel col-info-explicit-join-test (testing "Display name for a joined field should include a nice name for the join; include other info like :source_alias" @@ -142,10 +145,15 @@ :source-table (meta/id :categories)}]}]}] :database (meta/id) :lib/metadata meta/metadata-provider}] - (is (=? [(merge (meta/field-metadata :categories :name) - {:display_name "Categories → Name" - :source :fields - :field_ref [:field - {:lib/uuid string?, :join-alias "CATEGORIES__via__CATEGORY_ID"} - (meta/id :categories :name)]})] - (lib.metadata.calculation/metadata query -1 query)))))) + (let [metadata (lib.metadata.calculation/metadata query)] + (is (=? [(merge (meta/field-metadata :categories :name) + {:display_name "Categories → Name" + :lib/source :source/fields + :metabase.lib.field/join-alias "CATEGORIES__via__CATEGORY_ID"})] + metadata)) + (is (=? "CATEGORIES__via__CATEGORY_ID" + (lib.join/current-join-alias (first metadata)))) + (is (=? [:field + {:lib/uuid string?, :join-alias "CATEGORIES__via__CATEGORY_ID"} + (meta/id :categories :name)] + (lib/ref (first metadata)))))))) diff --git a/test/metabase/lib/metadata_test.cljc b/test/metabase/lib/metadata_test.cljc index beb930797b0ac2d1b798898b41afcd9f284258d9..5655f62e601576192aa6af72c374f6e29c21f5ee 100644 --- a/test/metabase/lib/metadata_test.cljc +++ b/test/metabase/lib/metadata_test.cljc @@ -34,7 +34,6 @@ (let [query (lib/saved-question-query meta/metadata-provider meta/saved-question)] (are [x] (=? {:lib/type :metadata/field :display_name "CATEGORY_ID" - :field_ref [:field "CATEGORY_ID" {:base-type :type/Integer}] :name "CATEGORY_ID" :base_type :type/Integer :effective_type :type/Integer diff --git a/test/metabase/lib/order_by_test.cljc b/test/metabase/lib/order_by_test.cljc index 0df5e17e90372fecdfbc33e9fa4899ca130ba392..637278d25e871f2b4579e802f4b66206b68ff473 100644 --- a/test/metabase/lib/order_by_test.cljc +++ b/test/metabase/lib/order_by_test.cljc @@ -98,7 +98,7 @@ lib/order-bys)))) ;;; the following tests use raw legacy MBQL because they're direct ports of JavaScript tests from MLv1 and I wanted to -;;; make sure that given an existing query, the expected description was generated correctly. +;;; make sure that given an existing query the expected description was generated correctly. (defn- describe-legacy-query-order-by [query] (-> (lib.query/query meta/metadata-provider (lib.convert/->pMBQL query)) @@ -122,26 +122,8 @@ (is (= "Sorted by Count ascending" (describe-legacy-query-order-by query))))) -(deftest ^:parallel describe-order-by-expression-reference-test - ;; it("should work with expressions", () => { - ;; const query = { - ;; "source-table": PRODUCTS.id, - ;; expressions: { - ;; Foo: ["concat", "Foo ", ["field", 4, null]], - ;; }, - ;; "order-by": [["asc", ["expression", "Foo", null]]], - ;; }; - ;; expect(base_question._getOrderByDescription(PRODUCTS, query)).toEqual([ - ;; "Sorted by ", - ;; ["Foo ascending"], - ;; ]); - ;; }); - ;; }); - - ) - (deftest ^:parallel orderable-columns-breakouts-test - (testing "If query has aggregations and/or breakouts, you can only order by those." + (testing "If query has aggregations and/or breakouts you can only order by those." (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") (lib/aggregate (lib/sum (lib/field "VENUES" "PRICE"))) (lib/aggregate (lib/avg (lib/+ (lib/field "VENUES" "PRICE") 1))) @@ -153,11 +135,8 @@ :table_id (meta/id :venues) :name "CATEGORY_ID" :has_field_values :none - :source :breakout + :lib/source :source/breakouts :fk_target_field_id (meta/id :categories :id) - :field_ref [:field - {:base-type :type/Integer, :lib/uuid string?} - (meta/id :venues :category-id)] :effective_type :type/Integer :id (meta/id :venues :category-id) :display_name "Category ID" @@ -166,28 +145,25 @@ :base_type :type/Integer :name "sum_price" :display_name "Sum of Price" - :field_ref [:aggregation {:lib/uuid string?, :base-type :type/Integer} 0] - :source :aggregation} + :lib/source :source/aggregations} {:lib/type :metadata/field :base_type :type/Float :name "avg_price_plus_1" :display_name "Average of Price + 1" - :field_ref [:aggregation {:lib/uuid string?, :base-type :type/Float} 1] - :source :aggregation}] + :lib/source :source/aggregations}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-columns-breakouts-with-expression-test - (testing "If query has aggregations and/or breakouts, you can only order by those (with an expression)" + (testing "If query has aggregations and/or breakouts you can only order by those (with an expression)" (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") (lib/expression "Category ID + 1" (lib/+ (lib/field "VENUES" "CATEGORY_ID") 1)) (lib/breakout [:expression {:lib/uuid (str (random-uuid))} "Category ID + 1"]))] (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query)) (is (=? [{:lib/type :metadata/field - :field_ref [:expression {:lib/uuid string?} "Category ID + 1"] :name "Category ID + 1" :display_name "Category ID + 1" :base_type :type/Integer - :source :breakout}] + :lib/source :source/breakouts}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-columns-test @@ -198,60 +174,48 @@ :display_name "ID" :id (meta/id :venues :id) :table_id (meta/id :venues) - :base_type :type/BigInteger - :field_ref [:field {:lib/uuid string?, :base-type :type/BigInteger} (meta/id :venues :id)]} + :base_type :type/BigInteger} {:lib/type :metadata/field :name "NAME" :display_name "Name" :id (meta/id :venues :name) :table_id (meta/id :venues) - :base_type :type/Text - :field_ref [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]} + :base_type :type/Text} {:lib/type :metadata/field :name "CATEGORY_ID" :display_name "Category ID" :id (meta/id :venues :category-id) - :table_id (meta/id :venues) - :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} (meta/id :venues :category-id)]} + :table_id (meta/id :venues)} {:lib/type :metadata/field :name "LATITUDE" :display_name "Latitude" :id (meta/id :venues :latitude) :table_id (meta/id :venues) - :base_type :type/Float - :field_ref [:field {:lib/uuid string?, :base-type :type/Float} (meta/id :venues :latitude)]} + :base_type :type/Float} {:lib/type :metadata/field :name "LONGITUDE" :display_name "Longitude" :id (meta/id :venues :longitude) :table_id (meta/id :venues) - :base_type :type/Float - :field_ref [:field {:lib/uuid string?, :base-type :type/Float} (meta/id :venues :longitude)]} + :base_type :type/Float} {:lib/type :metadata/field :name "PRICE" :display_name "Price" :id (meta/id :venues :price) :table_id (meta/id :venues) - :base_type :type/Integer - :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} (meta/id :venues :price)]} + :base_type :type/Integer} {:lib/type :metadata/field :name "ID" :display_name "ID" :id (meta/id :categories :id) :table_id (meta/id :categories) - :base_type :type/BigInteger - :field_ref [:field - {:lib/uuid string?, :base-type :type/BigInteger, :source-field (meta/id :venues :category-id)} - (meta/id :categories :id)]} + :base_type :type/BigInteger} {:lib/type :metadata/field :name "NAME" :display_name "Name" :id (meta/id :categories :name) :table_id (meta/id :categories) - :base_type :type/Text - :field_ref [:field - {:lib/uuid string?, :base-type :type/Text, :source-field (meta/id :venues :category-id)} - (meta/id :categories :name)]}] + :base_type :type/Text}] (lib/orderable-columns query)))))) (deftest ^:parallel orderable-expressions-test @@ -261,20 +225,17 @@ (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query)) (is (=? [{:lib/type :metadata/field :base_type :type/Integer - :name "category_id_plus_1" + :name "Category ID + 1" :display_name "Category ID + 1" - :field_ref [:expression - {:lib/uuid string?, :base-type :type/Integer} - "Category ID + 1"] - :source :expressions} - {:id (meta/id :venues :id), :name "ID"} - {:id (meta/id :venues :name), :name "NAME"} - {:id (meta/id :venues :category-id), :name "CATEGORY_ID"} - {:id (meta/id :venues :latitude), :name "LATITUDE"} - {:id (meta/id :venues :longitude), :name "LONGITUDE"} - {:id (meta/id :venues :price), :name "PRICE"} - {:id (meta/id :categories :id), :name "ID"} - {:id (meta/id :categories :name), :name "NAME"}] + :lib/source :source/expressions} + {:id (meta/id :venues :id) :name "ID"} + {:id (meta/id :venues :name) :name "NAME"} + {:id (meta/id :venues :category-id) :name "CATEGORY_ID"} + {:id (meta/id :venues :latitude) :name "LATITUDE"} + {:id (meta/id :venues :longitude) :name "LONGITUDE"} + {:id (meta/id :venues :price) :name "PRICE"} + {:id (meta/id :categories :id) :name "ID"} + {:id (meta/id :categories :name) :name "NAME"}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-expressions-exclude-boolean-expressions-test @@ -282,14 +243,14 @@ (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") (lib/expression "Name is empty?" (lib/is-empty (lib/field "VENUES" "NAME"))))] (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query)) - (is (=? [{:id (meta/id :venues :id), :name "ID"} - {:id (meta/id :venues :name), :name "NAME"} - {:id (meta/id :venues :category-id), :name "CATEGORY_ID"} - {:id (meta/id :venues :latitude), :name "LATITUDE"} - {:id (meta/id :venues :longitude), :name "LONGITUDE"} - {:id (meta/id :venues :price), :name "PRICE"} - {:id (meta/id :categories :id), :name "ID"} - {:id (meta/id :categories :name), :name "NAME"}] + (is (=? [{:id (meta/id :venues :id) :name "ID"} + {:id (meta/id :venues :name) :name "NAME"} + {:id (meta/id :venues :category-id) :name "CATEGORY_ID"} + {:id (meta/id :venues :latitude) :name "LATITUDE"} + {:id (meta/id :venues :longitude) :name "LONGITUDE"} + {:id (meta/id :venues :price) :name "PRICE"} + {:id (meta/id :categories :id) :name "ID"} + {:id (meta/id :categories :name) :name "NAME"}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-explicit-joins-test @@ -303,32 +264,26 @@ (lib/with-join-alias "Cat") (lib/with-join-fields :all))))] (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query)) - (is (=? [{:id (meta/id :venues :id), :name "ID"} - {:id (meta/id :venues :name), :name "NAME"} - {:id (meta/id :venues :category-id), :name "CATEGORY_ID"} - {:id (meta/id :venues :latitude), :name "LATITUDE"} - {:id (meta/id :venues :longitude), :name "LONGITUDE"} - {:id (meta/id :venues :price), :name "PRICE"} + (is (=? [{:id (meta/id :venues :id) :name "ID"} + {:id (meta/id :venues :name) :name "NAME"} + {:id (meta/id :venues :category-id) :name "CATEGORY_ID"} + {:id (meta/id :venues :latitude) :name "LATITUDE"} + {:id (meta/id :venues :longitude) :name "LONGITUDE"} + {:id (meta/id :venues :price) :name "PRICE"} {:lib/type :metadata/field :name "ID" :display_name "Categories → ID" ; should we be using the explicit alias we gave this join? :source_alias "Cat" :id (meta/id :categories :id) :table_id (meta/id :categories) - :base_type :type/BigInteger - :field_ref [:field - {:lib/uuid string?, :base-type :type/BigInteger, :join-alias "Cat"} - (meta/id :categories :id)]} + :base_type :type/BigInteger} {:lib/type :metadata/field :name "NAME" :display_name "Categories → Name" :source_alias "Cat" :id (meta/id :categories :name) :table_id (meta/id :categories) - :base_type :type/Text - :field_ref [:field - {:lib/uuid string?, :base-type :type/Text, :join-alias "Cat"} - (meta/id :categories :name)]}] + :base_type :type/Text}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-columns-source-metadata-test @@ -336,23 +291,17 @@ (let [query (lib.tu/query-with-card-source-table)] (testing (lib.util/format "Query =\n%s" (u/pprint-to-str query)) (is (=? [{:name "ID" - :base_type :type/BigInteger - :field_ref [:field {:lib/uuid string?, :base-type :type/BigInteger} "ID"]} + :base_type :type/BigInteger} {:name "NAME" - :base_type :type/Text - :field_ref [:field {:lib/uuid string?, :base-type :type/Text} "NAME"]} + :base_type :type/Text} {:name "CATEGORY_ID" - :base_type :type/Integer - :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} "CATEGORY_ID"]} + :base_type :type/Integer} {:name "LATITUDE" - :base_type :type/Float - :field_ref [:field {:lib/uuid string?, :base-type :type/Float} "LATITUDE"]} + :base_type :type/Float} {:name "LONGITUDE" - :base_type :type/Float - :field_ref [:field {:lib/uuid string?, :base-type :type/Float} "LONGITUDE"]} + :base_type :type/Float} {:name "PRICE" - :base_type :type/Integer - :field_ref [:field {:lib/uuid string?, :base-type :type/Integer} "PRICE"]}] + :base_type :type/Integer}] (lib/orderable-columns query))))))) (deftest ^:parallel orderable-columns-e2e-test @@ -375,9 +324,73 @@ :lib/options {:lib/uuid string?} :order-by [[:asc {:lib/uuid string?} - [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]]]}]} + [:field {:lib/uuid string? :base-type :type/Text} (meta/id :venues :name)]]]}]} query')) (is (=? [[:asc {:lib/uuid string?} - [:field {:lib/uuid string?, :base-type :type/Text} (meta/id :venues :name)]]] + [:field {:lib/uuid string? :base-type :type/Text} (meta/id :venues :name)]]] (lib/order-bys query')))))))) + +(deftest ^:parallel order-bys-with-duplicate-column-names-test + (testing "Order by stuff should work with two different columns named ID (#29702)" + (is (=? [{:id (meta/id :venues :id) + :name "ID" + :lib/source :source/previous-stage + :lib/type :metadata/field + :base_type :type/BigInteger + :effective_type :type/BigInteger + :display_name "ID" + :table_id (meta/id :venues)} + {:id (meta/id :categories :id) + :name "ID_2" + :lib/source :source/previous-stage + :lib/type :metadata/field + :base_type :type/BigInteger + :effective_type :type/BigInteger + :display_name "ID" + :table_id (meta/id :categories)}] + (-> (lib/query-for-table-name meta/metadata-provider "VENUES") + (lib/join (-> (lib/join-clause + (meta/table-metadata :categories) + (lib/= + (lib/field "VENUES" "CATEGORY_ID") + (lib/field "CATEGORIES" "ID"))) + (lib/with-join-fields :all))) + (lib/fields [(lib/field "VENUES" "ID") (lib/field "CATEGORIES" "ID")]) + (lib/append-stage) + (lib/orderable-columns)))))) + +(deftest ^:parallel orderable-columns-include-expressions-test + (testing "orderable-columns should include expressions" + (is (=? [{:lib/type :metadata/field + :name "expr" + :display_name "expr" + :lib/source :source/expressions} + {:name "ID"} + {:name "NAME"} + {:name "CATEGORY_ID"} + {:name "LATITUDE"} + {:name "LONGITUDE"} + {:name "PRICE"} + {:name "ID"} + {:name "NAME"}] + (-> (lib/query-for-table-name meta/metadata-provider "VENUES") + (lib/expression "expr" (lib/absolute-datetime "2020" :month)) + (lib/fields [(lib/field "VENUES" "ID")]) + (lib/orderable-columns)))))) + +(deftest ^:parallel order-by-expression-test + (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") + (lib/expression "expr" (lib/absolute-datetime "2020" :month)) + (lib/fields [(lib/field "VENUES" "ID")])) + [expr] (lib/orderable-columns query)] + (is (=? {:lib/type :metadata/field + :lib/source :source/expressions + :name "expr"} + expr)) + (let [updated-query (lib/order-by query expr)] + (is (=? {:stages [{:order-by [[:asc {} [:expression {} "expr"]]]}]} + updated-query)) + (testing "description" + (is (= "Venues, Sorted by expr ascending" + (lib/describe-query updated-query))))))) diff --git a/test/metabase/lib/stage_test.cljc b/test/metabase/lib/stage_test.cljc index 9a7885ccfc381bd8af8947591e72482ea3e7bdcc..5ff8d19cadd4a874d72a7647fc8550d2e51a81f7 100644 --- a/test/metabase/lib/stage_test.cljc +++ b/test/metabase/lib/stage_test.cljc @@ -26,8 +26,7 @@ :lib/metadata meta/metadata-provider}] (is (mc/validate ::lib.schema/query query)) (is (=? [(merge (meta/field-metadata :venues :price) - {:source :fields - :field_ref [:field {:lib/uuid string?} (meta/id :venues :price)]})] + {:lib/source :source/fields})] (lib.metadata.calculation/metadata query -1 query)))))) (deftest ^:parallel deduplicate-expression-names-in-aggregations-test @@ -44,12 +43,10 @@ [:avg {} (lib.tu/field-clause :venues :price)]]]})] (is (=? [{:base_type :type/Float :name "0_9_times_avg_price" - :display_name "0.9 × Average of Price" - :field_ref [:aggregation {:lib/uuid string?} 0]} + :display_name "0.9 × Average of Price"} {:base_type :type/Float :name "0_8_times_avg_price" - :display_name "0.8 × Average of Price" - :field_ref [:aggregation {:lib/uuid string?} 1]}] + :display_name "0.8 × Average of Price"}] (lib.metadata.calculation/metadata query -1 query))))))) (deftest ^:parallel stage-display-name-card-source-query diff --git a/test/metabase/lib/test_metadata.cljc b/test/metabase/lib/test_metadata.cljc index 8b54a78a42e296cf4fa9eb684a0ebee8dc8b9387..451dd6d07c854cd00edd5af9f5a7145be573f43c 100644 --- a/test/metabase/lib/test_metadata.cljc +++ b/test/metabase/lib/test_metadata.cljc @@ -801,7 +801,6 @@ {:lib/type :metadata/results :columns [{:lib/type :metadata/field :display_name "ID" - :field_ref [:field "ID" {:base-type :type/BigInteger}] :name "ID" :base_type :type/BigInteger :effective_type :type/BigInteger @@ -809,7 +808,6 @@ :fingerprint nil} {:lib/type :metadata/field :display_name "NAME" ; TODO -- these display names are icky - :field_ref [:field "NAME" {:base-type :type/Text}] :name "NAME" :base_type :type/Text :effective_type :type/Text @@ -822,7 +820,6 @@ :average-length 15.63}}}} {:lib/type :metadata/field :display_name "CATEGORY_ID" - :field_ref [:field "CATEGORY_ID" {:base-type :type/Integer}] :name "CATEGORY_ID" :base_type :type/Integer :effective_type :type/Integer @@ -832,7 +829,6 @@ {:min 2.0, :q1 6.89564392373896, :q3 49.240253073352044, :max 74.0, :sd 23.058108414099443, :avg 29.98}}}} {:lib/type :metadata/field :display_name "LATITUDE" - :field_ref [:field "LATITUDE" {:base-type :type/Float}] :name "LATITUDE" :base_type :type/Float :effective_type :type/Float @@ -847,7 +843,6 @@ :avg 35.505891999999996}}}} {:lib/type :metadata/field :display_name "LONGITUDE" - :field_ref [:field "LONGITUDE" {:base-type :type/Float}] :name "LONGITUDE" :base_type :type/Float :effective_type :type/Float @@ -862,7 +857,6 @@ :avg -115.99848699999998}}}} {:lib/type :metadata/field :display_name "PRICE" - :field_ref [:field "PRICE" {:base-type :type/Integer}] :name "PRICE" :base_type :type/Integer :effective_type :type/Integer