diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj index d048a8597efaf6bfc9a927dbd0c7bb0f87d655c0..cfa8b84ed2ab69b0ff501e3b39b203bed08e17ad 100644 --- a/src/metabase/driver/generic_sql/query_processor.clj +++ b/src/metabase/driver/generic_sql/query_processor.clj @@ -113,7 +113,7 @@ (->honeysql driver field)) (defmethod ->honeysql [Object BinnedField] - [driver {:keys [bin-width min-value max-value field]}] + [driver {:keys [bin-width min-value max-value field] :as binned-field}] (let [honeysql-field-form (->honeysql driver field)] ;; ;; Equation is | (value - min) | diff --git a/src/metabase/mbql/normalize.clj b/src/metabase/mbql/normalize.clj index 8b5364c37d1a359dd6de83d623e7e1594a7ce766..b8af46de4651ccf353cdbc0ff4b1964286ed8da5 100644 --- a/src/metabase/mbql/normalize.clj +++ b/src/metabase/mbql/normalize.clj @@ -197,20 +197,27 @@ (defn- normalize-source-query [{native? :native, :as source-query}] (normalize-tokens source-query [(if native? :native :query)])) +(defn- normalize-source-metadata [metadata] + (-> metadata + (update :base_type keyword) + (update :special_type keyword) + (update :fingerprint walk/keywordize-keys ))) + (def ^:private path->special-token-normalization-fn "Map of special functions that should be used to perform token normalization for a given path. For example, the `:expressions` key in an MBQL query should preserve the case of the expression names; this custom behavior is defined below." - {:type mbql.u/normalize-token + {:type mbql.u/normalize-token ;; don't normalize native queries - :native {:query identity - :template-tags normalize-template-tags} - :query {:aggregation normalize-ag-clause-tokens - :expressions normalize-expressions-tokens - :order-by normalize-order-by-tokens - :source-query normalize-source-query} - :parameters {::sequence normalize-query-parameter} - :context #(some-> % mbql.u/normalize-token)}) + :native {:query identity + :template-tags normalize-template-tags} + :query {:aggregation normalize-ag-clause-tokens + :expressions normalize-expressions-tokens + :order-by normalize-order-by-tokens + :source-query normalize-source-query} + :parameters {::sequence normalize-query-parameter} + :context #(some-> % mbql.u/normalize-token) + :source-metadata {::sequence normalize-source-metadata}}) (defn normalize-tokens "Recursively normalize tokens in `x`. diff --git a/src/metabase/mbql/schema.clj b/src/metabase/mbql/schema.clj index 2b6ebcb21ebd2691cf6225d248c5c60ff666b348..de7306ffb38a88d250b30df39c786081bc617c04 100644 --- a/src/metabase/mbql/schema.clj +++ b/src/metabase/mbql/schema.clj @@ -31,6 +31,7 @@ "Schema for an MBQL datetime string literal, in ISO-8601 format." (s/constrained su/NonBlankString du/date-string? "datetime-literal")) +;; TODO - `unit` is not allowed if `n` is `current` (defclause relative-datetime n (s/cond-pre (s/eq :current) s/Int) unit (optional RelativeDatetimeUnit)) @@ -71,13 +72,30 @@ unit DatetimeFieldUnit) ;; binning strategy can wrap any of the above clauses, but again, not another binning strategy clause -(def ^:private BinningStrategyName +(def BinningStrategyName + "Schema for a valid value for the `strategy-name` param of a `binning-strategy` clause." (s/enum :num-bins :bin-width :default)) +(def BinnableField + "Schema for any sort of field clause that can be wrapped by a `binning-strategy` clause." + (one-of field-id fk-> datetime-field field-literal)) + +(def ResolvedBinningStrategyOptions + "Schema for map of options tacked on to the end of `binning-strategy` clauses by the `binning` middleware." + {:num-bins su/IntGreaterThanZero + :bin-width (s/constrained s/Num (complement neg?) "bin width must be >= 0.") + :min-value s/Num + :max-value s/Num}) + +;; TODO - binning strategy param is disallowed for `:default` and required for the others. For `num-bins` it must also +;; be an integer. (defclause binning-strategy - field (one-of field-id field-literal fk-> expression datetime-field) - strategy-name BinningStrategyName - strategy-param (optional s/Num)) + field BinnableField + strategy-name BinningStrategyName + strategy-param (optional (s/maybe (s/constrained s/Num (complement neg?) "strategy param must be >= 0."))) + ;; These are added in automatically by the `binning` middleware. Don't add them yourself, as they're just be + ;; replaced. + resolved-options (optional ResolvedBinningStrategyOptions)) (def Field "Schema for anything that refers to a Field, from the common `[:field-id <id>]` to variants like `:datetime-field` or @@ -245,6 +263,7 @@ "Schema for things that make sense in a filter like `>` or `<`, i.e. things that can be sorted." (s/cond-pre s/Num + s/Str DatetimeLiteral FieldOrRelativeDatetime)) @@ -450,6 +469,17 @@ ;; when we get a chance (s/optional-key :query-type) (s/enum "MBQL" "native")}) +(def SourceQueryMetadata + "Schema for the expected keys in metadata about source query columns if it is passed in to the query." + ;; TODO - there is a very similar schema in `metabase.sync.analyze.query-results`; see if we can merge them + {:name su/NonBlankString + :display_name su/NonBlankString + :base_type su/FieldType + (s/optional-key :special_type) (s/maybe su/FieldType) + ;; you'll need to provide this in order to use BINNING + (s/optional-key :fingerprint) (s/maybe su/Map) + s/Any s/Any}) + ;;; --------------------------------------------- Metabase [Outer] Query --------------------------------------------- @@ -458,30 +488,36 @@ `Card.dataset_query`." (s/constrained ;; TODO - move database/virtual-id into this namespace so we don't have to use the magic number here - {:database (s/cond-pre (s/eq -1337) su/IntGreaterThanZero) + {:database (s/cond-pre (s/eq -1337) su/IntGreaterThanZero) ;; Type of query. `:query` = MBQL; `:native` = native. TODO - consider normalizing `:query` to `:mbql` - :type (s/enum :query :native) - (s/optional-key :native) NativeQuery - (s/optional-key :query) MBQLQuery - (s/optional-key :parameters) [Parameter] + :type (s/enum :query :native) + (s/optional-key :native) NativeQuery + (s/optional-key :query) MBQLQuery + (s/optional-key :parameters) [Parameter] ;; ;; OPTIONS ;; ;; These keys are used to tweak behavior of the Query Processor. ;; TODO - can we combine these all into a single `:options` map? ;; - (s/optional-key :settings) (s/maybe Settings) - (s/optional-key :constraints) (s/maybe Constraints) - (s/optional-key :middleware) (s/maybe MiddlewareOptions) + (s/optional-key :settings) (s/maybe Settings) + (s/optional-key :constraints) (s/maybe Constraints) + (s/optional-key :middleware) (s/maybe MiddlewareOptions) ;; ;; INFO ;; ;; Used when recording info about this run in the QueryExecution log; things like context query was ran in and ;; User who ran it - (s/optional-key :info) (s/maybe Info) + (s/optional-key :info) (s/maybe Info) + ;; Info about the columns of the source query. Added in automatically by middleware. This metadata is primarily + ;; used to let power things like binning when used with Field Literals instead of normal Fields + (s/optional-key :source-metadata) (s/maybe [SourceQueryMetadata]) + #_:fk-field-ids + #_:table-ids + ;; ;; Other various keys get stuck in the query dictionary at some point or another by various pieces of QP ;; middleware to record bits of state. Everyone else can ignore them. - s/Keyword s/Any} + s/Keyword s/Any} (fn [{native :native, mbql :query, query-type :type}] (case query-type :native (core/and native (core/not mbql)) diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj index 5af54c71b64aa402c87ff43db761fa47ea0ae012..963bb87b65e643de0ca868840fd06272d2d31d81 100644 --- a/src/metabase/query_processor.clj +++ b/src/metabase/query_processor.clj @@ -35,6 +35,7 @@ [permissions :as perms] [resolve :as resolve] [resolve-driver :as resolve-driver] + [resolve-fields :as resolve-fields] [results-metadata :as results-metadata] [source-table :as source-table] [store :as store] @@ -101,9 +102,10 @@ cumulative-ags/handle-cumulative-aggregations results-metadata/record-and-return-metadata! format-rows/format-rows - binning/update-binning-strategy resolve/resolve-middleware expand/expand-middleware ; ▲▲▲ QUERY EXPANSION POINT ▲▲▲ All functions *above* will see EXPANDED query during PRE-PROCESSING + binning/update-binning-strategy + resolve-fields/resolve-fields add-dim/add-remapping implicit-clauses/add-implicit-clauses bucket-datetime/auto-bucket-datetime-breakouts diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj index cb8e54032e5be84c081b7a98a57e68eafd0b62a5..95d410dddd43629742b6ecc19857c7a3e925ac3e 100644 --- a/src/metabase/query_processor/interface.clj +++ b/src/metabase/query_processor/interface.clj @@ -205,6 +205,7 @@ base-type :- su/FieldType binning-strategy :- (s/maybe (apply s/enum binning-strategies)) binning-param :- (s/maybe s/Num) + binning-opts :- s/Any fingerprint :- (s/maybe i/Fingerprint)] nil :load-ns true @@ -275,6 +276,7 @@ remapped-to :- (s/maybe s/Str) field-display-name :- (s/maybe s/Str) binning-strategy :- (s/maybe (apply s/enum binning-strategies)) + binning-opts :- s/Any binning-param :- (s/maybe s/Num)] nil :load-ns true) diff --git a/src/metabase/query_processor/middleware/annotate_and_sort.clj b/src/metabase/query_processor/middleware/annotate_and_sort.clj index 2c408596225625556b6028c7fdff93b1737696fe..2c63eccfa7b922c5d9153a3a8ce97cebd5b118ef 100644 --- a/src/metabase/query_processor/middleware/annotate_and_sort.clj +++ b/src/metabase/query_processor/middleware/annotate_and_sort.clj @@ -1,24 +1,17 @@ (ns metabase.query-processor.middleware.annotate-and-sort "Middleware for annotating (adding type information to) the results of a query and sorting the columns in the results." - ;; TODO - `annotate` and `sort` are technically two seperate steps. We should decouple everything so we get a namespace - ;; structure looking more like: - ;; `metabase.query-processor.middleware.sort` - ;; `metabase.query-processor.middleware.annotate` - ;; `metabase.query-processor.middleware.mbql` - ;; `metabase.query-processor.middleware.sql` (:require [metabase.driver :as driver] [metabase.models.humanization :as humanization] - [metabase.query-processor - [annotate :as annotate] - [util :as qputil]])) + [metabase.query-processor.annotate :as annotate])) -;;; +------------------------------------------------------------------------------------------------------------------------+ -;;; | NATIVE QUERY ANNOTATION | -;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | NATIVE QUERY ANNOTATION | +;;; +----------------------------------------------------------------------------------------------------------------+ (defn- infer-column-types - "Infer the types of columns by looking at the first value for each in the results, and add the relevant information in `:cols`. - This is used for native queries, which don't have the type information from the original `Field` objects used in the query, which is added to the results by `annotate`." + "Infer the types of columns by looking at the first value for each in the results, and add the relevant information in + `:cols`. This is used for native queries, which don't have the type information from the original `Field` objects + used in the query, which is added to the results by `annotate`." [{:keys [columns rows], :as results}] (assoc results :columns (mapv name columns) @@ -30,17 +23,16 @@ (nth row i))) :type/*)})))) - -;;; +------------------------------------------------------------------------------------------------------------------------+ -;;; | GENERAL MIDDLEWARE | -;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | GENERAL MIDDLEWARE | +;;; +----------------------------------------------------------------------------------------------------------------+ (defn annotate-and-sort "Middleware for adding type information to columns returned by running a query, and sorting the columns in the results." [qp] - (fn [query] + (fn [{query-type :type, :as query}] (let [results (qp query)] - (-> (if-not (or (qputil/mbql-query? query) + (-> (if-not (or (= query-type :query) (:annotate? results)) (infer-column-types results) (annotate/annotate-and-sort query results)) diff --git a/src/metabase/query_processor/middleware/binning.clj b/src/metabase/query_processor/middleware/binning.clj index 53c333bb23e80575af7357f2860499e1f9185a0f..7f98875b2cdbeb35b0d2f28d75ce77c487272d41 100644 --- a/src/metabase/query_processor/middleware/binning.clj +++ b/src/metabase/query_processor/middleware/binning.clj @@ -1,68 +1,91 @@ (ns metabase.query-processor.middleware.binning + "Middleware that handles `binning-strategy` Field clauses. This adds a `resolved-options` map to every + `binning-strategy` clause that contains the information query processors will need in order to perform binning." (:require [clojure.math.numeric-tower :refer [ceil expt floor]] - [clojure.walk :as walk] [metabase [public-settings :as public-settings] [util :as u]] - [metabase.query-processor.util :as qputil]) - (:import [metabase.query_processor.interface BetweenFilter BinnedField ComparisonFilter])) - -(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 - "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 {})] - (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 + [metabase.mbql + [schema :as mbql.s] + [util :as mbql.u]] + [metabase.query-processor.store :as qp.store] + [metabase.util + [i18n :refer [tru]] + [schema :as su]] + [schema.core :as s])) + +;;; ----------------------------------------------- Extracting Bounds ------------------------------------------------ + +(def ^:private FieldID->Filters {su/IntGreaterThanZero [mbql.s/Filter]}) + +(s/defn ^:private filter->field-map :- FieldID->Filters + "Find any comparison or `:between` filter and return a map of referenced Field ID -> all the clauses the reference + it." + [filter-clause :- (s/maybe mbql.s/Filter)] + (reduce + (partial merge-with concat) + {} + (for [filter-clause (mbql.u/clause-instances #{:between :< :<= :> :>=} filter-clause) + [_ field-id] (mbql.u/clause-instances #{:field-id} filter-clause)] + {field-id [filter-clause]}))) + +(s/defn ^:private extract-bounds :- {:min-value s/Num, :max-value s/Num} + "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 :- (s/maybe su/IntGreaterThanZero), fingerprint :- (s/maybe su/Map), field-id->filters :- FieldID->Filters] + (let [{global-min :min, global-max :max} (get-in fingerprint [:type :type/Number]) + filter-clauses (get field-id->filters field-id) + ;; [:between <field> <min> <max>] or [:< <field> <x>] + user-maxes (for [[clause-name :as filter-clause] filter-clauses + :when (#{:< :<= :between} clause-name)] + (last filter-clause)) + user-mins (for [[clause-name :as filter-clause] filter-clauses + :when (#{:> :>= :between} clause-name)] + (if (= :between clause-name) + (nth filter-clause 2) + (last filter-clause))) + min-value (or (when (seq user-mins) + (apply max user-mins)) + global-min) + max-value (or (when (seq user-maxes) + (apply min user-maxes)) + global-max)] + (when-not (and min-value max-value) + (throw (Exception. (str (tru "Unable to bin Field without a min/max value"))))) + {:min-value min-value, :max-value max-value})) + + +;;; ------------------------------------------ Calculating resolved options ------------------------------------------ + +(s/defn ^:private calculate-bin-width :- s/Num "Calculate bin width required to cover interval [`min-value`, `max-value`] with `num-bins`." - [min-value max-value num-bins] + [min-value :- s/Num, max-value :- s/Num, num-bins :- su/IntGreaterThanZero] (u/round-to-decimals 5 (/ (- max-value min-value) num-bins))) -(defn calculate-num-bins +(s/defn ^:private calculate-num-bins :- su/IntGreaterThanZero "Calculate number of bins of width `bin-width` required to cover interval [`min-value`, `max-value`]." - [min-value max-value bin-width] + [min-value :- s/Num, max-value :- s/Num, bin-width :- (s/constrained s/Num (complement neg?) "number >= 0")] (long (Math/ceil (/ (- max-value min-value) - bin-width)))) + bin-width)))) -(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." - [{:keys [field-id fingerprint]} field-filter-map] - (let [{global-min :min, global-max :max} (get-in fingerprint [:type :type/Number]) - 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])))] - [(or (when (seq user-mins) - (apply max user-mins)) - global-min) - (or (when (seq user-maxes) - (apply min user-maxes)) - global-max)])) +(s/defn ^:private resolve-default-strategy :- [(s/one (s/enum :bin-width :num-bins) "strategy") + (s/one {:bin-width s/Num, :num-bins su/IntGreaterThanZero} "opts")] + "Determine the approprate strategy & options to use when `:default` strategy was specified." + [metadata :- {:special_type su/FieldType, s/Any s/Any}, min-value :- s/Num, max-value :- s/Num] + (if (isa? (:special_type metadata) :type/Coordinate) + (let [bin-width (public-settings/breakout-bin-width)] + [:bin-width + {:bin-width bin-width + :num-bins (calculate-num-bins min-value max-value bin-width)}]) + (let [num-bins (public-settings/breakout-bins-num)] + [:num-bins + {:num-bins num-bins + :bin-width (calculate-bin-width min-value max-value num-bins)}]))) + + +;;; ------------------------------------- Humanized binning with nicer-breakout -------------------------------------- (defn- ceil-to [precision x] @@ -76,8 +99,8 @@ (def ^:private ^:const pleasing-numbers [1 1.25 2 2.5 3 5 7.5 10]) -(defn- nicer-bin-width - [min-value max-value num-bins] +(s/defn ^:private nicer-bin-width + [min-value :- s/Num, max-value :- s/Num, num-bins :- su/IntGreaterThanZero] (let [min-bin-width (calculate-bin-width min-value max-value num-bins) scale (expt 10 (u/order-of-magnitude min-bin-width))] (->> pleasing-numbers @@ -100,69 +123,75 @@ (drop-while (partial apply not=)) ffirst))) -(def ^{:arglists '([binned-field])} nicer-breakout +(s/defn ^:private nicer-breakout* :- mbql.s/ResolvedBinningStrategyOptions "Humanize binning: extend interval to start and end on a \"nice\" number and, when number of bins is fixed, have a \"nice\" step (bin width)." - (fixed-point - (fn - [{:keys [min-value max-value bin-width num-bins strategy] :as binned-field}] - (let [bin-width (if (= strategy :num-bins) - (nicer-bin-width min-value max-value num-bins) - bin-width) - [min-value max-value] (nicer-bounds min-value max-value bin-width)] - (-> binned-field - (assoc :min-value min-value - :max-value max-value - :num-bins (if (= strategy :num-bins) - num-bins - (calculate-num-bins min-value max-value bin-width)) - :bin-width bin-width)))))) - -(defn- resolve-default-strategy [{:keys [strategy field]} min-value max-value] - (if (isa? (:special-type field) :type/Coordinate) - (let [bin-width (public-settings/breakout-bin-width)] - {:strategy :bin-width - :bin-width bin-width - :num-bins (calculate-num-bins min-value max-value bin-width)}) - (let [num-bins (public-settings/breakout-bins-num)] - {:strategy :num-bins - :num-bins num-bins - :bin-width (calculate-bin-width min-value max-value num-bins)}))) - -(defn- update-binned-field - "Given a field, resolve the binning strategy (either provided or found if default is specified) and calculate the - number of bins and bin width for this file. `filter-field-map` contains related criteria that could narrow the - domain for the field." - [{:keys [field num-bins strategy bin-width] :as binned-field} filter-field-map] - (let [[min-value max-value] (extract-bounds field filter-field-map)] - (when-not (and min-value max-value) - (throw (Exception. (format "Unable to bin field '%s' with id '%s' without a min/max value" - (get-in binned-field [:field :field-name]) - (get-in binned-field [:field :field-id]))))) - (let [resolved-binned-field (merge binned-field - {:min-value min-value :max-value max-value} - (case strategy - - :num-bins - {:bin-width (calculate-bin-width min-value max-value num-bins)} - - :bin-width - {:num-bins (calculate-num-bins min-value max-value bin-width)} - - :default - (resolve-default-strategy binned-field min-value max-value)))] - ;; Bail out and use unmodifed version if we can't converge on a - ;; nice version. - (or (nicer-breakout resolved-binned-field) resolved-binned-field)))) + [strategy :- mbql.s/BinningStrategyName + {:keys [min-value max-value bin-width num-bins]} :- mbql.s/ResolvedBinningStrategyOptions] + (let [bin-width (if (= strategy :num-bins) + (nicer-bin-width min-value max-value num-bins) + bin-width) + [min-value max-value] (nicer-bounds min-value max-value bin-width)] + {:min-value min-value + :max-value max-value + :num-bins (if (= strategy :num-bins) + num-bins + (calculate-num-bins min-value max-value bin-width)) + :bin-width bin-width})) + +(s/defn ^:private nicer-breakout :- (s/maybe mbql.s/ResolvedBinningStrategyOptions) + [strategy :- mbql.s/BinningStrategyName, opts :- mbql.s/ResolvedBinningStrategyOptions] + (let [f (partial nicer-breakout* strategy)] + ((fixed-point f) opts))) + + +;;; -------------------------------------------- Adding resolved options --------------------------------------------- + +(defn- resolve-options [strategy strategy-param metadata min-value max-value] + (case strategy + :num-bins + [:num-bins + {:num-bins strategy-param + :bin-width (calculate-bin-width min-value max-value strategy-param)}] + + :bin-width + [:bin-width + {:bin-width strategy-param + :num-bins (calculate-num-bins min-value max-value strategy-param)}] + + :default + (resolve-default-strategy metadata min-value max-value))) + +(s/defn ^:private update-binned-field :- mbql.s/binning-strategy + "Given a `binning-strategy` clause, resolve the binning strategy (either provided or found if default is specified) + and calculate the number of bins and bin width for this field. `field-id->filters` contains related criteria that + could narrow the domain for the field. This info is saved as part of each `binning-strategy` clause." + [query, field-id->filters :- FieldID->Filters, [_ field-clause strategy strategy-param] :- mbql.s/binning-strategy] + (let [field-id-or-name (mbql.u/field-clause->id-or-literal field-clause) + metadata (if (integer? field-id-or-name) + (qp.store/field field-id-or-name) + (some (fn [metadata] + (when (= (:name metadata) field-id-or-name) + metadata)) + (:source-metadata query))) + {:keys [min-value max-value] + :as min-max} (extract-bounds (when (integer? field-id-or-name) field-id-or-name) + (:fingerprint metadata) + field-id->filters) + [new-strategy resolved-options] (resolve-options strategy strategy-param metadata min-value max-value) + resolved-options (merge min-max resolved-options)] + ;; Bail out and use unmodifed version if we can't converge on a nice version. + [:binning-strategy field-clause new-strategy strategy-param (or (nicer-breakout new-strategy resolved-options) + resolved-options)])) + + +(defn- update-binning-strategy* [query] + (let [field-id->filters (filter->field-map (get-in query [:query :filter]))] + (mbql.u/replace-clauses-in query [:query] :binning-strategy (partial update-binned-field query field-id->filters)))) (defn update-binning-strategy "When a binned field is found, it might need to be updated if a relevant query criteria affects the min/max value of the binned field. This middleware looks for that criteria, then updates the related min/max values and calculates the bin-width based on the criteria values (or global min/max information)." [qp] - (fn [query] - (let [filter-field-map (filter->field-map (get-in query [:query :filter]))] - (qp - (qputil/postwalk-pred #(instance? BinnedField %) - #(update-binned-field % filter-field-map) - query))))) + (comp qp update-binning-strategy*)) diff --git a/src/metabase/query_processor/middleware/expand.clj b/src/metabase/query_processor/middleware/expand.clj index 91222d3d45d565ec7cfc7da5b34e633c26ed32f8..b7ba2be89ebc99d7c222d2fc8a09e8ff2e6ba64c 100644 --- a/src/metabase/query_processor/middleware/expand.clj +++ b/src/metabase/query_processor/middleware/expand.clj @@ -161,12 +161,12 @@ (s/defn ^:deprecated ^:private ag-with-field :- i/Aggregation [ag-type f] (i/map->AggregationWithField {:aggregation-type ag-type, :field (field-or-expression f)})) -(def ^:ql ^{:arglists '([f])} avg "Aggregation clause. Return the average value of F." (partial ag-with-field :avg)) -(def ^:ql ^{:arglists '([f])} distinct "Aggregation clause. Return the number of distinct values of F." (partial ag-with-field :distinct)) -(def ^:ql ^{:arglists '([f])} sum "Aggregation clause. Return the sum of the values of F." (partial ag-with-field :sum)) -(def ^:ql ^{:arglists '([f])} cum-sum "Aggregation clause. Return the cumulative sum of the values of F." (partial ag-with-field :cumulative-sum)) -(def ^:ql ^{:arglists '([f])} min "Aggregation clause. Return the minimum value of F." (partial ag-with-field :min)) -(def ^:ql ^{:arglists '([f])} max "Aggregation clause. Return the maximum value of F." (partial ag-with-field :max)) +(def ^:ql ^:deprecated ^{:arglists '([f])} avg "Aggregation clause. Return the average value of F." (partial ag-with-field :avg)) +(def ^:ql ^:deprecated ^{:arglists '([f])} distinct "Aggregation clause. Return the number of distinct values of F." (partial ag-with-field :distinct)) +(def ^:ql ^:deprecated ^{:arglists '([f])} sum "Aggregation clause. Return the sum of the values of F." (partial ag-with-field :sum)) +(def ^:ql ^:deprecated ^{:arglists '([f])} cum-sum "Aggregation clause. Return the cumulative sum of the values of F." (partial ag-with-field :cumulative-sum)) +(def ^:ql ^:deprecated ^{:arglists '([f])} min "Aggregation clause. Return the minimum value of F." (partial ag-with-field :min)) +(def ^:ql ^:deprecated ^{:arglists '([f])} max "Aggregation clause. Return the maximum value of F." (partial ag-with-field :max)) (defn ^:deprecated ^:ql stddev "Aggregation clause. Return the standard deviation of values of F. @@ -219,17 +219,17 @@ (s/defn ^:deprecated ^:ql binning-strategy :- (s/cond-pre FieldPlaceholder FieldLiteral) "Reference to a `BinnedField`. This is just a `Field` reference with an associated `STRATEGY-NAME` and `STRATEGY-PARAM`" - ([f strategy-name & [strategy-param]] + ([f strategy-name & [strategy-param resolved-options]] (let [strategy (qputil/normalize-token strategy-name) field (field f)] - (assoc field :binning-strategy strategy, :binning-param strategy-param)))) + (assoc field :binning-strategy strategy, :binning-param strategy-param, :binning-opts resolved-options)))) (defn- ^:deprecated fields-list-clause ([_ query] query) ([k query & fields] (assoc query k (mapv field fields)))) -(def ^:ql ^{:arglists '([query & fields])} breakout "Specify which fields to breakout by." (partial fields-list-clause :breakout)) -(def ^:ql ^{:arglists '([query & fields])} fields "Specify which fields to return." (partial fields-list-clause :fields)) +(def ^:ql ^:deprecated ^{:arglists '([query & fields])} breakout "Specify which fields to breakout by." (partial fields-list-clause :breakout)) +(def ^:ql ^:deprecated ^{:arglists '([query & fields])} fields "Specify which fields to return." (partial fields-list-clause :fields)) ;;; ## filter @@ -241,8 +241,8 @@ ([compound-type, subclause :- i/Filter, & more :- [i/Filter]] (i/map->CompoundFilter {:compound-type compound-type, :subclauses (vec (cons subclause more))}))) -(def ^:ql ^{:arglists '([& subclauses])} and "Filter subclause. Return results that satisfy *all* SUBCLAUSES." (partial compound-filter :and)) -(def ^:ql ^{:arglists '([& subclauses])} or "Filter subclause. Return results that satisfy *any* of the SUBCLAUSES." (partial compound-filter :or)) +(def ^:ql ^:deprecated ^{:arglists '([& subclauses])} and "Filter subclause. Return results that satisfy *all* SUBCLAUSES." (partial compound-filter :and)) +(def ^:ql ^:deprecated ^{:arglists '([& subclauses])} or "Filter subclause. Return results that satisfy *any* of the SUBCLAUSES." (partial compound-filter :or)) (s/defn ^:deprecated ^:private equality-filter :- i/Filter ([filter-type _ f v] @@ -251,7 +251,7 @@ (apply compound-fn (for [v (cons v more)] (equality-filter filter-type compound-fn f v))))) -(def ^:ql ^{:arglists '([f v & more])} = +(def ^:ql ^:deprecated ^{:arglists '([f v & more])} = "Filter subclause. With a single value, return results where F == V. With two or more values, return results where F matches *any* of the values (i.e.`IN`) @@ -259,7 +259,7 @@ (= f v1 v2) ; same as (or (= f v1) (= f v2))" (partial equality-filter := or)) -(def ^:ql ^{:arglists '([f v & more])} != +(def ^:ql ^:deprecated ^{:arglists '([f v & more])} != "Filter subclause. With a single value, return results where F != V. With two or more values, return results where F does not match *any* of the values (i.e. `NOT IN`) @@ -273,10 +273,10 @@ (s/defn ^:deprecated ^:private comparison-filter :- ComparisonFilter [filter-type f v] (i/map->ComparisonFilter {:filter-type filter-type, :field (field f), :value (value f v)})) -(def ^:ql ^{:arglists '([f v])} < "Filter subclause. Return results where F is less than V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :<)) -(def ^:ql ^{:arglists '([f v])} <= "Filter subclause. Return results where F is less than or equal to V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :<=)) -(def ^:ql ^{:arglists '([f v])} > "Filter subclause. Return results where F is greater than V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :>)) -(def ^:ql ^{:arglists '([f v])} >= "Filter subclause. Return results where F is greater than or equal to V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :>=)) +(def ^:ql ^:deprecated ^{:arglists '([f v])} < "Filter subclause. Return results where F is less than V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :<)) +(def ^:ql ^:deprecated ^{:arglists '([f v])} <= "Filter subclause. Return results where F is less than or equal to V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :<=)) +(def ^:ql ^:deprecated ^{:arglists '([f v])} > "Filter subclause. Return results where F is greater than V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :>)) +(def ^:ql ^:deprecated ^{:arglists '([f v])} >= "Filter subclause. Return results where F is greater than or equal to V. V must be orderable, i.e. a number or datetime." (partial comparison-filter :>=)) (s/defn ^:deprecated ^:ql between :- BetweenFilter "Filter subclause. Return results where F is between MIN and MAX. MIN and MAX must be orderable, i.e. numbers or datetimes. @@ -309,17 +309,17 @@ :value (value f s) :case-sensitive? (get options-map :case-sensitive true)}))) -(def ^:ql ^{:arglists '([f s] [f s options-map])} starts-with +(def ^:ql ^:deprecated ^{:arglists '([f s] [f s options-map])} starts-with "Filter subclause. Return results where F starts with the string S. By default, is case-sensitive, but you may pass an `options-map` with `{:case-sensitive false}` for case-insensitive searches." (partial string-filter :starts-with)) -(def ^:ql ^{:arglists '([f s] [f s options-map])} contains +(def ^:ql ^:deprecated ^{:arglists '([f s] [f s options-map])} contains "Filter subclause. Return results where F contains the string S. By default, is case-sensitive, but you may pass an `options-map` with `{:case-sensitive false}` for case-insensitive searches." (partial string-filter :contains)) -(def ^:ql ^{:arglists '([f s] [f s options-map])} ends-with +(def ^:ql ^:deprecated ^{:arglists '([f s] [f s options-map])} ends-with "Filter subclause. Return results where F ends with with the string S. By default, is case-sensitive, but you may pass an `options-map` with `{:case-sensitive false}` for case-insensitive searches." (partial string-filter :ends-with)) @@ -353,7 +353,7 @@ (> field max-val))) (i/strict-map->NotFilter {:compound-type :not, :subclause clause}))))) -(def ^:ql ^{:arglists '([f s]), :added "0.15.0"} does-not-contain +(def ^:ql ^:deprecated ^{:arglists '([f s]), :added "0.15.0"} does-not-contain "Filter subclause. Return results where F does not start with the string S." (comp not contains)) @@ -421,13 +421,13 @@ (update f :datetime-unit (fn [unit] (core/or unit :default)))))}) -(def ^:ql ^{:arglists '([field])} asc +(def ^:ql ^:deprecated ^{:arglists '([field])} asc "`order-by` subclause. Specify that results should be returned in ascending order for Field or AgRef F. (order-by {} (asc 100))" (partial order-by-subclause :ascending)) -(def ^:ql ^{:arglists '([field])} desc +(def ^:ql ^:deprecated ^{:arglists '([field])} desc "`order-by` subclause. Specify that results should be returned in ascending order for Field or AgRef F. (order-by {} (desc 100))" @@ -503,10 +503,10 @@ (float arg) ; convert args to floats so things like 5 / 10 -> 0.5 instead of 0 arg)))})) -(def ^:ql ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} + "Arithmetic addition function." (partial expression-fn :+)) -(def ^:ql ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} - "Arithmetic subtraction function." (partial expression-fn :-)) -(def ^:ql ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} * "Arithmetic multiplication function." (partial expression-fn :*)) -(def ^:ql ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} / "Arithmetic division function." (partial expression-fn :/)) +(def ^:ql ^:deprecated ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} + "Arithmetic addition function." (partial expression-fn :+)) +(def ^:ql ^:deprecated ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} - "Arithmetic subtraction function." (partial expression-fn :-)) +(def ^:ql ^:deprecated ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} * "Arithmetic multiplication function." (partial expression-fn :*)) +(def ^:ql ^:deprecated ^{:arglists '([rvalue1 rvalue2 & more]), :added "0.17.0"} / "Arithmetic division function." (partial expression-fn :/)) ;;; Metric & Segment handlers diff --git a/src/metabase/query_processor/middleware/fetch_source_query.clj b/src/metabase/query_processor/middleware/fetch_source_query.clj index 54f551ab999dde626240c747c56670ffa966d0c4..8009900b56eb0b2eedc1ad0808e23eaf25a2358b 100644 --- a/src/metabase/query_processor/middleware/fetch_source_query.clj +++ b/src/metabase/query_processor/middleware/fetch_source_query.clj @@ -5,9 +5,10 @@ [metabase.mbql.schema :as mbql.s] [metabase.query-processor.interface :as i] [metabase.util :as u] - [metabase.util.i18n :refer [trs]] + [metabase.util.i18n :refer [trs tru]] [schema.core :as s] - [toucan.db :as db])) + [toucan.db :as db] + [metabase.mbql.normalize :as normalize])) (defn- trim-query "Native queries can have trailing SQL comments. This works when executed directly, but when we use the query in a @@ -32,22 +33,22 @@ {:native (trim-query card-id (:query native))} (when (seq (:template-tags native)) {:template-tags (:template-tags native)}))) - (throw (Exception. (str "Missing source query in Card " card-id)))) + (throw (Exception. (str (tru "Missing source query in Card {0}" card-id))))) ;; include database ID as well; we'll pass that up the chain so it eventually gets put in its spot in the ;; outer-query :database (:database card-query) - :result_metadata (:result_metadata card)))) + :source-metadata (normalize/normalize-fragment [:source-metadata] (:result_metadata card))))) (defn- source-table-str->source-query - "Given a SOURCE-TABLE-STR like `card__100` return the appropriate source query." + "Given a `source-table-str` like `card__100` return the appropriate source query." [source-table-str] (let [[_ card-id-str] (re-find #"^card__(\d+)$" source-table-str)] (u/prog1 (card-id->source-query (Integer/parseInt card-id-str)) (when-not i/*disable-qp-logging* - (log/infof "\nFETCHED SOURCE QUERY FROM CARD %s:\n%s" - card-id-str - ;; No need to include result metadata here, it can be large and will clutter the logs - (u/pprint-to-str 'yellow (dissoc <> :result_metadata))))))) + (log/info (trs "Fetched source query from Card {0}:" card-id-str) + "\n" + ;; No need to include result metadata here, it can be large and will clutter the logs + (u/pprint-to-str 'yellow (dissoc <> :result_metadata))))))) (defn- expand-card-source-tables "If `source-table` is a Card reference (a string like `card__100`) then replace that with appropriate @@ -63,9 +64,9 @@ ;; Add new `source-query` info in its place. Pass the database ID up the chain, removing it from the ;; source query (assoc - :source-query (dissoc source-query :database :result_metadata) + :source-query (dissoc source-query :database :source-metadata) :database (:database source-query) - :result_metadata (:result_metadata source-query)))))) + :source-metadata (:source-metadata source-query)))))) (s/defn ^:private fetch-source-query* :- mbql.s/Query [{inner-query :query, :as outer-query} :- mbql.s/Query] @@ -75,11 +76,11 @@ ;; otherwise attempt to expand any source queries as needed (let [expanded-inner-query (expand-card-source-tables inner-query)] (merge outer-query - {:query (dissoc expanded-inner-query :database :result_metadata)} + {:query (dissoc expanded-inner-query :database :source-metadata)} (when-let [database (:database expanded-inner-query)] {:database database}) - (when-let [result-metadata (:result_metadata expanded-inner-query)] - {:result_metadata result-metadata}))))) + (when-let [source-metadata (:source-metadata expanded-inner-query)] + {:source-metadata source-metadata}))))) (defn fetch-source-query "Middleware that assocs the `:source-query` for this query if it was specified using the shorthand `:source-table` diff --git a/src/metabase/query_processor/middleware/resolve.clj b/src/metabase/query_processor/middleware/resolve.clj index 8035dda23f20f72c75283553f017ad7fb6db7bfb..994a5d65a3165d4f5b7a4b0951d9d4127dac8e2e 100644 --- a/src/metabase/query_processor/middleware/resolve.clj +++ b/src/metabase/query_processor/middleware/resolve.clj @@ -1,4 +1,4 @@ -(ns metabase.query-processor.middleware.resolve +(ns ^:deprecated metabase.query-processor.middleware.resolve "Resolve references to `Fields`, `Tables`, and `Databases` in an expanded query dictionary. During the `expand` phase of the Query Processor, forms like `[:field-id 10]` are replaced with placeholder objects of types like `FieldPlaceholder` or similar. During this phase, we'll take those placeholder objects and fetch information from @@ -163,21 +163,10 @@ ;;; ----------------------------------------------- FIELD PLACEHOLDER ------------------------------------------------ -(defn- resolve-binned-field [binning-strategy binning-param field] - (let [binned-field (i/map->BinnedField {:field field - :strategy binning-strategy})] - (case binning-strategy - :num-bins - (assoc binned-field :num-bins binning-param) - - :bin-width - (assoc binned-field :bin-width binning-param) - - :default - binned-field - - :else - (throw (Exception. (format "Unregonized binning strategy '%s'" binning-strategy)))))) +(defn- resolve-binned-field [binning-strategy binning-param binning-opts field] + (i/map->BinnedField (merge {:field field + :strategy binning-strategy} + binning-opts))) (defn- merge-non-nils "Like `clojure.core/merge` but only merges non-nil values" @@ -186,7 +175,7 @@ (defn- field-ph-resolve-field "Attempt to resolve the `Field` for a `FieldPlaceholder`. Return a resolved `Field` or `DateTimeField`." - [{:keys [field-id datetime-unit binning-strategy binning-param], :as this} field-id->field] + [{:keys [field-id datetime-unit binning-strategy binning-param binning-opts], :as this} field-id->field] (if-let [{:keys [base-type special-type], :as field} (some-> (field-id->field field-id) convert-db-field (merge-non-nils (select-keys this [:fk-field-id :remapped-from :remapped-to :field-display-name])))] @@ -202,7 +191,7 @@ (i/map->TimeField {:field field}) binning-strategy - (resolve-binned-field binning-strategy binning-param field) + (resolve-binned-field binning-strategy binning-param binning-opts field) :else field) ;; If that fails just return ourselves as-is @@ -441,12 +430,12 @@ (defn- resolve-field-literals "When resolving a field, we connect a `field-id` with a `Field` in our metadata tables. This is a similar process for `FieldLiteral`s, except we are attempting to connect a `FieldLiteral` with an associated entry in the - `result_metadata` attached to the query (typically from the `Card` of a nested query)." - [{:keys [result_metadata] :as expanded-query-dict}] - (let [name->fingerprint (zipmap (map :name result_metadata) - (map :fingerprint result_metadata))] + `source-metadata` attached to the query (typically from the `Card` of a nested query)." + [{:keys [source-metadata] :as expanded-query-dict}] + (let [name->fingerprint (zipmap (map :name source-metadata) + (map :fingerprint source-metadata))] (qputil/postwalk-pred #(instance? FieldLiteral %) - (fn [{:keys [binning-strategy binning-param] :as node}] + (fn [{:keys [binning-strategy binning-param binning-opts] :as node}] (let [fingerprint (get name->fingerprint (:field-name node)) node-with-print (assoc node :fingerprint fingerprint)] (cond @@ -455,7 +444,7 @@ (throw (Exception. "Binning not supported on a field literal with no fingerprint")) (and fingerprint binning-strategy) - (resolve-binned-field binning-strategy binning-param node-with-print) + (resolve-binned-field binning-strategy binning-param binning-opts node-with-print) :else node-with-print))) @@ -464,7 +453,7 @@ ;;; ------------------------------------------------ PUBLIC INTERFACE ------------------------------------------------ -(defn resolve-fields-if-needed +(defn ^:deprecated resolve-fields-if-needed "Resolves any unresolved fields found in `fields`. Will just return resolved fields with no changes." [fields] (let [fields-to-resolve (map unresolved-field-id fields)] @@ -473,7 +462,7 @@ (map #(resolve-field % field-id->field) fields) fields))) -(s/defn resolve :- su/Map +(s/defn ^:deprecated resolve :- su/Map "Resolve placeholders by fetching `Fields`, `Databases`, and `Tables` that are referred to in EXPANDED-QUERY-DICT." [expanded-query-dict :- su/Map] (some-> expanded-query-dict @@ -482,7 +471,7 @@ resolve-field-literals resolve-tables)) -(defn resolve-middleware +(defn ^:deprecated resolve-middleware "Wraps the `resolve` function in a query-processor middleware" [qp] (fn [{database-id :database, query-type :type, :as query}] diff --git a/src/metabase/query_processor/middleware/resolve_fields.clj b/src/metabase/query_processor/middleware/resolve_fields.clj new file mode 100644 index 0000000000000000000000000000000000000000..129c39e4a5b030db5fc78050f8b9d1415ef84642 --- /dev/null +++ b/src/metabase/query_processor/middleware/resolve_fields.clj @@ -0,0 +1,21 @@ +(ns metabase.query-processor.middleware.resolve-fields + "Middleware that resolves the Fields referenced by a query." + (:require [metabase.mbql.util :as mbql.u] + [metabase.models.field :refer [Field]] + [metabase.query-processor.store :as qp.store] + [metabase.util :as u] + [toucan.db :as db])) + +(defn- resolve-fields* [{mbql-inner-query :query, :as query}] + (u/prog1 query + (when-let [field-ids (seq (map second (mbql.u/clause-instances :field-id mbql-inner-query)))] + ;; Just fetch the entire object for right now. We can pare this down at a later date + ;; TODO - figure out which Fields we need and only fetch those + (doseq [field (db/select Field :id [:in (set field-ids)])] + (qp.store/store-field! field))))) + +(defn resolve-fields + "Fetch the Fields referenced by `:field-id` clauses in a query and store them in the Query Processor Store for the + duration of the Query Execution." + [qp] + (comp qp resolve-fields*)) diff --git a/src/metabase/query_processor/middleware/source_table.clj b/src/metabase/query_processor/middleware/source_table.clj index dfd77808c5fe0fb58f691810f97ea3402465bc99..76908209eaa62c76fb75a6b0433ea8649fa53aca 100644 --- a/src/metabase/query_processor/middleware/source_table.clj +++ b/src/metabase/query_processor/middleware/source_table.clj @@ -8,7 +8,8 @@ (defn- resolve-source-table [query] (when-let [source-table-id (mbql.u/query->source-table-id query)] (let [source-table (or (db/select-one [Table :schema :name :id], :id source-table-id) - (throw (Exception. (str (trs "Cannot run query: could not find source table {0}." source-table-id)))))] + (throw (Exception. (str (trs "Cannot run query: could not find source table {0}." + source-table-id)))))] (qp.store/store-table! source-table))) query) diff --git a/src/metabase/query_processor/store.clj b/src/metabase/query_processor/store.clj index 2899160499510d4684129c79c2a086f815025f01..758db871040b44110d5d18b8012dc6c070fbb715 100644 --- a/src/metabase/query_processor/store.clj +++ b/src/metabase/query_processor/store.clj @@ -12,12 +12,12 @@ Of course, it would be entirely possible to call `(Field 10)` every time you needed information about that Field, but fetching all Fields in a single pass and storing them for reuse is dramatically more efficient than fetching those Fields potentially dozens of times in a single query execution." - (:require [schema.core :as s] - [metabase.models + (:require [metabase.models [field :refer [Field]] [table :refer [Table]]] [metabase.util :as u] - [metabase.util.schema :as su])) + [metabase.util.schema :as su] + [schema.core :as s])) ;;; ---------------------------------------------- Setting up the Store ---------------------------------------------- @@ -41,8 +41,6 @@ ;; TODO - DATABASE ?? (def ^:private TableInstanceWithRequiredStoreKeys - "Schema for Tables stored in the QP store (must be a `TableInstance` with at least the `:id`, `:schema`, and `:name` - keys)." (s/both (class Table) {:id su/IntGreaterThanZero ; TODO - what's the point of storing ID if it's already the key? @@ -50,6 +48,18 @@ :name su/NonBlankString s/Any s/Any})) +(def ^:private FieldInstanceWithRequiredStorekeys + (s/both + (class Field) + {:id su/IntGreaterThanZero + :name su/NonBlankString + :display_name su/NonBlankString + :description (s/maybe s/Str) + :base_type su/FieldType + :special_type (s/maybe su/FieldType) + :fingerprint (s/maybe su/Map) + s/Any s/Any})) + ;;; ------------------------------------------ Saving objects in the Store ------------------------------------------- @@ -62,15 +72,9 @@ (s/defn store-field! "Store a `field` in the QP Store for the duration of the current query execution. Throws an Exception if Field is invalid or doesn't have all required keys." - [field :- (class Field)] + [field :- FieldInstanceWithRequiredStorekeys] (swap! *store* assoc-in [:fields (u/get-id field)] field)) -(s/defn store-field-literal! - "Like `store-field!`, but designed for storing Fields referred to by `:field-literal` clauses, i.e. by name instead of - by ID." - [field-literal :- su/NonBlankString, field :- (class Field)] - (swap! *store* assoc-in [:field-literals field-literal] field)) - ;;; ---------------------------------------- Fetching objects from the Store ----------------------------------------- @@ -79,12 +83,7 @@ [table-id :- su/IntGreaterThanZero] (get-in @*store* [:tables table-id])) -(s/defn field :- (class Field) +(s/defn field :- FieldInstanceWithRequiredStorekeys "Fetch Field with `field-id` from the QP Store. Throws an Exception if valid item is not returned." [field-id :- su/IntGreaterThanZero] (get-in @*store* [:fields field-id])) - -(s/defn field-liteal :- (class Field) - "Fetch Field named by `field-literal` from the QP Store. Throws an Exception if valid item is not returned." - [field-literal :- su/NonBlankString] - (get-in @*store* [:field-literals field-literal])) diff --git a/test/metabase/mbql/normalize_test.clj b/test/metabase/mbql/normalize_test.clj index 6b7a9cacc958b6247f8d05210096173242cc6c23..9a648a1b41c45fca1aca688fc41d24ef3eac1033 100644 --- a/test/metabase/mbql/normalize_test.clj +++ b/test/metabase/mbql/normalize_test.clj @@ -748,3 +748,27 @@ :query {:source-table 1} :parameters [{:type "id", :target ["dimension" ["fk->" 3265 4575]], :value ["field-id"]} {:type "date/all-options", :target ["dimension" ["field-id" 3270]], :value "thismonth"}]})) + +;; make sure source-metadata gets normalized the way we'd expect (just the type names in this case) +(expect + {:source-metadata + [{:name "name" + :display_name "Name" + :base_type :type/Text + :special_type :type/Name + :fingerprint {:global {:distinct-count 100} + :type {:type/Text {:percent-json 0.0 + :percent-url 0.0 + :percent-email 0.0 + :average-length 15.63}}}}]} + (normalize/normalize + {:source-metadata [{:name "name" + :display_name "Name" + :description nil + :base_type "type/Text" + :special_type "type/Name" + :fingerprint {"global" {"distinct-count" 100} + "type" {"type/Text" {"percent-json" 0.0 + "percent-url" 0.0 + "percent-email" 0.0 + "average-length" 15.63}}}}]})) diff --git a/test/metabase/query_processor/middleware/binning_test.clj b/test/metabase/query_processor/middleware/binning_test.clj index 9447b83a6a6676eb404b0564bf269dfacd1a07f4..0b10ef19668ef6d293056c1397eda3e09af8270f 100644 --- a/test/metabase/query_processor/middleware/binning_test.clj +++ b/test/metabase/query_processor/middleware/binning_test.clj @@ -1,25 +1,28 @@ (ns metabase.query-processor.middleware.binning-test (:require [expectations :refer [expect]] - [metabase.query-processor.middleware - [binning :as binning] - [expand :as ql]])) + [metabase.models.field :as field :refer [Field]] + [metabase.query-processor.middleware.binning :as binning] + [metabase.query-processor.store :as qp.store] + [metabase.test.data :as data] + [metabase.util :as u] + [toucan.util.test :as tt])) (expect {} - (#'binning/filter->field-map (ql/and - (ql/= (ql/field-id 1) 10) - (ql/= (ql/field-id 2) 10)))) + (#'binning/filter->field-map [:and + [:= [:field-id 1] 10] + [:= [: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)]} - (#'binning/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)))) + {1 [[:< [:field-id 1] 10] [:> [:field-id 1] 1]] + 2 [[:> [:field-id 2] 20] [:< [:field-id 2] 10]] + 3 [[:between [:field-id 3] 5 10]]} + (#'binning/filter->field-map [:and + [:< [:field-id 1] 10] + [:> [:field-id 1] 1] + [:> [:field-id 2] 20] + [:< [:field-id 2] 10] + [:between [:field-id 3] 5 10]])) (expect [[1.0 1.0 1.0] @@ -34,48 +37,95 @@ [(#'binning/nicer-bin-width 27 135 8) (#'binning/nicer-bin-width -0.0002 10000.34 8)]) -(def ^:private test-min-max-field - {:field-id 1 :fingerprint {:type {:type/Number {:min 100 :max 1000}}}}) +(def ^:private test-min-max-fingerprint + {:type {:type/Number {:min 100 :max 1000}}}) (expect - [1 10] - (#'binning/extract-bounds test-min-max-field - {1 [(ql/> (ql/field-id 1) 1) (ql/< (ql/field-id 1) 10)]})) + {:min-value 1, :max-value 10} + (#'binning/extract-bounds 1 + test-min-max-fingerprint + {1 [[:> [:field-id 1] 1] [:< [:field-id 1] 10]]})) (expect - [1 10] - (#'binning/extract-bounds test-min-max-field - {1 [(ql/between (ql/field-id 1) 1 10)]})) + {:min-value 1, :max-value 10} + (#'binning/extract-bounds 1 + test-min-max-fingerprint + {1 [[:between [:field-id 1] 1 10]]})) (expect - [100 1000] - (#'binning/extract-bounds test-min-max-field + {:min-value 100, :max-value 1000} + (#'binning/extract-bounds 1 + test-min-max-fingerprint {})) (expect - [500 1000] - (#'binning/extract-bounds test-min-max-field - {1 [(ql/> (ql/field-id 1) 500)]})) + {:min-value 500, :max-value 1000} + (#'binning/extract-bounds 1 + test-min-max-fingerprint + {1 [[:> [:field-id 1] 500]]})) (expect - [100 500] - (#'binning/extract-bounds test-min-max-field - {1 [(ql/< (ql/field-id 1) 500)]})) + {:min-value 100, :max-value 500} + (#'binning/extract-bounds 1 + test-min-max-fingerprint + {1 [[:< [:field-id 1] 500]]})) (expect - [600 700] - (#'binning/extract-bounds test-min-max-field - {1 [(ql/> (ql/field-id 1) 200) - (ql/< (ql/field-id 1) 800) - (ql/between (ql/field-id 1) 600 700)]})) + {:min-value 600, :max-value 700} + (#'binning/extract-bounds 1 + test-min-max-fingerprint + {1 [[:> [:field-id 1] 200] + [:< [:field-id 1] 800] + [:between [:field-id 1] 600 700]]})) (expect - [[ 0.0 1000.0 125.0 8] - [200N 1600N 200 8] - [ 0.0 1200.0 200 8] - [ 0.0 1005.0 15.0 67]] - (for [breakout [{:field-id 1, :min-value 100, :max-value 1000, :strategy :num-bins, :num-bins 8} - {:field-id 1, :min-value 200, :max-value 1600, :strategy :num-bins, :num-bins 8} - {:field-id 1, :min-value 9, :max-value 1002, :strategy :num-bins, :num-bins 8} - {:field-id 1, :min-value 9, :max-value 1002, :strategy :bin-width, :bin-width 15.0}]] - ((juxt :min-value :max-value :bin-width :num-bins) (binning/nicer-breakout breakout)))) + [{:min-value 0.0, :max-value 1000.0, :num-bins 8, :bin-width 125.0} + {:min-value 200N, :max-value 1600N, :num-bins 8, :bin-width 200} + {:min-value 0.0, :max-value 1200.0, :num-bins 8, :bin-width 200} + {:min-value 0.0, :max-value 1005.0, :num-bins 67, :bin-width 15.0}] + (for [[strategy opts] [[:num-bins {:min-value 100, :max-value 1000, :num-bins 8, :bin-width 0}] + [:num-bins {:min-value 200, :max-value 1600, :num-bins 8, :bin-width 0}] + [:num-bins {:min-value 9, :max-value 1002, :num-bins 8, :bin-width 0}] + [:bin-width {:min-value 9, :max-value 1002, :num-bins 1, :bin-width 15.0}]]] + (#'binning/nicer-breakout strategy opts))) + +;; does `resolve-default-strategy` work the way we'd expect? +(expect + [:num-bins {:num-bins 8, :bin-width 28.28321}] + (#'binning/resolve-default-strategy {:special_type :type/Income} 12.061602936923117 238.32732001721533)) + +;; does `nicer-breakout` make things NICER? +(expect + {:min-value 0.0, :max-value 240.0, :num-bins 8, :bin-width 30} + (#'binning/nicer-breakout :num-bins {:min-value 12.061602936923117 + :max-value 238.32732001721533 + :bin-width 28.28321 + :num-bins 8})) + +;; Try an end-to-end test of the middleware +(tt/expect-with-temp [Field [field (field/map->FieldInstance + {:database_type "DOUBLE" + :table_id (data/id :checkins) + :special_type :type/Income + :name "TOTAL" + :display_name "Total" + :fingerprint {:global {:distinct-count 10000} + :type {:type/Number {:min 12.061602936923117 + :max 238.32732001721533 + :avg 82.96014815230829}}} + :base_type :type/Float})]] + {:query {:source-table (data/id :checkins) + :breakout [[:binning-strategy + [:field-id (u/get-id field)] + :num-bins + nil + {:min-value 0.0, :max-value 240.0, :num-bins 8, :bin-width 30}]]} + :type :query + :database (data/id)} + (qp.store/with-store + (qp.store/store-field! field) + ((binning/update-binning-strategy identity) + {:query {:source-table (data/id :checkins) + :breakout [[:binning-strategy [:field-id (u/get-id field)] :default]]} + :type :query + :database (data/id)}))) diff --git a/test/metabase/query_processor/middleware/fetch_source_query_test.clj b/test/metabase/query_processor/middleware/fetch_source_query_test.clj index be66bbe7a5afa73d0c927170f0d7543c21d6623c..dd550ef05c6166dbf21e792c6f9e5d5193d20824 100644 --- a/test/metabase/query_processor/middleware/fetch_source_query_test.clj +++ b/test/metabase/query_processor/middleware/fetch_source_query_test.clj @@ -75,7 +75,12 @@ :query {:source-table (str "card__" (u/get-id card))}}))) (expect - (let [date-field-literal {:field-name "date", :base-type :type/Date, :binning-strategy nil, :binning-param nil, :fingerprint nil}] + (let [date-field-literal {:field-name "date" + :base-type :type/Date + :binning-strategy nil + :binning-param nil + :binning-opts nil + :fingerprint nil}] (default-expanded-results {:source-query {:source-table (data/id :checkins) :join-tables nil} diff --git a/test/metabase/query_processor_test/breakout_test.clj b/test/metabase/query_processor_test/breakout_test.clj index cd7fd767ae9bd9d9ceb0af728e932466a9872dc7..fd027c8646af935e5a1d118b6a6a98b95ed9a48f 100644 --- a/test/metabase/query_processor_test/breakout_test.clj +++ b/test/metabase/query_processor_test/breakout_test.clj @@ -14,6 +14,7 @@ [metabase.test [data :as data] [util :as tu]] + [metabase.util.i18n :refer [tru]] [metabase.test.data [dataset-definitions :as defs] [datasets :as datasets]] @@ -217,8 +218,8 @@ :num_bins 4, :min_value 10.0 :max_value 50.0}) (-> (data/run-mbql-query venues - {:aggregation [[:count]] - :breakout [[:binning-strategy $latitude :default]]}) + {:aggregation [[:count]] + :breakout [[:binning-strategy $latitude :default]]}) tu/round-fingerprint-cols (get-in [:data :cols]) first)) @@ -229,8 +230,8 @@ :num_bins 5, :min_value 7.5, :max_value 45.0}) (-> (data/run-mbql-query venues - {:aggregation [[:count]] - :breakout [[:binning-strategy $latitude :num-bins 5]]}) + {:aggregation [[:count]] + :breakout [[:binning-strategy $latitude :num-bins 5]]}) tu/round-fingerprint-cols (get-in [:data :cols]) first)) @@ -239,13 +240,11 @@ (datasets/expect-with-engines (non-timeseries-engines-with-feature :binning) {:status :failed :class Exception - :error (format "Unable to bin field '%s' with id '%s' without a min/max value" - (:name (Field (data/id :venues :latitude))) - (data/id :venues :latitude))} + :error "Unable to bin Field without a min/max value"} (tu/with-temp-vals-in-db Field (data/id :venues :latitude) {:fingerprint {:type {:type/Number {:min nil, :max nil}}}} (-> (data/run-mbql-query venues - {:aggregation [[:count]] - :breakout [[:binning-strategy $latitude :default]]}) + {:aggregation [[:count]] + :breakout [[:binning-strategy $latitude :default]]}) (select-keys [:status :class :error])))) (defn- field->result-metadata [field] @@ -253,10 +252,10 @@ (defn- nested-venues-query [card-or-card-id] {:database metabase.models.database/virtual-id - :type :query - :query {:source-table (str "card__" (u/get-id card-or-card-id)) - :aggregation [:count] - :breakout [[:binning-strategy [:field-literal (data/format-name :latitude) :type/Float] :num-bins 20]]}}) + :type :query + :query {:source-table (str "card__" (u/get-id card-or-card-id)) + :aggregation [:count] + :breakout [[:binning-strategy [:field-literal (data/format-name :latitude) :type/Float] :num-bins 20]]}}) ;; Binning should be allowed on nested queries that have result metadata (datasets/expect-with-engines (non-timeseries-engines-with-feature :binning :nested-queries)