From d96c318dbc779f950bfc73af84403ab3b2f44475 Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Fri, 2 Jun 2023 12:07:33 -0700 Subject: [PATCH] Use MLv2 for metadata calculation in QP (part 1); make column name calculation consistent with QP (#29583) * Simplified impl * Add #31266 test * Yay * Very nice * Test fixes * Fix Kondo warnings --- .../row_level_restrictions_test.clj | 8 +- .../metabase/driver/druid/query_processor.clj | 8 +- .../metabase/driver/mongo/query_processor.clj | 14 +- .../mongo/test/metabase/test/data/mongo.clj | 11 + shared/src/metabase/mbql/normalize.cljc | 6 + shared/test/metabase/mbql/normalize_test.cljc | 12 + src/metabase/driver/sql/query_processor.clj | 31 +- src/metabase/lib/aggregation.cljc | 53 +- src/metabase/lib/core.cljc | 3 +- src/metabase/lib/equality.cljc | 2 + src/metabase/lib/expression.cljc | 16 +- src/metabase/lib/field.cljc | 1 - .../lib/metadata/cached_provider.cljc | 2 + src/metabase/lib/metadata/calculation.cljc | 6 +- .../lib/metadata/composed_provider.cljc | 25 + src/metabase/lib/metadata/protocols.cljc | 8 +- .../query_processor/middleware/annotate.clj | 222 ++------ .../middleware/pre_alias_aggregations.clj | 10 +- .../middleware/resolve_joins.clj | 29 +- src/metabase/query_processor/store.clj | 42 +- .../store/metadata_provider.clj | 39 ++ .../query_processor/util/nest_query.clj | 9 +- test/metabase/api/card_test.clj | 2 +- test/metabase/api/embed_test.clj | 14 +- .../driver/sql/query_processor_test.clj | 105 ++-- test/metabase/lib/aggregation_test.cljc | 48 +- test/metabase/lib/expression_test.cljc | 2 +- test/metabase/lib/field_test.cljc | 23 +- test/metabase/lib/join_test.cljc | 3 +- .../lib/metadata/composed_provider_test.cljc | 28 ++ test/metabase/lib/order_by_test.cljc | 4 +- test/metabase/lib/remove_replace_test.cljc | 4 +- test/metabase/lib/stage_test.cljc | 12 +- test/metabase/lib/test_util.cljc | 43 +- .../middleware/add_source_metadata_test.clj | 174 ++++--- .../middleware/annotate_test.clj | 475 +++++++++--------- .../pre_alias_aggregations_test.clj | 95 ++-- test/metabase/query_processor/pivot_test.clj | 8 +- .../util/add_alias_info_test.clj | 38 +- .../query_processor/util/nest_query_test.clj | 22 +- .../query_processor_test/aggregation_test.clj | 154 +++--- .../query_processor_test/filter_test.clj | 6 +- .../nested_queries_test.clj | 42 +- .../server/middleware/session_test.clj | 14 +- test/metabase/test/data.clj | 2 +- test/metabase/test/data/h2.clj | 10 +- test/metabase/test/data/interface.clj | 15 +- test/metabase/test/data/sql.clj | 3 + 48 files changed, 1039 insertions(+), 864 deletions(-) create mode 100644 src/metabase/lib/metadata/composed_provider.cljc create mode 100644 src/metabase/query_processor/store/metadata_provider.clj create mode 100644 test/metabase/lib/metadata/composed_provider_test.cljc diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj index cb24be83d08..469aa6c0e8b 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/query_processor/middleware/row_level_restrictions_test.clj @@ -215,7 +215,7 @@ :condition [:= $venue_id &v.venues.id]}] :aggregation [[:count]]} - ::row-level-restrictions/original-metadata [{:base_type :type/BigInteger + ::row-level-restrictions/original-metadata [{:base_type :type/Integer :semantic_type :type/Quantity :name "count" :display_name "Count" @@ -229,8 +229,10 @@ :joins [{:source-table $$venues :alias "v" :strategy :left-join - :condition [:= $venue_id &v.venues.id]}]})))))) + :condition [:= $venue_id &v.venues.id]}]})))))))) +(deftest middleware-native-quest-test + (testing "Make sure the middleware does the correct transformation given the GTAPs we have" (testing "Should substitute appropriate value in native query" (met/with-gtaps {:gtaps {:venues (venues-category-native-gtap-def)} :attributes {"cat" 50}} @@ -243,7 +245,7 @@ "ORDER BY \"PUBLIC\".\"VENUES\".\"ID\" ASC") :params []}} - ::row-level-restrictions/original-metadata [{:base_type :type/BigInteger + ::row-level-restrictions/original-metadata [{:base_type :type/Integer :semantic_type :type/Quantity :name "count" :display_name "Count" 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 989e02e21b8..d53acd48edc 100644 --- a/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj +++ b/modules/drivers/druid/src/metabase/driver/druid/query_processor.clj @@ -669,8 +669,8 @@ {:aggregations [(ag:doubleMax ag-field (or output-name :max))]}]))) (s/defn ^:private handle-aggregation - [query-type, ag-clause :- mbql.s/Aggregation, druid-query] - (let [output-name (annotate/aggregation-name ag-clause) + [query-type ag-clause :- mbql.s/Aggregation druid-query] + (let [output-name (annotate/aggregation-name *query* ag-clause) [ag-type ag-field & args] (mbql.u/match-one ag-clause [:aggregation-options ag & _] #_:clj-kondo/ignore (recur ag) _ &match)] @@ -727,11 +727,11 @@ ;; 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)}) + {:name (annotate/aggregation-name *query* expression)}) _ {:type :arithmetic - :name (annotate/aggregation-name expression) + :name (annotate/aggregation-name *query* expression) :fn operator :fields (vec (for [arg args] (mbql.u/match-one arg 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 30aab14b58b..a7acd4a2cab 100644 --- a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj +++ b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj @@ -222,7 +222,7 @@ ;; (defmethod ->lvalue :aggregation [[_ index]] - (annotate/aggregation-name (mbql.u/aggregation-at-index *query* index *nesting-level*))) + (annotate/aggregation-name (:query *query*) (mbql.u/aggregation-at-index *query* index *nesting-level*))) (defmethod ->lvalue :field [[_ id-or-name _props :as field]] @@ -976,7 +976,7 @@ (for [field-or-expr breakout-fields] [(field-alias field-or-expr) (format "$_id.%s" (field-alias field-or-expr))]) (for [ag aggregations - :let [ag-name (annotate/aggregation-name ag)]] + :let [ag-name (annotate/aggregation-name (:query *query*) ag)]] [ag-name (if (mbql.u/is-clause? :distinct (unwrap-named-ag ag)) {$size (str \$ ag-name)} true)]))) @@ -1007,7 +1007,7 @@ pred)] {:group {(subs count-where-expr 1) (aggregation->rvalue [:count-where pred]) (subs count-expr 1) (aggregation->rvalue [:count])} - :post [{(annotate/aggregation-name ag) {$divide [count-where-expr count-expr]}}]})) + :post [{(annotate/aggregation-name (:query *query*) ag) {$divide [count-where-expr count-expr]}}]})) ;; MongoDB doesn't have a variance operator, but you calculate it by taking the square of the standard deviation. ;; However, `$pow` is not allowed in the `$group` stage. So calculate standard deviation in the @@ -1016,11 +1016,11 @@ (let [[_ expr] (unwrap-named-ag ag) stddev-expr (name (gensym "$stddev-"))] {:group {(subs stddev-expr 1) (aggregation->rvalue [:stddev expr])} - :post [{(annotate/aggregation-name ag) {:$pow [stddev-expr 2]}}]})) + :post [{(annotate/aggregation-name (:query *query*) ag) {:$pow [stddev-expr 2]}}]})) (defmethod expand-aggregation :default [ag] - {:group {(annotate/aggregation-name ag) (aggregation->rvalue ag)}}) + {:group {(annotate/aggregation-name (:query *query*) ag) (aggregation->rvalue ag)}}) (defn- extract-aggregations "Extract aggregation expressions embedded in `aggr-expr` using `parent-name` @@ -1055,7 +1055,7 @@ (aggregation-op op) (let [aliases-taken (set (vals aggregations-seen)) - aggr-name (annotate/aggregation-name aggr-expr) + aggr-name (annotate/aggregation-name (:query *query*) aggr-expr) desired-alias (str parent-name "~" aggr-name) ;; find a free alias by appending increasing integers ;; to the desired alias @@ -1098,7 +1098,7 @@ fields generated by the groups. Each map in the `:post` vector may (and usually does) refer to the fields introduced by the preceding maps." [aggr-expr] - (let [aggr-name (annotate/aggregation-name aggr-expr) + (let [aggr-name (annotate/aggregation-name (:query *query*) aggr-expr) [aggr-expr' aggregations-seen] (simplify-extracted-aggregations aggr-name (extract-aggregations aggr-expr aggr-name)) diff --git a/modules/drivers/mongo/test/metabase/test/data/mongo.clj b/modules/drivers/mongo/test/metabase/test/data/mongo.clj index c362a93ed69..59d1b6c0517 100644 --- a/modules/drivers/mongo/test/metabase/test/data/mongo.clj +++ b/modules/drivers/mongo/test/metabase/test/data/mongo.clj @@ -124,3 +124,14 @@ {:$sort {"_id" 1}} {:$project {"_id" false, "count" true}}]) :collection (name table-name)}) + +(defmethod tx/aggregate-column-info :mongo + ([driver ag-type] + (merge ((get-method tx/aggregate-column-info ::tx/test-extensions) driver ag-type) + (when (#{:count :cum-count} ag-type) + {:base_type :type/Integer}))) + + ([driver ag-type column-metadata] + (merge ((get-method tx/aggregate-column-info ::tx/test-extensions) driver ag-type column-metadata) + (when (= ag-type :cum-count) + {:base_type :type/Integer})))) diff --git a/shared/src/metabase/mbql/normalize.cljc b/shared/src/metabase/mbql/normalize.cljc index 9aabf5b8189..b14f57a9a44 100644 --- a/shared/src/metabase/mbql/normalize.cljc +++ b/shared/src/metabase/mbql/normalize.cljc @@ -453,6 +453,12 @@ ;; afterwards [:field id-or-name (not-empty opts)])) +(defmethod canonicalize-mbql-clause :aggregation + [[_tag index opts]] + (if (empty? opts) + [:aggregation index] + [:aggregation index opts])) + ;;; legacy Field clauses (defmethod canonicalize-mbql-clause :field-id diff --git a/shared/test/metabase/mbql/normalize_test.cljc b/shared/test/metabase/mbql/normalize_test.cljc index 378e0b85184..884560edbc5 100644 --- a/shared/test/metabase/mbql/normalize_test.cljc +++ b/shared/test/metabase/mbql/normalize_test.cljc @@ -1447,3 +1447,15 @@ [:field "date_seen" {:base-type :type/Date}] "2021-05-01T12:30:00"]}} (mbql.normalize/normalize query)))))) + +(t/deftest ^:parallel normalize-aggregation-ref-test + (t/are [clause] (= {:database 1 + :type :query + :query {:order-by [[:asc [:aggregation 0]]]}} + (metabase.mbql.normalize/normalize + {:database 1 + :type :query + :query {:order-by [[:asc clause]]}})) + [:aggregation 0 nil] + [:aggregation 0 {}] + [:aggregation 0])) diff --git a/src/metabase/driver/sql/query_processor.clj b/src/metabase/driver/sql/query_processor.clj index 81957c8929c..4ab0ae757ed 100644 --- a/src/metabase/driver/sql/query_processor.clj +++ b/src/metabase/driver/sql/query_processor.clj @@ -1028,12 +1028,13 @@ ;;; -------------------------------------------------- aggregation --------------------------------------------------- (defmethod apply-top-level-clause [:sql :aggregation] - [driver _ honeysql-form {aggregations :aggregation}] + [driver _top-level-clause honeysql-form {aggregations :aggregation, :as inner-query}] (let [honeysql-ags (vec (for [ag aggregations :let [ag-expr (->honeysql driver ag) + ag-name (annotate/aggregation-name inner-query ag) ag-alias (->honeysql driver (hx/identifier :field-alias - (driver/escape-alias driver (annotate/aggregation-name ag))))]] + (driver/escape-alias driver ag-name)))]] (case (long hx/*honey-sql-version*) 1 [ag-expr ag-alias] 2 [ag-expr [ag-alias]])))] @@ -1371,13 +1372,19 @@ (defn- apply-top-level-clauses "`apply-top-level-clause` for all of the top-level clauses in `inner-query`, progressively building a HoneySQL form. Clauses are applied according to the order in `top-level-clause-application-order`." - [driver honeysql-form inner-query] - (->> (reduce - (fn [honeysql-form k] - (apply-top-level-clause driver k honeysql-form inner-query)) - honeysql-form - (query->keys-in-application-order inner-query)) - (add-default-select driver))) + ([driver honeysql-form inner-query] + (apply-top-level-clauses driver honeysql-form inner-query identity)) + + ([driver honeysql-form inner-query xform] + (transduce + xform + (fn + ([honeysql-form] + (add-default-select driver honeysql-form)) + ([honeysql-form k] + (apply-top-level-clause driver k honeysql-form inner-query))) + honeysql-form + (query->keys-in-application-order inner-query)))) (declare apply-clauses) @@ -1403,7 +1410,7 @@ 2 [table-alias]))]])) (defn- apply-clauses - "Like `apply-top-level-clauses`, but handles `source-query` as well, which needs to be handled in a special way + "Like [[apply-top-level-clauses]], but handles `source-query` as well, which needs to be handled in a special way because it is aliased." [driver honeysql-form {:keys [source-query], :as inner-query}] (binding [*inner-query* inner-query] @@ -1411,7 +1418,9 @@ (apply-top-level-clauses driver (apply-source-query driver honeysql-form inner-query) - (dissoc inner-query :source-query)) + inner-query + ;; don't try to do anything with the source query recursively. + (remove (partial = :source-query))) (apply-top-level-clauses driver honeysql-form inner-query)))) (defmulti preprocess diff --git a/src/metabase/lib/aggregation.cljc b/src/metabase/lib/aggregation.cljc index 5e09a68844d..07b74dc4d2c 100644 --- a/src/metabase/lib/aggregation.cljc +++ b/src/metabase/lib/aggregation.cljc @@ -1,7 +1,6 @@ (ns metabase.lib.aggregation (:refer-clojure :exclude [count distinct max min var]) (:require - [clojure.math :as math] [medley.core :as m] [metabase.lib.common :as lib.common] [metabase.lib.equality :as lib.equality] @@ -86,14 +85,16 @@ :count (i18n/tru "Count") :cum-count (i18n/tru "Cumulative count")))) -(defmethod lib.metadata.calculation/column-name-method :count - [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/column-name-method ::count-aggregation + [_query _stage-number [tag :as _clause]] + (case tag + :count "count" + :cum-count "cum_count")) + +(defmethod lib.metadata.calculation/metadata-method ::count-aggregation + [query stage-number clause] + (assoc ((get-method lib.metadata.calculation/metadata-method ::aggregation) query stage-number clause) + :semantic-type :type/Quantity)) (defmethod lib.metadata.calculation/display-name-method :case [_query _stage-number _case _style] @@ -119,20 +120,18 @@ (lib.hierarchy/derive tag ::unary-aggregation)) (defmethod lib.metadata.calculation/column-name-method ::unary-aggregation - [query stage-number [tag _opts arg]] - (let [arg (lib.metadata.calculation/column-name-method query stage-number arg)] - (str - (case tag - :avg "avg_" - :cum-sum "cum_sum_" - :distinct "distinct_" - :max "max_" - :median "median_" - :min "min_" - :stddev "std_dev_" - :sum "sum_" - :var "var_") - arg))) + [_query _stage-number [tag _opts _arg]] + (case tag + :avg "avg" + :cum-sum "sum" + :distinct "count" + :max "max" + :median "median" + :min "min" + :stddev "stddev" + :sum "sum" + :var "var")) + (defmethod lib.metadata.calculation/display-name-method ::unary-aggregation [query stage-number [tag _opts arg] style] @@ -153,12 +152,8 @@ (i18n/tru "{0}th percentile of {1}" p (lib.metadata.calculation/display-name query stage-number x style))) (defmethod lib.metadata.calculation/column-name-method :percentile - [query stage-number [_percentile _opts x p]] - ;; if `p` is between `0` and `1` then just use the first two digits for the name, e.g. `p95_whatever` - (let [p (if (< 0 p 1) - (int (math/round (* p 100.0))) - p)] - (lib.util/format "p%s_%s" p (lib.metadata.calculation/column-name query stage-number x)))) + [_query _stage-number _clause] + "percentile") (lib.hierarchy/derive :percentile ::aggregation) diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc index 89faa7f9aab..2e45cb45f63 100644 --- a/src/metabase/lib/core.cljc +++ b/src/metabase/lib/core.cljc @@ -91,7 +91,8 @@ field query-for-table-id query-for-table-name - table] + table + ref-lookup] [lib.expression expression expressions diff --git a/src/metabase/lib/equality.cljc b/src/metabase/lib/equality.cljc index ea1576d7275..e431da8475a 100644 --- a/src/metabase/lib/equality.cljc +++ b/src/metabase/lib/equality.cljc @@ -87,6 +87,8 @@ (sequential? x) ((get-method = :dispatch-type/sequential) x y) :else (clojure.core/= x y))) +;;; TODO I think to field refs with different `:base-type`s but with other info the same should probably be considered +;;; the same, right? Like if we improve type calculation it shouldn't break existing queries (defn ref= "Are two refs `x` and `y` equal?" [x y] diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc index 13e369dc947..9f04ef125da 100644 --- a/src/metabase/lib/expression.cljc +++ b/src/metabase/lib/expression.cljc @@ -110,21 +110,9 @@ [query stage-number [tag _opts & args] _style] (infix-display-name query stage-number (get infix-operator-display-name tag) args)) -(defn- infix-column-name - [query stage-number operator-str args] - (str/join (str \_ operator-str \_) - (map (partial lib.metadata.calculation/column-name query stage-number) - args))) - -(def ^:private infix-operator-column-name - {:+ "plus" - :- "minus" - :/ "divided_by" - :* "times"}) - (defmethod lib.metadata.calculation/column-name-method ::infix-operator - [query stage-number [tag _opts & args]] - (infix-column-name query stage-number (get infix-operator-column-name tag) args)) + [_query _stage-number _expr] + "expression") ;;; `:+`, `:-`, and `:*` all have the same logic; also used for [[metabase.lib.schema.expression/type-of]]. ;;; diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc index a9e26602a10..08598b9f331 100644 --- a/src/metabase/lib/field.cljc +++ b/src/metabase/lib/field.cljc @@ -192,7 +192,6 @@ join-display-name (when (= style :long) (or (when fk-field-id - ;; Implicitly joined column pickers don't use the target table's name, they use the FK field's name with ;; "ID" dropped instead. ;; This is very intentional: one table might have several FKs to one foreign table, each with different diff --git a/src/metabase/lib/metadata/cached_provider.cljc b/src/metabase/lib/metadata/cached_provider.cljc index b540370c161..e5f8ef39f6c 100644 --- a/src/metabase/lib/metadata/cached_provider.cljc +++ b/src/metabase/lib/metadata/cached_provider.cljc @@ -49,6 +49,8 @@ (for [id ids] (get-in-cache cache [metadata-type id])))) +;;; wraps another metadata provider and caches results. Implements +;;; the [[lib.metadata.protocols/CachedMetadataProvider]] protocol which allows warming the cache before use. (deftype CachedProxyMetadataProvider [cache metadata-provider] lib.metadata.protocols/MetadataProvider (database [_this] (get-in-cache-or-fetch cache [:metadata/database] #(lib.metadata.protocols/database metadata-provider))) diff --git a/src/metabase/lib/metadata/calculation.cljc b/src/metabase/lib/metadata/calculation.cljc index 373776cef5a..45740d614bf 100644 --- a/src/metabase/lib/metadata/calculation.cljc +++ b/src/metabase/lib/metadata/calculation.cljc @@ -27,6 +27,10 @@ name generation. For a joined column, this might look like \"Venues → Price\"." [:enum :default :long]) +(def ^:dynamic *display-name-style* + "Display name style to use when not explicitly passed in to [[display-name]]." + :default) + (defmulti display-name-method "Calculate a nice human-friendly display name for something." {:arglists '([query stage-number x display-name-style])} @@ -48,7 +52,7 @@ (display-name query -1 x)) ([query stage-number x] - (display-name query stage-number x :default)) + (display-name query stage-number x *display-name-style*)) ([query :- ::lib.schema/query stage-number :- :int diff --git a/src/metabase/lib/metadata/composed_provider.cljc b/src/metabase/lib/metadata/composed_provider.cljc new file mode 100644 index 00000000000..63881417053 --- /dev/null +++ b/src/metabase/lib/metadata/composed_provider.cljc @@ -0,0 +1,25 @@ +(ns metabase.lib.metadata.composed-provider + (:require + [clojure.core.protocols] + [clojure.datafy :as datafy] + [medley.core :as m] + [metabase.lib.metadata.protocols :as metadata.protocols])) + +(defn composed-metadata-provider + "A metadata provider composed of several different `metadata-providers`. Methods try each constituent provider in + turn from left to right until one returns a truthy result." + [& metadata-providers] + (reify + metadata.protocols/MetadataProvider + (database [_this] (some metadata.protocols/database metadata-providers)) + (table [_this table-id] (some #(metadata.protocols/table % table-id) metadata-providers)) + (field [_this field-id] (some #(metadata.protocols/field % field-id) metadata-providers)) + (card [_this card-id] (some #(metadata.protocols/card % card-id) metadata-providers)) + (metric [_this metric-id] (some #(metadata.protocols/metric % metric-id) metadata-providers)) + (segment [_this segment-id] (some #(metadata.protocols/segment % segment-id) metadata-providers)) + (tables [_this] (m/distinct-by :id (mapcat metadata.protocols/tables metadata-providers))) + (fields [_this table-id] (m/distinct-by :id (mapcat #(metadata.protocols/fields % table-id) metadata-providers))) + + clojure.core.protocols/Datafiable + (datafy [_this] + (cons `composed-metadata-provider (map datafy/datafy metadata-providers))))) diff --git a/src/metabase/lib/metadata/protocols.cljc b/src/metabase/lib/metadata/protocols.cljc index 6b8045930e9..9df210d30a0 100644 --- a/src/metabase/lib/metadata/protocols.cljc +++ b/src/metabase/lib/metadata/protocols.cljc @@ -69,9 +69,11 @@ (satisfies? MetadataProvider x)) (#?(:clj p/defprotocol+ :cljs defprotocol) CachedMetadataProvider - "Optional. A protocol for a MetadataProvider that some sort of internal cache. This is mostly useful for MetadataProviders that - can hit some sort of relatively expensive external service, - e.g. [[metabase.lib.metadata.jvm/application-database-metadata-provider]]. + "Optional. A protocol for a MetadataProvider that some sort of internal cache. This is mostly useful for + MetadataProviders that can hit some sort of relatively expensive external service, + e.g. [[metabase.lib.metadata.jvm/application-database-metadata-provider]]. The main purpose of this is to allow + pre-warming the cache with stuff that was already fetched elsewhere. + See [[metabase.models.metric/warmed-metadata-provider]] for example. See [[cached-metadata-provider]] below to wrap for a way to wrap an existing MetadataProvider to add caching on top of it." diff --git a/src/metabase/query_processor/middleware/annotate.clj b/src/metabase/query_processor/middleware/annotate.clj index dc847f145e7..393d69bb688 100644 --- a/src/metabase/query_processor/middleware/annotate.clj +++ b/src/metabase/query_processor/middleware/annotate.clj @@ -4,21 +4,26 @@ [clojure.set :as set] [clojure.string :as str] [medley.core :as m] - [metabase.driver :as driver] [metabase.driver.common :as driver.common] - [metabase.mbql.predicates :as mbql.preds] + [metabase.lib.convert :as lib.convert] + [metabase.lib.core :as lib] + [metabase.lib.metadata.calculation :as lib.metadata.calculation] + [metabase.lib.schema.common :as lib.schema.common] + [metabase.mbql.normalize :as mbql.normalize] [metabase.mbql.schema :as mbql.s] [metabase.mbql.util :as mbql.u] [metabase.mbql.util.match :as mbql.match] [metabase.models.humanization :as humanization] [metabase.query-processor.error-type :as qp.error-type] - [metabase.query-processor.middleware.escape-join-aliases :as escape-join-aliases] + [metabase.query-processor.middleware.escape-join-aliases + :as escape-join-aliases] [metabase.query-processor.reducible :as qp.reducible] [metabase.query-processor.store :as qp.store] [metabase.query-processor.util :as qp.util] [metabase.sync.analyze.fingerprint.fingerprinters :as fingerprinters] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru tru]] + [metabase.util.malli :as mu] [metabase.util.schema :as su] [schema.core :as s])) @@ -307,176 +312,53 @@ (throw (ex-info (tru "Don''t know how to get information about Field: {0}" &match) {:field &match})))) - -;;; ---------------------------------------------- Aggregate Field Info ---------------------------------------------- - -(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 - ;; if the arg itself is a nested expression, recursively find a name for it, and wrap in parens - [(_ :guard #{:+ :- :/ :*}) & _] - (str "(" (recursive-name-fn &match) ")") - - ;; if the arg is another aggregation, recurse to get its name. (Only aggregations, nested expressions, or numbers - ;; are allowed as args to expression aggregations; thus anything that's an MBQL clause, but not a nested - ;; expression, is a ag clause.) - [(_ :guard keyword?) & _] - (recursive-name-fn &match) - - ;; otherwise for things like numbers just use that directly - _ &match)) - -(s/defn aggregation-name :- su/NonBlankString +(defn- mlv2-query [inner-query] + (qp.store/cached [:mlv2-query (hash inner-query)] + (try + (lib/query + (qp.store/metadata-provider) + (lib.convert/->pMBQL (lib.convert/legacy-query-from-inner-query + (:id (qp.store/database)) + (mbql.normalize/normalize-fragment [:query] inner-query)))) + (catch Throwable e + (throw (ex-info (tru "Error converting query to pMBQL: {0}" (ex-message e)) + {:inner-query inner-query, :type qp.error-type/qp} + e)))))) + +(s/defn ^:private col-info-for-aggregation-clause + "Return appropriate column metadata for an `:aggregation` clause." + ;; `clause` is normally an aggregation clause but this function can call itself recursively; see comments by the + ;; `match` pattern for field clauses below + [inner-query :- su/Map clause] + (let [mlv2-clause (lib.convert/->pMBQL clause)] + ;; for some mystery reason it seems like the annotate code uses `:long` style display names when something appears + ;; inside an aggregation clause, e.g. + ;; + ;; Distinct values of Category → Name + ;; + ;; but `:default` style names when they appear on their own or in breakouts, e.g. + ;; + ;; Name + ;; + ;; why is this the case? Who knows! But that's the old pre-MLv2 behavior. I think we should try to fix it, but it's + ;; probably going to involve updating a ton of tests that encode the old behavior. + (binding [lib.metadata.calculation/*display-name-style* :long] + (-> (lib.metadata.calculation/metadata (mlv2-query inner-query) -1 mlv2-clause) + (update-keys u/->snake_case_en) + (dissoc :lib/type))))) + +(mu/defn aggregation-name :- ::lib.schema.common/non-blank-string "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] - (when-not driver/*driver* - (throw (Exception. (tru "*driver* is unbound.")))) - (mbql.u/match-one ag-clause - [:aggregation-options _ (options :guard :name)] - (:name options) - - [:aggregation-options ag _] - #_:clj-kondo/ignore - (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] - "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. - ;; - ;; `cum-count` and `cum-sum` get the same names as the non-cumulative versions, due to limitations (they are - ;; written out of the query before we ever see them). For other code that makes use of this function give them - ;; the correct names to expect. - [(_ :guard #{:distinct :cum-count}) _] - "count" - - [:cum-sum _] - "sum" - - ;; for any other aggregation just use the name of the clause e.g. `sum`. - [clause-name & _] - (name clause-name))) - -(declare aggregation-display-name) - -(s/defn ^:private aggregation-arg-display-name :- su/NonBlankString - "Name to use for an aggregation clause argument such as a Field when constructing the complete aggregation name." - [inner-query, ag-arg :- Object] - (or (when (mbql.preds/Field? ag-arg) - (when-let [info (col-info-for-field-clause inner-query ag-arg)] - (some info [:display_name :name]))) - (aggregation-display-name inner-query ag-arg))) - -(s/defn aggregation-display-name :- su/NonBlankString - "Return an appropriate user-facing display name for an aggregation clause." - [inner-query ag-clause] - (mbql.u/match-one ag-clause - [:aggregation-options _ (options :guard :display-name)] - (:display-name options) - - [:aggregation-options ag _] - #_:clj-kondo/ignore - (recur ag) - - [(operator :guard #{:+ :- :/ :*}) & args] - (str/join (format " %s " (name operator)) - (for [arg args] - (expression-arg-display-name (partial aggregation-arg-display-name inner-query) arg))) - - [:count] (tru "Count") - [:case] (tru "Case") - [:distinct arg] (tru "Distinct values of {0}" (aggregation-arg-display-name inner-query arg)) - [:count arg] (tru "Count of {0}" (aggregation-arg-display-name inner-query arg)) - [:avg arg] (tru "Average of {0}" (aggregation-arg-display-name inner-query arg)) - ;; cum-count and cum-sum get names for count and sum, respectively (see explanation in `aggregation-name`) - [:cum-count arg] (tru "Count of {0}" (aggregation-arg-display-name inner-query arg)) - [:cum-sum arg] (tru "Sum of {0}" (aggregation-arg-display-name inner-query arg)) - [:stddev arg] (tru "SD of {0}" (aggregation-arg-display-name inner-query arg)) - [:sum arg] (tru "Sum of {0}" (aggregation-arg-display-name inner-query arg)) - [:min arg] (tru "Min of {0}" (aggregation-arg-display-name inner-query arg)) - [:max arg] (tru "Max of {0}" (aggregation-arg-display-name inner-query arg)) - [:var arg] (tru "Variance of {0}" (aggregation-arg-display-name inner-query arg)) - [:median arg] (tru "Median of {0}" (aggregation-arg-display-name inner-query arg)) - [:percentile arg p] (tru "{0}th percentile of {1}" p (aggregation-arg-display-name inner-query arg)) - - ;; until we have a way to generate good names for filters we'll just have to say 'matching condition' for now - [:sum-where arg _] (tru "Sum of {0} matching condition" (aggregation-arg-display-name inner-query arg)) - [:share _] (tru "Share of rows matching condition") - [:count-where _] (tru "Count of rows matching condition") - - (_ :guard mbql.preds/Field?) - (:display_name (col-info-for-field-clause inner-query ag-clause)) - - _ - (aggregation-name ag-clause))) - -(defn- ag->name-info [inner-query ag] - {:name (aggregation-name ag) - :display_name (aggregation-display-name inner-query ag)}) - -(s/defn col-info-for-aggregation-clause - "Return appropriate column metadata for an `:aggregation` clause." - ;; `clause` is normally an aggregation clause but this function can call itself recursively; see comments by the - ;; `match` pattern for field clauses below - [inner-query :- su/Map, clause] - (mbql.u/match-one clause - ;; 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 inner-query &match)) - - ;; Always treat count or distinct count as an integer even if the DB in question returns it as something - ;; wacky like a BigDecimal or Float - [(_ :guard #{:count :distinct}) & args] - (merge - (col-info-for-aggregation-clause inner-query args) - {:base_type :type/BigInteger - :semantic_type :type/Quantity} - (ag->name-info inner-query &match)) - - [:count-where _] - (merge - {:base_type :type/Integer - :semantic_type :type/Quantity} - (ag->name-info inner-query &match)) - - [:share _] - (merge - {:base_type :type/Float - :semantic_type :type/Share} - (ag->name-info inner-query &match)) - - ;; get info from a Field if we can (theses Fields are matched when ag clauses recursively call - ;; `col-info-for-ag-clause`, and this info is added into the results) - (_ :guard mbql.preds/Field?) - (select-keys (col-info-for-field-clause inner-query &match) [:base_type :semantic_type :settings]) - #{:expression :+ :- :/ :*} - (merge - (infer-expression-type &match) - (when (mbql.preds/Aggregation? &match) - (ag->name-info inner-query &match))) - - ;; the type returned by a case statement depends on what its expressions are; we'll just return the type info for - ;; the first expression for the time being. I guess it's possible the expression might return a string for one - ;; case and a number for another, but I think in post cases it should be the same type for every clause. - [:case & _] - (merge - (infer-expression-type &match) - (ag->name-info inner-query &match)) - - ;; get name/display-name of this ag - [(_ :guard keyword?) arg & _] - (merge - (col-info-for-aggregation-clause inner-query arg) - (ag->name-info inner-query &match)))) + [inner-query :- [:and + [:map] + [:fn + {:error/message "legacy inner-query with :source-table or :source-query"} + (some-fn :source-table :source-query)]] + ag-clause] + (lib.metadata.calculation/column-name (mlv2-query inner-query) (lib.convert/->pMBQL ag-clause))) ;;; ----------------------------------------- Putting it all together (MBQL) ----------------------------------------- @@ -521,8 +403,6 @@ (cols-for-ags-and-breakouts inner-query) (cols-for-fields inner-query))) - - (s/defn ^:private merge-source-metadata-col :- (s/maybe su/Map) [source-metadata-col :- (s/maybe su/Map) col :- (s/maybe su/Map)] (merge diff --git a/src/metabase/query_processor/middleware/pre_alias_aggregations.clj b/src/metabase/query_processor/middleware/pre_alias_aggregations.clj index de48ce8639f..9b9844c9eab 100644 --- a/src/metabase/query_processor/middleware/pre_alias_aggregations.clj +++ b/src/metabase/query_processor/middleware/pre_alias_aggregations.clj @@ -4,24 +4,24 @@ [metabase.mbql.util :as mbql.u] [metabase.query-processor.middleware.annotate :as annotate])) -(defn- ag-name [ag-clause] - (driver/escape-alias driver/*driver* (annotate/aggregation-name ag-clause))) +(defn- ag-name [inner-query ag-clause] + (driver/escape-alias driver/*driver* (annotate/aggregation-name inner-query ag-clause))) -(defn- pre-alias-and-uniquify [aggregations] +(defn- pre-alias-and-uniquify [inner-query aggregations] (mapv (fn [original-ag updated-ag] (if (= original-ag updated-ag) original-ag (with-meta updated-ag {:auto-generated? true}))) aggregations - (mbql.u/pre-alias-and-uniquify-aggregations ag-name aggregations))) + (mbql.u/pre-alias-and-uniquify-aggregations (partial ag-name inner-query) aggregations))) (defn pre-alias-aggregations-in-inner-query "Make sure all aggregations have aliases, and all aliases are unique, in an 'inner' MBQL query." [{:keys [aggregation source-query joins], :as inner-query}] (cond-> inner-query (seq aggregation) - (update :aggregation pre-alias-and-uniquify) + (update :aggregation (partial pre-alias-and-uniquify inner-query)) source-query (update :source-query pre-alias-aggregations-in-inner-query) diff --git a/src/metabase/query_processor/middleware/resolve_joins.clj b/src/metabase/query_processor/middleware/resolve_joins.clj index f30467812f4..415fdd56457 100644 --- a/src/metabase/query_processor/middleware/resolve_joins.clj +++ b/src/metabase/query_processor/middleware/resolve_joins.clj @@ -9,6 +9,7 @@ [metabase.query-processor.middleware.add-implicit-clauses :as qp.add-implicit-clauses] [metabase.query-processor.store :as qp.store] + [metabase.query-processor.util.add-alias-info :as add] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.schema :as su] @@ -117,6 +118,28 @@ [{breakouts :breakout, aggregations :aggregation}] (every? empty? [aggregations breakouts])) +(defn- append-join-fields [fields join-fields] + (into [] + (comp cat + (m/distinct-by (fn [clause] + (-> clause + ;; remove namespaced options and other things that are definitely irrelevant + add/normalize-clause + ;; we shouldn't consider different type info to mean two Fields are different even if + ;; everything else is the same. So give everything `:base-type` of `:type/*` (it will + ;; complain if we remove `:base-type` entirely from fields with a string name) + (mbql.u/update-field-options (fn [opts] + (-> opts + (assoc :base-type :type/*) + (dissoc :effective-type)))))))) + [fields join-fields])) + +(defn append-join-fields-to-fields + "Add the fields from join `:fields`, if any, to the parent-level `:fields`." + [inner-query join-fields] + (cond-> inner-query + (seq join-fields) (update :fields append-join-fields join-fields))) + (s/defn ^:private merge-joins-fields :- UnresolvedMBQLQuery "Append the `:fields` from `:joins` into their parent level as appropriate so joined columns appear in the final query results, and remove the `:fields` entry for all joins. @@ -132,11 +155,7 @@ (cond-> join (keyword? fields) (dissoc :fields))) joins)))] - (cond-> inner-query - (seq join-fields) (update :fields (fn [fields] - (into [] - (comp cat (m/distinct-by mbql.u/remove-namespaced-options)) - [fields join-fields])))))) + (append-join-fields-to-fields inner-query join-fields))) (s/defn ^:private resolve-joins-in-mbql-query :- ResolvedMBQLQuery [query :- mbql.s/MBQLQuery] diff --git a/src/metabase/query_processor/store.clj b/src/metabase/query_processor/store.clj index 7a37c66e614..a7d6d80fd95 100644 --- a/src/metabase/query_processor/store.clj +++ b/src/metabase/query_processor/store.clj @@ -13,6 +13,10 @@ but fetching all Fields in a single pass and storing them for reuse is dramatically more efficient than fetching those Fields potentially dozens of times in a single query execution." (:require + [metabase.lib.metadata.composed-provider + :as lib.metadata.composed-provider] + [metabase.lib.metadata.jvm :as lib.metadata.jvm] + [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.models.database :refer [Database]] [metabase.models.field :refer [Field]] [metabase.models.interface :as mi] @@ -20,13 +24,19 @@ [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.schema :as su] + [pretty.core :as pretty] [schema.core :as s] [toucan2.core :as t2])) +(set! *warn-on-reflection* true) + ;;; ---------------------------------------------- Setting up the Store ---------------------------------------------- (def ^:private uninitialized-store - (delay (throw (Exception. (tru "Error: Query Processor store is not initialized."))))) + (reify + clojure.lang.IDeref + (deref [_this] + (throw (ex-info (tru "Error: Query Processor store is not initialized.") {}))))) (def ^:private ^:dynamic *store* "Dynamic var used as the QP store for a given query execution." @@ -316,3 +326,33 @@ ;; for the unique key use a gensym prefixed by the namespace to make for easier store debugging if needed (let [ks (into [(list 'quote (gensym (str (name (ns-name *ns*)) "/misc-cache-")))] (u/one-or-many k-or-ks))] `(cached-fn ~ks (fn [] ~@body)))) + +(defn- base-metadata-provider [] + (reify + lib.metadata.protocols/MetadataProvider + (database [_this] + (some-> (database) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/database))) + + (table [_this table-id] + (some-> (table table-id) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/table))) + + (field [_this field-id] + (some-> (field field-id) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/field))) + + (card [_this _card-id] nil) + (metric [_this _metric-id] nil) + (segment [_this _segment-id] nil) + (tables [_metadata-provider] nil) + (fields [_metadata-provider _table-id] nil) + + pretty/PrettyPrintable + (pretty [_this] + `metadata-provider))) + +(defn metadata-provider + "Create a new MLv2 metadata provider that uses the QP store." + [] + (cached ::metadata-provider + (lib.metadata.composed-provider/composed-metadata-provider + (base-metadata-provider) + (lib.metadata.jvm/application-database-metadata-provider (:id (database)))))) diff --git a/src/metabase/query_processor/store/metadata_provider.clj b/src/metabase/query_processor/store/metadata_provider.clj new file mode 100644 index 00000000000..ebbb135a021 --- /dev/null +++ b/src/metabase/query_processor/store/metadata_provider.clj @@ -0,0 +1,39 @@ +(ns metabase.query-processor.store.metadata-provider + (:require + [metabase.lib.metadata.composed-provider + :as lib.metadata.composed-provider] + [metabase.lib.metadata.jvm :as lib.metadata.jvm] + [metabase.lib.metadata.protocols :as lib.metadata.protocols] + [metabase.query-processor.store :as qp.store] + [metabase.util :as u] + [pretty.core :as pretty])) + +(defn- base-metadata-provider [] + (reify + lib.metadata.protocols/MetadataProvider + (database [_this] + (some-> (qp.store/database) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/database))) + + (table [_this table-id] + (some-> (qp.store/table table-id) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/table))) + + (field [_this field-id] + (some-> (qp.store/field field-id) (update-keys u/->kebab-case-en) (assoc :lib/type :metadata/field))) + + (card [_this _card-id] nil) + (metric [_this _metric-id] nil) + (segment [_this _segment-id] nil) + (tables [_metadata-provider] nil) + (fields [_metadata-provider _table-id] nil) + + pretty/PrettyPrintable + (pretty [_this] + `metadata-provider))) + +(defn metadata-provider + "Create a new MLv2 metadata provider that uses the QP store." + [] + (qp.store/cached ::metadata-provider + (lib.metadata.composed-provider/composed-metadata-provider + (base-metadata-provider) + (lib.metadata.jvm/application-database-metadata-provider (:id (qp.store/database)))))) diff --git a/src/metabase/query_processor/util/nest_query.clj b/src/metabase/query_processor/util/nest_query.clj index b5402fcb411..4ec6caa9ca6 100644 --- a/src/metabase/query_processor/util/nest_query.clj +++ b/src/metabase/query_processor/util/nest_query.clj @@ -12,6 +12,8 @@ [metabase.mbql.util :as mbql.u] [metabase.plugins.classloader :as classloader] [metabase.query-processor.middleware.annotate :as annotate] + [metabase.query-processor.middleware.resolve-joins + :as qp.middleware.resolve-joins] [metabase.query-processor.store :as qp.store] [metabase.query-processor.util.add-alias-info :as add] [metabase.util :as u])) @@ -27,11 +29,6 @@ [:field _ (_ :guard :join-alias)] &match))) -(defn- add-joined-fields-to-fields [joined-fields source] - (cond-> source - (seq joined-fields) (update :fields (fn [fields] - (m/distinct-by add/normalize-clause (concat fields joined-fields)))))) - (defn- keep-source+alias-props [field] (update field 2 select-keys [::add/source-alias ::add/source-table :join-alias])) @@ -70,7 +67,7 @@ (add/add-alias-info source) (:query source) (dissoc source :limit) - (add-joined-fields-to-fields (joined-fields inner-query) source) + (qp.middleware.resolve-joins/append-join-fields-to-fields source (joined-fields inner-query)) (remove-unused-fields inner-query source) (cond-> source keep-filter? (assoc :filter filter-clause)))] diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 84a7b38e7af..1bc7d825c78 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -849,7 +849,7 @@ (assoc (card-with-name-and-query card-name) :collection_id (u/the-id collection))) (testing "check the correct metadata was fetched and was saved in the DB" - (is (= [{:base_type :type/BigInteger + (is (= [{:base_type :type/Integer :display_name "Count" :name "count" :semantic_type :type/Quantity diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj index b48afa78c9f..2a815a943b3 100644 --- a/test/metabase/api/embed_test.clj +++ b/test/metabase/api/embed_test.clj @@ -105,13 +105,13 @@ (defn ^:deprecated test-query-results "DEPRECATED -- you should use `schema=` instead" ([actual] - (is (= {:data {:cols [(mt/obj->json->obj (qp.test/aggregate-col :count))] - :rows [[100]] - :insights nil - :results_timezone "UTC"} - :json_query {} - :status "completed"} - actual))) + (is (=? {:data {:cols [(mt/obj->json->obj (qp.test/aggregate-col :count))] + :rows [[100]] + :insights nil + :results_timezone "UTC"} + :json_query {} + :status "completed"} + actual))) ([results-format actual] (case results-format diff --git a/test/metabase/driver/sql/query_processor_test.clj b/test/metabase/driver/sql/query_processor_test.clj index c05545316be..f8c442a3ffc 100644 --- a/test/metabase/driver/sql/query_processor_test.clj +++ b/test/metabase/driver/sql/query_processor_test.clj @@ -503,44 +503,47 @@ mbql->native sql.qp-test-util/sql->sql-map)))))) +(def ^:private reference-aggregation-expressions-in-joins-test-expected-sql + '{:select [source.ID AS ID + source.NAME AS NAME + source.CATEGORY_ID AS CATEGORY_ID + source.LATITUDE AS LATITUDE + source.LONGITUDE AS LONGITUDE + source.PRICE AS PRICE + source.RelativePrice AS RelativePrice + source.CategoriesStats__CATEGORY_ID AS CategoriesStats__CATEGORY_ID + source.CategoriesStats__MaxPrice AS CategoriesStats__MaxPrice + source.CategoriesStats__AvgPrice AS CategoriesStats__AvgPrice + source.CategoriesStats__MinPrice AS CategoriesStats__MinPrice] + :from [{:select [VENUES.ID AS ID + VENUES.NAME AS NAME + VENUES.CATEGORY_ID AS CATEGORY_ID + VENUES.LATITUDE AS LATITUDE + VENUES.LONGITUDE AS LONGITUDE + VENUES.PRICE AS PRICE + CAST (VENUES.PRICE AS float) + / + CASE WHEN CategoriesStats.AvgPrice = 0 THEN NULL + ELSE CategoriesStats.AvgPrice END AS RelativePrice + CategoriesStats.CATEGORY_ID AS CategoriesStats__CATEGORY_ID + CategoriesStats.MaxPrice AS CategoriesStats__MaxPrice + CategoriesStats.AvgPrice AS CategoriesStats__AvgPrice + CategoriesStats.MinPrice AS CategoriesStats__MinPrice] + :from [VENUES] + :left-join [{:select [VENUES.CATEGORY_ID AS CATEGORY_ID + MAX (VENUES.PRICE) AS MaxPrice + AVG (VENUES.PRICE) AS AvgPrice + MIN (VENUES.PRICE) AS MinPrice] + :from [VENUES] + :group-by [VENUES.CATEGORY_ID] + :order-by [VENUES.CATEGORY_ID ASC]} AS CategoriesStats + ON VENUES.CATEGORY_ID = CategoriesStats.CATEGORY_ID]} + AS source] + :limit [3]}) + (deftest ^:parallel reference-aggregation-expressions-in-joins-test (testing "See if we can correctly compile a query that references expressions that come from a join" - (is (= '{:select [source.ID AS ID - source.NAME AS NAME - source.CATEGORY_ID AS CATEGORY_ID - source.LATITUDE AS LATITUDE - source.LONGITUDE AS LONGITUDE - source.PRICE AS PRICE - source.RelativePrice AS RelativePrice - source.CategoriesStats__CATEGORY_ID AS CategoriesStats__CATEGORY_ID - source.CategoriesStats__MaxPrice AS CategoriesStats__MaxPrice - source.CategoriesStats__AvgPrice AS CategoriesStats__AvgPrice - source.CategoriesStats__MinPrice AS CategoriesStats__MinPrice] - :from [{:select [VENUES.ID AS ID - VENUES.NAME AS NAME - VENUES.CATEGORY_ID AS CATEGORY_ID - VENUES.LATITUDE AS LATITUDE - VENUES.LONGITUDE AS LONGITUDE - VENUES.PRICE AS PRICE - CAST (VENUES.PRICE AS float) - / - CASE WHEN CategoriesStats.AvgPrice = 0 THEN NULL - ELSE CategoriesStats.AvgPrice END AS RelativePrice - CategoriesStats.CATEGORY_ID AS CategoriesStats__CATEGORY_ID - CategoriesStats.MaxPrice AS CategoriesStats__MaxPrice - CategoriesStats.AvgPrice AS CategoriesStats__AvgPrice - CategoriesStats.MinPrice AS CategoriesStats__MinPrice] - :from [VENUES] - :left-join [{:select [VENUES.CATEGORY_ID AS CATEGORY_ID - MAX (VENUES.PRICE) AS MaxPrice - AVG (VENUES.PRICE) AS AvgPrice - MIN (VENUES.PRICE) AS MinPrice] - :from [VENUES] - :group-by [VENUES.CATEGORY_ID] - :order-by [VENUES.CATEGORY_ID ASC]} AS CategoriesStats - ON VENUES.CATEGORY_ID = CategoriesStats.CATEGORY_ID]} - AS source] - :limit [3]} + (is (= reference-aggregation-expressions-in-joins-test-expected-sql (-> (mt/mbql-query venues {:fields [$id $name @@ -567,6 +570,36 @@ mbql->native sql.qp-test-util/sql->sql-map))))) +(deftest ^:parallel wrong-type-info-test + (testing (str "Same as the test above, but the :field refs have base types different from what the QP would normally " + "calculate; query should still work") + (is (= reference-aggregation-expressions-in-joins-test-expected-sql + (-> (mt/mbql-query venues + {:fields [$id + $name + $category_id + $latitude + $longitude + $price + [:expression "RelativePrice"] + &CategoriesStats.category_id + &CategoriesStats.*MaxPrice/Number + &CategoriesStats.*AvgPrice/Number + &CategoriesStats.*MinPrice/Number] + :expressions {"RelativePrice" [:/ $price &CategoriesStats.*AvgPrice/Number]} + :joins [{:strategy :left-join + :condition [:= $category_id &CategoriesStats.venues.category_id] + :source-query {:source-table $$venues + :aggregation [[:aggregation-options [:max $price] {:name "MaxPrice"}] + [:aggregation-options [:avg $price] {:name "AvgPrice"}] + [:aggregation-options [:min $price] {:name "MinPrice"}]] + :breakout [$category_id]} + :alias "CategoriesStats" + :fields :all}] + :limit 3}) + mbql->native + sql.qp-test-util/sql->sql-map))))) + (deftest expressions-and-coercions-test (testing "Don't cast in both inner select and outer select when expression (#12430)" (mt/with-temp-vals-in-db Field (mt/id :venues :price) {:coercion_strategy :Coercion/UNIXSeconds->DateTime diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc index 37b443f6a01..824119bb86a 100644 --- a/test/metabase/lib/aggregation_test.cljc +++ b/test/metabase/lib/aggregation_test.cljc @@ -2,6 +2,7 @@ (:require #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])) [clojure.test :refer [are deftest is testing]] + [medley.core :as m] [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib] [metabase.lib.metadata :as lib.metadata] @@ -53,19 +54,19 @@ {:column-name "count", :display-name "Count"} [:distinct {} (lib.tu/field-clause :venues :id)] - {:column-name "distinct_ID", :display-name "Distinct values of ID"} + {:column-name "count", :display-name "Distinct values of ID"} [:sum {} (lib.tu/field-clause :venues :id)] - {:column-name "sum_ID", :display-name "Sum of ID"} + {:column-name "sum", :display-name "Sum of ID"} [:+ {} [:count {}] 1] - {:column-name "count_plus_1", :display-name "Count + 1"} + {:column-name "expression", :display-name "Count + 1"} [:+ {} [:min {} (lib.tu/field-clause :venues :id)] [:* {} 2 [:avg {} (lib.tu/field-clause :venues :price)]]] - {:column-name "min_ID_plus_2_times_avg_PRICE" + {:column-name "expression" :display-name "Min of ID + (2 × Average of Price)"} [:+ @@ -77,7 +78,7 @@ [:avg {} (lib.tu/field-clause :venues :price)] 3 [:- {} [:max {} (lib.tu/field-clause :venues :category-id)] 4]]] - {:column-name "min_ID_plus_2_times_avg_PRICE_times_3_times_max_CATEGORY_ID_minus_4" + {:column-name "expression" :display-name "Min of ID + (2 × Average of Price × 3 × (Max of Category ID - 4))"} ;; user-specified names @@ -97,11 +98,11 @@ {:display-name "User-specified Name"} [:min {} (lib.tu/field-clause :venues :id)] [:* {} 2 [:avg {} (lib.tu/field-clause :venues :price)]]] - {:column-name "min_ID_plus_2_times_avg_PRICE" + {:column-name "expression" :display-name "User-specified Name"} [:percentile {} (lib.tu/field-clause :venues :id) 0.95] - {:column-name "p95_ID", :display-name "0.95th percentile of ID"})) + {:column-name "percentile", :display-name "0.95th percentile of ID"})) ;;; the following tests use raw legacy MBQL because they're direct ports of JavaScript tests from MLv1 and I wanted to ;;; make sure that given an existing query, the expected description was generated correctly. @@ -144,13 +145,13 @@ ;; :count, no field [:/ {} [:count {}] 2] {:base-type :type/Float - :name "count_divided_by_2" + :name "expression" :display-name "Count ÷ 2"} ;; :sum [:sum {} [:+ {} (lib.tu/field-clause :venues :price) 1]] {:base-type :type/Integer - :name "sum_PRICE_plus_1" + :name "sum" :display-name "Sum of Price + 1"} ;; options map @@ -164,7 +165,7 @@ (deftest ^:parallel col-info-named-aggregation-test (testing "col info for an `expression` aggregation w/ a named expression should work as expected" (is (=? {:base-type :type/Integer - :name "sum_double-price" + :name "sum" :display-name "Sum of double-price"} (col-info-for-aggregation-clause (lib.tu/venues-query-with-last-stage @@ -240,7 +241,7 @@ (lib/aggregate (lib/sum [:expression {:lib/uuid (str (random-uuid))} "double-price"])))] (is (=? [{:lib/type :metadata/field :base-type :type/Integer - :name "sum_double-price" + :name "sum" :display-name "Sum of double-price"}] (lib/aggregations-metadata query))) (is (= :type/Integer @@ -410,7 +411,7 @@ agg-query)) (is (=? [{:lib/type :metadata/field :effective-type :type/Integer - :name "sum_double-price" + :name "sum" :display-name "Sum of double-price" :lib/source :source/aggregations} {:lib/type :metadata/field @@ -421,7 +422,7 @@ {:settings {:is_priceless true} :lib/type :metadata/field :effective-type :type/Integer - :name "sum_PRICE" + :name "sum" :display-name "Sum of Price" :lib/source :source/aggregations}] (lib/aggregations-metadata agg-query))))))) @@ -528,7 +529,7 @@ (is (=? {:settings {:is_priceless true} :lib/type :metadata/field :effective-type :type/Integer - :name "sum_PRICE" + :name "sum" :display-name "Sum of Price" :lib/source :source/aggregations} (lib.metadata.calculation/metadata query (first (lib/aggregations-metadata query -1)))))))) @@ -634,12 +635,12 @@ :fingerprint {:global {:distinct-count 28, :nil% 0.0}}} {:lib/type :metadata/field :base-type :type/Integer - :name "sum_case" + :name "sum" :display-name "Sum of Case" :lib/source :source/aggregations :lib/source-uuid string? - :lib/source-column-alias "sum_case" - :lib/desired-column-alias "sum_case"}] + :lib/source-column-alias "sum" + :lib/desired-column-alias "sum"}] (lib.metadata.calculation/metadata query))))) (deftest ^:parallel count-display-name-test @@ -668,3 +669,16 @@ (map (partial lib/display-name query) (lib.metadata.calculation/metadata query))) "display name")))))))) + +(deftest ^:parallel aggregation-name-from-previous-stage-test + (testing "Maintain the column names in refs from the QP/MLv1 (#31266)" + (let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :products)) + (lib/expression "Half Price" (lib// (meta/field-metadata :products :price) 2)) + (lib/aggregate (lib/avg (lib/ref-lookup :expression "Half Price"))) + (lib/breakout (lib/with-temporal-bucket (meta/field-metadata :products :created-at) :month)) + lib/append-stage) + ag-op (m/find-first #(= (:short %) :max) + (lib/available-aggregation-operators query)) + expr-metadata (last (lib/aggregation-operator-columns ag-op))] + (is (=? [:field {:lib/uuid string?, :base-type :type/Float} "avg"] + (lib/ref expr-metadata)))))) diff --git a/test/metabase/lib/expression_test.cljc b/test/metabase/lib/expression_test.cljc index 850bf851703..606aabb66cd 100644 --- a/test/metabase/lib/expression_test.cljc +++ b/test/metabase/lib/expression_test.cljc @@ -131,7 +131,7 @@ [:expression {:lib/uuid (str (random-uuid))} "double-price"]]] (is (= "Sum of double-price" (lib.metadata.calculation/display-name query -1 expr))) - (is (= "sum_double-price" + (is (= "sum" (lib.metadata.calculation/column-name query -1 expr))))) (deftest ^:parallel coalesce-names-test diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc index 069baac40f7..9cc55c27795 100644 --- a/test/metabase/lib/field_test.cljc +++ b/test/metabase/lib/field_test.cljc @@ -6,6 +6,8 @@ [metabase.lib.core :as lib] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] + [metabase.lib.metadata.composed-provider + :as lib.metadata.composed-provider] [metabase.lib.schema.expression :as lib.schema.expression] [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.test-metadata :as meta] @@ -157,7 +159,7 @@ time-field (assoc (meta/field-metadata :orders :created-at) :base-type :type/Time :effective-type :type/Time) - metadata-provider (lib.tu/composed-metadata-provider + metadata-provider (lib.metadata.composed-provider/composed-metadata-provider (lib.tu/mock-metadata-provider {:fields [date-field time-field]}) @@ -391,7 +393,16 @@ (meta/id :categories :name)]]]}]} query'))) (is (= "Venues, Sorted by Category → Name ascending" - (lib/describe-query query'))))))) + (lib/describe-query query')))) + (testing "inside aggregations" + (let [query' (lib/aggregate query (lib/distinct categories-name)) + [aggregation] (lib/aggregations query')] + (is (= "Venues, Distinct values of Category → Name" + (lib/describe-query query'))) + (are [style expected] (= expected + (lib/display-name query' -1 aggregation style)) + :long "Distinct values of Category → Name" + :default "Distinct values of Name")))))) (deftest ^:parallel source-card-table-display-info-test (let [query (assoc lib.tu/venues-query :lib/metadata lib.tu/metadata-provider-with-card) @@ -413,7 +424,7 @@ :query {:source-table (meta/id :checkins) :aggregation [[:count]] :breakout [[:field (meta/id :checkins :user-id) nil]]}}} - metadata-provider (lib.tu/composed-metadata-provider + metadata-provider (lib.metadata.composed-provider/composed-metadata-provider meta/metadata-provider (lib.tu/mock-metadata-provider {:cards [card-1]})) @@ -453,10 +464,10 @@ :lib/source :source/breakouts :lib/source-column-alias "LAST_LOGIN" :lib/desired-column-alias "LAST_LOGIN"} - {:name "avg_count" + {:name "avg" :lib/source :source/aggregations - :lib/source-column-alias "avg_count" - :lib/desired-column-alias "avg_count"}] + :lib/source-column-alias "avg" + :lib/desired-column-alias "avg"}] (lib.metadata.calculation/metadata query)))))) (deftest ^:parallel with-fields-test diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc index 76556cf5dee..e5aed011b8a 100644 --- a/test/metabase/lib/join_test.cljc +++ b/test/metabase/lib/join_test.cljc @@ -6,6 +6,7 @@ [metabase.lib.join :as lib.join] [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.calculation :as lib.metadata.calculation] + [metabase.lib.metadata.composed-provider :as lib.metadata.composed-provider] [metabase.lib.test-metadata :as meta] [metabase.lib.test-util :as lib.tu] #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) @@ -158,7 +159,7 @@ :query {:source-table (meta/id :checkins) :aggregation [[:count]] :breakout [[:field (meta/id :checkins :user-id) nil]]}}} - metadata-provider (lib.tu/composed-metadata-provider + metadata-provider (lib.metadata.composed-provider/composed-metadata-provider meta/metadata-provider (lib.tu/mock-metadata-provider {:cards [card-1]})) diff --git a/test/metabase/lib/metadata/composed_provider_test.cljc b/test/metabase/lib/metadata/composed_provider_test.cljc new file mode 100644 index 00000000000..a54b3735109 --- /dev/null +++ b/test/metabase/lib/metadata/composed_provider_test.cljc @@ -0,0 +1,28 @@ +(ns metabase.lib.metadata.composed-provider-test + (:require + [clojure.test :refer [deftest is testing]] + [metabase.lib.metadata :as lib.metadata] + [metabase.lib.metadata.composed-provider + :as lib.metadata.composed-provider] + [metabase.lib.test-metadata :as meta] + [metabase.lib.test-util :as lib.tu] + #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal])))) + +#?(:cljs + (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me)) + +(deftest ^:parallel composed-metadata-provider-test + (testing "Return things preferentially from earlier metadata providers" + (let [time-field (assoc (meta/field-metadata :people :birth-date) + :base-type :type/Time + :effective-type :type/Time) + metadata-provider (lib.metadata.composed-provider/composed-metadata-provider + (lib.tu/mock-metadata-provider + {:fields [time-field]}) + meta/metadata-provider)] + (is (=? {:name "BIRTH_DATE" + :base-type :type/Time + :effective-type :type/Time} + (lib.metadata/field + metadata-provider + (meta/id :people :birth-date))))))) diff --git a/test/metabase/lib/order_by_test.cljc b/test/metabase/lib/order_by_test.cljc index 005e3d28417..0caf3bcc031 100644 --- a/test/metabase/lib/order_by_test.cljc +++ b/test/metabase/lib/order_by_test.cljc @@ -141,12 +141,12 @@ :base-type :type/Integer} {:lib/type :metadata/field :base-type :type/Integer - :name "sum_PRICE" + :name "sum" :display-name "Sum of Price" :lib/source :source/aggregations} {:lib/type :metadata/field :base-type :type/Float - :name "avg_PRICE_plus_1" + :name "avg" :display-name "Average of Price + 1" :lib/source :source/aggregations}] (lib/orderable-columns query))))))) diff --git a/test/metabase/lib/remove_replace_test.cljc b/test/metabase/lib/remove_replace_test.cljc index ae426057aeb..018f5ac03eb 100644 --- a/test/metabase/lib/remove_replace_test.cljc +++ b/test/metabase/lib/remove_replace_test.cljc @@ -190,7 +190,7 @@ (-> query (lib/order-by (lib.dev/ref-lookup :aggregation 0)) (lib/append-stage) - (lib/filter (lib/= [:field {:lib/uuid (str (random-uuid)) :base-type :type/Integer} "sum_ID"] 1)) + (lib/filter (lib/= [:field {:lib/uuid (str (random-uuid)) :base-type :type/Integer} "sum"] 1)) (lib/remove-clause 0 (first aggregations)))))))) (deftest ^:parallel remove-clause-expression-test @@ -360,7 +360,7 @@ (lib/expression "expr" (lib.dev/ref-lookup :aggregation 0)) (lib/append-stage) ;; TODO Should be able to create a ref with lib/field [#29763] - (lib/filter (lib/= [:field {:lib/uuid (str (random-uuid)) :base-type :type/Integer} "sum_ID"] 1)) + (lib/filter (lib/= [:field {:lib/uuid (str (random-uuid)) :base-type :type/Integer} "sum"] 1)) (lib/replace-clause 0 (first aggregations) (lib/sum (lib/field "VENUES" "PRICE"))))))))) (deftest ^:parallel replace-clause-expression-test diff --git a/test/metabase/lib/stage_test.cljc b/test/metabase/lib/stage_test.cljc index bb19c5846be..09e4007ca49 100644 --- a/test/metabase/lib/stage_test.cljc +++ b/test/metabase/lib/stage_test.cljc @@ -41,15 +41,15 @@ 0.8 [:avg {} (lib.tu/field-clause :venues :price)]]]})] (is (=? [{:base-type :type/Float - :name "0_8_times_avg_PRICE" + :name "expression" :display-name "0.8 × Average of Price" - :lib/source-column-alias "0_8_times_avg_PRICE" - :lib/desired-column-alias "0_8_times_avg_PRICE"} + :lib/source-column-alias "expression" + :lib/desired-column-alias "expression"} {:base-type :type/Float - :name "0_8_times_avg_PRICE" + :name "expression" :display-name "0.8 × Average of Price" - :lib/source-column-alias "0_8_times_avg_PRICE" - :lib/desired-column-alias "0_8_times_avg_PRICE_2"}] + :lib/source-column-alias "expression" + :lib/desired-column-alias "expression_2"}] (lib.metadata.calculation/metadata query -1 query))))))) (deftest ^:parallel stage-display-name-card-source-query diff --git a/test/metabase/lib/test_util.cljc b/test/metabase/lib/test_util.cljc index a87bab7327d..bc5bdb2b025 100644 --- a/test/metabase/lib/test_util.cljc +++ b/test/metabase/lib/test_util.cljc @@ -2,12 +2,12 @@ "Misc test utils for Metabase lib." (:require [clojure.core.protocols] - [clojure.datafy :as datafy] - [clojure.test :refer [deftest is testing]] + [clojure.test :refer [is]] [malli.core :as mc] [medley.core :as m] [metabase.lib.core :as lib] - [metabase.lib.metadata :as lib.metadata] + [metabase.lib.metadata.composed-provider + :as lib.metadata.composed-provider] [metabase.lib.metadata.protocols :as metadata.protocols] [metabase.lib.schema :as lib.schema] [metabase.lib.test-metadata :as meta] @@ -71,44 +71,11 @@ (datafy [_this] (list `mock-metadata-provider m)))) -(defn composed-metadata-provider - "A metadata provider composed of several different `metadata-providers`. Methods try each constituent provider in - turn from left to right until one returns a truthy result." - [& metadata-providers] - (reify - metadata.protocols/MetadataProvider - (database [_this] (some metadata.protocols/database metadata-providers)) - (table [_this table-id] (some #(metadata.protocols/table % table-id) metadata-providers)) - (field [_this field-id] (some #(metadata.protocols/field % field-id) metadata-providers)) - (card [_this card-id] (some #(metadata.protocols/card % card-id) metadata-providers)) - (metric [_this metric-id] (some #(metadata.protocols/metric % metric-id) metadata-providers)) - (segment [_this segment-id] (some #(metadata.protocols/segment % segment-id) metadata-providers)) - (tables [_this] (m/distinct-by :id (mapcat metadata.protocols/tables metadata-providers))) - (fields [_this table-id] (m/distinct-by :id (mapcat #(metadata.protocols/fields % table-id) metadata-providers))) - clojure.core.protocols/Datafiable - (datafy [_this] - (cons `composed-metadata-provider (map datafy/datafy metadata-providers))))) - -(deftest ^:parallel composed-metadata-provider-test - (testing "Return things preferentially from earlier metadata providers" - (let [time-field (assoc (meta/field-metadata :people :birth-date) - :base-type :type/Time - :effective-type :type/Time) - metadata-provider (composed-metadata-provider - (mock-metadata-provider - {:fields [time-field]}) - meta/metadata-provider)] - (is (=? {:name "BIRTH_DATE" - :base-type :type/Time - :effective-type :type/Time} - (lib.metadata/field - metadata-provider - (meta/id :people :birth-date))))))) (def metadata-provider-with-card "[[meta/metadata-provider]], but with a Card with ID 1." - (composed-metadata-provider + (lib.metadata.composed-provider/composed-metadata-provider meta/metadata-provider (mock-metadata-provider {:cards [{:name "My Card" @@ -131,7 +98,7 @@ (def metadata-provider-with-card-with-result-metadata "[[meta/metadata-provider]], but with a Card with results metadata as ID 1." - (composed-metadata-provider + (lib.metadata.composed-provider/composed-metadata-provider meta/metadata-provider (mock-metadata-provider {:cards [{:name "My Card" 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 89c247720bd..8d9a72f767b 100644 --- a/test/metabase/query_processor/middleware/add_source_metadata_test.clj +++ b/test/metabase/query_processor/middleware/add_source_metadata_test.clj @@ -32,7 +32,7 @@ (mt/run-mbql-query venues {:fields (for [id field-ids] [:field id nil]) :limit 1}))))) -(deftest basic-test +(deftest ^:parallel basic-test (testing "Can we automatically add source metadata to the parent level of a query? If the source query has `:fields`" (is (= (mt/mbql-query venues {:source-query {:source-table $$venues @@ -41,8 +41,9 @@ (add-source-metadata (mt/mbql-query venues {:source-query {:source-table $$venues - :fields [$id $name]}}))))) + :fields [$id $name]}})))))) +(deftest ^:parallel basic-parent-level-test (testing (str "Can we automatically add source metadata to the parent level of a query? If the source query does not " "have `:fields`") (is (= (mt/mbql-query venues @@ -50,8 +51,9 @@ :source-metadata (venues-source-metadata)}) (add-source-metadata (mt/mbql-query venues - {:source-query {:source-table $$venues}}))))) + {:source-query {:source-table $$venues}})))))) +(deftest ^:parallel basic-summary-columns-test (testing "Can we add source metadata for a source query that has breakouts/aggregations?" (is (= (mt/mbql-query venues {:source-query {:source-table $$venues @@ -61,15 +63,16 @@ (venues-source-metadata :price) [{:name "count" :display_name "Count" - :base_type :type/BigInteger + :base_type :type/Integer :semantic_type :type/Quantity :field_ref [:aggregation 0]}])}) (add-source-metadata (mt/mbql-query venues {:source-query {:source-table $$venues :aggregation [[:count]] - :breakout [$price]}}))))) + :breakout [$price]}})))))) +(deftest ^:parallel basic-aggregation-with-field-test (testing "Can we add source metadata for a source query that has an aggregation for a specific Field?" (is (= (mt/mbql-query venues {:source-query {:source-table $$venues @@ -79,8 +82,7 @@ (venues-source-metadata :price) [{:name "avg" :display_name "Average of ID" - :base_type :type/BigInteger - :semantic_type :type/PK + :base_type :type/Float :settings nil :field_ref [:aggregation 0]}])}) (add-source-metadata @@ -92,7 +94,7 @@ (defn- source-metadata [query] (get-in query [:query :source-metadata] query)) -(deftest named-aggregations-test +(deftest ^:parallel named-aggregations-test (testing "adding source metadata for source queries with named aggregations" (testing "w/ `:name` and `:display-name`" (is (= (mt/mbql-query venues @@ -105,8 +107,7 @@ (venues-source-metadata :price) [{:name "some_generated_name" :display_name "My Cool Ag" - :base_type :type/BigInteger - :semantic_type :type/PK + :base_type :type/Float :settings nil :field_ref [:aggregation 0]}])}) (add-source-metadata @@ -115,35 +116,35 @@ :aggregation [[:aggregation-options [:avg $id] {:name "some_generated_name", :display-name "My Cool Ag"}]] - :breakout [$price]}}))))) + :breakout [$price]}}))))))) - (testing "w/ `:name` only" - (is (= [{:name "some_generated_name" - :display_name "Average of ID" - :base_type :type/BigInteger - :semantic_type :type/PK - :settings nil - :field_ref [:aggregation 0]}] - (source-metadata - (add-source-metadata - (mt/mbql-query venues - {:source-query {:source-table $$venues - :aggregation [[:aggregation-options [:avg $id] {:name "some_generated_name"}]]}})))))) +(deftest ^:parallel named-aggregations-name-only-test + (testing "w/ `:name` only" + (is (= [{:name "some_generated_name" + :display_name "Average of ID" + :base_type :type/Float + :settings nil + :field_ref [:aggregation 0]}] + (source-metadata + (add-source-metadata + (mt/mbql-query venues + {:source-query {:source-table $$venues + :aggregation [[:aggregation-options [:avg $id] {:name "some_generated_name"}]]}}))))))) - (testing "w/ `:display-name` only" - (is (= [{:name "avg" - :display_name "My Cool Ag" - :base_type :type/BigInteger - :semantic_type :type/PK - :settings nil - :field_ref [:aggregation 0]}] - (source-metadata - (add-source-metadata - (mt/mbql-query venues - {:source-query {:source-table $$venues - :aggregation [[:aggregation-options [:avg $id] {:display-name "My Cool Ag"}]]}})))))))) +(deftest ^:parallel named-aggregations-display-name-only-test + (testing "w/ `:display-name` only" + (is (= [{:name "avg" + :display_name "My Cool Ag" + :base_type :type/Float + :settings nil + :field_ref [:aggregation 0]}] + (source-metadata + (add-source-metadata + (mt/mbql-query venues + {:source-query {:source-table $$venues + :aggregation [[:aggregation-options [:avg $id] {:display-name "My Cool Ag"}]]}}))))))) -(deftest nested-sources-test +(deftest ^:parallel nested-sources-test (testing (str "Can we automatically add source metadata to the parent level of a query? If the source query has a " "source query with source metadata") (is (= (mt/mbql-query venues @@ -155,8 +156,9 @@ (mt/mbql-query venues {:source-query {:source-query {:source-table $$venues :fields [$id $name]} - :source-metadata (venues-source-metadata :id :name)}}))))) + :source-metadata (venues-source-metadata :id :name)}})))))) +(deftest ^:parallel nested-sources-3-levels-with-source-metadata-test (testing "Can we automatically add source metadata if a source-query nested 3 levels has `:source-metadata`?" (is (= (mt/mbql-query venues {:source-query {:source-query {:source-query {:source-table $$venues @@ -170,8 +172,9 @@ {:source-query {:source-query {:source-table $$venues :fields [$id $name]} - :source-metadata (venues-source-metadata :id :name)}}}))))) + :source-metadata (venues-source-metadata :id :name)}}})))))) +(deftest ^:parallel nested-sources-3-levels-with-no-source-metadata-test (testing "Ok, how about a source query nested 3 levels with no `source-metadata`?" (is (= (mt/mbql-query venues {:source-query {:source-query {:source-query {:source-table $$venues} @@ -180,44 +183,49 @@ :source-metadata (venues-source-metadata)}) (add-source-metadata (mt/mbql-query venues - {:source-query {:source-query {:source-query {:source-table $$venues}}}})))) + {:source-query {:source-query {:source-query {:source-table $$venues}}}})))))) - (testing "with `fields`" - (is (= (mt/mbql-query venues - {:source-query {:source-query {:source-query {:source-table $$venues - :fields [$id $name]} - :source-metadata (venues-source-metadata :id :name)} - :source-metadata (venues-source-metadata :id :name)} - :source-metadata (venues-source-metadata :id :name)}) - (add-source-metadata - (mt/mbql-query venues - {:source-query {:source-query {:source-query {:source-table $$venues - :fields [$id $name]}}}}))))) - (testing "with breakouts/aggregations" - ;; field ref for the count aggregation differs slightly depending on what level of the query we're at; at the - ;; most-deeply-nested level we can use the `[:aggregation 0]` ref to refer to it; at higher levels we have to - ;; refer to it with a field literal - (is (= (letfn [(metadata-with-count-field-ref [field-ref] - (concat - (venues-source-metadata :price) - (let [[count-col] (results-metadata (mt/run-mbql-query venues {:aggregation [[:count]]}))] - [(-> count-col - (dissoc :effective_type) - (assoc :field_ref field-ref))])))] - (mt/mbql-query venues - {:source-query {:source-query {:source-query {:source-table $$venues - :aggregation [[:count]] - :breakout [$price]} - :source-metadata (metadata-with-count-field-ref [:aggregation 0])} - :source-metadata (metadata-with-count-field-ref *count/BigInteger)} - :source-metadata (metadata-with-count-field-ref *count/BigInteger)})) - (add-source-metadata - (mt/mbql-query venues - {:source-query {:source-query {:source-query {:source-table $$venues - :aggregation [[:count]] - :breakout [$price]}}}})))))) +(deftest ^:parallel nested-sources-3-levels-with-fields-test + (testing "nested 3 levels with `fields`" + (is (= (mt/mbql-query venues + {:source-query {:source-query {:source-query {:source-table $$venues + :fields [$id $name]} + :source-metadata (venues-source-metadata :id :name)} + :source-metadata (venues-source-metadata :id :name)} + :source-metadata (venues-source-metadata :id :name)}) + (add-source-metadata + (mt/mbql-query venues + {:source-query {:source-query {:source-query {:source-table $$venues + :fields [$id $name]}}}})))))) + +(deftest ^:parallel nested-sources-3-levels-with-summary-columns-test + (testing "nested 3 levels with breakouts/aggregations" + ;; field ref for the count aggregation differs slightly depending on what level of the query we're at; at the + ;; most-deeply-nested level we can use the `[:aggregation 0]` ref to refer to it; at higher levels we have to + ;; refer to it with a field literal + (is (= (letfn [(metadata-with-count-field-ref [field-ref] + (concat + (venues-source-metadata :price) + (let [[count-col] (results-metadata (mt/run-mbql-query venues {:aggregation [[:count]]}))] + [(-> count-col + (dissoc :effective_type) + (assoc :field_ref field-ref + :base_type :type/Integer))])))] + (mt/mbql-query venues + {:source-query {:source-query {:source-query {:source-table $$venues + :aggregation [[:count]] + :breakout [$price]} + :source-metadata (metadata-with-count-field-ref [:aggregation 0])} + :source-metadata (metadata-with-count-field-ref *count/Integer)} + :source-metadata (metadata-with-count-field-ref *count/Integer)})) + (add-source-metadata + (mt/mbql-query venues + {:source-query {:source-query {:source-query {:source-table $$venues + :aggregation [[:count]] + :breakout [$price]}}}})))))) +(deftest ^:parallel nested-sources-with-source-native-query-test (testing "can we add `source-metadata` to the parent level if the source query has a native source query, but itself has `source-metadata`?" (is (= (mt/mbql-query venues {:source-query {:source-query {:native "SELECT \"ID\", \"NAME\" FROM \"VENUES\";"} @@ -228,7 +236,7 @@ {:source-query {:source-query {:native "SELECT \"ID\", \"NAME\" FROM \"VENUES\";"} :source-metadata (venues-source-metadata :id :name)}})))))) -(deftest joins-test +(deftest ^:parallel joins-test (testing "should work inside JOINS as well" (is (= (mt/mbql-query venues {:source-table $$venues @@ -249,17 +257,21 @@ :aggregation [[:count]] :breakout [[:field %latitude {:binning {:strategy :default}}]]} :source-metadata (concat - (let [[lat-col] (venues-source-metadata :latitude)] + (let [[lat-col] (venues-source-metadata :latitude) + [count-col] (results-metadata (mt/run-mbql-query venues {:aggregation [[:count]]}))] [(assoc lat-col :field_ref [:field (mt/id :venues :latitude) - {:binning {:strategy :bin-width + {:binning {:strategy :bin-width :min-value 10.0 :max-value 45.0 :num-bins 7 - :bin-width 5.0}}])]) - ;; computed column doesn't have an effective type in middleware before query - (map #(dissoc % :effective_type) - (results-metadata (mt/run-mbql-query venues {:aggregation [[:count]]}))))}) + :bin-width 5.0}}]) + ;; computed column doesn't have an effective type in middleware before query + (-> count-col + (dissoc :effective_type) + ;; the type that comes back from H2 is :type/BigInteger but the type that comes + ;; back from calculating it with MLv2 is just plain :type/Integer + (assoc :base_type :type/Integer))]))}) (add-source-metadata (mt/mbql-query venues {:source-query @@ -267,7 +279,7 @@ :aggregation [[:count]] :breakout [[:field %latitude {:binning {:strategy :default}}]]}}))))))) -(deftest deduplicate-column-names-test +(deftest ^:parallel deduplicate-column-names-test (testing "Metadata that gets added to source queries should have deduplicated column names" (let [query (add-source-metadata (mt/mbql-query checkins diff --git a/test/metabase/query_processor/middleware/annotate_test.clj b/test/metabase/query_processor/middleware/annotate_test.clj index 9a1b4b0358d..a15db537ba5 100644 --- a/test/metabase/query_processor/middleware/annotate_test.clj +++ b/test/metabase/query_processor/middleware/annotate_test.clj @@ -69,61 +69,81 @@ (is (= [(merge (info-for-field :venues :price) {:source :fields :field_ref $price})] - (doall - (annotate/column-info - {:type :query, :query {:fields [$price]}} - {:columns [:price]})))))))) + (annotate/column-info + {:type :query, :query {:fields [$price]}} + {:columns [:price]}))))))) -(deftest ^:parallel col-info-for-fks-and-joins-test +(deftest ^:parallel col-info-for-implicit-joins-test (mt/with-everything-store (mt/$ids venues (testing (str "when a `:field` with `:source-field` (implicit join) is used, we should add in `:fk_field_id` " "info about the source Field") (is (= [(merge (info-for-field :categories :name) - {:fk_field_id %category_id - :source :fields - :field_ref $category_id->categories.name})] - (doall + {:fk_field_id %category_id + :source :fields + :field_ref $category_id->categories.name + ;; for whatever reason this is what the `annotate` middleware traditionally returns here, for + ;; some reason we use the `:long` style inside aggregations and the `:default` style elsewhere, + ;; who knows why. See notes + ;; on [[metabase.query-processor.middleware.annotate/col-info-for-aggregation-clause]] + :display_name "Name"})] + (annotate/column-info + {:type :query, :query {:fields [$category_id->categories.name]}} + {:columns [:name]}))))))) + +(deftest ^:parallel col-info-for-implicit-joins-aggregation-test + (mt/with-everything-store + (mt/$ids venues + (testing (str "when a `:field` with `:source-field` (implicit join) is used, we should add in `:fk_field_id` " + "info about the source Field") + (is (=? [{:source :aggregation + :field_ref [:aggregation 0] + :display_name "Distinct values of Category → Name"}] (annotate/column-info - {:type :query, :query {:fields [$category_id->categories.name]}} - {:columns [:name]}))))) - - (testing "joins" - (testing (str "we should get `:fk_field_id` and information where possible when using joins; " - "display_name should include the display name of the FK field (for IMPLICIT JOINS)") - (is (= [(merge (info-for-field :categories :name) - {:display_name "Category → Name" - :source :fields - :field_ref $category_id->categories.name - :fk_field_id %category_id - :source_alias "CATEGORIES__via__CATEGORY_ID"})] - (doall - (annotate/column-info - {:type :query - :query {:fields [&CATEGORIES__via__CATEGORY_ID.categories.name] - :joins [{:alias "CATEGORIES__via__CATEGORY_ID" - :source-table $$venues - :condition [:= $category_id &CATEGORIES__via__CATEGORY_ID.categories.id] - :strategy :left-join - :fk-field-id %category_id}]}} - {:columns [:name]}))))) - - (testing (str "for EXPLICIT JOINS (which do not include an `:fk-field-id` in the Join info) the returned " - "`:field_ref` should be have only `:join-alias`, and no `:source-field`") - (is (= [(merge (info-for-field :categories :name) - {:display_name "Categories → Name" - :source :fields - :field_ref &Categories.categories.name - :source_alias "Categories"})] - (doall - (annotate/column-info - {:type :query - :query {:fields [&Categories.categories.name] - :joins [{:alias "Categories" - :source-table $$venues - :condition [:= $category_id &Categories.categories.id] - :strategy :left-join}]}} - {:columns [:name]}))))))))) + {:type :query + :query {:source-table $$venues + :aggregation [[:distinct $category_id->categories.name]]}} + {:columns [:name]}))))))) + +(deftest ^:parallel col-info-for-explicit-joins-with-fk-field-id-test + (mt/with-everything-store + (mt/$ids venues + (testing (str "we should get `:fk_field_id` and information where possible when using joins; " + "display_name should include the display name of the FK field (for IMPLICIT JOINS)") + (is (= [(merge (info-for-field :categories :name) + {:display_name "Category → Name" + :source :fields + :field_ref $category_id->categories.name + :fk_field_id %category_id + :source_alias "CATEGORIES__via__CATEGORY_ID"})] + (annotate/column-info + {:type :query + :query {:fields [&CATEGORIES__via__CATEGORY_ID.categories.name] + :joins [{:alias "CATEGORIES__via__CATEGORY_ID" + :source-table $$venues + :condition [:= $category_id &CATEGORIES__via__CATEGORY_ID.categories.id] + :strategy :left-join + :fk-field-id %category_id}]}} + {:columns [:name]}))))))) + +(deftest ^:parallel col-info-for-explicit-joins-without-fk-field-id-test + (mt/with-everything-store + (mt/$ids venues + (testing (str "for EXPLICIT JOINS (which do not include an `:fk-field-id` in the Join info) the returned " + "`:field_ref` should be have only `:join-alias`, and no `:source-field`") + (is (= [(merge (info-for-field :categories :name) + {:display_name "Categories → Name" + :source :fields + :field_ref &Categories.categories.name + :source_alias "Categories"})] + (annotate/column-info + {:type :query + :query {:fields [&Categories.categories.name] + :joins [{:alias "Categories" + :source-table $$venues + :condition [:= $category_id &Categories.categories.id] + :strategy :left-join}]}} + {:columns [:name]}))))))) (deftest ^:parallel col-info-for-field-with-temporal-unit-test (mt/with-everything-store @@ -136,8 +156,11 @@ (doall (annotate/column-info {:type :query, :query {:fields (mt/$ids venues [!month.price])}} - {:columns [:price]}))))) + {:columns [:price]})))))))) +(deftest ^:parallel col-info-for-field-literal-with-temporal-unit-test + (mt/with-everything-store + (mt/$ids venues (testing "datetime unit should work on field literals too" (is (= [{:name "price" :base_type :type/Number @@ -148,8 +171,10 @@ (doall (annotate/column-info {:type :query, :query {:fields [[:field "price" {:base-type :type/Number, :temporal-unit :month}]]}} - {:columns [:price]})))))) + {:columns [:price]})))))))) +(deftest ^:parallel col-info-for-field-with-temporal-unit-from-nested-query-test + (mt/with-everything-store (testing "should add the correct info if the Field originally comes from a nested query" (mt/$ids checkins (is (= [{:name "DATE", :unit :month, :field_ref [:field %date {:temporal-unit :default}]} @@ -230,8 +255,9 @@ :display_name "Child" :fingerprint nil :base_type :type/Text} - (into {} (#'annotate/col-info-for-field-clause {} [:field (u/the-id child) nil]))))))) + (into {} (#'annotate/col-info-for-field-clause {} [:field (u/the-id child) nil])))))))) +(deftest col-info-combine-grandparent-field-names-test (testing "nested-nested fields should include grandparent name (etc)" (t2.with-temp/with-temp [Field grandparent {:name "grandparent", :table_id (mt/id :venues)} Field parent {:name "parent", :table_id (mt/id :venues), :parent_id (u/the-id grandparent)} @@ -322,13 +348,12 @@ ;; test that added information about aggregations looks the way we'd expect (defn- aggregation-names ([ag-clause] - (aggregation-names {} ag-clause)) + (aggregation-names {:source-table (mt/id :venues)} ag-clause)) ([inner-query ag-clause] (binding [driver/*driver* :h2] (mt/with-everything-store - {:name (annotate/aggregation-name ag-clause) - :display_name (annotate/aggregation-display-name inner-query ag-clause)})))) + (select-keys (#'annotate/col-info-for-aggregation-clause inner-query ag-clause) [:name :display_name]))))) (deftest ^:parallel aggregation-names-test (testing "basic aggregations" @@ -350,14 +375,14 @@ (aggregation-names [:+ [:count] 1])))) (testing "expression with nested expressions" - (is (= {:name "expression", :display_name "Min of ID + (2 * Average of Price)"} + (is (= {:name "expression", :display_name "Min of ID + (2 × Average of Price)"} (aggregation-names [:+ [:min [:field (mt/id :venues :id) nil]] [:* 2 [:avg [:field (mt/id :venues :price) nil]]]])))) (testing "very complicated expression" - (is (= {:name "expression", :display_name "Min of ID + (2 * Average of Price * 3 * (Max of Category ID - 4))"} + (is (= {:name "expression", :display_name "Min of ID + (2 × Average of Price × 3 × (Max of Category ID - 4))"} (aggregation-names [:+ [:min [:field (mt/id :venues :id) nil]] @@ -376,7 +401,7 @@ {:name "generated_name", :display-name "User-specified Name"}])))) (testing "`:name` only" - (is (= {:name "generated_name", :display_name "Min of ID + (2 * Average of Price)"} + (is (= {:name "generated_name", :display_name "Min of ID + (2 × Average of Price)"} (aggregation-names [:aggregation-options [:+ [:min [:field (mt/id :venues :id) nil]] [:* 2 [:avg [:field (mt/id :venues :price) nil]]]] @@ -391,7 +416,7 @@ (defn- col-info-for-aggregation-clause ([clause] - (col-info-for-aggregation-clause {} clause)) + (col-info-for-aggregation-clause {:source-table (mt/id :venues)} clause)) ([inner-query clause] (binding [driver/*driver* :h2] @@ -401,60 +426,60 @@ (mt/with-everything-store (testing "basic aggregation clauses" (testing "`:count` (no field)" - (is (= {:base_type :type/Float, :name "expression", :display_name "Count / 2"} - (col-info-for-aggregation-clause [:/ [:count] 2])))) + (is (=? {:base_type :type/Float, :name "expression", :display_name "Count ÷ 2"} + (col-info-for-aggregation-clause [:/ [:count] 2])))) (testing "`:sum`" - (is (= {:base_type :type/Float, :name "sum", :display_name "Sum of Price + 1"} - (mt/$ids venues - (col-info-for-aggregation-clause [:sum [:+ $price 1]])))))) + (is (=? {:base_type :type/Integer, :name "sum", :display_name "Sum of Price + 1"} + (mt/$ids venues + (col-info-for-aggregation-clause [:sum [:+ $price 1]])))))) (testing "`:aggregation-options`" (testing "`:name` and `:display-name`" - (is (= {:base_type :type/Integer - :semantic_type :type/Category - :settings nil - :name "sum_2" - :display_name "My custom name"} - (mt/$ids venues - (col-info-for-aggregation-clause - [:aggregation-options [:sum $price] {:name "sum_2", :display-name "My custom name"}]))))) + (is (=? {:base_type :type/Integer + :settings nil + :name "sum_2" + :display_name "My custom name"} + (mt/$ids venues + (col-info-for-aggregation-clause + [:aggregation-options [:sum $price] {:name "sum_2", :display-name "My custom name"}]))))) (testing "`:name` only" - (is (= {:base_type :type/Integer - :semantic_type :type/Category - :settings nil - :name "sum_2" - :display_name "Sum of Price"} - (mt/$ids venues - (col-info-for-aggregation-clause [:aggregation-options [:sum $price] {:name "sum_2"}]))))) + (is (=? {:base_type :type/Integer + :settings nil + :name "sum_2" + :display_name "Sum of Price"} + (mt/$ids venues + (col-info-for-aggregation-clause [:aggregation-options [:sum $price] {:name "sum_2"}]))))) (testing "`:display-name` only" - (is (= {:base_type :type/Integer - :semantic_type :type/Category - :settings nil - :name "sum" - :display_name "My Custom Name"} - (mt/$ids venues - (col-info-for-aggregation-clause - [:aggregation-options [:sum $price] {:display-name "My Custom Name"}])))))) + (is (=? {:base_type :type/Integer + :settings nil + :name "sum" + :display_name "My Custom Name"} + (mt/$ids venues + (col-info-for-aggregation-clause + [:aggregation-options [:sum $price] {:display-name "My Custom Name"}])))))) (testing (str "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") - (is (= {:cols [{:name "metric" - :display_name "Total Events" - :base_type :type/Text - :effective_type :type/Text - :source :aggregation - :field_ref [:aggregation 0]}]} - (add-column-info - (mt/mbql-query venues {:aggregation [[:metric "ga:totalEvents"]]}) - {:cols [{:name "totalEvents", :display_name "Total Events", :base_type :type/Text}]})))) + (is (=? {:cols [{:name "metric" + :display_name "Total Events" + :base_type :type/Text + :effective_type :type/Text + :source :aggregation + :field_ref [:aggregation 0]}]} + (add-column-info + (mt/mbql-query venues {:aggregation [[:metric "ga:totalEvents"]]}) + {:cols [{:name "totalEvents", :display_name "Total Events", :base_type :type/Text}]})))) (testing "col info for an `expression` aggregation w/ a named expression should work as expected" - (is (= {:base_type :type/Float, :name "sum", :display_name "Sum of double-price"} - (mt/$ids venues - (col-info-for-aggregation-clause {:expressions {"double-price" [:* $price 2]}} [:sum [:expression "double-price"]]))))))) + (is (=? {:base_type :type/Integer, :name "sum", :display_name "Sum of double-price"} + (mt/$ids venues + (col-info-for-aggregation-clause + {:source-table (mt/id :venues) + :expressions {"double-price" [:* $price 2]}} + [:sum [:expression "double-price"]]))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -550,71 +575,71 @@ (deftest ^:parallel test-string-extracts (is (= {:base_type :type/Text} - (infered-col-type [:trim "foo"]))) + (infered-col-type [:trim "foo"]))) (is (= {:base_type :type/Text} - (infered-col-type [:ltrim "foo"]))) + (infered-col-type [:ltrim "foo"]))) (is (= {:base_type :type/Text} - (infered-col-type [:rtrim "foo"]))) + (infered-col-type [:rtrim "foo"]))) (is (= {:base_type :type/BigInteger} - (infered-col-type [:length "foo"]))) + (infered-col-type [:length "foo"]))) (is (= {:base_type :type/Text} - (infered-col-type [:upper "foo"]))) + (infered-col-type [:upper "foo"]))) (is (= {:base_type :type/Text} - (infered-col-type [:lower "foo"]))) + (infered-col-type [:lower "foo"]))) (is (= {:base_type :type/Text} - (infered-col-type [:substring "foo" 2]))) + (infered-col-type [:substring "foo" 2]))) (is (= {:base_type :type/Text} - (infered-col-type [:replace "foo" "f" "b"]))) + (infered-col-type [:replace "foo" "f" "b"]))) (is (= {:base_type :type/Text} - (infered-col-type [:regex-match-first "foo" "f"]))) + (infered-col-type [:regex-match-first "foo" "f"]))) (is (= {:base_type :type/Text} - (infered-col-type [:concat "foo" "bar"]))) + (infered-col-type [:concat "foo" "bar"]))) (is (= {:base_type :type/Text} - (infered-col-type [:coalesce "foo" "bar"]))) + (infered-col-type [:coalesce "foo" "bar"]))) (is (= {:base_type :type/Text :semantic_type :type/Name} - (infered-col-type [:coalesce [:field (mt/id :venues :name) nil] "bar"])))) + (infered-col-type [:coalesce [:field (mt/id :venues :name) nil] "bar"])))) (deftest ^:parallel unique-name-key-test (testing "Make sure `:cols` always come back with a unique `:name` key (#8759)" - (is (= {:cols - [{:base_type :type/Number - :effective_type :type/Number - :semantic_type :type/Quantity - :name "count" - :display_name "count" - :source :aggregation - :field_ref [:aggregation 0]} - {:source :aggregation - :name "sum" - :display_name "sum" - :base_type :type/Number - :effective_type :type/Number - :field_ref [:aggregation 1]} - {:base_type :type/Number - :effective_type :type/Number - :semantic_type :type/Quantity - :name "count_2" - :display_name "count" - :source :aggregation - :field_ref [:aggregation 2]} - {:base_type :type/Number - :effective_type :type/Number - :semantic_type :type/Quantity - :name "count_3" - :display_name "count_2" - :source :aggregation - :field_ref [:aggregation 3]}]} - (add-column-info - (mt/mbql-query venues - {:aggregation [[:count] - [:sum] - [:count] - [:aggregation-options [:count] {:display-name "count_2"}]]}) - {: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}]}))))) + (is (=? {:cols + [{:base_type :type/Number + :effective_type :type/Number + :semantic_type :type/Quantity + :name "count" + :display_name "count" + :source :aggregation + :field_ref [:aggregation 0]} + {:source :aggregation + :name "sum" + :display_name "sum" + :base_type :type/Number + :effective_type :type/Number + :field_ref [:aggregation 1]} + {:base_type :type/Number + :effective_type :type/Number + :semantic_type :type/Quantity + :name "count_2" + :display_name "count" + :source :aggregation + :field_ref [:aggregation 2]} + {:base_type :type/Number + :effective_type :type/Number + :semantic_type :type/Quantity + :name "count_3" + :display_name "count_2" + :source :aggregation + :field_ref [:aggregation 3]}]} + (add-column-info + (mt/mbql-query venues + {:aggregation [[:count] + [:sum] + [:count] + [:aggregation-options [:count] {:display-name "count_2"}]]}) + {: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}]}))))) (deftest ^:parallel expressions-keys-test (testing "make sure expressions come back with the right set of keys, including `:expression_name` (#8854)" @@ -636,20 +661,20 @@ (deftest ^:parallel deduplicate-expression-names-test (testing "make sure multiple expressions come back with deduplicated names" (testing "expressions in aggregations" - (is (= [{:base_type :type/Float, :name "expression", :display_name "0.9 * Average of Price", :source :aggregation, :field_ref [:aggregation 0]} - {:base_type :type/Float, :name "expression_2", :display_name "0.8 * Average of Price", :source :aggregation, :field_ref [:aggregation 1]}] - (:cols (add-column-info - (mt/mbql-query venues - {:aggregation [[:* 0.9 [:avg $price]] [:* 0.8 [:avg $price]]] - :limit 10}) - {}))))) + (is (=? [{:base_type :type/Float, :name "expression", :display_name "0.9 × Average of Price", :source :aggregation, :field_ref [:aggregation 0]} + {:base_type :type/Float, :name "expression_2", :display_name "0.8 × Average of Price", :source :aggregation, :field_ref [:aggregation 1]}] + (:cols (add-column-info + (mt/mbql-query venues + {:aggregation [[:* 0.9 [:avg $price]] [:* 0.8 [:avg $price]]] + :limit 10}) + {}))))) (testing "named :expressions" - (is (= [{:name "prev_month", :display_name "prev_month", :base_type :type/DateTime, :expression_name "prev_month", :source :fields, :field_ref [:expression "prev_month"]}] - (:cols (add-column-info - (mt/mbql-query users - {:expressions {:prev_month [:+ $last_login [:interval -1 :month]]} - :fields [[:expression "prev_month"]], :limit 10}) - {}))))))) + (is (=? [{:name "prev_month", :display_name "prev_month", :base_type :type/DateTime, :expression_name "prev_month", :source :fields, :field_ref [:expression "prev_month"]}] + (:cols (add-column-info + (mt/mbql-query users + {:expressions {:prev_month [:+ $last_login [:interval -1 :month]]} + :fields [[:expression "prev_month"]], :limit 10}) + {}))))))) (deftest mbql-cols-nested-queries-test (testing "Should be able to infer MBQL columns with nested queries" @@ -677,39 +702,38 @@ (testing "Aggregated question with source is an aggregated models should infer display_name correctly (#23248)" (mt/dataset sample-dataset - (mt/with-temp* [Card [{card-id :id} - {:dataset true - :dataset_query - (mt/$ids :products - {:type :query - :database (mt/id) - :query {:source-table $$products - :aggregation - [[:aggregation-options - [:sum $price] - {:name "sum"}] - [:aggregation-options - [:max $rating] - {:name "max"}]] - :breakout $category - :order-by [[:asc $category]]}})}]] - (let [query (qp/preprocess + (t2.with-temp/with-temp [Card {card-id :id} {:dataset true + :dataset_query + (mt/$ids :products + {:type :query + :database (mt/id) + :query {:source-table $$products + :aggregation + [[:aggregation-options + [:sum $price] + {:name "sum"}] + [:aggregation-options + [:max $rating] + {:name "max"}]] + :breakout $category + :order-by [[:asc $category]]}})}] + (let [query (qp/preprocess (mt/mbql-query nil - {:source-table (str "card__" card-id) - :aggregation [[:aggregation-options - [:sum - [:field - "sum" - {:base-type :type/Float}]] - {:name "sum"}] - [:aggregation-options - [:count] - {:name "count"}]] - :limit 1}))] - (is (= ["Sum of Sum of Price" "Count"] - (->> (add-column-info query {}) - :cols - (map :display_name))))))))) + {:source-table (str "card__" card-id) + :aggregation [[:aggregation-options + [:sum + [:field + "sum" + {:base-type :type/Float}]] + {:name "sum"}] + [:aggregation-options + [:count] + {:name "count"}]] + :limit 1}))] + (is (= ["Sum of Sum of Price" "Count"] + (->> (add-column-info query {}) + :cols + (map :display_name))))))))) (deftest ^:parallel inception-test (testing "Should return correct metadata for an 'inception-style' nesting of source > source > source with a join (#14745)" @@ -741,42 +765,43 @@ :field_ref &Products.ean}) (ean-metadata (add-column-info nested-query {})))))))))))))) -;; metabase#14787 (deftest col-info-for-fields-from-card-test - (mt/dataset sample-dataset - (let [card-1-query (mt/mbql-query orders - {:joins [{:fields :all - :source-table $$products - :condition [:= $product_id &Products.products.id] - :alias "Products"}]})] - (mt/with-temp* [Card [{card-1-id :id} {:dataset_query card-1-query}] - Card [{card-2-id :id} {:dataset_query (mt/mbql-query people)}]] - (testing "when a nested query is from a saved question, there should be no `:join-alias` on the left side" - (mt/$ids nil - (let [base-query (qp/preprocess - (mt/mbql-query nil - {:source-table (str "card__" card-1-id) - :joins [{:fields :all - :source-table (str "card__" card-2-id) - :condition [:= $orders.user_id &Products.products.id] - :alias "Q"}] - :limit 1})) - fields #{%orders.discount %products.title %people.source}] - (is (= [{:display_name "Discount" :field_ref [:field %orders.discount nil]} - {:display_name "Products → Title" :field_ref [:field %products.title nil]} - {:display_name "Q → Source" :field_ref [:field %people.source {:join-alias "Q"}]}] - (->> (:cols (add-column-info base-query {})) - (filter #(fields (:id %))) - (map #(select-keys % [:display_name :field_ref]))))))))))) - - (testing "Has the correct display names for joined fields from cards" + (testing "#14787" + (mt/dataset sample-dataset + (let [card-1-query (mt/mbql-query orders + {:joins [{:fields :all + :source-table $$products + :condition [:= $product_id &Products.products.id] + :alias "Products"}]})] + (t2.with-temp/with-temp [Card {card-1-id :id} {:dataset_query card-1-query} + Card {card-2-id :id} {:dataset_query (mt/mbql-query people)}] + (testing "when a nested query is from a saved question, there should be no `:join-alias` on the left side" + (mt/$ids nil + (let [base-query (qp/preprocess + (mt/mbql-query nil + {:source-table (str "card__" card-1-id) + :joins [{:fields :all + :source-table (str "card__" card-2-id) + :condition [:= $orders.user_id &Products.products.id] + :alias "Q"}] + :limit 1})) + fields #{%orders.discount %products.title %people.source}] + (is (= [{:display_name "Discount" :field_ref [:field %orders.discount nil]} + {:display_name "Products → Title" :field_ref [:field %products.title nil]} + {:display_name "Q → Source" :field_ref [:field %people.source {:join-alias "Q"}]}] + (->> (:cols (add-column-info base-query {})) + (filter #(fields (:id %))) + (map #(select-keys % [:display_name :field_ref]))))))))))))) + +(deftest col-info-for-joined-fields-from-card-test + (testing "Has the correct display names for joined fields from cards (#14787)" (letfn [(native [query] {:type :native :native {:query query :template-tags {}} :database (mt/id)})] - (mt/with-temp* [Card [{card1-id :id} {:dataset_query - (native "select 'foo' as A_COLUMN")}] - Card [{card2-id :id} {:dataset_query - (native "select 'foo' as B_COLUMN")}]] + (t2.with-temp/with-temp [Card {card1-id :id} {:dataset_query + (native "select 'foo' as A_COLUMN")} + Card {card2-id :id} {:dataset_query + (native "select 'foo' as B_COLUMN")}] (doseq [card-id [card1-id card2-id]] ;; populate metadata (mt/user-http-request :rasta :post 202 (format "card/%d/query" card-id))) 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 e3b3b552a13..fef9d4daa15 100644 --- a/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj +++ b/test/metabase/query_processor/middleware/pre_alias_aggregations_test.clj @@ -7,13 +7,15 @@ [metabase.driver :as driver] [metabase.query-processor.middleware.pre-alias-aggregations :as qp.pre-alias-aggregations] + [metabase.query-processor.store :as qp.store] [metabase.test :as mt])) (defn- pre-alias [query] - (driver/with-driver (or driver/*driver* :h2) - (qp.pre-alias-aggregations/pre-alias-aggregations query))) + (mt/with-everything-store + (driver/with-driver (or driver/*driver* :h2) + (qp.pre-alias-aggregations/pre-alias-aggregations query)))) -(deftest pre-alias-aggregations-test +(deftest ^:parallel pre-alias-aggregations-test (is (= (mt/mbql-query checkins {:source-table $$checkins :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] @@ -23,7 +25,7 @@ {:source-table $$checkins :aggregation [[:sum $user_id] [:sum $venue_id]]}))))) -(deftest named-aggregations-test +(deftest ^:parallel named-aggregations-test (testing "if one or more aggregations are already named, do things still work correctly?" (is (= (mt/mbql-query checkins {:source-table $$checkins @@ -35,20 +37,20 @@ :aggregation [[:sum $user_id] [:aggregation-options [:sum $venue_id] {:name "sum"}]]})))))) -(deftest source-queries-test - (testing "do aggregations inside source queries get pre-aliased?") - (is (= (mt/mbql-query checkins - {:source-query {:source-table $$checkins - :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] - [:aggregation-options [:sum $venue_id] {:name "sum_2"}]]} - :aggregation [[:aggregation-options [:count] {:name "count"}]]}) - (pre-alias - (mt/mbql-query checkins - {:source-query {:source-table $$checkins - :aggregation [[:sum $user_id] [:sum $venue_id]]} - :aggregation [[:count]]}))))) +(deftest ^:parallel source-queries-test + (testing "do aggregations inside source queries get pre-aliased?" + (is (= (mt/mbql-query checkins + {:source-query {:source-table $$checkins + :aggregation [[:aggregation-options [:sum $user_id] {:name "sum"}] + [:aggregation-options [:sum $venue_id] {:name "sum_2"}]]} + :aggregation [[:aggregation-options [:count] {:name "count"}]]}) + (pre-alias + (mt/mbql-query checkins + {:source-query {:source-table $$checkins + :aggregation [[:sum $user_id] [:sum $venue_id]]} + :aggregation [[:count]]})))))) -(deftest source-queries-inside-joins-test +(deftest ^:parallel source-queries-inside-joins-test (testing "do aggregatons inside of source queries inside joins get pre-aliased?" (is (= (mt/mbql-query checkins {:source-table $$venues @@ -69,7 +71,7 @@ :alias "checkins" :condition [:= &checkins.venue_id $venues.id]}]})))))) -(deftest expressions-test +(deftest ^:parallel expressions-test (testing "does pre-aliasing work the way we'd expect with expressions?" (is (= {:database 1 :type :query @@ -79,30 +81,32 @@ {:database 1 :type :query :query {:source-table 1 - :aggregation [[:+ 20 [:sum [:field 2 nil]]]]}}))) + :aggregation [[:+ 20 [:sum [:field 2 nil]]]]}}))))) - (is (= {:database 1 - :type :query - :query {:source-table 1 - :aggregation [[:aggregation-options - [:+ 20 [:sum [:field 2 nil]]] - {:name "expression"}] - [:aggregation-options - [:- 20 [:sum [:field 2 nil]]] - {:name "expression_2"}]]}} - (pre-alias - {:database 1 - :type :query - :query {:source-table 1 - :aggregation [[:+ 20 [:sum [:field 2 nil]]] - [:- 20 [:sum [:field 2 nil]]]]}}))))) +(deftest ^:parallel expressions-test-2 + (is (= {:database 1 + :type :query + :query {:source-table 1 + :aggregation [[:aggregation-options + [:+ 20 [:sum [:field 2 nil]]] + {:name "expression"}] + [:aggregation-options + [:- 20 [:sum [:field 2 nil]]] + {:name "expression_2"}]]}} + (pre-alias + {:database 1 + :type :query + :query {:source-table 1 + :aggregation [[:+ 20 [:sum [:field 2 nil]]] + [:- 20 [:sum [:field 2 nil]]]]}})))) -(driver/register! ::test-driver, :abstract? true, :parent :sql) +(driver/register! ::test-driver, :parent :sql) -(defmethod driver/escape-alias ::test-driver [_ custom-field-name] +(defmethod driver/escape-alias ::test-driver + [_driver custom-field-name] (str \_ custom-field-name)) -(deftest use-escape-alias-test +(deftest ^:parallel use-escape-alias-test (testing (str "we should use [[driver/escape-alias]] on the generated aggregation names in case the " "drivers need to tweak the default names we generate.")) (is (= {:database 1 @@ -110,10 +114,13 @@ :query {:source-table 1 :aggregation [[:aggregation-options [:+ 20 [:sum [:field 2 nil]]] {:name "_expression"}] [:aggregation-options [:count] {:name "_count"}]]}} - (driver/with-driver ::test-driver - (pre-alias - {:database 1 - :type :query - :query {:source-table 1 - :aggregation [[:+ 20 [:sum [:field 2 nil]]] - [:count]]}}))))) + (let [db (mt/db)] + (driver/with-driver ::test-driver + (qp.store/with-store + (qp.store/store-database! db) + (qp.pre-alias-aggregations/pre-alias-aggregations + {:database 1 + :type :query + :query {:source-table 1 + :aggregation [[:+ 20 [:sum [:field 2 nil]]] + [:count]]}}))))))) diff --git a/test/metabase/query_processor/pivot_test.clj b/test/metabase/query_processor/pivot_test.clj index 22fc61bdb45..993d704e907 100644 --- a/test/metabase/query_processor/pivot_test.clj +++ b/test/metabase/query_processor/pivot_test.clj @@ -17,7 +17,7 @@ (set! *warn-on-reflection* true) -(deftest group-bitmask-test +(deftest ^:parallel group-bitmask-test (doseq [[indices expected] {[0] 6 [0 1] 4 [0 1 2] 0 @@ -25,7 +25,7 @@ (is (= expected (#'qp.pivot/group-bitmask 3 indices))))) -(deftest powerset-test +(deftest ^:parallel powerset-test (is (= [[]] (#'qp.pivot/powerset []))) (is (= [[0] []] @@ -35,7 +35,7 @@ (is (= [[0 1 2] [1 2] [0 2] [2] [0 1] [1] [0] []] (#'qp.pivot/powerset [0 1 2])))) -(deftest breakout-combinations-test +(deftest ^:parallel breakout-combinations-test (testing "Should return the combos that Paul specified in (#14329)" (is (= [[0 1 2] [0 1] @@ -70,7 +70,7 @@ []] (#'qp.pivot/breakout-combinations 3 [] [])))))) -(deftest validate-pivot-rows-cols-test +(deftest ^:parallel validate-pivot-rows-cols-test (testing "Should throw an Exception if you pass in invalid pivot-rows" (is (thrown-with-msg? clojure.lang.ExceptionInfo diff --git a/test/metabase/query_processor/util/add_alias_info_test.clj b/test/metabase/query_processor/util/add_alias_info_test.clj index 28238422004..741bd3c50c2 100644 --- a/test/metabase/query_processor/util/add_alias_info_test.clj +++ b/test/metabase/query_processor/util/add_alias_info_test.clj @@ -39,7 +39,7 @@ (driver/with-driver (or driver/*driver* :h2) (-> query qp/preprocess add/add-alias-info remove-source-metadata (dissoc :middleware))))) -(deftest join-in-source-query-test +(deftest ^:parallel join-in-source-query-test (is (query= (mt/mbql-query venues {:source-query {:source-table $$venues :joins [{:strategy :left-join @@ -83,7 +83,7 @@ :breakout [&Cat.categories.name] :limit 1}))))) -(deftest multiple-joins-test +(deftest ^:parallel multiple-joins-test (mt/dataset sample-dataset (is (query= (mt/mbql-query orders {:source-query {:source-table $$orders @@ -159,7 +159,7 @@ :alias "P2"}]}}] :limit 1})))))) -(deftest uniquify-aliases-test +(deftest ^:parallel uniquify-aliases-test (mt/dataset sample-dataset (is (query= (mt/mbql-query products {:source-table $$products @@ -184,7 +184,7 @@ [:expression "CATEGORY"]] :limit 1})))))) -(deftest not-null-test +(deftest ^:parallel not-null-test (is (query= (mt/mbql-query checkins {:aggregation [[:aggregation-options [:count] @@ -208,7 +208,7 @@ {:aggregation [[:count]] :filter [:not-null $date]}))))) -(deftest duplicate-aggregations-test +(deftest ^:parallel duplicate-aggregations-test (is (query= (mt/mbql-query venues {:source-query {:source-table $$venues :aggregation [[:aggregation-options @@ -229,17 +229,17 @@ ::add/source-alias "count_3" ::add/desired-alias "count_3" ::add/position 2}]]} - :fields [[:field "count" {:base-type :type/BigInteger + :fields [[:field "count" {:base-type :type/Integer ::add/source-table ::add/source ::add/source-alias "count" ::add/desired-alias "count" ::add/position 0}] - [:field "count_2" {:base-type :type/BigInteger + [:field "count_2" {:base-type :type/Integer ::add/source-table ::add/source ::add/source-alias "count_2" ::add/desired-alias "count_2" ::add/position 1}] - [:field "count_3" {:base-type :type/BigInteger + [:field "count_3" {:base-type :type/Integer ::add/source-table ::add/source ::add/source-alias "count_3" ::add/desired-alias "count_3" @@ -257,7 +257,7 @@ [:count]]} :limit 1}))))) -(deftest multiple-expressions-test +(deftest ^:parallel multiple-expressions-test (mt/with-everything-store (is (query= (mt/$ids venues {$price 0 @@ -384,7 +384,7 @@ [_driver field-alias] (prefix-alias field-alias)) -(deftest custom-escape-alias-test +(deftest ^:parallel custom-escape-alias-test (let [db (mt/db)] (driver/with-driver ::custom-escape (mt/with-db db @@ -424,7 +424,7 @@ add-alias-info :query))))))) -(deftest custom-escape-alias-filtering-aggregation-test +(deftest ^:parallel custom-escape-alias-filtering-aggregation-test (let [db (mt/db)] (driver/with-driver ::custom-escape (mt/with-db db @@ -443,7 +443,7 @@ ::add/position 1} outer-count-opts (-> count-opts (dissoc :name) - (assoc :base-type :type/BigInteger + (assoc :base-type :type/Integer ::add/source-table ::add/source) (update ::add/source-alias prefix-alias) (update ::add/desired-alias prefix-alias))] @@ -460,7 +460,7 @@ [:field "strange count" outer-count-opts] - [:value 10 {:base_type :type/BigInteger}]] + [:value 10 {:base_type :type/Integer}]] :limit 1})) (-> (mt/mbql-query venues {:source-query {:source-table $$venues @@ -468,7 +468,7 @@ [:count] {:name "strange count"}]] :breakout [$price]} - :filter [:< [:field "strange count" {:base-type :type/BigInteger}] 10] + :filter [:< [:field "strange count" {:base-type :type/Integer}] 10] :limit 1}) add-alias-info :query))))))) @@ -480,7 +480,7 @@ (-> ((get-method driver/escape-alias :h2) driver field-alias) (str/replace #"\s" "_"))) -(deftest use-correct-alias-for-joined-field-test +(deftest ^:parallel use-correct-alias-for-joined-field-test (testing "Make sure we call `driver/escape-alias` for the `:source-alias` for Fields coming from joins (#20413)" (mt/dataset sample-dataset (let [db (mt/db)] @@ -570,7 +570,7 @@ add-alias-info :query))))))))) -(deftest query->expected-cols-test +(deftest ^:parallel query->expected-cols-test (testing "field_refs in expected columns have the original join aliases (#30648)" (mt/dataset sample-dataset (binding [driver/*driver* ::custom-escape-spaces-to-underscores] @@ -646,7 +646,7 @@ add-alias-info :query))))))) -(deftest aggregation-reference-test +(deftest ^:parallel aggregation-reference-test (testing "Make sure we add info to `:aggregation` reference clauses correctly" (is (query= (mt/mbql-query checkins {:aggregation [[:aggregation-options @@ -663,7 +663,7 @@ {:aggregation [[:sum $user_id]] :order-by [[:asc [:aggregation 0]]]})))))) -(deftest uniquify-aggregation-names-text +(deftest ^:parallel uniquify-aggregation-names-text (is (query= (mt/mbql-query checkins {:expressions {"count" [:+ 1 1]} :breakout [[:expression "count" {::add/desired-alias "count" @@ -682,7 +682,7 @@ :aggregation [[:count]] :limit 1}))))) -(deftest fuzzy-field-info-test +(deftest ^:parallel fuzzy-field-info-test (testing "[[add/alias-from-join]] should match Fields in the Join source query even if they have temporal units" (mt/with-driver :h2 (mt/dataset sample-dataset diff --git a/test/metabase/query_processor/util/nest_query_test.clj b/test/metabase/query_processor/util/nest_query_test.clj index 676e29b0c16..d27ec5eefa1 100644 --- a/test/metabase/query_processor/util/nest_query_test.clj +++ b/test/metabase/query_processor/util/nest_query_test.clj @@ -27,7 +27,7 @@ nest-query/nest-expressions remove-source-metadata)))) -(deftest nest-expressions-test +(deftest ^:parallel nest-expressions-test (driver/with-driver :h2 (mt/with-everything-store (is (partial= (mt/$ids venues @@ -68,7 +68,7 @@ add/add-alias-info nest-expressions)))))) -(deftest nest-expressions-with-existing-non-expression-fields-test +(deftest ^:parallel nest-expressions-with-existing-non-expression-fields-test (driver/with-driver :h2 (mt/with-everything-store (testing "Other `:fields` besides the `:expressions` should be preserved in the top level" @@ -119,7 +119,7 @@ add/add-alias-info nest-expressions))))))) -(deftest multiple-expressions-test +(deftest ^:parallel multiple-expressions-test (testing "Make sure the nested version of the query doesn't mix up expressions if we have ones that reference others" (driver/with-driver :h2 (mt/with-everything-store @@ -170,7 +170,7 @@ add/add-alias-info nest-expressions))))))) -(deftest nest-expressions-ignore-source-queries-test +(deftest ^:parallel nest-expressions-ignore-source-queries-test (testing (str "When 'raising' :expression clauses, only raise ones in the current level. Handle duplicate expression " "names correctly.") (driver/with-driver :h2 @@ -235,7 +235,9 @@ :desired-alias "PRICE"}] [:expression "x" #::add{:desired-alias "x"}]]}}} :limit 1}) - (-> query add/add-alias-info nest-expressions)))))))) + (-> query add/add-alias-info nest-expressions))))))))) + +(deftest nest-expressions-ignore-source-queries-from-joins-test (testing "Ignores source-query from joins (#20809)" (let [query {:source-table 2, :expressions {"CC" [:+ 1 1]}, @@ -305,7 +307,7 @@ :expressions {"CC" [:+ 1 1]} :limit 2}))))))))) -(deftest nest-expressions-with-joins-test +(deftest ^:parallel nest-expressions-with-joins-test (driver/with-driver :h2 (mt/with-everything-store (testing "If there are any `:joins`, those need to be nested into the `:source-query` as well." @@ -364,7 +366,7 @@ ::add/source-alias "MaxPrice" ::add/desired-alias "CategoriesStats__MaxPrice" ::add/position 8}] - [:field "AvgPrice" {:base-type :type/Integer + [:field "AvgPrice" {:base-type :type/Float :join-alias "CategoriesStats" ::add/source-table "CategoriesStats" ::add/source-alias "AvgPrice" @@ -424,7 +426,7 @@ ::add/source-alias "MaxPrice" ::add/desired-alias "CategoriesStats__MaxPrice" ::add/position 8}] - [:field "AvgPrice" {:base-type :type/Integer + [:field "AvgPrice" {:base-type :type/Float :join-alias "CategoriesStats" ::add/source-table "CategoriesStats" ::add/source-alias "AvgPrice" @@ -552,7 +554,7 @@ add/add-alias-info nest-expressions)))))))) -(deftest multiple-joins-with-expressions-test +(deftest ^:parallel multiple-joins-with-expressions-test (testing "We should be able to compile a complicated query with multiple joins and expressions correctly" (driver/with-driver :h2 (mt/dataset sample-dataset @@ -632,7 +634,7 @@ add/add-alias-info nest-expressions)))))))) -(deftest uniquify-aliases-test +(deftest ^:parallel uniquify-aliases-test (driver/with-driver :h2 (mt/dataset sample-dataset (mt/with-everything-store diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj index 7aceba838a7..9dfe3186403 100644 --- a/test/metabase/query_processor_test/aggregation_test.clj +++ b/test/metabase/query_processor_test/aggregation_test.clj @@ -8,7 +8,7 @@ [metabase.test.data :as data] [metabase.test.util :as tu])) -(deftest no-aggregation-test +(deftest ^:parallel no-aggregation-test (mt/test-drivers (mt/normal-drivers) (testing "Test that no aggregation just returns rows as-is." (is (= [[1 "Red Medicine" 4 10.0646 -165.374 3] @@ -25,33 +25,39 @@ (mt/run-mbql-query venues {:limit 10, :order-by [[:asc $id]]}))))))) -(deftest basic-aggregations-test +(deftest ^:parallel count-test (mt/test-drivers (mt/normal-drivers) (testing "count aggregation" (is (= [[100]] (mt/formatted-rows [int] (mt/run-mbql-query venues - {:aggregation [[:count]]}))))) + {:aggregation [[:count]]}))))))) +(deftest ^:parallel sum-test + (mt/test-drivers (mt/normal-drivers) (testing "sum aggregation" (is (= [[203]] (mt/formatted-rows [int] (mt/run-mbql-query venues - {:aggregation [[:sum $price]]}))))) + {:aggregation [[:sum $price]]}))))))) +(deftest ^:parallel avg-test + (mt/test-drivers (mt/normal-drivers) (testing "avg aggregation" (is (= [[35.5059]] (mt/formatted-rows [4.0] (mt/run-mbql-query venues - {:aggregation [[:avg $latitude]]}))))) + {:aggregation [[:avg $latitude]]}))))))) +(deftest ^:parallel distinct-count-test + (mt/test-drivers (mt/normal-drivers) (testing "distinct count aggregation" (is (= [[15]] (mt/formatted-rows [int] (mt/run-mbql-query checkins {:aggregation [[:distinct $user_id]]}))))))) -(deftest standard-deviation-test +(deftest ^:parallel standard-deviation-test (mt/test-drivers (mt/normal-drivers-with-feature :standard-deviation-aggregations) (testing "standard deviation aggregations" (let [query (mt/mbql-query venues {:aggregation [[:stddev $latitude]]})] @@ -60,8 +66,9 @@ :rows [[3.4]]} (qp.test/rows-and-cols (mt/format-rows-by [1.0] - (mt/process-query query))))))))) + (mt/process-query query)))))))))) +(deftest ^:parallel standard-deviation-unsupported-test (mt/test-drivers (mt/normal-drivers-without-feature :standard-deviation-aggregations) (testing "Make sure standard deviations fail for drivers that don't support it" (is (thrown-with-msg? @@ -77,28 +84,32 @@ ;;; | MIN & MAX | ;;; +----------------------------------------------------------------------------------------------------------------+ -(deftest min-test +(deftest ^:parallel min-test (mt/test-drivers (mt/normal-drivers) (is (= [1] (mt/first-row (mt/format-rows-by [int] (mt/run-mbql-query venues - {:aggregation [[:min $price]]}))))) + {:aggregation [[:min $price]]}))))))) +(deftest ^:parallel min-test-2 + (mt/test-drivers (mt/normal-drivers) (is (= [[1 34.0071] [2 33.7701] [3 10.0646] [4 33.983]] (mt/formatted-rows [int 4.0] (mt/run-mbql-query venues {:aggregation [[:min $latitude]] :breakout [$price]})))))) -(deftest max-test +(deftest ^:parallel max-test (mt/test-drivers (mt/normal-drivers) (is (= [4] (mt/first-row (mt/format-rows-by [int] (mt/run-mbql-query venues - {:aggregation [[:max $price]]}))))) + {:aggregation [[:max $price]]}))))))) +(deftest ^:parallel max-test-2 + (mt/test-drivers (mt/normal-drivers) (is (= [[1 37.8078] [2 40.7794] [3 40.7262] [4 40.7677]] (mt/formatted-rows [int 4.0] (mt/run-mbql-query venues @@ -110,45 +121,44 @@ ;;; | MULTIPLE AGGREGATIONS | ;;; +----------------------------------------------------------------------------------------------------------------+ -(deftest multiple-aggregations-test +(deftest ^:parallel multiple-aggregations-test (mt/test-drivers (mt/normal-drivers) (testing "two aggregations" (is (= [[100 203]] (mt/formatted-rows [int int] (mt/run-mbql-query venues - {:aggregation [[:count] [:sum $price]]}))))) + {:aggregation [[:count] [:sum $price]]}))))))) +(deftest ^:parallel multiple-aggregations-test-2 + (mt/test-drivers (mt/normal-drivers) (testing "three aggregations" (is (= [[2 100 203]] (mt/formatted-rows [int int int] (mt/run-mbql-query venues {:aggregation [[:avg $price] [:count] [:sum $price]]}))))))) -(deftest multiple-aggregations-metadata-test - ;; TODO - this isn't tested against Mongo because those driver doesn't currently work correctly with multiple - ;; columns with the same name. It seems like it would be pretty easy to take the stuff we have for BigQuery and - ;; generalize it so we can use it with Mongo - ;; - ;; TODO part 2 -- not sure if this is still the case? - (mt/test-drivers (disj (mt/normal-drivers) :mongo) +(deftest ^:parallel multiple-aggregations-metadata-test + (mt/test-drivers (mt/normal-drivers) (testing "make sure that multiple aggregations of the same type have the correct metadata (#4003)" (is (= [(qp.test/aggregate-col :count) (assoc (qp.test/aggregate-col :count) :name "count_2", :field_ref [:aggregation 1])] (mt/cols - (mt/run-mbql-query venues - {:aggregation [[:count] [:count]]}))))))) + (mt/run-mbql-query venues + {:aggregation [[:count] [:count]]}))))))) ;;; ------------------------------------------------- CUMULATIVE SUM ------------------------------------------------- -(deftest cumulate-sum-test +(deftest ^:parallel cumulative-sum-test (mt/test-drivers (mt/normal-drivers) (testing "cum_sum w/o breakout should be treated the same as sum" (is (= [[120]] (mt/formatted-rows [int] (mt/run-mbql-query users - {:aggregation [[:cum-sum $id]]}))))) + {:aggregation [[:cum-sum $id]]}))))))) +(deftest ^:parallel cumulative-sum-test-2 + (mt/test-drivers (mt/normal-drivers) (testing " Simple cumulative sum where breakout field is same as cum_sum field" (is (= [[ 1 1] [ 2 3] @@ -168,8 +178,10 @@ (mt/formatted-rows [int int] (mt/run-mbql-query users {:aggregation [[:cum-sum $id]] - :breakout [$id]}))))) + :breakout [$id]}))))))) +(deftest ^:parallel cumulative-sum-test-3 + (mt/test-drivers (mt/normal-drivers) (testing " Cumulative sum w/ a different breakout field" (is (= [["Broen Olujimi" 14] ["Conchúr Tihomir" 21] @@ -189,8 +201,10 @@ (mt/formatted-rows [str int] (mt/run-mbql-query users {:aggregation [[:cum-sum $id]] - :breakout [$name]}))))) + :breakout [$name]}))))))) +(deftest ^:parallel cumulative-sum-test-4 + (mt/test-drivers (mt/normal-drivers) (testing " Cumulative sum w/ a different breakout field that requires grouping" (is (= [[1 1211] [2 4066] @@ -203,7 +217,7 @@ ;;; ------------------------------------------------ CUMULATIVE COUNT ------------------------------------------------ -(deftest cumulative-count-test +(deftest ^:parallel cumulative-count-test (mt/test-drivers (mt/normal-drivers) (testing "cumulative count aggregations" (testing "w/o breakout should be treated the same as count" @@ -212,44 +226,48 @@ (qp.test/rows-and-cols (mt/format-rows-by [int] (mt/run-mbql-query users - {:aggregation [[:cum-count $id]]})))))) - - (testing "w/ breakout on field with distinct values" - (is (= {:rows [["Broen Olujimi" 1] - ["Conchúr Tihomir" 2] - ["Dwight Gresham" 3] - ["Felipinho Asklepios" 4] - ["Frans Hevel" 5] - ["Kaneonuskatew Eiran" 6] - ["Kfir Caj" 7] - ["Nils Gotam" 8] - ["Plato Yeshua" 9] - ["Quentin Sören" 10] - ["Rüstem Hebel" 11] - ["Shad Ferdynand" 12] - ["Simcha Yan" 13] - ["Spiros Teofil" 14] - ["Szymon Theutrich" 15]] - :cols [(qp.test/breakout-col :users :name) - (qp.test/aggregate-col :cum-count :users :id)]} - (qp.test/rows-and-cols - (mt/format-rows-by [str int] - (mt/run-mbql-query users - {:aggregation [[:cum-count $id]] - :breakout [$name]})))))) - - (testing "w/ breakout on field that requires grouping" - (is (= {:cols [(qp.test/breakout-col :venues :price) - (qp.test/aggregate-col :cum-count :venues :id)] - :rows [[1 22] - [2 81] - [3 94] - [4 100]]} - (qp.test/rows-and-cols - (mt/format-rows-by [int int] - (mt/run-mbql-query venues - {:aggregation [[:cum-count $id]] - :breakout [$price]}))))))))) + {:aggregation [[:cum-count $id]]}))))))))) + +(deftest ^:parallel cumulative-count-with-breakout-test + (mt/test-drivers (mt/normal-drivers) + (testing "w/ breakout on field with distinct values" + (is (= {:rows [["Broen Olujimi" 1] + ["Conchúr Tihomir" 2] + ["Dwight Gresham" 3] + ["Felipinho Asklepios" 4] + ["Frans Hevel" 5] + ["Kaneonuskatew Eiran" 6] + ["Kfir Caj" 7] + ["Nils Gotam" 8] + ["Plato Yeshua" 9] + ["Quentin Sören" 10] + ["Rüstem Hebel" 11] + ["Shad Ferdynand" 12] + ["Simcha Yan" 13] + ["Spiros Teofil" 14] + ["Szymon Theutrich" 15]] + :cols [(qp.test/breakout-col :users :name) + (qp.test/aggregate-col :cum-count :users :id)]} + (qp.test/rows-and-cols + (mt/format-rows-by [str int] + (mt/run-mbql-query users + {:aggregation [[:cum-count $id]] + :breakout [$name]})))))))) + +(deftest ^:parallel cumulative-count-with-breakout-test-2 + (mt/test-drivers (mt/normal-drivers) + (testing "w/ breakout on field that requires grouping" + (is (= {:cols [(qp.test/breakout-col :venues :price) + (qp.test/aggregate-col :cum-count :venues :id)] + :rows [[1 22] + [2 81] + [3 94] + [4 100]]} + (qp.test/rows-and-cols + (mt/format-rows-by [int int] + (mt/run-mbql-query venues + {:aggregation [[:cum-count $id]] + :breakout [$price]})))))))) (deftest field-settings-for-aggregate-fields-test (testing "Does `:settings` show up for aggregate Fields?" @@ -261,15 +279,15 @@ (or (-> results mt/cols first) results))))))) -(deftest duplicate-aggregations-test +(deftest ^:parallel duplicate-aggregations-test (mt/test-drivers (mt/normal-drivers) (testing "Do we properly handle queries that have more than one of the same aggregation? (#5393)" (is (= [[5050 203]] (mt/formatted-rows [int int] (mt/run-mbql-query venues - {:aggregation [[:sum $id] [:sum $price]]}))))))) + {:aggregation [[:sum $id] [:sum $price]]}))))))) -(deftest multiple-distinct-aggregations-test +(deftest ^:parallel multiple-distinct-aggregations-test (testing "Multiple `:distinct` aggregations should work correctly (#13097)" (mt/test-drivers (mt/normal-drivers) (is (= [[100 4]] diff --git a/test/metabase/query_processor_test/filter_test.clj b/test/metabase/query_processor_test/filter_test.clj index 9eb1a4fbc15..08fcc24b1c1 100644 --- a/test/metabase/query_processor_test/filter_test.clj +++ b/test/metabase/query_processor_test/filter_test.clj @@ -104,9 +104,9 @@ ;; test's results (when (= :snowflake driver/*driver*) (driver/notify-database-updated driver/*driver* (mt/id))) - (is (= {:rows [[29]] - :cols [(qp.test/aggregate-col :count)]} - (qp.test/rows-and-cols + (is (=? {:rows [[29]] + :cols [(qp.test/aggregate-col :count)]} + (qp.test/rows-and-cols (mt/format-rows-by [int] (mt/run-mbql-query checkins {:aggregation [[:count]] diff --git a/test/metabase/query_processor_test/nested_queries_test.clj b/test/metabase/query_processor_test/nested_queries_test.clj index 8a4f9a3520b..faf7b6c109d 100644 --- a/test/metabase/query_processor_test/nested_queries_test.clj +++ b/test/metabase/query_processor_test/nested_queries_test.clj @@ -1,10 +1,12 @@ (ns metabase.query-processor-test.nested-queries-test "Tests for handling queries with nested expressions." (:require + [clojure.string :as str] [clojure.test :refer :all] [honey.sql :as sql] [java-time :as t] [medley.core :as m] + [metabase.db.query :as mdb.query] [metabase.driver :as driver] [metabase.driver.sql.query-processor-test-util :as sql.qp-test-util] [metabase.mbql.schema :as mbql.s] @@ -401,20 +403,30 @@ (deftest aggregatation-references-test (testing "make sure that aggregation references match up to aggregations from the same level they're from" ;; e.g. the ORDER BY in the source-query should refer the 'stddev' aggregation, NOT the 'avg' aggregation - (is (= {:query (str "SELECT AVG(\"source\".\"stddev\") AS \"avg\" FROM (" - "SELECT \"PUBLIC\".\"VENUES\".\"PRICE\" AS \"PRICE\", STDDEV_POP(\"PUBLIC\".\"VENUES\".\"ID\") AS \"stddev\" " - "FROM \"PUBLIC\".\"VENUES\" " - "GROUP BY \"PUBLIC\".\"VENUES\".\"PRICE\" " - "ORDER BY \"stddev\" DESC, \"PUBLIC\".\"VENUES\".\"PRICE\" ASC" - ") AS \"source\"") + (is (= {:query ["SELECT" + " AVG(\"source\".\"stddev\") AS \"avg\"" + "FROM" + " (" + " SELECT" + " \"PUBLIC\".\"VENUES\".\"PRICE\" AS \"PRICE\"," + " STDDEV_POP(\"PUBLIC\".\"VENUES\".\"ID\") AS \"stddev\"" + " FROM" + " \"PUBLIC\".\"VENUES\"" + " GROUP BY" + " \"PUBLIC\".\"VENUES\".\"PRICE\"" + " ORDER BY" + " \"stddev\" DESC," + " \"PUBLIC\".\"VENUES\".\"PRICE\" ASC" + " ) AS \"source\""] :params nil} - (qp/compile - (mt/mbql-query venues - {:source-query {:source-table $$venues - :aggregation [[:stddev $id]] - :breakout [$price] - :order-by [[[:aggregation 0] :descending]]} - :aggregation [[:avg *stddev/Integer]]})))))) + (-> (qp/compile + (mt/mbql-query venues + {:source-query {:source-table $$venues + :aggregation [[:stddev $id]] + :breakout [$price] + :order-by [[[:aggregation 0] :descending]]} + :aggregation [[:avg *stddev/Integer]]})) + (update :query #(str/split-lines (mdb.query/format-sql % :h2)))))))) (deftest handle-incorrect-field-forms-gracefully-test (testing "make sure that we handle [:field [:field <name> ...]] forms gracefully, despite that not making any sense" @@ -549,7 +561,7 @@ ;; include the unit; however `:unit` is still `:year` so the frontend can use the correct formatting to ;; display values of the column. (is (= [(assoc date-col :field_ref [:field (mt/id :checkins :date) {:temporal-unit :default}], :unit :year) - (assoc count-col :field_ref [:field "count" {:base-type (:base_type count-col)}])] + (assoc count-col :field_ref [:field "count" {:base-type :type/Integer}])] (mt/cols (qp/process-query (query-with-source-card card))))))))))) @@ -866,7 +878,7 @@ :base_type :type/Text} {:name "count" :display_name "Count" - :field_ref [:field "count" {:base-type :type/BigInteger}] + :field_ref [:field "count" {:base-type :type/Integer}] :base_type (:base_type (qp.test/aggregate-col :count))}]) (for [col (mt/cols results)] (select-keys col [:name :display_name :id :field_ref :base_type])))))))) diff --git a/test/metabase/server/middleware/session_test.clj b/test/metabase/server/middleware/session_test.clj index a3eceb18b96..d1195ebc0d8 100644 --- a/test/metabase/server/middleware/session_test.clj +++ b/test/metabase/server/middleware/session_test.clj @@ -87,7 +87,7 @@ ;; if request is an HTTPS request then we should set `:secure true`. There are several different headers we check for ;; this. Make sure they all work. -(deftest secure-cookie-test +(deftest ^:parallel secure-cookie-test (doseq [[headers expected] [[{"x-forwarded-proto" "https"} true] [{"x-forwarded-proto" "http"} false] [{"x-forwarded-protocol" "https"} true] @@ -190,20 +190,20 @@ identity (fn [e] (throw e)))) -(deftest no-session-id-in-request-test +(deftest ^:parallel no-session-id-in-request-test (testing "no session-id in the request" (is (= nil (-> (wrapped-handler (ring.mock/request :get "/anyurl")) :metabase-session-id))))) -(deftest header-test +(deftest ^:parallel header-test (testing "extract session-id from header" (is (= "foobar" (:metabase-session-id (wrapped-handler (ring.mock/header (ring.mock/request :get "/anyurl") session-header "foobar"))))))) -(deftest cookie-test +(deftest ^:parallel cookie-test (testing "extract session-id from cookie" (is (= "cookie-session" (:metabase-session-id @@ -211,7 +211,7 @@ (assoc (ring.mock/request :get "/anyurl") :cookies {session-cookie {:value "cookie-session"}}))))))) -(deftest both-header-and-cookie-test +(deftest ^:parallel both-header-and-cookie-test (testing "if both header and cookie session-ids exist, then we expect the cookie to take precedence" (is (= "cookie-session" (:metabase-session-id @@ -219,7 +219,7 @@ (assoc (ring.mock/header (ring.mock/request :get "/anyurl") session-header "foobar") :cookies {session-cookie {:value "cookie-session"}}))))))) -(deftest anti-csrf-headers-test +(deftest ^:parallel anti-csrf-headers-test (testing "`wrap-session-id` should handle anti-csrf headers they way we'd expect" (let [request (-> (ring.mock/request :get "/anyurl") (assoc :cookies {embedded-session-cookie {:value (str test-uuid)}}) @@ -347,7 +347,7 @@ (-> (ring.mock/request :get "/anyurl") (assoc :metabase-user-id user-id))) -(deftest add-user-id-key-test +(deftest ^:parallel add-user-id-key-test (testing "with valid user-id" (is (= {:user-id (mt/user->id :rasta) :user {:id (mt/user->id :rasta) diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj index ae2d7c80daf..cd79fc18fbe 100644 --- a/test/metabase/test/data.clj +++ b/test/metabase/test/data.clj @@ -112,7 +112,7 @@ * Only symbols that end in alphanumeric characters will be parsed, so as to avoid accidentally parsing things that do not refer to Fields." - {:style/indent [:defn 1]} + {:style/indent :defn} ([form] `($ids nil ~form)) diff --git a/test/metabase/test/data/h2.clj b/test/metabase/test/data/h2.clj index 34bc053b3b7..2f66cb801d5 100644 --- a/test/metabase/test/data/h2.clj +++ b/test/metabase/test/data/h2.clj @@ -4,6 +4,7 @@ [metabase.db :as mdb] [metabase.db.spec :as mdb.spec] [metabase.driver.ddl.interface :as ddl.i] + [metabase.driver.h2] [metabase.driver.sql.util :as sql.u] [metabase.models.database :refer [Database]] [metabase.test.data.impl :as data.impl] @@ -17,6 +18,8 @@ [metabase.util :as u] [toucan2.core :as t2])) +(comment metabase.driver.h2/keep-me) + (sql-jdbc.tx/add-test-extensions! :h2) (defonce ^:private h2-test-dbs-created-by-this-instance (atom #{})) @@ -95,12 +98,15 @@ (defmethod tx/aggregate-column-info :h2 ([driver ag-type] - ((get-method tx/aggregate-column-info ::tx/test-extensions) driver ag-type)) + (merge + ((get-method tx/aggregate-column-info ::tx/test-extensions) driver ag-type) + (when (= ag-type :count) + {:base_type :type/BigInteger}))) ([driver ag-type field] (merge ((get-method tx/aggregate-column-info ::tx/test-extensions) driver ag-type field) - (when (= ag-type :sum) + (when (#{:sum :cum-count} ag-type) {:base_type :type/BigInteger})))) (defmethod execute/execute-sql! :h2 diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj index 5fa109cd5cf..7411f7ccdd0 100644 --- a/test/metabase/test/data/interface.clj +++ b/test/metabase/test/data/interface.clj @@ -375,8 +375,7 @@ (defmethod aggregate-column-info ::test-extensions ([_ aggregation-type] - ;; TODO - Can `:cum-count` be used without args as well ?? - (assert (= aggregation-type :count)) + (assert (#{:count :cum-count} aggregation-type)) {:base_type :type/BigInteger :semantic_type :type/Quantity :name "count" @@ -386,10 +385,14 @@ ([_driver aggregation-type {field-id :id, table-id :table_id}] {:pre [(some? table-id)]} - (first (qp/query->expected-cols {:database (t2/select-one-fn :db_id Table :id table-id) - :type :query - :query {:source-table table-id - :aggregation [[aggregation-type [:field-id field-id]]]}})))) + (merge + (first (qp/query->expected-cols {:database (t2/select-one-fn :db_id Table :id table-id) + :type :query + :query {:source-table table-id + :aggregation [[aggregation-type [:field-id field-id]]]}})) + (when (= aggregation-type :cum-count) + {:base_type :type/BigInteger + :semantic_type :type/Quantity})))) (defmulti count-with-template-tag-query diff --git a/test/metabase/test/data/sql.clj b/test/metabase/test/data/sql.clj index c30e602b73b..514b5700a18 100644 --- a/test/metabase/test/data/sql.clj +++ b/test/metabase/test/data/sql.clj @@ -4,12 +4,15 @@ [clojure.string :as str] [metabase.driver :as driver] [metabase.driver.ddl.interface :as ddl.i] + [metabase.driver.sql] [metabase.driver.sql.util :as sql.u] [metabase.query-processor :as qp] [metabase.test.data :as data] [metabase.test.data.interface :as tx] [metabase.util.log :as log])) +(comment metabase.driver.sql/keep-me) + (driver/register! :sql/test-extensions, :abstract? true) (tx/add-test-extensions! :sql/test-extensions) -- GitLab