Skip to content
Snippets Groups Projects
Unverified Commit 29bf76ee authored by Cam Saul's avatar Cam Saul Committed by GitHub
Browse files

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
parent a7a23c7f
No related branches found
No related tags found
No related merge requests found
Showing
with 1831 additions and 1941 deletions
(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))))))
......@@ -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")
......
......@@ -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
......
......@@ -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."
......
(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))))))))
(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))))))
(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)))))))
(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))))))
(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"]}))))))))
(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]]]}))))))))
(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))))))
(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])})))))
(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}))))))
(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)))))))))
(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)))))))))))
......@@ -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))
(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))))))
(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)))))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment