diff --git a/src/metabase/query_processor/middleware/annotate.clj b/src/metabase/query_processor/middleware/annotate.clj index 31440e1236739bafa0be0630f7b4fc81107fcb74..7d8d5386d2923cb0e2347f331489939a45bc9312 100644 --- a/src/metabase/query_processor/middleware/annotate.clj +++ b/src/metabase/query_processor/middleware/annotate.clj @@ -164,15 +164,19 @@ :field_ref &match) [:expression expression-name] - (merge - ;; There's some inconsistency when expression names are keywords and when strings. - ;; TODO: remove this duality once https://github.com/metabase/mbql/issues/5 is resolved. - (infer-expression-type (some expressions ((juxt identity keyword) expression-name))) - {:name expression-name - :display_name expression-name - ;; provided so the FE can add easily add sorts and the like when someone clicks a column header - :expression_name expression-name - :field_ref &match}) + (if-let [matching-expression (when (seq expressions) + (some expressions ((juxt keyword u/keyword->qualified-name) expression-name)))] + (merge + ;; There's some inconsistency when expression names are keywords and when strings. + ;; TODO: remove this duality once https://github.com/metabase/mbql/issues/5 is resolved. + (infer-expression-type matching-expression) + {:name expression-name + :display_name expression-name + ;; provided so the FE can add easily add sorts and the like when someone clicks a column header + :expression_name expression-name + :field_ref &match}) + (throw (ex-info (str (tru "No expression named {0} found. Found: {1}" expression-name (keys expressions))) + {:type :invalid-query, :clause &match, :expressions expressions}))) [:field-id id] (let [{parent-id :parent_id, :as field} (dissoc (qp.store/field id) :database_type)] @@ -247,15 +251,15 @@ (s/defn ^:private aggregation-arg-display-name :- su/NonBlankString "Name to use for an aggregation clause argument such as a Field when constructing the complete aggregation name." - [ag-arg :- Object] + [inner-query, ag-arg :- Object] (or (when (mbql.preds/Field? ag-arg) - (when-let [info (col-info-for-field-clause {} ag-arg)] + (when-let [info (col-info-for-field-clause inner-query ag-arg)] (some info [:display_name :name]))) - (aggregation-display-name ag-arg))) + (aggregation-display-name inner-query ag-arg))) (s/defn aggregation-display-name :- su/NonBlankString "Return an appropriate user-facing display name for an aggregation clause." - [ag-clause] + [inner-query ag-clause] (mbql.u/match-one ag-clause [:aggregation-options _ (options :guard :display-name)] (:display-name options) @@ -265,36 +269,37 @@ [(operator :guard #{:+ :- :/ :*}) & args] (str/join (format " %s " (name operator)) - (map (partial expression-arg-display-name aggregation-arg-display-name) args)) + (for [arg args] + (expression-arg-display-name (partial aggregation-arg-display-name inner-query) arg))) [:count] (str (tru "count")) - [:distinct arg] (str (tru "distinct count of {0}" (aggregation-arg-display-name arg))) - [:count arg] (str (tru "count of {0}" (aggregation-arg-display-name arg))) - [:avg arg] (str (tru "average of {0}" (aggregation-arg-display-name arg))) + [:distinct arg] (str (tru "distinct count of {0}" (aggregation-arg-display-name inner-query arg))) + [:count arg] (str (tru "count of {0}" (aggregation-arg-display-name inner-query arg))) + [:avg arg] (str (tru "average of {0}" (aggregation-arg-display-name inner-query arg))) ;; cum-count and cum-sum get names for count and sum, respectively (see explanation in `aggregation-name`) - [:cum-count arg] (str (tru "count of {0}" (aggregation-arg-display-name arg))) - [:cum-sum arg] (str (tru "sum of {0}" (aggregation-arg-display-name arg))) - [:stddev arg] (str (tru "standard deviation of {0}" (aggregation-arg-display-name arg))) - [:sum arg] (str (tru "sum of {0}" (aggregation-arg-display-name arg))) - [:min arg] (str (tru "minimum value of {0}" (aggregation-arg-display-name arg))) - [:max arg] (str (tru "maximum value of {0}" (aggregation-arg-display-name arg))) + [:cum-count arg] (str (tru "count of {0}" (aggregation-arg-display-name inner-query arg))) + [:cum-sum arg] (str (tru "sum of {0}" (aggregation-arg-display-name inner-query arg))) + [:stddev arg] (str (tru "standard deviation of {0}" (aggregation-arg-display-name inner-query arg))) + [:sum arg] (str (tru "sum of {0}" (aggregation-arg-display-name inner-query arg))) + [:min arg] (str (tru "minimum value of {0}" (aggregation-arg-display-name inner-query arg))) + [:max arg] (str (tru "maximum value of {0}" (aggregation-arg-display-name inner-query arg))) ;; until we have a way to generate good names for filters we'll just have to say 'matching condition' for now - [:sum-where arg _] (str (tru "sum of {0} matching condition" (aggregation-arg-display-name arg))) + [:sum-where arg _] (str (tru "sum of {0} matching condition" (aggregation-arg-display-name inner-query arg))) [:share _] (str (tru "share of rows matching condition")) [:count-where _] (str (tru "count of rows matching condition")) (_ :guard mbql.preds/Field?) - (:display_name (col-info-for-field-clause nil ag-clause)) + (:display_name (col-info-for-field-clause inner-query ag-clause)) _ - (aggregation-name ag-clause {:recursive-name-fn aggregation-arg-display-name}))) + (aggregation-name ag-clause {:recursive-name-fn (partial aggregation-arg-display-name inner-query)}))) -(defn- ag->name-info [ag] +(defn- ag->name-info [inner-query ag] {:name (aggregation-name ag) - :display_name (aggregation-display-name ag)}) + :display_name (aggregation-display-name inner-query ag)}) (s/defn col-info-for-aggregation-clause "Return appropriate column metadata for an `:aggregation` clause." @@ -306,7 +311,7 @@ [:aggregation-options ag _] (merge (col-info-for-aggregation-clause inner-query ag) - (ag->name-info &match)) + (ag->name-info inner-query &match)) ;; Always treat count or distinct count as an integer even if the DB in question returns it as something ;; wacky like a BigDecimal or Float @@ -315,20 +320,20 @@ (col-info-for-aggregation-clause inner-query args) {:base_type :type/Integer :special_type :type/Number} - (ag->name-info &match)) + (ag->name-info inner-query &match)) ; TODO - should we be doing this for `:sum-where` as well? [:count-where _] (merge {:base_type :type/Integer :special_type :type/Number} - (ag->name-info &match)) + (ag->name-info inner-query &match)) [:share _] (merge {:base_type :type/Float :special_type :type/Number} - (ag->name-info &match)) + (ag->name-info inner-query &match)) ;; get info from a Field if we can (theses Fields are matched when ag clauses recursively call ;; `col-info-for-ag-clause`, and this info is added into the results) @@ -343,13 +348,13 @@ (merge (infer-expression-type &match) (when (mbql.preds/Aggregation? &match) - (ag->name-info &match))) + (ag->name-info inner-query &match))) ;; get name/display-name of this ag [(_ :guard keyword?) arg & _] (merge (col-info-for-aggregation-clause inner-query arg) - (ag->name-info &match)))) + (ag->name-info inner-query &match)))) ;;; ----------------------------------------- Putting it all together (MBQL) ----------------------------------------- diff --git a/test/metabase/query_processor/middleware/annotate_test.clj b/test/metabase/query_processor/middleware/annotate_test.clj index 9f97366e3369f40cb357d653f5902092b30c8c8e..6e83e05cc8e54e0da5b0d6cbe0177e3f3a4986f1 100644 --- a/test/metabase/query_processor/middleware/annotate_test.clj +++ b/test/metabase/query_processor/middleware/annotate_test.clj @@ -288,17 +288,51 @@ {:name "sum", :display_name "sum of User ID", :base_type :type/Integer, :special_type :type/FK}]} [:field-literal "sum" :type/Integer]))) +;; col info for an `expression` should work as expected +(expect + {:base_type :type/Float + :special_type :type/Number + :name "double-price" + :display_name "double-price" + :expression_name "double-price" + :field_ref [:expression "double-price"]} + (qp.test-util/with-everything-store + (data/$ids venues + (#'annotate/col-info-for-field-clause + {:expressions {"double-price" [:* $price 2]}} + [:expression "double-price"])))) + +;; if there is no matching expression it should give a meaningful error message +(expect + {:message "No expression named double-price found. Found: (\"one-hundred\")" + :data {:type :invalid-query + :clause [:expression "double-price"] + :expressions {"one-hundred" 100}}} + (try + (qp.test-util/with-everything-store + (data/$ids venues + (#'annotate/col-info-for-field-clause + {:expressions {"one-hundred" 100}} + [:expression "double-price"]))) + (catch Throwable e + {:message (.getMessage e) + :data (ex-data e)}))) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | (MBQL) Col info for Aggregation clauses | ;;; +----------------------------------------------------------------------------------------------------------------+ ;; test that added information about aggregations looks the way we'd expect -(defn- aggregation-names [ag-clause] - (binding [driver/*driver* :h2] - (qp.test-util/with-everything-store - {:name (annotate/aggregation-name ag-clause) - :display_name (annotate/aggregation-display-name ag-clause)}))) +(defn- aggregation-names + ([ag-clause] + (aggregation-names {} ag-clause)) + + ([inner-query ag-clause] + (binding [driver/*driver* :h2] + (qp.test-util/with-everything-store + {:name (annotate/aggregation-name ag-clause) + :display_name (annotate/aggregation-display-name inner-query ag-clause)})))) (expect {:name "count", :display_name "count"} @@ -363,9 +397,13 @@ {:display-name "User-specified Name"}])) ;; make sure custom aggregation names get included in the col info -(defn- col-info-for-aggregation-clause [clause] - (binding [driver/*driver* :h2] - (#'annotate/col-info-for-aggregation-clause {} clause))) +(defn- col-info-for-aggregation-clause + ([clause] + (col-info-for-aggregation-clause {} clause)) + + ([inner-query clause] + (binding [driver/*driver* :h2] + (#'annotate/col-info-for-aggregation-clause inner-query clause)))) (expect {:base_type :type/Float @@ -435,6 +473,18 @@ (data/mbql-query venues {:aggregation [[:metric "ga:totalEvents"]]})))) +;; col info for an `expression` aggregation w/ a named expression should work as expected +(expect + {:base_type :type/Float + :special_type :type/Number + :name "sum" + :display_name "sum of double-price"} + (qp.test-util/with-everything-store + (data/$ids venues + (col-info-for-aggregation-clause + {:expressions {"double-price" [:* $price 2]}} + [:sum [:expression "double-price"]])))) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Other MBQL col info tests | diff --git a/test/metabase/query_processor_test/expression_aggregations_test.clj b/test/metabase/query_processor_test/expression_aggregations_test.clj index 76ccc21af31d7d1fcf50d3924596df2a7afb3541..d51238f4b21b7e33dfe5cc3de531fc8c895e8727 100644 --- a/test/metabase/query_processor_test/expression_aggregations_test.clj +++ b/test/metabase/query_processor_test/expression_aggregations_test.clj @@ -270,3 +270,12 @@ (qp.test/rows (data/run-mbql-query venues {:aggregation [["*" ["cum_count"] 10]]})))) + +;; can we use named expressions inside expression aggregations? +(datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) + [[406]] + (qp.test/format-rows-by [int] + (qp.test/rows + (data/run-mbql-query venues + {:aggregation [[:sum [:expression "double-price"]]] + :expressions {"double-price" [:* $price 2]}}))))