diff --git a/src/metabase/query_processor/middleware/catch_exceptions.clj b/src/metabase/query_processor/middleware/catch_exceptions.clj index 086a23f28e34254b5003e1933cefa07998ce7d13..e8e5eda82d605f3625c605f2adfb1660a65954ab 100644 --- a/src/metabase/query_processor/middleware/catch_exceptions.clj +++ b/src/metabase/query_processor/middleware/catch_exceptions.clj @@ -34,11 +34,15 @@ (defmethod format-exception ExceptionInfo [e] - (let [{error-type :type, :as data} (ex-data e)] + ;; `:is-curated` is a flag that signals whether the error message in `e` was approved by product + ;; to be shown to the user. It is used by FE. + (let [{error-type :type, is-curated :is-curated, :as data} (ex-data e)] (merge ((get-method format-exception Throwable) e) (when (qp.error-type/known-error-type? error-type) {:error_type error-type}) + (when is-curated + {:error_is_curated is-curated}) ;; TODO - we should probably change this key to `:data` so we're not mixing lisp-case and snake_case keys {:ex-data data}))) diff --git a/src/metabase/query_processor/middleware/parameters/mbql.clj b/src/metabase/query_processor/middleware/parameters/mbql.clj index b75c16b5c2112207a2bb591170177724df0310ab..4d17a4d0d9a77a3d7e925a6b4f3b4eb52009524a 100644 --- a/src/metabase/query_processor/middleware/parameters/mbql.clj +++ b/src/metabase/query_processor/middleware/parameters/mbql.clj @@ -7,9 +7,13 @@ [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.convert :as lib.convert] [metabase.lib.core :as lib] + [metabase.lib.metadata :as lib.metadata] [metabase.lib.metadata.protocols :as lib.metadata.protocols] [metabase.lib.util.match :as lib.util.match] + [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.store :as qp.store] + [metabase.query-processor.util.temporal-bucket :as qp.u.temporal-bucket] + [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu])) (set! *warn-on-reflection* true) @@ -90,9 +94,19 @@ (defn- update-breakout-unit [query - {[_dimension [_field target-field-id {:keys [temporal-unit]}]] :target + {[_dimension [_field target-field-id {:keys [base-type temporal-unit]}]] :target :keys [value] :as _param}] - (let [new-unit (keyword value)] + (let [new-unit (keyword value) + base-type (or base-type + (when (integer? target-field-id) + (:base-type (lib.metadata/field (qp.store/metadata-provider) target-field-id))))] + (assert (some? base-type) "`base-type` is not set.") + (when-not (qp.u.temporal-bucket/compatible-temporal-unit? base-type new-unit) + (throw (ex-info (tru "This chart can not be broken out by the selected unit of time: {0}." value) + {:type qp.error-type/invalid-query + :is-curated true + :base-type base-type + :unit new-unit}))) (lib.util.match/replace-in query [:query :breakout] [:field (_ :guard #(= target-field-id %)) (opts :guard #(= temporal-unit (:temporal-unit %)))] diff --git a/src/metabase/query_processor/middleware/validate_temporal_bucketing.clj b/src/metabase/query_processor/middleware/validate_temporal_bucketing.clj index f480c540576224bd7f94b6ea5f50b71c81d562c9..5c4c66d3c1374797dc5275447ef330c9401fea9f 100644 --- a/src/metabase/query_processor/middleware/validate_temporal_bucketing.clj +++ b/src/metabase/query_processor/middleware/validate_temporal_bucketing.clj @@ -1,33 +1,12 @@ (ns metabase.query-processor.middleware.validate-temporal-bucketing (:require - [clojure.set :as set] [metabase.lib.metadata :as lib.metadata] [metabase.lib.util.match :as lib.util.match] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.store :as qp.store] + [metabase.query-processor.util.temporal-bucket :as qp.u.temporal-bucket] [metabase.util.i18n :refer [tru]])) -(def ^:private valid-date-units - #{:default :day :day-of-week :day-of-month :day-of-year - :week :week-of-year :month :month-of-year :quarter :quarter-of-year :year}) - -(def ^:private valid-time-units - #{:default :millisecond :second :minute :minute-of-hour :hour :hour-of-day}) - -(def ^:private valid-datetime-units (set/union valid-date-units valid-time-units)) - -;; TODO -- this should be changed to `:effective-type` once we finish the metadata changes. -(defmulti ^:private valid-units-for-base-type - {:arglists '([base-type])} - keyword) - -;; for stuff like UNIX timestamps -- skip validation for now. (UNIX timestamp should be bucketable with any unit -;; anyway). Once `:effective-type` is in place, we can actually check those Fields here. -(defmethod valid-units-for-base-type :type/* [_] valid-datetime-units) -(defmethod valid-units-for-base-type :type/Date [_] valid-date-units) -(defmethod valid-units-for-base-type :type/Time [_] valid-time-units) -(defmethod valid-units-for-base-type :type/DateTime [_] valid-datetime-units) - (defn validate-temporal-bucketing "Make sure temporal bucketing of Fields (i.e., `:datetime-field` clauses) in this query is valid given the combination of Field base-type and unit. For example, you should not be allowed to bucket a `:type/Date` Field by `:minute`." @@ -36,7 +15,7 @@ (let [base-type (if (integer? id-or-name) (:base-type (lib.metadata/field (qp.store/metadata-provider) id-or-name)) base-type) - valid-units (valid-units-for-base-type base-type)] + valid-units (qp.u.temporal-bucket/valid-units-for-base-type base-type)] (when-not (valid-units temporal-unit) (throw (ex-info (tru "Unsupported temporal bucketing: You can''t bucket a {0} Field by {1}." base-type temporal-unit) diff --git a/src/metabase/query_processor/util/temporal_bucket.clj b/src/metabase/query_processor/util/temporal_bucket.clj new file mode 100644 index 0000000000000000000000000000000000000000..68b5042ba751a92b6fa8b29a5992a70ca9bafe6e --- /dev/null +++ b/src/metabase/query_processor/util/temporal_bucket.clj @@ -0,0 +1,27 @@ +(ns metabase.query-processor.util.temporal-bucket + (:require + [metabase.lib.schema.temporal-bucketing :as lib.schema.temporal-bucketing])) + +(def ^:private valid-date-units (into #{:default} lib.schema.temporal-bucketing/date-bucketing-units)) +(def ^:private valid-time-units (into #{:default} lib.schema.temporal-bucketing/time-bucketing-units)) +(def ^:private valid-datetime-units lib.schema.temporal-bucketing/temporal-bucketing-units) + +;; TODO -- this should be changed to `:effective-type` once we finish the metadata changes. +(defmulti valid-units-for-base-type + "Returns valid temrpoal units for the `base-type`." + {:arglists '([base-type])} + keyword) + +;; for stuff like UNIX timestamps -- skip validation for now. (UNIX timestamp should be bucketable with any unit +;; anyway). Once `:effective-type` is in place, we can actually check those Fields here. +(defmethod valid-units-for-base-type :type/* [_] valid-datetime-units) +(defmethod valid-units-for-base-type :type/Date [_] valid-date-units) +(defmethod valid-units-for-base-type :type/Time [_] valid-time-units) +(defmethod valid-units-for-base-type :type/DateTime [_] valid-datetime-units) + +(defn compatible-temporal-unit? + "Check whether some column of `a-type` can be bucketted by the`temporal-unit`. Any column can be bucketed by `nil` + temporal unit." + [a-type temporal-unit] + (or (nil? temporal-unit) + (contains? (valid-units-for-base-type a-type) temporal-unit))) diff --git a/test/metabase/query_processor/middleware/catch_exceptions_test.clj b/test/metabase/query_processor/middleware/catch_exceptions_test.clj index 13d5a9cbbf295fc0eb4bfab3ebae2926b6a8c75e..2bf40a704527ff503da56c373de92ee14d8e5c59 100644 --- a/test/metabase/query_processor/middleware/catch_exceptions_test.clj +++ b/test/metabase/query_processor/middleware/catch_exceptions_test.clj @@ -28,7 +28,7 @@ (testing "Should nicely format a chain of exceptions, with the top-level Exception appearing first" (testing "lowest-level error `:type` should be pulled up to the top-level" (let [e1 (ex-info "1" {:level 1}) - e2 (ex-info "2" {:level 2, :type qp.error-type/qp} e1) + e2 (ex-info "2" {:level 2, :type qp.error-type/qp :is-curated true} e1) e3 (ex-info "3" {:level 3} e2)] (is (= {:status :failed :class clojure.lang.ExceptionInfo @@ -36,12 +36,13 @@ :stacktrace true :error_type :qp :ex-data {:level 1} - :via [{:status :failed - :class clojure.lang.ExceptionInfo - :error "2" - :stacktrace true - :ex-data {:level 2, :type :qp} - :error_type :qp} + :via [{:status :failed + :class clojure.lang.ExceptionInfo + :error "2" + :stacktrace true + :ex-data {:level 2, :type :qp, :is-curated true} + :error_type :qp + :error_is_curated true} {:status :failed :class clojure.lang.ExceptionInfo :error "3" diff --git a/test/metabase/query_processor_test/date_bucketing_test.clj b/test/metabase/query_processor_test/date_bucketing_test.clj index d89b44514d76464f0b1cf9e38f889363805ced39..1f0ed84ff0b83e63da4836b2dca57a8332d55253 100644 --- a/test/metabase/query_processor_test/date_bucketing_test.clj +++ b/test/metabase/query_processor_test/date_bucketing_test.clj @@ -33,6 +33,7 @@ [metabase.query-processor :as qp] [metabase.query-processor.compile :as qp.compile] [metabase.query-processor.middleware.format-rows :as format-rows] + [metabase.query-processor.preprocess :as qp.preprocess] [metabase.query-processor.test-util :as qp.test-util] [metabase.test :as mt] [metabase.test.data.interface :as tx] @@ -1659,6 +1660,20 @@ (testing "annual" (is (= [488444.41] (unit-totals "year"))))))) +(deftest ^:parallel incompatible-temporal-unit-parameter-test + (testing "Incompatible time unit parameter yields expected error" + (is (thrown-with-msg? + clojure.lang.ExceptionInfo #"This chart can not be broken out by the selected unit of time: minute\." + (qp.preprocess/preprocess + (mt/mbql-query + checkins + {:type :query + :query {:aggregation [[:count]] + :breakout [!day.date]} + :parameters [{:type :temporal-unit + :target [:dimension !day.date] + :value "minute"}]})))))) + (deftest day-of-week-custom-start-of-week-test (mt/test-drivers (mt/normal-drivers) (testing "`:day-of-week` bucketing should respect the `start-of-week` Setting (#13604)"