From bd28194b49a7efb41493afb12214ee04f8a62bb5 Mon Sep 17 00:00:00 2001 From: Braden Shepherdson <braden@metabase.com> Date: Mon, 23 Oct 2023 18:34:20 -0400 Subject: [PATCH] [MLv2] Full `:column`s for `:dimensions` in `available-drill-thrus` (#34810) Previously only the `:column-name` was provided, and there is a self-proclaimed "icky hack" to guess at the full columns. The FE provides the (JSON) columns, so this PR converts and uses them. The icky hack can be removed. `:column-ref` is included because sometimes it's important that the refs use the same UUID. --- frontend/src/metabase-lib/drills.unit.spec.ts | 1 + src/metabase/lib/convert.cljc | 27 ++ src/metabase/lib/drill_thru.cljc | 53 +--- .../lib/drill_thru/object_details.cljc | 4 +- src/metabase/lib/drill_thru/quick_filter.cljc | 2 +- .../lib/drill_thru/zoom_in_timeseries.cljc | 70 +++-- src/metabase/lib/js.cljs | 55 ++-- src/metabase/lib/js/metadata.cljs | 30 ++- src/metabase/lib/schema/drill_thru.cljc | 31 +-- .../lib/drill_thru/distribution_test.cljc | 5 +- test/metabase/lib/drill_thru/sort_test.cljc | 16 +- .../summarize_column_by_time_test.cljc | 5 +- test/metabase/lib/drill_thru/test_util.cljc | 22 +- .../drill_thru/zoom_in_timeseries_test.cljc | 48 ++-- test/metabase/lib/drill_thru_test.cljc | 239 +++++++++++------- 15 files changed, 329 insertions(+), 279 deletions(-) diff --git a/frontend/src/metabase-lib/drills.unit.spec.ts b/frontend/src/metabase-lib/drills.unit.spec.ts index 85986a56455..66d743787b1 100644 --- a/frontend/src/metabase-lib/drills.unit.spec.ts +++ b/frontend/src/metabase-lib/drills.unit.spec.ts @@ -181,6 +181,7 @@ const AGGREGATED_ORDERS_COLUMNS = { "temporal-unit": "month", }, ], + unit: "month", }), count: createMockColumn({ diff --git a/src/metabase/lib/convert.cljc b/src/metabase/lib/convert.cljc index 61bf9071143..2a16fceea27 100644 --- a/src/metabase/lib/convert.cljc +++ b/src/metabase/lib/convert.cljc @@ -533,3 +533,30 @@ :legacy-ref legacy-ref :legacy-index->pMBQL-uuid *legacy-index->pMBQL-uuid*} e)))))))) + +(defn- from-json [query-fragment] + #?(:cljs (if (object? query-fragment) + (js->clj query-fragment :keywordize-keys true) + query-fragment) + :clj query-fragment)) + +(defn js-legacy-query->pMBQL + "Given a JSON-formatted legacy MBQL query, transform it to pMBQL. + + If you have only the inner query map (`{:source-table 2 ...}` or similar), call [[js-legacy-inner-query->pMBQL]] + instead." + [query-map] + (-> query-map + from-json + (u/assoc-default :type :query) + mbql.normalize/normalize + ->pMBQL)) + +(defn js-legacy-inner-query->pMBQL + "Given a JSON-formatted *inner* query, transform it to pMBQL. + + If you have a complete legacy query (`{:type :query, :query {...}}` or similar), call [[js-legacy-query->pMBQL]] + instead." + [inner-query] + (js-legacy-query->pMBQL {:type :query + :query (from-json inner-query)})) diff --git a/src/metabase/lib/drill_thru.cljc b/src/metabase/lib/drill_thru.cljc index 356799c9e9d..13fbf6ffce9 100644 --- a/src/metabase/lib/drill_thru.cljc +++ b/src/metabase/lib/drill_thru.cljc @@ -1,6 +1,5 @@ (ns metabase.lib.drill-thru (:require - [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.drill-thru.column-filter :as lib.drill-thru.column-filter] [metabase.lib.drill-thru.common :as lib.drill-thru.common] [metabase.lib.drill-thru.distribution :as lib.drill-thru.distribution] @@ -16,12 +15,9 @@ [metabase.lib.drill-thru.underlying-records :as lib.drill-thru.underlying-records] [metabase.lib.drill-thru.zoom :as lib.drill-thru.zoom] [metabase.lib.drill-thru.zoom-in-timeseries :as lib.drill-thru.zoom-in-timeseries] - [metabase.lib.field :as lib.field] [metabase.lib.metadata.calculation :as lib.metadata.calculation] - [metabase.lib.options :as lib.options] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.drill-thru :as lib.schema.drill-thru] - [metabase.lib.util :as lib.util] [metabase.util.malli :as mu])) (comment @@ -39,28 +35,6 @@ ;; TODO: ActionMode, PublicMode, MetabotMode need to be captured in the FE before calling `available-drill-thrus`. -(defn- icky-hack-add-source-uuid-to-aggregation-column-metadata - "This is an evil HACK -- the FE calls [[available-drill-thrus]] with query results metadata as produced - by [[metabase.query-processor.middleware.annotate]], which does not include the `:lib/source-uuid` for aggregation - columns (since it's still using legacy MBQL at this point), which - means [[metabase.lib.aggregation/column-metadata->aggregation-ref]] can't generate references (since it requires - `:lib/source-uuid`)... so we have to add it back in manually. I've added `:aggregation-index` to the annotate - results (see [[metabase.query-processor.middleware.annotate/cols-for-ags-and-breakouts]]), and we can use that to - determine the correct `:lib/source-uuid`. - - There's probably a more general place we can be doing this, but it's escaping me, so I guess this will have to do - for now. It doesn't seem like the FE generally uses result metadata in most other places so this is not an issue - elsewhere. - - Once we're using MLv2 queries everywhere and stop converting back to legacy we should be able to remove this ICKY - HACK." - [query stage-number {{:keys [aggregation-index], :as column} :column, :as context}] - (or (when (and aggregation-index - (not (:lib/source-uuid column))) - (when-let [ag (lib.aggregation/aggregation-at-index query stage-number aggregation-index)] - (assoc-in context [:column :lib/source-uuid] (lib.options/uuid ag)))) - context)) - ;;; TODO: Missing drills: automatic insights, format. (def ^:private available-drill-thru-fns "Some drill thru functions are expected to return drills for just the specified `:column`; others are expected to @@ -70,30 +44,22 @@ {:f #'lib.drill-thru.column-filter/column-filter-drill, :return-drills-for-dimensions? true} {:f #'lib.drill-thru.foreign-key/foreign-key-drill, :return-drills-for-dimensions? false} {:f #'lib.drill-thru.object-details/object-detail-drill, :return-drills-for-dimensions? false} - {:f #'lib.drill-thru.pivot/pivot-drill, :return-drills-for-dimensions? true} + {:f #'lib.drill-thru.pivot/pivot-drill, :return-drills-for-dimensions? false} {:f #'lib.drill-thru.quick-filter/quick-filter-drill, :return-drills-for-dimensions? false} {:f #'lib.drill-thru.sort/sort-drill, :return-drills-for-dimensions? true} {:f #'lib.drill-thru.summarize-column/summarize-column-drill, :return-drills-for-dimensions? true} {:f #'lib.drill-thru.summarize-column-by-time/summarize-column-by-time-drill, :return-drills-for-dimensions? true} {:f #'lib.drill-thru.underlying-records/underlying-records-drill, :return-drills-for-dimensions? false} - {:f #'lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill, :return-drills-for-dimensions? true}]) + {:f #'lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill, :return-drills-for-dimensions? false}]) (mu/defn ^:private dimension-contexts :- [:maybe [:sequential {:min 1} ::lib.schema.drill-thru/context]] "Create new context maps (with updated `:column` and `:value` keys) for each of the `:dimensions` passed in. Some drill thru functions are expected to return drills for each of these columns, while others are expected to ignore them. Why? Who knows." - [query :- ::lib.schema/query - stage-number :- :int - {:keys [dimensions], :as context} :- ::lib.schema.drill-thru/context] - (when (seq dimensions) - (let [stage (lib.util/query-stage query stage-number) - returned-columns (lib.metadata.calculation/returned-columns query stage-number stage)] - (for [{:keys [column-name value]} dimensions - :let [col (lib.field/resolve-column-name-in-metadata column-name returned-columns)] - :when col] - (assoc context - :column col - :value value))))) + [{:keys [dimensions], :as context} :- ::lib.schema.drill-thru/context] + (not-empty + (for [dimension dimensions] + (merge context dimension)))) (mu/defn available-drill-thrus :- [:sequential [:ref ::lib.schema.drill-thru/drill-thru]] "Get a list (possibly empty) of available drill-thrus for a column, or a column + value pair. @@ -109,13 +75,12 @@ stage-number :- :int context :- ::lib.schema.drill-thru/context] (try - (let [primary-context (icky-hack-add-source-uuid-to-aggregation-column-metadata query stage-number context) - dim-contexts (dimension-contexts query stage-number primary-context)] + (let [dim-contexts (dimension-contexts context)] (into [] (for [{:keys [f return-drills-for-dimensions?]} available-drill-thru-fns - context (if (and return-drills-for-dimensions? (seq dim-contexts)) + context (if (and return-drills-for-dimensions? dim-contexts) dim-contexts - [primary-context]) + [context]) :let [drill (f query stage-number context)] :when drill] drill))) diff --git a/src/metabase/lib/drill_thru/object_details.cljc b/src/metabase/lib/drill_thru/object_details.cljc index 7f64bb59d8d..225f23672d7 100644 --- a/src/metabase/lib/drill_thru/object_details.cljc +++ b/src/metabase/lib/drill_thru/object_details.cljc @@ -1,5 +1,6 @@ (ns metabase.lib.drill-thru.object-details (:require + [medley.core :as m] [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.drill-thru.common :as lib.drill-thru.common] [metabase.lib.metadata.calculation :as lib.metadata.calculation] @@ -24,8 +25,7 @@ (empty? (lib.aggregation/aggregations query stage-number))) (let [[pk-column] (lib.metadata.calculation/primary-keys query) ; Already know there's only one. pk-value (->> row - (filter #(= (:column-name %) (:name pk-column))) - first + (m/find-first #(-> % :column :name (= (:name pk-column)))) :value)] (when (and pk-value ;; Only recurse if this is a different column - otherwise it's an infinite loop. diff --git a/src/metabase/lib/drill_thru/quick_filter.cljc b/src/metabase/lib/drill_thru/quick_filter.cljc index 6759f75cfbc..75d9060f509 100644 --- a/src/metabase/lib/drill_thru/quick_filter.cljc +++ b/src/metabase/lib/drill_thru/quick_filter.cljc @@ -52,7 +52,7 @@ (when (and (lib.drill-thru.common/mbql-stage? query stage-number) ;; (editable? query stage-number) column - (some? value) + (some? value) ; Deliberately allows value :null, only a missing value should fail this test. (not (lib.types.isa/primary-key? column)) (not (lib.types.isa/foreign-key? column))) ;; for aggregate columns, we want to introduce a new stage when applying the drill-thru, `:new-stage?` is used to diff --git a/src/metabase/lib/drill_thru/zoom_in_timeseries.cljc b/src/metabase/lib/drill_thru/zoom_in_timeseries.cljc index 02ba4198f8d..57e7b6ba6af 100644 --- a/src/metabase/lib/drill_thru/zoom_in_timeseries.cljc +++ b/src/metabase/lib/drill_thru/zoom_in_timeseries.cljc @@ -1,10 +1,9 @@ (ns metabase.lib.drill-thru.zoom-in-timeseries (:require - [medley.core :as m] [metabase.lib.breakout :as lib.breakout] [metabase.lib.drill-thru.common :as lib.drill-thru.common] + [metabase.lib.equality :as lib.equality] [metabase.lib.filter :as lib.filter] - [metabase.lib.join.util :as lib.join.util] [metabase.lib.metadata :as lib.metadata] [metabase.lib.remove-replace :as lib.remove-replace] [metabase.lib.schema :as lib.schema] @@ -25,26 +24,18 @@ (zipmap (drop-last valid-current-units) (drop 1 valid-current-units))) -(defn- is-ref-for-source-column? [a-ref column] - (and (lib.util/clause-of-type? a-ref :field) - (let [[_field _opts id-or-name] a-ref] - (if (integer? id-or-name) - (= id-or-name (:id column)) - (and (if-let [join-alias (lib.join.util/current-join-alias a-ref)] - (= join-alias (lib.join.util/current-join-alias column)) - true) - (= id-or-name (:lib/source-column-alias column))))))) - -(mu/defn ^:private matching-breakout-ref :- [:maybe :mbql.clause/field] +(mu/defn ^:private matching-breakout-dimension :- [:maybe ::lib.schema.drill-thru/context.row.value] [query :- ::lib.schema/query stage-number :- :int - column :- lib.metadata/ColumnMetadata] - (let [breakouts (lib.breakout/breakouts query stage-number)] - (m/find-first (fn [breakout] - (and (is-ref-for-source-column? breakout column) - (= (lib.temporal-bucket/temporal-bucket breakout) - (lib.temporal-bucket/temporal-bucket column)))) - breakouts))) + dimensions :- [:sequential ::lib.schema.drill-thru/context.row.value]] + (first (for [breakout (lib.breakout/breakouts query stage-number) + :when (and (lib.util/clause-of-type? breakout :field) + (lib.temporal-bucket/temporal-bucket breakout)) + {:keys [column] :as dimension} dimensions + :when (and (lib.equality/find-matching-column breakout [column]) + (= (lib.temporal-bucket/temporal-bucket breakout) + (lib.temporal-bucket/temporal-bucket column)))] + (assoc dimension :column-ref breakout)))) (mu/defn ^:private next-breakout-unit :- [:maybe ::lib.schema.temporal-bucketing/unit.date-time.truncate] [column :- lib.metadata/ColumnMetadata] @@ -69,27 +60,28 @@ This is different from the `:drill-thru/zoom` type, which is for showing the details of a single object." ;; TODO: This naming is confusing. Fix it? - [query :- ::lib.schema/query - stage-number :- :int - {:keys [column value]} :- ::lib.schema.drill-thru/context] + [query :- ::lib.schema/query + stage-number :- :int + {:keys [column dimensions value]} :- ::lib.schema.drill-thru/context] (when (and (lib.drill-thru.common/mbql-stage? query stage-number) column - (some? value) - (matching-breakout-ref query stage-number column)) - (when-let [next-unit (next-breakout-unit column)] - {:lib/type :metabase.lib.drill-thru/drill-thru - :display-name (describe-next-unit next-unit) - :type :drill-thru/zoom-in.timeseries - :column column - :value value - :next-unit next-unit}))) + (not-empty dimensions) + (some? value)) + (when-let [dimension (matching-breakout-dimension query stage-number dimensions)] + (when-let [next-unit (next-breakout-unit (:column dimension))] + {:lib/type :metabase.lib.drill-thru/drill-thru + :display-name (describe-next-unit next-unit) + :type :drill-thru/zoom-in.timeseries + :dimension dimension + :next-unit next-unit})))) (mu/defmethod lib.drill-thru.common/drill-thru-method :drill-thru/zoom-in.timeseries - [query :- ::lib.schema/query - stage-number :- :int - {:keys [column value next-unit]} :- ::lib.schema.drill-thru/drill-thru.zoom-in.timeseries] - (let [breakout (matching-breakout-ref query stage-number column) - new-breakout (lib.temporal-bucket/with-temporal-bucket breakout next-unit)] + [query :- ::lib.schema/query + stage-number :- :int + {:keys [dimension next-unit]} :- ::lib.schema.drill-thru/drill-thru.zoom-in.timeseries] + (let [{:keys [column value]} dimension + old-breakout (:column-ref dimension) + new-breakout (lib.temporal-bucket/with-temporal-bucket old-breakout next-unit)] (-> query - (lib.filter/filter stage-number (lib.filter/= column value)) - (lib.remove-replace/replace-clause stage-number breakout new-breakout)))) + (lib.filter/filter stage-number (lib.filter/= column value)) + (lib.remove-replace/replace-clause stage-number old-breakout new-breakout)))) diff --git a/src/metabase/lib/js.cljs b/src/metabase/lib/js.cljs index eb68caed8da..46cb63a5c0b 100644 --- a/src/metabase/lib/js.cljs +++ b/src/metabase/lib/js.cljs @@ -23,7 +23,6 @@ [metabase.lib.stage :as lib.stage] [metabase.lib.util :as lib.util] [metabase.mbql.js :as mbql.js] - [metabase.mbql.normalize :as mbql.normalize] [metabase.shared.util.time :as shared.ut] [metabase.util :as u] [metabase.util.log :as log])) @@ -69,15 +68,6 @@ [query] (lib.core/suggested-name query)) -(defn- pMBQL [query-map] - (as-> query-map <> - (js->clj <> :keywordize-keys true) - (if (:type <>) - <> - (assoc <> :type :query)) - (mbql.normalize/normalize <>) - (lib.convert/->pMBQL <>))) - (defn ^:export metadataProvider "Convert metadata to a metadata provider if it is not one already." [database-id metadata] @@ -88,7 +78,7 @@ (defn ^:export query "Coerce a plain map `query` to an actual query object that you can use with MLv2." [database-id metadata query-map] - (let [query-map (pMBQL query-map)] + (let [query-map (lib.convert/js-legacy-query->pMBQL query-map)] (log/debugf "query map: %s" (pr-str query-map)) (lib.core/query (metadataProvider database-id metadata) query-map))) @@ -853,16 +843,31 @@ [a-query stage-number join-condition bucketing-option] (lib.core/join-condition-update-temporal-bucketing a-query stage-number join-condition bucketing-option)) +(defn- fix-column-with-ref [a-ref column] + (cond-> column + ;; Sometimes the FE has result metadata from the QP, without the required :lib/source-uuid on it. + ;; We have the UUID for the aggregation in its ref, so use that here. + (some-> a-ref first (= :aggregation)) (assoc :lib/source-uuid (last a-ref)))) + (defn- js-cells-by "Given a `col-fn`, returns a function that will extract a JS object like - `{col: {name: \"ID\", ...}, value: 12}` into a CLJS map like `{:column-name \"ID\", :value 12}`. + `{col: {name: \"ID\", ...}, value: 12}` into a CLJS map like + ``` + {:column {:lib/type :metadata/column ...} + :column-ref [:field ...] + :value 12} + ``` The spelling of the column key differs between multiple JS objects of this same general shape (`col` on data rows, `column` on dimensions), etc., hence the abstraction." [col-fn] (fn [^js cell] - {:column-name (.-name (col-fn cell)) - :value (.-value cell)})) + (let [column (js.metadata/parse-column (col-fn cell)) + column-ref (when-let [a-ref (:field-ref column)] + (legacy-ref->pMBQL a-ref))] + {:column (fix-column-with-ref column-ref column) + :column-ref column-ref + :value (.-value cell)}))) (def ^:private row-cell (js-cells-by #(.-col ^js %))) (def ^:private dimension-cell (js-cells-by #(.-column ^js %))) @@ -874,15 +879,19 @@ - Nullable data row (the array of `{col, value}` pairs from `clicked.data`) - Nullable dimensions list (`{column, value}` pairs from `clicked.dimensions`)" [a-query stage-number column value row dimensions] - (->> (merge {:column (js.metadata/parse-column column) - :value (cond - (undefined? value) nil ; Missing a value, ie. a column click - (nil? value) :null ; Provided value is null, ie. database NULL - :else value)} - (when row {:row (mapv row-cell row)}) - (when (not-empty dimensions) {:dimensions (mapv dimension-cell dimensions)})) - (lib.core/available-drill-thrus a-query stage-number) - to-array)) + (lib.convert/with-aggregation-list (lib.core/aggregations a-query stage-number) + (let [column-ref (when-let [a-ref (.-field_ref ^js column)] + (legacy-ref->pMBQL a-ref))] + (->> (merge {:column (fix-column-with-ref column-ref (js.metadata/parse-column column)) + :column-ref column-ref + :value (cond + (undefined? value) nil ; Missing a value, ie. a column click + (nil? value) :null ; Provided value is null, ie. database NULL + :else value)} + (when row {:row (mapv row-cell row)}) + (when (not-empty dimensions) {:dimensions (mapv dimension-cell dimensions)})) + (lib.core/available-drill-thrus a-query stage-number) + to-array)))) (defn ^:export drill-thru "Applies the given `drill-thru` to the specified query and stage. Returns the updated query. diff --git a/src/metabase/lib/js/metadata.cljs b/src/metabase/lib/js/metadata.cljs index 7a3faf1c178..d3c4775ad83 100644 --- a/src/metabase/lib/js/metadata.cljs +++ b/src/metabase/lib/js/metadata.cljs @@ -211,7 +211,8 @@ (defmethod rename-key-fn :field [_object-type] - {:source :lib/source}) + {:source :lib/source + :unit :metabase.lib.field/temporal-unit}) (defn- parse-field-id [id] @@ -224,19 +225,20 @@ [_object-type] (fn [k v] (case k - :base-type (keyword v) - :coercion-strategy (keyword v) - :effective-type (keyword v) - :fingerprint (if (map? v) - (walk/keywordize-keys v) - (js->clj v :keywordize-keys true)) - :has-field-values (keyword v) - :lib/source (if (= v "aggregation") - :source/aggregations - (keyword "source" v)) - :semantic-type (keyword v) - :visibility-type (keyword v) - :id (parse-field-id v) + :base-type (keyword v) + :coercion-strategy (keyword v) + :effective-type (keyword v) + :fingerprint (if (map? v) + (walk/keywordize-keys v) + (js->clj v :keywordize-keys true)) + :has-field-values (keyword v) + :lib/source (if (= v "aggregation") + :source/aggregations + (keyword "source" v)) + :metabase.lib.field/temporal-unit (keyword v) + :semantic-type (keyword v) + :visibility-type (keyword v) + :id (parse-field-id v) v))) (defmethod parse-objects :field diff --git a/src/metabase/lib/schema/drill_thru.cljc b/src/metabase/lib/schema/drill_thru.cljc index 849449044bf..cb28d9008fb 100644 --- a/src/metabase/lib/schema/drill_thru.cljc +++ b/src/metabase/lib/schema/drill_thru.cljc @@ -10,6 +10,7 @@ [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.schema.order-by :as lib.schema.order-by] + [metabase.lib.schema.ref :as lib.schema.ref] [metabase.lib.schema.temporal-bucketing :as lib.schema.temporal-bucketing] [metabase.util.malli.registry :as mr])) @@ -157,8 +158,7 @@ ::drill-thru.common [:map [:type [:= :drill-thru/zoom-in.timeseries]] - [:column [:ref ::lib.schema.metadata/column]] - [:value some?] + [:dimension [:ref ::context.row.value]] [:next-unit [:ref ::drill-thru.zoom-in.timeseries.next-unit]]]]) (mr/def ::drill-thru @@ -182,34 +182,19 @@ [:drill-thru/automatic-insights ::drill-thru.automatic-insights] [:drill-thru/zoom-in.timeseries ::drill-thru.zoom-in.timeseries]]]) -;;; Frontend passes in something that looks like this. Why this shape? Who knows. -(comment - {:column {:lib/type :metadata/column - :remapped-from-index nil - :base-type :type/BigInteger - :semantic-type :type/Quantity - :name "count" - :lib/source :source/aggregations - :aggregation-index 0 - :effective-type :type/BigInteger - :display-name "Count" - :remapping nil} - :value 457 - :row [{:column-name "CREATED_AT", :value "2024-01-01T00:00:00Z"} - {:column-name "count", :value 457}] - :dimensions [{:column-name "CREATED_AT", :value "2024-01-01T00:00:00Z"}]}) - (mr/def ::context.row.value [:map - [:column-name string?] - [:value :any]]) + [:column [:ref ::lib.schema.metadata/column]] + [:column-ref [:ref ::lib.schema.ref/ref]] + [:value :any]]) (mr/def ::context.row [:sequential [:ref ::context.row.value]]) (mr/def ::context [:map - [:column [:ref ::lib.schema.metadata/column]] - [:value [:maybe :any]] + [:column [:ref ::lib.schema.metadata/column]] + [:column-ref [:ref ::lib.schema.ref/ref]] + [:value [:maybe :any]] [:row {:optional true} [:ref ::context.row]] [:dimensions {:optional true} [:maybe [:ref ::context.row]]]]) diff --git a/test/metabase/lib/drill_thru/distribution_test.cljc b/test/metabase/lib/drill_thru/distribution_test.cljc index 15e8041633f..8cbd37a373c 100644 --- a/test/metabase/lib/drill_thru/distribution_test.cljc +++ b/test/metabase/lib/drill_thru/distribution_test.cljc @@ -18,8 +18,9 @@ count-col (m/find-first (fn [col] (= (:display-name col) "Count")) (lib/returned-columns query)) - context {:column count-col - :value nil}] + context {:column count-col + :column-ref (lib/ref count-col) + :value nil}] (is (some? count-col)) (is (nil? (lib.drill-thru.distribution/distribution-drill query -1 context)))))) diff --git a/test/metabase/lib/drill_thru/sort_test.cljc b/test/metabase/lib/drill_thru/sort_test.cljc index 06f82ee521e..57120947e68 100644 --- a/test/metabase/lib/drill_thru/sort_test.cljc +++ b/test/metabase/lib/drill_thru/sort_test.cljc @@ -16,8 +16,9 @@ (let [query (lib/query meta/metadata-provider (meta/table-metadata :orders)) drill (lib.drill-thru.sort/sort-drill query -1 - {:column (meta/field-metadata :orders :id) - :value nil})] + {:column (meta/field-metadata :orders :id) + :column-ref (lib/ref (meta/field-metadata :orders :id)) + :value nil})] (is (=? {:type :drill-thru/sort :column {:id (meta/id :orders :id)} :sort-directions [:asc :desc]} @@ -55,8 +56,9 @@ count-col (m/find-first (fn [col] (= (:display-name col) "Count")) (lib/returned-columns query)) - context {:column count-col - :value nil}] + context {:column count-col + :column-ref (lib/ref count-col) + :value nil}] (is (some? count-col)) (let [drill (lib.drill-thru.sort/sort-drill query -1 context)] (is (=? {:lib/type :metabase.lib.drill-thru/drill-thru @@ -79,8 +81,10 @@ (let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders)) (lib/order-by (meta/field-metadata :orders :user-id)) (lib/order-by (meta/field-metadata :orders :id))) - context {:column (meta/field-metadata :orders :user-id) - :value nil} + user-id (meta/field-metadata :orders :user-id) + context {:column user-id + :column-ref (lib/ref user-id) + :value nil} drill (lib.drill-thru.sort/sort-drill query -1 context)] (is (=? {:stages [{:order-by [[:asc {} [:field {} (meta/id :orders :user-id)]] diff --git a/test/metabase/lib/drill_thru/summarize_column_by_time_test.cljc b/test/metabase/lib/drill_thru/summarize_column_by_time_test.cljc index e4f086708f2..a7bb5818a8f 100644 --- a/test/metabase/lib/drill_thru/summarize_column_by_time_test.cljc +++ b/test/metabase/lib/drill_thru/summarize_column_by_time_test.cljc @@ -19,8 +19,9 @@ count-col (m/find-first (fn [col] (= (:display-name col) "Count")) (lib/returned-columns query)) - context {:column count-col - :value nil}] + context {:column count-col + :column-ref (lib/ref count-col) + :value nil}] (is (some? count-col)) (is (nil? (lib.drill-thru.summarize-column-by-time/summarize-column-by-time-drill query -1 context)))))) diff --git a/test/metabase/lib/drill_thru/test_util.cljc b/test/metabase/lib/drill_thru/test_util.cljc index 77998658261..46474f67197 100644 --- a/test/metabase/lib/drill_thru/test_util.cljc +++ b/test/metabase/lib/drill_thru/test_util.cljc @@ -85,9 +85,9 @@ row :- Row {:keys [column-name click-type query-type], :as _test-case} :- TestCase] (let [cols (lib/returned-columns query -1 (lib.util/query-stage query -1)) - col (m/find-first (fn [col] - (= (:name col) column-name)) - cols) + by-name (m/index-by :name cols) + col (get by-name column-name) + refs (update-vals by-name lib/ref) _ (assert col (lib.util/format "No column found named %s; found: %s" (pr-str column-name) (pr-str (map :name cols)))) @@ -98,14 +98,20 @@ (for [col cols :when (and (= (:lib/source col) :source/breakouts) (not= (:name col) column-name))] - {:column-name (:name col), :value (get row (:name col))}))] + {:column col + :column-ref (get refs (:name col)) + :value (get row (:name col))}))] (merge - {:column col - :value nil} + {:column col + :column-ref (get refs column-name) + :value nil} (when (= click-type :cell) {:value value - :row (for [[column-name value] row] - {:column-name column-name, :value value}) + :row (for [[column-name value] row + :let [column (by-name column-name)]] + {:column column + :column-ref (get refs column-name) + :value value}) :dimensions dimensions})))) (def ^:private AvailableDrillsTestCase diff --git a/test/metabase/lib/drill_thru/zoom_in_timeseries_test.cljc b/test/metabase/lib/drill_thru/zoom_in_timeseries_test.cljc index e9d1b37ec49..ec3202375cc 100644 --- a/test/metabase/lib/drill_thru/zoom_in_timeseries_test.cljc +++ b/test/metabase/lib/drill_thru/zoom_in_timeseries_test.cljc @@ -20,18 +20,26 @@ :breakout [[:field {:temporal-unit :day} (meta/id :orders :created-at)] [:field {:temporal-unit :year} (meta/id :orders :created-at)]]}]} query)) - (let [created-at (m/find-first #(and (= (:id %) (meta/id :orders :created-at)) + (let [columns (lib/returned-columns query) + created-at (m/find-first #(and (= (:id %) (meta/id :orders :created-at)) (= (lib.temporal-bucket/raw-temporal-bucket %) :year)) (lib/returned-columns query)) _ (assert created-at) - drill (lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill query - -1 - {:column created-at - :value 2022})] + count-col (m/find-first #(= (:name %) "count") columns) + _ (assert count-col) + drill (lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill + query -1 + {:column count-col + :column-ref (lib/ref count-col) + :value 200 + :dimensions [{:column created-at + :column-ref (lib/ref created-at) + :value 2022}]})] (is (=? {:type :drill-thru/zoom-in.timeseries - :column {:id (meta/id :orders :created-at) - :metabase.lib.field/temporal-unit :year} - :value 2022 + :dimension {:column {:id (meta/id :orders :created-at) + :metabase.lib.field/temporal-unit :year} + :column-ref [:field {} (meta/id :orders :created-at)] + :value 2022} :next-unit :quarter :display-name "See this year by quarter"} drill)) @@ -46,18 +54,24 @@ [:field {:temporal-unit :year} (meta/id :orders :created-at)] 2022]]}]} query')) - (let [created-at (m/find-first #(and (= (:id %) (meta/id :orders :created-at)) + (let [columns (lib/returned-columns query') + created-at (m/find-first #(and (= (:id %) (meta/id :orders :created-at)) (= (lib.temporal-bucket/raw-temporal-bucket %) :quarter)) - (lib/returned-columns query')) + columns) _ (assert created-at) - drill (lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill query' - -1 - {:column created-at - :value "2022-04-01T00:00:00"})] + drill (lib.drill-thru.zoom-in-timeseries/zoom-in-timeseries-drill + query' -1 + {:column count-col + :column-ref (lib/ref count-col) + :value 19 + :dimensions [{:column created-at + :column-ref (lib/ref created-at) + :value "2022-04-01T00:00:00"}]})] (is (=? {:type :drill-thru/zoom-in.timeseries - :column {:id (meta/id :orders :created-at) - :metabase.lib.field/temporal-unit :quarter} - :value "2022-04-01T00:00:00" + :dimension {:column {:id (meta/id :orders :created-at) + :metabase.lib.field/temporal-unit :quarter} + :column-ref [:field {} (meta/id :orders :created-at)] + :value "2022-04-01T00:00:00"} :next-unit :month :display-name "See this quarter by month"} drill)) diff --git a/test/metabase/lib/drill_thru_test.cljc b/test/metabase/lib/drill_thru_test.cljc index 547ba40b227..727270b5889 100644 --- a/test/metabase/lib/drill_thru_test.cljc +++ b/test/metabase/lib/drill_thru_test.cljc @@ -6,6 +6,7 @@ [medley.core :as m] [metabase.lib.core :as lib] [metabase.lib.drill-thru.test-util :as lib.drill-thru.tu] + [metabase.lib.field :as-alias lib.field] [metabase.lib.schema :as lib.schema] [metabase.lib.test-metadata :as meta] [metabase.util :as u] @@ -20,29 +21,42 @@ (def ^:private orders-query (lib/query meta/metadata-provider (meta/table-metadata :orders))) +(defn- basic-context + [column value] + {:column column + :column-ref (lib/ref column) + :value value}) + +(defn- row-for [table col-values] + (mapv (fn [[col value]] + (basic-context (meta/field-metadata table col) value)) + col-values)) + (def ^:private orders-row - [{:column-name "ID" :value 2} - {:column-name "USER_ID" :value 1} - {:column-name "PRODUCT_ID" :value 123} - {:column-name "SUBTOTAL" :value 110.93} - {:column-name "TAX" :value 6.10} - {:column-name "TOTAL" :value 117.03} - {:column-name "DISCOUNT" :value nil} - {:column-name "CREATED_AT" :value "2018-05-15T08:04:04.58Z"} - {:column-name "QUANTITY" :value 3}]) - -(def ^:private products-query + (row-for :orders + [[:id 2] + [:user-id 1] + [:product-id 123] + [:subtotal 110.93] + [:tax 6.10] + [:total 117.03] + [:discount nil] + [:created-at "2018-05-15T08:04:04.58Z"] + [:quantity 3]])) + + (def ^:private products-query (lib/query meta/metadata-provider (meta/table-metadata :products))) (def ^:private products-row - [{:column-name "ID" :value 118} - {:column-name "EAN" :value "5291392809646"} - {:column-name "TITLE" :value "Synergistic Rubber Shoes"} - {:column-name "CATEGORY" :value "Gadget"} - {:column-name "VENDOR" :value "Herta Skiles and Sons"} - {:column-name "PRICE" :value 38.42} - {:column-name "RATING" :value 3.5} - {:column-name "CREATED_AT" :value "2016-10-19T12:34:56.789Z"}]) + (row-for :products + [[:id 118] + [:ean "5291392809646"] + [:title "Synergistic Rubber Shoes"] + [:category "Gadget"] + [:vendor "Herta Skiles and Sons"] + [:price 38.42] + [:rating 3.5] + [:created-at "2016-10-19T12:34:56.789Z"]])) (defn- drill-thru-test-args [drill] (case (:type drill) @@ -76,10 +90,10 @@ args (drill-thru-test-args drill)] (condp = (:type drill) :drill-thru/pivot - (log/warnf "drill-thru-method is not yet implemented for :drill-thru/pivot (#33559)") + (log/warn "drill-thru-method is not yet implemented for :drill-thru/pivot (#33559)") :drill-thru/underlying-records - (log/warnf "drill-thru-method is not yet implemented for :drill-thru/underlying-records (#34233)") + (log/warn "drill-thru-method is not yet implemented for :drill-thru/underlying-records (#34233)") (testing (str "\nquery =\n" (u/pprint-to-str query) "\ndrill =\n" (u/pprint-to-str drill) @@ -96,8 +110,7 @@ (deftest ^:parallel table-view-available-drill-thrus-headers-pk-test (testing "column headers: click on" (testing "primary key - column filter (default: Is), sort, summarize (distinct only)" - (let [context {:column (meta/field-metadata :orders :id) - :value nil}] + (let [context (basic-context (meta/field-metadata :orders :id) nil)] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/column-filter :column (meta/field-metadata :orders :id) @@ -116,8 +129,7 @@ (deftest ^:parallel table-view-available-drill-thrus-headers-fk-test (testing "column headers: click on" (testing "foreign key - distribution, column filter (default: Is), sort, summarize (distinct only)" - (let [context {:column (meta/field-metadata :orders :user-id) - :value nil}] + (let [context (basic-context (meta/field-metadata :orders :user-id) nil)] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/distribution :column (meta/field-metadata :orders :user-id)} @@ -139,8 +151,7 @@ (deftest ^:parallel table-view-available-drill-thrus-headers-numeric-column-test (testing "column headers: click on" (testing "numeric column - distribution, column filter (default: Equal To), sort, summarize (all 3), summarize by time" - (let [context {:column (meta/field-metadata :orders :subtotal) - :value nil}] + (let [context (basic-context (meta/field-metadata :orders :subtotal) nil)] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/distribution :column (meta/field-metadata :orders :subtotal)} @@ -167,8 +178,7 @@ (deftest ^:parallel table-view-available-drill-thrus-headers-date-column-test (testing "column headers: click on" (testing "date column - distribution, column filter (no default), sort, summarize (distinct only)" - (let [context {:column (meta/field-metadata :orders :created-at) - :value nil}] + (let [context (basic-context (meta/field-metadata :orders :created-at) nil)] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/distribution :column (meta/field-metadata :orders :created-at)} @@ -215,8 +225,7 @@ (testing (str "which is " sort-dir " and the sort drill only offers " other-option) (let [query (-> orders-query (lib/order-by -1 (meta/field-metadata :orders :subtotal) sort-dir)) - context {:column (meta/field-metadata :orders :subtotal) - :value nil}] + context (basic-context (meta/field-metadata :orders :subtotal) nil)] (is (=? (assoc-in expected [2 :sort-directions] [other-option]) (lib/available-drill-thrus query -1 context))) (test-drill-applications query context)))))))) @@ -224,9 +233,8 @@ (deftest ^:parallel table-view-available-drill-thrus-fk-value-test (testing "table values: click on" (testing "foreign key - FK filter and FK details" - (let [context {:column (meta/field-metadata :orders :user-id) - :value 1 - :row orders-row}] + (let [context (merge (basic-context (meta/field-metadata :orders :user-id) 1) + {:row orders-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/fk-filter :filter [:= {:lib/uuid string?} @@ -243,9 +251,8 @@ (deftest ^:parallel table-view-available-drill-thrus-numeric-value-test (testing "table values: click on" (testing "numeric value - numeric quick filters and object details *for the PK column*" - (let [context {:column (meta/field-metadata :orders :subtotal) - :value 110.93 - :row orders-row}] + (let [context (merge (basic-context (meta/field-metadata :orders :subtotal) 110.93) + {:row orders-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/zoom :column (meta/field-metadata :orders :id) ; It should correctly find the PK column @@ -267,9 +274,8 @@ (deftest ^:parallel table-view-available-drill-thrus-category-value-test (testing "table values: click on" (testing "category/enum value - filter is/is not, and object details *for the PK column*" - (let [context {:column (meta/field-metadata :products :category) - :value "Gadget" - :row products-row}] + (let [context (merge (basic-context (meta/field-metadata :products :category) "Gadget") + {:row products-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/zoom :column (meta/field-metadata :products :id) ; It should correctly find the PK column @@ -289,9 +295,8 @@ (deftest ^:parallel table-view-available-drill-thrus-string-value-test (testing "table values: click on" (testing "string value - filter (not) equal, and object details *for the PK column*" - (let [context {:column (meta/field-metadata :products :vendor) - :value "Herta Skiles and Sons" - :row products-row}] + (let [context (merge (basic-context (meta/field-metadata :products :vendor) "Herta Skiles and Sons") + {:row products-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/zoom :column (meta/field-metadata :products :id) ; It should correctly find the PK column @@ -311,9 +316,8 @@ (deftest ^:parallel table-view-available-drill-thrus-null-value-test (testing "table values: click on" (testing "NULL value - basic quick filters and object details *for the PK column*" - (let [context {:column (meta/field-metadata :orders :discount) - :value :null - :row orders-row}] + (let [context (merge (basic-context (meta/field-metadata :orders :discount) :null) + {:row orders-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/zoom :column (meta/field-metadata :orders :id) ; It should correctly find the PK column @@ -332,9 +336,8 @@ (deftest ^:parallel table-view-available-drill-thrus-date-value-test (testing "table values: click on" (testing "date value - date quick filters and object details *for the PK column*" - (let [context {:column (meta/field-metadata :orders :created-at) - :value "2018-05-15T08:04:04.58Z" - :row orders-row}] + (let [context (merge (basic-context (meta/field-metadata :orders :created-at) "2018-05-15T08:04:04.58Z") + {:row orders-row})] (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/zoom :column (meta/field-metadata :orders :id) ; It should correctly find the PK column @@ -369,8 +372,8 @@ created-at-column (m/find-first #(= (:name %) "CREATED_AT") (lib/returned-columns orders-count-aggregation-breakout-on-created-at-by-month-query)) _ (assert created-at-column) - row [{:column-name "CREATED_AT", :value "2018-05-01T00:00:00Z"} - {:column-name "count", :value 457}] + row [(basic-context created-at-column "2018-05-01T00:00:00Z") + (basic-context count-column 457)] expected-drills {:quick-filter {:lib/type :metabase.lib.drill-thru/drill-thru :type :drill-thru/quick-filter :operators [{:name "<"} @@ -384,24 +387,28 @@ :zoom-in.timeseries {:lib/type :metabase.lib.drill-thru/drill-thru :display-name "See this month by week" :type :drill-thru/zoom-in.timeseries - :column {:name "CREATED_AT" - :metabase.lib.field/temporal-unit :month} - :value "2018-05-01T00:00:00Z" - :next-unit :week}}] - (let [context {:column created-at-column - :value "2018-05-01T00:00:00Z" - :row row}] + :dimension {:column {:name "CREATED_AT" + ::lib.field/temporal-unit :month} + :column-ref some? + :value "2018-05-01T00:00:00Z"} + :next-unit :week} + :pivot {:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/pivot + :pivots {:category sequential? + :location sequential? + :time (symbol "nil #_\"key is not present.\"")}}}] + (let [context (merge (basic-context count-column 123) + {:row row})] (testing (str "\ncontext =\n" (u/pprint-to-str context)) - (is (=? (map expected-drills [:quick-filter :zoom-in.timeseries]) + (is (=? (map expected-drills [:pivot :quick-filter]) (lib/available-drill-thrus query -1 context))) (test-drill-applications query context))) (testing "with :dimensions" - (let [context {:column count-column - :value 457 - :row row - :dimensions [{:column-name "CREATED_AT", :value "2018-05-01T00:00:00Z"}]}] + (let [context (merge (basic-context count-column 457) + {:row row + :dimensions [(basic-context created-at-column "2018-05-01T00:00:00Z")]})] (testing (str "\ncontext =\n" (u/pprint-to-str context)) - (is (=? (map expected-drills [:quick-filter :underlying-records :zoom-in.timeseries]) + (is (=? (map expected-drills [:pivot :quick-filter :underlying-records :zoom-in.timeseries]) (lib/available-drill-thrus query -1 context))) (test-drill-applications query context)))))))) @@ -411,36 +418,29 @@ (let [query orders-count-aggregation-breakout-on-created-at-by-month-query column (m/find-first #(= (:name %) "count") (lib/returned-columns orders-count-aggregation-breakout-on-created-at-by-month-query)) - _ (assert column)] - (doseq [column [column - ;; should still work even if we're using metadata as returned by the QP... - ;; see [[metabase.lib.drill-thru/icky-hack-add-source-uuid-to-aggregation-column-metadata]] - (-> column - (dissoc :lib/source-uuid) - (assoc :aggregation-index 0))] - :let [context {:column column - :value "2018-05" - :row [{:column-name "CREATED_AT", :value "2018-05"} - {:column-name "count", :value 10}]}]] - (testing (str "\ncontext =\n" (u/pprint-to-str context)) - (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru - :type :drill-thru/pivot - :pivots {:category [{:name "NAME"} - {:name "SOURCE"} - {:name "TITLE"} - {:name "CATEGORY"} - {:name "VENDOR"}] - :location [{:name "CITY"} - {:name "STATE"} - {:name "ZIP"}]}} - {:lib/type :metabase.lib.drill-thru/drill-thru - :type :drill-thru/quick-filter - :operators [{:name "<"} - {:name ">"} - {:name "="} - {:name "≠"}]}] - (lib/available-drill-thrus query -1 context))) - (test-drill-applications query context))))))) + _ (assert column) + context (merge (basic-context column "2018-05") + {:row [(basic-context (meta/field-metadata :orders :created-at) "2018-05") + (basic-context column 10)]})] + (testing (str "\ncontext =\n" (u/pprint-to-str context)) + (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/pivot + :pivots {:category [{:name "NAME"} + {:name "SOURCE"} + {:name "TITLE"} + {:name "CATEGORY"} + {:name "VENDOR"}] + :location [{:name "CITY"} + {:name "STATE"} + {:name "ZIP"}]}} + {:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/quick-filter + :operators [{:name "<"} + {:name ">"} + {:name "="} + {:name "≠"}]}] + (lib/available-drill-thrus query -1 context))) + (test-drill-applications query context)))))) (deftest ^:parallel table-view-available-drill-thrus-aggregate-column-header-test (let [query (-> (lib/query meta/metadata-provider (meta/table-metadata :orders)) @@ -455,8 +455,9 @@ (= (:display-name col) "Count")) (lib/returned-columns query))] (is (some? count-col)) - (let [context {:column count-col - :value nil}] + (let [context {:column count-col + :column-ref (lib/ref count-col) + :value nil}] (is (=? [{:type :drill-thru/column-filter :column {:name "count"} :initial-op {:display-name-variant :equal-to @@ -470,8 +471,9 @@ (= (:display-name col) "Max of Discount")) (lib/returned-columns query))] (is (some? max-of-discount-col)) - (let [context {:column max-of-discount-col - :value nil}] + (let [context {:column max-of-discount-col + :column-ref (lib/ref max-of-discount-col) + :value nil}] (is (=? [{:type :drill-thru/column-filter, :column {:display-name "Max of Discount"} :initial-op {:display-name-variant :equal-to @@ -481,6 +483,47 @@ (lib/available-drill-thrus query -1 context))) (test-drill-applications query context)))))) +(deftest ^:parallel line-chart-available-drill-thrus-time-series-point-test + (testing "line chart: click on" + (testing "time series data point - underlying records, date zoom, pivot by non-date, automatic insights" + (let [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))) + columns (lib/returned-columns query) + sum (by-name columns "sum") + breakout (by-name columns "CREATED_AT") + sum-dim {:column sum + :column-ref (lib/ref sum) + :value 42295.12} + breakout-dim {:column breakout + :column-ref (first (lib/breakouts query)) + :value "2024-11-01T00:00:00Z"} + context (merge sum-dim + {:row [breakout-dim sum-dim] + :dimensions [breakout-dim]})] + (is (=? [{:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/pivot + :pivots {:category (repeat 5 {}) + :location (repeat 3 {})}} + {:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/quick-filter + :operators [{:name "<"} + {:name ">"} + {:name "="} + {:name "≠"}]} + {:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/underlying-records + #_#_:row-count (:value sum-dim) + #_#_:dimensions [breakout-dim] + #_#_:column-ref (:column-ref sum-dim)} + {:lib/type :metabase.lib.drill-thru/drill-thru + :type :drill-thru/zoom-in.timeseries + :dimension {:column breakout}}] + (lib/available-drill-thrus query -1 context))) + (test-drill-applications query context))))) + ;; TODO: Restore this test once zoom-in and underlying-records are checked properly. #_(deftest ^:parallel histogram-available-drill-thrus-test (testing "histogram breakout view" -- GitLab