From 5872cd6171592f5a9a74a65747963f6a7b6fc36f Mon Sep 17 00:00:00 2001 From: Cam Saul <cammsaul@gmail.com> Date: Tue, 25 Jun 2019 14:25:11 -0700 Subject: [PATCH] Display metric name instead of underlying aggregation name in QP results (#10251) * Display metric name instead of underlying aggregation name in QP results [ci drivers] Resolves #1851 * Test fixes :wrench: --- src/metabase/models/common.clj | 1 + src/metabase/models/interface.clj | 2 +- .../middleware/expand_macros.clj | 73 ++++++++++++------ test/expectation_options.clj | 4 +- test/metabase/api/embed_test.clj | 3 +- test/metabase/api/public_test.clj | 9 +-- .../middleware/expand_macros_test.clj | 77 +++++++++---------- 7 files changed, 95 insertions(+), 74 deletions(-) diff --git a/src/metabase/models/common.clj b/src/metabase/models/common.clj index f244680c732..bb6c07e2bef 100644 --- a/src/metabase/models/common.clj +++ b/src/metabase/models/common.clj @@ -1,5 +1,6 @@ (ns metabase.models.common) +;; TODO - why is this here? Doesn't seem like the right place for it (def timezones "The different timezones supported by Metabase. Presented as options for the `report-timezone` Setting in the admin panel." diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index c51aa4cf1eb..3ed34892693 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -127,7 +127,7 @@ :out (comp encryption/maybe-decrypt u/jdbc-clob->str)) (defn decompress - "Decompress COMPRESSED-BYTES." + "Decompress `compressed-bytes`." [compressed-bytes] (if (instance? Blob compressed-bytes) (recur (.getBytes ^Blob compressed-bytes 0 (.length ^Blob compressed-bytes))) diff --git a/src/metabase/query_processor/middleware/expand_macros.clj b/src/metabase/query_processor/middleware/expand_macros.clj index a51212a3902..486e19a2fe7 100644 --- a/src/metabase/query_processor/middleware/expand_macros.clj +++ b/src/metabase/query_processor/middleware/expand_macros.clj @@ -13,13 +13,12 @@ [metabase.models [metric :refer [Metric]] [segment :refer [Segment]]] - [metabase.query-processor.interface :as i] [metabase.util :as u] - [puppetlabs.i18n.core :refer [tru]] + [metabase.util.schema :as su] + [puppetlabs.i18n.core :refer [trs tru]] [schema.core :as s] [toucan.db :as db])) - ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | SEGMENTS | ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -51,34 +50,67 @@ ;; metrics won't be in a native query but they could be in source-query or aggregation clause (mbql.u/match query [:metric (_ :guard (complement mbql.u/ga-id?))])) -(defn- metric-clauses->id->definition [metric-clauses] - (db/select-id->field :definition Metric, :id [:in (set (map second metric-clauses))])) - -(defn- add-metrics-filters [query metric-id->definition] - (let [filters (filter identity (map :filter (vals metric-id->definition)))] +(def ^:private MetricInfo + {:id su/IntGreaterThanZero + :name su/NonBlankString + :definition {:aggregation [(s/one mbql.s/Aggregation "aggregation clause")] + (s/optional-key :filter) (s/maybe mbql.s/Filter) + s/Keyword s/Any}}) + +(def ^:private ^{:arglists '([metric-info])} metric-info-validation-errors (s/checker MetricInfo)) + +(s/defn ^:private metric-clauses->id->info :- {su/IntGreaterThanZero MetricInfo} + [metric-clauses :- [mbql.s/metric]] + (when (seq metric-clauses) + (u/key-by :id (for [metric (db/select [Metric :id :name :definition] :id [:in (set (map second metric-clauses))]) + :let [errors (u/prog1 (metric-info-validation-errors metric) + (when <> + (log/warn (trs "Invalid metric: {0} reason: {1}" metric <>))))] + :when (not errors)] + metric)))) + +(defn- add-metrics-filters [query metric-id->info] + (let [filters (for [{{filter-clause :filter} :definition} (vals metric-id->info) + :when filter-clause] + filter-clause)] (reduce mbql.u/add-filter-clause query filters))) -(defn- replace-metrics-aggregations [query metric-id->definition] - (mbql.u/replace-in query [:query] - [:metric (metric-id :guard (complement mbql.u/ga-id?))] - (or (first (:aggregation (metric-id->definition metric-id))) - (throw (IllegalArgumentException. - (str (tru "Metric {0} does not exist, or is invalid." metric-id))))))) +(s/defn ^:private metric-info->ag-clause :- mbql.s/Aggregation + "Return an appropriate aggregation clause from `metric-info`." + [{{[aggregation] :aggregation} :definition, metric-name :name} :- MetricInfo + {:keys [wrap-in-named?]} :- {:wrap-in-named? s/Bool}] + ;; try to give the resulting aggregation the name of the Metric it came from, unless it's already `:named` in + ;; which case keep that name + (if (or (not wrap-in-named?) (mbql.u/is-clause? :named aggregation)) + aggregation + [:named aggregation metric-name])) + +(defn- replace-metrics-aggregations [query metric-id->info] + (let [metric (fn [metric-id] + (or (get metric-id->info metric-id) + (throw (IllegalArgumentException. + (str (tru "Metric {0} does not exist, or is invalid." metric-id))))))] + (mbql.u/replace-in query [:query] + [:named [:metric (metric-id :guard (complement mbql.u/ga-id?))] metric-name] + [:named (metric-info->ag-clause (metric metric-id) {:wrap-in-named? false}) metric-name] + + [:metric (metric-id :guard (complement mbql.u/ga-id?))] + (metric-info->ag-clause (metric metric-id) {:wrap-in-named? true})))) (defn- add-metrics-clauses "Add appropriate `filter` and `aggregation` clauses for a sequence of Metrics. (add-metrics-clauses {:query {}} [[:metric 10]]) ;; -> {:query {:aggregation [[:count]], :filter [:= [:field-id 10] 20]}}" - [query metric-id->definition] + [query metric-id->info] (-> query - (add-metrics-filters metric-id->definition) - (replace-metrics-aggregations metric-id->definition))) + (add-metrics-filters metric-id->info) + (replace-metrics-aggregations metric-id->info))) (s/defn ^:private expand-metrics :- mbql.s/Query [query :- mbql.s/Query] (if-let [metrics (metrics query)] - (add-metrics-clauses query (metric-clauses->id->definition metrics)) + (add-metrics-clauses query (metric-clauses->id->info metrics)) query)) @@ -97,10 +129,7 @@ [{query-type :type, :as query}] (if-not (= query-type :query) query - (u/prog1 (expand-metrics-and-segments query) - (when (and (not i/*disable-qp-logging*) - (not= <> query)) - (log/debug (u/format-color 'cyan "\n\nMACRO/SUBSTITUTED: %s\n%s" (u/emoji "😻") (u/pprint-to-str <>))))))) + (expand-metrics-and-segments query))) (defn expand-macros "Middleware that looks for `:metric` and `:segment` macros in an unexpanded MBQL query and substitute the macros for diff --git a/test/expectation_options.clj b/test/expectation_options.clj index 8364ad8da4b..0525a642dae 100644 --- a/test/expectation_options.clj +++ b/test/expectation_options.clj @@ -31,9 +31,9 @@ :actual-message (when-let [in-a (first (data/diff a e))] (format "\nin actual, not expected:\n%s" (u/pprint-to-str 'red in-a))) :raw [str-e str-a] - :result ["\nexpected:\n" + :result [(format "\nexpected: %s\n" (class e)) (u/pprint-to-str 'green e) - "\nwas:\n" + (format "\nwas: %s\n" (class a)) (u/pprint-to-str 'red a)]}) (defmethod expectations/compare-expr :expectations/maps [e a str-e str-a] diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj index 1ee4e028201..23976b26b50 100644 --- a/test/metabase/api/embed_test.clj +++ b/test/metabase/api/embed_test.clj @@ -325,7 +325,8 @@ (tt/with-temp Card [card (card-with-date-field-filter)] ;; make sure the URL doesn't include /api/ at the beginning like it normally would (binding [http/*url-prefix* (str/replace http/*url-prefix* #"/api/$" "/")] - (http/client :get 200 (str "embed/question/" (card-token card) ".csv?date=Q1-2014")))))) + (tu/with-temporary-setting-values [site-url http/*url-prefix*] + (http/client :get 200 (str "embed/question/" (card-token card) ".csv?date=Q1-2014"))))))) ;;; ---------------------------------------- GET /api/embed/dashboard/:token ----------------------------------------- diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj index 627487a0510..35497a3c59e 100644 --- a/test/metabase/api/public_test.clj +++ b/test/metabase/api/public_test.clj @@ -220,16 +220,11 @@ (tt/with-temp Card [{uuid :public_uuid} (card-with-date-field-filter)] ;; make sure the URL doesn't include /api/ at the beginning like it normally would (binding [http/*url-prefix* (str/replace http/*url-prefix* #"/api/$" "/")] - (try + (tu/with-temporary-setting-values [site-url http/*url-prefix*] (http/client :get 200 (str "public/question/" uuid ".csv") :parameters (json/encode [{:type :date/quarter-year :target [:dimension [:template-tag :date]] - :value "Q1-2014"}])) - ;; this test is failing a lot recently with ConnectExceptions. This is here to debug it - (catch java.net.ConnectException e - (println "Connection to " http/*url-prefix* "public/question/" uuid ".csv refused.") - (println (.getMessage e)) - (throw e))))))) + :value "Q1-2014"}]))))))) ;; make sure we include all the relevant fields like `:insights` (defn- card-with-trendline [] diff --git a/test/metabase/query_processor/middleware/expand_macros_test.clj b/test/metabase/query_processor/middleware/expand_macros_test.clj index 4423dd365af..86937437b33 100644 --- a/test/metabase/query_processor/middleware/expand_macros_test.clj +++ b/test/metabase/query_processor/middleware/expand_macros_test.clj @@ -1,15 +1,15 @@ (ns metabase.query-processor.middleware.expand-macros-test - (:require [expectations :refer :all] + (:require [expectations :refer [expect]] [metabase [query-processor :as qp] - [query-processor-test :refer :all] + [query-processor-test :as qp.test] [util :as u]] [metabase.models [database :refer [Database]] [metric :refer [Metric]] [segment :refer [Segment]] [table :refer [Table]]] - [metabase.query-processor.middleware.expand-macros :as expand-macros :refer :all] + [metabase.query-processor.middleware.expand-macros :as expand-macros] [metabase.test.data :as data] [metabase.test.data.datasets :as datasets] [toucan.util.test :as tt])) @@ -52,30 +52,10 @@ [:> [:field-id 4] 1]]] :breakout [[:field-id 17]]})))) -;; Does expansion work if :and isn't capitalized? (MBQL is case-insensitive!) (#5706, #5530) -(expect - (mbql-query - {:filter [:and - [:= [:field-id 5] "abc"] - [:is-null [:field-id 7]]] - :breakout [[:field-id 17]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Segment [{segment-1-id :id} {:table_id table-id - :definition {:filter [:= [:field-id 5] "abc"]}}] - Segment [{segment-2-id :id} {:table_id table-id - :definition {:filter [:is-null [:field-id 7]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:filter [:and - [:segment segment-1-id] - [:segment segment-2-id]] - :breakout [[:field-id 17]]})))) - ;; just a metric (w/out nested segments) (expect (mbql-query - {:aggregation [[:count]] + {:aggregation [[:named [:count] "Toucans in the rainforest"]] :filter [:and [:> [:field-id 4] 1] [:= [:field-id 5] "abc"]] @@ -83,7 +63,8 @@ :order-by [[:asc [:field-id 1]]]}) (tt/with-temp* [Database [{database-id :id}] Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:table_id table-id + Metric [{metric-1-id :id} {:name "Toucans in the rainforest" + :table_id table-id :definition {:aggregation [[:count]] :filter [:and [:= [:field-id 5] "abc"]]}}]] (#'expand-macros/expand-metrics-and-segments @@ -97,13 +78,14 @@ (expect (mbql-query {:source-table 1000 - :aggregation [[:count]] + :aggregation [[:named [:count] "ABC Fields"]] :filter [:= [:field-id 5] "abc"] :breakout [[:field-id 17]] :order-by [[:asc [:field-id 1]]]}) (tt/with-temp* [Database [{database-id :id}] Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:table_id table-id + Metric [{metric-1-id :id} {:name "ABC Fields" + :table_id table-id :definition {:aggregation [[:count]] :filter [:and [:= [:field-id 5] "abc"]]}}]] (#'expand-macros/expand-metrics-and-segments @@ -116,13 +98,14 @@ ;; metric w/ no filter definition (expect (mbql-query - {:aggregation [[:count]] + {:aggregation [[:named [:count] "My Metric"]] :filter [:= [:field-id 5] "abc"] :breakout [[:field-id 17]] :order-by [[:asc [:field-id 1]]]}) (tt/with-temp* [Database [{database-id :id}] Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:table_id table-id + Metric [{metric-1-id :id} {:name "My Metric" + :table_id table-id :definition {:aggregation [[:count]]}}]] (#'expand-macros/expand-metrics-and-segments (mbql-query @@ -135,7 +118,7 @@ (expect (mbql-query {:source-table 1000 - :aggregation [[:sum [:field-id 18]]] + :aggregation [[:named [:sum [:field-id 18]] "My Metric"]] :filter [:and [:> [:field-id 4] 1] [:is-null [:field-id 7]] @@ -149,7 +132,8 @@ :definition {:filter [:and [:between [:field-id 9] 0 25]]}}] Segment [{segment-2-id :id} {:table_id table-id :definition {:filter [:and [:is-null [:field-id 7]]]}}] - Metric [{metric-1-id :id} {:table_id table-id + Metric [{metric-1-id :id} {:name "My Metric" + :table_id table-id :definition {:aggregation [[:sum [:field-id 18]]] :filter [:and [:= [:field-id 5] "abc"] @@ -165,20 +149,21 @@ :order-by [[:asc [:field-id 1]]]})))) ;; Check that a metric w/ multiple aggregation syntax (nested vector) still works correctly -(datasets/expect-with-drivers (non-timeseries-drivers-with-feature :expression-aggregations) +(datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) [[2 118] [3 39] [4 24]] (tt/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)]] - :breakout [[:field-id (data/id :venues :price)]]}}))))) + (qp.test/format-rows-by [int int] + (qp.test/rows + (qp/process-query + {:database (data/id) + :type :query + :query {:source-table (data/id :venues) + :aggregation [[:metric (u/get-id metric)]] + :breakout [[:field-id (data/id :venues :price)]]}}))))) ;; make sure that we don't try to expand GA "metrics" (#6104) (expect @@ -208,11 +193,21 @@ ;; make sure we can name a :metric (ick) (expect (mbql-query - {:aggregation [[:named [:sum [:field-id 20]] "My Cool Metric"]] + {:aggregation [[:named [:sum [:field-id 20]] "Named Metric"]] :breakout [[:field-id 10]]}) (tt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:named [:metric (u/get-id metric)] "My Cool Metric"]] + (mbql-query {:aggregation [[:named [:metric (u/get-id metric)] "Named Metric"]] + :breakout [[:field-id 10]]})))) + +;; a Metric whose :aggregation is already named should not get wrapped in a `:named` clause +(expect + (mbql-query + {:aggregation [[:named [:sum [:field-id 20]] "My Cool Aggregation"]] + :breakout [[:field-id 10]]}) + (tt/with-temp Metric [metric {:definition {:aggregation [[:named [:sum [:field-id 20]] "My Cool Aggregation"]]}}] + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric (u/get-id metric)]] :breakout [[:field-id 10]]})))) -- GitLab