SQL places restrictions when using a Bad: SELECT count(*) FROM table GROUP BY CAST(x AS date) ORDER BY x ASC (MBQL) {:source-table 1 :breakout [[:field 1 {:temporal-unit :day}]] :order-by [[:asc [:field 1 nil]]]} Good: SELECT count(*) FROM table GROUP BY CAST(x AS date) ORDER BY CAST(x AS date) ASC (MBQL) {:source-table 1 :breakout [[:field 1 {:temporal-unit :day}]] :order-by [[:asc [:field 1 {:temporal-unit :day}]]]} The frontend, on the rare occasion it generates a query that explicitly specifies an | (ns metabase.query-processor.middleware.reconcile-breakout-and-order-by-bucketing (:require [metabase.legacy-mbql.schema :as mbql.s] [metabase.lib.util.match :as lib.util.match] [metabase.util.malli :as mu])) |
(mu/defn- reconcile-bucketing :- mbql.s/Query [{{breakouts :breakout} :query, :as query} :- :map] ;; Look for bucketed fields in the `breakout` clause and build a map of unbucketed reference -> bucketed reference, ;; like: ;; ;; {[:field 1 nil] [:field 1 {:temporal-unit :day}]} ;; ;; In causes where a Field is broken out more than once, prefer the bucketing used by the first breakout; accomplish ;; this by reversing the sequence of matches below, meaning the first match will get merged into the map last, ;; overwriting later matches (let [unbucketed-ref->bucketed-ref (into {} (reverse (lib.util.match/match breakouts [:field id-or-name opts] [[:field id-or-name (not-empty (dissoc opts :temporal-unit :binning))] &match])))] ;; rewrite order-by clauses as needed... (-> (lib.util.match/replace-in query [:query :order-by] ;; if order by is already bucketed, nothing to do [:field id-or-name (_ :guard (some-fn :temporal-unit :binning))] &match ;; if we run into a field that wasn't matched by the last pattern, see if there's an unbucketed reference ;; -> bucketed reference from earlier :field (if-let [bucketed-reference (unbucketed-ref->bucketed-ref &match)] ;; if there is, replace it with the bucketed reference bucketed-reference ;; if there's not, again nothing to do. &match)) ;; now remove any duplicate order-by clauses we may have introduced, as those are illegal in MBQL 2000 (update-in [:query :order-by] (comp vec distinct))))) | |
Replace any unbucketed {:query {:breakout [[:field 1 {:temporal-unit :day}]] :order-by [[:asc [:field 1 nil]]]}} -> {:query {:breakout [[:field 1 {:temporal-unit :day}]] :order-by [[:asc [:field 1 {:temporal-unit :day}]]]}} | (defn reconcile-breakout-and-order-by-bucketing [{{breakouts :breakout, order-bys :order-by} :query, :as query}] (if (or ;; if there's no breakouts bucketed by a datetime-field or binning-strategy... (empty? (lib.util.match/match breakouts [:field _ (_ :guard (some-fn :temporal-unit :binning))])) ;; or if there's no order-bys that are *not* bucketed... (empty? (lib.util.match/match order-bys [:field _ (_ :guard (some-fn :temporal-unit :binning))] nil :field &match))) ;; return query as is query ;; otherwise, time to bucket (reconcile-bucketing query))) |