diff --git a/backend/mbql/test/metabase/mbql/normalize_test.clj b/backend/mbql/test/metabase/mbql/normalize_test.clj index 6d67dd3dc2fcdc32cf70ea0b1299d9e48e4eb72a..f1a154a53237b3829cb229b94064dae4a5c0fb95 100644 --- a/backend/mbql/test/metabase/mbql/normalize_test.clj +++ b/backend/mbql/test/metabase/mbql/normalize_test.clj @@ -2,366 +2,354 @@ (:require [clojure [set :as set] [test :refer :all]] - [expectations :refer [expect]] [metabase.mbql.normalize :as normalize])) +(defn- tests {:style/indent 2} [f-symb f group->input->expected] + (doseq [[group input->expected] group->input->expected] + (testing group + (doseq [[input expected] input->expected] + (testing (str "\n" (pr-str (list f-symb input))) + (is (= expected + (f input)))))))) + + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | NORMALIZE TOKENS | ;;; +----------------------------------------------------------------------------------------------------------------+ +(defn- normalize-tests {:style/indent 0} [& {:as group->input->expected}] + (tests 'normalize-tokens #'normalize/normalize-tokens group->input->expected)) + (deftest normalize-tokens-test - (doseq [[group query->expected] - {"Query type should get normalized" - {{:type "NATIVE"} - {:type :native}} + (normalize-tests + "Query type should get normalized" + {{:type "NATIVE"} + {:type :native}} - "native queries should NOT get normalized" - {{:type "NATIVE", :native {"QUERY" "SELECT COUNT(*) FROM CANS;"}} - {:type :native, :native {:query "SELECT COUNT(*) FROM CANS;"}} + "native queries should NOT get normalized" + {{:type "NATIVE", :native {"QUERY" "SELECT COUNT(*) FROM CANS;"}} + {:type :native, :native {:query "SELECT COUNT(*) FROM CANS;"}} - {:native {:query {:NAME "FAKE_QUERY" - :description "Theoretical fake query in a JSON-based query lang"}}} - {:native {:query {:NAME "FAKE_QUERY" - :description "Theoretical fake query in a JSON-based query lang"}}}} + {:native {:query {:NAME "FAKE_QUERY" + :description "Theoretical fake query in a JSON-based query lang"}}} + {:native {:query {:NAME "FAKE_QUERY" + :description "Theoretical fake query in a JSON-based query lang"}}}} - "METRICS shouldn't get normalized in some kind of wacky way" - {{:aggregation ["+" ["METRIC" 10] 1]} - {:aggregation [:+ [:metric 10] 1]}} + "METRICS shouldn't get normalized in some kind of wacky way" + {{:aggregation ["+" ["METRIC" 10] 1]} + {:aggregation [:+ [:metric 10] 1]}} - "Nor should SEGMENTS" - {{:filter ["=" ["+" ["SEGMENT" 10] 1] 10]} - {:filter [:= [:+ [:segment 10] 1] 10]}} + "Nor should SEGMENTS" + {{:filter ["=" ["+" ["SEGMENT" 10] 1] 10]} + {:filter [:= [:+ [:segment 10] 1] 10]}} - "are expression names exempt from lisp-casing/lower-casing?" - {{"query" {"expressions" {:sales_tax ["-" ["field-id" 10] ["field-id" 20]]}}} - {:query {:expressions {:sales_tax [:- [:field-id 10] [:field-id 20]]}}}} + "are expression names exempt from lisp-casing/lower-casing?" + {{"query" {"expressions" {:sales_tax ["-" ["field-id" 10] ["field-id" 20]]}}} + {:query {:expressions {:sales_tax [:- [:field-id 10] [:field-id 20]]}}}} - "expression references should be exempt too" - {{:order-by [[:desc [:expression "SALES_TAX"]]]} - {:order-by [[:desc [:expression "SALES_TAX"]]]}} + "expression references should be exempt too" + {{:order-by [[:desc [:expression "SALES_TAX"]]]} + {:order-by [[:desc [:expression "SALES_TAX"]]]}} - "... but they should be converted to strings if passed in as a KW for some reason. Make sure we preserve namespace!" - {{:order-by [[:desc ["expression" :SALES/TAX]]]} - {:order-by [[:desc [:expression "SALES/TAX"]]]}} + "... but they should be converted to strings if passed in as a KW for some reason. Make sure we preserve namespace!" + {{:order-by [[:desc ["expression" :SALES/TAX]]]} + {:order-by [[:desc [:expression "SALES/TAX"]]]}} - "field literals should be exempt too" - {{:order-by [[:desc [:field-literal "SALES_TAX" :type/Number]]]} - {:order-by [[:desc [:field-literal "SALES_TAX" :type/Number]]]}} + "field literals should be exempt too" + {{:order-by [[:desc [:field-literal "SALES_TAX" :type/Number]]]} + {:order-by [[:desc [:field-literal "SALES_TAX" :type/Number]]]}} - "... but they should be converted to strings if passed in as a KW for some reason" - {{:order-by [[:desc ["field_literal" :SALES/TAX "type/Number"]]]} - {:order-by [[:desc [:field-literal "SALES/TAX" :type/Number]]]}}}] - (testing group - (doseq [[query expected] query->expected] - (is (= expected - (#'normalize/normalize-tokens query))))))) + "... but they should be converted to strings if passed in as a KW for some reason" + {{:order-by [[:desc ["field_literal" :SALES/TAX "type/Number"]]]} + {:order-by [[:desc [:field-literal "SALES/TAX" :type/Number]]]}})) ;;; -------------------------------------------------- aggregation --------------------------------------------------- -(expect - {:query {:aggregation :rows}} - (#'normalize/normalize-tokens {:query {"AGGREGATION" "ROWS"}})) - -(expect - {:query {:aggregation [:rows]}} - (#'normalize/normalize-tokens {:query {"AGGREGATION" ["ROWS"]}})) - -(expect - {:query {:aggregation [:count 10]}} - (#'normalize/normalize-tokens {:query {"AGGREGATION" ["COUNT" 10]}})) - -(expect - {:query {:aggregation [[:count 10]]}} - (#'normalize/normalize-tokens {:query {"AGGREGATION" [["COUNT" 10]]}})) - -;; make sure we normalize ag tokens properly when there's wacky MBQL 95 ag syntax -(expect - {:query {:aggregation [:rows :count]}} - (#'normalize/normalize-tokens {:query {:aggregation ["rows" "count"]}})) - -(expect - {:query {:aggregation [:count :count]}} - (#'normalize/normalize-tokens {:query {:aggregation ["count" "count"]}})) - -;; don't normalize names of expression refs! -(expect - {:query {:aggregation [:count [:count [:expression "ABCDEF"]]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["count" ["count" ["expression" "ABCDEF"]]]}})) - -;; make sure binning-strategy clauses get normalized the way we'd expect -(expect - {:query {:breakout [[:binning-strategy 10 :bin-width 2000]]}} - (#'normalize/normalize-tokens {:query {:breakout [["BINNING_STRATEGY" 10 "BIN-WIDTH" 2000]]}})) - -;; or field literals! -(expect - {:query {:aggregation [:count [:count [:field-literal "ABCDEF" :type/Text]]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["count" ["count" ["field_literal" "ABCDEF" "type/Text"]]]}})) - -;; event if you try your best to break things it should handle it -(expect - {:query {:aggregation [:count [:sum 10] [:count 20] :count]}} - (#'normalize/normalize-tokens {:query {:aggregation ["count" ["sum" 10] ["count" 20] "count"]}})) - -;; try an ag that is named using legacy `:named` clause -(expect - {:query {:aggregation [:named [:sum 10] "My COOL AG"]}} - (#'normalize/normalize-tokens {:query {:aggregation ["named" ["SuM" 10] "My COOL AG"]}})) - -(expect - {:query {:aggregation [:named [:sum 10] "My COOL AG" {:use-as-display-name? false}]}} - (#'normalize/normalize-tokens - {:query {:aggregation ["named" ["SuM" 10] "My COOL AG" {:use-as-display-name? false}]}})) - -;; try w/ `:aggregation-options`, the replacement for `:named` -(expect - {:query {:aggregation [:aggregation-options [:sum 10] {:display-name "My COOL AG"}]}} - (#'normalize/normalize-tokens - {:query {:aggregation ["aggregation_options" ["SuM" 10] {"display_name" "My COOL AG"}]}})) - -;; try an expression ag -(expect - {:query {:aggregation [:+ [:sum 10] [:* [:sum 20] 3]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["+" ["sum" 10] ["*" ["SUM" 20] 3]]}})) - -;; expression ags should handle varargs -(expect - {:query {:aggregation [:+ [:sum 10] [:sum 20] [:sum 30]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["+" ["sum" 10] ["SUM" 20] ["sum" 30]]}})) - -;; expression ags should handle datetime arithemtics -(expect - {:query {:expressions {:prev_month [:+ [:field-id 13] [:interval -1 :month]]}}} - (#'normalize/normalize-tokens {:query {:expressions {:prev_month ["+" ["field-id" 13] - ["interval" -1 "month"]]}}})) - -(expect - {:query {:expressions {:prev_month [:- [:field-id 13] [:interval 1 :month] [:interval 1 :day]]}}} - (#'normalize/normalize-tokens {:query {:expressions {:prev_month ["-" ["field-id" 13] - ["interval" 1 "month"] - ["interval" 1 "day"]]}}})) - -;; case -(expect - {:query {:aggregation [:sum [:case [[[:> [:field-id 12] 10] 10] - [[:> [:field-id 12] 100] [:field-id 1]] - [[:= [:field-id 2] 1] "foo"]] - {:default [:field-id 2]}]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["sum" ["case" [[[">" ["field-id" 12] 10] 10] - [[">" ["field-id" 12] 100] ["field-id" 1]] - [["=" ["field-id" 2] 1] "foo"]] - {:default ["field-id" 2]}]]}})) - -(expect - {:query {:aggregation [:median [:field-id 13]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["median" ["field-id" 13]]}})) - -(expect - {:query {:aggregation [:var [:field-id 13]]}} - (#'normalize/normalize-tokens {:query {:aggregation ["var" ["field-id" 13]]}})) - -(expect - {:query {:aggregation [:percentile [:field-id 13] 0.9]}} - (#'normalize/normalize-tokens {:query {:aggregation ["percentile" ["field-id" 13] 0.9]}})) +(deftest normalize-aggregations-test + (normalize-tests + "Legacy 'rows' aggregations" + {{:query {"AGGREGATION" "ROWS"}} + {:query {:aggregation :rows}} + + {:query {"AGGREGATION" ["ROWS"]}} + {:query {:aggregation [:rows]}}} + + "Other uppercase tokens" + {{:query {"AGGREGATION" ["COUNT" 10]}} + {:query {:aggregation [:count 10]}} + + {:query {"AGGREGATION" [["COUNT" 10]]}} + {:query {:aggregation [[:count 10]]}}} + + "make sure we normalize ag tokens properly when there's wacky MBQL 95 ag syntax" + {{:query {:aggregation ["rows" "count"]}} + {:query {:aggregation [:rows :count]}} + + {:query {:aggregation ["count" "count"]}} + {:query {:aggregation [:count :count]}}} + + "don't normalize names of expression refs!" + {{:query {:aggregation ["count" ["count" ["expression" "ABCDEF"]]]}} + {:query {:aggregation [:count [:count [:expression "ABCDEF"]]]}}} + + "make sure binning-strategy clauses get normalized the way we'd expect" + {{:query {:breakout [["BINNING_STRATEGY" 10 "BIN-WIDTH" 2000]]}} + {:query {:breakout [[:binning-strategy 10 :bin-width 2000]]}}} + + "or field literals!" + {{:query {:aggregation ["count" ["count" ["field_literal" "ABCDEF" "type/Text"]]]}} + {:query {:aggregation [:count [:count [:field-literal "ABCDEF" :type/Text]]]}}} + + "event if you try your best to break things it should handle it" + {{:query {:aggregation ["count" ["sum" 10] ["count" 20] "count"]}} + {:query {:aggregation [:count [:sum 10] [:count 20] :count]}}} + + "try an ag that is named using legacy `:named` clause" + {{:query {:aggregation ["named" ["SuM" 10] "My COOL AG"]}} + {:query {:aggregation [:named [:sum 10] "My COOL AG"]}} + + {:query {:aggregation ["named" ["SuM" 10] "My COOL AG" {:use-as-display-name? false}]}} + {:query {:aggregation [:named [:sum 10] "My COOL AG" {:use-as-display-name? false}]}}} + + "try w/ `:aggregation-options`, the replacement for `:named`" + {{:query {:aggregation ["aggregation_options" ["SuM" 10] {"display_name" "My COOL AG"}]}} + {:query {:aggregation [:aggregation-options [:sum 10] {:display-name "My COOL AG"}]}}} + + "try an expression ag" + {{:query {:aggregation ["+" ["sum" 10] ["*" ["SUM" 20] 3]]}} + {:query {:aggregation [:+ [:sum 10] [:* [:sum 20] 3]]}}} + + "expression ags should handle varargs" + {{:query {:aggregation ["+" ["sum" 10] ["SUM" 20] ["sum" 30]]}} + {:query {:aggregation [:+ [:sum 10] [:sum 20] [:sum 30]]}}} + + "expression ags should handle datetime arithemtics" + {{:query {:expressions {:prev_month ["+" ["field-id" 13] ["interval" -1 "month"]]}}} + {:query {:expressions {:prev_month [:+ [:field-id 13] [:interval -1 :month]]}}}, + + {:query {:expressions {:prev_month ["-" ["field-id" 13] ["interval" 1 "month"] ["interval" 1 "day"]]}}} + {:query {:expressions {:prev_month [:- [:field-id 13] [:interval 1 :month] [:interval 1 :day]]}}}} + + "case" + {{:query {:aggregation ["sum" ["case" + [[[">" ["field-id" 12] 10] 10] + [[">" ["field-id" 12] 100] ["field-id" 1]] + [["=" ["field-id" 2] 1] "foo"]] + {:default ["field-id" 2]}]]}} + {:query {:aggregation [:sum [:case + [[[:> [:field-id 12] 10] 10] + [[:> [:field-id 12] 100] [:field-id 1]] + [[:= [:field-id 2] 1] "foo"]] + {:default [:field-id 2]}]]}}} + + "various other new ag types" + {{:query {:aggregation ["median" ["field-id" 13]]}} + {:query {:aggregation [:median [:field-id 13]]}} + + {:query {:aggregation ["var" ["field-id" 13]]}} + {:query {:aggregation [:var [:field-id 13]]}} + + {:query {:aggregation ["percentile" ["field-id" 13] 0.9]}} + {:query {:aggregation [:percentile [:field-id 13] 0.9]}}})) + ;;; ---------------------------------------------------- order-by ---------------------------------------------------- -;; does order-by get properly normalized? -(expect - {:query {:order-by [[10 :asc]]}} - (#'normalize/normalize-tokens {:query {"ORDER_BY" [[10 "ASC"]]}})) +(deftest normalize-order-by-test + (normalize-tests + "does order-by get properly normalized?" + {{:query {"ORDER_BY" [[10 "ASC"]]}} + {:query {:order-by [[10 :asc]]}} -(expect - {:query {:order-by [[:asc 10]]}} - (#'normalize/normalize-tokens {:query {"ORDER_BY" [["ASC" 10]]}})) + {:query {"ORDER_BY" [["ASC" 10]]}} + {:query {:order-by [[:asc 10]]}} -(expect - {:query {:order-by [[[:field-id 10] :asc]]}} - (#'normalize/normalize-tokens {:query {"ORDER_BY" [[["field_id" 10] "ASC"]]}})) + {:query {"ORDER_BY" [[["field_id" 10] "ASC"]]}} + {:query {:order-by [[[:field-id 10] :asc]]}} -(expect - {:query {:order-by [[:desc [:field-id 10]]]}} - (#'normalize/normalize-tokens {:query {"ORDER_BY" [["DESC" ["field_id" 10]]]}})) + {:query {"ORDER_BY" [["DESC" ["field_id" 10]]]}} + {:query {:order-by [[:desc [:field-id 10]]]}}})) ;;; ----------------------------------------------------- filter ----------------------------------------------------- -;; the unit & amount in time interval clauses should get normalized -(expect - {:query {:filter [:time-interval 10 :current :day]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["time-interval" 10 "current" "day"]}})) +(deftest normalize-filter-test + (normalize-tests + "the unit & amount in time interval clauses should get normalized" + {{:query {"FILTER" ["time-interval" 10 "current" "day"]}} + {:query {:filter [:time-interval 10 :current :day]}}} -;; but amount should not get normalized if it's an integer -(expect - {:query {:filter [:time-interval 10 -10 :day]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["time-interval" 10 -10 "day"]}})) + "but amount should not get normalized if it's an integer" + {{:query {"FILTER" ["time-interval" 10 -10 "day"]}} + {:query {:filter [:time-interval 10 -10 :day]}}} -;; make sure we support time-interval options -(expect - [:time-interval 10 -30 :day {:include-current true}] - (#'normalize/normalize-tokens ["TIME_INTERVAL" 10 -30 "DAY" {"include_current" true}])) + "make sure we support time-interval options" + {["TIME_INTERVAL" 10 -30 "DAY" {"include_current" true}] + [:time-interval 10 -30 :day {:include-current true}]} -;; the unit in relative datetime clauses should get normalized -(expect - {:query {:filter [:= [:field-id 10] [:relative-datetime -31 :day]]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["=" [:field-id 10] ["RELATIVE_DATETIME" -31 "DAY"]]}})) + "the unit in relative datetime clauses should get normalized" + {{:query {"FILTER" ["=" [:field-id 10] ["RELATIVE_DATETIME" -31 "DAY"]]}} + {:query {:filter [:= [:field-id 10] [:relative-datetime -31 :day]]}}} -;; should work if we do [:relative-datetime :current] as well -(expect - {:query {:filter [:= [:field-id 10] [:relative-datetime :current]]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["=" [:field-id 10] ["RELATIVE_DATETIME" "CURRENT"]]}})) + "should work if we do [:relative-datetime :current] as well" + {{:query {"FILTER" ["=" [:field-id 10] ["RELATIVE_DATETIME" "CURRENT"]]}} + {:query {:filter [:= [:field-id 10] [:relative-datetime :current]]}}} -;; and in datetime-field clauses (MBQL 98+) -(expect - {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["=" [:datetime-field ["field_id" 10] "day"] "2018-09-05"]}})) + "and in datetime-field clauses (MBQL 98+)" + {{:query {"FILTER" ["=" [:datetime-field ["field_id" 10] "day"] "2018-09-05"]}} + {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}}} -;; (or in long-since-deprecated MBQL 95 format) -(expect - {:query {:filter [:= [:datetime-field 10 :as :day] "2018-09-05"]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["=" [:datetime-field 10 "as" "day"] "2018-09-05"]}})) + "(or in long-since-deprecated MBQL 95 format)" + {{:query {"FILTER" ["=" [:datetime-field 10 "as" "day"] "2018-09-05"]}} + {:query {:filter [:= [:datetime-field 10 :as :day] "2018-09-05"]}}} -;; if string filters have an options map that should get normalized -(expect - {:query {:filter [:starts-with 10 "ABC" {:case-sensitive true}]}} - (#'normalize/normalize-tokens {:query {"FILTER" ["starts_with" 10 "ABC" {"case_sensitive" true}]}})) + "if string filters have an options map that should get normalized" + {{:query {"FILTER" ["starts_with" 10 "ABC" {"case_sensitive" true}]}} + {:query {:filter [:starts-with 10 "ABC" {:case-sensitive true}]}}})) ;;; --------------------------------------------------- parameters --------------------------------------------------- -;; make sure we're not running around trying to normalize the type in native query params -(expect - {:type :native - :parameters [{:type :date/range - :target [:dimension [:template-tag "checkin_date"]] - :value "2015-04-01~2015-05-01"}]} - (#'normalize/normalize-tokens {:type :native - :parameters [{:type "date/range" - :target [:dimension [:template-tag "checkin_date"]] - :value "2015-04-01~2015-05-01"}]})) - -;; oh yeah, also we don't want to go around trying to normalize template-tag names -(expect - {:type :native - :native {:query "SELECT COUNT(*) FROM \"PUBLIC\".\"CHECKINS\" WHERE {{checkin_date}}" - :template-tags {"checkin_date" {:name "checkin_date" - :display-name "Checkin Date" - :type :dimension - :dimension [:field-id 14]}}}} - (#'normalize/normalize-tokens - {:type "native" - :native {:query "SELECT COUNT(*) FROM \"PUBLIC\".\"CHECKINS\" WHERE {{checkin_date}}" - :template_tags {:checkin_date {:name "checkin_date" - :display_name "Checkin Date" - :type :dimension - :dimension ["field-id" 14]}}}})) - -;; native template tags `:type` should get normalized -(expect - {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" - :template-tags {"names_list" {:name "names_list" - :display-name "Names List" - :type :dimension - :dimension [:field-id 49]}}}} - (#'normalize/normalize-tokens - {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" - "template_tags" {:names_list {:name "names_list" - :display_name "Names List" - :type "dimension" - :dimension ["field-id" 49]}}}})) - -;; `:parameters` `:type` should get normalized, but `:value` should not. -(expect - {:type :native - :parameters [{:type :text - :target [:dimension [:template-tag "names_list"]] - :value ["BBQ" "Bakery" "Bar"]}]} - (#'normalize/normalize-tokens - {:type "native" - :parameters [{:type "text" - :target ["dimension" ["template-tag" "names_list"]] - :value ["BBQ" "Bakery" "Bar"]}]})) - -;; make sure normalization doesn't try to parse value as an MBQL clause -(expect - {:type :native - :parameters [{:type :text - :target [:dimension [:template-tag "names_list"]] - :value ["=" 10 20]}]} - (#'normalize/normalize-tokens - {:type "native" - :parameters [{:type "text" - :target ["dimension" ["template-tag" "names_list"]] - :value ["=" 10 20]}]})) +(deftest normalize-parmaeters-test + (normalize-tests + "make sure we're not running around trying to normalize the type in native query params" + {{:type :native + :parameters [{:type "date/range" + :target [:dimension [:template-tag "checkin_date"]] + :value "2015-04-01~2015-05-01"}]} + {:type :native + :parameters [{:type :date/range + :target [:dimension [:template-tag "checkin_date"]] + :value "2015-04-01~2015-05-01"}]}} + + "oh yeah, also we don't want to go around trying to normalize template-tag names" + {{:type "native" + :native {:query "SELECT COUNT(*) FROM \"PUBLIC\".\"CHECKINS\" WHERE {{checkin_date}}" + :template_tags {:checkin_date {:name "checkin_date" + :display_name "Checkin Date" + :type :dimension + :dimension ["field-id" 14]}}}} + {:type :native + :native {:query "SELECT COUNT(*) FROM \"PUBLIC\".\"CHECKINS\" WHERE {{checkin_date}}" + :template-tags {"checkin_date" {:name "checkin_date" + :display-name "Checkin Date" + :type :dimension + :dimension [:field-id 14]}}}}} + + "native template tags `:type` should get normalized" + {{:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" + "template_tags" {:names_list {:name "names_list" + :display_name "Names List" + :type "dimension" + :dimension ["field-id" 49]}}}} + {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" + :template-tags {"names_list" {:name "names_list" + :display-name "Names List" + :type :dimension + :dimension [:field-id 49]}}}}} + + "`:parameters` `:type` should get normalized, but `:value` should not." + {{:type "native" + :parameters [{:type "text" + :target ["dimension" ["template-tag" "names_list"]] + :value ["BBQ" "Bakery" "Bar"]}]} + {:type :native + :parameters [{:type :text + :target [:dimension [:template-tag "names_list"]] + :value ["BBQ" "Bakery" "Bar"]}]}} + + + "make sure normalization doesn't try to parse value as an MBQL clause" + {{:type "native" + :parameters [{:type "text" + :target ["dimension" ["template-tag" "names_list"]] + :value ["=" 10 20]}]} + {:type :native + :parameters [{:type :text + :target [:dimension [:template-tag "names_list"]] + :value ["=" 10 20]}]}})) + ;;; ------------------------------------------------- source queries ------------------------------------------------- -;; Make sure token normalization works correctly on source queries -(expect - {:database 4 - :type :query - :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", - :template-tags {"category" {:name "category" - :display-name "Category" - :type :text - :required true - :default "Widget"}}}}} - (#'normalize/normalize-tokens - {:database 4 - :type :query - :query {"source_query" {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", - "template_tags" {:category {:name "category" - :display-name "Category" - :type "text" - :required true - :default "Widget"}}}}})) - -(expect - {:database 4, - :type :query, - :query {:source-query {:source-table 1, :aggregation :rows}}} - (#'normalize/normalize-tokens - {:database 4 - :type :query - :query {"source_query" {"source_table" 1, "aggregation" "rows"}}})) +(deftest normalize-source-queries-test + (normalize-tests + "Make sure token normalization works correctly on source queries" + {{:database 4 + :type :query + :query {"source_query" {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", + "template_tags" {:category {:name "category" + :display-name "Category" + :type "text" + :required true + :default "Widget"}}}}} + {:database 4 + :type :query + :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", + :template-tags {"category" {:name "category" + :display-name "Category" + :type :text + :required true + :default "Widget"}}}}} + + {:database 4 + :type :query + :query {"source_query" {"source_table" 1, "aggregation" "rows"}}} + {:database 4, + :type :query + :query {:source-query {:source-table 1, :aggregation :rows}}}})) + ;;; ----------------------------------------------------- joins ------------------------------------------------------ -;; do entries in the `:joins` clause get normalized? -(expect - {:database 4 - :type :query - :query {:source-table 1 - :joins [{:source-table 2 - :alias "my/table" - :strategy :left-join - :fields :all}]}} - (#'normalize/normalize-tokens - {:database 4 - :type :query - :query {"source_table" 1 - "Joins" [{"source_table" 2 - "alias" :my/table - "strategy" "left-join" - "fields" "all"}]}})) - -;; what about with a sequence of :fields? -(expect - {:database 4 - :type :query - :query {:source-table 1 - :joins [{:fields [[:field-id 1] - [:field-literal "MY_FIELD" :type/Integer]]}]}} - (#'normalize/normalize-tokens - {:database 4 - :type :query - :query {"source_table" 1 - "joins" [{"fields" [["field_id" 1] ["field_literal" :MY_FIELD "type/Integer"]]}]}})) +(deftest normalize-joins-test + (normalize-tests + "do entries in the `:joins` clause get normalized?" + {{:database 4 + :type :query + :query {"source_table" 1 + "Joins" [{"source_table" 2 + "alias" :my/table + "strategy" "left-join" + "fields" "all"}]}} + {:database 4 + :type :query + :query {:source-table 1 + :joins [{:source-table 2 + :alias "my/table" + :strategy :left-join + :fields :all}]}}} + + "what about with a sequence of :fields?" + {{:database 4 + :type :query + :query {"source_table" 1 + "joins" [{"fields" [["field_id" 1] ["field_literal" :MY_FIELD "type/Integer"]]}]}} + {:database 4 + :type :query + :query {:source-table 1 + :joins [{:fields [[:field-id 1] + [:field-literal "MY_FIELD" :type/Integer]]}]}}} + + "do `:joins` inside a nested query get normalized?" + {{:database 4 + :type :query + :query {"source_query" + {"source_table" 1 + "joins" + [{"strategy" "right-join" + "fields" [["field_id" 1] ["field_literal" :MY_FIELD "type/Integer"]]}]}}} + {:database 4 + :type :query + :query {:source-query {:source-table 1 + :joins [{:strategy :right-join + :fields [[:field-id 1] + [:field-literal "MY_FIELD" :type/Integer]]}]}}}})) (deftest normalize-source-query-in-joins-test (testing "does a `:source-query` in `:joins` get normalized?" @@ -388,698 +376,617 @@ (#'normalize/normalize-tokens (query-with-joins [{"source_query" {"NATIVE" {"this_is_a_native_query" "TRUE"}}}]))))))))) -;; do `:joins` inside a nested query get normalized? -(expect - {:database 4 - :type :query - :query {:source-query {:source-table 1 - :joins [{:strategy :right-join - :fields [[:field-id 1] - [:field-literal "MY_FIELD" :type/Integer]]}]}}} - (#'normalize/normalize-tokens - {:database 4 - :type :query - :query {"source_query" - {"source_table" 1 - "joins" - [{"strategy" "right-join" - "fields" [["field_id" 1] ["field_literal" :MY_FIELD "type/Integer"]]}]}}})) - ;;; ----------------------------------------------------- other ------------------------------------------------------ -;; Does the QueryExecution context get normalized? -(expect - {:context :json-download} - (#'normalize/normalize-tokens {:context "json-download"})) +(deftest normalize-execution-context-test + (normalize-tests + "Does the QueryExecution context get normalized?" + {{:context "json-download"} + {:context :json-download}} -;; if `:context` is `nil` it's not our problem -(expect - {:context nil} - (#'normalize/normalize-tokens {:context nil})) + "if `:context` is `nil` it's not our problem" + {{:context nil} + {:context nil}})) (deftest params-normalization-test - (is (= {:native {:query "SELECT * FROM venues WHERE name = ?" - :params ["Red Medicine"]}} - (#'normalize/normalize-tokens - {:native {:query "SELECT * FROM venues WHERE name = ?" - :params ["Red Medicine"]}})) - ":native :params shouldn't get normalized.")) + (normalize-tests + ":native :params shouldn't get normalized." + {{:native {:query "SELECT * FROM venues WHERE name = ?" + :params ["Red Medicine"]}} + {:native {:query "SELECT * FROM venues WHERE name = ?" + :params ["Red Medicine"]}}})) (deftest normalize-projections-test - (testing "Native :projections shouldn't get normalized." - (is (= {:type :native - :native {:projections ["_id" "name" "category_id" "latitude" "longitude" "price"]}} - (#'normalize/normalize-tokens - {:type :native - :native {:projections ["_id" "name" "category_id" "latitude" "longitude" "price"]}}))))) + (normalize-tests + "Native :projections shouldn't get normalized." + {{:type :native + :native {:projections ["_id" "name" "category_id" "latitude" "longitude" "price"]}} + {:type :native + :native {:projections ["_id" "name" "category_id" "latitude" "longitude" "price"]}}})) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | CANONICALIZE | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; Does our `wrap-implict-field-id` fn work? -(expect - [:field-id 10] - (#'normalize/wrap-implicit-field-id 10)) +(defn- canonicalize-tests {:style/indent 0} [& {:as group->input->expected}] + (tests 'canonicalize #'normalize/canonicalize group->input->expected)) + +(deftest wrap-implicit-field-id-test + (testing "Does our `wrap-implict-field-id` fn work?" + (doseq [input [10 [:field-id 10]]] + (testing (pr-str (list 'wrap-implicit-field-id input)) + (is (= [:field-id 10] + (#'normalize/wrap-implicit-field-id input))))))) + -(expect - [:field-id 10] - (#'normalize/wrap-implicit-field-id [:field-id 10])) +;;; ------------------------------------------------ binning strategy ------------------------------------------------ -;; make sure `binning-strategy` wraps implicit Field IDs -(expect - {:query {:breakout [[:binning-strategy [:field-id 10] :bin-width 2000]]}} - (#'normalize/canonicalize {:query {:breakout [[:binning-strategy 10 :bin-width 2000]]}})) +(deftest canonicalize-binning-strategy-test + (canonicalize-tests + "make sure `binning-strategy` wraps implicit Field IDs" + {{:query {:breakout [[:binning-strategy 10 :bin-width 2000]]}} + {:query {:breakout [[:binning-strategy [:field-id 10] :bin-width 2000]]}}})) ;;; -------------------------------------------------- aggregation --------------------------------------------------- -;; Do aggregations get canonicalized properly? -;; field ID should get wrapped in field-id and ags should be converted to multiple ag syntax -(expect - {:query {:aggregation [[:count [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:aggregation [:count 10]}})) - -;; ag with no Field ID -(expect - {:query {:aggregation [[:count]]}} - (#'normalize/canonicalize {:query {:aggregation [:count]}})) - -;; if already wrapped in field-id it's ok -(expect - {:query {:aggregation [[:count [:field-id 1000]]]}} - (#'normalize/canonicalize {:query {:aggregation [:count [:field-id 1000]]}})) - -;; ags in the canonicalized format should pass thru ok -(expect - {:query {:aggregation [[:metric "ga:sessions"] [:metric "ga:1dayUsers"]]}} - (#'normalize/canonicalize - {:query {:aggregation [[:metric "ga:sessions"] [:metric "ga:1dayUsers"]]}})) - -;; :rows aggregation type, being deprecated since FOREVER, should just get removed -(expect - {:query {:aggregation []}} - (#'normalize/canonicalize {:query {:aggregation [:rows]}})) - -(expect - {:query {:aggregation []}} - (#'normalize/canonicalize {:query {:aggregation :rows}})) - -;; if just a single aggregation is supplied it should always be converted to new-style multiple-aggregation syntax -(expect - {:query {:aggregation [[:count]]}} - (#'normalize/canonicalize {:query {:aggregation :count}})) - -;; make sure we handle single :count with :field-id correctly -(expect - {:query {:aggregation [[:count [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:aggregation [:count [:field-id 10]]}})) - -;; make sure for multiple aggregations we can handle `:count` that doesn't appear wrapped in brackets -(expect - {:query {:aggregation [[:count] [:sum [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:aggregation [:count [:sum 10]]}})) - -;; this doesn't make sense, but make sure if someone specifies a `:rows` ag and another one we don't end up with a -;; `nil` in the ags list -(expect - {:query {:aggregation [[:count]]}} - (#'normalize/canonicalize {:query {:aggregation [:rows :count]}})) - -;; another stupid aggregation that we need to be able to handle -(expect - {:query {:aggregation [[:count] [:count]]}} - (#'normalize/canonicalize {:query {:aggregation [:count :count]}})) - -;; a mix of unwrapped & wrapped should still work -(expect - {:query {:aggregation [[:count] [:sum [:field-id 10]] [:count [:field-id 20]] [:count]]}} - (#'normalize/canonicalize {:query {:aggregation [:count [:sum 10] [:count 20] :count]}})) - -;; legacy `:named` aggregation clauses should get converted to `:aggregation-options` -(expect - {:query {:aggregation [[:aggregation-options [:sum [:field-id 10]] {:display-name "Sum *TEN*"}]]}} - (#'normalize/canonicalize {:query {:aggregation [:named [:sum 10] "Sum *TEN*"]}})) - -(expect - {:query {:aggregation [[:aggregation-options [:sum [:field-id 10]] {:name "Sum *TEN*"}]]}} - (#'normalize/canonicalize {:query {:aggregation [:named [:sum 10] "Sum *TEN*" {:use-as-display-name? false}]}})) - -;; subclauses of `:aggregation-options` should get canonicalized correctly -(expect - {:query {:aggregation [[:aggregation-options] [:sum [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:aggregation [:aggregation-options [:sum 10] {}]}})) - -;; make sure expression aggregations work correctly -(expect - {:query {:aggregation [[:+ [:sum [:field-id 10]] 2]]}} - (#'normalize/canonicalize {:query {:aggregation [:+ [:sum 10] 2]}})) - -(expect - {:query {:aggregation [[:+ [:sum [:field-id 10]] [:* [:sum [:field-id 20]] [:sum [:field-id 30]]]]]}} - (#'normalize/canonicalize {:query {:aggregation [:+ [:sum 10] [:* [:sum 20] [:sum 30]]]}})) - -;; expression ags should handle varargs -(expect - {:query {:aggregation [[:+ [:sum [:field-id 10]] [:sum [:field-id 20]] [:sum [:field-id 30]]]]}} - (#'normalize/canonicalize {:query {:aggregation [[:+ [:sum 10] [:sum 20] [:sum 30]]]}})) - -;; METRICS shouldn't get canonicalized in some kind of wacky way -(expect - {:query {:aggregation [[:+ [:metric 1] 2]]}} - (#'normalize/canonicalize {:query {:aggregation [:+ [:metric 1] 2]}})) - -;; can cumulative-count be handled with or without a Field? -(expect - {:query {:aggregation [[:cum-count]]}} - (#'normalize/canonicalize {:query {:aggregation [:cum-count]}})) - -(expect - {:query {:aggregation [[:cum-count [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:aggregation [:cum-count 10]}})) - -;; should handle seqs without a problem -(expect - {:query {:aggregation [[:min [:field-id 1]] [:min [:field-id 2]]]}} - (#'normalize/canonicalize {:query {:aggregation '([:min 1] [:min 2])}})) - -;; make sure canonicalization can handle aggregations with expressions where the Field normally goes -(expect - {:query {:aggregation [[:sum [:* [:field-id 4] [:field-id 1]]]]}} - (#'normalize/canonicalize - {:query {:aggregation [[:sum [:* [:field-id 4] [:field-id 1]]]]}})) +(deftest canonicalize-aggregations-test + (canonicalize-tests + "field ID should get wrapped in field-id and ags should be converted to multiple ag syntax" + {{:query {:aggregation [:count 10]}} + {:query {:aggregation [[:count [:field-id 10]]]}}} + + "ag with no Field ID" + {{:query {:aggregation [:count]}} + {:query {:aggregation [[:count]]}}} + + "if already wrapped in field-id it's ok" + {{:query {:aggregation [:count [:field-id 1000]]}} + {:query {:aggregation [[:count [:field-id 1000]]]}}} + + "ags in the canonicalized format should pass thru ok" + {{:query {:aggregation [[:metric "ga:sessions"] [:metric "ga:1dayUsers"]]}} + {:query {:aggregation [[:metric "ga:sessions"] [:metric "ga:1dayUsers"]]}}} + + ":rows aggregation type, being deprecated since FOREVER, should just get removed" + {{:query {:aggregation [:rows]}} + {:query {:aggregation []}} + + {:query {:aggregation :rows}} + {:query {:aggregation []}}} + + "if just a single aggregation is supplied it should always be converted to new-style multiple-aggregation syntax" + {{:query {:aggregation :count}} + {:query {:aggregation [[:count]]}}} + + "make sure we handle single :count with :field-id correctly" + {{:query {:aggregation [:count [:field-id 10]]}} + {:query {:aggregation [[:count [:field-id 10]]]}}} + + "make sure for multiple aggregations we can handle `:count` that doesn't appear wrapped in brackets" + {{:query {:aggregation [:count [:sum 10]]}} + {:query {:aggregation [[:count] [:sum [:field-id 10]]]}}} + + (str "this doesn't make sense, but make sure if someone specifies a `:rows` ag and another one we don't end up " + "with a `nil` in the ags list") + {{:query {:aggregation [:rows :count]}} + {:query {:aggregation [[:count]]}}} + + "another stupid aggregation that we need to be able to handle" + {{:query {:aggregation [:count :count]}} + {:query {:aggregation [[:count] [:count]]}}} + + "a mix of unwrapped & wrapped should still work" + {{:query {:aggregation [:count [:sum 10] [:count 20] :count]}} + {:query {:aggregation [[:count] [:sum [:field-id 10]] [:count [:field-id 20]] [:count]]}}} + + "legacy `:named` aggregation clauses should get converted to `:aggregation-options`" + {{:query {:aggregation [:named [:sum 10] "Sum *TEN*"]}} + {:query {:aggregation [[:aggregation-options [:sum [:field-id 10]] {:display-name "Sum *TEN*"}]]}} + + {:query {:aggregation [:named [:sum 10] "Sum *TEN*" {:use-as-display-name? false}]}} + {:query {:aggregation [[:aggregation-options [:sum [:field-id 10]] {:name "Sum *TEN*"}]]}}} + + "subclauses of `:aggregation-options` should get canonicalized correctly" + {{:query {:aggregation [:aggregation-options [:sum 10] {}]}} + {:query {:aggregation [[:aggregation-options] [:sum [:field-id 10]]]}}} + + "make sure expression aggregations work correctly" + {{:query {:aggregation [:+ [:sum 10] 2]}} + {:query {:aggregation [[:+ [:sum [:field-id 10]] 2]]}} + + {:query {:aggregation [:+ [:sum 10] [:* [:sum 20] [:sum 30]]]}} + {:query {:aggregation [[:+ [:sum [:field-id 10]] [:* [:sum [:field-id 20]] [:sum [:field-id 30]]]]]}}} + + "expression ags should handle varargs" + {{:query {:aggregation [[:+ [:sum 10] [:sum 20] [:sum 30]]]}} + {:query {:aggregation [[:+ [:sum [:field-id 10]] [:sum [:field-id 20]] [:sum [:field-id 30]]]]}}} + + "METRICS shouldn't get canonicalized in some kind of wacky way" + {{:query {:aggregation [:+ [:metric 1] 2]}} + {:query {:aggregation [[:+ [:metric 1] 2]]}}} + + "can cumulative-count be handled with or without a Field?" + {{:query {:aggregation [:cum-count]}} + {:query {:aggregation [[:cum-count]]}} + + {:query {:aggregation [:cum-count 10]}} + {:query {:aggregation [[:cum-count [:field-id 10]]]}}} + + "should handle seqs without a problem" + {{:query {:aggregation '([:min 1] [:min 2])}} + {:query {:aggregation [[:min [:field-id 1]] [:min [:field-id 2]]]}}} + + "make sure canonicalization can handle aggregations with expressions where the Field normally goes" + {{:query {:aggregation [[:sum [:* [:field-id 4] [:field-id 1]]]]}} + {:query {:aggregation [[:sum [:* [:field-id 4] [:field-id 1]]]]}}})) ;;; ---------------------------------------------------- breakout ---------------------------------------------------- -;; implicit Field IDs should get wrapped in [:field-id] in :breakout -(expect - {:query {:breakout [[:field-id 10]]}} - (#'normalize/canonicalize {:query {:breakout [10]}})) +(deftest canonicalize-breakout-test + (canonicalize-tests + "implicit Field IDs should get wrapped in [:field-id] in :breakout" + {{:query {:breakout [10]}} + {:query {:breakout [[:field-id 10]]}} -(expect - {:query {:breakout [[:field-id 10] [:field-id 20]]}} - (#'normalize/canonicalize {:query {:breakout [10 20]}})) + {:query {:breakout [10 20]}} + {:query {:breakout [[:field-id 10] [:field-id 20]]}}} -;; should handle seqs -(expect - {:query {:breakout [[:field-id 10] [:field-id 20]]}} - (#'normalize/canonicalize {:query {:breakout '(10 20)}})) + "should handle seqs" + {{:query {:breakout '(10 20)}} + {:query {:breakout [[:field-id 10] [:field-id 20]]}} -(expect - {:query {:breakout [[:field-id 1000]]}} - (#'normalize/canonicalize {:query {:breakout [[:field-id 1000]]}})) + {:query {:breakout [[:field-id 1000]]}} + {:query {:breakout [[:field-id 1000]]}}})) ;;; ----------------------------------------------------- fields ----------------------------------------------------- -(expect - {:query {:fields [[:field-id 10]]}} - (#'normalize/canonicalize {:query {:fields [10]}})) +(deftest canonicalize-fields-test + (canonicalize-tests + "implicit Field IDs should get wrapped in [:field-id] in :fields" + {{:query {:fields [10]}} + {:query {:fields [[:field-id 10]]}} -;; implicit Field IDs should get wrapped in [:field-id] in :fields -(expect - {:query {:fields [[:field-id 10] [:field-id 20]]}} - (#'normalize/canonicalize {:query {:fields [10 20]}})) + {:query {:fields [10 20]}} + {:query {:fields [[:field-id 10] [:field-id 20]]}} -(expect - {:query {:fields [[:field-id 1000]]}} - (#'normalize/canonicalize {:query {:fields [[:field-id 1000]]}})) + {:query {:fields [[:field-id 1000]]}} + {:query {:fields [[:field-id 1000]]}}} -;; should handle seqs -(expect - {:query {:fields [[:field-id 10] [:field-id 20]]}} - (#'normalize/canonicalize {:query {:fields '(10 20)}})) + "should handle seqs" + {{:query {:fields '(10 20)}} + {:query {:fields [[:field-id 10] [:field-id 20]]}}})) ;;; ----------------------------------------------------- filter ----------------------------------------------------- -;; implicit Field IDs should get wrapped in [:field-id] in filters -(expect - {:query {:filter [:= [:field-id 10] 20]}} - (#'normalize/canonicalize {:query {:filter [:= 10 20]}})) - -(expect - {:query {:filter [:and [:= [:field-id 10] 20] [:= [:field-id 20] 30]]}} - (#'normalize/canonicalize {:query {:filter [:and [:= 10 20] [:= 20 30]]}})) - -(expect - {:query {:filter [:between [:field-id 10] 20 30]}} - (#'normalize/canonicalize {:query {:filter [:between 10 20 30]}})) - -;; `:inside` filters should get implict Field IDs for the first two args -(expect - {:query {:filter [:inside [:field-id 1] [:field-id 2] 90 -90 90 -90]}} - (#'normalize/canonicalize {:query {:filter [:inside 1 2 90 -90 90 -90]}})) - -;; compound filters with only one arg should get automatically de-compounded -(expect - {:query {:filter [:= [:field-id 100] 2]}} - (#'normalize/canonicalize {:query {:filter [:and [:= 100 2]]}})) - -(expect - {:query {:filter [:= [:field-id 100] 2]}} - (#'normalize/canonicalize {:query {:filter [:or [:= 100 2]]}})) - -;; compound filters should "pull-up" any args that are the same compound filter -(expect - {:query {:filter [:and - [:= [:field-id 100] 1] - [:= [:field-id 200] 2] - [:= [:field-id 300] 3] - [:= [:field-id 400] 4]]}} - (#'normalize/canonicalize {:query {:filter [:and - [:and - [:= [:field-id 100] 1] - [:= [:field-id 200] 2]] - [:and - [:= [:field-id 300] 3] - [:= [:field-id 400] 4]]]}})) -(expect - {:query {:filter [:and - [:> [:field-id 4] 1] - [:is-null [:field-id 7]] - [:= [:field-id 5] "abc"] - [:between [:field-id 9] 0 25]]}} - (#'normalize/canonicalize {:query {:filter [:and - [:> [:field-id 4] 1] - [:is-null [:field-id 7]] - [:and - [:= [:field-id 5] "abc"] - [:between [:field-id 9] 0 25]]]}})) - -(expect - {:query {:filter [:or - [:= [:field-id 100] 1] - [:= [:field-id 200] 2] - [:= [:field-id 300] 3] - [:= [:field-id 400] 4]]}} - (#'normalize/canonicalize {:query {:filter [:or - [:or - [:= 100 1] - [:= 200 2]] - [:or - [:= [:field-id 300] 3] - [:= [:field-id 400] 4]]]}})) - -(expect - {:query {:filter [:or - [:> [:field-id 4] 1] - [:is-null [:field-id 7]] - [:= [:field-id 5] "abc"] - [:between [:field-id 9] 0 25]]}} - (#'normalize/canonicalize {:query {:filter [:or - [:> [:field-id 4] 1] - [:is-null [:field-id 7]] - [:or - [:= [:field-id 5] "abc"] - [:between [:field-id 9] 0 25]]]}})) - -;; not inside of a not should get elimated entirely -(expect - {:query {:filter [:= [:field-id 100] 1]}} - (#'normalize/canonicalize {:query {:filter [:not [:not [:= [:field-id 100] 1]]]}})) - -;; make sure we don't overwrite options if specified -(expect - {:query {:filter [:contains [:field-id 10] "ABC" {:case-sensitive false}]}} - (#'normalize/canonicalize {:query {:filter [:contains 10 "ABC" {:case-sensitive false}]}})) - -;; or for time-interval options -(expect - [:time-interval 10 -30 :day {:include-current true}] - (#'normalize/canonicalize [:time-interval 10 -30 :day {:include-current true}])) - -;; make sure empty filter clauses don't explode in canonicalize -(expect - {:database 1, :type :query, :query {:filter []}} - (#'normalize/canonicalize - {:database 1 - :type :query - :query {:filter []}})) - -;; make sure we can handle GA segments -(expect - {:database 1, - :type :query - :query {:filter - [:and - [:segment "gaid:-11"] - [:time-interval [:field-id 6851] -365 :day {}]]}} - (#'normalize/canonicalize - {:database 1 - :type :query - :query {:filter [:and - [:segment "gaid:-11"] - [:time-interval [:field-id 6851] -365 :day {}]]}})) - -;; should handle seqs -(expect - {:query {:filter [:and [:= [:field-id 100] 1] [:= [:field-id 200] 2]]}} - (#'normalize/canonicalize {:query {:filter '(:and - [:= 100 1] - [:= 200 2])}})) - -;; if you put a `:datetime-field` inside a `:time-interval` we should fix it for you -(expect - {:query {:filter [:time-interval [:field-id 8] -30 :day]}} - (#'normalize/canonicalize {:query {:filter [:time-interval [:datetime-field [:field-id 8] :month] -30 :day]}})) - -;; fk-> clauses should get the field-id treatment -(expect - {:query {:filter [:= [:fk-> [:field-id 10] [:field-id 20]] "ABC"]}} - (#'normalize/canonicalize {:query {:filter [:= [:fk-> 10 20] "ABC"]}})) - -;; as should datetime-field clauses... -(expect - {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}} - (#'normalize/canonicalize {:query {:filter [:= [:datetime-field 10 :day] "2018-09-05"]}})) - -;; MBQL 95 datetime-field clauses ([:datetime-field <field> :as <unit>]) should get converted to MBQL 2000 -(expect - {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}} - (#'normalize/canonicalize {:query {:filter [:= [:datetime-field 10 :as :day] "2018-09-05"]}})) - -;; if someone is dumb and passes something like a field-literal inside a field-id, fix it for them. -(expect - {:query {:filter [:= [:field-literal "my_field" "type/Number"] 10]}} - (#'normalize/canonicalize {:query {:filter [:= [:field-id [:field-literal "my_field" "type/Number"]] 10]}})) - -;; we should fix :field-ids inside :field-ids too -(expect - {:query {:filter [:= [:field-id 1] 10]}} - (#'normalize/canonicalize {:query {:filter [:= [:field-id [:field-id 1]] 10]}})) - -;; we should handle seqs no prob -(expect - {:query {:filter [:= [:field-id 1] 10]}} - (#'normalize/canonicalize {:query {:filter '(:= 1 10)}})) +(deftest canonicalize-filter-test + (canonicalize-tests + "implicit Field IDs should get wrapped in [:field-id] in filters" + {{:query {:filter [:= 10 20]}} + {:query {:filter [:= [:field-id 10] 20]}} + + {:query {:filter [:and [:= 10 20] [:= 20 30]]}} + {:query {:filter [:and [:= [:field-id 10] 20] [:= [:field-id 20] 30]]}} + + {:query {:filter [:between 10 20 30]}} + {:query {:filter [:between [:field-id 10] 20 30]}}} + + "`:inside` filters should get implict Field IDs for the first two args" + {{:query {:filter [:inside 1 2 90 -90 90 -90]}} + {:query {:filter [:inside [:field-id 1] [:field-id 2] 90 -90 90 -90]}}} + + "compound filters with only one arg should get automatically de-compounded" + {{:query {:filter [:and [:= 100 2]]}} + {:query {:filter [:= [:field-id 100] 2]}} + + {:query {:filter [:or [:= 100 2]]}} + {:query {:filter [:= [:field-id 100] 2]}}} + + "compound filters should \"pull-up\" any args that are the same compound filter" + {{:query {:filter [:and + [:and + [:= [:field-id 100] 1] + [:= [:field-id 200] 2]] + [:and + [:= [:field-id 300] 3] + [:= [:field-id 400] 4]]]}} + {:query {:filter [:and + [:= [:field-id 100] 1] + [:= [:field-id 200] 2] + [:= [:field-id 300] 3] + [:= [:field-id 400] 4]]}} + + {:query {:filter [:and + [:> [:field-id 4] 1] + [:is-null [:field-id 7]] + [:and + [:= [:field-id 5] "abc"] + [:between [:field-id 9] 0 25]]]}} + {:query {:filter [:and + [:> [:field-id 4] 1] + [:is-null [:field-id 7]] + [:= [:field-id 5] "abc"] + [:between [:field-id 9] 0 25]]}} + + {:query {:filter [:or + [:or + [:= 100 1] + [:= 200 2]] + [:or + [:= [:field-id 300] 3] + [:= [:field-id 400] 4]]]}} + {:query {:filter [:or + [:= [:field-id 100] 1] + [:= [:field-id 200] 2] + [:= [:field-id 300] 3] + [:= [:field-id 400] 4]]}} + + {:query {:filter [:or + [:> [:field-id 4] 1] + [:is-null [:field-id 7]] + [:or + [:= [:field-id 5] "abc"] + [:between [:field-id 9] 0 25]]]}} + {:query {:filter [:or + [:> [:field-id 4] 1] + [:is-null [:field-id 7]] + [:= [:field-id 5] "abc"] + [:between [:field-id 9] 0 25]]}}} + + "not inside of a not should get elimated entirely" + {{:query {:filter [:not [:not [:= [:field-id 100] 1]]]}} + {:query {:filter [:= [:field-id 100] 1]}}} + + "make sure we don't overwrite options if specified" + {{:query {:filter [:contains 10 "ABC" {:case-sensitive false}]}} + {:query {:filter [:contains [:field-id 10] "ABC" {:case-sensitive false}]}}} + + "or for time-interval options" + {[:time-interval 10 -30 :day {:include-current true}] + [:time-interval 10 -30 :day {:include-current true}]} + + "make sure empty filter clauses don't explode in canonicalize" + {{:database 1, :type :query, :query {:filter []}} + {:database 1, :type :query, :query {:filter []}}} + + "make sure we can handle GA segments" + {{:database 1 + :type :query + :query {:filter [:and + [:segment "gaid:-11"] + [:time-interval [:field-id 6851] -365 :day {}]]}} + {:database 1, + :type :query + :query {:filter + [:and + [:segment "gaid:-11"] + [:time-interval [:field-id 6851] -365 :day {}]]}}} + + "should handle seqs" + {{:query {:filter '(:and + [:= 100 1] + [:= 200 2])}} + {:query {:filter [:and [:= [:field-id 100] 1] [:= [:field-id 200] 2]]}}} + + "if you put a `:datetime-field` inside a `:time-interval` we should fix it for you" + {{:query {:filter [:time-interval [:datetime-field [:field-id 8] :month] -30 :day]}} + {:query {:filter [:time-interval [:field-id 8] -30 :day]}}} + + "fk-> clauses should get the field-id treatment" + {{:query {:filter [:= [:fk-> 10 20] "ABC"]}} + {:query {:filter [:= [:fk-> [:field-id 10] [:field-id 20]] "ABC"]}}} + + "as should datetime-field clauses..." + {{:query {:filter [:= [:datetime-field 10 :day] "2018-09-05"]}} + {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}}} + + "MBQL 95 datetime-field clauses ([:datetime-field <field> :as <unit>]) should get converted to MBQL 2000" + {{:query {:filter [:= [:datetime-field 10 :as :day] "2018-09-05"]}} + {:query {:filter [:= [:datetime-field [:field-id 10] :day] "2018-09-05"]}}} + + "if someone is dumb and passes something like a field-literal inside a field-id, fix it for them." + {{:query {:filter [:= [:field-id [:field-literal "my_field" "type/Number"]] 10]}} + {:query {:filter [:= [:field-literal "my_field" "type/Number"] 10]}}} + + "we should fix :field-ids inside :field-ids too" + {{:query {:filter [:= [:field-id [:field-id 1]] 10]}} + {:query {:filter [:= [:field-id 1] 10]}}} + + "we should handle seqs no prob" + {{:query {:filter '(:= 1 10)}} + {:query {:filter [:= [:field-id 1] 10]}}})) ;;; ---------------------------------------------------- order-by ---------------------------------------------------- -;; ORDER BY: MBQL 95 [field direction] should get translated to MBQL 98+ [direction field] -(expect - {:query {:order-by [[:asc [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:order-by [[[:field-id 10] :asc]]}})) +(deftest canonicalize-order-by-test + (canonicalize-tests + "ORDER BY: MBQL 95 [field direction] should get translated to MBQL 98+ [direction field]" + {{:query {:order-by [[[:field-id 10] :asc]]}} + {:query {:order-by [[:asc [:field-id 10]]]}}} + + "MBQL 95 old order-by names should be handled" + {{:query {:order-by [[10 :ascending]]}} + {:query {:order-by [[:asc [:field-id 10]]]}}} -;; MBQL 95 old order-by names should be handled -(expect - {:query {:order-by [[:asc [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:order-by [[10 :ascending]]}})) + "field-id should be added if needed" + {{:query {:order-by [[10 :asc]]}} + {:query {:order-by [[:asc [:field-id 10]]]}} -;; field-id should be added if needed -(expect - {:query {:order-by [[:asc [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:order-by [[10 :asc]]}})) + {:query {:order-by [[:asc 10]]}} + {:query {:order-by [[:asc [:field-id 10]]]}}} -(expect - {:query {:order-by [[:asc [:field-id 10]]]}} - (#'normalize/canonicalize {:query {:order-by [[:asc 10]]}})) -;; we should handle seqs no prob -(expect - {:query {:order-by [[:asc [:field-id 1]]]}} - (#'normalize/canonicalize {:query {:order-by '((1 :ascending))}})) + "we should handle seqs no prob" + {{:query {:order-by '((1 :ascending))}} + {:query {:order-by [[:asc [:field-id 1]]]}}} -;; duplicate order-by clauses should get removed -(expect - {:query {:order-by [[:asc [:field-id 1]] - [:desc [:field-id 2]]]}} - (#'normalize/canonicalize {:query {:order-by [[:asc [:field-id 1]] - [:desc [:field-id 2]] - [:asc 1]]}})) + "duplicate order-by clauses should get removed" + {{:query {:order-by [[:asc [:field-id 1]] + [:desc [:field-id 2]] + [:asc 1]]}} + {:query {:order-by [[:asc [:field-id 1]] + [:desc [:field-id 2]]]}}})) ;;; ------------------------------------------------- source queries ------------------------------------------------- -;; Make sure canonicalization works correctly on source queries -(expect - {:database 4 - :type :query - :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", - :template-tags {"category" {:name "category" - :display-name "Category" - :type :text - :required true - :default "Widget"}}}}} - ;; nothing to really do here - (#'normalize/canonicalize - {:database 4 - :type :query - :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10" - :template-tags {"category" {:name "category" - :display-name "Category" - :type :text - :required true - :default "Widget"}}}}})) - -;; make sure we recursively canonicalize source queries -(expect - {:database 4 - :type :query - :query {:source-query {:source-table 1, :aggregation []}}} - (#'normalize/canonicalize - {:database 4 - :type :query - :query {:source-query {:source-table 1, :aggregation :rows}}})) +(deftest canonicalize-source-queries-test + (canonicalize-tests + "Make sure canonicalization works correctly on source queries" + {{:database 4 + :type :query + :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10" + :template-tags {"category" {:name "category" + :display-name "Category" + :type :text + :required true + :default "Widget"}}}}} + {:database 4 + :type :query + :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", + :template-tags {"category" {:name "category" + :display-name "Category" + :type :text + :required true + :default "Widget"}}}}}} + + "make sure we recursively canonicalize source queries" + {{:database 4 + :type :query + :query {:source-query {:source-table 1, :aggregation :rows}}} + {:database 4 + :type :query + :query {:source-query {:source-table 1, :aggregation []}}}})) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | WHOLE-QUERY TRANSFORMATIONS | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; If you specify a field in a breakout and in the Fields clause, we should go ahead and remove it from the Fields -;; clause, because it is (obviously) implied that you should get that Field back. -(expect - {:type :query - :query {:breakout [[:field-id 1] [:field-id 2]] - :fields [[:field-id 3]]}} - (#'normalize/perform-whole-query-transformations - {:type :query - :query {:breakout [[:field-id 1] [:field-id 2]] - :fields [[:field-id 2] [:field-id 3]]}})) - -;; should work with FKs -(expect - {:type :query - :query {:breakout [[:field-id 1] - [:fk-> [:field-id 2] [:field-id 4]]] - :fields [[:field-id 3]]}} - (#'normalize/perform-whole-query-transformations - {:type :query - :query {:breakout [[:field-id 1] - [:fk-> [:field-id 2] [:field-id 4]]] - :fields [[:fk-> [:field-id 2] [:field-id 4]] - [:field-id 3]]}})) - -;; should work if the Field is bucketed in the breakout & in fields -(expect - {:type :query - :query {:breakout [[:field-id 1] - [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] - :fields [[:field-id 3]]}} - (#'normalize/perform-whole-query-transformations - {:type :query - :query {:breakout [[:field-id 1] - [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] - :fields [[:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month] - [:field-id 3]]}})) - -;; should work if the Field is bucketed in the breakout but not in fields -(expect - {:type :query - :query {:breakout [[:field-id 1] - [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] - :fields [[:field-id 3]]}} - (#'normalize/perform-whole-query-transformations - {:type :query - :query {:breakout [[:field-id 1] - [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] - :fields [[:fk-> [:field-id 2] [:field-id 4]] - [:field-id 3]]}})) +(deftest whole-query-transformations-test + (tests 'perform-whole-query-transformations #'normalize/perform-whole-query-transformations + {(str "If you specify a field in a breakout and in the Fields clause, we should go ahead and remove it from the " + "Fields clause, because it is (obviously) implied that you should get that Field back.") + {{:type :query + :query {:breakout [[:field-id 1] [:field-id 2]] + :fields [[:field-id 2] [:field-id 3]]}} + {:type :query + :query {:breakout [[:field-id 1] [:field-id 2]] + :fields [[:field-id 3]]}}} + + "should work with FKs" + {{:type :query + :query {:breakout [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 4]]] + :fields [[:fk-> [:field-id 2] [:field-id 4]] + [:field-id 3]]}} + {:type :query + :query {:breakout [[:field-id 1] + [:fk-> [:field-id 2] [:field-id 4]]] + :fields [[:field-id 3]]}}} + + "should work if the Field is bucketed in the breakout & in fields" + {{:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month] + [:field-id 3]]}} + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:field-id 3]]}}} + + "should work if the Field is bucketed in the breakout but not in fields" + {{:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:fk-> [:field-id 2] [:field-id 4]] + [:field-id 3]]}} + {:type :query + :query {:breakout [[:field-id 1] + [:datetime-field [:fk-> [:field-id 2] [:field-id 4]] :month]] + :fields [[:field-id 3]]}}}})) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | REMOVE EMPTY CLAUSES | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; empty sequences should get removed -(expect - {:y [100]} - (#'normalize/remove-empty-clauses {:x [], :y [100]})) +(deftest remove-empty-clauses-test + (tests 'remove-empty-clauses #'normalize/remove-empty-clauses + {"empty sequences should get removed" + {{:x [], :y [100]} + {:y [100]}} -;; nil values should get removed -(expect - {:y 100} - (#'normalize/remove-empty-clauses {:x nil, :y 100})) + "nil values should get removed" + {{:x nil, :y 100} + {:y 100}} -;; sequences containing only nil should get removed -(expect - {:a [nil 100]} - (#'normalize/remove-empty-clauses {:a [nil 100], :b [nil nil]})) + "sequences containing only nil should get removed" + {{:a [nil 100], :b [nil nil]} + {:a [nil 100]}} -;; empty maps should get removed -(expect - {:a {:b 100}} - (#'normalize/remove-empty-clauses {:a {:b 100}, :c {}})) + "empty maps should get removed" + {{:a {:b 100}, :c {}} + {:a {:b 100}} -(expect - {:a {:b 100}} - (#'normalize/remove-empty-clauses {:a {:b 100}, :c {:d nil}})) + {:a {:b 100}, :c {:d nil}} + {:a {:b 100}}}})) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | PUTTING IT ALL TOGETHER | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; With an ugly MBQL 95 query, does everything get normalized nicely? -(expect - {:type :query - :query {:source-table 10 - :breakout [[:field-id 10] [:field-id 20]] - :filter [:= [:field-id 10] [:datetime-field [:field-id 20] :day]] - :order-by [[:desc [:field-id 10]]]}} - (normalize/normalize {:type "query" - :query {"source_table" 10 - "AGGREGATION" "ROWS" - "breakout" [10 20] - "filter" ["and" ["=" 10 ["datetime-field" 20 "as" "day"]]] - "order-by" [[10 "desc"]]}})) - -;; let's try doing the full normalization on a native query w/ params -(expect - {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" - :template-tags {"names_list" {:name "names_list" - :display-name "Names List" - :type :dimension - :dimension [:field-id 49]}}} - :parameters [{:type :text - :target [:dimension [:template-tag "names_list"]] - :value ["BBQ" "Bakery" "Bar"]}]} - (normalize/normalize - {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" - "template_tags" {:names_list {:name "names_list" - :display_name "Names List" - :type "dimension" - :dimension ["field-id" 49]}}} - :parameters [{:type "text" - :target ["dimension" ["template-tag" "names_list"]] - :value ["BBQ" "Bakery" "Bar"]}]})) - -;; let's try normalizing a big query with SEGMENTS -(expect - {:database 1 - :type :query - :query {:source-table 2 - :filter [:and - [:= [:field-id 3] "Toucan-friendly"] - [:segment 4] - [:segment 5]]}} - (normalize/normalize - {:database 1 - :type :query - :query {:source-table 2 - :filter ["AND" - ["=" 3 "Toucan-friendly"] - ["SEGMENT" 4] - ["SEGMENT" 5]]}})) - -;; make sure source queries get normalized properly! -(expect - {:database 4 - :type :query - :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", - :template-tags {"category" {:name "category" - :display-name "Category" - :type :text - :required true - :default "Widget"}}}}} - (normalize/normalize - {:database 4 - :type :query - :query {"source_query" {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", - "template_tags" {:category {:name "category" - :display-name "Category" - :type "text" - :required true - :default "Widget"}}}}})) - -;; make sure `rows` queries get removed -(expect - {:database 4, - :type :query, - :query {:source-query {:source-table 1}}} - (normalize/normalize - {:database 4 - :type :query - :query {"source_query" {"source_table" 1, "aggregation" "rows"}}})) - -;; make sure that parameters get normalized/canonicalized correctly. value should not get normalized, but type should; -;; target should do canonicalization for MBQL clauses -(expect - {:type :query, - :query {:source-table 1} - :parameters [{:type :id, :target [:dimension [:fk-> [:field-id 3265] [:field-id 4575]]], :value ["field-id"]} - {:type :date/all-options, :target [:dimension [:field-id 3270]], :value "thismonth"}]} - (normalize/normalize - {:type :query - :query {:source-table 1} - :parameters [{:type "id", :target ["dimension" ["fk->" 3265 4575]], :value ["field-id"]} - {:type "date/all-options", :target ["dimension" ["field-id" 3270]], :value "thismonth"}]})) - -;; make sure `:source-metadata` gets normalized the way we'd expect: - -;; 1. Type names should get converted to keywords -(expect - {:query {:source-metadata - [{:name "name" - :display_name "Name" - :base_type :type/Text - :special_type :type/Name - :fingerprint {:global {:distinct-count 100} - :type {:type/Text {:percent-json 0.0 - :percent-url 0.0 - :percent-email 0.0 - :average-length 15.63}}}}]}} - (normalize/normalize - {:query {:source-metadata [{:name "name" - :display_name "Name" - :description nil - :base_type "type/Text" - :special_type "type/Name" - :fingerprint {"global" {"distinct-count" 100} - "type" {"type/Text" {"percent-json" 0.0 - "percent-url" 0.0 - "percent-email" 0.0 - "average-length" 15.63}}}}]}})) - -;; 2. if `:source-metadata` is at the top-level, it should get moved to the correct location inside the 'inner' MBQL -;; query -(expect - {:query {:source-metadata - [{:name "name" - :display_name "Name" - :base_type :type/Text - :special_type :type/Name - :fingerprint {:global {:distinct-count 100} - :type {:type/Text {:percent-json 0.0 - :percent-url 0.0 - :percent-email 0.0 - :average-length 15.63}}}}]}} - (normalize/normalize - {:source-metadata [{:name "name" - :display_name "Name" - :description nil - :base_type "type/Text" - :special_type "type/Name" - :fingerprint {"global" {"distinct-count" 100} - "type" {"type/Text" {"percent-json" 0.0 - "percent-url" 0.0 - "percent-email" 0.0 - "average-length" 15.63}}}}]})) +(deftest e2e-mbql-95-query-test + (testing "With an ugly MBQL 95 query, does everything get normalized nicely?" + (is (= {:type :query + :query {:source-table 10 + :breakout [[:field-id 10] [:field-id 20]] + :filter [:= [:field-id 10] [:datetime-field [:field-id 20] :day]] + :order-by [[:desc [:field-id 10]]]}} + (normalize/normalize {:type "query" + :query {"source_table" 10 + "AGGREGATION" "ROWS" + "breakout" [10 20] + "filter" ["and" ["=" 10 ["datetime-field" 20 "as" "day"]]] + "order-by" [[10 "desc"]]}}))))) + +(deftest e2e-native-query-with-params-test + (testing "let's try doing the full normalization on a native query w/ params" + (is (= {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" + :template-tags {"names_list" {:name "names_list" + :display-name "Names List" + :type :dimension + :dimension [:field-id 49]}}} + :parameters [{:type :text + :target [:dimension [:template-tag "names_list"]] + :value ["BBQ" "Bakery" "Bar"]}]} + (normalize/normalize + {:native {:query "SELECT * FROM CATEGORIES WHERE {{names_list}}" + "template_tags" {:names_list {:name "names_list" + :display_name "Names List" + :type "dimension" + :dimension ["field-id" 49]}}} + :parameters [{:type "text" + :target ["dimension" ["template-tag" "names_list"]] + :value ["BBQ" "Bakery" "Bar"]}]}))))) + +(deftest e2e-big-query-with-segments-test + (testing "let's try normalizing a big query with SEGMENTS" + (is (= {:database 1 + :type :query + :query {:source-table 2 + :filter [:and + [:= [:field-id 3] "Toucan-friendly"] + [:segment 4] + [:segment 5]]}} + (normalize/normalize + {:database 1 + :type :query + :query {:source-table 2 + :filter ["AND" + ["=" 3 "Toucan-friendly"] + ["SEGMENT" 4] + ["SEGMENT" 5]]}}))))) + +(deftest e2e-source-queries-test + (testing "make sure source queries get normalized properly!" + (is (= {:database 4 + :type :query + :query {:source-query {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", + :template-tags {"category" {:name "category" + :display-name "Category" + :type :text + :required true + :default "Widget"}}}}} + (normalize/normalize + {:database 4 + :type :query + :query {"source_query" {:native "SELECT * FROM PRODUCTS WHERE CATEGORY = {{category}} LIMIT 10", + "template_tags" {:category {:name "category" + :display-name "Category" + :type "text" + :required true + :default "Widget"}}}}}))))) + +(deftest e2e-rows-aggregation-test + (testing "make sure `rows` aggregations get removed" + (is (= {:database 4, + :type :query, + :query {:source-query {:source-table 1}}} + (normalize/normalize + {:database 4 + :type :query + :query {"source_query" {"source_table" 1, "aggregation" "rows"}}}))))) + +(deftest e2e-parameters-test + (testing (str "make sure that parameters get normalized/canonicalized correctly. value should not get normalized, " + "but type should; target should do canonicalization for MBQL clauses") + (is (= {:type :query, + :query {:source-table 1} + :parameters [{:type :id, :target [:dimension [:fk-> [:field-id 3265] [:field-id 4575]]], :value ["field-id"]} + {:type :date/all-options, :target [:dimension [:field-id 3270]], :value "thismonth"}]} + (normalize/normalize + {:type :query + :query {:source-table 1} + :parameters [{:type "id", :target ["dimension" ["fk->" 3265 4575]], :value ["field-id"]} + {:type "date/all-options", :target ["dimension" ["field-id" 3270]], :value "thismonth"}]}))))) + +(deftest e2e-source-metadata-test + (testing "make sure `:source-metadata` gets normalized the way we'd expect:" + (testing "1. Type names should get converted to keywords" + (is (= {:query {:source-metadata + [{:name "name" + :display_name "Name" + :base_type :type/Text + :special_type :type/Name + :fingerprint {:global {:distinct-count 100} + :type {:type/Text {:percent-json 0.0 + :percent-url 0.0 + :percent-email 0.0 + :average-length 15.63}}}}]}} + (normalize/normalize + {:query {:source-metadata [{:name "name" + :display_name "Name" + :description nil + :base_type "type/Text" + :special_type "type/Name" + :fingerprint {"global" {"distinct-count" 100} + "type" {"type/Text" {"percent-json" 0.0 + "percent-url" 0.0 + "percent-email" 0.0 + "average-length" 15.63}}}}]}})))) + + (testing (str "2. if `:source-metadata` is at the top-level, it should get moved to the correct location inside " + "the 'inner' MBQL query") + (is (= {:query {:source-metadata + [{:name "name" + :display_name "Name" + :base_type :type/Text + :special_type :type/Name + :fingerprint {:global {:distinct-count 100} + :type {:type/Text {:percent-json 0.0 + :percent-url 0.0 + :percent-email 0.0 + :average-length 15.63}}}}]}} + (normalize/normalize + {:source-metadata [{:name "name" + :display_name "Name" + :description nil + :base_type "type/Text" + :special_type "type/Name" + :fingerprint {"global" {"distinct-count" 100} + "type" {"type/Text" {"percent-json" 0.0 + "percent-url" 0.0 + "percent-email" 0.0 + "average-length" 15.63}}}}]})))))) (deftest normalize-nil-values-in-native-maps-test (testing "nil values in native query maps (e.g. MongoDB queries) should not get removed during normalization.\n"