diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc index 4288c3b99085d4ed98dc6bef87308e5e2995c882..5e09a68844d72df755eb54b6b75fae7cc502c26f 100644 --- a/src/metabase/lib/aggregation.cljc +++ b/src/metabase/lib/aggregation.cljc @@ -66,20 +66,34 @@ [query stage-number [_tag _opts index] style] (lib.metadata.calculation/display-name query stage-number (resolve-aggregation query stage-number index) style)) -(defmethod lib.metadata.calculation/display-name-method :count - [query stage-number [_count _opts x] style] +(lib.hierarchy/derive ::count-aggregation ::aggregation) + +;;; count and cumulative count can both be used either with no args (count of rows) or with one arg (count of X, which +;;; I think means count where X is not NULL or something like that. Basically `count(x)` in SQL) +(doseq [tag [:count + :cum-count]] + (lib.hierarchy/derive tag ::count-aggregation)) + +(defmethod lib.metadata.calculation/display-name-method ::count-aggregation + [query stage-number [tag _opts x] style] ;; x is optional. (if x - (i18n/tru "Count of {0}" (lib.metadata.calculation/display-name query stage-number x style)) - (i18n/tru "Count"))) + (let [x-display-name (lib.metadata.calculation/display-name query stage-number x style)] + (case tag + :count (i18n/tru "Count of {0}" x-display-name) + :cum-count (i18n/tru "Cumulative count of {0}" x-display-name))) + (case tag + :count (i18n/tru "Count") + :cum-count (i18n/tru "Cumulative count")))) (defmethod lib.metadata.calculation/column-name-method :count - [query stage-number [_count _opts x]] - (if x - (str "count_" (lib.metadata.calculation/column-name query stage-number x)) - "count")) - -(lib.hierarchy/derive :count ::aggregation) + [query stage-number [tag _opts x]] + (let [prefix (case tag + :count "count" + :cum-count "cum_count")] + (if x + (str prefix \_ (lib.metadata.calculation/column-name query stage-number x)) + prefix))) (defmethod lib.metadata.calculation/display-name-method :case [_query _stage-number _case _style] @@ -89,10 +103,11 @@ [_query _stage-number _case] "case") +;;; TODO - Should `:case` derive from `::aggregation` as well??? + (lib.hierarchy/derive ::unary-aggregation ::aggregation) (doseq [tag [:avg - :cum-count :cum-sum :distinct :max @@ -109,7 +124,6 @@ (str (case tag :avg "avg_" - :cum-count "cum_count_" :cum-sum "cum_sum_" :distinct "distinct_" :max "max_" @@ -125,7 +139,6 @@ (let [arg (lib.metadata.calculation/display-name query stage-number arg style)] (case tag :avg (i18n/tru "Average of {0}" arg) - :cum-count (i18n/tru "Cumulative count of {0}" arg) :cum-sum (i18n/tru "Cumulative sum of {0}" arg) :distinct (i18n/tru "Distinct values of {0}" arg) :max (i18n/tru "Max of {0}" arg) diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index 55274b9142dd5ebabc80892ded5e300bbd84a7b4..246b26ad9364f3be790d919773e7f1fc9e535e2e 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -73,7 +73,9 @@ stddev sum sum-where - var] + var + cum-count + cum-sum] [lib.binning available-binning-strategies binning diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc index c089d9ff81045539e4ad05a313cd00201fe6f600..68a1ff4426df33b89ad97e775da544d43df4bc4a 100644 --- a/test/metabase/lib/aggregation_test.cljc +++ b/test/metabase/lib/aggregation_test.cljc @@ -245,147 +245,151 @@ (is (= :type/Integer (lib/type-of query (first (lib/aggregations-metadata query))))))) +(def ^:private op-test-summable-cols + [{:display-name "Latitude" + :effective-type :type/Float + :semantic-type :type/Latitude + :lib/source :source/table-defaults} + {:display-name "Longitude" + :effective-type :type/Float + :semantic-type :type/Longitude + :lib/source :source/table-defaults} + {:display-name "Price" + :effective-type :type/Integer + :semantic-type :type/Category + :lib/source :source/table-defaults} + {:display-name "double-price" + :effective-type :type/Integer + :lib/source :source/expressions}]) + +(def ^:private op-test-all-cols + [{:display-name "ID" + :effective-type :type/BigInteger + :semantic-type :type/PK + :lib/source :source/table-defaults} + {:display-name "Name" + :effective-type :type/Text + :semantic-type :type/Name + :lib/source :source/table-defaults} + {:display-name "Category ID" + :effective-type :type/Integer + :semantic-type :type/FK + :lib/source :source/table-defaults} + {:display-name "Latitude" + :effective-type :type/Float + :semantic-type :type/Latitude + :lib/source :source/table-defaults} + {:display-name "Longitude" + :effective-type :type/Float + :semantic-type :type/Longitude + :lib/source :source/table-defaults} + {:display-name "Price" + :effective-type :type/Integer + :semantic-type :type/Category + :lib/source :source/table-defaults} + {:display-name "double-price" + :effective-type :type/Integer + :lib/source :source/expressions} + {:display-name "budget?" + :effective-type :type/Boolean + :lib/source :source/expressions} + {:display-name "ID" + :effective-type :type/BigInteger + :semantic-type :type/PK + :lib/source :source/implicitly-joinable} + {:display-name "Name" + :effective-type :type/Text + :semantic-type :type/Name + :lib/source :source/implicitly-joinable}]) + (deftest ^:parallel aggregation-operator-test (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES") (lib/expression "double-price" (lib/* (lib/field (meta/id :venues :price)) 2)) (lib/expression "budget?" (lib/< (lib/field (meta/id :venues :price)) 2)) (lib/aggregate (lib/sum [:expression {:lib/uuid (str (random-uuid))} "double-price"]))) - summable-cols [{:display-name "Latitude", - :effective-type :type/Float, - :semantic-type :type/Latitude, - :lib/source :source/table-defaults} - {:display-name "Longitude", - :effective-type :type/Float, - :semantic-type :type/Longitude, - :lib/source :source/table-defaults} - {:display-name "Price", - :effective-type :type/Integer, - :semantic-type :type/Category, - :lib/source :source/table-defaults} - {:display-name "double-price", - :effective-type :type/Integer - :lib/source :source/expressions}] - all-cols [{:display-name "ID" - :effective-type :type/BigInteger - :semantic-type :type/PK - :lib/source :source/table-defaults} - {:display-name "Name" - :effective-type :type/Text - :semantic-type :type/Name - :lib/source :source/table-defaults} - {:display-name "Category ID", - :effective-type :type/Integer, - :semantic-type :type/FK, - :lib/source :source/table-defaults} - {:display-name "Latitude", - :effective-type :type/Float, - :semantic-type :type/Latitude, - :lib/source :source/table-defaults} - {:display-name "Longitude", - :effective-type :type/Float, - :semantic-type :type/Longitude, - :lib/source :source/table-defaults} - {:display-name "Price", - :effective-type :type/Integer, - :semantic-type :type/Category, - :lib/source :source/table-defaults} - {:display-name "double-price" - :effective-type :type/Integer - :lib/source :source/expressions} - {:display-name "budget?" - :effective-type :type/Boolean - :lib/source :source/expressions} - {:display-name "ID", - :effective-type :type/BigInteger, - :semantic-type :type/PK, - :lib/source :source/implicitly-joinable} - {:display-name "Name", - :effective-type :type/Text, - :semantic-type :type/Name, - :lib/source :source/implicitly-joinable}] - scope-cols all-cols + scope-cols op-test-all-cols aggregation-operators (lib/available-aggregation-operators query) count-op (first aggregation-operators) sum-op (second aggregation-operators)] (testing "available aggregation operators" - (is (=? [{:short :count, + (is (=? [{:short :count :requires-column? false} - {:short :sum, - :requires-column? true, - :columns summable-cols} - {:short :avg, - :requires-column? true, - :columns summable-cols} - {:short :distinct, - :requires-column? true, - :columns all-cols} - {:short :cum-sum, - :requires-column? true, - :columns summable-cols} - {:short :cum-count, + {:short :sum + :requires-column? true + :columns op-test-summable-cols} + {:short :avg + :requires-column? true + :columns op-test-summable-cols} + {:short :distinct + :requires-column? true + :columns op-test-all-cols} + {:short :cum-sum + :requires-column? true + :columns op-test-summable-cols} + {:short :cum-count :requires-column? false} - {:short :stddev, - :requires-column? true, - :columns summable-cols} - {:short :min, - :requires-column? true, + {:short :stddev + :requires-column? true + :columns op-test-summable-cols} + {:short :min + :requires-column? true :columns scope-cols} - {:short :max, - :requires-column? true, + {:short :max + :requires-column? true :columns scope-cols}] aggregation-operators))) (testing "aggregation operator display info" - (is (=? [{:display-name "Count of rows", - :column-name "Count", - :description "Total number of rows in the answer.", - :short-name "count", + (is (=? [{:display-name "Count of rows" + :column-name "Count" + :description "Total number of rows in the answer." + :short-name "count" :requires-column false} - {:display-name "Sum of ...", - :column-name "Sum", - :description "Sum of all the values of a column.", - :short-name "sum", + {:display-name "Sum of ..." + :column-name "Sum" + :description "Sum of all the values of a column." + :short-name "sum" :requires-column true} - {:display-name "Average of ...", - :column-name "Average", - :description "Average of all the values of a column", - :short-name "avg", + {:display-name "Average of ..." + :column-name "Average" + :description "Average of all the values of a column" + :short-name "avg" :requires-column true} - {:display-name "Number of distinct values of ...", - :column-name "Distinct values", - :description "Number of unique values of a column among all the rows in the answer.", - :short-name "distinct", + {:display-name "Number of distinct values of ..." + :column-name "Distinct values" + :description "Number of unique values of a column among all the rows in the answer." + :short-name "distinct" :requires-column true} - {:display-name "Cumulative sum of ...", - :column-name "Sum", - :description "Additive sum of all the values of a column.\ne.x. total revenue over time.", - :short-name "cum-sum", + {:display-name "Cumulative sum of ..." + :column-name "Sum" + :description "Additive sum of all the values of a column.\ne.x. total revenue over time." + :short-name "cum-sum" :requires-column true} - {:display-name "Cumulative count of rows", - :column-name "Count", - :description "Additive count of the number of rows.\ne.x. total number of sales over time.", - :short-name "cum-count", + {:display-name "Cumulative count of rows" + :column-name "Count" + :description "Additive count of the number of rows.\ne.x. total number of sales over time." + :short-name "cum-count" :requires-column false} - {:display-name "Standard deviation of ...", - :column-name "SD", - :description "Number which expresses how much the values of a column vary among all rows in the answer.", - :short-name "stddev", + {:display-name "Standard deviation of ..." + :column-name "SD" + :description "Number which expresses how much the values of a column vary among all rows in the answer." + :short-name "stddev" :requires-column true} - {:display-name "Minimum of ...", - :column-name "Min", - :description "Minimum value of a column", - :short-name "min", + {:display-name "Minimum of ..." + :column-name "Min" + :description "Minimum value of a column" + :short-name "min" :requires-column true} - {:display-name "Maximum of ...", - :column-name "Max", - :description "Maximum value of a column", - :short-name "max", + {:display-name "Maximum of ..." + :column-name "Max" + :description "Maximum value of a column" + :short-name "max" :requires-column true}] (map #(lib/display-info query %) aggregation-operators)))) (testing "display name" (is (= "Count of rows" (lib/display-name query (first aggregation-operators))))) (testing "testing getting the available columns for an aggregation operator" (is (nil? (lib/aggregation-operator-columns count-op))) - (is (=? summable-cols (lib/aggregation-operator-columns sum-op)))) + (is (=? op-test-summable-cols (lib/aggregation-operator-columns sum-op)))) (testing "aggregation operators can be added as aggregates" (let [price-col (-> sum-op lib/aggregation-operator-columns pop peek) agg-query (-> query @@ -393,8 +397,8 @@ (lib/aggregate (lib/aggregation-clause sum-op price-col)))] (is (=? {:lib/type :mbql/query :stages - [{:lib/type :mbql.stage/mbql, - :source-table int?, + [{:lib/type :mbql.stage/mbql + :source-table int? :expressions {"double-price" [:* {} [:field {:base-type :type/Integer, :effective-type :type/Integer} int?] 2] @@ -405,21 +409,21 @@ [:count {}] [:sum {} [:field {:base-type :type/Integer, :effective-type :type/Integer} int?]]]}]} agg-query)) - (is (=? [{:lib/type :metadata/field, - :effective-type :type/Integer, - :name "sum_double-price", - :display-name "Sum of double-price", + (is (=? [{:lib/type :metadata/field + :effective-type :type/Integer + :name "sum_double-price" + :display-name "Sum of double-price" :lib/source :source/aggregations} - {:lib/type :metadata/field, - :effective-type :type/Integer, - :name "count", - :display-name "Count", + {:lib/type :metadata/field + :effective-type :type/Integer + :name "count" + :display-name "Count" :lib/source :source/aggregations} - {:settings {:is_priceless true}, - :lib/type :metadata/field, - :effective-type :type/Integer, - :name "sum_PRICE", - :display-name "Sum of Price", + {:settings {:is_priceless true} + :lib/type :metadata/field + :effective-type :type/Integer + :name "sum_PRICE" + :display-name "Sum of Price" :lib/source :source/aggregations}] (lib/aggregations-metadata agg-query))))))) @@ -432,136 +436,83 @@ (lib/aggregate (lib/count))) aggregations (lib/aggregations query) aggregation-operators (lib/available-aggregation-operators query) - summable-cols [{:display-name "Latitude", - :effective-type :type/Float, - :semantic-type :type/Latitude, - :lib/source :source/table-defaults} - {:display-name "Longitude", - :effective-type :type/Float, - :semantic-type :type/Longitude, - :lib/source :source/table-defaults} - {:display-name "Price", - :effective-type :type/Integer, - :semantic-type :type/Category, - :lib/source :source/table-defaults} - {:display-name "double-price", - :effective-type :type/Integer - :lib/source :source/expressions}] - all-cols [{:display-name "ID" - :effective-type :type/BigInteger - :semantic-type :type/PK - :lib/source :source/table-defaults} - {:display-name "Name" - :effective-type :type/Text - :semantic-type :type/Name - :lib/source :source/table-defaults} - {:display-name "Category ID", - :effective-type :type/Integer, - :semantic-type :type/FK, - :lib/source :source/table-defaults} - {:display-name "Latitude", - :effective-type :type/Float, - :semantic-type :type/Latitude, - :lib/source :source/table-defaults} - {:display-name "Longitude", - :effective-type :type/Float, - :semantic-type :type/Longitude, - :lib/source :source/table-defaults} - {:display-name "Price", - :effective-type :type/Integer, - :semantic-type :type/Category, - :lib/source :source/table-defaults} - {:display-name "double-price" - :effective-type :type/Integer - :lib/source :source/expressions} - {:display-name "budget?" - :effective-type :type/Boolean - :lib/source :source/expressions} - {:display-name "ID", - :effective-type :type/BigInteger, - :semantic-type :type/PK, - :lib/source :source/implicitly-joinable} - {:display-name "Name", - :effective-type :type/Text, - :semantic-type :type/Name, - :lib/source :source/implicitly-joinable}] - scope-cols all-cols] + scope-cols op-test-all-cols] (testing "aggregations" (is (=? [[:sum {} [:expression {} "double-price"]] [:count {}]] aggregations))) (testing "selected-aggregation-operators w/o column" - (is (=? [{:short :count, + (is (=? [{:short :count :requires-column? false :selected? true} - {:short :sum, - :requires-column? true, - :columns summable-cols} - {:short :avg, - :requires-column? true, - :columns summable-cols} - {:short :distinct, - :requires-column? true, - :columns all-cols} - {:short :cum-sum, - :requires-column? true, - :columns summable-cols} - {:short :cum-count, + {:short :sum + :requires-column? true + :columns op-test-summable-cols} + {:short :avg + :requires-column? true + :columns op-test-summable-cols} + {:short :distinct + :requires-column? true + :columns op-test-all-cols} + {:short :cum-sum + :requires-column? true + :columns op-test-summable-cols} + {:short :cum-count :requires-column? false} - {:short :stddev, - :requires-column? true, - :columns summable-cols} - {:short :min, - :requires-column? true, + {:short :stddev + :requires-column? true + :columns op-test-summable-cols} + {:short :min + :requires-column? true :columns scope-cols} - {:short :max, - :requires-column? true, + {:short :max + :requires-column? true :columns scope-cols}] (lib/selected-aggregation-operators aggregation-operators (second aggregations))))) (testing "selected-aggregation-operators w/ column" (let [selected-operators (lib/selected-aggregation-operators aggregation-operators (first aggregations))] - (is (=? [{:short :count, + (is (=? [{:short :count :requires-column? false} - {:short :sum, - :requires-column? true, + {:short :sum + :requires-column? true :columns (mapv (fn [col] (cond-> col (= (:display-name col) "double-price") (assoc :selected? true))) - summable-cols) + op-test-summable-cols) :selected? true} - {:short :avg, - :requires-column? true, - :columns summable-cols} - {:short :distinct, - :requires-column? true, - :columns all-cols} - {:short :cum-sum, - :requires-column? true, - :columns summable-cols} - {:short :cum-count, + {:short :avg + :requires-column? true + :columns op-test-summable-cols} + {:short :distinct + :requires-column? true + :columns op-test-all-cols} + {:short :cum-sum + :requires-column? true + :columns op-test-summable-cols} + {:short :cum-count :requires-column? false} - {:short :stddev, - :requires-column? true, - :columns summable-cols} - {:short :min, - :requires-column? true, + {:short :stddev + :requires-column? true + :columns op-test-summable-cols} + {:short :min + :requires-column? true :columns scope-cols} - {:short :max, - :requires-column? true, + {:short :max + :requires-column? true :columns scope-cols}] selected-operators)) - (is (= [{:display-name "Count of rows", - :column-name "Count", - :description "Total number of rows in the answer.", - :short-name "count", + (is (= [{:display-name "Count of rows" + :column-name "Count" + :description "Total number of rows in the answer." + :short-name "count" :requires-column false} - {:display-name "Sum of ...", - :column-name "Sum", - :description "Sum of all the values of a column.", - :short-name "sum", - :requires-column true, + {:display-name "Sum of ..." + :column-name "Sum" + :description "Sum of all the values of a column." + :short-name "sum" + :requires-column true :selected true}] (take 2 (map #(lib/display-info query %) selected-operators)))) (is (=? [{:display-name "Latitude",} @@ -691,3 +642,30 @@ :lib/source-column-alias "sum_case" :lib/desired-column-alias "sum_case"}] (lib.metadata.calculation/metadata query))))) + +(deftest ^:parallel count-display-name-test + (testing "#31255" + (doseq [{:keys [k f expected]} [{:k :cum-count + :f lib/cum-count + :expected {:with-field "Cumulative count of ID" + :without-field "Cumulative count"}} + {:k :count + :f lib/count + :expected {:with-field "Count of ID" + :without-field "Count"}}]] + (testing k + (doseq [field? [true false]] + (testing (if field? "with field" "without field") + (let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :venues)) + (lib/aggregate (if field? + (f (meta/field-metadata :venues :id)) + (f))))] + (is (=? {:stages [{:aggregation [(if field? + [k {} [:field {} integer?]] + [k {}])]}]} + query) + "query") + (is (= [(expected (if field? :with-field :without-field))] + (map (partial lib/display-name query) + (lib.metadata.calculation/metadata query))) + "display name"))))))))