Skip to content
Snippets Groups Projects
Unverified Commit 2e76d413 authored by Braden Shepherdson's avatar Braden Shepherdson Committed by GitHub
Browse files

[MLv2] New "canned" queries and clicks for drill-thru testing (#38210)

This also adds a couple of hand-written test cases in addition to
testing the canned set of queries against most of the drills.

Progress towards #36253.
parent 2d6d12c8
No related branches found
No related tags found
No related merge requests found
Showing
with 568 additions and 20 deletions
......@@ -4,6 +4,7 @@
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
[metabase.lib.test-util.metadata-providers.mock :as providers.mock]
[metabase.util :as u]
......@@ -41,6 +42,28 @@
:column-ref [:aggregation {} u/uuid-regex]
:dimensions [{} {}]}}))
(def ^:private metadata-no-xrays
(meta/updated-metadata-provider assoc-in [:settings :enable-xrays] false))
(deftest ^:parallel automatic-insights-availability-test
(testing "automatic-insights is"
(testing "available for cell clicks subject to at least one breakout"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)
:when (= click :cell)]
(is (= (boolean (not-empty (:dimensions context)))
(boolean (canned/returned test-case context :drill-thru/automatic-insights))))))
(testing "not available for any header clicks"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)
:when (= click :header)]
(is (nil? (canned/returned test-case context :drill-thru/automatic-insights)))))
(testing "available for pivot and legend clicks"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)
:when (#{:pivot :legend} click)]
(is (canned/returned test-case context :drill-thru/automatic-insights))))
(testing "not available at all with xrays disabled"
(doseq [[test-case context _details] (canned/canned-clicks metadata-no-xrays)]
(is (nil? (canned/returned test-case context :drill-thru/automatic-insights)))))))
(defn- auto-insights [query exp-filters]
(let [[created-at sum] (lib/returned-columns query)
drills (lib/available-drill-thrus
......
......@@ -4,6 +4,7 @@
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.metadata :as lib.metadata]
[metabase.lib.test-metadata :as meta]
[metabase.lib.test-util :as lib.tu]
......@@ -11,6 +12,13 @@
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel column-filter-availability-test
(testing "column-filter is available for any header click, and nothing else"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)]
(if (= click :header)
(is (canned/returned test-case context :drill-thru/column-filter))
(is (not (canned/returned test-case context :drill-thru/column-filter)))))))
(deftest ^:parallel returns-column-filter-test-1
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/column-filter
......
......@@ -6,11 +6,22 @@
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.distribution :as lib.drill-thru.distribution]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel distribution-availability-test
(testing "distribution is available only for header clicks on non-aggregate, non-breakout columns which are not PKs, JSON, comments or descriptions"
(doseq [[test-case context {:keys [click column-kind column-type]}] (canned/canned-clicks)]
(if (and (= click :header)
(not (#{:aggregation :breakout} column-kind))
(not= column-type :pk)
(not (#{:type/Comment :type/Description} (:semantic-type (:column context)))))
(is (canned/returned test-case context :drill-thru/distribution))
(is (not (canned/returned test-case context :drill-thru/distribution)))))))
(deftest ^:parallel aggregate-column-test
(testing "Don't suggest distribution drill thrus for aggregate columns like `count(*)`"
(let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
......@@ -63,3 +74,55 @@
:breakout [[:field
{:binning (symbol "nil #_\"key is not present.\"")}
(meta/id :orders :user-id)]]}]}})))
(deftest ^:parallel apply-to-column-types-test
(testing "distribution drill"
(let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
(lib/order-by (meta/field-metadata :orders :subtotal))
(lib/limit 100))]
(testing "on numeric columns uses default binning"
(lib.drill-thru.tu/test-drill-application
{:click-type :header
:custom-query query
:column-name "QUANTITY"
:query-type :unaggregated
:drill-type :drill-thru/distribution
:expected {:type :drill-thru/distribution
:column {:name "QUANTITY"}}
:expected-query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
;; Limit and order-by get removed, then COUNT broken out by the clicked column.
;; Numeric columns use default binning.
(lib/breakout (lib/with-binning
(meta/field-metadata :orders :quantity)
{:strategy :default}))
(lib/aggregate (lib/count)))}))
(testing "on date columns uses month bucketing"
(lib.drill-thru.tu/test-drill-application
{:click-type :header
:custom-query query
:column-name "CREATED_AT"
:query-type :unaggregated
:drill-type :drill-thru/distribution
:expected {:type :drill-thru/distribution
:column {:name "CREATED_AT"}}
:expected-query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
;; Limit and order-by get removed, then COUNT broken out by the clicked column.
;; Datetime columns use month bucketing.
(lib/breakout (lib/with-temporal-bucket
(meta/field-metadata :orders :created-at)
:month))
(lib/aggregate (lib/count)))}))
(testing "on other columns does no extra bucketing"
(lib.drill-thru.tu/test-drill-application
{:click-type :header
:custom-query query
:column-name "PRODUCT_ID"
:query-type :unaggregated
:drill-type :drill-thru/distribution
:expected {:type :drill-thru/distribution
:column {:name "PRODUCT_ID"}}
:expected-query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
;; Limit and order-by get removed, then COUNT broken out by the clicked column.
;; Other columns get no bucketing (including FKs, despite being numeric).
(lib/breakout (meta/field-metadata :orders :product-id))
(lib/aggregate (lib/count)))})))))
......@@ -4,11 +4,21 @@
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel fk-details-availability-test
(testing "FK details is available for cell clicks on non-NULL FKs"
(doseq [[test-case context {:keys [click column-type]}] (canned/canned-clicks)]
(if (and (= click :cell)
(= column-type :fk)
(not= (:value context) :null))
(is (canned/returned test-case context :drill-thru/fk-details))
(is (not (canned/returned test-case context :drill-thru/fk-details)))))))
(deftest ^:parallel returns-fk-details-test-1
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/fk-details
......
(ns metabase.lib.drill-thru.pivot-test
(:require
[clojure.test :refer [deftest is]]
[clojure.test :refer [deftest is testing]]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
[metabase.util.malli :as mu]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
......@@ -16,6 +17,13 @@
;; 3. Category + Time: a. address, b. category, c. category + category
;; 4. All types: a. no breakouts.
(deftest ^:parallel pivot-availability-test
(testing "pivot drill is available only for cell clicks"
;; Other conditions are too complex to capture here; other tests check them.
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)
:when (not= click :cell)]
(is (not (canned/returned test-case context :drill-thru/pivot))))))
(def ^:private orders-date-only-test-case
{:drill-type :drill-thru/pivot
:click-type :cell
......
......@@ -3,37 +3,76 @@
[clojure.test :refer [deftest is testing]]
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
[metabase.lib.test-util :as lib.tu]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(def ^:private multi-pk-provider
;; simulate a table with multiple PK columns: mark orders.product-id as a PK column
(lib.tu/merged-mock-metadata-provider
meta/metadata-provider
{:fields [{:id (meta/id :orders :product-id)
:semantic-type :type/PK}]}))
(defn- find-drill [query context]
(m/find-first #(= (:type %) :drill-thru/pk)
(lib/available-drill-thrus query -1 context)))
(deftest ^:parallel pk-unavailable-for-non-cell-test
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)
:when (not= click :cell)]
(is (not (canned/returned test-case context :drill-thru/pk)))))
(deftest ^:parallel do-not-return-pk-for-nil-test
(testing "do not return pk drills for nil cell values (#36126)"
;; simulate a table with multiple PK columns: mark orders.product-id as a PK column
(let [metadata-provider (lib.tu/merged-mock-metadata-provider
meta/metadata-provider
{:fields [{:id (meta/id :orders :product-id)
:semantic-type :type/PK}]})
query (lib/query metadata-provider (meta/table-metadata :orders))
(testing "do not return pk drills for nil PK values (#36126)"
(let [query (lib/query multi-pk-provider (meta/table-metadata :orders))
context {:column (meta/field-metadata :orders :id)
:column-ref (lib/ref (meta/field-metadata :orders :id))
:value :null
:row [{:column (meta/field-metadata :orders :id)
:column-ref (lib/ref (meta/field-metadata :orders :id))
:value nil}]}]
(is (not (m/find-first #(= (:type %) :drill-thru/pk)
(lib/available-drill-thrus query -1 context)))))))
(is (not (find-drill query context)))
(testing "but a nil clicked value with defined PKs is fine"
(is (find-drill query {:column (meta/field-metadata :orders :subtotal)
:column-ref (lib/ref (meta/field-metadata :orders :subtotal))
:value :null
:row [{:column (meta/field-metadata :orders :id)
:column-ref (lib/ref (meta/field-metadata :orders :id))
:value 12}]}))))))
(deftest ^:parallel do-not-return-pk-with-aggregations-test
(testing "PK drill is not returned when clicking an aggregation"
(let [query (-> (lib/query multi-pk-provider (meta/table-metadata :orders))
(lib/aggregate (lib/count))
(lib/breakout (meta/field-metadata :orders :created-at)))
{count-col "count"
created-at "CREATED_AT"} (m/index-by :name (lib/returned-columns query))
count-dim {:column count-col
:column-ref (lib/ref count-col)
:value 123}
created-at-dim {:column created-at
:column-ref (lib/ref count-col)
:value "2022-12-01T00:00:00+02:00"}]
(is (not (find-drill query (merge count-dim
{:row [count-dim created-at-dim]
:dimensions [created-at-dim]})))))))
(deftest ^:parallel do-not-return-pk-for-click-on-fk-test
(testing "PK drill is not returned when clicking an FK"
(is (not (find-drill (lib/query multi-pk-provider (meta/table-metadata :orders))
{:column (meta/field-metadata :orders :user-id)
:column-ref (lib/ref (meta/field-metadata :orders :user-id))
:value 456})))))
(deftest ^:parallel return-pk-drill-for-query-with-multiple-pks-on-non-pk-columns-click-test
(testing "should drill thru a non-PK and non-FK cell when there are multiple PK columns (#35618)"
;; simulate a table with multiple PK columns: mark orders.product-id as a PK column
(let [metadata-provider (lib.tu/merged-mock-metadata-provider
meta/metadata-provider
{:fields [{:id (meta/id :orders :product-id)
:semantic-type :type/PK}]})
query (lib/query metadata-provider (meta/table-metadata :orders))
(let [query (lib/query multi-pk-provider (meta/table-metadata :orders))
context-with-values (fn [column-value pk-1-value pk-2-value]
{:column (meta/field-metadata :orders :total)
:value column-value
......@@ -47,10 +86,7 @@
{:column (meta/field-metadata :orders :total)
:column-ref (lib/ref (meta/field-metadata :orders :total))
:value (when-not (= column-value :null)
column-value)}]})
find-drill (fn [query context]
(m/find-first #(= (:type %) :drill-thru/pk)
(lib/available-drill-thrus query -1 context)))]
column-value)}]})]
(testing "both PKs have values"
(doseq [column-value [10 :null]]
(let [context (context-with-values column-value 100 200)
......
......@@ -5,11 +5,19 @@
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel quick-filter-availability-test
(doseq [[test-case context {:keys [click column-type]}] (canned/canned-clicks)]
(if (and (= click :cell)
(not (#{:pk :fk} column-type)))
(is (canned/returned test-case context :drill-thru/quick-filter))
(is (not (canned/returned test-case context :drill-thru/quick-filter))))))
(deftest ^:parallel returns-quick-filter-test-1
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/quick-filter
......@@ -111,6 +119,19 @@
:operators [{:name "="}
{:name "≠"}]}})))
(deftest ^:parallel returns-quick-filter-test-9
(testing "quick-filter should return = and ≠ only for other field types (eg. generic strings)"
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/quick-filter
:click-type :cell
:query-type :unaggregated
:query-table "PRODUCTS"
:column-name "TITLE"
:expected {:type :drill-thru/quick-filter
:value (get-in lib.drill-thru.tu/test-queries ["PRODUCTS" :unaggregated :row "TITLE"])
:operators [{:name "="}
{:name "≠"}]}})))
(deftest ^:parallel apply-quick-filter-on-correct-level-test
(testing "quick-filter on an aggregation should introduce an new stage (#34346)"
(lib.drill-thru.tu/test-drill-application
......
......@@ -6,12 +6,20 @@
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.sort :as lib.drill-thru.sort]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
[metabase.util.malli :as mu]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel sort-drill-availability-test
(testing "sort is available on column headers only"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)]
(if (= click :header)
(is (canned/returned test-case context :drill-thru/sort))
(is (not (canned/returned test-case context :drill-thru/sort)))))))
(deftest ^:parallel sort-e2e-test
(let [query (lib/query meta/metadata-provider (meta/table-metadata :orders))
drill (lib.drill-thru.sort/sort-drill query
......
......@@ -6,11 +6,26 @@
[metabase.lib.drill-thru.summarize-column-by-time
:as lib.drill-thru.summarize-column-by-time]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]
#?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
#?(:cljs (comment metabase.test-runner.assert-exprs.approximately-equal/keep-me))
(deftest ^:parallel summarize-column-by-time-availability-test
(testing (str "summarize-column-by-time is available for header click with no aggregations or breakouts, "
"for a summable column and at least one date-flavoured breakout available")
(doseq [[test-case context {:keys [click column-type]}] (canned/canned-clicks)]
(if (and (= click :header)
(= column-type :number)
(zero? (:aggregations test-case))
(zero? (:breakouts test-case))
(some #(or (isa? (:effective-type %) :type/Date)
(isa? (:effective-type %) :type/DateTime))
(lib/breakoutable-columns (:query test-case))))
(is (canned/returned test-case context :drill-thru/summarize-column-by-time))
(is (not (canned/returned test-case context :drill-thru/summarize-column-by-time)))))))
(deftest ^:parallel aggregate-column-test
(testing "Don't suggest summarize-column-by-time drill thrus for aggregate columns like `count(*)`"
(let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
......@@ -48,3 +63,18 @@
:query-type :unaggregated
:column-name "QUANTITY"
:expected {:type :drill-thru/summarize-column-by-time}}))
(deftest ^:parallel apply-summarize-column-by-time-test
(lib.drill-thru.tu/test-drill-application
{:drill-type :drill-thru/summarize-column-by-time
:click-type :header
:query-type :unaggregated
:column-name "SUBTOTAL"
:expected {:type :drill-thru/summarize-column-by-time}
:expected-query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/sum (meta/field-metadata :orders :subtotal)))
(lib/breakout (lib/with-temporal-bucket (meta/field-metadata :orders :created-at) :month)))}))
;; TODO: Bring the fingerprint-based unit selection logic from
;; https://github.com/metabase/metabase/blob/0624d8d0933f577cc70c03948f4b57f73fe13ada/frontend/src/metabase-lib/metadata/Field.ts#L397
;; into this drill. Currently it always chooses the default date unit of months.
(ns metabase.lib.drill-thru.summarize-column-test
(:require
[clojure.test :refer [deftest testing]]
[clojure.test :refer [deftest is testing]]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]))
(deftest ^:parallel summarize-column-availability-test
(testing "summarize-column is available for column headers with no aggregations or breakouts"
(doseq [[test-case context {:keys [click]}] (canned/canned-clicks)]
(if (and (= click :header)
(zero? (:aggregations test-case))
(zero? (:breakouts test-case)))
(is (canned/returned test-case context :drill-thru/summarize-column))
(is (not (canned/returned test-case context :drill-thru/summarize-column)))))))
(deftest ^:parallel returns-summarize-column-test-1
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/summarize-column
......
(ns metabase.lib.drill-thru.test-util.canned
(:require
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.test-metadata :as meta]))
(defn- base-context [column value]
{:column column
:column-ref (when column (lib/ref column))
:value value})
(defn- dimensions [{:keys [query row] :as _test-case}]
(not-empty (for [breakout (lib/breakouts query)
:let [column (lib/breakout-column query -1 breakout)]]
(base-context column (get row (:name column))))))
(defn- column-by-name [{:keys [query]} column-name]
(let [columns (lib/returned-columns query)]
(m/find-first #(= (:name %) column-name) columns)))
(defn- null-value [{:keys [value] :as context}]
; This special case is only for *top-level* contexts, not :dimensions or :row, hence the separate function.
(cond-> context
(nil? value) (assoc :value :null)))
(defn cell-click
"Given a test case and a column name, returns a drill-thru context for a click on a cell of that column.
(\"Cell\" is used broadly to mean clicking a literal table cell, a point in a time series, a bar in a histogram, etc.)
Such a context has both `:column` and `:value` set. The `:value` comes from the `test-case`. Note that a SQL NULL
value appears as `:value :null`. (`:value nil` indicates no value was provided.)
Any breakouts in the query **except the one which was clicked** appear in `:dimensions`."
[{:keys [row] :as test-case} column-name]
(let [base (-> (column-by-name test-case column-name)
(base-context (get row column-name))
null-value
(assoc :row (for [[col value] row]
(base-context (column-by-name test-case col)
value))))
dims (dimensions test-case)]
;; If the query contains aggregations, the resulting context depends on what we clicked.
;; On clicking an aggregation, :dimensions is populated. On clicking a breakout, we just get that column.
(cond
;; Clicking a breakout - just the base context.
(and dims (some #(= (:name (:column %)) column-name) dims)) base
;; Clicking an aggregation - include the dimensions
dims (assoc base :dimensions dims)
;; Clicking neither kind of cell - just the base context.
:else base)))
(defn header-click
"Given a test case and a column name, returns a drill-thru context for a click on that column's header.
Such a context has a `:column` but no `:value`. Likewise there are no `:dimensions` at the column level."
[test-case column-name]
(base-context (column-by-name test-case column-name) nil))
;; Legend clicks have nil column, nil value, but have exactly one dimensions with a value.
(defn legend-click
"Given a test case and a column name, returns a drill-thru context for a click on a legend entry on a multi-series
chart. (The `column-name` is the name of the column shown in the legend, typically a category or bucketed cohort.)
Such a context has `nil` `:column` and `:value`, and only one breakout (that named by `column-name`, the column of
the legend) is listed."
[test-case column-name]
{:column nil
:column-ref nil
:value nil
:dimensions (filter #(= (:name (:column %)) column-name) (dimensions test-case))})
;; Pivot clicks have a value of :null (that is, SQL NULL) and no column, and with dimensions.
(defn pivot-click
"Given a test case, returns a drill-thru context for a click on a pivot table cell.
Such a context has `:value :null` but no column, and the dimensions are populated for that pivot row."
[test-case]
{:column nil
:column-ref nil
:value :null
:dimensions (dimensions test-case)})
(defn- canned-queries
([] (canned-queries meta/metadata-provider))
([metadata-provider]
{:test.query/orders
{:query (lib/query metadata-provider (meta/table-metadata :orders))
:row {"ID" "3"
"USER_ID" "1"
"PRODUCT_ID" "105"
"SUBTOTAL" 52.723521442619514
"TAX" 2.9
"TOTAL" 49.206842233769756
"DISCOUNT" nil
"CREATED_AT" "2025-12-06T22:22:48.544+02:00"
"QUANTITY" 2}
:aggregations 0
:breakouts 0}
:test.query/orders-count
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/count)))
:row {"count" 77}
:aggregations 1
:breakouts 0
:default-column "count"}
:test.query/orders-count-by-product-id
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/count))
(lib/breakout (meta/field-metadata :orders :product-id)))
:row {"PRODUCT_ID" 77
"count" 3}
:aggregations 1
:breakouts 1
:default-column "PRODUCT_ID"}
:test.query/orders-count-by-created-at
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/count))
(lib/breakout (meta/field-metadata :orders :created-at)))
:row {"CREATED_AT" "2022-12-01T00:00:00+02:00"
"count" 3}
:aggregations 1
:breakouts 1
:default-column "CREATED_AT"}
:test.query/orders-count-by-created-at-and-product-category
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/count))
(lib/breakout (meta/field-metadata :orders :created-at))
(lib/breakout (meta/field-metadata :products :category)))
:row {"CREATED_AT" "2022-12-01T00:00:00+02:00"
"CATEGORY" "Doohickey"
"count" 3}
:aggregations 1
:breakouts 2
:default-column "CREATED_AT"}
:test.query/orders-sum-subtotal-by-product-id
{:query (-> (lib/query metadata-provider (meta/table-metadata :orders))
(lib/aggregate (lib/sum (meta/field-metadata :orders :subtotal)))
(lib/breakout (meta/field-metadata :orders :product-id)))
:row {"PRODUCT_ID" 77
"sum" 986.34}
:aggregations 1
:breakouts 1
:default-column "PRODUCT_ID"}
:test.query/products
{:query (lib/query metadata-provider (meta/table-metadata :products))
:row {"ID" "3"
"EAN" "4966277046676"
"TITLE" "Synergistic Granite Chair"
"CATEGORY" "Doohickey"
"VENDOR" "Murray, Watsica and Wunsch"
"PRICE" 35.38
"RATING" 4
"CREATED_AT" "2024-09-08T22:03:20.239+03:00"}
:aggregations 0
:breakouts 0}
:test.query/reviews
{:query (lib/query metadata-provider (meta/table-metadata :reviews))
:row {"ID" "301"
"REVIEWER" "J. Some Guy"
"BODY" "I think this product is terrible! It solved my problem perfectly but arrived late."
"RATING" 3
; This doesn't appear in the sample data, but a NULL FK is useful for testing some drills.
"PRODUCT_ID" nil
"CREATED_AT" "2024-09-08T22:03:20.239+03:00"}
:aggregations 0
:breakouts 0}}))
(defn returned "Given a test case, a context, and a target drill type (eg. `:drill-thru/quick-filter`), calls
[[lib/available-drill-thrus]] and looks for the specified drill.
Either returns the drill itself or nil if it can't be found.
Intended to be used as `(is (returned ...))` or `(is (not (returned ...)))`, though since it returns the whole drill
it's possible to check the returned drill more deeply with `(is (=? {...} (returned ...)))`."
[test-case context drill]
(let [drills (lib/available-drill-thrus (:query test-case) -1 context)
by-type (m/index-by :type drills)]
(get by-type drill)))
(defn test-case
"Given a query's key from the [[canned-queries]] map, returns a `test-case` with an instance of that query.
If a `metadata-provider` is given, uses that; otherwise uses the default [[meta/metadata-provider]]."
([query-key] (test-case meta/metadata-provider query-key))
([metadata-provider query-key]
(let [queries (canned-queries metadata-provider)]
(get queries query-key))))
(defn- click [tc click-type column-name column-kind column-type]
(let [context (case click-type
:cell (cell-click tc column-name)
:header (header-click tc column-name)
:legend (legend-click tc column-name)
:pivot (pivot-click tc))]
[tc context {:click click-type
:column-name column-name
:column-kind column-kind
:column-type column-type}]))
(defn canned-clicks
"Given an optional `metadata-provider`, returns a list of `[test-case context details]` triples for a standard set of
interesting clicks. This helps to factor the basic tests for each drill that it appears in the contexts where it is
supposed to, and no others.
The `details` map contains `:column-name` of course, but also some other interesting fields:
- `:click` is one of `:cell`, `:header`, `:pivot`, and `:legend`.
- `:cell-kind` is one of `:basic`, `:aggregation` or `:breakout`.
- `:cell-type` is one of `:pk`, `:fk`, `:string`, `:number`, `:datetime`."
([] (canned-clicks meta/metadata-provider))
([metadata-provider]
(->> [;; Basic query for Orders, no aggregations or breakouts - cell and header clicks on different column types.
(let [tc (test-case metadata-provider :test.query/orders)]
[(click tc :cell "ID" :basic :pk)
(click tc :cell "PRODUCT_ID" :basic :fk)
(click tc :cell "SUBTOTAL" :basic :number)
(click tc :cell "CREATED_AT" :basic :datetime)
(click tc :header "ID" :basic :pk)
(click tc :header "PRODUCT_ID" :basic :fk)
(click tc :header "SUBTOTAL" :basic :number)
(click tc :header "CREATED_AT" :basic :datetime)])
;; Singular aggregation for Orders, just clicking that single cell.
[(click (test-case metadata-provider :test.query/orders-count) :cell "count" :aggregation :number)]
;; Count broken out by Product ID - click both count and Product ID, both the cells and headers; also a pivot.
(let [tc (test-case metadata-provider :test.query/orders-count-by-product-id)]
[(click tc :cell "count" :aggregation :number)
(click tc :cell "PRODUCT_ID" :breakout :fk)
(click tc :header "count" :aggregation :number)
(click tc :header "PRODUCT_ID" :breakout :fk)
(click tc :pivot nil :basic :number)])
;; Count broken out by Created At - click both count and Created At, both the cells and headers; also a pivot.
(let [tc (test-case metadata-provider :test.query/orders-count-by-created-at)]
[(click tc :cell "count" :aggregation :number)
(click tc :cell "CREATED_AT" :breakout :datetime)
(click tc :header "count" :aggregation :number)
(click tc :header "CREATED_AT" :breakout :datetime)
(click tc :pivot nil :basic :number)])
;; SUM(Subtotal) broken out by Product ID - same as the count case above.
(let [tc (test-case metadata-provider :test.query/orders-sum-subtotal-by-product-id)]
[(click tc :cell "sum" :aggregation :number)
(click tc :cell "PRODUCT_ID" :breakout :fk)
(click tc :header "sum" :aggregation :number)
(click tc :header "PRODUCT_ID" :breakout :fk)
(click tc :pivot nil :basic :number)])
;; Count broken out by both Created At and Product.CATEGORY
;; Click all three cells and headers, also a legend click on a category.
(let [tc (test-case metadata-provider :test.query/orders-count-by-created-at-and-product-category)]
[(click tc :cell "count" :aggregation :number)
(click tc :cell "CREATED_AT" :breakout :datetime)
(click tc :cell "CATEGORY" :breakout :string)
(click tc :header "count" :aggregation :number)
(click tc :header "CREATED_AT" :breakout :datetime)
(click tc :header "CATEGORY" :breakout :string)
(click tc :legend "CATEGORY" :breakout :string)])
;; Simple query against Products.
(let [tc (test-case metadata-provider :test.query/products)]
[(click tc :cell "ID" :basic :pk)
(click tc :cell "EAN" :basic :string)
(click tc :cell "TITLE" :basic :string)
(click tc :cell "PRICE" :basic :number)
(click tc :cell "RATING" :basic :number)
(click tc :cell "CREATED_AT" :basic :datetime)
(click tc :header "ID" :basic :pk)
(click tc :header "EAN" :basic :string)
(click tc :header "TITLE" :basic :string)
(click tc :header "PRICE" :basic :number)
(click tc :header "RATING" :basic :number)
(click tc :header "CREATED_AT" :basic :datetime)])
;; Simple query against Reviews.
;; This one has a :type/Description column (BODY) which matters for Distribution drills.
(let [tc (test-case metadata-provider :test.query/reviews)]
[(click tc :cell "ID" :basic :pk)
(click tc :cell "REVIEWER" :basic :string)
(click tc :cell "BODY" :basic :string)
(click tc :cell "RATING" :basic :number)
(click tc :cell "PRODUCT_ID" :basic :fk)
(click tc :cell "CREATED_AT" :basic :datetime)
(click tc :header "ID" :basic :pk)
(click tc :header "REVIEWER" :basic :string)
(click tc :header "BODY" :basic :string)
(click tc :header "RATING" :basic :number)
(click tc :header "PRODUCT_ID" :basic :fk)
(click tc :header "CREATED_AT" :basic :datetime)])]
(apply concat))))
......@@ -4,8 +4,26 @@
[medley.core :as m]
[metabase.lib.core :as lib]
[metabase.lib.drill-thru.test-util :as lib.drill-thru.tu]
[metabase.lib.drill-thru.test-util.canned :as canned]
[metabase.lib.test-metadata :as meta]))
(deftest ^:parallel zoom-availability-test
(testing "zoom drill is available for cell clicks on non-FKs in tables with only 1 PK, and the PK in the result set"
(doseq [[test-case {:keys [value] :as context} {:keys [click column-type]}] (canned/canned-clicks)]
(if (and (= click :cell)
;; With an FK column and a non-NULL value, this will be an fk-filter drill instead.
;; So we don't expect a zoom drill in that case.
(not (and (= column-type :fk)
(some? value)
(not= value :null)))
;; PK must be in the result set; if not, no zoom drill. This happens for eg. aggregations.
((set (keys (:row test-case))) "ID")
;; Special case: clicking a NULL PK does not return the zoom drill.
(not (and (= value :null)
(= column-type :pk))))
(is (canned/returned test-case context :drill-thru/zoom))
(is (not (canned/returned test-case context :drill-thru/zoom)))))))
(deftest ^:parallel returns-zoom-test-1
(lib.drill-thru.tu/test-returns-drill
{:drill-type :drill-thru/zoom
......
......@@ -2428,6 +2428,12 @@
"[[metabase.lib.metadata.protocols/MetadataProvider]] using the test [[metadata]]."
(meta.graph-provider/->SimpleGraphMetadataProvider metadata))
(defn updated-metadata-provider
"[[metabase.lib.metadata.protocols/MetadataProvider]] using the test [[metadata]] after it has been adjusted by
the provided function, called like [[update]], that is `(f metadata args...)`."
[f & args]
(meta.graph-provider/->SimpleGraphMetadataProvider (apply f metadata args)))
(mu/defn tables :- [:set :keyword]
"Set of valid table names."
[]
......
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