diff --git a/src/metabase/query_processor/middleware/binning.clj b/src/metabase/query_processor/middleware/binning.clj index 84330472eaa80707058bdf4f1721be1356966f03..fd7a74efb308bbd8c07f7e32f761c74a162289e7 100644 --- a/src/metabase/query_processor/middleware/binning.clj +++ b/src/metabase/query_processor/middleware/binning.clj @@ -4,17 +4,22 @@ [metabase.query-processor.interface :as i]) (:import [metabase.query_processor.interface BinnedField ComparisonFilter BetweenFilter])) -(defn- update! [^clojure.lang.ITransientAssociative coll k f] +(defn- update! + "Similar to `clojure.core/update` but works on transient maps" + [^clojure.lang.ITransientAssociative coll k f] (assoc! coll k (f (get coll k)))) -(defn- filter->field-map [mbql-filter] +(defn- filter->field-map + "A bit of a stateful hack using clojure.walk/prewalk to find any + comparison or between filter. This should be replaced by a zipper + for a more functional/composable approach to this problem." + [mbql-filter] (let [acc (transient {})] - (clojure.walk/prewalk + (walk/prewalk (fn [x] - (when (or - (instance? BetweenFilter x) - (and (instance? ComparisonFilter x) - (contains? #{:< :> :<= :>=} (:filter-type x)))) + (when (or (instance? BetweenFilter x) + (and (instance? ComparisonFilter x) + (contains? #{:< :> :<= :>=} (:filter-type x)))) (update! acc (get-in x [:field :field-id]) #(if (seq %) (conj % x) [x]))) @@ -26,29 +31,35 @@ (u/round-to-decimals 5 (/ (- max-value min-value) num-bins))) -(defn- extract-bounds [{field-id :field-id, global-min :min-value, global-max :max-value} field-filter-map] - ;; Assuming only one for now - (let [bound-1 (first (for [{:keys [value filter-type]} (get field-filter-map field-id) - :when (or (= :> filter-type) - (= :>= filter-type))] - (:value value) )) - bound-2 (first (for [{:keys [value filter-type]} (get field-filter-map field-id) - :when (or (= :< filter-type) - (= :<= filter-type))] - (:value value))) - comparison-bounds (when (and bound-1 bound-2) - (if (> bound-1 bound-2) - [bound-2 bound-1] - [bound-1 bound-2])) - ;;Assuming either >/< or between - between-bounds (first (for [{:keys [filter-type min-value max-value]} (get field-filter-map field-id) - :when (= :between filter-type)] - [min-value max-value]))] - (or (seq comparison-bounds) - (seq between-bounds) - [global-min global-max]))) +(defn- extract-bounds + "Given query criteria, find a min/max value for the binning strategy + using the greatest user specified min value and the smallest user + specified max value. When a user specified min or max is not found, + use the global min/max for the given field." + [{field-id :field-id, global-min :min-value, global-max :max-value} field-filter-map] + (let [user-maxes (for [{:keys [filter-type] :as query-filter} (get field-filter-map field-id) + :when (contains? #{:< :<= :between} filter-type)] + (if (= :between filter-type) + (get-in query-filter [:max-val :value]) + (get-in query-filter [:value :value]))) + user-mins (for [{:keys [filter-type] :as query-filter} (get field-filter-map field-id) + :when (contains? #{:> :>= :between} filter-type)] + (if (= :between filter-type) + (get-in query-filter [:min-val :value]) + (get-in query-filter [:value :value])))] -(defn- update-bin-width [breakouts filter-field-map] + [(or (when (seq user-mins) + (apply max user-mins)) + global-min) + (or (when (seq user-maxes) + (apply min user-maxes)) + global-max)])) + +(defn- update-bin-width + "Calculates the bin width given the global min/max and user + specified crtieria that could impact that min/max. Throws an + Exception if no min/max values are found." + [breakouts filter-field-map] (mapv (fn [{:keys [field num-bins] :as breakout}] (if (instance? BinnedField breakout) (let [[min-value max-value] (extract-bounds field filter-field-map)] diff --git a/test/metabase/query_processor/middleware/binning_test.clj b/test/metabase/query_processor/middleware/binning_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..7b9ed4339a077bdaf92f7e7000c54b02ae819d74 --- /dev/null +++ b/test/metabase/query_processor/middleware/binning_test.clj @@ -0,0 +1,56 @@ +(ns metabase.query-processor.middleware.binning-test + (:require [expectations :refer [expect]] + [metabase.query-processor.middleware.binning :refer :all] + [metabase.query-processor.expand :as ql] + [metabase.test.util :as tu])) + +(tu/resolve-private-vars metabase.query-processor.middleware.binning filter->field-map extract-bounds) + +(expect + {} + (filter->field-map (ql/and + (ql/= (ql/field-id 1) 10) + (ql/= (ql/field-id 2) 10)))) + +(expect + {1 [(ql/< (ql/field-id 1) 10) (ql/> (ql/field-id 1) 1)] + 2 [(ql/> (ql/field-id 2) 20) (ql/< (ql/field-id 2) 10)] + 3 [(ql/between (ql/field-id 3) 5 10)]} + (filter->field-map (ql/and + (ql/< (ql/field-id 1) 10) + (ql/> (ql/field-id 1) 1) + (ql/> (ql/field-id 2) 20) + (ql/< (ql/field-id 2) 10) + (ql/between (ql/field-id 3) 5 10)))) + +(expect + [1 10] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {1 [(ql/> (ql/field-id 1) 1) (ql/< (ql/field-id 1) 10)]})) + +(expect + [1 10] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {1 [(ql/between (ql/field-id 1) 1 10)]})) + +(expect + [100 1000] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {})) + +(expect + [500 1000] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {1 [(ql/> (ql/field-id 1) 500)]})) + +(expect + [100 500] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {1 [(ql/< (ql/field-id 1) 500)]})) + +(expect + [600 700] + (extract-bounds {:field-id 1 :min-value 100 :max-value 1000} + {1 [(ql/> (ql/field-id 1) 200) + (ql/< (ql/field-id 1) 800) + (ql/between (ql/field-id 1) 600 700)]}))