From f5760ce39fb7508689b2959153143976c8e0cab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cam=20Sa=C3=BCl?= <cammsaul@gmail.com> Date: Fri, 9 Dec 2016 18:14:18 -0800 Subject: [PATCH] Handle METRICs inside expression aggregation clauses [ci drivers] --- src/metabase/query_processor/macros.clj | 62 +++++++------------ test/metabase/driver/druid_test.clj | 19 +++++- .../expression_aggregations_test.clj | 19 ++++++ test/metabase/test/data.clj | 1 + test/metabase/test/util.clj | 6 +- 5 files changed, 65 insertions(+), 42 deletions(-) diff --git a/src/metabase/query_processor/macros.clj b/src/metabase/query_processor/macros.clj index 1a3a4fffbb8..d567c110544 100644 --- a/src/metabase/query_processor/macros.clj +++ b/src/metabase/query_processor/macros.clj @@ -1,5 +1,8 @@ (ns metabase.query-processor.macros + "TODO - this namespace is ancient and written with MBQL '95 in mind, e.g. it is case-sensitive. + At some point this ought to be reworked to be case-insensitive and cleaned up." (:require [clojure.core.match :refer [match]] + [clojure.walk :as walk] [metabase.db :as db] [metabase.util :as u])) @@ -46,56 +49,39 @@ ;;; ------------------------------------------------------------ Metrics ------------------------------------------------------------ -(defn- merge-aggregation [aggregations new-ag] - (if (map? aggregations) - (recur [aggregations] new-ag) - (conj aggregations new-ag))) - -(defn- is-metric? [aggregation] +(defn- metric? [aggregation] (match aggregation ["METRIC" (_ :guard integer?)] true _ false)) (defn- metric-id [metric] - (when (is-metric? metric) + (when (metric? metric) (second metric))) -(defn- merge-aggregations {:style/indent 0} [query-dict [aggregation & more]] - (if-not aggregation - ;; no more aggregations? we're done - query-dict - ;; otherwise determine if this aggregation is a METRIC and recur - (let [metric-def (when (is-metric? aggregation) - (db/select-one-field :definition 'Metric, :id (metric-id aggregation)))] - (recur (if-not metric-def - ;; not a metric, move to next aggregation - query-dict - ;; it *is* a metric, insert it into the query appropriately - (-> query-dict - (update-in [:query :aggregation] merge-aggregation (:aggregation metric-def)) - (update-in [:query :filter] merge-filter-clauses (:filter metric-def)))) - more)))) - -(defn- remove-metrics [aggregations] - (if-not (and (sequential? aggregations) - (every? coll? aggregations)) - (recur [aggregations]) - (vec (for [ag aggregations - :when (not (is-metric? ag))] - ag)))) +(defn- expand-metric [metric-clause filter-clauses-atom] + (let [{filter-clause :filter, ag-clause :aggregation} (db/select-one-field :definition 'Metric, :id (metric-id metric-clause))] + (when filter-clause + (swap! filter-clauses-atom conj filter-clause)) + ag-clause)) + +(defn- expand-metrics-in-ag-clause [query-dict filter-clauses-atom] + (walk/postwalk (fn [form] + (if-not (metric? form) + form + (expand-metric form filter-clauses-atom))) + query-dict)) + +(defn- expand-metrics [query-dict] + (let [filter-clauses-atom (atom []) + query-dict (expand-metrics-in-ag-clause query-dict filter-clauses-atom)] + (update-in query-dict [:query :filter] merge-filter-clauses @filter-clauses-atom))) (defn- macroexpand-metric [{{aggregations :aggregation} :query, :as query-dict}] (if-not (seq aggregations) ;; :aggregation is empty, so no METRIC to expand query-dict - ;; we have an aggregation clause, so lets see if we are using a METRIC - ;; (since `:aggregation` can be either single or multiple, wrap single ones so `merge-aggregations` can always assume input is multiple) - (merge-aggregations - (update-in query-dict [:query :aggregation] remove-metrics) - (if (and (sequential? aggregations) - (every? coll? aggregations)) - aggregations - [aggregations])))) + ;; otherwise walk the query dict and expand METRIC clauses + (expand-metrics query-dict))) ;;; ------------------------------------------------------------ Middleware ------------------------------------------------------------ diff --git a/test/metabase/driver/druid_test.clj b/test/metabase/driver/druid_test.clj index 5577a389dd4..58d803d0cbd 100644 --- a/test/metabase/driver/druid_test.clj +++ b/test/metabase/driver/druid_test.clj @@ -1,13 +1,15 @@ (ns metabase.driver.druid-test (:require [cheshire.core :as json] [expectations :refer :all] + [metabase.models.metric :refer [Metric]] [metabase.query-processor :as qp] [metabase.query-processor.expand :as ql] [metabase.query-processor-test :refer [rows rows+column-names]] [metabase.test.data :as data] [metabase.test.data.datasets :as datasets, :refer [expect-with-engine]] + [metabase.test.util :as tu] [metabase.timeseries-query-processor-test :as timeseries-qp-test] - [metabase.query :as q])) + [metabase.util :as u])) (def ^:const ^:private ^String native-query-1 (json/generate-string @@ -230,3 +232,18 @@ (druid-query (ql/aggregation (ql/named (ql/- (ql/sum $venue_price) 41) "Sum-41")) (ql/breakout $venue_price)))) + +;; check that we can handle METRICS (ick) inside expression aggregation clauses +(expect-with-engine :druid + [["2" 1231.0] + ["3" 346.0] + ["4" 197.0]] + (timeseries-qp-test/with-flattened-dbdef + (tu/with-temp Metric [metric {:definition {:aggregation [:sum [:field-id (data/id :checkins :venue_price)]] + :filter [:> [:field-id (data/id :checkins :venue_price)] 1]}}] + (rows (qp/process-query + {:database (data/id) + :type :query + :query {:source-table (data/id :checkins) + :aggregation [:+ ["METRIC" (u/get-id metric)] 1] + :breakout [(ql/breakout (ql/field-id (data/id :checkins :venue_price)))]}}))))) diff --git a/test/metabase/query_processor_test/expression_aggregations_test.clj b/test/metabase/query_processor_test/expression_aggregations_test.clj index 0fae5b957e4..5b6452442b8 100644 --- a/test/metabase/query_processor_test/expression_aggregations_test.clj +++ b/test/metabase/query_processor_test/expression_aggregations_test.clj @@ -1,10 +1,13 @@ (ns metabase.query-processor-test.expression-aggregations-test "Tests for expression aggregations." (:require [expectations :refer :all] + [metabase.models.metric :refer [Metric]] + [metabase.query-processor :as qp] [metabase.query-processor.expand :as ql] [metabase.query-processor-test :refer :all] [metabase.test.data :as data] [metabase.test.data.datasets :as datasets, :refer [*engine*]] + [metabase.test.util :as tu] [metabase.util :as u])) ;; sum, * @@ -182,3 +185,19 @@ (rows+column-names (data/run-query venues (ql/aggregation (ql/named (ql/- (ql/sum $price) 41) "Sum-41")) (ql/breakout $price))))) + +;; check that we can handle METRICS (ick) inside expression aggregation clauses +(datasets/expect-with-engines (engines-that-support :expression-aggregations) + [[2 119] + [3 40] + [4 25]] + (tu/with-temp Metric [metric {:table_id (data/id :venues) + :definition {:aggregation [:sum [:field-id (data/id :venues :price)]] + :filter [:> [:field-id (data/id :venues :price)] 1]}}] + (format-rows-by [int int] + (rows (qp/process-query + {:database (data/id) + :type :query + :query {:source-table (data/id :venues) + :aggregation [:+ ["METRIC" (u/get-id metric)] 1] + :breakout [(ql/breakout (ql/field-id (data/id :venues :price)))]}}))))) diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj index 5f23a1336ad..7f54967b198 100644 --- a/test/metabase/test/data.clj +++ b/test/metabase/test/data.clj @@ -105,6 +105,7 @@ "Call `driver/process-query` on expanded inner QUERY, looking up the `Database` ID for the `source-table.` (run-query* (query (source-table 5) ...))" + {:style/indent 0} [query :- qi/Query] (qp/process-query (wrap-inner-query query))) diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index a91f7567950..3ee0d3fc00a 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -124,7 +124,7 @@ {:with-temp-defaults (fn [_] {:base_type :type/Text :name (random-name) :position 1 - :table_id (data/id :venues)})}) + :table_id (data/id :checkins)})}) (u/strict-extend (class Metric) WithTempDefaults @@ -132,7 +132,7 @@ :definition {} :description "Lookin' for a blueberry" :name "Toucans in the rainforest" - :table_id (data/id :venues)})}) + :table_id (data/id :checkins)})}) (u/strict-extend (class PermissionsGroup) WithTempDefaults @@ -172,7 +172,7 @@ :definition {} :description "Lookin' for a blueberry" :name "Toucans in the rainforest" - :table_id (data/id :venues)})}) + :table_id (data/id :checkins)})}) ;; TODO - `with-temp` doesn't return `Sessions`, probably because their ID is a string? -- GitLab