From 6a2868841eae4c151ca55aedd30922bb38d7de81 Mon Sep 17 00:00:00 2001
From: Ryan Senior <ryan@metabase.com>
Date: Tue, 13 Jun 2017 19:29:30 -0500
Subject: [PATCH] First cut at constraing min/max from query criteria

---
 .../driver/generic_sql/query_processor.clj    |  2 +-
 src/metabase/query_processor.clj              |  2 +
 src/metabase/query_processor/interface.clj    |  7 +-
 .../query_processor/middleware/binning.clj    | 67 +++++++++++++++++++
 src/metabase/query_processor/resolve.clj      |  8 +--
 .../query_processor_test/breakout_test.clj    | 10 +++
 6 files changed, 86 insertions(+), 10 deletions(-)
 create mode 100644 src/metabase/query_processor/middleware/binning.clj

diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index e89476b054e..a9d8a0d4833 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -89,7 +89,7 @@
     (sql/date (driver) unit (formatted field)))
 
   BinnedField
-  (formatted [{{:keys [min-value max-value] :as field} :field bin-width :bin-width}]
+  (formatted [{:keys [bin-width min-value max-value field]}]
     (let [formatted-field (formatted field)]
       (apply hsql/call :case
              (mapcat (fn [bin-floor]
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index 9f486d28aa3..1594d6b26f2 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -12,6 +12,7 @@
              [add-row-count-and-status :as row-count-and-status]
              [add-settings :as add-settings]
              [annotate-and-sort :as annotate-and-sort]
+             [binning :as binning]
              [cache :as cache]
              [catch-exceptions :as catch-exceptions]
              [cumulative-aggregations :as cumulative-ags]
@@ -85,6 +86,7 @@
       cumulative-ags/handle-cumulative-aggregations
       implicit-clauses/add-implicit-clauses
       format-rows/format-rows
+      binning/update-binning-strategy
       expand-resolve/expand-resolve                    ; ▲▲▲ QUERY EXPANSION POINT  ▲▲▲ All functions *above* will see EXPANDED query during PRE-PROCESSING
       row-count-and-status/add-row-count-and-status    ; ▼▼▼ RESULTS WRAPPING POINT ▼▼▼ All functions *below* will see results WRAPPED in `:data` during POST-PROCESSING
       parameters/substitute-parameters
diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj
index c9c9599c730..10e8793c8e8 100644
--- a/src/metabase/query_processor/interface.clj
+++ b/src/metabase/query_processor/interface.clj
@@ -141,8 +141,11 @@
   clojure.lang.Named
   (getName [_] (name field)))
 
-(s/defrecord BinnedField [field :- Field
-                          num-bins  :- s/Int]
+(s/defrecord BinnedField [field     :- Field
+                          num-bins  :- s/Int
+                          min-value :- s/Num
+                          max-value :- s/Num
+                          bin-width :- s/Num]
   clojure.lang.Named
   (getName [_] (name field)))
 
diff --git a/src/metabase/query_processor/middleware/binning.clj b/src/metabase/query_processor/middleware/binning.clj
new file mode 100644
index 00000000000..2879947fae7
--- /dev/null
+++ b/src/metabase/query_processor/middleware/binning.clj
@@ -0,0 +1,67 @@
+(ns metabase.query-processor.middleware.binning
+  (:require [clojure.walk :as walk]
+            [metabase.util :as u])
+  (:import [metabase.query_processor.interface BinnedField ComparisonFilter BetweenFilter]))
+
+(defn- update! [^clojure.lang.ITransientAssociative coll k f]
+  (assoc! coll k (f (get coll k))))
+
+(defn- filter->field-map [mbql-filter]
+  (let [acc (transient {})]
+    (clojure.walk/prewalk
+     (fn [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])))
+       x)
+     mbql-filter)
+    (persistent! acc)))
+
+(defn- calculate-bin-width [min-value max-value num-bins]
+  (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 update-bin-width [breakouts filter-field-map]
+  (mapv (fn [{:keys [field num-bins] :as breakout}]
+          (if-let [[min-value max-value] (and (instance? BinnedField breakout)
+                                              (extract-bounds field filter-field-map))]
+            (assoc breakout
+              :min-value min-value
+              :max-value max-value
+              :bin-width (calculate-bin-width min-value max-value num-bins))
+            breakout))
+        breakouts))
+
+(defn update-binning-strategy
+  [qp]
+  (fn [query]
+    (let [binned-breakouts (filter #(instance? BinnedField %) (get-in query [:query :breakout]))]
+      (if (seq binned-breakouts)
+        (qp (update-in query [:query :breakout] update-bin-width (filter->field-map (get-in query [:query :filter]))))
+        (qp query)))))
diff --git a/src/metabase/query_processor/resolve.clj b/src/metabase/query_processor/resolve.clj
index 458d7d93ea6..7bfaa3b565d 100644
--- a/src/metabase/query_processor/resolve.clj
+++ b/src/metabase/query_processor/resolve.clj
@@ -100,11 +100,6 @@
 
 ;;; ## ------------------------------------------------------------ FIELD PLACEHOLDER ------------------------------------------------------------
 
-(defn- calculate-bin-width [field num-bins]
-  (u/round-to-decimals 5 (/ (- (:max-value field)
-                               (:min-value field))
-                            num-bins)))
-
 (defn- field-ph-resolve-field [{:keys [field-id datetime-unit fk-field-id binning-strategy], :as this} field-id->field]
   (if-let [{:keys [base-type special-type], :as field} (some-> (field-id->field field-id)
                                                                i/map->Field
@@ -116,8 +111,7 @@
                              :unit  (or datetime-unit :day)}) ; default to `:day` if a unit wasn't specified
       binning-strategy
       (i/map->BinnedField {:field field
-                           :num-bins binning-strategy
-                           :bin-width (calculate-bin-width field binning-strategy)})
+                           :num-bins binning-strategy})
       :else field)
     ;; If that fails just return ourselves as-is
     this))
diff --git a/test/metabase/query_processor_test/breakout_test.clj b/test/metabase/query_processor_test/breakout_test.clj
index 5e3089a5a8b..90dcc0ab719 100644
--- a/test/metabase/query_processor_test/breakout_test.clj
+++ b/test/metabase/query_processor_test/breakout_test.clj
@@ -111,6 +111,16 @@
               (ql/aggregation (ql/count))
               (ql/breakout (ql/binning-strategy $latitude :default)))))))
 
+(expect-with-non-timeseries-dbs
+  [[33.0 4] [34.0 57]]
+  (tu/with-temporary-setting-values [breakout-bins-num 15]
+    (format-rows-by [(partial u/round-to-decimals 1) int]
+      (rows (data/run-query venues
+              (ql/aggregation (ql/count))
+              (ql/filter (ql/and (ql/< $latitude 35)
+                                 (ql/> $latitude 20)))
+              (ql/breakout (ql/binning-strategy $latitude :default)))))))
+
 ;;Validate binning info is returned with the binning-strategy
 (expect-with-non-timeseries-dbs
   (merge (venues-col :latitude)
-- 
GitLab