Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QP] Support :temporal-unit parameters on MBQL queries #42116

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 %)))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it essential to check for the expected temporal-unit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, not really. Soon we plan to allow multiple breakouts for the same column with different units, and this would become a latent bug.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be a better world where parameters were attached to specific occurrences of a column (via uuid)?

[: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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"created" {:type :date
:display-name "Genesis"
:name "created"}
;; Time granularity
"time_unit" {:type :temporal-unit
:display-name "Unit"
:name "time_unit"}
;; Field Filters: Dates
"created_range" {:type :dimension,
:name "created_range"
Expand Down Expand Up @@ -207,6 +211,8 @@
:id
:category
:boolean
;; no valid default for temporal-unit
:temporal-unit
;; no longer in use
:location/city
:location/state
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