Skip to content

Commit

Permalink
[QP] Support :temporal-unit parameters on MBQL queries
Browse files Browse the repository at this point in the history
This is a new parameter `:type` (and `:widget-type`) to choose a time
granularity (a `:temporal-unit`, eg. `month`, `day`, `day-of-week`)
which should replace the unit on the target field for the parameter.

The target field should be a breakout with a temporal type (date,
datetime) that's compatible with the input unit. The value of the
parameter should be a string or keyword naming one of the units:
`"month"`, `:month`.
  • Loading branch information
bshepherdson committed May 1, 2024
1 parent d142da9 commit c4416d5
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/metabase/lib/schema/parameter.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@
;; any date option above.
:date/all-options {:type :date, :allowed-for #{:date/all-options}}

;; `:temporal-unit` is a specialized type of parameter, and specialized widget. In MBQL queries, it maps only to
;; breakout columns which have temporal bucketing set, and overrides the unit from the query.
;; The value for this type of parameter is one of the temporal units from [[metabase.lib.schema.temporal-bucketing]].
;; TODO: Document how this works for native queries.
:temporal-unit {:allowed-for #{:temporal-unit}}

;; "operator" parameter types.
:number/!= {:type :numeric, :operator :variadic, :allowed-for #{:number/!=}}
:number/<= {:type :numeric, :operator :unary, :allowed-for #{:number/<=}}
Expand Down
3 changes: 2 additions & 1 deletion src/metabase/query_processor/middleware/parameters.clj
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@
`:template-tags` and removes those keys, splicing appropriate conditions into the queries they affect.
A SQL query with a param like `{{param}}` will have that part of the query replaced with an appropriate snippet as
well as any prepared statement args needed. MBQL queries will have additional filter clauses added."
well as any prepared statement args needed. MBQL queries will have additional filter clauses added. (Or in a special
case, the temporal bucketing on a breakout altered by a `:temporal-unit` parameter.)"
[query]
(-> query
hoist-database-for-snippet-tags
Expand Down
14 changes: 14 additions & 0 deletions src/metabase/query_processor/middleware/parameters/mbql.clj
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@
(mbql.u/wrap-field-id-if-needed field)
(parse-param-value-for-type query param-type param-value (mbql.u/unwrap-field-or-expression-clause field))]))

(defn- update-breakout-unit
[query
{[_dimension [_field target-field-id {:keys [temporal-unit]}]] :target
:keys [value] :as _param}]
(let [new-unit (keyword value)]
(lib.util.match/replace-in
query [:query :breakout]
[:field (_ :guard #(= target-field-id %)) (opts :guard #(= temporal-unit (:temporal-unit %)))]
[:field target-field-id (assoc opts :temporal-unit new-unit)])))

(defn expand
"Expand parameters for MBQL queries in `query` (replacing Dashboard or Card-supplied params with the appropriate
values in the queries themselves)."
Expand All @@ -101,6 +111,10 @@
(not param-value))
(recur query rest)

(= (:type param) :temporal-unit)
(let [query (update-breakout-unit query (assoc param :value param-value))]
(recur query rest))

:else
(let [filter-clause (build-filter-clause query (assoc param :value param-value))
query (mbql.u/add-filter-clause query filter-clause)]
Expand Down
28 changes: 28 additions & 0 deletions test/metabase/query_processor_test/date_bucketing_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,34 @@
(mt/first-row
(qp/process-query query))))))))))

(deftest temporal-unit-parameters-test
(mt/dataset test-data
(let [query-months (mt/query orders
{:type :query
:query {:source-table $$orders
:aggregation [[:sum $subtotal]]
:breakout [!month.created_at]
:filter [:between $created_at "2018-01-01T00:00:00Z" "2018-12-31T23:59:59Z"]}
;; The value for this parameter gets overridden below.
:parameters [{:type :temporal-unit
:target [:dimension !month.created_at]
:value "month"}]})
unit-totals (fn [unit]
(->> (qp/process-query (assoc-in query-months [:parameters 0 :value] unit))
(mt/formatted-rows [identity 2.0])
(map second)))]
(testing "monthly"
(is (= [37019.52 32923.82 36592.60 35548.11 43556.61 39537.82
42292.10 42443.71 42077.35 45708.74 44498.55 46245.48]
(unit-totals "month"))))

(testing "quarterly"
(is (= [106535.94 118642.54 126813.16 136452.77]
(unit-totals "quarter"))))

(testing "annual"
(is (= [488444.41] (unit-totals "year")))))))

(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)"
Expand Down

0 comments on commit c4416d5

Please sign in to comment.