diff --git a/frontend/src/metabase-lib/lib/Dimension.js b/frontend/src/metabase-lib/lib/Dimension.js index 89d272cc074ba528a5ca17b01d41161b9ea9de4b..4cc2a0f1ba2f49984a31a4496d78fadf17aca720 100644 --- a/frontend/src/metabase-lib/lib/Dimension.js +++ b/frontend/src/metabase-lib/lib/Dimension.js @@ -639,7 +639,9 @@ export class AggregationDimension extends Dimension { const aggregation = this._query && this._query.aggregations()[this.aggregationIndex()]; if (aggregation) { - return aggregation[0] === "named" ? aggregation[1] : aggregation; + return aggregation[0] === "aggregation-options" + ? aggregation[1] + : aggregation; } return null; } @@ -649,13 +651,15 @@ export class AggregationDimension extends Dimension { this._query && this._query.aggregations()[this.aggregationIndex()]; if (aggregation) { // FIXME: query lib - if (aggregation[0] === "named") { - return aggregation[2]; - } else { - const short = aggregation[0]; - // NOTE: special case for "distinct" - return short === "distinct" ? "count" : short; + if (aggregation[0] === "aggregation-options") { + const { "display-name": displayName } = aggregation[2]; + if (displayName) { + return displayName; + } } + const short = aggregation[0]; + // NOTE: special case for "distinct" + return short === "distinct" ? "count" : short; } return null; } diff --git a/frontend/src/metabase/lib/query.js b/frontend/src/metabase/lib/query.js index 3db02d1b56d92d020caaae7b39a1c5667eb921ea..7b8b6663518f51587af823021e7fcc38a64b0053 100644 --- a/frontend/src/metabase/lib/query.js +++ b/frontend/src/metabase/lib/query.js @@ -824,22 +824,30 @@ for (const prop in Q) { import { isMath } from "metabase/lib/expressions"; export const NamedClause = { + hasOptions(clause) { + return Array.isArray(clause) && clause[0] === "aggregation-options"; + }, + getOptions(clause) { + return NamedClause.hasOptions(clause) ? clause[2] : {}; + }, isNamed(clause) { - return Array.isArray(clause) && clause[0] === "named"; + return NamedClause.getOptions(clause)["display-name"]; }, getName(clause) { - return NamedClause.isNamed(clause) ? clause[2] : null; + return NamedClause.getOptions(clause)["display-name"]; }, getContent(clause) { - return NamedClause.isNamed(clause) ? clause[1] : clause; + return NamedClause.hasOptions(clause) ? clause[1] : clause; }, setName(clause, name) { - return ["named", NamedClause.getContent(clause), name]; + return [ + "aggregation-options", + NamedClause.getContent(clause), + { "display-name": name, ...NamedClause.getOptions(clause) }, + ]; }, setContent(clause, content) { - return NamedClause.isNamed(clause) - ? ["named", content, NamedClause.getName(clause)] - : content; + return ["aggregation-options", content, NamedClause.getOptions(clause)]; }, }; diff --git a/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js b/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js index 3551660a0bf5be57af3387d221f74f9bfe5d5407..d645c11f2e32988135034cd1378e5bc09e13547e 100644 --- a/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js +++ b/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js @@ -275,9 +275,9 @@ describe("StructuredQuery unit tests", () => { it("returns a named expression name", () => { expect( makeQueryWithAggregation([ - "named", + "aggregation-options", ["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]], - "Named", + { "display-name": "Named" }, ]).aggregationName(), ).toBe("Named"); }); diff --git a/frontend/test/metabase/lib/query.unit.spec.js b/frontend/test/metabase/lib/query.unit.spec.js index e2f1551aa69f7320cad4f3070e4cd2879f56f837..174434e9fc741b230bea40189c702b8cb8fb2e8d 100644 --- a/frontend/test/metabase/lib/query.unit.spec.js +++ b/frontend/test/metabase/lib/query.unit.spec.js @@ -344,7 +344,13 @@ describe("generateQueryDescription", () => { expect( Query.generateQueryDescription(mockTableMetadata, { "source-table": 1, - aggregation: [["named", ["sum", ["field-id", 1]], "Revenue"]], + aggregation: [ + [ + "aggregation-options", + ["sum", ["field-id", 1]], + { "display-name": "Revenue" }, + ], + ], }), ).toEqual("Orders, Revenue"); }); diff --git a/modules/drivers/bigquery/test/metabase/driver/bigquery_test.clj b/modules/drivers/bigquery/test/metabase/driver/bigquery_test.clj index 272e5dbbf058a29ea62c282c77d8fe9b29b67543..3320e24aaffedf36a00da8041a6c1ba79f76227a 100644 --- a/modules/drivers/bigquery/test/metabase/driver/bigquery_test.clj +++ b/modules/drivers/bigquery/test/metabase/driver/bigquery_test.clj @@ -77,22 +77,10 @@ :type :native :database (data/id)}))) -;; make sure that the bigquery driver can handle named columns with characters that aren't allowed in BQ itself -(datasets/expect-with-driver :bigquery - {:rows [[113]] - :columns ["User_ID_Plus_Venue_ID"]} - (qp.test/rows+column-names - (qp/process-query {:database (data/id) - :type "query" - :query {:source-table (data/id :checkins) - :aggregation [["named" ["max" ["+" ["field-id" (data/id :checkins :user_id)] - ["field-id" (data/id :checkins :venue_id)]]] - "User ID Plus Venue ID"]]}}))) - -;; ok, make sure we actually wrap all of our ag clauses in `:named` clauses with unique names +;; ok, make sure we actually wrap all of our ag clauses in `:aggregation-options` clauses with unique names (defn- aggregation-names [query] (mbql.u/match (-> query :query :aggregation) - [:named _ ag-name] ag-name)) + [:aggregation-options _ {:name ag-name}] ag-name)) ;; make sure queries with two or more of the same aggregation type still work. Aggregations used to be deduplicated ;; here in the BigQuery driver; now they are deduplicated as part of the main QP middleware, but no reason not to keep diff --git a/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj b/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj index b05fecd24d25b4b63aab74ac2f32d62188503264..d48440140bafccdc1b246f4cbf94f084291d2581 100644 --- a/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj +++ b/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj @@ -10,7 +10,9 @@ [clojure.tools.logging :as log] [flatland.ordered.map :as ordered-map] [metabase.driver.druid.js :as js] - [metabase.mbql.util :as mbql.u] + [metabase.mbql + [schema :as mbql.s] + [util :as mbql.u]] [metabase.query-processor [interface :as i] [store :as qp.store]] @@ -81,7 +83,7 @@ (= ag-type :distinct) :distinct___count - (= ag-type :named) + (= ag-type :aggregation-options) (recur (second ag)) ag-type @@ -139,18 +141,23 @@ (dimension-or-metric? field)) -(def ^:private ^:const query-type->default-query - (let [defaults {:intervals ["1900-01-01/2100-01-01"] - :granularity :all - :context {:timeout 60000 - :queryId (str (java.util.UUID/randomUUID))}}] - {::select (merge defaults {:queryType :select - :pagingSpec {:threshold i/absolute-max-results}}) - ::total (merge defaults {:queryType :timeseries}) - ::grouped-timeseries (merge defaults {:queryType :timeseries}) - ::topN (merge defaults {:queryType :topN - :threshold topN-max-results}) - ::groupBy (merge defaults {:queryType :groupBy})})) +(defn- random-query-id [] + (str (java.util.UUID/randomUUID))) + +(defn- query-type->default-query [query-type] + (merge + {:intervals ["1900-01-01/2100-01-01"] + :granularity :all + :context {:timeout 60000 + :queryId (random-query-id)}} + (case query-type + ::select {:queryType :select + :pagingSpec {:threshold i/absolute-max-results}} + ::total {:queryType :timeseries} + ::grouped-timeseries {:queryType :timeseries} + ::topN {:queryType :topN + :threshold topN-max-results} + ::groupBy {:queryType :groupBy}))) @@ -582,12 +589,12 @@ [:max _] [[(or output-name-kwd :max)] {:aggregations [(ag:doubleMax ag-field (or output-name :max))]}]))) -(defn- handle-aggregation - [query-type ag-clause updated-query] +(s/defn ^:private handle-aggregation + [query-type, ag-clause :- mbql.s/Aggregation, updated-query] (let [output-name (annotate/aggregation-name ag-clause) [ag-type ag-field & args] (mbql.u/match-one ag-clause - [:named ag & _] (recur ag) - [_ _ & _] &match)] + [:aggregation-options ag & _] (recur ag) + _ &match)] (if-not (isa? query-type ::ag-query) updated-query (let [[projections ag-clauses] (create-aggregation-clause output-name ag-type ag-field args)] @@ -595,49 +602,61 @@ (update :projections #(vec (concat % projections))) (update :query #(merge-with concat % ag-clauses))))))) -(defn- add-expression-aggregation-output-names - [[operator & args :as expression]] - (if (mbql.u/is-clause? :named expression) - (update (vec expression) 1 add-expression-aggregation-output-names) - (into [operator] - (for [arg args] - (cond - (number? arg) - arg +(defn- deduplicate-aggregation-options [expression] + (mbql.u/replace expression + [:aggregation-options [:aggregation-options ag options-1] options-2] + [:aggregation-options ag (merge options-1 options-2)])) - (mbql.u/is-clause? :named arg) - arg +(def ^:private ^:dynamic *query-unique-identifier-counter* + "Counter used for generating unique identifiers for use in the query. Bound to `(atom 0)` and incremented on each use + as the MBQL query is compiled." + nil) - (mbql.u/is-clause? #{:count :avg :distinct :stddev :sum :min :max} arg) - [:named arg (name (gensym (str "___" (name (first arg)) "_")))] +(defn- aggregation-unique-identifier [clause] + (format "__%s_%d" (name clause) (first (swap-vals! *query-unique-identifier-counter* inc)))) + +(defn- add-expression-aggregation-output-names + [expression] + (mbql.u/replace expression + [:aggregation-options ag options] + (deduplicate-aggregation-options [:aggregation-options (add-expression-aggregation-output-names ag) options]) - (mbql.u/is-clause? #{:+ :- :/ :*} arg) - (add-expression-aggregation-output-names arg)))))) + [(clause :guard #{:count :avg :distinct :stddev :sum :min :max}) & _] + [:aggregation-options &match {:name (aggregation-unique-identifier clause)}])) (defn- expression-post-aggregation [[operator & args, :as expression]] - (if (mbql.u/is-clause? :named expression) + (mbql.u/match-one expression ;; If it's a named expression, we want to preserve the included name, so recurse, but merge in the name + [:aggregation-options ag _] (merge (expression-post-aggregation (second expression)) - {:name (annotate/aggregation-name expression {:top-level? true})}) + {:name (annotate/aggregation-name expression)}) + + _ {:type :arithmetic - :name (annotate/aggregation-name expression {:top-level? true}) + :name (annotate/aggregation-name expression) :fn operator - :fields (for [arg args] - (cond - (number? arg) - {:type :constant, :name (str arg), :value arg} + :fields (vec (for [arg args] + (mbql.u/match-one arg + number? + {:type :constant, :name (str &match), :value &match} + + [:aggregation-options _ (options :guard :name)] + {:type :fieldAccess, :fieldName (:name options)} + + #{:+ :- :/ :*} + (expression-post-aggregation &match) - (mbql.u/is-clause? :named arg) - {:type :fieldAccess, :fieldName (last arg)} + ;; we should never get here unless our code is B U S T E D + _ + (throw (ex-info (str (tru "Expected :aggregation-options, constant, or expression.")) + {:type :bug, :input arg})))))})) - (mbql.u/is-clause? #{:+ :- :/ :*} arg) - (expression-post-aggregation arg)))})) (declare handle-aggregations) (defn- expression->actual-ags - "Return a flattened list of actual aggregations that are needed for EXPRESSION." + "Return a flattened list of actual aggregations that are needed for `expression`." [[_ & args]] (apply concat (for [arg args :when (not (number? arg))] @@ -647,7 +666,7 @@ (defn- unwrap-name [x] - (if (mbql.u/is-clause? :named x) + (if (mbql.u/is-clause? :aggregation-options x) (second x) x)) @@ -662,24 +681,23 @@ post-agg (expression-post-aggregation expression)] (-> updated-query (update :projections conj (keyword (:name post-agg))) - (update :query #(merge-with concat % {:postAggregations [post-agg]}))))) + (update-in [:query :postAggregations] concat [post-agg])))) (defn- handle-aggregations [query-type {aggregations :aggregation} updated-query] - (loop [[ag & more] aggregations, query updated-query] - (cond - (and (mbql.u/is-clause? :named ag) - (mbql.u/is-clause? #{:+ :- :/ :*} (second ag))) - (handle-expression-aggregation query-type ag query) - - (mbql.u/is-clause? #{:+ :- :/ :*} ag) - (handle-expression-aggregation query-type ag query) + (reduce + (fn [updated-query aggregation] + (mbql.u/match-one aggregation + [:aggregation-options [(_ :guard #{:+ :- :/ :*}) & _] _] + (handle-expression-aggregation query-type &match updated-query) - (not ag) - query + #{:+ :- :/ :*} + (handle-expression-aggregation query-type &match updated-query) - :else - (recur more (handle-aggregation query-type ag query))))) + _ + (handle-aggregation query-type &match updated-query))) + updated-query + aggregations)) ;;; ------------------------------------------------ handle-breakout ------------------------------------------------- @@ -871,7 +889,7 @@ :distinct :distinct___count - [:named wrapped-ag & _] + [:aggregation-options wrapped-ag _] (recur wrapped-ag) [(ag-type :guard keyword?) & _] @@ -1139,7 +1157,8 @@ ;; Merge `:settings` into the inner query dict so the QP has access to it (let [query (assoc (:query query) :settings (:settings query))] - (binding [*query* query] + (binding [*query* query + *query-unique-identifier-counter* (atom 0)] (build-druid-query query)))) @@ -1188,7 +1207,7 @@ (json/parse-string query keyword) query) query-type (or query-type - (keyword "metabase.driver.druid.query-processor" (name (:queryType query)))) + (keyword (namespace ::query) (name (:queryType query)))) post-proc-map (->> query (do-query details) (post-process query-type projections diff --git a/modules/drivers/druid/test/metabase/driver/druid/query_processor_test.clj b/modules/drivers/druid/test/metabase/driver/druid/query_processor_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..7eb078516f614cec7edce40cb2c92bbad68d1831 --- /dev/null +++ b/modules/drivers/druid/test/metabase/driver/druid/query_processor_test.clj @@ -0,0 +1,48 @@ +(ns metabase.driver.druid.query-processor-test + "Some tests to make sure the Druid Query Processor is generating sane Druid queries when compiling MBQL." + (:require [metabase + [driver :as driver] + [query-processor :as qp]] + [metabase.driver.druid.query-processor :as druid.qp] + [metabase.test.data :as data] + [metabase.test.data.datasets :as datasets] + [metabase.timeseries-query-processor-test.util :as tqpt])) + +(defn- do-query->native [query] + (driver/with-driver :druid + (tqpt/with-flattened-dbdef + (with-redefs [druid.qp/random-query-id (constantly "<Query ID>")] + (qp/query->native + query))))) + +(defmacro ^:private query->native [query] + `(do-query->native + (data/mbql-query ~'checkins + ~query))) + +(datasets/expect-with-driver :druid + {:projections [:venue_price :__count_0 :expression] + :query {:queryType :topN + :threshold 1000 + :granularity :all + :dataSource "checkins" + :dimension "venue_price" + :context {:timeout 60000, :queryId "<Query ID>"} + :postAggregations [{:type :arithmetic + :name "expression" + :fn :* + :fields [{:type :fieldAccess, :fieldName "__count_0"} + {:type :constant, :name "10", :value 10}]}] + :intervals ["1900-01-01/2100-01-01"] + :metric {:type :alphaNumeric} + :aggregations + [{:type :filtered + :filter + {:type :not + :field {:type :selector, :dimension "id", :value nil}} + :aggregator {:type :count, :name "__count_0"}}]} + :query-type ::druid.qp/topN + :mbql? true} + (query->native + {:aggregation [[:* [:count $id] 10]] + :breakout [$venue_price]})) diff --git a/modules/drivers/druid/test/metabase/driver/druid_test.clj b/modules/drivers/druid/test/metabase/driver/druid_test.clj index 2f37d59eded1e1268f85849332d0f9793b6e6481..609023cfb3970ff2ec01d517c02cbad21085501c 100644 --- a/modules/drivers/druid/test/metabase/driver/druid_test.clj +++ b/modules/drivers/druid/test/metabase/driver/druid_test.clj @@ -325,7 +325,7 @@ ["4" 245.0]] (qp.test/rows (druid-query - {:aggregation [[:named [:sum [:+ $venue_price 1]] "New Price"]] + {:aggregation [[:aggregation-options [:sum [:+ $venue_price 1]] {:name "New Price"}]] :breakout [$venue_price]}))) ;; check that we can name an expression aggregation w/ expression at top-level @@ -337,7 +337,7 @@ :columns ["venue_price" "Sum-41"]} (qp.test/rows+column-names (druid-query - {:aggregation [[:named [:- [:sum $venue_price] 41] "Sum-41"]] + {:aggregation [[:aggregation-options [:- [:sum $venue_price] 41] {:name "Sum-41"}]] :breakout [$venue_price]}))) ;; check that we can handle METRICS (ick) inside expression aggregation clauses diff --git a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj index 82e4271ebab71bc8cce26bc322826ad50fd5e174..b1b809dc0a7805b3dfe14b4274d9de108e418995 100644 --- a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj +++ b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj @@ -390,29 +390,49 @@ ;;; -------------------------------------------------- aggregation --------------------------------------------------- -(defn- aggregation->rvalue [[aggregation-type arg & args]] - {:pre [(keyword? aggregation-type)]} - (if-not arg - (case aggregation-type - :count {$sum 1}) - (case aggregation-type - :named (recur arg) - :avg {$avg (->rvalue arg)} - :count {$sum {$cond {:if (->rvalue arg) - :then 1 - :else 0}}} - :distinct {$addToSet (->rvalue arg)} - :sum {$sum (->rvalue arg)} - :min {$min (->rvalue arg)} - :max {$max (->rvalue arg)} - :sum-where (let [[pred] args] - {$sum {$cond {:if (parse-cond pred) - :then (->rvalue arg) - :else 0}}}) - :count-where (recur [:sum-where [:value 1] arg])))) +(defn- aggregation->rvalue [ag] + (mbql.u/match-one ag + [:aggregation-options ag _] + (recur ag) + + [:count] + {$sum 1} + + [:count arg] + {$sum {$cond {:if (->rvalue arg) + :then 1 + :else 0}}} + [:avg arg] + {$avg (->rvalue arg)} + + + [:distinct arg] + {$addToSet (->rvalue arg)} + + [:sum arg] + {$sum (->rvalue arg)} + + [:min arg] + {$min (->rvalue arg)} + + [:max arg] + {$max (->rvalue arg)} + + [:sum-where arg pred] + {$sum {$cond {:if (parse-cond pred) + :then (->rvalue arg) + :else 0}}} + + [:count-where pred] + (recur [:sum-where [:value 1] pred]) + + :else + (throw + (ex-info (str (tru "Don't know how to handle aggregation {0}" ag)) + {:type :invalid-query, :clause ag})))) (defn- unwrap-named-ag [[ag-type arg :as ag]] - (if (= ag-type :named) + (if (= ag-type :aggregation-options) (recur arg) ag)) diff --git a/project.clj b/project.clj index 51fac8c50c9a0502b0ccfa00ce9bead9078aa74c..22a3847376969c2e597e1e1dc68eb009ef107b94 100644 --- a/project.clj +++ b/project.clj @@ -99,7 +99,7 @@ com.sun.jmx/jmxri]] [medley "1.2.0"] ; lightweight lib of useful functions [metabase/connection-pool "1.0.2"] ; simple wrapper around C3P0. JDBC connection pools - [metabase/mbql "1.2.0"] ; MBQL language schema & util fns + [metabase/mbql "1.3.1"] ; MBQL language schema & util fns [metabase/throttle "1.0.1"] ; Tools for throttling access to API endpoints and other code pathways [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"] ; add the `javax.xml.bind` classes which we're still using but were removed in Java 11 [net.sf.cssbox/cssbox "4.12" :exclusions [org.slf4j/slf4j-api]] ; HTML / CSS rendering diff --git a/src/metabase/automagic_dashboards/core.clj b/src/metabase/automagic_dashboards/core.clj index 58c2995c8ac4658ad86ebce585e2d74e29c526bb..2249a19195ea83ca395cb9ffdaef9109f68a8d84 100644 --- a/src/metabase/automagic_dashboards/core.clj +++ b/src/metabase/automagic_dashboards/core.clj @@ -89,7 +89,7 @@ (complement mbql.u/ga-metric-or-segment?))) (def ^:private ^{:arglists '([metric])} custom-expression? - (partial mbql.u/is-clause? :named)) + (partial mbql.u/is-clause? :aggregation-options)) (def ^:private ^{:arglists '([metric])} adhoc-metric? (complement (some-fn saved-metric? custom-expression?))) diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index a53dd8679f93ce5ce9cc047d331bf01e32942b68..0037187d8c4f1bac9b9d0ffdef03450c824d19aa 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -52,6 +52,15 @@ below. The QP binds the driver this way in the `bind-driver` middleware." nil) +(defn do-with-driver + "Impl for `with-driver`." + [driver f] + ;; it's substantially faster not to call `binding` in the first place if it's already bound + (if (= *driver* driver) + (f) + (binding [*driver* driver] + (f)))) + (defmacro with-driver "Bind current driver to `driver` and execute `body`. @@ -59,8 +68,7 @@ ...)" {:style/indent 1} [driver & body] - `(binding [*driver* ~driver] - ~@body)) + `(do-with-driver ~driver (fn [] ~@body))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -537,12 +545,24 @@ (defmethod supports? [::driver :case-sensitivity-string-filter-options] [_ _] true) -(defmulti format-custom-field-name - "Return the custom name passed via an MBQL `:named` clause so it matches the way it is returned in the results. This - is used by the post-processing annotation stage to find the correct metadata to include with fields in the results. - The default implementation is `identity`, meaning the resulting field will have exactly the same name as passed to - the `:named` clause. Certain drivers like Redshift always lowercase these names, so this method is provided for - those situations." +(defmulti ^:deprecated format-custom-field-name + "Prior to Metabase 0.33.0, you could specifiy custom names for aggregations in MBQL by wrapping the clause in a + `:named` clause: + + [:named [:count] \"My Count\"] + + This name was used for both the `:display_name` in the query results, and for the `:name` used as an alias in the + query (e.g. the right-hand side of a SQL `AS` expression). Because some custom display names weren't allowed by some + drivers, or would be transformed in some way (for example, Redshift always lowercases custom aliases), this method + was needed so we could match the name we had given the column with the one in the query results. + + In 0.33.0, we started using `:named` internally to alias aggregations in middleware in *all* queries to prevent + issues with referring to multiple aggregations of the same type when that query was used as a source query. + See [#9767](https://github.com/metabase/metabase/issues/9767) for more details. After this change, it became + desirable to differentiate between such internally-generated aliases and display names, which need not be used in + the query at all; thus in MBQL 1.3.0 [`:named` was replaced by the more general + `:aggregation-options`](https://github.com/metabase/mbql/pull/7). Because user-generated names are no longer used as + aliases in native queries themselves, this method is no longer needed and will be removed in a future release." {:arglists '([driver custom-field-name])} dispatch-on-initialized-driver :hierarchy #'hierarchy) diff --git a/src/metabase/driver/sql/query_processor.clj b/src/metabase/driver/sql/query_processor.clj index 12afab53945d873ef093ee20bb6c9eb880a77ba7..9f9df2324b396d75eb9b88a7a1343e85d87ec26f 100644 --- a/src/metabase/driver/sql/query_processor.clj +++ b/src/metabase/driver/sql/query_processor.clj @@ -273,15 +273,18 @@ (hsql/call :/ (->honeysql driver [:count-where pred]) :%count.*)) ;; actual handling of the name is done in the top-level clause handler for aggregations -(defmethod ->honeysql [:sql :named] [driver [_ ag ag-name]] +(defmethod ->honeysql [:sql :aggregation-options] [driver [_ ag]] (->honeysql driver ag)) ;; aggregation REFERENCE e.g. the ["aggregation" 0] fields we allow in order-by (defmethod ->honeysql [:sql :aggregation] [driver [_ index]] (mbql.u/match-one (mbql.u/aggregation-at-index *query* index *nested-query-level*) - [:named _ ag-name & _] - (->honeysql driver (hx/identifier :field-alias ag-name)) + [:aggregation-options ag (options :guard :name)] + (->honeysql driver (hx/identifier :field-alias (:name options))) + + [:aggregation-options ag _] + (recur ag) ;; For some arcane reason we name the results of a distinct aggregation "count", everything else is named the ;; same as the aggregation diff --git a/src/metabase/query_processor/middleware/annotate.clj b/src/metabase/query_processor/middleware/annotate.clj index 36f621f6710e2491e9420007c546cf5bbb3ae26f..2194441b46dcf54e168e17916ad7ced95db7b2d2 100644 --- a/src/metabase/query_processor/middleware/annotate.clj +++ b/src/metabase/query_processor/middleware/annotate.clj @@ -175,13 +175,7 @@ ;;; ---------------------------------------------- Aggregate Field Info ---------------------------------------------- -(def ^:private arithmetic-op->text - {:+ "add" - :- "sub" - :/ "div" - :* "mul"}) - -(defn- expression-ag-arg->name +(defn- expression-arg-display-name "Generate an appropriate name for an `arg` in an expression aggregation." [recursive-name-fn arg] (mbql.u/match-one arg @@ -199,29 +193,24 @@ _ &match)) (s/defn aggregation-name :- su/NonBlankString - "Return an appropriate field *and* display name for an `:aggregation` subclause (an aggregation or - expression). Takes an options map as schema won't support passing keypairs directly as a varargs. `{:top-level? - true}` will cause a name to be generated that will appear in the results, other names with a leading `__` will be - trimmed on some backends. + "Return an appropriate aggregation name/alias *used inside a query* for an `:aggregation` subclause (an aggregation + or expression). Takes an options map as schema won't support passing keypairs directly as a varargs. These names are also used directly in queries, e.g. in the equivalent of a SQL `AS` clause." - [ag-clause :- mbql.s/Aggregation & [{:keys [top-level? recursive-name-fn], :or {recursive-name-fn aggregation-name}}]] + [ag-clause :- mbql.s/Aggregation & [{:keys [recursive-name-fn], :or {recursive-name-fn aggregation-name}}]] (when-not driver/*driver* (throw (Exception. (str (tru "*driver* is unbound."))))) (mbql.u/match-one ag-clause - ;; if a custom name was provided use it. Some drivers have limits on column names, so call - ;; `format-custom-field-name` so they can modify it as needed. - [:named _ ag-name & _] - (driver/format-custom-field-name driver/*driver* ag-name) + [:aggregation-options _ (options :guard :name)] + (:name options) + + [:aggregation-options ag _] + (recur ag) ;; For unnamed expressions, just compute a name like "sum + count" ;; Top level expressions need a name without a leading __ as those are automatically removed from the results [(operator :guard #{:+ :- :/ :*}) & args] - (str (when top-level? - (str (arithmetic-op->text operator) - "__")) - (str/join (str " " (name operator) " ") - (map (partial expression-ag-arg->name recursive-name-fn) args))) + "expression" ;; for historic reasons a distinct count clause is still named "count". Don't change this or you might break FE ;; stuff that keys off of aggregation names. @@ -253,18 +242,15 @@ "Return an appropriate user-facing display name for an aggregation clause." [ag-clause] (mbql.u/match-one ag-clause - ;; if the `named` clause has options, and *explicity* specifies {:use-as-display-name? false}, generate a name based - ;; on the wrapped clause instead - [:named wrapped-clause _ (_ :guard #(false? (:use-as-display-name? %)))] - (aggregation-display-name wrapped-clause) + [:aggregation-options _ (options :guard :display-name)] + (:display-name options) - ;; otherwise for a `named` aggregation use the supplied name as the display name - [:named _ ag-name & _] - ag-name + [:aggregation-options ag _] + (recur ag) [(operator :guard #{:+ :- :/ :*}) & args] (str/join (format " %s " (name operator)) - (map (partial expression-ag-arg->name aggregation-arg-display-name) args)) + (map (partial expression-arg-display-name aggregation-arg-display-name) args)) [:count] (str (tru "count")) @@ -301,8 +287,8 @@ ; `match` pattern for field clauses below [inner-query :- su/Map, clause] (mbql.u/match-one clause - ;; ok, if this is a named aggregation recurse so we can get information about the ag we are naming - [:named ag & _] + ;; ok, if this is a aggregation w/ options recurse so we can get information about the ag it wraps + [:aggregation-options ag _] (merge (col-info-for-aggregation-clause inner-query ag) (ag->name-info &match)) @@ -338,14 +324,14 @@ ;; hardcoding these types is fine; In the future when we extend Expressions to handle more functionality ;; we'll want to introduce logic that associates a return type with a given expression. But this will work ;; for the purposes of a patch release. - [(_ :guard #{:expression :+ :- :/ :*}) & _] + #{:expression :+ :- :/ :*} (merge (infer-expression-type &match) (when (mbql.preds/Aggregation? &match) (ag->name-info &match))) ;; get name/display-name of this ag - [(_ :guard keyword?) arg & args] + [(_ :guard keyword?) arg & _] (merge (col-info-for-aggregation-clause inner-query arg) (ag->name-info &match)))) diff --git a/src/metabase/query_processor/middleware/expand_macros.clj b/src/metabase/query_processor/middleware/expand_macros.clj index 486e19a2fe7a7a8ebbaf5a27e753daa75969463b..df2cfb9cb44fc71e88d5b6cfa071a5302ba66ad4 100644 --- a/src/metabase/query_processor/middleware/expand_macros.clj +++ b/src/metabase/query_processor/middleware/expand_macros.clj @@ -78,24 +78,42 @@ (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)) + {:keys [use-metric-name-as-display-name?]} :- {:use-metric-name-as-display-name? s/Bool}] + (if-not use-metric-name-as-display-name? aggregation - [:named aggregation metric-name])) + ;; try to give the resulting aggregation the name of the Metric it came from, unless it already has a display + ;; name in which case keep that name + (mbql.u/match-one aggregation + [:aggregation-options _ (_ :guard :display-name)] + &match + + [:aggregation-options ag options] + [:aggregation-options ag (assoc options :display-name metric-name)] + + _ + [:aggregation-options &match {:display-name 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))))))] + (throw (ex-info (str (tru "Metric {0} does not exist, or is invalid." metric-id)) + {:type :invalid-query, :metric 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] - + ;; if metric is wrapped in aggregation options that give it a display name, expand the metric but do not name it + [:aggregation-options [:metric (metric-id :guard (complement mbql.u/ga-id?))] (options :guard :display-name)] + [:aggregation-options + (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? false}) + options] + + ;; if metric is wrapped in aggregation options that *do not* give it a display name, expand the metric and then + ;; merge the options + [:aggregation-options [:metric (metric-id :guard (complement mbql.u/ga-id?))] options] + (let [[_ ag ag-options] (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? true})] + [:aggregation-options ag (merge ag-options options)]) + + ;; otherwise for unwrapped metrics expand them in-place [:metric (metric-id :guard (complement mbql.u/ga-id?))] - (metric-info->ag-clause (metric metric-id) {:wrap-in-named? true})))) + (metric-info->ag-clause (metric metric-id) {:use-metric-name-as-display-name? true})))) (defn- add-metrics-clauses "Add appropriate `filter` and `aggregation` clauses for a sequence of Metrics. diff --git a/test/metabase/driver/sql/query_processor_test.clj b/test/metabase/driver/sql/query_processor_test.clj index 61e2a77440503b7e4ad260685f15a91a4dd56d9f..2cc90efbed657dec92124fe9104c7b9fae05d966 100644 --- a/test/metabase/driver/sql/query_processor_test.clj +++ b/test/metabase/driver/sql/query_processor_test.clj @@ -143,18 +143,18 @@ :fields :none}]}))))) -;; This query tests that named aggregations are handled correctly +;; Check that named aggregations are handled correctly (expect {:select [[(id :field "PUBLIC" "VENUES" "PRICE") (id :field-alias "PRICE")] - [(hsql/call :avg (id :field "PUBLIC" "VENUES" "CATEGORY_ID")) (id :field-alias "my_average")]] + [(hsql/call :avg (id :field "PUBLIC" "VENUES" "CATEGORY_ID")) (id :field-alias "avg_2")]] :from [(id :table "PUBLIC" "VENUES")] - :group-by [(id :field "PUBLIC" "VENUES" "PRICE")], - :order-by [[(id :field-alias "my_average") :asc]]} + :group-by [(id :field "PUBLIC" "VENUES" "PRICE")] + :order-by [[(id :field-alias "avg_2") :asc]]} (qp.test-util/with-everything-store (metabase.driver/with-driver :h2 (#'sql.qp/mbql->honeysql ::id-swap (data/mbql-query venues - {:aggregation [[:named [:avg $category_id] "my_average"]] - :breakout [$price] - :order-by [[:asc [:aggregation 0]]]}))))) + {:aggregation [[:aggregation-options [:avg $category_id] {:name "avg_2"}]] + :breakout [$price] + :order-by [[:asc [:aggregation 0]]]}))))) diff --git a/test/metabase/query_processor/middleware/add_source_metadata_test.clj b/test/metabase/query_processor/middleware/add_source_metadata_test.clj index 3f44f68956bc3739b7cab27e1efd03d7bbb1b88a..00775833423677ff1557d42332d1e5b487402e47 100644 --- a/test/metabase/query_processor/middleware/add_source_metadata_test.clj +++ b/test/metabase/query_processor/middleware/add_source_metadata_test.clj @@ -93,25 +93,58 @@ :aggregation [[:avg $id]] :breakout [$price]}}))) -;; Can we add source metadata for a source query that has a named aggregation? +;; Can we add source metadata for a source query that has a named aggregation? (w/ `:name` and `:display-name`) (expect (data/mbql-query venues {:source-query {:source-table $$venues - :aggregation [[:named [:avg $id] "my_cool_aggregation"]] + :aggregation [[:aggregation-options + [:avg $id] + {:name "some_generated_name", :display-name "My Cool Ag"}]] :breakout [$price]} :source-metadata (concat (venues-source-metadata :price) - [{:name "my_cool_aggregation" - :display_name "my_cool_aggregation" + [{:name "some_generated_name" + :display_name "My Cool Ag" :base_type :type/BigInteger :special_type :type/PK :settings nil}])}) (add-source-metadata (data/mbql-query venues {:source-query {:source-table $$venues - :aggregation [[:named [:avg $id] "my_cool_aggregation"]] + :aggregation [[:aggregation-options + [:avg $id] + {:name "some_generated_name", :display-name "My Cool Ag"}]] :breakout [$price]}}))) +(defn- source-metadata [query] + (get-in query [:query :source-metadata] query)) + +;; Can we add source metadata for a source query that has a named aggregation? (w/ `:name` only) +(expect + [{:name "some_generated_name" + :display_name "average of ID" + :base_type :type/BigInteger + :special_type :type/PK + :settings nil}] + (source-metadata + (add-source-metadata + (data/mbql-query venues + {:source-query {:source-table $$venues + :aggregation [[:aggregation-options [:avg $id] {:name "some_generated_name"}]]}})))) + +;; Can we add source metadata for a source query that has a named aggregation? (w/ `:display-name` only) +(expect + [{:name "avg" + :display_name "My Cool Ag" + :base_type :type/BigInteger + :special_type :type/PK + :settings nil}] + (source-metadata + (add-source-metadata + (data/mbql-query venues + {:source-query {:source-table $$venues + :aggregation [[:aggregation-options [:avg $id] {:display-name "My Cool Ag"}]]}})))) + ;; Can we automatically add source metadata to the parent level of a query? If the source query has a source query ;; with source metadata (expect diff --git a/test/metabase/query_processor/middleware/annotate_test.clj b/test/metabase/query_processor/middleware/annotate_test.clj index 6cc37387195bb888d3c7a26940a431a4c73526ee..0020e444162da55e5961563b14ea8e65fd5879d1 100644 --- a/test/metabase/query_processor/middleware/annotate_test.clj +++ b/test/metabase/query_processor/middleware/annotate_test.clj @@ -264,11 +264,11 @@ (aggregation-names [:sum [:field-id (data/id :venues :id)]])) (expect - {:name "count + 1", :display_name "count + 1"} + {:name "expression", :display_name "count + 1"} (aggregation-names [:+ [:count] 1])) (expect - {:name "min + (2 * avg)" + {:name "expression" :display_name "minimum value of ID + (2 * average of Price)"} (aggregation-names [:+ @@ -276,7 +276,7 @@ [:* 2 [:avg [:field-id (data/id :venues :price)]]]])) (expect - {:name "min + (2 * avg * 3 * (max - 4))" + {:name "expression" :display_name "minimum value of ID + (2 * average of Price * 3 * (maximum value of Category ID - 4))"} (aggregation-names [:+ @@ -289,16 +289,29 @@ [:max [:field-id (data/id :venues :category_id)]] 4]]])) +;; `aggregation-options` (`:name` and `:display-name`) (expect - {:name "x", :display_name "x"} + {:name "generated_name", :display_name "User-specified Name"} (aggregation-names - [:named + [:aggregation-options [:+ [:min [:field-id (data/id :venues :id)]] [:* 2 [:avg [:field-id (data/id :venues :price)]]]] - "x"])) + {:name "generated_name", :display-name "User-specified Name"}])) +;; `aggregation-options` (`:name` only) (expect - {:name "My Cool Aggregation", :display_name "My Cool Aggregation"} - (aggregation-names [:named [:avg [:field-id (data/id :venues :price)]] "My Cool Aggregation"])) + {:name "generated_name", :display_name "minimum value of ID + (2 * average of Price)"} + (aggregation-names + [:aggregation-options + [:+ [:min [:field-id (data/id :venues :id)]] [:* 2 [:avg [:field-id (data/id :venues :price)]]]] + {:name "generated_name"}])) + +;; `aggregation-options` (`:display-name` only) +(expect + {:name "expression", :display_name "User-specified Name"} + (aggregation-names + [:aggregation-options + [:+ [:min [:field-id (data/id :venues :id)]] [:* 2 [:avg [:field-id (data/id :venues :price)]]]] + {:display-name "User-specified Name"}])) ;; make sure custom aggregation names get included in the col info (defn- col-info-for-aggregation-clause [clause] @@ -308,7 +321,7 @@ (expect {:base_type :type/Float :special_type :type/Number - :name "count / 2" + :name "expression" :display_name "count / 2"} (col-info-for-aggregation-clause [:/ [:count] 2])) @@ -321,31 +334,41 @@ (data/$ids venues (col-info-for-aggregation-clause [:sum [:+ $price 1]])))) -;; if a `:named` aggregation supplies optional `:use-as-display-name?` `options` we should respect that -;; `use-as-disply-name?` is `true` by default, e.g. in cases where the user supplies the names themselves +;; col info for `:aggregation-options` (`:name` and `:display-name`) (expect {:base_type :type/Integer :special_type :type/Category :settings nil - :name "sum" - :display_name "sum"} + :name "sum_2" + :display_name "My custom name"} (qp.test-util/with-everything-store (data/$ids venues - (col-info-for-aggregation-clause [:named [:sum $price] "sum" {:use-as-display-name? true}])))) + (col-info-for-aggregation-clause + [:aggregation-options [:sum $price] {:name "sum_2", :display-name "My custom name"}])))) -;; `use-as-display-name?` will normally be `false` when the `:named` clause is generated automatically, e.g. by the -;; `pre-alias-aggregations` middleware. In this case we want to use the name internally in the query to prevent -;; duplicate column names, but do not want to use them as display names. See -;; https://github.com/metabase/mbql/releases/tag/1.2.0 for detailed explanation. +;; col info for `:aggregation-options` (`:name` only) (expect {:base_type :type/Integer :special_type :type/Category :settings nil - :name "sum" + :name "sum_2" :display_name "sum of Price"} (qp.test-util/with-everything-store (data/$ids venues - (col-info-for-aggregation-clause [:named [:sum $price] "sum" {:use-as-display-name? false}])))) + (col-info-for-aggregation-clause + [:aggregation-options [:sum $price] {:name "sum_2"}])))) + +;; col info for `:aggregation-options` (`:display-name` only) +(expect + {:base_type :type/Integer + :special_type :type/Category + :settings nil + :name "sum" + :display_name "My Custom Name"} + (qp.test-util/with-everything-store + (data/$ids venues + (col-info-for-aggregation-clause + [:aggregation-options [:sum $price] {:display-name "My Custom Name"}])))) ;; if a driver is kind enough to supply us with some information about the `:cols` that come back, we should include ;; that information in the results. Their information should be preferred over ours @@ -394,22 +417,24 @@ :display_name "count_2" :source :aggregation :field_ref [:aggregation 3]}]} - (binding [driver/*driver* :h2] - ((annotate/add-column-info (constantly {:cols [{:name "count" - :display_name "count" - :base_type :type/Number} - {:name "sum" - :display_name "sum" - :base_type :type/Number} - {:name "count" - :display_name "count" - :base_type :type/Number} - {:name "count_2" - :display_name "count_2" - :base_type :type/Number}] - :columns ["count" "sum" "count" "count_2"]})) + (driver/with-driver :h2 + ((annotate/add-column-info (constantly {:cols [{:name "count" + :display_name "count" + :base_type :type/Number} + {:name "sum" + :display_name "sum" + :base_type :type/Number} + {:name "count" + :display_name "count" + :base_type :type/Number} + {:name "count_2" + :display_name "count_2" + :base_type :type/Number}]})) (data/mbql-query venues - {:aggregation [[:count] [:sum] [:count] [:named [:count] "count_2"]]})))) + {:aggregation [[:count] + [:sum] + [:count] + [:aggregation-options [:count] {:display-name "count_2"}]]})))) ;; make sure expressions come back with the right set of keys, including `:expression_name` (#8854) (expect @@ -423,12 +448,36 @@ (-> (qp.test-util/with-everything-store ((annotate/add-column-info (constantly {})) (data/mbql-query venues - {:expressions {:discount_price [:* 0.9 [:field-id $price]]} + {:expressions {"discount_price" [:* 0.9 $price]} :fields [$name [:expression "discount_price"]] :limit 10}))) :cols second)) + +;; make sure multiple expressions come back with deduplicated names +(expect + [{:base_type :type/Float + :special_type :type/Number + :name "expression" + :display_name "0.9 * average of Price" + :source :aggregation + :field_ref [:aggregation 0]} + {:base_type :type/Float + :special_type :type/Number + :name "expression_2" + :display_name "0.8 * average of Price" + :source :aggregation + :field_ref [:aggregation 1]}] + (-> (driver/with-driver :h2 + (qp.test-util/with-everything-store + ((annotate/add-column-info (constantly {})) + (data/mbql-query venues + {:aggregation [[:* 0.9 [:avg $price]] + [:* 0.8 [:avg $price]]] + :limit 10})))) + :cols)) + (expect {:name "prev_month" :display_name "prev_month" diff --git a/test/metabase/query_processor/middleware/expand_macros_test.clj b/test/metabase/query_processor/middleware/expand_macros_test.clj index 86937437b33d72e0bdb5baa408980ba2df3c541d..4c98f2ebf4f5ae656b43abe768f8be56459179ba 100644 --- a/test/metabase/query_processor/middleware/expand_macros_test.clj +++ b/test/metabase/query_processor/middleware/expand_macros_test.clj @@ -1,7 +1,6 @@ (ns metabase.query-processor.middleware.expand-macros-test (:require [expectations :refer [expect]] [metabase - [query-processor :as qp] [query-processor-test :as qp.test] [util :as u]] [metabase.models @@ -55,7 +54,7 @@ ;; just a metric (w/out nested segments) (expect (mbql-query - {:aggregation [[:named [:count] "Toucans in the rainforest"]] + {:aggregation [[:aggregation-options [:count] {:display-name "Toucans in the rainforest"}]] :filter [:and [:> [:field-id 4] 1] [:= [:field-id 5] "abc"]] @@ -78,7 +77,7 @@ (expect (mbql-query {:source-table 1000 - :aggregation [[:named [:count] "ABC Fields"]] + :aggregation [[:aggregation-options [:count] {:display-name "ABC Fields"}]] :filter [:= [:field-id 5] "abc"] :breakout [[:field-id 17]] :order-by [[:asc [:field-id 1]]]}) @@ -98,7 +97,7 @@ ;; metric w/ no filter definition (expect (mbql-query - {:aggregation [[:named [:count] "My Metric"]] + {:aggregation [[:aggregation-options [:count] {:display-name "My Metric"}]] :filter [:= [:field-id 5] "abc"] :breakout [[:field-id 17]] :order-by [[:asc [:field-id 1]]]}) @@ -118,7 +117,7 @@ (expect (mbql-query {:source-table 1000 - :aggregation [[:named [:sum [:field-id 18]] "My Metric"]] + :aggregation [[:aggregation-options [:sum [:field-id 18]] {:display-name "My Metric"}]] :filter [:and [:> [:field-id 4] 1] [:is-null [:field-id 7]] @@ -158,12 +157,9 @@ :filter [:> [:field-id (data/id :venues :price)] 1]}}] (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)]]}}))))) + (data/run-mbql-query venues + {:aggregation [[:metric (u/get-id metric)]] + :breakout [$price]}))))) ;; make sure that we don't try to expand GA "metrics" (#6104) (expect @@ -193,19 +189,48 @@ ;; make sure we can name a :metric (ick) (expect (mbql-query - {:aggregation [[:named [:sum [:field-id 20]] "Named Metric"]] + {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "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)] "Named Metric"]] + (mbql-query {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:display-name "Named Metric"}]] :breakout [[:field-id 10]]})))) -;; a Metric whose :aggregation is already named should not get wrapped in a `:named` clause +;; if the `:metric` is wrapped in aggregation options that do *not* give it a display name, `:display-name` should be +;; added to the options (expect (mbql-query - {:aggregation [[:named [:sum [:field-id 20]] "My Cool Aggregation"]] + {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] :breakout [[:field-id 10]]}) - (tt/with-temp Metric [metric {:definition {:aggregation [[:named [:sum [:field-id 20]] "My Cool Aggregation"]]}}] + (tt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] + :breakout [[:field-id 10]]})))) + +;; a Metric whose :aggregation is already named should not get wrapped in an `:aggregation-options` clause +(expect + (mbql-query + {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "My Cool Aggregation"}]] + :breakout [[:field-id 10]]}) + (tt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:display-name "My Cool Aggregation"}]]}}] + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric (u/get-id metric)]] + :breakout [[:field-id 10]]})))) + +;; ...but if it's wrapped in `:aggregation-options`, but w/o given a display name, we should merge the options +(expect + (mbql-query + {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] + :breakout [[:field-id 10]]}) + (tt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name"}]]}}] (#'expand-macros/expand-metrics-and-segments (mbql-query {:aggregation [[:metric (u/get-id metric)]] :breakout [[:field-id 10]]})))) diff --git a/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj b/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj index 8bc40efaa1cae782a659811d660973cacecb3e40..970bab79e30ab9ed3f59adc0d5b5dc44067b99fd 100644 --- a/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj +++ b/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj @@ -2,8 +2,7 @@ "Tests for the `pre-alias-aggregations` middleware. For the most part we don't need to test the actual pre-alias logic, as that comes from the MBQL library and is tested thoroughly there -- we just need to test that it gets applied in the correct places." - (:require [clojure.string :as str] - [expectations :refer [expect]] + (:require [expectations :refer [expect]] [metabase.driver :as driver] [metabase.query-processor.middleware.pre-alias-aggregations :as pre-alias-aggregations] [metabase.test.data :as data])) @@ -16,8 +15,8 @@ (expect (data/mbql-query checkins {:source-table $$checkins - :aggregation [[:named [:sum $user_id] "sum" {:use-as-display-name? false}] - [:named [:sum $venue_id] "sum_2" {:use-as-display-name? false}]]}) + :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"} ] + [:aggregation-options [:sum $venue_id] {:name "sum_2"} ]]}) (pre-alias (data/mbql-query checkins {:source-table $$checkins @@ -27,21 +26,21 @@ (expect (data/mbql-query checkins {:source-table $$checkins - :aggregation [[:named [:sum $user_id] "sum" {:use-as-display-name? false}] - [:named [:sum $venue_id] "sum_2"]]}) + :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] + [:aggregation-options [:sum $venue_id] {:name "sum_2"}]]}) (pre-alias (data/mbql-query checkins {:source-table $$checkins :aggregation [[:sum $user_id] - [:named [:sum $venue_id] "sum"]]}))) + [:aggregation-options [:sum $venue_id] {:name "sum"}]]}))) ;; do aggregations inside source queries get pre-aliased? (expect (data/mbql-query checkins {:source-query {:source-table $$checkins - :aggregation [[:named [:sum $user_id] "sum" {:use-as-display-name? false}] - [:named [:sum $venue_id] "sum_2" {:use-as-display-name? false}]]} - :aggregation [[:named [:count] "count" {:use-as-display-name? false}]]}) + :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] + [:aggregation-options [:sum $venue_id] {:name "sum_2"}]]} + :aggregation [[:aggregation-options [:count] {:name "count"}]]}) (pre-alias (data/mbql-query checkins {:source-query {:source-table $$checkins @@ -52,10 +51,10 @@ (expect (data/mbql-query checkins {:source-table $$venues - :aggregation [[:named [:count] "count" {:use-as-display-name? false}]] + :aggregation [[:aggregation-options [:count] {:name "count"}]] :joins [{:source-query {:source-table $$checkins - :aggregation [[:named [:sum $user_id] "sum" {:use-as-display-name? false}] - [:named [:sum $venue_id] "sum_2" {:use-as-display-name? false}]] + :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] + [:aggregation-options [:sum $venue_id] {:name "sum_2"}]] :breakout [$venue_id]} :alias "checkins" :condition [:= &checkins.venue_id $venues.id]}]}) @@ -74,28 +73,48 @@ {:database 1 :type :query :query {:source-table 1 - :aggregation [[:named [:+ 20 [:sum [:field-id 2]]] "20 + sum" {:use-as-display-name? false}]]}} + :aggregation [[:aggregation-options [:+ 20 [:sum [:field-id 2]]] {:name "expression"}]]}} (pre-alias {:database 1 :type :query :query {:source-table 1 :aggregation [[:+ 20 [:sum [:field-id 2]]]]}})) +(expect + {:database 1 + :type :query + :query {:source-table 1 + :aggregation [[:aggregation-options + [:+ 20 [:sum [:field-id 2]]] + {:name "expression"}] + [:aggregation-options + [:- 20 [:sum [:field-id 2]]] + {:name "expression_2"}]]}} + (pre-alias + {:database 1 + :type :query + :query {:source-table 1 + :aggregation [[:+ 20 [:sum [:field-id 2]]] + [:- 20 [:sum [:field-id 2]]]]}})) + ;; we should use `driver/format-custom-field-name` on the generated aggregation names in case the drivers need to -;; tweak the default names we generate +;; tweak the default names we generate. +;; +;; TODO - `format-custom-field-name` is officially deprecated because it is no longer needed now that custom +;; aggregation names are not used in queries themselves. It can be removed soon, and we can remove this test (driver/register! ::test-driver, :abstract? true, :parent :sql) -;; this implementation prepends an underscore to any name that starts with a number. Some DBs, such as BigQuery, do -;; not allow aliases that start with an underscore +;; this implementation prepends an underscore to any all names. Some DBs, such as BigQuery, do ;; not allow aliases +;; that start with an number for example (defmethod driver/format-custom-field-name ::test-driver [_ custom-field-name] - (str/replace custom-field-name #"(^\d)" "_$1")) + (str \_ custom-field-name)) (expect {:database 1 :type :query :query {:source-table 1 - :aggregation [[:named [:+ 20 [:sum [:field-id 2]]] "_20 + sum" {:use-as-display-name? false}] - [:named [:count] "count" {:use-as-display-name? false}]]}} + :aggregation [[:aggregation-options [:+ 20 [:sum [:field-id 2]]] {:name "_expression"}] + [:aggregation-options [:count] {:name "_count"}]]}} (driver/with-driver ::test-driver (pre-alias {:database 1 diff --git a/test/metabase/query_processor_test/expression_aggregations_test.clj b/test/metabase/query_processor_test/expression_aggregations_test.clj index 482ad89d1f91d38ac540e9744dc3511e8188ce87..76ccc21af31d7d1fcf50d3924596df2a7afb3541 100644 --- a/test/metabase/query_processor_test/expression_aggregations_test.clj +++ b/test/metabase/query_processor_test/expression_aggregations_test.clj @@ -184,12 +184,11 @@ [3 52] [4 30]] :columns [(data/format-name "price") - ;; Redshift annoyingly always lowercases column aliases - (driver/format-custom-field-name driver/*driver* "New Price")]} + "sum_of_price"]} (qp.test/format-rows-by [int int] (qp.test/rows+column-names (data/run-mbql-query venues - {:aggregation [[:named [:sum [:+ $price 1]] "New Price"]] + {:aggregation [[:aggregation-options [:sum [:+ $price 1]] {:name "sum_of_price"}]] :breakout [$price]})))) ;; check that we can name an expression aggregation w/ expression at top-level @@ -199,14 +198,14 @@ [3 -2] [4 -17]] :columns [(data/format-name "price") - (driver/format-custom-field-name driver/*driver* "Sum-41")]} + "sum_41"]} (qp.test/format-rows-by [int int] (qp.test/rows+column-names (data/run-mbql-query venues - {:aggregation [[:named [:- [:sum $price] 41] "Sum-41"]] + {:aggregation [[:aggregation-options [:- [:sum $price] 41] {:name "sum_41"}]] :breakout [$price]})))) -;; check that we can handle METRICS (ick) inside expression aggregation clauses +;; check that we can handle Metrics inside expression aggregation clauses (datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) [[2 119] [3 40] @@ -220,29 +219,30 @@ {:aggregation [:+ [:metric (u/get-id metric)] 1] :breakout [[:field-id $price]]}))))) -;; check that we can handle METRICS (ick) inside a NAMED clause +;; check that we can handle Metrics inside an `:aggregation-options` clause +;; TODO - Pretty sure this test doesn't belong here (?) (datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) {:rows [[2 118] [3 39] [4 24]] :columns [(data/format-name "price") - (driver/format-custom-field-name driver/*driver* "My Cool Metric")]} + "auto_generated_name"]} (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]}}] (qp.test/format-rows-by [int int] (qp.test/rows+column-names (data/run-mbql-query venues - {:aggregation [[:named [:metric (u/get-id metric)] "My Cool Metric"]] + {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] :breakout [[:field-id $price]]}))))) -;; check that METRICS (ick) with a nested aggregation still work inside a NAMED clause +;; check that Metrics with a nested aggregation still work inside an `:aggregation-options` clause (datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) {:rows [[2 118] [3 39] [4 24]] :columns [(data/format-name "price") - (driver/format-custom-field-name driver/*driver* "My Cool Metric")]} + "auto_generated_name"]} (tt/with-temp Metric [metric (data/$ids venues {:table_id $$venues :definition {:aggregation [[:sum $price]] @@ -250,16 +250,16 @@ (qp.test/format-rows-by [int int] (qp.test/rows+column-names (data/run-mbql-query venues - {:aggregation [[:named [:metric (u/get-id metric)] "My Cool Metric"]] + {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] :breakout [[:field-id $price]]}))))) ;; check that named aggregations come back with the correct column metadata (#4002) (datasets/expect-with-drivers (qp.test/non-timeseries-drivers-with-feature :expression-aggregations) (assoc (qp.test/aggregate-col :count) - :name (driver/format-custom-field-name driver/*driver* "Count of Things") + :name "auto_generated_name" :display_name "Count of Things") (-> (data/run-mbql-query venues - {:aggregation [[:named ["COUNT"] "Count of Things"]]}) + {:aggregation [[:aggregation-options ["COUNT"] {:name "auto_generated_name", :display-name "Count of Things"}]]}) qp.test/cols first)) diff --git a/test/metabase/query_processor_test/expressions_test.clj b/test/metabase/query_processor_test/expressions_test.clj index 47238370288d3bed6de83f0003a3158036669122..bc6fd61ca22e9e752d8f6a53c97bd0003038b4f0 100644 --- a/test/metabase/query_processor_test/expressions_test.clj +++ b/test/metabase/query_processor_test/expressions_test.clj @@ -106,10 +106,10 @@ :snowflake :type/Number :type/Integer)}) (set (map #(select-keys % [:name :base_type]) - (-> (data/run-mbql-query venues - {:aggregation [:named [:sum [:* $price -1]] "x"] - :breakout [$category_id]}) - (get-in [:data :cols]))))) + (qp.test/cols + (data/run-mbql-query venues + {:aggregation [[:aggregation-options [:sum [:* $price -1]] {:name "x"}]] + :breakout [$category_id]}))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | HANDLING NULLS AND ZEROES | diff --git a/test/metabase/test/data/datasets.clj b/test/metabase/test/data/datasets.clj index 8742bc08bdb825e416dccaa04a297a24f1f8be10..cfa852dc563aee7e44da59161364c07e43597ec6 100644 --- a/test/metabase/test/data/datasets.clj +++ b/test/metabase/test/data/datasets.clj @@ -2,7 +2,7 @@ "Interface + implementations for loading test datasets for different drivers, and getting information about the dataset's tables, fields, etc. - TODO - rename this to `metabase.driver.test-extensions.expect` or something like that" + TODO - rename this to `metabase.driver.test-extensions.expect` or `metabase.driver.test` or something like that" (:require [expectations :refer [expect]] [metabase.driver :as driver] [metabase.test.data.env :as tx.env]))