From 29bf76eee265af8382a77f3080858909a25a5b36 Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:37:11 -0800 Subject: [PATCH] Convert hundreds of tests to the new style (#14623) * Goodbye forever to expect-with-driver * :wrench: * Update expectations counts * Convert metabase.mbql.util-test to the new style * Convert some more test namespaces to the new style * Convert four more namespaces to the new style --- backend/mbql/test/metabase/mbql/util_test.clj | 1168 ++++++++--------- .../test/metabase/driver/oracle_test.clj | 81 +- .../test/metabase/driver/sqlserver_test.clj | 167 +-- test/expectations.clj | 5 +- test/metabase/api/transform_test.clj | 48 +- test/metabase/models/field_test.clj | 20 +- .../permissions_group_membership_test.clj | 29 +- .../middleware/expand_macros_test.clj | 479 ++++--- .../query_processor_test/count_where_test.clj | 167 +-- .../expression_aggregations_test.clj | 505 ++++--- .../implicit_joins_test.clj | 266 ++-- .../nested_field_test.clj | 269 ++-- .../query_processor_test/sum_where_test.clj | 176 +-- test/metabase/server/middleware/json_test.clj | 19 +- test/metabase/server/middleware/util_test.clj | 38 +- .../sync_metadata/metabase_metadata_test.clj | 72 +- .../sync/sync_metadata/sync_timezone_test.clj | 106 +- test/metabase/test/data/datasets.clj | 51 - test/metabase/util/infer_spaces_test.clj | 24 +- test/metabase/util/password_test.clj | 82 +- 20 files changed, 1831 insertions(+), 1941 deletions(-) diff --git a/backend/mbql/test/metabase/mbql/util_test.clj b/backend/mbql/test/metabase/mbql/util_test.clj index 0e44c432e02..928ad83b33e 100644 --- a/backend/mbql/test/metabase/mbql/util_test.clj +++ b/backend/mbql/test/metabase/mbql/util_test.clj @@ -1,6 +1,5 @@ (ns metabase.mbql.util-test (:require [clojure.test :refer :all] - [expectations :refer [expect]] [java-time :as t] [metabase.mbql.util :as mbql.u] metabase.types)) @@ -27,48 +26,52 @@ ;;; | match | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; can we use `match` to find the instances of a clause? -(expect - [[:field-id 10] - [:field-id 20]] - (mbql.u/match {:query {:filter [:= - [:field-id 10] - [:field-id 20]]}} - [:field-id & _])) - -;; is `match` nice enought to automatically wrap raw keywords in appropriate patterns for us? -(expect - [[:field-id 1] - [:field-id 2] - [:field-id 3]] - (mbql.u/match {:fields [[:field-id 1] [:fk-> [:field-id 2] [:field-id 3]]]} - :field-id)) - -;; if we pass a set of keywords, will that generate an appropriate pattern to match multiple clauses as well? -(expect - [[:field-id 1] - [:field-id 2] - [:field-id 3] - [:datetime-field [:field-id 4]]] - (mbql.u/match {:fields [[:field-id 1] - [:fk-> [:field-id 2] [:field-id 3]] - [:datetime-field [:field-id 4]]]} - #{:field-id :datetime-field})) - -;; `match` shouldn't include subclauses of matches -(expect - [[:field-id 1] - [:fk-> [:field-id 2] [:field-id 3]]] - (mbql.u/match [[:field-id 1] [:fk-> [:field-id 2] [:field-id 3]]] - [(:or :field-id :fk->) & _])) - -(expect - [[:field-id 10] - [:field-id 20]] - (mbql.u/match {:query {:filter [:= - [:field-id 10] - [:field-id 20]]}} - [(:or :field-id :+ :-) & _])) +(deftest basic-match-test + (testing "can we use `match` to find the instances of a clause?" + (is (= [[:field-id 10] + [:field-id 20]] + (mbql.u/match {:query {:filter [:= + [:field-id 10] + [:field-id 20]]}} + [:field-id & _]))))) + +(deftest match-keywords-test + (testing "is `match` nice enought to automatically wrap raw keywords in appropriate patterns for us?" + (is (= [[:field-id 1] + [:field-id 2] + [:field-id 3]] + (mbql.u/match {:fields [[:field-id 1] [:fk-> [:field-id 2] [:field-id 3]]]} + :field-id))) + + (is (= [[:field-id 1] + [:field-id 2]] + (mbql.u/match {:fields [[:field-id 1] [:datetime-field [:field-id 2] :day]]} + :field-id))))) + +(deftest match-set-of-keywords-tes + (testing "if we pass a set of keywords, will that generate an appropriate pattern to match multiple clauses as well?" + (is (= [[:field-id 1] + [:field-id 2] + [:field-id 3] + [:datetime-field [:field-id 4]]] + (mbql.u/match {:fields [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 3]] + [:datetime-field [:field-id 4]]]} + #{:field-id :datetime-field}))))) + +(deftest match-dont-include-subclauses-test + (testing "`match` shouldn't include subclauses of matches" + (is (= [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 3]]] + (mbql.u/match [[:field-id 1] [:fk-> [:field-id 2] [:field-id 3]]] + [(:or :field-id :fk->) & _]))) + + (is (= [[:field-id 10] + [:field-id 20]] + (mbql.u/match {:query {:filter [:= + [:field-id 10] + [:field-id 20]]}} + [(:or :field-id :+ :-) & _]))))) ;; can we use some of the cool features of pattern matching? (def ^:private a-query @@ -79,288 +82,277 @@ [:field-id 30] [:field-id 40]]]}) -;; can we use the optional `result` parameter to find return something other than the whole clause? -(expect - [41] - ;; return just the dest IDs of Fields in a fk-> clause - (mbql.u/match a-query - [:fk-> _ [:field-id dest-id]] (inc dest-id))) - -(expect - [10 20] - (mbql.u/match (:breakout a-query) [:field-id id] id)) - -;; match should return `nil` if there are no matches so you don't need to call `seq` -(expect - nil - (mbql.u/match {} [:datetime-field _ unit] unit)) - -;; if pattern is just a raw keyword `match` should be kind enough to turn it into a pattern for you -(expect - [[:field-id 1] - [:field-id 2]] - (mbql.u/match {:fields [[:field-id 1] [:datetime-field [:field-id 2] :day]]} - :field-id)) - -;; can we `:guard` a pattern? -(expect - [[:field-id 2]] - (let [a-field-id 2] - (mbql.u/match {:fields [[:field-id 1] [:field-id 2]]} - [:field-id (id :guard (partial = a-field-id))]))) - -;; ok, if for some reason we can't use `:guard` in the pattern will `match` filter out nil results? -(expect - [2] - (mbql.u/match {:fields [[:field-id 1] [:field-id 2]]} - [:field-id id] - (when (= id 2) - id))) - -;; Ok, if we want to use predicates but still return the whole match, can we use the anaphoric `&match` symbol to -;; return the whole thing? +(deftest match-result-paramater-test + (testing "can we use the optional `result` parameter to find return something other than the whole clause?" + (is (= [41] + ;; return just the dest IDs of Fields in a fk-> clause + (mbql.u/match a-query + [:fk-> _ [:field-id dest-id]] (inc dest-id)))) + + (is (= [10 20] + (mbql.u/match (:breakout a-query) [:field-id id] id))))) + +(deftest match-return-nil-for-empty-sequences-test + (testing "match should return `nil` if there are no matches so you don't need to call `seq`" + (is (= nil + (mbql.u/match {} [:datetime-field _ unit] unit))))) + +(deftest match-guard-test + (testing "can we `:guard` a pattern?" + (is (= [[:field-id 2]] + (let [a-field-id 2] + (mbql.u/match {:fields [[:field-id 1] [:field-id 2]]} + [:field-id (id :guard (partial = a-field-id))]))))) + + (testing "ok, if for some reason we can't use `:guard` in the pattern will `match` filter out nil results?" + (is (= [2] + (mbql.u/match {:fields [[:field-id 1] [:field-id 2]]} + [:field-id id] + (when (= id 2) + id)))))) + (def ^:private another-query {:fields [[:field-id 1] [:datetime-field [:field-id 2] :day] [:datetime-field [:fk-> [:field-id 3] [:field-id 4]] :month]]}) -(expect - [[:field-id 1] - [:field-id 2] - [:field-id 3] - [:field-id 4]] - (let [some-pred? (constantly true)] - (mbql.u/match another-query - :field-id - (when some-pred? - &match)))) - -;; can we use the anaphoric `&parents` symbol to examine the parents of the collection? let's see if we can match -;; `:field-id` clauses that are inside `:datetime-field` clauses, regardless of whether something else wraps them -(expect - [[:field-id 2] - [:field-id 3] - [:field-id 4]] - (mbql.u/match another-query - :field-id - (when (contains? (set &parents) :datetime-field) - &match))) - -;; can we match using a CLASS? -(expect - [#inst "2018-10-08T00:00:00.000-00:00"] - (mbql.u/match [[:field-id 1] - [:field-id 2] - #inst "2018-10-08" - 4000] - java.util.Date)) - -;; can we match using a PREDICATE? -(expect - [4000 5000] - ;; find the integer args to `:=` clauses that are not inside `:field-id` clauses - (mbql.u/match {:filter [:and - [:= [:field-id 1] 4000] - [:= [:field-id 2] 5000]]} - integer? - (when (= := (last &parents)) - &match))) - -;; how can we use predicates not named by a symbol? -(expect - [1 4000 2 5000] - (mbql.u/match {:filter [:and - [:= [:field-id 1] 4000] - [:= [:field-id 2] 5000]]} - (&match :guard #(integer? %)))) - -;; can we use a predicate and bind the match at the same time? -(expect - [2 4001 3 5001] - (mbql.u/match {:filter [:and - [:= [:field-id 1] 4000] - [:= [:field-id 2] 5000]]} - (i :guard #(integer? %)) - (inc i))) - -;; can we match against a map? -(expect - ["card__1847"] - (let [x {:source-table "card__1847"}] - (mbql.u/match x - (m :guard (every-pred map? (comp string? :source-table))) - (:source-table m)))) - -;; how about a sequence of maps? -(expect - ["card__1847"] - (let [x [{:source-table "card__1847"}]] - (mbql.u/match x - (m :guard (every-pred map? (comp string? :source-table))) - (:source-table m)))) - -;; can we use `recur` inside a pattern? -(expect - [[0 :month]] - (mbql.u/match {:filter [:time-interval [:field-id 1] :current :month]} - [:time-interval field :current unit] (recur [:time-interval field 0 unit]) - [:time-interval _ n unit] [n unit])) - -;; can we short-circut a match to prevent recursive matching? -(expect - [10] - (mbql.u/match [[:field-id 10] - [:datetime-field [:field-id 20] :day]] - [:field-id id] id - [_ [:field-id & _] & _] nil)) - -;; can we use a list with a :guard clause? -(expect - [10 20] - (mbql.u/match {:query {:filter [:= - [:field-id 10] - [:field-id 20]]}} - (id :guard int?) id)) +(deftest match-&match-test + (testing (str "Ok, if we want to use predicates but still return the whole match, can we use the anaphoric `&match` " + "symbol to return the whole thing?") + (is (= [[:field-id 1] + [:field-id 2] + [:field-id 3] + [:field-id 4]] + (let [some-pred? (constantly true)] + (mbql.u/match another-query + :field-id + (when some-pred? + &match))))))) + +(deftest match-&parents-test + (testing (str "can we use the anaphoric `&parents` symbol to examine the parents of the collection? let's see if we " + "can match `:field-id` clauses that are inside `:datetime-field` clauses, regardless of whether " + "something else wraps them") + (is (= [[:field-id 2] + [:field-id 3] + [:field-id 4]] + (mbql.u/match another-query + :field-id + (when (contains? (set &parents) :datetime-field) + &match)))))) + +(deftest match-by-class-test + (testing "can we match using a CLASS?" + (is (= [#inst "2018-10-08T00:00:00.000-00:00"] + (mbql.u/match [[:field-id 1] + [:field-id 2] + #inst "2018-10-08" + 4000] + java.util.Date))))) + +(deftest match-by-predicate-test + (testing "can we match using a PREDICATE?" + (is (= [4000 5000] + ;; find the integer args to `:=` clauses that are not inside `:field-id` clauses + (mbql.u/match {:filter [:and + [:= [:field-id 1] 4000] + [:= [:field-id 2] 5000]]} + integer? + (when (= := (last &parents)) + &match))))) + + (testing "how can we use predicates not named by a symbol?" + (is (= [1 4000 2 5000] + (mbql.u/match {:filter [:and + [:= [:field-id 1] 4000] + [:= [:field-id 2] 5000]]} + (&match :guard #(integer? %)))))) + + (testing "can we use a predicate and bind the match at the same time?" + (is (= [2 4001 3 5001] + (mbql.u/match {:filter [:and + [:= [:field-id 1] 4000] + [:= [:field-id 2] 5000]]} + (i :guard #(integer? %)) + (inc i)))))) + +(deftest match-map-test + (testing "can we match against a map?" + (is (= ["card__1847"] + (let [x {:source-table "card__1847"}] + (mbql.u/match x + (m :guard (every-pred map? (comp string? :source-table))) + (:source-table m))))))) + +(deftest match-sequence-of-maps-test + (testing "how about a sequence of maps?" + (is (= ["card__1847"] + (let [x [{:source-table "card__1847"}]] + (mbql.u/match x + (m :guard (every-pred map? (comp string? :source-table))) + (:source-table m))))))) + +(deftest match-recur-inside-pattern-test + (testing "can we use `recur` inside a pattern?" + (is (= [[0 :month]] + (mbql.u/match {:filter [:time-interval [:field-id 1] :current :month]} + [:time-interval field :current unit] (recur [:time-interval field 0 unit]) + [:time-interval _ n unit] [n unit]))))) + +(deftest match-short-circut-test + (testing "can we short-circut a match to prevent recursive matching?" + (is (= [10] + (mbql.u/match [[:field-id 10] + [:datetime-field [:field-id 20] :day]] + [:field-id id] id + [_ [:field-id & _] & _] nil))))) + +(deftest match-list-with-guard-clause-test + (testing "can we use a list with a :guard clause?" + (is (= [10 20] + (mbql.u/match {:query {:filter [:= + [:field-id 10] + [:field-id 20]]}} + (id :guard int?) id))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | replace | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; can we use `replace` to replace a specific clause? -(expect - {:breakout [[:datetime-field [:field-id 10] :day] - [:datetime-field [:field-id 20] :day] - [:field-literal "Wow"]] - :fields [[:fk-> - [:datetime-field [:field-id 30] :day] - [:datetime-field [:field-id 40] :day]]]} - (mbql.u/replace a-query [:field-id id] - [:datetime-field [:field-id id] :day])) - -;; can we wrap the pattern in a map to restrict what gets replaced? -(expect - {:breakout [[:datetime-field [:field-id 10] :day] - [:datetime-field [:field-id 20] :day] - [:field-literal "Wow"]] - :fields [[:fk-> [:field-id 30] [:field-id 40]]]} - (mbql.u/replace-in a-query [:breakout] [:field-id id] - [:datetime-field [:field-id id] :day])) - -;; can we use multiple patterns at the same time?! -(expect - {:breakout [[:field-id 10] [:field-id 20] {:name "Wow"}], :fields [30]} - (mbql.u/replace a-query - [:fk-> [:field-id field-id] _] field-id - [:field-literal field-name] {:name field-name})) - -;; can we use `replace` to replace the ID of the dest Field in fk-> clauses? -(expect - {:breakout [[:field-id 10] - [:field-id 20] - [:field-literal "Wow"]] - :fields [[:fk-> [:field-id 30] [:field-id 100]]]} - (mbql.u/replace a-query [:fk-> source [:field-id 40]] - [:fk-> source [:field-id 100]])) - -;; can we use `replace` to fix `fk->` clauses where both args are unwrapped IDs? -(expect - {:query {:fields [[:fk-> [:field-id 1] [:field-id 2]] - [:fk-> [:field-id 3] [:field-id 4]]]}} - (mbql.u/replace-in - {:query {:fields [[:fk-> 1 2] - [:fk-> [:field-id 3] [:field-id 4]]]}} - [:query :fields] - [:fk-> (source :guard integer?) (dest :guard integer?)] - [:fk-> [:field-id source] [:field-id dest]])) - -;; does `replace` accept a raw keyword as the pattern the way `match` does? -(expect - {:fields ["WOW" - [:datetime-field "WOW" :day] - [:datetime-field [:fk-> "WOW" "WOW"] :month]]} - (mbql.u/replace another-query :field-id "WOW")) - -;; does `replace` accept a set of keywords the way `match` does? -(expect - {:fields ["WOW" "WOW" "WOW"]} - (mbql.u/replace another-query #{:datetime-field :field-id} "WOW")) - -;; can we use the anaphor `&match` to look at the entire match? -(expect - {:fields [[:field-id 1] - [:magical-field - [:datetime-field [:field-id 2] :day]] - [:magical-field - [:datetime-field [:fk-> [:field-id 3] [:field-id 4]] :month]]]} - (mbql.u/replace another-query :datetime-field [:magical-field &match])) - -;; can we use the anaphor `&parents` to look at the parents of the match? -(expect - {:fields - [[:field-id 1] - [:datetime-field "WOW" :day] - [:datetime-field [:fk-> "WOW" "WOW"] :month]]} - ;; replace field ID clauses that are inside a datetime-field clause - (mbql.u/replace another-query :field-id - (if (contains? (set &parents) :datetime-field) - "WOW" - &match))) - -;; can we replace using a CLASS? -(expect - [[:field-id 1] - [:field-id 2] - [:timestamp #inst "2018-10-08T00:00:00.000-00:00"] - 4000] - (mbql.u/replace [[:field-id 1] - [:field-id 2] - #inst "2018-10-08" - 4000] - java.util.Date - [:timestamp &match])) - -;; can we replace using a PREDICATE? -(expect - {:filter [:and [:= [:field-id nil] 4000.0] [:= [:field-id nil] 5000.0]]} - ;; find the integer args to `:=` clauses that are not inside `:field-id` clauses and make them FLOATS - (mbql.u/replace {:filter [:and - [:= [:field-id 1] 4000] - [:= [:field-id 2] 5000]]} - integer? - (when (= := (last &parents)) - (float &match)))) - -;; can we do fancy stuff like remove all the filters that use datetime fields from a query? -;; -;; (NOTE: this example doesn't take into account the fact that [:binning-strategy ...] can wrap a `:datetime-field`, -;; so it's only appropriate for drivers that don't support binning (e.g. GA). Also the driver QP will need to be -;; written to handle the nils in a filter clause appropriately.) -(expect - [:and nil [:= [:field-id 100] 20]] - (mbql.u/replace [:and - [:= - [:datetime-field [:field-literal "ga:date"] :day] - [:absolute-datetime #inst "2016-11-08T00:00:00.000-00:00" :day]] - [:= [:field-id 100] 20]] - [_ [:datetime-field & _] & _] nil)) - -;; can we use short-circuting patterns to do something tricky like only replace `:field-id` clauses that aren't -;; wrapped by other clauses? -(expect - [[:datetime-field [:field-id 10] :day] - [:datetime-field [:field-id 20] :month] - [:field-id 30]] - (let [id-is-datetime-field? #{10}] - (mbql.u/replace [[:field-id 10] - [:datetime-field [:field-id 20] :month] - [:field-id 30]] - ;; don't replace anything that's already wrapping a `field-id` - [_ [:field-id & _] & _] - &match - - [:field-id (_ :guard id-is-datetime-field?)] - [:datetime-field &match :day]))) +(deftest basic-replace-test + (testing "can we use `replace` to replace a specific clause?" + (is (= {:breakout [[:datetime-field [:field-id 10] :day] + [:datetime-field [:field-id 20] :day] + [:field-literal "Wow"]] + :fields [[:fk-> + [:datetime-field [:field-id 30] :day] + [:datetime-field [:field-id 40] :day]]]} + (mbql.u/replace a-query [:field-id id] + [:datetime-field [:field-id id] :day]))))) + +(deftest basic-replace-in-test + (testing "can we wrap the pattern in a map to restrict what gets replaced?" + (is (= {:breakout [[:datetime-field [:field-id 10] :day] + [:datetime-field [:field-id 20] :day] + [:field-literal "Wow"]] + :fields [[:fk-> [:field-id 30] [:field-id 40]]]} + (mbql.u/replace-in a-query [:breakout] [:field-id id] + [:datetime-field [:field-id id] :day]))))) + +(deftest replace-multiple-patterns-test + (testing "can we use multiple patterns at the same time?!" + (is (= {:breakout [[:field-id 10] [:field-id 20] {:name "Wow"}], :fields [30]} + (mbql.u/replace a-query + [:fk-> [:field-id field-id] _] field-id + [:field-literal field-name] {:name field-name}))))) + +(deftest replace-field-ids-test + (testing "can we use `replace` to replace the ID of the dest Field in fk-> clauses?" + (is (= {:breakout [[:field-id 10] + [:field-id 20] + [:field-literal "Wow"]] + :fields [[:fk-> [:field-id 30] [:field-id 100]]]} + (mbql.u/replace a-query [:fk-> source [:field-id 40]] + [:fk-> source [:field-id 100]]))))) + +(deftest replace-fix-bad-mbql-test + (testing "can we use `replace` to fix `fk->` clauses where both args are unwrapped IDs?" + (is (= {:query {:fields [[:fk-> [:field-id 1] [:field-id 2]] + [:fk-> [:field-id 3] [:field-id 4]]]}} + (mbql.u/replace-in + {:query {:fields [[:fk-> 1 2] + [:fk-> [:field-id 3] [:field-id 4]]]}} + [:query :fields] + [:fk-> (source :guard integer?) (dest :guard integer?)] + [:fk-> [:field-id source] [:field-id dest]]))))) + +(deftest replace-raw-keyword-patterns-test + (testing "does `replace` accept a raw keyword as the pattern the way `match` does?" + (is (= {:fields ["WOW" + [:datetime-field "WOW" :day] + [:datetime-field [:fk-> "WOW" "WOW"] :month]]} + (mbql.u/replace another-query :field-id "WOW"))))) + +(deftest replace-set-of-keywords-test + (testing "does `replace` accept a set of keywords the way `match` does?" + (is (= {:fields ["WOW" "WOW" "WOW"]} + (mbql.u/replace another-query #{:datetime-field :field-id} "WOW"))))) + +(deftest replace-&match-test + (testing "can we use the anaphor `&match` to look at the entire match?" + (is (= {:fields [[:field-id 1] + [:magical-field + [:datetime-field [:field-id 2] :day]] + [:magical-field + [:datetime-field [:fk-> [:field-id 3] [:field-id 4]] :month]]]} + (mbql.u/replace another-query :datetime-field [:magical-field &match]))))) + +(deftest replace-&parents-test + (testing "can we use the anaphor `&parents` to look at the parents of the match?" + (is (= {:fields + [[:field-id 1] + [:datetime-field "WOW" :day] + [:datetime-field [:fk-> "WOW" "WOW"] :month]]} + ;; replace field ID clauses that are inside a datetime-field clause + (mbql.u/replace another-query :field-id + (if (contains? (set &parents) :datetime-field) + "WOW" + &match)))))) + +(deftest replace-by-class-test + (testing "can we replace using a CLASS?" + (is (= [[:field-id 1] + [:field-id 2] + [:timestamp #inst "2018-10-08T00:00:00.000-00:00"] + 4000] + (mbql.u/replace [[:field-id 1] + [:field-id 2] + #inst "2018-10-08" + 4000] + java.util.Date + [:timestamp &match]))))) + +(deftest replace-by-predicate-test + (testing "can we replace using a PREDICATE?" + (is (= {:filter [:and [:= [:field-id nil] 4000.0] [:= [:field-id nil] 5000.0]]} + ;; find the integer args to `:=` clauses that are not inside `:field-id` clauses and make them FLOATS + (mbql.u/replace {:filter [:and + [:= [:field-id 1] 4000] + [:= [:field-id 2] 5000]]} + integer? + (when (= := (last &parents)) + (float &match))))))) + +(deftest complex-replace-test + (testing "can we do fancy stuff like remove all the filters that use datetime fields from a query?" + ;; (NOTE: this example doesn't take into account the fact that [:binning-strategy ...] can wrap a `:datetime-field`, + ;; so it's only appropriate for drivers that don't support binning (e.g. GA). Also the driver QP will need to be + ;; written to handle the nils in a filter clause appropriately.) + (is (= [:and nil [:= [:field-id 100] 20]] + (mbql.u/replace [:and + [:= + [:datetime-field [:field-literal "ga:date"] :day] + [:absolute-datetime #inst "2016-11-08T00:00:00.000-00:00" :day]] + [:= [:field-id 100] 20]] + [_ [:datetime-field & _] & _] nil))))) + +(deftest replace-short-circut-test + (testing (str "can we use short-circuting patterns to do something tricky like only replace `:field-id` clauses that " + "aren't wrapped by other clauses?") + (is (= [[:datetime-field [:field-id 10] :day] + [:datetime-field [:field-id 20] :month] + [:field-id 30]] + (let [id-is-datetime-field? #{10}] + (mbql.u/replace [[:field-id 10] + [:datetime-field [:field-id 20] :month] + [:field-id 30]] + ;; don't replace anything that's already wrapping a `field-id` + [_ [:field-id & _] & _] + &match + + [:field-id (_ :guard id-is-datetime-field?)] + [:datetime-field &match :day])))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -472,41 +464,38 @@ (mbql.u/simplify-compound-filter [:and nil [:= [:field-id 1] nil]]))))) -;; can we add an order-by clause to a query? -(expect - {:source-table 1, :order-by [[:asc [:field-id 10]]]} - (mbql.u/add-order-by-clause {:source-table 1} [:asc [:field-id 10]])) - -(expect - {:source-table 1 - :order-by [[:asc [:field-id 10]] - [:asc [:field-id 20]]]} - (mbql.u/add-order-by-clause {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - [:asc [:field-id 20]])) - -;; duplicate clauses should get ignored -(expect - {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - (mbql.u/add-order-by-clause {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - [:asc [:field-id 10]])) - -;; as should clauses that reference the same Field -(expect - {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - (mbql.u/add-order-by-clause {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - [:desc [:field-id 10]])) - -(expect - {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - (mbql.u/add-order-by-clause {:source-table 1 - :order-by [[:asc [:field-id 10]]]} - [:asc [:datetime-field [:field-id 10] :day]])) +(deftest add-order-by-clause-test + (testing "can we add an order-by clause to a query?" + (is (= {:source-table 1, :order-by [[:asc [:field-id 10]]]} + (mbql.u/add-order-by-clause {:source-table 1} [:asc [:field-id 10]]))) + + (is (= {:source-table 1 + :order-by [[:asc [:field-id 10]] + [:asc [:field-id 20]]]} + (mbql.u/add-order-by-clause {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + [:asc [:field-id 20]])))) + + (testing "duplicate clauses should get ignored" + (is (= {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + (mbql.u/add-order-by-clause {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + [:asc [:field-id 10]])))) + + (testing "as should clauses that reference the same Field" + (is (= {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + (mbql.u/add-order-by-clause {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + [:desc [:field-id 10]]))) + + (testing "fields with different amounts of wrapping (plain field vs datetime-field)" + (is (= {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + (mbql.u/add-order-by-clause {:source-table 1 + :order-by [[:asc [:field-id 10]]]} + [:asc [:datetime-field [:field-id 10] :day]])))))) (deftest combine-filter-clauses-test (is (= [:and [:= [:field-id 1] 100] [:= [:field-id 2] 200]] @@ -763,151 +752,134 @@ :aggregation [[:avg [:field-id 1]] [:max [:field-id 1]]]}}) -(expect - [:avg [:field-id 1]] - (mbql.u/aggregation-at-index query-with-some-nesting 0)) - -(expect - [:max [:field-id 1]] - (mbql.u/aggregation-at-index query-with-some-nesting 1)) - -(expect - [:avg [:field-id 1]] - (mbql.u/aggregation-at-index query-with-some-nesting 0 0)) - -(expect - [:stddev [:field-id 1]] - (mbql.u/aggregation-at-index query-with-some-nesting 0 1)) - -(expect - [:min [:field-id 1]] - (mbql.u/aggregation-at-index query-with-some-nesting 1 1)) +(deftest aggregation-at-index-test + (doseq [[input expected] {[0] [:avg [:field-id 1]] + [1] [:max [:field-id 1]] + [0 0] [:avg [:field-id 1]] + [0 1] [:stddev [:field-id 1]] + [1 1] [:min [:field-id 1]]}] + (testing (pr-str (cons 'aggregation-at-index input)) + (is (= expected + (apply mbql.u/aggregation-at-index query-with-some-nesting input)))))) ;;; --------------------------------- Unique names & transforming ags to have names ---------------------------------- -;; can we generate unique names? -(expect - ["count" "sum" "count_2" "count_3"] - (mbql.u/uniquify-names ["count" "sum" "count" "count"])) - -(expect - [[:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:count] {:name "count_2"}] - [:aggregation-options [:count] {:name "count_3"}]] - (mbql.u/uniquify-named-aggregations - [[:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:count] {:name "count"}]])) - -;; what if we try to trick it by using a name it would have generated? -(expect - ["count" "count_2" "count_2_2"] - (mbql.u/uniquify-names ["count" "count" "count_2"])) - -(expect - [[:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:count] {:name "count_2"}] - [:aggregation-options [:count] {:name "count_2_2"}]] - (mbql.u/uniquify-named-aggregations - [[:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:count] {:name "count"}] - [:aggregation-options [:count] {:name "count_2"}]])) - -;; for wacky DBMSes like SQLServer that return blank column names sometimes let's make sure we handle those without -;; exploding -(expect - ["" "_2"] - (mbql.u/uniquify-names ["" ""])) - -;; can we wrap all of our aggregation clauses in `:named` clauses? -(defn- simple-ag->name [[ag-name]] - (name ag-name)) - -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:count [:field-id 1]] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:min [:field-id 1]] {:name "min"}]] - (mbql.u/pre-alias-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:count [:field-id 1]] - [:sum [:field-id 1]] - [:avg [:field-id 1]] - [:sum [:field-id 1]] - [:min [:field-id 1]]])) - -;; we shouldn't change the name of ones that are already named -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:count [:field-id 1]] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:aggregation-options [:min [:field-id 1]] {:name "min"}]] - (mbql.u/pre-alias-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:count [:field-id 1]] - [:sum [:field-id 1]] - [:avg [:field-id 1]] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:min [:field-id 1]]])) - -;; ok, can we do the same thing as the tests above but make those names *unique* at the same time? -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"} ] - [:aggregation-options [:count [:field-id 1]] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:aggregation-options [:avg [:field-id 1]] {:name "avg"} ] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_3"}] - [:aggregation-options [:min [:field-id 1]] {:name "min"} ]] - (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:count [:field-id 1]] - [:sum [:field-id 1]] - [:avg [:field-id 1]] - [:sum [:field-id 1]] - [:min [:field-id 1]]])) - -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:count [:field-id 1]] {:name "count"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2_2"}] - [:aggregation-options [:min [:field-id 1]] {:name "min"}]] - (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:count [:field-id 1]] - [:sum [:field-id 1]] - [:avg [:field-id 1]] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:min [:field-id 1]]])) - -;; if `:aggregation-options` only specifies `:display-name` it should still a new `:name`. -;; `pre-alias-and-uniquify-aggregations` shouldn't stomp over display name -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:aggregation-options [:sum [:field-id 1]] {:display-name "Sum of Field 1", :name "sum_3"}]] - (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:sum [:field-id 1]] - [:aggregation-options [:sum [:field-id 1]] {:display-name "Sum of Field 1"}]])) - -;; if both are specified, `display-name` should still be propogated -(expect - [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2_2", :display-name "Sum of Field 1"}]] - (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name - [[:sum [:field-id 1]] - [:sum [:field-id 1]] - [:aggregation-options [:sum [:field-id 1]] {:name "sum_2", :display-name "Sum of Field 1"}]])) +(deftest uniquify-names + (testing "can we generate unique names?" + (is (= ["count" "sum" "count_2" "count_3"] + (mbql.u/uniquify-names ["count" "sum" "count" "count"])))) + + (testing "what if we try to trick it by using a name it would have generated?" + (is (= ["count" "count_2" "count_2_2"] + (mbql.u/uniquify-names ["count" "count" "count_2"])))) + + (testing (str "for wacky DBMSes like SQL Server that return blank column names sometimes let's make sure we handle " + "those without exploding") + (is (= ["" "_2"] + (mbql.u/uniquify-names ["" ""]))))) + +(deftest uniquify-named-aggregations-test + (is (= [[:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:count] {:name "count_2"}] + [:aggregation-options [:count] {:name "count_3"}]] + (mbql.u/uniquify-named-aggregations + [[:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:count] {:name "count"}]]))) + + (testing "what if we try to trick it by using a name it would have generated?" + (is (= [[:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:count] {:name "count_2"}] + [:aggregation-options [:count] {:name "count_2_2"}]] + (mbql.u/uniquify-named-aggregations + [[:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:count] {:name "count"}] + [:aggregation-options [:count] {:name "count_2"}]]))))) + +(deftest pre-alias-aggregations-test + (letfn [(simple-ag->name [[ag-name]] + (name ag-name))] + (testing "can we wrap all of our aggregation clauses in `:named` clauses?" + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:count [:field-id 1]] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:min [:field-id 1]] {:name "min"}]] + (mbql.u/pre-alias-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:count [:field-id 1]] + [:sum [:field-id 1]] + [:avg [:field-id 1]] + [:sum [:field-id 1]] + [:min [:field-id 1]]])))) + + (testing "we shouldn't change the name of ones that are already named" + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:count [:field-id 1]] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:aggregation-options [:min [:field-id 1]] {:name "min"}]] + (mbql.u/pre-alias-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:count [:field-id 1]] + [:sum [:field-id 1]] + [:avg [:field-id 1]] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:min [:field-id 1]]])))) + + + (testing "ok, can we do the same thing as the tests above but make those names *unique* at the same time?" + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"} ] + [:aggregation-options [:count [:field-id 1]] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:aggregation-options [:avg [:field-id 1]] {:name "avg"} ] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_3"}] + [:aggregation-options [:min [:field-id 1]] {:name "min"} ]] + (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:count [:field-id 1]] + [:sum [:field-id 1]] + [:avg [:field-id 1]] + [:sum [:field-id 1]] + [:min [:field-id 1]]]))) + + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:count [:field-id 1]] {:name "count"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:aggregation-options [:avg [:field-id 1]] {:name "avg"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2_2"}] + [:aggregation-options [:min [:field-id 1]] {:name "min"}]] + (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:count [:field-id 1]] + [:sum [:field-id 1]] + [:avg [:field-id 1]] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:min [:field-id 1]]])))) + + (testing (str "if `:aggregation-options` only specifies `:display-name` it should still a new `:name`. " + "`pre-alias-and-uniquify-aggregations` shouldn't stomp over display name") + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:aggregation-options [:sum [:field-id 1]] {:display-name "Sum of Field 1", :name "sum_3"}]] + (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:sum [:field-id 1]] + [:aggregation-options [:sum [:field-id 1]] {:display-name "Sum of Field 1"}]]))) + + (testing "if both are specified, `display-name` should still be propagated" + (is (= [[:aggregation-options [:sum [:field-id 1]] {:name "sum"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2"}] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2_2", :display-name "Sum of Field 1"}]] + (mbql.u/pre-alias-and-uniquify-aggregations simple-ag->name + [[:sum [:field-id 1]] + [:sum [:field-id 1]] + [:aggregation-options [:sum [:field-id 1]] {:name "sum_2", :display-name "Sum of Field 1"}]]))))))) (deftest unique-name-generator-test (testing "Can we get a simple unique name generator" @@ -923,100 +895,69 @@ ;;; --------------------------------------------- query->max-rows-limit ---------------------------------------------- -;; should return `:limit` if set -(expect - 10 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1, :limit 10}})) - -;; should return `:page` items if set -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1, :page {:page 1, :items 5}}})) - -;; if `:max-results` is set return that -(expect - 15 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1}, :constraints {:max-results 15}})) - -;; if `:max-results-bare-rows` is set AND query has no aggregations, return that -(expect - 10 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1}, :constraints {:max-results 5, :max-results-bare-rows 10}})) - -(expect - 10 - (mbql.u/query->max-rows-limit - {:database 1 - :type :native - :native {:query "SELECT * FROM my_table"} - :constraints {:max-results 5, :max-results-bare-rows 10}})) - -;; if `:max-results-bare-rows` is set but query has aggregations, return `:max-results` instead -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1, :aggregation [[:count]]} - :constraints {:max-results 5, :max-results-bare-rows 10}})) - -;; if both `:limit` and `:page` are set (not sure makes sense), return the smaller of the two -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1, :limit 10, :page {:page 1, :items 5}}})) - -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1, :type :query, :query {:source-table 1, :limit 5, :page {:page 1, :items 10}}})) - -;; if both `:limit` and `:constraints` are set, prefer the smaller of the two -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1, :limit 5} - :constraints {:max-results 10}})) - -(expect - 10 - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1, :limit 15} - :constraints {:max-results 10}})) - -;; since this query doesn't have an aggregation we should be using `max-results-bare-rows` -(expect - 5 - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1, :limit 10} - :constraints {:max-results 15, :max-results-bare-rows 5}})) - -;; add an aggregation, and `:max-results` is used instead; since `:limit` is lower, return that -(expect - 10 - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1, :limit 10, :aggregation [[:count]]} - :constraints {:max-results 15, :max-results-bare-rows 5}})) - -;; if nothing is set return `nil` -(expect - nil - (mbql.u/query->max-rows-limit - {:database 1 - :type :query - :query {:source-table 1}})) +(deftest query->max-rows-limit-test + (doseq [[group query->expected] + {"should return `:limit` if set" + {{:database 1, :type :query, :query {:source-table 1, :limit 10}} 10} + + "should return `:page` items if set" + {{:database 1, :type :query, :query {:source-table 1, :page {:page 1, :items 5}}} 5} + + "if `:max-results` is set return that" + {{:database 1, :type :query, :query {:source-table 1}, :constraints {:max-results 15}} 15} + + "if `:max-results-bare-rows` is set AND query has no aggregations, return that" + {{:database 1 + :type :query + :query {:source-table 1} + :constraints {:max-results 5, :max-results-bare-rows 10}} 10 + {:database 1 + :type :native + :native {:query "SELECT * FROM my_table"} + :constraints {:max-results 5, :max-results-bare-rows 10}} 10} + + "if `:max-results-bare-rows` is set but query has aggregations, return `:max-results` instead" + {{:database 1 + :type :query + :query {:source-table 1, :aggregation [[:count]]} + :constraints {:max-results 5, :max-results-bare-rows 10}} 5} + + "if both `:limit` and `:page` are set (not sure makes sense), return the smaller of the two" + {{:database 1, :type :query, :query {:source-table 1, :limit 10, :page {:page 1, :items 5}}} 5 + {:database 1, :type :query, :query {:source-table 1, :limit 5, :page {:page 1, :items 10}}} 5} + + "if both `:limit` and `:constraints` are set, prefer the smaller of the two" + {{:database 1 + :type :query + :query {:source-table 1, :limit 5} + :constraints {:max-results 10}} 5 + + {:database 1 + :type :query + :query {:source-table 1, :limit 15} + :constraints {:max-results 10}} 10} + + "since this query doesn't have an aggregation we should be using `max-results-bare-rows`" + {{:database 1 + :type :query + :query {:source-table 1, :limit 10} + :constraints {:max-results 15, :max-results-bare-rows 5}} 5} + + "add an aggregation, and `:max-results` is used instead; since `:limit` is lower, return that" + {{:database 1 + :type :query + :query {:source-table 1, :limit 10, :aggregation [[:count]]} + :constraints {:max-results 15, :max-results-bare-rows 5}} 10} + + "if nothing is set return `nil`" + {{:database 1 + :type :query + :query {:source-table 1}} nil}} ] + (testing group + (doseq [[query expected] query->expected] + (testing (pr-str (list 'query->max-rows-limit query)) + (is (= expected + (mbql.u/query->max-rows-limit query)))))))) (deftest joined-field-test (is (= [:joined-field "a" [:field-id 10]] @@ -1035,35 +976,30 @@ (is (thrown? Exception (mbql.u/->joined-field "a" [:datetime-field [:joined-field "a" [:field-id 1]] :month]))))) -(expect - (mbql.u/datetime-arithmetics? [:+ [:field-id 13] [:interval -1 :month]])) - -(expect - (mbql.u/datetime-arithmetics? [:datetime-field [:joined-field "a" [:field-id 1]] :month])) - -(expect - false - (mbql.u/datetime-arithmetics? [:+ [:field-id 13] 3])) - -(expect - (mbql.u/expression-with-name {:expressions {:two [:+ 1 1]} - :source-table 1} - "two")) - -;; Make sure `expression-with-name` knows how to reach into the parent query if need be -(expect - (mbql.u/expression-with-name {:source-query {:expressions {:two [:+ 1 1]} - :source-table 1}} - "two")) - -(expect - 1 - (mbql.u/field-clause->id-or-literal [:field-id 1])) - -(expect - "foo" - (mbql.u/field-clause->id-or-literal [:field-literal "foo" :type/Integer])) - -(expect - "foo" - (mbql.u/field-clause->id-or-literal [:expression "foo"])) +(deftest datetime-arithmetics?-test + (is (mbql.u/datetime-arithmetics? + [:+ [:field-id 13] [:interval -1 :month]])) + (is (mbql.u/datetime-arithmetics? + [:datetime-field [:joined-field "a" [:field-id 1]] :month])) + (is (not (mbql.u/datetime-arithmetics? + [:+ [:field-id 13] 3])))) + +(deftest expression-with-name-test + (is (= [:+ 1 1] + (mbql.u/expression-with-name {:expressions {:two [:+ 1 1]} + :source-table 1} + "two"))) + + (testing "Make sure `expression-with-name` knows how to reach into the parent query if need be" + (is (= [:+ 1 1] + (mbql.u/expression-with-name {:source-query {:expressions {:two [:+ 1 1]} + :source-table 1}} + "two"))))) + +(deftest field-clause->id-or-literal-test + (doseq [[input expected] {[:field-id 1] 1 + [:field-literal "foo" :type/Integer] "foo" + [:expression "foo"] "foo"}] + (testing (pr-str (list 'field-clause->id-or-literal input)) + (is (= expected + (mbql.u/field-clause->id-or-literal input)))))) diff --git a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj index f8c1d4a83ec..e75f2a2156d 100644 --- a/modules/drivers/oracle/test/metabase/driver/oracle_test.clj +++ b/modules/drivers/oracle/test/metabase/driver/oracle_test.clj @@ -2,7 +2,6 @@ "Tests for specific behavior of the Oracle driver." (:require [clojure.java.jdbc :as jdbc] [clojure.test :refer :all] - [expectations :refer [expect]] [honeysql.core :as hsql] [metabase.driver :as driver] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] @@ -16,7 +15,6 @@ [metabase.query-processor.test-util :as qp.test-util] [metabase.test :as mt] [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets :refer [expect-with-driver]] [metabase.test.data.oracle :as oracle.tx] [metabase.test.data.sql :as sql.tx] [metabase.test.data.sql.ddl :as ddl] @@ -54,18 +52,17 @@ (sql-jdbc.conn/connection-details->spec :oracle details)) message))) -;; no SID and not Service Name should throw an exception -(expect - AssertionError - (sql-jdbc.conn/connection-details->spec :oracle {:host "localhost" - :port 1521})) - -(expect - "You must specify the SID and/or the Service Name." - (try (sql-jdbc.conn/connection-details->spec :oracle {:host "localhost" - :port 1521}) - (catch Throwable e - (driver/humanize-connection-error-message :oracle (.getMessage e))))) +(deftest require-sid-or-service-name-test + (testing "no SID and no Service Name should throw an exception" + (is (thrown? + AssertionError + (sql-jdbc.conn/connection-details->spec :oracle {:host "localhost" + :port 1521}))) + (is (= "You must specify the SID and/or the Service Name." + (try (sql-jdbc.conn/connection-details->spec :oracle {:host "localhost" + :port 1521}) + (catch Throwable e + (driver/humanize-connection-error-message :oracle (.getMessage e)))))))) (deftest test-ssh-connection (testing "Gets an error when it can't connect to oracle via ssh tunnel" @@ -96,9 +93,10 @@ (throw e)) (some-> (.getCause e) recur)))))))))) -(expect-with-driver :oracle - "UTC" - (tu/db-timezone-id)) +(deftest timezone-id-test + (mt/test-driver :oracle + (is (= "UTC" + (tu/db-timezone-id))))) (deftest insert-rows-ddl-test (is (= [[(str "INSERT ALL" @@ -127,31 +125,32 @@ `(do-with-temp-user ~username (fn [~username-binding] ~@body))) -;; Make sure Oracle CLOBs are returned as text (#9026) -(expect-with-driver :oracle - [[1M "Hello"] - [2M nil]] - (let [details (:details (data/db)) - spec (sql-jdbc.conn/connection-details->spec :oracle details) - execute! (fn [format-string & args] - (jdbc/execute! spec (apply format format-string args))) - pk-type (sql.tx/pk-sql-type :oracle)] - (with-temp-user [username] - (execute! "CREATE TABLE \"%s\".\"messages\" (\"id\" %s, \"message\" CLOB)" username pk-type) - (execute! "INSERT INTO \"%s\".\"messages\" (\"id\", \"message\") VALUES (1, 'Hello')" username) - (execute! "INSERT INTO \"%s\".\"messages\" (\"id\", \"message\") VALUES (2, NULL)" username) - (tt/with-temp* [Table [table {:schema username, :name "messages", :db_id (data/id)}] - Field [id-field {:table_id (u/get-id table), :name "id", :base_type "type/Integer"}] - Field [_ {:table_id (u/get-id table), :name "message", :base_type "type/Text"}]] - (qp.test/rows - (qp/process-query - {:database (data/id) - :type :query - :query {:source-table (u/get-id table) - :order-by [[:asc [:field-id (u/get-id id-field)]]]}})))))) +(deftest return-clobs-as-text-test + (mt/test-driver :oracle + (testing "Make sure Oracle CLOBs are returned as text (#9026)" + (let [details (:details (data/db)) + spec (sql-jdbc.conn/connection-details->spec :oracle details) + execute! (fn [format-string & args] + (jdbc/execute! spec (apply format format-string args))) + pk-type (sql.tx/pk-sql-type :oracle)] + (with-temp-user [username] + (execute! "CREATE TABLE \"%s\".\"messages\" (\"id\" %s, \"message\" CLOB)" username pk-type) + (execute! "INSERT INTO \"%s\".\"messages\" (\"id\", \"message\") VALUES (1, 'Hello')" username) + (execute! "INSERT INTO \"%s\".\"messages\" (\"id\", \"message\") VALUES (2, NULL)" username) + (tt/with-temp* [Table [table {:schema username, :name "messages", :db_id (data/id)}] + Field [id-field {:table_id (u/get-id table), :name "id", :base_type "type/Integer"}] + Field [_ {:table_id (u/get-id table), :name "message", :base_type "type/Text"}]] + (is (= [[1M "Hello"] + [2M nil]] + (qp.test/rows + (qp/process-query + {:database (data/id) + :type :query + :query {:source-table (u/get-id table) + :order-by [[:asc [:field-id (u/get-id id-field)]]]}})))))))))) (deftest handle-slashes-test - (datasets/test-driver :oracle + (mt/test-driver :oracle (let [details (:details (data/db)) spec (sql-jdbc.conn/connection-details->spec :oracle details) execute! (fn [format-string & args] @@ -170,7 +169,7 @@ ;; let's make sure we're actually attempting to generate the correctl HoneySQL for joins and source queries so we ;; don't sit around scratching our heads wondering why the queries themselves aren't working (deftest honeysql-test - (datasets/test-driver :oracle + (mt/test-driver :oracle (is (= {:select [:*] :from [{:select [[(hx/identifier :field oracle.tx/session-schema "test_data_venues" "id") diff --git a/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj b/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj index 50473c48e3d..e9cdbea0c05 100644 --- a/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj +++ b/modules/drivers/sqlserver/test/metabase/driver/sqlserver_test.clj @@ -22,7 +22,6 @@ ;;; -------------------------------------------------- VARCHAR(MAX) -------------------------------------------------- -;; Make sure something long doesn't come back as some weird type like `ClobImpl` (def ^:private a-gene "Really long string representing a gene like \"GGAGCACCTCCACAAGTGCAGGCTATCCTGTCGAGTAAGGCCT...\"" (apply str (repeatedly 1000 (partial rand-nth [\A \G \C \T])))) @@ -32,12 +31,14 @@ [{:field-name "gene", :base-type {:native "VARCHAR(MAX)"}}] [[a-gene]]]]) -(datasets/expect-with-driver :sqlserver - [[1 a-gene]] - (-> (data/dataset metabase.driver.sqlserver-test/genetic-data (data/run-mbql-query genetic-data)) - :data - :rows - obj->json->obj)) ; convert to JSON + back so the Clob gets stringified +(deftest clobs-should-come-back-as-text-test + (mt/test-driver :sqlserver + (testing "Make sure something long doesn't come back as some weird type like `ClobImpl`" + (is (= [[1 a-gene]] + (-> (data/dataset metabase.driver.sqlserver-test/genetic-data (data/run-mbql-query genetic-data)) + :data + :rows + obj->json->obj)))))) ; convert to JSON + back so the Clob gets stringified (deftest connection-spec-test (testing "Test that additional connection string options work (#5296)" @@ -63,84 +64,88 @@ ;; `<version>` (update :applicationName #(str/replace % #"\s.*$" " <version>"))))))) -(datasets/expect-with-driver :sqlserver - "UTC" - (tu/db-timezone-id)) +(deftest timezone-id-test + (mt/test-driver :sqlserver + (is (= "UTC" + (tu/db-timezone-id))))) -;; SQL Server doesn't let you use ORDER BY in nested SELECTs unless you also specify a TOP (their equivalent of -;; LIMIT). Make sure we add a max-results LIMIT to the nested query -(datasets/expect-with-driver :sqlserver - {:query (str - "SELECT TOP 1048576 \"source\".\"name\" AS \"name\" " - "FROM (" - "SELECT TOP 1048576 " - "\"dbo\".\"venues\".\"name\" AS \"name\" " - "FROM \"dbo\".\"venues\" " - "ORDER BY \"dbo\".\"venues\".\"id\" ASC" - " ) \"source\" ") ; not sure why this generates an extra space before the closing paren, but it does - :params nil} - (qp/query->native - (data/mbql-query venues - {:source-query {:source-table $$venues - :fields [$name] - :order-by [[:asc $id]]}}))) +(deftest add-max-results-limit-test + (mt/test-driver :sqlserver + (testing (str "SQL Server doesn't let you use ORDER BY in nested SELECTs unless you also specify a TOP (their " + "equivalent of LIMIT). Make sure we add a max-results LIMIT to the nested query") + (is (= {:query (str "SELECT TOP 1048576 \"source\".\"name\" AS \"name\" " + "FROM (" + "SELECT TOP 1048576 " + "\"dbo\".\"venues\".\"name\" AS \"name\" " + "FROM \"dbo\".\"venues\" " + "ORDER BY \"dbo\".\"venues\".\"id\" ASC" + " ) \"source\" ") ; not sure why this generates an extra space before the closing paren, but it does + :params nil} + (qp/query->native + (mt/mbql-query venues + {:source-query {:source-table $$venues + :fields [$name] + :order-by [[:asc $id]]}}))))))) -;; make sure when adding TOP clauses to make ORDER BY work we don't stomp over any explicit TOP clauses that may have -;; been set in the query -(datasets/expect-with-driver :sqlserver - {:query (str "SELECT TOP 10 \"source\".\"name\" AS \"name\" " - "FROM (" - "SELECT TOP 20 " - "\"dbo\".\"venues\".\"name\" AS \"name\" " - "FROM \"dbo\".\"venues\" " - "ORDER BY \"dbo\".\"venues\".\"id\" ASC" - " ) \"source\" ") - :params nil} - (qp/query->native - (data/mbql-query venues - {:source-query {:source-table $$venues - :fields [$name] - :order-by [[:asc $id]] - :limit 20} - :limit 10}))) +(deftest preserve-existing-top-clauses + (mt/test-driver :sqlserver + (testing (str "make sure when adding TOP clauses to make ORDER BY work we don't stomp over any explicit TOP " + "clauses that may have been set in the query") + (is (= {:query (str "SELECT TOP 10 \"source\".\"name\" AS \"name\" " + "FROM (" + "SELECT TOP 20 " + "\"dbo\".\"venues\".\"name\" AS \"name\" " + "FROM \"dbo\".\"venues\" " + "ORDER BY \"dbo\".\"venues\".\"id\" ASC" + " ) \"source\" ") + :params nil} + (qp/query->native + (mt/mbql-query venues + {:source-query {:source-table $$venues + :fields [$name] + :order-by [[:asc $id]] + :limit 20} + :limit 10}))))))) -;; We don't need to add TOP clauses for top-level order by. Normally we always add one anyway because of the -;; max-results stuff, but make sure our impl doesn't add one when it's not in the source MBQL -(datasets/expect-with-driver :sqlserver - {:query (str "SELECT \"source\".\"name\" AS \"name\" " - "FROM (" - "SELECT TOP 1048576 " - "\"dbo\".\"venues\".\"name\" AS \"name\" " - "FROM \"dbo\".\"venues\" " - "ORDER BY \"dbo\".\"venues\".\"id\" ASC" - " ) \"source\" " - "ORDER BY \"source\".\"id\" ASC") - :params nil} - ;; in order to actually see how things would work without the implicit max-results limit added we'll preprocess - ;; the query, strip off the `:limit` that got added, and then feed it back to the QP where we left off - (let [preprocessed (-> (data/mbql-query venues - {:source-query {:source-table $$venues - :fields [$name] - :order-by [[:asc $id]]} - :order-by [[:asc $id]]}) - qp/query->preprocessed - (m/dissoc-in [:query :limit]))] - (qp.test-util/with-everything-store - (driver/mbql->native :sqlserver preprocessed)))) +(deftest dont-add-top-clauses-for-top-level-test + (mt/test-driver :sqlserver + (testing (str "We don't need to add TOP clauses for top-level order by. Normally we always add one anyway because " + "of the max-results stuff, but make sure our impl doesn't add one when it's not in the source MBQL")) + ;; in order to actually see how things would work without the implicit max-results limit added we'll preprocess + ;; the query, strip off the `:limit` that got added, and then feed it back to the QP where we left off + (let [preprocessed (-> (mt/mbql-query venues + {:source-query {:source-table $$venues + :fields [$name] + :order-by [[:asc $id]]} + :order-by [[:asc $id]]}) + qp/query->preprocessed + (m/dissoc-in [:query :limit]))] + (qp.test-util/with-everything-store + (is (= {:query (str "SELECT \"source\".\"name\" AS \"name\" " + "FROM (" + "SELECT TOP 1048576 " + "\"dbo\".\"venues\".\"name\" AS \"name\" " + "FROM \"dbo\".\"venues\" " + "ORDER BY \"dbo\".\"venues\".\"id\" ASC" + " ) \"source\" " + "ORDER BY \"source\".\"id\" ASC") + :params nil} + (driver/mbql->native :sqlserver preprocessed))))))) -;; ok, generating all that SQL above is nice, but let's make sure our queries actually work! -(datasets/expect-with-driver :sqlserver - [["Red Medicine"] - ["Stout Burgers & Beers"] - ["The Apple Pan"]] - (qp.test/rows - (qp/process-query - (data/mbql-query venues - {:source-query {:source-table $$venues - :fields [$name] - :order-by [[:asc $id]] - :limit 5} - :limit 3})))) +(deftest max-results-should-actually-work-test + (mt/test-driver :sqlserver + (testing "ok, generating all that SQL above is nice, but let's make sure our queries actually work!" + (is (= [["Red Medicine"] + ["Stout Burgers & Beers"] + ["The Apple Pan"]] + (qp.test/rows + (qp/process-query + (mt/mbql-query venues + {:source-query {:source-table $$venues + :fields [$name] + :order-by [[:asc $id]] + :limit 5} + :limit 3})))))))) (deftest locale-bucketing-test (datasets/test-driver :sqlserver diff --git a/test/expectations.clj b/test/expectations.clj index 773543d84e9..52adcebb0f8 100644 --- a/test/expectations.clj +++ b/test/expectations.clj @@ -136,9 +136,8 @@ (when-not (env/env :drivers) (t/testing "Don't write any new tests using expect!" (let [ee? (u/ignore-exceptions (require 'metabase-enterprise.core) true)] - ;; TODO - update the numbers for EE - (t/is (<= total-expect-forms (if ee? 871 842))) - (t/is (<= total-namespaces-using-expect (if ee? 84 80)))))))) + (t/is (<= total-expect-forms (if ee? 549 524))) + (t/is (<= total-namespaces-using-expect (if ee? 59 56)))))))) (defmacro ^:deprecated expect "Simple macro that simulates converts an Expectations-style `expect` form into a `clojure.test` `deftest` form." diff --git a/test/metabase/api/transform_test.clj b/test/metabase/api/transform_test.clj index 3e373286f54..63462b5bf72 100644 --- a/test/metabase/api/transform_test.clj +++ b/test/metabase/api/transform_test.clj @@ -1,46 +1,42 @@ (ns metabase.api.transform-test (:require [clojure.test :refer :all] - [expectations :refer :all] [metabase.models.card :refer [Card]] [metabase.models.collection :refer [Collection]] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] [metabase.query-processor :as qp] - [metabase.test.data :as data] - [metabase.test.data.users :as test-users] + [metabase.test :as mt] [metabase.test.domain-entities :refer :all] [metabase.test.fixtures :as fixtures] - [metabase.test.transforms :refer :all] - [metabase.test.util :as tu])) + [metabase.test.transforms :refer :all])) (use-fixtures :once (fixtures/initialize :db)) -(defn- test-endpoint - [] - (format "transform/%s/%s/%s" (data/id) "PUBLIC" "Test transform")) +(defn- test-endpoint [] + (format "transform/%s/%s/%s" (mt/id) "PUBLIC" "Test transform")) -;; Run the transform and make sure it produces the correct result -(expect - [[1 "Red Medicine" 4 10.0646 -165.374 3 1.5 4 3 2 1] - [2 "Stout Burgers & Beers" 11 34.0996 -118.329 2 2.0 11 2 1 1] - [3 "The Apple Pan" 11 34.0406 -118.428 2 2.0 11 2 1 1]] - (test-users/with-test-user :rasta - (with-test-transform-specs - (with-test-domain-entity-specs - (tu/with-model-cleanup [Card Collection] - (-> ((test-users/user->client :rasta) :get 200 (test-endpoint)) - first - :dataset_query - qp/process-query - :data - :rows)))))) +(deftest transform-test + (testing "GET /api/transform/:db-id/:schema/:transform-name" + (testing "Run the transform and make sure it produces the correct result" + (mt/with-test-user :rasta + (with-test-transform-specs + (with-test-domain-entity-specs + (mt/with-model-cleanup [Card Collection] + (is (= [[1 "Red Medicine" 4 10.0646 -165.374 3 1.5 4 3 2 1] + [2 "Stout Burgers & Beers" 11 34.0996 -118.329 2 2.0 11 2 1 1] + [3 "The Apple Pan" 11 34.0406 -118.428 2 2.0 11 2 1 1]] + (-> (mt/user-http-request :rasta :get 200 (test-endpoint)) + first + :dataset_query + qp/process-query + mt/rows)))))))))) (deftest permissions-test (testing "GET /api/transform/:db-id/:schema/:transform-name" (testing "Do we correctly check for permissions?" (try - (perms/revoke-permissions! (perms-group/all-users) (data/id)) + (perms/revoke-permissions! (perms-group/all-users) (mt/id)) (is (= "You don't have permissions to do that." - ((test-users/user->client :rasta) :get 403 (test-endpoint)))) + (mt/user-http-request :rasta :get 403 (test-endpoint)))) (finally - (perms/grant-permissions! (perms-group/all-users) (perms/object-path (data/id)))))))) + (perms/grant-permissions! (perms-group/all-users) (perms/object-path (mt/id)))))))) diff --git a/test/metabase/models/field_test.clj b/test/metabase/models/field_test.clj index a2981731b28..86e6e005f88 100644 --- a/test/metabase/models/field_test.clj +++ b/test/metabase/models/field_test.clj @@ -1,13 +1,15 @@ (ns metabase.models.field-test "Tests for specific behavior related to the Field model." - (:require [expectations :refer :all] + (:require [clojure.test :refer :all] [metabase.sync.analyze.classifiers.name :as name])) - -;;; infer-field-special-type -(expect :type/PK (#'name/special-type-for-name-and-base-type "id" :type/Integer)) -;; other pattern matches based on type/regex (remember, base_type matters in matching!) -(expect :type/Score (#'name/special-type-for-name-and-base-type "rating" :type/Integer)) -(expect nil (#'name/special-type-for-name-and-base-type "rating" :type/Boolean)) -(expect :type/Country (#'name/special-type-for-name-and-base-type "country" :type/Text)) -(expect nil (#'name/special-type-for-name-and-base-type "country" :type/Integer)) +(deftest special-type-for-name-and-base-type-test + (doseq [[input expected] {["id" :type/Integer] :type/PK + ;; other pattern matches based on type/regex (remember, base_type matters in matching!) + ["rating" :type/Integer] :type/Score + ["rating" :type/Boolean] nil + ["country" :type/Text] :type/Country + ["country" :type/Integer] nil}] + (testing (pr-str (cons 'special-type-for-name-and-base-type input)) + (is (= expected + (apply #'name/special-type-for-name-and-base-type input)))))) diff --git a/test/metabase/models/permissions_group_membership_test.clj b/test/metabase/models/permissions_group_membership_test.clj index 12c5e2bab6e..c3b0aa45418 100644 --- a/test/metabase/models/permissions_group_membership_test.clj +++ b/test/metabase/models/permissions_group_membership_test.clj @@ -1,26 +1,25 @@ (ns metabase.models.permissions-group-membership-test (:require [clojure.test :refer :all] - [expectations :refer [expect]] [metabase.models.permissions-group :as group] [metabase.models.permissions-group-membership :as pgm :refer [PermissionsGroupMembership]] [metabase.models.user :refer [User]] + [metabase.test :as mt] [metabase.test.fixtures :as fixtures] [metabase.util :as u] - [toucan.db :as db] - [toucan.util.test :as tt])) + [toucan.db :as db])) (use-fixtures :once (fixtures/initialize :test-users)) -;; when you create a PermissionsGroupMembership for a User in the admin group, it should set their `is_superuser` flag -(expect - true - (tt/with-temp User [user] - (db/insert! PermissionsGroupMembership {:user_id (u/get-id user), :group_id (u/get-id (group/admin))}) - (db/select-one-field :is_superuser User :id (u/get-id user)))) +(deftest set-is-superuser-test + (testing "when you create a PermissionsGroupMembership for a User in the admin group, it should set their `is_superuser` flag" + (mt/with-temp User [user] + (db/insert! PermissionsGroupMembership {:user_id (u/the-id user), :group_id (u/the-id (group/admin))}) + (is (= true + (db/select-one-field :is_superuser User :id (u/the-id user))))))) -;; when you delete a PermissionsGroupMembership for a User in the admin group, it should set their `is_superuser` flag -(expect - false - (tt/with-temp User [user {:is_superuser true}] - (db/delete! PermissionsGroupMembership :user_id (u/get-id user), :group_id (u/get-id (group/admin))) - (db/select-one-field :is_superuser User :id (u/get-id user)))) +(deftest remove-is-superuser-test + (testing "when you delete a PermissionsGroupMembership for a User in the admin group, it should set their `is_superuser` flag" + (mt/with-temp User [user {:is_superuser true}] + (db/delete! PermissionsGroupMembership :user_id (u/the-id user), :group_id (u/the-id (group/admin))) + (is (= false + (db/select-one-field :is_superuser User :id (u/the-id user))))))) diff --git a/test/metabase/query_processor/middleware/expand_macros_test.clj b/test/metabase/query_processor/middleware/expand_macros_test.clj index 048d69f81ac..71084abc128 100644 --- a/test/metabase/query_processor/middleware/expand_macros_test.clj +++ b/test/metabase/query_processor/middleware/expand_macros_test.clj @@ -1,5 +1,5 @@ (ns metabase.query-processor.middleware.expand-macros-test - (:require [expectations :refer [expect]] + (:require [clojure.test :refer :all] [metabase.models.database :refer [Database]] [metabase.models.metric :refer [Metric]] [metabase.models.segment :refer [Segment]] @@ -7,252 +7,241 @@ [metabase.query-processor-test :as qp.test] [metabase.query-processor.middleware.expand-macros :as expand-macros] [metabase.test :as mt] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets] - [metabase.util :as u] - [toucan.util.test :as tt])) + [metabase.util :as u])) (defn- mbql-query [inner-query] {:database 1, :type :query, :query (merge {:source-table 1} inner-query)}) -;; no Segment or Metric should yield exact same query -(expect - (mbql-query - {:filter [:> [:field-id 4] 1] - :breakout [[:field-id 17]]}) - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:filter [:> [:field-id 4] 1] - :breakout [[:field-id 17]]}))) - -;; just segments -(expect - (mbql-query - {:filter [:and - [:= [:field-id 5] "abc"] - [:or - [:is-null [:field-id 7]] - [:> [:field-id 4] 1]]] - :breakout [[:field-id 17]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Segment [{segment-1-id :id} {:table_id table-id - :definition {:filter [:and [:= [:field-id 5] "abc"]]}}] - Segment [{segment-2-id :id} {:table_id table-id - :definition {:filter [:and [:is-null [:field-id 7]]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:filter [:and - [:segment segment-1-id] - [:or - [:segment segment-2-id] - [:> [:field-id 4] 1]]] - :breakout [[:field-id 17]]})))) - -;; just a metric (w/out nested segments) -(expect - (mbql-query - {:aggregation [[:aggregation-options [:count] {:display-name "Toucans in the rainforest"}]] - :filter [:and - [:> [:field-id 4] 1] - [:= [:field-id 5] "abc"]] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:name "Toucans in the rainforest" - :table_id table-id - :definition {:aggregation [[:count]] - :filter [:and [:= [:field-id 5] "abc"]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:aggregation [[:metric metric-1-id]] - :filter [:> [:field-id 4] 1] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]})))) - -;; check that when the original filter is empty we simply use our metric filter definition instead -(expect - (mbql-query - {:source-table 1000 - :aggregation [[:aggregation-options [:count] {:display-name "ABC Fields"}]] - :filter [:= [:field-id 5] "abc"] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:name "ABC Fields" - :table_id table-id - :definition {:aggregation [[:count]] - :filter [:and [:= [:field-id 5] "abc"]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:source-table 1000 - :aggregation [[:metric metric-1-id]] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]})))) - -;; metric w/ no filter definition -(expect - (mbql-query - {:aggregation [[:aggregation-options [:count] {:display-name "My Metric"}]] - :filter [:= [:field-id 5] "abc"] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Metric [{metric-1-id :id} {:name "My Metric" - :table_id table-id - :definition {:aggregation [[:count]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:aggregation [[:metric metric-1-id]] - :filter [:= [:field-id 5] "abc"] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]})))) - -;; a metric w/ nested segments -(expect - (mbql-query - {:source-table 1000 - :aggregation [[:aggregation-options [:sum [:field-id 18]] {:display-name "My Metric"}]] - :filter [:and - [:> [:field-id 4] 1] - [:is-null [:field-id 7]] - [:= [:field-id 5] "abc"] - [:between [:field-id 9] 0 25]] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Segment [{segment-1-id :id} {:table_id table-id - :definition {:filter [:and [:between [:field-id 9] 0 25]]}}] - Segment [{segment-2-id :id} {:table_id table-id - :definition {:filter [:and [:is-null [:field-id 7]]]}}] - Metric [{metric-1-id :id} {:name "My Metric" - :table_id table-id - :definition {:aggregation [[:sum [:field-id 18]]] - :filter [:and - [:= [:field-id 5] "abc"] - [:segment segment-1-id]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:source-table 1000 - :aggregation [[:metric metric-1-id]] - :filter [:and - [:> [:field-id 4] 1] - [:segment segment-2-id]] - :breakout [[:field-id 17]] - :order-by [[:asc [:field-id 1]]]})))) - -;; Check that a metric w/ multiple aggregation syntax (nested vector) still works correctly -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[2 118] - [3 39] - [4 24]] - (tt/with-temp Metric [metric {:table_id (data/id :venues) - :definition {:aggregation [[:sum [:field-id (data/id :venues :price)]]] - :filter [:> [:field-id (data/id :venues :price)] 1]}}] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:metric (u/get-id metric)]] - :breakout [$price]}))))) - -;; make sure that we don't try to expand GA "metrics" (#6104) -(expect - (mbql-query {:aggregation [[:metric "ga:users"]]}) - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:metric "ga:users"]]}))) - -(expect - (mbql-query {:aggregation [[:metric "gaid:users"]]}) - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:metric "gaid:users"]]}))) - -;; make sure expansion works with multiple GA "metrics" (#7399) -(expect - (mbql-query {:aggregation [[:metric "ga:users"] - [:metric "ga:1dayUsers"]]}) - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:metric "ga:users"] - [:metric "ga:1dayUsers"]]}))) - -;; make sure we don't try to expand GA "segments" -(expect - (mbql-query {:filter [:segment "gaid:-11"]}) - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:filter [:segment "gaid:-11"]}))) - -;; make sure we can name a :metric (ick) -(expect - (mbql-query - {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "Named Metric"}]] - :breakout [[:field-id 10]]}) - (tt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:display-name "Named Metric"}]] - :breakout [[:field-id 10]]})))) - -;; if the `:metric` is wrapped in aggregation options that do *not* give it a display name, `:display-name` should be -;; added to the options -(expect - (mbql-query - {:aggregation [[:aggregation-options - [:sum [:field-id 20]] - {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] - :breakout [[:field-id 10]]}) - (tt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] - :breakout [[:field-id 10]]})))) - -;; a Metric whose :aggregation is already named should not get wrapped in an `:aggregation-options` clause -(expect - (mbql-query - {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "My Cool Aggregation"}]] - :breakout [[:field-id 10]]}) - (tt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options - [:sum [:field-id 20]] - {:display-name "My Cool Aggregation"}]]}}] - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:metric (u/get-id metric)]] - :breakout [[:field-id 10]]})))) - -;; ...but if it's wrapped in `:aggregation-options`, but w/o given a display name, we should merge the options -(expect - (mbql-query - {:aggregation [[:aggregation-options - [:sum [:field-id 20]] - {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] - :breakout [[:field-id 10]]}) - (tt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options - [:sum [:field-id 20]] - {:name "auto_generated_name"}]]}}] - (#'expand-macros/expand-metrics-and-segments - (mbql-query {:aggregation [[:metric (u/get-id metric)]] - :breakout [[:field-id 10]]})))) - - -;; segments in :share clauses -(expect - (mbql-query - {:aggregation [[:share [:and - [:= [:field-id 5] "abc"] - [:or - [:is-null [:field-id 7]] - [:> [:field-id 4] 1]]]]]}) - (tt/with-temp* [Database [{database-id :id}] - Table [{table-id :id} {:db_id database-id}] - Segment [{segment-1-id :id} {:table_id table-id - :definition {:filter [:and [:= [:field-id 5] "abc"]]}}] - Segment [{segment-2-id :id} {:table_id table-id - :definition {:filter [:and [:is-null [:field-id 7]]]}}]] - (#'expand-macros/expand-metrics-and-segments - (mbql-query - {:aggregation [[:share [:and - [:segment segment-1-id] - [:or - [:segment segment-2-id] - [:> [:field-id 4] 1]]]]]})))) +(deftest basic-expansion-test + (testing "no Segment or Metric should yield exact same query" + (is (= (mbql-query + {:filter [:> [:field-id 4] 1] + :breakout [[:field-id 17]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:filter [:> [:field-id 4] 1] + :breakout [[:field-id 17]]})))))) + +(deftest segments-test + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Segment [{segment-1-id :id} {:table_id table-id + :definition {:filter [:and [:= [:field-id 5] "abc"]]}}] + Segment [{segment-2-id :id} {:table_id table-id + :definition {:filter [:and [:is-null [:field-id 7]]]}}]] + (is (= (mbql-query + {:filter [:and + [:= [:field-id 5] "abc"] + [:or + [:is-null [:field-id 7]] + [:> [:field-id 4] 1]]] + :breakout [[:field-id 17]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:filter [:and + [:segment segment-1-id] + [:or + [:segment segment-2-id] + [:> [:field-id 4] 1]]] + :breakout [[:field-id 17]]})))))) + +(deftest metric-test + (testing "just a metric (w/out nested segments)" + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Metric [{metric-1-id :id} {:name "Toucans in the rainforest" + :table_id table-id + :definition {:aggregation [[:count]] + :filter [:and [:= [:field-id 5] "abc"]]}}]] + (is (= (mbql-query + {:aggregation [[:aggregation-options [:count] {:display-name "Toucans in the rainforest"}]] + :filter [:and + [:> [:field-id 4] 1] + [:= [:field-id 5] "abc"]] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:aggregation [[:metric metric-1-id]] + :filter [:> [:field-id 4] 1] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}))))))) + +(deftest use-metric-filter-definition-test + (testing "check that when the original filter is empty we simply use our metric filter definition instead" + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Metric [{metric-1-id :id} {:name "ABC Fields" + :table_id table-id + :definition {:aggregation [[:count]] + :filter [:and [:= [:field-id 5] "abc"]]}}]] + (is (= (mbql-query + {:source-table 1000 + :aggregation [[:aggregation-options [:count] {:display-name "ABC Fields"}]] + :filter [:= [:field-id 5] "abc"] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:source-table 1000 + :aggregation [[:metric metric-1-id]] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}))))))) + +(deftest metric-with-no-filter-test + (testing "metric w/ no filter definition" + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Metric [{metric-1-id :id} {:name "My Metric" + :table_id table-id + :definition {:aggregation [[:count]]}}]] + (is (= (mbql-query + {:aggregation [[:aggregation-options [:count] {:display-name "My Metric"}]] + :filter [:= [:field-id 5] "abc"] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:aggregation [[:metric metric-1-id]] + :filter [:= [:field-id 5] "abc"] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}))))))) + +(deftest metric-with-nested-segments-test + (testing "metric w/ nested segments" + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Segment [{segment-1-id :id} {:table_id table-id + :definition {:filter [:and [:between [:field-id 9] 0 25]]}}] + Segment [{segment-2-id :id} {:table_id table-id + :definition {:filter [:and [:is-null [:field-id 7]]]}}] + Metric [{metric-1-id :id} {:name "My Metric" + :table_id table-id + :definition {:aggregation [[:sum [:field-id 18]]] + :filter [:and + [:= [:field-id 5] "abc"] + [:segment segment-1-id]]}}]] + (is (= (mbql-query + {:source-table 1000 + :aggregation [[:aggregation-options [:sum [:field-id 18]] {:display-name "My Metric"}]] + :filter [:and + [:> [:field-id 4] 1] + [:is-null [:field-id 7]] + [:= [:field-id 5] "abc"] + [:between [:field-id 9] 0 25]] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:source-table 1000 + :aggregation [[:metric metric-1-id]] + :filter [:and + [:> [:field-id 4] 1] + [:segment segment-2-id]] + :breakout [[:field-id 17]] + :order-by [[:asc [:field-id 1]]]}))))))) + +(deftest metric-with-multiple-aggregation-syntax-test + (testing "Check that a metric w/ multiple aggregation syntax (nested vector) still works correctly" + ;; so-called "multiple aggregation syntax" is the norm now -- query normalization will do this automatically + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (mt/with-temp Metric [metric {:table_id (mt/id :venues) + :definition {:aggregation [[:sum [:field-id (mt/id :venues :price)]]] + :filter [:> [:field-id (mt/id :venues :price)] 1]}}] + (is (= [[2 118] + [3 39] + [4 24]] + (qp.test/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:metric (u/the-id metric)]] + :breakout [$price]})))))))) + +(deftest dont-expand-ga-metrics-test + (testing "make sure that we don't try to expand GA \"metrics\" (#6104)" + (doseq [metric ["ga:users" "gaid:users"]] + (is (= (mbql-query {:aggregation [[:metric metric]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric metric]]})))))) + + (testing "make sure expansion works with multiple GA \"metrics\" (#7399)" + (is (= (mbql-query {:aggregation [[:metric "ga:users"] + [:metric "ga:1dayUsers"]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric "ga:users"] + [:metric "ga:1dayUsers"]]})))))) + +(deftest dont-expand-ga-segments-test + (testing "make sure we don't try to expand GA 'segments'" + (is (= (mbql-query {:filter [:segment "gaid:-11"]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:filter [:segment "gaid:-11"]})))))) + +(deftest named-metrics-test + (testing "make sure we can name a :metric" + (mt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] + (is (= (mbql-query + {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "Named Metric"}]] + :breakout [[:field-id 10]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:aggregation-options [:metric (u/the-id metric)] {:display-name "Named Metric"}]] + :breakout [[:field-id 10]]}))))))) + +(deftest include-display-name-test + (testing (str "if the `:metric` is wrapped in aggregation options that do *not* give it a display name, " + "`:display-name` should be added to the options") + (mt/with-temp Metric [metric {:definition {:aggregation [[:sum [:field-id 20]]]}}] + (is (= (mbql-query + {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] + :breakout [[:field-id 10]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:aggregation-options [:metric (u/the-id metric)] {:name "auto_generated_name"}]] + :breakout [[:field-id 10]]})))))) + + (testing "a Metric whose :aggregation is already named should not get wrapped in an `:aggregation-options` clause" + (mt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:display-name "My Cool Aggregation"}]]}}] + (is (= (mbql-query + {:aggregation [[:aggregation-options [:sum [:field-id 20]] {:display-name "My Cool Aggregation"}]] + :breakout [[:field-id 10]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric (u/the-id metric)]] + :breakout [[:field-id 10]]})))))) + + (testing "...but if it's wrapped in `:aggregation-options`, but w/o given a display name, we should merge the options" + (mt/with-temp Metric [metric {:definition {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name"}]]}}] + (is (= (mbql-query + {:aggregation [[:aggregation-options + [:sum [:field-id 20]] + {:name "auto_generated_name", :display-name "Toucans in the rainforest"}]] + :breakout [[:field-id 10]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query {:aggregation [[:metric (u/the-id metric)]] + :breakout [[:field-id 10]]}))))))) + +(deftest segments-in-share-clauses-test + (testing "segments in :share clauses" + (mt/with-temp* [Database [{database-id :id}] + Table [{table-id :id} {:db_id database-id}] + Segment [{segment-1-id :id} {:table_id table-id + :definition {:filter [:and [:= [:field-id 5] "abc"]]}}] + Segment [{segment-2-id :id} {:table_id table-id + :definition {:filter [:and [:is-null [:field-id 7]]]}}]] + (is (= (mbql-query + {:aggregation [[:share [:and + [:= [:field-id 5] "abc"] + [:or + [:is-null [:field-id 7]] + [:> [:field-id 4] 1]]]]]}) + (#'expand-macros/expand-metrics-and-segments + (mbql-query + {:aggregation [[:share [:and + [:segment segment-1-id] + [:or + [:segment segment-2-id] + [:> [:field-id 4] 1]]]]]}))))))) diff --git a/test/metabase/query_processor_test/count_where_test.clj b/test/metabase/query_processor_test/count_where_test.clj index a908eb2d647..1f4334a0229 100644 --- a/test/metabase/query_processor_test/count_where_test.clj +++ b/test/metabase/query_processor_test/count_where_test.clj @@ -1,88 +1,97 @@ (ns metabase.query-processor-test.count-where-test - (:require [metabase.models.metric :refer [Metric]] + (:require [clojure.test :refer :all] + [metabase.models.metric :refer [Metric]] [metabase.models.segment :refer [Segment]] - [metabase.query-processor-test :refer :all] - [metabase.test :as mt] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets] - [metabase.test.util :as tu] - [toucan.util.test :as tt])) + [metabase.test :as mt])) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 94 - (->> {:aggregation [[:count-where [:< [:field-id (data/id :venues :price)] 4]]]} - (data/run-mbql-query venues) - rows - ffirst - long)) +(deftest basic-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= 94 + (->> {:aggregation [[:count-where [:< [:field-id (mt/id :venues :price)] 4]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long))) + (testing "normalization" + (is (= 94 + (->> {:aggregation [["count-where" ["<" ["field-id" (mt/id :venues :price)] 4]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long)))))) -;; Test normalization -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 94 - (->> {:aggregation [["count-where" ["<" ["field-id" (data/id :venues :price)] 4]]]} - (data/run-mbql-query venues) - rows - ffirst - long)) +(deftest compound-condition-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= 17 + (->> {:aggregation [[:count-where + [:and + [:< [:field-id (mt/id :venues :price)] 4] + [:or + [:starts-with [:field-id (mt/id :venues :name)] "M"] + [:ends-with [:field-id (mt/id :venues :name)] "t"]]]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 17 - (->> {:aggregation [[:count-where [:and [:< [:field-id (data/id :venues :price)] 4] - [:or [:starts-with [:field-id (data/id :venues :name)] "M"] - [:ends-with [:field-id (data/id :venues :name)] "t"]]]]]} - (data/run-mbql-query venues) - rows - ffirst - long)) +(deftest filter-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= nil + (->> {:aggregation [[:count-where [:< [:field-id (mt/id :venues :price)] 4]]] + :filter [:> [:field-id (mt/id :venues :price)] Long/MAX_VALUE]} + (mt/run-mbql-query venues) + mt/rows + ffirst))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - nil - (->> {:aggregation [[:count-where [:< [:field-id (data/id :venues :price)] 4]]] - :filter [:> [:field-id (data/id :venues :price)] Long/MAX_VALUE]} - (data/run-mbql-query venues) - rows - ffirst)) +(deftest breakout-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= [[2 0] + [3 0] + [4 1] + [5 1]] + (->> {:aggregation [[:count-where [:< [:field-id (mt/id :venues :price)] 2]]] + :breakout [[:field-id (mt/id :venues :category_id)]] + :limit 4} + (mt/run-mbql-query venues) + (mt/round-all-decimals 2) + mt/rows + (map (fn [[k v]] + [(long k) (long v)]))))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - [[2 0] - [3 0] - [4 1] - [5 1]] - (->> {:aggregation [[:count-where [:< [:field-id (data/id :venues :price)] 2]]] - :breakout [[:field-id (data/id :venues :category_id)]] - :limit 4} - (data/run-mbql-query venues) - (tu/round-all-decimals 2) - rows - (map (fn [[k v]] - [(long k) (long v)])))) +(deftest count-where-inside-expression-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations :expressions) + (is (= 48 + (->> {:aggregation [[:+ + [:/ + [:count-where [:< [:field-id (mt/id :venues :price)] 4]] + 2] + 1]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations :expressions) - 48 - (->> {:aggregation [[:+ [:/ [:count-where [:< [:field-id (data/id :venues :price)] 4]] 2] 1]]} - (data/run-mbql-query venues) - rows - ffirst - long)) +(deftest segment-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (mt/with-temp Segment [{segment-id :id} {:table_id (mt/id :venues) + :definition {:source-table (mt/id :venues) + :filter [:< [:field-id (mt/id :venues :price)] 4]}}] + (is (= 94 + (->> {:aggregation [[:count-where [:segment segment-id]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long)))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 94 - (tt/with-temp* [Segment [{segment-id :id} {:table_id (data/id :venues) - :definition {:source-table (data/id :venues) - :filter [:< [:field-id (data/id :venues :price)] 4]}}]] - (->> {:aggregation [[:count-where [:segment segment-id]]]} - (data/run-mbql-query venues) - rows - ffirst - long))) - -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 94 - (tt/with-temp* [Metric [{metric-id :id} {:table_id (data/id :venues) - :definition {:source-table (data/id :venues) - :aggregation [:count-where [:< [:field-id (data/id :venues :price)] 4]]}}]] - (->> {:aggregation [[:metric metric-id]]} - (data/run-mbql-query venues) - rows - ffirst - long))) +(deftest metric-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (mt/with-temp Metric [{metric-id :id} {:table_id (mt/id :venues) + :definition {:source-table (mt/id :venues) + :aggregation [:count-where + [:< [:field-id (mt/id :venues :price)] 4]]}}] + (is (= 94 + (->> {:aggregation [[:metric metric-id]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + long)))))) diff --git a/test/metabase/query_processor_test/expression_aggregations_test.clj b/test/metabase/query_processor_test/expression_aggregations_test.clj index 81740c8416f..58603fc1c9c 100644 --- a/test/metabase/query_processor_test/expression_aggregations_test.clj +++ b/test/metabase/query_processor_test/expression_aggregations_test.clj @@ -1,281 +1,276 @@ (ns metabase.query-processor-test.expression-aggregations-test "Tests for expression aggregations and for named aggregations." - (:require [metabase.driver :as driver] + (:require [clojure.test :refer :all] + [metabase.driver :as driver] [metabase.models.metric :refer [Metric]] [metabase.query-processor-test :as qp.test] [metabase.test :as mt] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets] - [metabase.util :as u] - [toucan.util.test :as tt])) + [metabase.util :as u])) -;; sum, * -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 1211] - [2 5710] - [3 1845] - [4 1476]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:sum [:* $id $price]]] - :breakout [$price]})))) +(deftest sum-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "sum, *" + (is (= [[1 1211] + [2 5710] + [3 1845] + [4 1476]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:sum [:* $id $price]]] + :breakout [$price]}))))))) -;; min, + -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 10] - [2 4] - [3 4] - [4 20]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:min [:+ $id $price]]] - :breakout [$price]})))) +(deftest min-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "min, +" + (is (= [[1 10] + [2 4] + [3 4] + [4 20]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:min [:+ $id $price]]] + :breakout [$price]}))))))) -;; max, / -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 94] - [2 50] - [3 26] - [4 20]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:max [:/ $id $price]]] - :breakout [$price]})))) +(deftest max-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "max, /" + (is (= [[1 94] + [2 50] + [3 26] + [4 20]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:max [:/ $id $price]]] + :breakout [$price]}))))))) -;; avg, - -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - (if (= driver/*driver* :h2) - [[1 55] - [2 97] - [3 142] - [4 246]] - [[1 55] - [2 96] - [3 141] - [4 246]]) - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:avg [:* $id $price]]] - :breakout [$price]})))) +(deftest avg-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "avg, -" + (is (= (if (= driver/*driver* :h2) + [[1 55] + [2 97] + [3 142] + [4 246]] + [[1 55] + [2 96] + [3 141] + [4 246]]) + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:avg [:* $id $price]]] + :breakout [$price]}))))))) -;; post-aggregation math w/ 2 args: count + sum -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 44] - [2 177] - [3 52] - [4 30]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ - [:count $id] - [:sum $price]]] - :breakout [$price]})))) +(deftest post-aggregation-math-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "post-aggregation math" + (testing "w/ 2 args: count + sum" + (is (= [[1 44] + [2 177] + [3 52] + [4 30]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ + [:count $id] + [:sum $price]]] + :breakout [$price]}))))) -;; post-aggregation math w/ 3 args: count + sum + count -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 66] - [2 236] - [3 65] - [4 36]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ [:count $id] [:sum $price] [:count $price]]] - :breakout [$price]})))) + (testing "w/ 3 args: count + sum + count" + (is (= [[1 66] + [2 236] + [3 65] + [4 36]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ [:count $id] [:sum $price] [:count $price]]] + :breakout [$price]}))))) -;; post-aggregation math w/ a constant: count * 10 -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 220] - [2 590] - [3 130] - [4 60]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:* [:count $id] 10]] - :breakout [$price]})))) + (testing "w/ a constant: count * 10" + (is (= [[1 220] + [2 590] + [3 130] + [4 60]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:* [:count $id] 10]] + :breakout [$price]}))))) -;; nested post-aggregation math: count + (count * sum) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 506] - [2 7021] - [3 520] - [4 150]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ - [:count $id] - [:* [:count $id] [:sum $price]]]] - :breakout [$price]})))) + (testing "w/ avg: count + avg" + (is (= (if (= driver/*driver* :h2) + [[1 77] + [2 107] + [3 60] + [4 68]] + [[1 77] + [2 107] + [3 60] + [4 67]]) + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ [:count $id] [:avg $id]]] + :breakout [$price]})))))))) -;; post-aggregation math w/ avg: count + avg -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - (if (= driver/*driver* :h2) - [[1 77] - [2 107] - [3 60] - [4 68]] - [[1 77] - [2 107] - [3 60] - [4 67]]) - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ [:count $id] [:avg $id]]] - :breakout [$price]})))) +(deftest nested-post-aggregation-mat-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "nested post-aggregation math: count + (count * sum)" + (is (= [[1 506] + [2 7021] + [3 520] + [4 150]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ + [:count $id] + [:* [:count $id] [:sum $price]]]] + :breakout [$price]}))))))) -;; post aggregation math + math inside aggregations: max(venue_price) + min(venue_price - id) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 -92] - [2 -96] - [3 -74] - [4 -73]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ [:max $price] [:min [:- $price $id]]]] - :breakout [$price]})))) +(deftest math-inside-aggregations-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "post aggregation math + math inside aggregations: max(venue_price) + min(venue_price - id)" + (is (= [[1 -92] + [2 -96] + [3 -74] + [4 -73]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ [:max $price] [:min [:- $price $id]]]] + :breakout [$price]}))))))) -;; aggregation w/o field -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 23] - [2 60] - [3 14] - [4 7]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:+ 1 [:count]]] - :breakout [$price]})))) +(deftest aggregation-without-field-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "aggregation w/o field" + (is (= [[1 23] + [2 60] + [3 14] + [4 7]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:+ 1 [:count]]] + :breakout [$price]}))))))) -;; Sorting by an un-named aggregate expression -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 2] [2 2] [12 2] [4 4] [7 4] [10 4] [11 4] [8 8]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query users - {:aggregation [[:* [:count] 2]] - :breakout [[:datetime-field $last_login :month-of-year]] - :order-by [[:asc [:aggregation 0]]]})))) +(deftest sort-by-unnamed-aggregate-expression-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "Sorting by an un-named aggregate expression" + (is (= [[1 2] [2 2] [12 2] [4 4] [7 4] [10 4] [11 4] [8 8]] + (mt/formatted-rows [int int] + (mt/run-mbql-query users + {:aggregation [[:* [:count] 2]] + :breakout [[:datetime-field $last_login :month-of-year]] + :order-by [[:asc [:aggregation 0]]]}))))))) -;; aggregation with math inside the aggregation :scream_cat: -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1 44] - [2 177] - [3 52] - [4 30]] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:sum [:+ $price 1]]] - :breakout [$price]})))) +(deftest math-inside-the-aggregation-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "aggregation with math inside the aggregation :scream_cat:" + (is (= [[1 44] + [2 177] + [3 52] + [4 30]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [[:sum [:+ $price 1]]] + :breakout [$price]}))))))) -;; check that we can name an expression aggregation w/ aggregation at top-level -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - {:rows [[1 44] - [2 177] - [3 52] - [4 30]] - :columns [(data/format-name "price") - "sum_of_price"]} - (qp.test/format-rows-by [int int] - (qp.test/rows+column-names - (data/run-mbql-query venues - {:aggregation [[:aggregation-options [:sum [:+ $price 1]] {:name "sum_of_price"}]] - :breakout [$price]})))) +(deftest named-expression-aggregation-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "check that we can name an expression aggregation w/ aggregation at top-level" + (is (= {:rows [[1 44] + [2 177] + [3 52] + [4 30]] + :columns [(mt/format-name "price") + "sum_of_price"]} + (mt/format-rows-by [int int] + (mt/rows+column-names + (mt/run-mbql-query venues + {:aggregation [[:aggregation-options [:sum [:+ $price 1]] {:name "sum_of_price"}]] + :breakout [$price]})))))) -;; check that we can name an expression aggregation w/ expression at top-level -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - {:rows [[1 -19] - [2 77] - [3 -2] - [4 -17]] - :columns [(data/format-name "price") - "sum_41"]} - (qp.test/format-rows-by [int int] - (qp.test/rows+column-names - (data/run-mbql-query venues - {:aggregation [[:aggregation-options [:- [:sum $price] 41] {:name "sum_41"}]] - :breakout [$price]})))) + (testing "check that we can name an expression aggregation w/ expression at top-level" + (is (= {:rows [[1 -19] + [2 77] + [3 -2] + [4 -17]] + :columns [(mt/format-name "price") + "sum_41"]} + (mt/format-rows-by [int int] + (mt/rows+column-names + (mt/run-mbql-query venues + {:aggregation [[:aggregation-options [:- [:sum $price] 41] {:name "sum_41"}]] + :breakout [$price]})))))))) -;; check that we can handle Metrics inside expression aggregation clauses -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[2 119] - [3 40] - [4 25]] - (tt/with-temp Metric [metric {:table_id (data/id :venues) - :definition {:aggregation [:sum [:field-id (data/id :venues :price)]] - :filter [:> [:field-id (data/id :venues :price)] 1]}}] - (qp.test/format-rows-by [int int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [:+ [:metric (u/get-id metric)] 1] - :breakout [[:field-id $price]]}))))) +(deftest metrics-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "check that we can handle Metrics inside expression aggregation clauses" + (mt/with-temp Metric [metric {:table_id (mt/id :venues) + :definition {:aggregation [:sum [:field-id (mt/id :venues :price)]] + :filter [:> [:field-id (mt/id :venues :price)] 1]}}] + (is (= [[2 119] + [3 40] + [4 25]] + (mt/formatted-rows [int int] + (mt/run-mbql-query venues + {:aggregation [:+ [:metric (u/the-id metric)] 1] + :breakout [[:field-id $price]]})))))) -;; check that we can handle Metrics inside an `:aggregation-options` clause -;; TODO - Pretty sure this test doesn't belong here (?) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - {:rows [[2 118] - [3 39] - [4 24]] - :columns [(data/format-name "price") - "auto_generated_name"]} - (tt/with-temp Metric [metric {:table_id (data/id :venues) - :definition {:aggregation [:sum [:field-id (data/id :venues :price)]] - :filter [:> [:field-id (data/id :venues :price)] 1]}}] - (qp.test/format-rows-by [int int] - (qp.test/rows+column-names - (data/run-mbql-query venues - {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] - :breakout [[:field-id $price]]}))))) + (testing "check that we can handle Metrics inside an `:aggregation-options` clause" + (mt/with-temp Metric [metric {:table_id (mt/id :venues) + :definition {:aggregation [:sum [:field-id (mt/id :venues :price)]] + :filter [:> [:field-id (mt/id :venues :price)] 1]}}] + (is (= {:rows [[2 118] + [3 39] + [4 24]] + :columns [(mt/format-name "price") + "auto_generated_name"]} + (mt/format-rows-by [int int] + (mt/rows+column-names + (mt/run-mbql-query venues + {:aggregation [[:aggregation-options [:metric (u/the-id metric)] {:name "auto_generated_name"}]] + :breakout [[:field-id $price]]}))))))) -;; check that Metrics with a nested aggregation still work inside an `:aggregation-options` clause -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - {:rows [[2 118] - [3 39] - [4 24]] - :columns [(data/format-name "price") - "auto_generated_name"]} - (tt/with-temp Metric [metric (data/$ids venues - {:table_id $$venues - :definition {:aggregation [[:sum $price]] - :filter [:> $price 1]}})] - (qp.test/format-rows-by [int int] - (qp.test/rows+column-names - (data/run-mbql-query venues - {:aggregation [[:aggregation-options [:metric (u/get-id metric)] {:name "auto_generated_name"}]] - :breakout [[:field-id $price]]}))))) + (testing "check that Metrics with a nested aggregation still work inside an `:aggregation-options` clause" + (mt/with-temp Metric [metric (mt/$ids venues + {:table_id $$venues + :definition {:aggregation [[:sum $price]] + :filter [:> $price 1]}})] + (is (= {:rows [[2 118] + [3 39] + [4 24]] + :columns [(mt/format-name "price") + "auto_generated_name"]} + (mt/format-rows-by [int int] + (mt/rows+column-names + (mt/run-mbql-query venues + {:aggregation [[:aggregation-options [:metric (u/the-id metric)] {:name "auto_generated_name"}]] + :breakout [[:field-id $price]]}))))))))) -;; check that named aggregations come back with the correct column metadata (#4002) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - (assoc (qp.test/aggregate-col :count) - :name "auto_generated_name" - :display_name "Count of Things") - (-> (data/run-mbql-query venues - {:aggregation [[:aggregation-options ["COUNT"] {:name "auto_generated_name", :display-name "Count of Things"}]]}) - qp.test/cols - first)) +(deftest named-aggregations-metadata-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "check that named aggregations come back with the correct column metadata (#4002)" + (is (= (assoc (qp.test/aggregate-col :count) + :name "auto_generated_name" + :display_name "Count of Things") + (-> (mt/run-mbql-query venues + {:aggregation [[:aggregation-options ["COUNT"] + {:name "auto_generated_name" + :display-name "Count of Things"}]]}) + mt/cols + first)))))) -;; check that we can use cumlative count in expression aggregations -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[1000]] - (qp.test/format-rows-by [int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [["*" ["cum_count"] 10]]})))) +(deftest cumulative-count-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "check that we can use cumlative count in expression aggregations" + (is (= [[1000]] + (mt/format-rows-by [int] + (mt/rows + (mt/run-mbql-query venues + {:aggregation [["*" ["cum_count"] 10]]})))))))) -;; can we use named expressions inside expression aggregations? -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :expression-aggregations) - [[406]] - (qp.test/format-rows-by [int] - (qp.test/rows - (data/run-mbql-query venues - {:aggregation [[:sum [:expression "double-price"]]] - :expressions {"double-price" [:* $price 2]}})))) +(deftest named-expressions-inside-expression-aggregations-test + (mt/test-drivers (mt/normal-drivers-with-feature :expression-aggregations) + (testing "can we use named expressions inside expression aggregations?" + (is (= [[406]] + (mt/format-rows-by [int] + (mt/rows + (mt/run-mbql-query venues + {:aggregation [[:sum [:expression "double-price"]]] + :expressions {"double-price" [:* $price 2]}})))))))) diff --git a/test/metabase/query_processor_test/implicit_joins_test.clj b/test/metabase/query_processor_test/implicit_joins_test.clj index a5edbc997b7..b7e8bd57821 100644 --- a/test/metabase/query_processor_test/implicit_joins_test.clj +++ b/test/metabase/query_processor_test/implicit_joins_test.clj @@ -1,151 +1,139 @@ (ns metabase.query-processor-test.implicit-joins-test - "Test for JOIN behavior." + "Tests for joins that are created automatically when an `:fk->` column is present." (:require [clojure.test :refer :all] - [expectations :refer [expect]] [metabase.driver :as driver] - [metabase.query-processor-test :as qp.test] - [metabase.test :as mt] - [metabase.test.data.datasets :as datasets])) + [metabase.test :as mt])) -;; The top 10 cities by number of Tupac sightings -;; Test that we can breakout on an FK field (Note how the FK Field is returned in the results) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :foreign-keys) - [["Arlington" 16] - ["Albany" 15] - ["Portland" 14] - ["Louisville" 13] - ["Philadelphia" 13] - ["Anchorage" 12] - ["Lincoln" 12] - ["Houston" 11] - ["Irvine" 11] - ["Lakeland" 11]] - (->> (mt/dataset tupac-sightings - (mt/run-mbql-query sightings - {:aggregation [[:count]] - :breakout [$city_id->cities.name] - :order-by [[:desc [:aggregation 0]]] - :limit 10})) - qp.test/rows - (qp.test/format-rows-by [str int]))) - - -;; Number of Tupac sightings in the Expa office -;; (he was spotted here 60 times) -;; Test that we can filter on an FK field -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :foreign-keys) - [[60]] - (->> (mt/dataset tupac-sightings - (mt/run-mbql-query sightings - {:aggregation [[:count]] - :filter [:= $category_id->categories.id 8]})) - qp.test/rows - (qp.test/format-rows-by [int]))) - - -;; THE 10 MOST RECENT TUPAC SIGHTINGS (!) -;; (What he was doing when we saw him, sighting ID) -;; Check that we can include an FK field in the :fields clause -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :foreign-keys) - [[772 "In the Park"] - [894 "Working at a Pet Store"] - [684 "At the Airport"] - [199 "At a Restaurant"] - [33 "Working as a Limo Driver"] - [902 "At Starbucks"] - [927 "On TV"] - [996 "At a Restaurant"] - [897 "Wearing a Biggie Shirt"] - [499 "In the Expa Office"]] - (->> (mt/dataset tupac-sightings - (mt/run-mbql-query sightings - {:fields [$id $category_id->categories.name] - :order-by [[:desc $timestamp]] - :limit 10})) - qp.test/rows - (qp.test/format-rows-by [int str]))) +(deftest breakout-on-fk-field-test + (mt/test-drivers (mt/normal-drivers-with-feature :foreign-keys) + (testing "Test that we can breakout on an FK field (Note how the FK Field is returned in the results)" + (mt/dataset tupac-sightings + ;; The top 10 cities by number of Tupac sightings + (is (= [["Arlington" 16] + ["Albany" 15] + ["Portland" 14] + ["Louisville" 13] + ["Philadelphia" 13] + ["Anchorage" 12] + ["Lincoln" 12] + ["Houston" 11] + ["Irvine" 11] + ["Lakeland" 11]] + (mt/formatted-rows [str int] + (mt/run-mbql-query sightings + {:aggregation [[:count]] + :breakout [$city_id->cities.name] + :order-by [[:desc [:aggregation 0]]] + :limit 10})))))))) +(deftest filter-by-fk-field-test + (mt/test-drivers (mt/normal-drivers-with-feature :foreign-keys) + (testing "Test that we can filter on an FK field" + (mt/dataset tupac-sightings + ;; Number of Tupac sightings in the Expa office (he was spotted here 60 times) + (is (= [[60]] + (mt/formatted-rows [int] + (mt/run-mbql-query sightings + {:aggregation [[:count]] + :filter [:= $category_id->categories.id 8]})))))))) -;; 1. Check that we can order by Foreign Keys -;; (this query targets sightings and orders by cities.name and categories.name) -;; 2. Check that we can join MULTIPLE tables in a single query -;; (this query joins both cities and categories) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :foreign-keys) - ;; ID, CITY_ID, CATEGORY_ID - ;; Cities are already alphabetized in the source data which is why CITY_ID is sorted - [[6 1 12] - [355 1 11] - [596 1 11] - [379 1 13] - [413 1 5] - [426 1 1] - [67 2 11] - [524 2 11] - [77 2 13] - [202 2 13]] - (->> (mt/dataset tupac-sightings - (mt/run-mbql-query sightings - {:order-by [[:asc $city_id->cities.name] - [:desc $category_id->categories.name] - [:asc $id]] - :limit 10})) - ;; drop timestamps. - qp.test/rows - (map butlast) - (qp.test/format-rows-by [int int int]))) +(deftest fk-field-in-fields-test + (mt/test-drivers (mt/normal-drivers-with-feature :foreign-keys) + (testing "Check that we can include an FK field in `:fields`" + (mt/dataset tupac-sightings + ;; THE 10 MOST RECENT TUPAC SIGHTINGS (!) (What he was doing when we saw him, sighting ID) + (is (= [[772 "In the Park"] + [894 "Working at a Pet Store"] + [684 "At the Airport"] + [199 "At a Restaurant"] + [33 "Working as a Limo Driver"] + [902 "At Starbucks"] + [927 "On TV"] + [996 "At a Restaurant"] + [897 "Wearing a Biggie Shirt"] + [499 "In the Expa Office"]] + (mt/formatted-rows [int str] + (mt/run-mbql-query sightings + {:fields [$id $category_id->categories.name] + :order-by [[:desc $timestamp]] + :limit 10})))))))) +(deftest join-multiple-tables-test + (mt/test-drivers (mt/normal-drivers-with-feature :foreign-keys) + (testing (str "1. Check that we can order by Foreign Keys (this query targets sightings and orders by cities.name " + "and categories.name)" + "\n" + "2. Check that we can join MULTIPLE tables in a single query (this query joins both cities and " + "categories)") + (mt/dataset tupac-sightings + ;; ID, CITY_ID, CATEGORY_ID + ;; Cities are already alphabetized in the source data which is why CITY_ID is sorted + (is (= [[6 1 12] + [355 1 11] + [596 1 11] + [379 1 13] + [413 1 5] + [426 1 1] + [67 2 11] + [524 2 11] + [77 2 13] + [202 2 13]] + (->> (mt/run-mbql-query sightings + {:order-by [[:asc $city_id->cities.name] + [:desc $category_id->categories.name] + [:asc $id]] + :limit 10}) + ;; drop timestamps. + mt/rows + (map butlast) + (mt/format-rows-by [int int int])))))))) -;; Check that trying to use a Foreign Key fails for Mongo and other DBs (deftest feature-check-test (mt/test-drivers (mt/normal-drivers-without-feature :foreign-keys) - (is - (thrown-with-msg? - clojure.lang.ExceptionInfo - (re-pattern (format "%s driver does not support foreign keys" driver/*driver*)) - (mt/dataset tupac-sightings - (mt/run-mbql-query sightings - {:order-by [[:asc $city_id->cities.name] - [:desc $category_id->categories.name] - [:asc $id]] - :limit 10})))))) - -;; Implicit joins should come back with `:fk->` field refs -(expect - (mt/$ids venues $category_id->categories.name) - (-> (mt/cols - (mt/run-mbql-query :venues - {:fields [$category_id->categories.name] - :order-by [[:asc $id]] - :limit 1})) - first - :field_ref)) - + (testing "Check that trying to use a Foreign Key fails for Mongo and other DBs" + (is + (thrown-with-msg? + clojure.lang.ExceptionInfo + (re-pattern (format "%s driver does not support foreign keys" driver/*driver*)) + (mt/dataset tupac-sightings + (mt/run-mbql-query sightings + {:order-by [[:asc $city_id->cities.name] + [:desc $category_id->categories.name] + [:asc $id]] + :limit 10}))))))) -;;; +----------------------------------------------------------------------------------------------------------------+ -;;; | MULTIPLE JOINS | -;;; +----------------------------------------------------------------------------------------------------------------+ +(deftest field-refs-test + (testing "Implicit joins should come back with `:fk->` field refs" + (is (= (mt/$ids venues $category_id->categories.name) + (-> (mt/cols + (mt/run-mbql-query :venues + {:fields [$category_id->categories.name] + :order-by [[:asc $id]] + :limit 1})) + first + :field_ref))))) -;;; CAN WE JOIN AGAINST THE SAME TABLE TWICE (MULTIPLE FKS TO A SINGLE TABLE!?) -;; Query should look something like: -;; SELECT USERS__via__SENDER_ID.NAME AS NAME, count(*) AS count -;; FROM PUBLIC.MESSAGES -;; LEFT JOIN PUBLIC.USERS USERS__via__RECIEVER_ID -;; ON PUBLIC.MESSAGES.RECIEVER_ID = USERS__via__RECIEVER_ID.ID -;; LEFT JOIN PUBLIC.USERS USERS__via__SENDER_ID -;; ON PUBLIC.MESSAGES.SENDER_ID = USERS__via__SENDER_ID.ID -;; WHERE USERS__via__RECIEVER_ID.NAME = 'Rasta Toucan' -;; GROUP BY USERS__via__SENDER_ID.NAME -;; ORDER BY USERS__via__SENDER_ID.NAME ASC -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :foreign-keys) - [["Bob the Sea Gull" 2] - ["Brenda Blackbird" 2] - ["Lucky Pigeon" 2] - ["Peter Pelican" 5] - ["Ronald Raven" 1]] - (mt/dataset avian-singles - (qp.test/format-rows-by [str int] - (qp.test/rows - (mt/run-mbql-query messages - {:aggregation [[:count]] - :breakout [$sender_id->users.name] - :filter [:= $receiver_id->users.name "Rasta Toucan"]}))))) +(deftest multiple-joins-test + (mt/test-drivers (mt/normal-drivers-with-feature :foreign-keys) + (testing "Can we join against the same table twice (multiple fks to a single table?)" + (mt/dataset avian-singles + ;; Query should look something like: + ;; SELECT USERS__via__SENDER_ID.NAME AS NAME, count(*) AS count + ;; FROM PUBLIC.MESSAGES + ;; LEFT JOIN PUBLIC.USERS USERS__via__RECIEVER_ID + ;; ON PUBLIC.MESSAGES.RECIEVER_ID = USERS__via__RECIEVER_ID.ID + ;; LEFT JOIN PUBLIC.USERS USERS__via__SENDER_ID + ;; ON PUBLIC.MESSAGES.SENDER_ID = USERS__via__SENDER_ID.ID + ;; WHERE USERS__via__RECIEVER_ID.NAME = 'Rasta Toucan' + ;; GROUP BY USERS__via__SENDER_ID.NAME + ;; ORDER BY USERS__via__SENDER_ID.NAME ASC + (is (= [["Bob the Sea Gull" 2] + ["Brenda Blackbird" 2] + ["Lucky Pigeon" 2] + ["Peter Pelican" 5] + ["Ronald Raven" 1]] + (mt/formatted-rows [str int] + (mt/run-mbql-query messages + {:aggregation [[:count]] + :breakout [$sender_id->users.name] + :filter [:= $receiver_id->users.name "Rasta Toucan"]})))))))) diff --git a/test/metabase/query_processor_test/nested_field_test.clj b/test/metabase/query_processor_test/nested_field_test.clj index 89e60a13719..04e1f7ae0a6 100644 --- a/test/metabase/query_processor_test/nested_field_test.clj +++ b/test/metabase/query_processor_test/nested_field_test.clj @@ -1,143 +1,144 @@ (ns metabase.query-processor-test.nested-field-test "Tests for nested field access." - (:require [metabase.query-processor-test :as qp.test] - [metabase.test :as mt] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets])) + (:require [clojure.test :refer :all] + [metabase.test :as mt])) -;;; Nested Field in FILTER -;; Get the first 10 tips where tip.venue.name == "Kyle's Low-Carb Grill" -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [[8 "Kyle's Low-Carb Grill"] - [67 "Kyle's Low-Carb Grill"] - [80 "Kyle's Low-Carb Grill"] - [83 "Kyle's Low-Carb Grill"] - [295 "Kyle's Low-Carb Grill"] - [342 "Kyle's Low-Carb Grill"] - [417 "Kyle's Low-Carb Grill"] - [426 "Kyle's Low-Carb Grill"] - [470 "Kyle's Low-Carb Grill"]] - (mapv - (fn [[id _ _ _ {venue-name :name}]] [id venue-name]) - (qp.test/rows - (data/dataset geographical-tips - (data/run-mbql-query tips - {:filter [:= $tips.venue.name "Kyle's Low-Carb Grill"] - :order-by [[:asc $id]] - :limit 10}))))) +(deftest filter-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field in FILTER" + (mt/dataset geographical-tips + ;; Get the first 10 tips where tip.venue.name == "Kyle's Low-Carb Grill" + (is (= [[8 "Kyle's Low-Carb Grill"] + [67 "Kyle's Low-Carb Grill"] + [80 "Kyle's Low-Carb Grill"] + [83 "Kyle's Low-Carb Grill"] + [295 "Kyle's Low-Carb Grill"] + [342 "Kyle's Low-Carb Grill"] + [417 "Kyle's Low-Carb Grill"] + [426 "Kyle's Low-Carb Grill"] + [470 "Kyle's Low-Carb Grill"]] + (mapv + (fn [[id _ _ _ {venue-name :name}]] [id venue-name]) + (mt/rows + (mt/run-mbql-query tips + {:filter [:= $tips.venue.name "Kyle's Low-Carb Grill"] + :order-by [[:asc $id]] + :limit 10}))))))))) -;;; Nested Field in ORDER -;; Let's get all the tips Kyle posted on Twitter sorted by tip.venue.name -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [[446 - {:mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :service "twitter", :username "kyle"} - "Cam's Mexican Gastro Pub is a historical and underappreciated place to conduct a business meeting with friends." - {:large "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/large.jpg", - :medium "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/med.jpg", - :small "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/small.jpg"} - {:phone "415-320-9123", :name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :id "bb958ac5-758e-4f42-b984-6b0e13f25194"}] - [230 - {:mentions ["@haight_european_grill"], :tags ["#european" "#grill"], :service "twitter", :username "kyle"} - "Haight European Grill is a horrible and amazing place to have a birthday party during winter." - {:large "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/large.jpg", - :medium "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/med.jpg", - :small "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/small.jpg"} - {:phone "415-191-2778", :name "Haight European Grill", :categories ["European" "Grill"], :id "7e6281f7-5b17-4056-ada0-85453247bc8f"}] - [319 - {:mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :service "twitter", :username "kyle"} - "Haight Soul Food Pop-Up Food Stand is a underground and modern place to have breakfast on a Tuesday afternoon." - {:large "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/large.jpg", - :medium "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/med.jpg", - :small "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/small.jpg"} - {:phone "415-741-8726", :name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :id "9735184b-1299-410f-a98e-10d9c548af42"}] - [224 - {:mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :service "twitter", :username "kyle"} - "Pacific Heights Free-Range Eatery is a wonderful and modern place to take visiting friends and relatives Friday nights." - {:large "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/large.jpg", - :medium "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/med.jpg", - :small "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/small.jpg"} - {:phone "415-901-6541", :name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"}]] - (qp.test/rows - (data/dataset geographical-tips - (data/run-mbql-query tips - {:filter [:and - [:= $tips.source.service "twitter"] - [:= $tips.source.username "kyle"]] - :order-by [[:asc $tips.venue.name]]})))) +(deftest order-by-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field in ORDER-BY" + (mt/dataset geographical-tips + ;; Let's get all the tips Kyle posted on Twitter sorted by tip.venue.name + (is (= [[446 + {:mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :service "twitter", :username "kyle"} + "Cam's Mexican Gastro Pub is a historical and underappreciated place to conduct a business meeting with friends." + {:large "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/large.jpg", + :medium "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/med.jpg", + :small "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/small.jpg"} + {:phone "415-320-9123", :name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :id "bb958ac5-758e-4f42-b984-6b0e13f25194"}] + [230 + {:mentions ["@haight_european_grill"], :tags ["#european" "#grill"], :service "twitter", :username "kyle"} + "Haight European Grill is a horrible and amazing place to have a birthday party during winter." + {:large "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/large.jpg", + :medium "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/med.jpg", + :small "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/small.jpg"} + {:phone "415-191-2778", :name "Haight European Grill", :categories ["European" "Grill"], :id "7e6281f7-5b17-4056-ada0-85453247bc8f"}] + [319 + {:mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :service "twitter", :username "kyle"} + "Haight Soul Food Pop-Up Food Stand is a underground and modern place to have breakfast on a Tuesday afternoon." + {:large "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/large.jpg", + :medium "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/med.jpg", + :small "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/small.jpg"} + {:phone "415-741-8726", :name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :id "9735184b-1299-410f-a98e-10d9c548af42"}] + [224 + {:mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :service "twitter", :username "kyle"} + "Pacific Heights Free-Range Eatery is a wonderful and modern place to take visiting friends and relatives Friday nights." + {:large "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/large.jpg", + :medium "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/med.jpg", + :small "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/small.jpg"} + {:phone "415-901-6541", :name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"}]] + (mt/rows + (mt/run-mbql-query tips + {:filter [:and + [:= $tips.source.service "twitter"] + [:= $tips.source.username "kyle"]] + :order-by [[:asc $tips.venue.name]]})))))))) -;; Nested Field in AGGREGATION -;; Let's see how many *distinct* venue names are mentioned -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [99] - (qp.test/first-row - (data/dataset geographical-tips - (data/run-mbql-query tips - {:aggregation [[:distinct $tips.venue.name]]})))) +(deftest aggregation-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field in AGGREGATION" + (mt/dataset geographical-tips + (testing ":distinct aggregation" + ;; Let's see how many *distinct* venue names are mentioned + (is (= [99] + (mt/first-row + (mt/run-mbql-query tips + {:aggregation [[:distinct $tips.venue.name]]}))))) -;; Now let's just get the regular count -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [500] - (qp.test/first-row - (data/dataset geographical-tips - (data/run-mbql-query tips - {:aggregation [[:count $tips.venue.name]]})))) + (testing ":count aggregation" + ;; Now let's just get the regular count + (is (= [500] + (mt/first-row + (mt/run-mbql-query tips + {:aggregation [[:count $tips.venue.name]]}))))))))) -;;; Nested Field in BREAKOUT -;; Let's see how many tips we have by source.service -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [["facebook" 107] - ["flare" 105] - ["foursquare" 100] - ["twitter" 98] - ["yelp" 90]] - (qp.test/rows - (qp.test/format-rows-by [str int] - (data/dataset geographical-tips - (data/run-mbql-query tips - {:aggregation [[:count]] - :breakout [$tips.source.service]}))))) +(deftest breakout-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field in BREAKOUT" + ;; Let's see how many tips we have by source.service + (mt/dataset geographical-tips + (is (= [["facebook" 107] + ["flare" 105] + ["foursquare" 100] + ["twitter" 98] + ["yelp" 90]] + (mt/formatted-rows [str int] + (mt/run-mbql-query tips + {:aggregation [[:count]] + :breakout [$tips.source.service]})))))))) -;;; Nested Field in FIELDS -;; Return the first 10 tips with just tip.venue.name -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [["Lucky's Gluten-Free Café"] - ["Joe's Homestyle Eatery"] - ["Lower Pac Heights Cage-Free Coffee House"] - ["Oakland European Liquor Store"] - ["Tenderloin Gormet Restaurant"] - ["Marina Modern Sushi"] - ["Sunset Homestyle Grill"] - ["Kyle's Low-Carb Grill"] - ["Mission Homestyle Churros"] - ["Sameer's Pizza Liquor Store"]] - (qp.test/rows - (data/dataset geographical-tips - (data/run-mbql-query tips - {:fields [$tips.venue.name] - :order-by [[:asc $id]] - :limit 10})))) +(deftest fields-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field in FIELDS" + (mt/dataset geographical-tips + ;; Return the first 10 tips with just tip.venue.name + (is (= [["Lucky's Gluten-Free Café"] + ["Joe's Homestyle Eatery"] + ["Lower Pac Heights Cage-Free Coffee House"] + ["Oakland European Liquor Store"] + ["Tenderloin Gormet Restaurant"] + ["Marina Modern Sushi"] + ["Sunset Homestyle Grill"] + ["Kyle's Low-Carb Grill"] + ["Mission Homestyle Churros"] + ["Sameer's Pizza Liquor Store"]] + (mt/rows + (mt/run-mbql-query tips + {:fields [$tips.venue.name] + :order-by [[:asc $id]] + :limit 10})))))))) - -;;; Nested Field w/ ordering by aggregation -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :nested-fields) - [["jane" 4] - ["kyle" 5] - ["tupac" 5] - ["jessica" 6] - ["bob" 7] - ["lucky_pigeon" 7] - ["joe" 8] - ["mandy" 8] - ["amy" 9] - ["biggie" 9] - ["sameer" 9] - ["cam_saul" 10] - ["rasta_toucan" 13] - [nil 400]] - (qp.test/format-rows-by [identity int] - (qp.test/rows - (data/dataset geographical-tips - (data/run-mbql-query tips - {:aggregation [[:count]] - :breakout [$tips.source.mayor] - :order-by [[:asc [:aggregation 0]]]}))))) +(deftest order-by-aggregation-test + (mt/test-drivers (mt/normal-drivers-with-feature :nested-fields) + (testing "Nested Field w/ ordering by aggregation" + (mt/dataset geographical-tips + (is (= [["jane" 4] + ["kyle" 5] + ["tupac" 5] + ["jessica" 6] + ["bob" 7] + ["lucky_pigeon" 7] + ["joe" 8] + ["mandy" 8] + ["amy" 9] + ["biggie" 9] + ["sameer" 9] + ["cam_saul" 10] + ["rasta_toucan" 13] + [nil 400]] + (mt/formatted-rows [identity int] + (mt/run-mbql-query tips + {:aggregation [[:count]] + :breakout [$tips.source.mayor] + :order-by [[:asc [:aggregation 0]]]})))))))) diff --git a/test/metabase/query_processor_test/sum_where_test.clj b/test/metabase/query_processor_test/sum_where_test.clj index 0ce4eb2b217..19929b2eef9 100644 --- a/test/metabase/query_processor_test/sum_where_test.clj +++ b/test/metabase/query_processor_test/sum_where_test.clj @@ -1,90 +1,106 @@ (ns metabase.query-processor-test.sum-where-test - (:require [metabase.models.metric :refer [Metric]] + (:require [clojure.test :refer :all] + [metabase.models.metric :refer [Metric]] [metabase.models.segment :refer [Segment]] - [metabase.query-processor-test :refer :all] - [metabase.test :as mt] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets] - [metabase.test.util :as tu] - [toucan.util.test :as tt])) + [metabase.test :as mt])) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 179.0 - (->> {:aggregation [[:sum-where [:field-id (data/id :venues :price)] [:< [:field-id (data/id :venues :price)] 4]]]} - (data/run-mbql-query venues) - rows - ffirst - double)) +(deftest basic-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= 179.0 + (->> {:aggregation [[:sum-where + [:field-id (mt/id :venues :price)] + [:< [:field-id (mt/id :venues :price)] 4]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double))) -;; Test normalization -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 179.0 - (->> {:aggregation [["sum-where" ["field-id" (data/id :venues :price)] ["<" ["field-id" (data/id :venues :price)] 4]]]} - (data/run-mbql-query venues) - rows - ffirst - double)) + (testing "Should get normalized correctly and work as expected" + (is (= 179.0 + (->> {:aggregation [["sum-where" + ["field-id" (mt/id :venues :price)] + ["<" ["field-id" (mt/id :venues :price)] 4]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double)))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 34.0 - (->> {:aggregation [[:sum-where - [:field-id (data/id :venues :price)] - [:and [:< [:field-id (data/id :venues :price)] 4] - [:or [:starts-with [:field-id (data/id :venues :name)] "M"] - [:ends-with [:field-id (data/id :venues :name)] "t"]]]]]} - (data/run-mbql-query venues) - rows - ffirst - double)) +(deftest compound-condition-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= 34.0 + (->> {:aggregation [[:sum-where + [:field-id (mt/id :venues :price)] + [:and [:< [:field-id (mt/id :venues :price)] 4] + [:or [:starts-with [:field-id (mt/id :venues :name)] "M"] + [:ends-with [:field-id (mt/id :venues :name)] "t"]]]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - nil - (->> {:aggregation [[:sum-where [:field-id (data/id :venues :price)] [:< [:field-id (data/id :venues :price)] 4]]] - :filter [:> [:field-id (data/id :venues :price)] Long/MAX_VALUE]} - (data/run-mbql-query venues) - rows - ffirst)) +(deftest filter-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= nil + (->> {:aggregation [[:sum-where [:field-id (mt/id :venues :price)] [:< [:field-id (mt/id :venues :price)] 4]]] + :filter [:> [:field-id (mt/id :venues :price)] Long/MAX_VALUE]} + (mt/run-mbql-query venues) + mt/rows + ffirst))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - [[2 0.0] - [3 0.0] - [4 1.0] - [5 1.0]] - (->> {:aggregation [[:sum-where [:field-id (data/id :venues :price)] [:< [:field-id (data/id :venues :price)] 2]]] - :breakout [[:field-id (data/id :venues :category_id)]] - :limit 4} - (data/run-mbql-query venues) - (tu/round-all-decimals 2) - rows - (map (fn [[k v]] - [(long k) (double v)])))) +(deftest breakout-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (is (= [[2 0.0] + [3 0.0] + [4 1.0] + [5 1.0]] + (->> {:aggregation [[:sum-where + [:field-id (mt/id :venues :price)] + [:< [:field-id (mt/id :venues :price)] 2]]] + :breakout [[:field-id (mt/id :venues :category_id)]] + :limit 4} + (mt/run-mbql-query venues) + (mt/round-all-decimals 2) + mt/rows + (map (fn [[k v]] + [(long k) (double v)]))))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations :expressions) - 90.5 - (->> {:aggregation [[:+ [:/ [:sum-where [:field-id (data/id :venues :price)] [:< [:field-id (data/id :venues :price)] 4]] 2] 1]]} - (data/run-mbql-query venues) - rows - ffirst - double)) +(deftest sum-where-inside-expressions-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations :expressions) + (is (= 90.5 + (->> {:aggregation [[:+ + [:/ + [:sum-where + [:field-id (mt/id :venues :price)] + [:< [:field-id (mt/id :venues :price)] 4]] + 2] + 1]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 179.0 - (tt/with-temp* [Segment [{segment-id :id} {:table_id (data/id :venues) - :definition {:source-table (data/id :venues) - :filter [:< [:field-id (data/id :venues :price)] 4]}}]] - (->> {:aggregation [[:sum-where [:field-id (data/id :venues :price)] [:segment segment-id]]]} - (data/run-mbql-query venues) - rows - ffirst - double))) +(deftest segment-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (mt/with-temp Segment [{segment-id :id} {:table_id (mt/id :venues) + :definition {:source-table (mt/id :venues) + :filter [:< [:field-id (mt/id :venues :price)] 4]}}] + (is (= 179.0 + (->> {:aggregation [[:sum-where [:field-id (mt/id :venues :price)] [:segment segment-id]]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double)))))) -(datasets/expect-with-drivers (mt/normal-drivers-with-feature :basic-aggregations) - 179.0 - (tt/with-temp* [Metric [{metric-id :id} {:table_id (data/id :venues) - :definition {:source-table (data/id :venues) - :aggregation [:sum-where [:field-id (data/id :venues :price)] [:< [:field-id (data/id :venues :price)] 4]]}}]] - (->> {:aggregation [[:metric metric-id]]} - (data/run-mbql-query venues) - rows - ffirst - double))) +(deftest metric-test + (mt/test-drivers (mt/normal-drivers-with-feature :basic-aggregations) + (mt/with-temp Metric [{metric-id :id} {:table_id (mt/id :venues) + :definition {:source-table (mt/id :venues) + :aggregation [:sum-where + [:field-id (mt/id :venues :price)] + [:< [:field-id (mt/id :venues :price)] 4]]}}] + (is (= 179.0 + (->> {:aggregation [[:metric metric-id]]} + (mt/run-mbql-query venues) + mt/rows + ffirst + double)))))) diff --git a/test/metabase/server/middleware/json_test.clj b/test/metabase/server/middleware/json_test.clj index 03db9f5aded..9a75b0cfd57 100644 --- a/test/metabase/server/middleware/json_test.clj +++ b/test/metabase/server/middleware/json_test.clj @@ -1,14 +1,13 @@ (ns metabase.server.middleware.json-test (:require [cheshire.core :as json] - [expectations :refer [expect]] - [metabase.plugins.classloader :as classloader])) + [clojure.test :refer :all] + [metabase.server.middleware.json :as mw.json])) -;;; JSON encoding tests -;; Required here so so custom Cheshire encoders are loaded -(classloader/require 'metabase.server.middleware.json) +(comment mw.json/keep-me) ; so custom Cheshire encoders are loaded -;; Check that we encode byte arrays as the hex values of their first four bytes -(expect - "{\"my-bytes\":\"0xC42360D7\"}" - (json/generate-string {:my-bytes (byte-array [196 35 96 215 8 106 108 248 183 215 244 143 17 160 53 186 - 213 30 116 25 87 31 123 172 207 108 47 107 191 215 76 92])})) +(deftest encode-byte-arrays-test + (testing "Check that we encode byte arrays as the hex values of their first four bytes" + (is (= "{\"my-bytes\":\"0xC42360D7\"}" + (json/generate-string + {:my-bytes (byte-array [196 35 96 215 8 106 108 248 183 215 244 143 17 160 53 186 + 213 30 116 25 87 31 123 172 207 108 47 107 191 215 76 92])}))))) diff --git a/test/metabase/server/middleware/util_test.clj b/test/metabase/server/middleware/util_test.clj index 52b57ea46b1..42b08fea773 100644 --- a/test/metabase/server/middleware/util_test.clj +++ b/test/metabase/server/middleware/util_test.clj @@ -1,24 +1,20 @@ (ns metabase.server.middleware.util-test - (:require [expectations :refer [expect]] + (:require [clojure.test :refer :all] [metabase.server.middleware.util :as mw.util])) -(defn- https? [headers] - (mw.util/https-request? {:headers headers})) - -(expect true (https? {"x-forwarded-proto" "https"})) -(expect false (https? {"x-forwarded-proto" "http"})) - -(expect true (https? {"x-forwarded-protocol" "https"})) -(expect false (https? {"x-forwarded-protocol" "http"})) - -(expect true (https? {"x-url-scheme" "https"})) -(expect false (https? {"x-url-scheme" "http"})) - -(expect true (https? {"x-forwarded-ssl" "on"})) -(expect false (https? {"x-forwarded-ssl" "off"})) - -(expect true (https? {"front-end-https" "on"})) -(expect false (https? {"front-end-https" "off"})) - -(expect true (https? {"origin" "https://mysite.com"})) -(expect false (https? {"origin" "http://mysite.com"})) +(deftest https-request?-test + (doseq [[headers expected] {{"x-forwarded-proto" "https"} true + {"x-forwarded-proto" "http"} false + {"x-forwarded-protocol" "https"} true + {"x-forwarded-protocol" "http"} false + {"x-url-scheme" "https"} true + {"x-url-scheme" "http"} false + {"x-forwarded-ssl" "on"} true + {"x-forwarded-ssl" "off"} false + {"front-end-https" "on"} true + {"front-end-https" "off"} false + {"origin" "https://mysite.com"} true + {"origin" "http://mysite.com"} false}] + (testing (pr-str (list 'https-request? {:headers headers})) + (is (= expected + (mw.util/https-request? {:headers headers})))))) diff --git a/test/metabase/sync/sync_metadata/metabase_metadata_test.clj b/test/metabase/sync/sync_metadata/metabase_metadata_test.clj index b167640d941..fd45b51f4fa 100644 --- a/test/metabase/sync/sync_metadata/metabase_metadata_test.clj +++ b/test/metabase/sync/sync_metadata/metabase_metadata_test.clj @@ -1,47 +1,45 @@ (ns metabase.sync.sync-metadata.metabase-metadata-test "Tests for the logic that syncs the `_metabase_metadata` Table." - (:require [expectations :refer :all] + (:require [clojure.test :refer :all] [metabase.models.database :refer [Database]] [metabase.models.field :refer [Field]] [metabase.models.table :refer [Table]] [metabase.sync.sync-metadata.metabase-metadata :as metabase-metadata] + [metabase.test :as mt] [metabase.test.mock.moviedb :as moviedb] - [metabase.test.util :as tu] [metabase.util :as u] [toucan.db :as db] - [toucan.hydrate :refer [hydrate]] - [toucan.util.test :as tt])) + [toucan.hydrate :refer [hydrate]])) -;; Test that the `_metabase_metadata` table can be used to populate values for things like descriptions -(defn- get-table-and-fields-descriptions [table-or-id] - (-> (db/select-one [Table :id :name :description], :id (u/get-id table-or-id)) - (hydrate :fields) - (update :fields #(for [field %] - (select-keys field [:name :description]))) - tu/boolean-ids-and-timestamps)) - -(expect - [{:name "movies" - :description nil - :id true - :fields [{:name "filming", :description nil}]} - {:name "movies" - :description "A cinematic adventure." - :id true - :fields [{:name "filming", :description "If the movie is currently being filmed."}]}] - (tt/with-temp* [Database [db {:engine ::moviedb/moviedb}]] - ;; manually add in the movies table - (let [table (db/insert! Table - :db_id (u/get-id db) - :name "movies" - :active true)] - (db/insert! Field - :database_type "BOOL" - :base_type :type/Boolean - :table_id (u/get-id table) - :name "filming") - ;; here we go - [(get-table-and-fields-descriptions table) - (do - (metabase-metadata/sync-metabase-metadata! db) - (get-table-and-fields-descriptions table))]))) +(deftest sync-metabase-metadata-test + (testing ":Test that the `_metabase_metadata` table can be used to populate values for things like descriptions" + (letfn [(get-table-and-fields-descriptions [table-or-id] + (-> (db/select-one [Table :id :name :description], :id (u/the-id table-or-id)) + (hydrate :fields) + (update :fields #(for [field %] + (select-keys field [:name :description]))) + mt/boolean-ids-and-timestamps))] + (mt/with-temp Database [db {:engine ::moviedb/moviedb}] + ;; manually add in the movies table + (let [table (db/insert! Table + :db_id (u/the-id db) + :name "movies" + :active true)] + (db/insert! Field + :database_type "BOOL" + :base_type :type/Boolean + :table_id (u/the-id table) + :name "filming") + (testing "before" + (is (= {:name "movies" + :description nil + :id true + :fields [{:name "filming", :description nil}]} + (get-table-and-fields-descriptions table)))) + (metabase-metadata/sync-metabase-metadata! db) + (testing "after" + (is (= {:name "movies" + :description "A cinematic adventure." + :id true + :fields [{:name "filming", :description "If the movie is currently being filmed."}]} + (get-table-and-fields-descriptions table))))))))) diff --git a/test/metabase/sync/sync_metadata/sync_timezone_test.clj b/test/metabase/sync/sync_metadata/sync_timezone_test.clj index 2762d1933e6..b00b9cb5b3d 100644 --- a/test/metabase/sync/sync_metadata/sync_timezone_test.clj +++ b/test/metabase/sync/sync_metadata/sync_timezone_test.clj @@ -1,67 +1,63 @@ (ns metabase.sync.sync-metadata.sync-timezone-test (:require [clj-time.core :as time] + [clojure.test :refer :all] [metabase.driver :as driver] [metabase.models.database :refer [Database]] [metabase.sync.sync-metadata.sync-timezone :as sync-tz] [metabase.sync.util-test :as sut] - [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets] - [metabase.test.util :as tu] + [metabase.test :as mt] [metabase.util :as u] [toucan.db :as db])) (defn- db-timezone [db-or-id] - (db/select-one-field :timezone Database :id (u/get-id db-or-id))) + (db/select-one-field :timezone Database :id (u/the-id db-or-id))) -;; This tests populating the timezone field for a given database. The -;; sync happens automatically, so this test removes it first to ensure -;; that it gets set when missing -(datasets/expect-with-drivers #{:h2 :postgres} - (concat - (repeat 2 {:timezone-id "UTC"}) - [true true true]) - (data/dataset test-data - (let [db (data/db) - tz-on-load (db-timezone db) - _ (db/update! Database (:id db) :timezone nil) - tz-after-update (db-timezone db) - ;; It looks like we can get some stale timezone information depending on which thread is used for querying the - ;; database in sync. Clearing the connection pool to ensure we get the most updated TZ data - _ (driver/notify-database-updated driver/*driver* db) - {:keys [step-info task-history]} (sut/sync-database! "sync-timezone" db)] +(deftest sync-timezone-test + (mt/test-drivers #{:h2 :postgres} + (testing (str "This tests populating the timezone field for a given database. The sync happens automatically, so " + "this test removes it first to ensure that it gets set when missing") + (mt/dataset test-data + (let [db (mt/db) + tz-on-load (db-timezone db) + _ (db/update! Database (:id db) :timezone nil) + tz-after-update (db-timezone db) + ;; It looks like we can get some stale timezone information depending on which thread is used for querying the + ;; database in sync. Clearing the connection pool to ensure we get the most updated TZ data + _ (driver/notify-database-updated driver/*driver* db) + {:keys [step-info task-history]} (sut/sync-database! "sync-timezone" db)] + (testing "only step keys" + (is (= {:timezone-id "UTC"} + (sut/only-step-keys step-info)))) + (testing "task details" + (is (= {:timezone-id "UTC"} + (:task_details task-history)))) + (testing "On startup is the timezone specified?" + (is (time/time-zone-for-id tz-on-load))) + (testing "Check to make sure the test removed the timezone" + (is (nil? tz-after-update))) + (testing "Check that the value was set again after sync" + (is (time/time-zone-for-id (db-timezone db))))))))) - [(sut/only-step-keys step-info) - (:task_details task-history) - ;; On startup is the timezone specified? - (boolean (time/time-zone-for-id tz-on-load)) - ;; Check to make sure the test removed the timezone - (nil? tz-after-update) - ;; Check that the value was set again after sync - (boolean (time/time-zone-for-id (db-timezone db)))]))) - -;; Test that if timezone is changed to something that fails timezone is unaffected. -;; -;; Setting timezone to "Austrailia/Sydney" fails on some computers, especially the CI ones. In that case it fails as -;; the dates on PostgreSQL return 'AEST' for the time zone name. The Exception is logged, but the timezone column -;; should be left alone and processing should continue. -;; -;; TODO - Recently this call has started *succeeding* for me on Java 10/11 and Postgres 9.6. I've seen it sync as both -;; "Australia/Hobart" and "Australia/Sydney". Since setting the timezone no longer always fails it's no longer a good -;; test. We need to think of something else here. In the meantime, I'll go ahead and consider any of the three options -;; valid answers. -(datasets/expect-with-drivers #{:postgres} - {:before "UTC" - :after true} - (data/dataset test-data - ;; use `with-temp-vals-in-db` to make sure the test data DB timezone gets reset to whatever it was before the test - ;; ran if we accidentally end up setting it in the `:after` part - (tu/with-temp-vals-in-db Database (data/db) {:timezone (db-timezone (data/db))} - (sync-tz/sync-timezone! (data/db)) - {:before - (db-timezone (data/db)) - - ;; TODO - this works for me ok with Postgres 9.6 & Java 10. Returns - :after - (tu/with-temporary-setting-values [report-timezone "Australia/Sydney"] - (sync-tz/sync-timezone! (data/db)) - (contains? #{"Australia/Hobart" "Australia/Sydney" "UTC"} (db-timezone (data/db))))}))) +(deftest bad-change-test + (mt/test-drivers #{:postgres} + (testing "Test that if timezone is changed to something that fails, timezone is unaffected." + ;; Setting timezone to "Austrailia/Sydney" fails on some computers, especially the CI ones. In that case it fails as + ;; the dates on PostgreSQL return 'AEST' for the time zone name. The Exception is logged, but the timezone column + ;; should be left alone and processing should continue. + ;; + ;; TODO - Recently this call has started *succeeding* for me on Java 10/11 and Postgres 9.6. I've seen it sync as both + ;; "Australia/Hobart" and "Australia/Sydney". Since setting the timezone no longer always fails it's no longer a good + ;; test. We need to think of something else here. In the meantime, I'll go ahead and consider any of the three options + ;; valid answers. + (mt/dataset test-data + ;; use `with-temp-vals-in-db` to make sure the test data DB timezone gets reset to whatever it was before the test + ;; ran if we accidentally end up setting it in the `:after` part + (mt/with-temp-vals-in-db Database (mt/db) {:timezone (db-timezone (mt/db))} + (sync-tz/sync-timezone! (mt/db)) + (testing "before" + (is (= "UTC" + (db-timezone (mt/db))))) + (testing "after" + (mt/with-temporary-setting-values [report-timezone "Australia/Sydney"] + (sync-tz/sync-timezone! (mt/db)) + (is (contains? #{"Australia/Hobart" "Australia/Sydney" "UTC"} (db-timezone (mt/db))))))))))) diff --git a/test/metabase/test/data/datasets.clj b/test/metabase/test/data/datasets.clj index feb60b02e36..9315521f398 100644 --- a/test/metabase/test/data/datasets.clj +++ b/test/metabase/test/data/datasets.clj @@ -63,54 +63,3 @@ `(doseq [driver# ~drivers] (test-driver driver# ~@body))) - -(defmacro ^:deprecated expect-with-drivers - "Generate unit tests for all drivers in env var `DRIVERS`; each test will only run if we're currently testing the - corresponding driver. `*driver*` is bound to the current driver inside each test. - - DEPRECATED: use `deftest` with `test-drivers` instead. - - (deftest my-test - (datasets/test-drivers #{:h2 :postgres} - (is (= ...))))" - {:style/indent 1} - [drivers expected actual] - ;; Make functions to get expected/actual so the code is only compiled one time instead of for every single driver - ;; speeds up loading of metabase.driver.query-processor-test significantly - (let [symb (symbol (str "expect-with-drivers-" (hash &form)))] - `(t/deftest ~symb - (t/testing (str "\n" (format ~(str (name (ns-name *ns*)) ":%d") (:line (meta #'~symb)))) - (test-drivers ~drivers - (t/is (~'expect= ~expected ~actual))))))) - -(defmacro ^:deprecated expect-with-driver - "Generate a unit test that only runs if we're currently testing against `driver`, and that binds `*driver*` when it - runs. - - DEPRECATED: Use `deftest` with `test-driver` instead. - - (deftest my-test - (datasets/test-driver :mysql - (is (= ...))))" - {:style/indent 1} - [driver expected actual] - `(expect-with-drivers [~driver] ~expected ~actual)) - -(defmacro test-all-drivers - "Execute body (presumably containing tests) against all drivers we're currently testing against." - {:style/indent 0} - [& body] - `(test-drivers (tx.env/test-drivers) ~@body)) - -(defmacro ^:deprecated expect-with-all-drivers - "Generate unit tests for all drivers specified in env var `DRIVERS`. `*driver*` is bound to the current driver inside - each test. - - DEPRECATED: Use `test-all-drivers` instead. - - (deftest my-test - (datasets/test-all-drivers - (is (= ...))))" - {:style/indent 0} - [expected actual] - `(expect-with-drivers (tx.env/test-drivers) ~expected ~actual)) diff --git a/test/metabase/util/infer_spaces_test.clj b/test/metabase/util/infer_spaces_test.clj index 8364c1b1443..6a7c1fe75af 100644 --- a/test/metabase/util/infer_spaces_test.clj +++ b/test/metabase/util/infer_spaces_test.clj @@ -1,13 +1,15 @@ (ns metabase.util.infer-spaces-test - (:require [expectations :refer :all] - [metabase.util.infer-spaces :refer :all])) + (:require [clojure.test :refer :all] + [metabase.util.infer-spaces :as infer-spaces])) -(expect ["user"] (infer-spaces "user")) -(expect ["users"] (infer-spaces "users")) -(expect ["orders"] (infer-spaces "orders")) -(expect ["products"] (infer-spaces "products")) -(expect ["events"] (infer-spaces "events")) - -(expect ["checkins"] (infer-spaces "checkins")) - -(expect ["dashboard" "card" "subscription"] (infer-spaces "dashboardcardsubscription")) +(deftest infer-spaces-test + (doseq [[input expected] {"user" ["user"] + "users" ["users"] + "orders" ["orders"] + "products" ["products"] + "events" ["events"] + "checkins" ["checkins"] + "dashboardcardsubscription" ["dashboard" "card" "subscription"]}] + (testing (pr-str (list 'infer-spaces input)) + (is (= expected + (infer-spaces/infer-spaces input)))))) diff --git a/test/metabase/util/password_test.clj b/test/metabase/util/password_test.clj index 28f589d93c1..579a6a979ef 100644 --- a/test/metabase/util/password_test.clj +++ b/test/metabase/util/password_test.clj @@ -1,43 +1,59 @@ (ns metabase.util.password-test (:require [expectations :refer [expect]] + [clojure.test :refer :all] [metabase.util.password :as pwu])) ;; Password Complexity testing -;; Check that password occurance counting works -(expect {:total 3, :lower 3, :upper 0, :letter 3, :digit 0, :special 0} (#'pwu/count-occurrences "abc")) -(expect {:total 8, :lower 0, :upper 8, :letter 8, :digit 0, :special 0} (#'pwu/count-occurrences "PASSWORD")) -(expect {:total 3, :lower 0, :upper 0, :letter 0, :digit 3, :special 0} (#'pwu/count-occurrences "123")) -(expect {:total 8, :lower 4, :upper 2, :letter 6, :digit 0, :special 2} (#'pwu/count-occurrences "GoodPw!!")) -(expect {:total 9, :lower 7, :upper 1, :letter 8, :digit 1, :special 0} (#'pwu/count-occurrences "passworD1")) -(expect {:total 10, :lower 3, :upper 2, :letter 5, :digit 1, :special 4} (#'pwu/count-occurrences "^^Wut4nG^^")) +(deftest count-occurrences-test + (testing "Check that password occurance counting works" + (doseq [[input expected] {"abc" {:total 3, :lower 3, :upper 0, :letter 3, :digit 0, :special 0} + "PASSWORD" {:total 8, :lower 0, :upper 8, :letter 8, :digit 0, :special 0} + "123" {:total 3, :lower 0, :upper 0, :letter 0, :digit 3, :special 0} + "GoodPw!!" {:total 8, :lower 4, :upper 2, :letter 6, :digit 0, :special 2} + "passworD1" {:total 9, :lower 7, :upper 1, :letter 8, :digit 1, :special 0} + "^^Wut4nG^^" {:total 10, :lower 3, :upper 2, :letter 5, :digit 1, :special 4}}] + (testing (pr-str (list 'count-occurrences input)) + (is (= expected + (#'pwu/count-occurrences input))))))) -;; Check that password length complexity applies -(expect true (#'pwu/password-has-char-counts? {:total 3} "god1")) -(expect true (#'pwu/password-has-char-counts? {:total 4} "god1")) -(expect false (#'pwu/password-has-char-counts? {:total 5} "god1")) +(deftest password-has-char-counts?-test + (doseq [[group input->expected] + {"Check that password length complexity applies" + {[{:total 3} "god1"] true + [{:total 4} "god1"] true + [{:total 5} "god1"] false} -;; Check that testing password character type complexity works -(expect true (#'pwu/password-has-char-counts? {} "ABC")) -(expect false (#'pwu/password-has-char-counts? {:lower 1} "ABC")) -(expect true (#'pwu/password-has-char-counts? {:lower 1} "abc")) -(expect false (#'pwu/password-has-char-counts? {:digit 1} "abc")) -(expect true (#'pwu/password-has-char-counts? {:digit 1, :special 2} "!0!")) + "Check that testing password character type complexity works" + {[{} "ABC"] true + [{:lower 1} "ABC"] false + [{:lower 1} "abc"] true + [{:digit 1} "abc"] false + [{:digit 1, :special 2} "!0!"] true} -;; Do some tests that combine both requirements -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "^aA2")) -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password")) -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password1")) -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password1!")) -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD!")) -(expect false (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD1")) -(expect true (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD1!")) -(expect true (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "paSS&&word1")) -(expect true (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passW0rd))")) -(expect true (#'pwu/password-has-char-counts? {:total 6, :lower 1, :upper 1, :digit 1, :special 1} "^^Wut4nG^^")) + "Do some tests that combine both requirements" + {[{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "^aA2"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password1"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "password1!"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD!"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD1"] false + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passworD1!"] true + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "paSS&&word1"] true + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "passW0rd))"] true + [{:total 6, :lower 1, :upper 1, :digit 1, :special 1} "^^Wut4nG^^"] true}}] + (testing group + (doseq [[input expected] input->expected] + (testing (pr-str (cons 'password-has-char-counts? input)) + (is (= expected + (apply #'pwu/password-has-char-counts? input)))))))) -;; Do some tests with the default (:normal) password requirements -(expect false (pwu/is-complex? "ABC")) -(expect false (pwu/is-complex? "ABCDEF")) -(expect true (pwu/is-complex? "ABCDE1")) -(expect true (pwu/is-complex? "123456")) +(deftest is-complex?-test + (testing "Do some tests with the default (:normal) password requirements" + (doseq [[input expected] {"ABC" false + "ABCDEF" false + "ABCDE1" true + "123456" true}] + (testing (pr-str (list 'is-complex? input)) + (is (= expected + (pwu/is-complex? input))))))) -- GitLab