Skip to content
This repository was archived by the owner on May 30, 2025. It is now read-only.

Commit 538e5e3

Browse files
authored
Optimize relative datetime filters (metabase#14835)
1. Rename optimize-datetime-filters middleware -> optimize-temporal-filters (it's more accurate, because this also optimizes date or time filter clauses) 2. optimize-temporal-filters middleware now optimizes relative-datetime clauses (which represent a moment in time relative to when the query is ran, e.g. "last month") in addition to absolute-datetime clauses (which represent an absolute moment in time, e.g. 2021-02-15T14:40:00-08:00) . This middleware rewrites queries so we filter against specific temporal ranges without casting the column itself, meaning we can leverage indexes on that column. See metabase#11837 for more details 3. Added new validate-temporal-bucketing middleware that throws an Exception if you try to do something that makes no sense, e.g. bucket a DATE field by :time or a TIME field by :month. This is a better situation then running the query and waiting for the DB to complain. (In practice, I don't think the FE client would let you generate a query like this in the first place) 4. Fix random test failures for MySQL in task-history-cleanup-test
1 parent b4d8e35 commit 538e5e3

File tree

29 files changed

+1033
-437
lines changed

29 files changed

+1033
-437
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ jobs:
492492
# .BACKEND-CHECKSUMS is every Clojure source file as well as dependency files like deps.edn and plugin manifests
493493
- create-checksum-file:
494494
filename: .BACKEND-CHECKSUMS
495-
find-args: ". -type f -name '*.clj' -or -name '*.java' -or -name deps.edn -or -name metabase-plugin.yaml"
495+
find-args: ". -type f -name '*.clj' -or -name '*.java' -or -name '*.edn' -or -name '*.yaml' -or -name sample-dataset.db.mv.db"
496496
# .FRONTEND-CHECKSUMS is every JavaScript source file as well as dependency files like yarn.lock
497497
- create-checksum-file:
498498
filename: .FRONTEND-CHECKSUMS

.dir-locals.el

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
;; Define custom indentation for functions inside metabase.
1818
;; This list isn't complete; add more forms as we come across them.
1919
(define-clojure-indent
20-
(let-404 1)
20+
(let-404)
2121
(match 1)
22-
(merge-with 1)
2322
(l/matche '(1 (:defn)))
2423
(l/matcha '(1 (:defn)))
2524
(p/defprotocol+ '(1 (:defn)))

backend/mbql/src/metabase/mbql/schema.clj

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@
558558
relative-datetime
559559
Field))
560560

561-
(def ^:private EqualityComparible
561+
(def ^:private EqualityComparable
562562
"Schema for things things that make sense in a `=` or `!=` filter, i.e. things that can be compared for equality."
563563
(s/maybe
564564
(s/cond-pre
@@ -570,7 +570,7 @@
570570
ExpressionArg
571571
value)))
572572

573-
(def ^:private OrderComparible
573+
(def ^:private OrderComparable
574574
"Schema for things that make sense in a filter like `>` or `<`, i.e. things that can be sorted."
575575
(s/if (partial is-clause? :value)
576576
value
@@ -594,24 +594,25 @@
594594
;;
595595
;; [:!= [:field 1 nil] 2 3] --[DESUGAR]--> [:and [:!= [:field 1 nil] 2] [:!= [:field 1 nil] 3]]
596596

597-
(defclause =, field EqualityComparible, value-or-field EqualityComparible, more-values-or-fields (rest EqualityComparible))
598-
(defclause !=, field EqualityComparible, value-or-field EqualityComparible, more-values-or-fields (rest EqualityComparible))
597+
(defclause =, field EqualityComparable, value-or-field EqualityComparable, more-values-or-fields (rest EqualityComparable))
598+
(defclause !=, field EqualityComparable, value-or-field EqualityComparable, more-values-or-fields (rest EqualityComparable))
599599

600-
(defclause <, field OrderComparible, value-or-field OrderComparible)
601-
(defclause >, field OrderComparible, value-or-field OrderComparible)
602-
(defclause <=, field OrderComparible, value-or-field OrderComparible)
603-
(defclause >=, field OrderComparible, value-or-field OrderComparible)
600+
(defclause <, field OrderComparable, value-or-field OrderComparable)
601+
(defclause >, field OrderComparable, value-or-field OrderComparable)
602+
(defclause <=, field OrderComparable, value-or-field OrderComparable)
603+
(defclause >=, field OrderComparable, value-or-field OrderComparable)
604604

605-
(defclause between field OrderComparible, min OrderComparible, max OrderComparible)
605+
;; :between is INCLUSIVE just like SQL !!!
606+
(defclause between field OrderComparable, min OrderComparable, max OrderComparable)
606607

607608
;; SUGAR CLAUSE: This is automatically written as a pair of `:between` clauses by the `:desugar` middleware.
608609
(defclause ^:sugar inside
609-
lat-field OrderComparible
610-
lon-field OrderComparible
611-
lat-max OrderComparible
612-
lon-min OrderComparible
613-
lat-min OrderComparible
614-
lon-max OrderComparible)
610+
lat-field OrderComparable
611+
lon-field OrderComparable
612+
lat-max OrderComparable
613+
lon-min OrderComparable
614+
lat-min OrderComparable
615+
lon-max OrderComparable)
615616

616617
;; SUGAR CLAUSES: These are rewritten as `[:= <field> nil]` and `[:not= <field> nil]` respectively
617618
(defclause ^:sugar is-null, field Field)

backend/mbql/src/metabase/mbql/util.clj

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
"Utilitiy functions for working with MBQL queries."
33
(:refer-clojure :exclude [replace])
44
(:require [clojure.string :as str]
5-
[java-time.amount :as t.amount]
6-
[java-time.core :as t.core]
75
[metabase.mbql.schema :as mbql.s]
86
[metabase.mbql.util.match :as mbql.match]
7+
[metabase.util.date-2 :as u.date]
98
[metabase.util.i18n :refer [tru]]
109
[metabase.util.schema :as su]
1110
[schema.core :as s]))
@@ -460,43 +459,25 @@
460459
;; otherwise add new clause at the end
461460
(update inner-query :order-by (comp vec distinct conj) order-by-clause))))
462461

463-
(defn relative-date
464-
"Return a new Temporal value relative to `t` using a relative date `unit`.
465-
466-
(relative-date :year -1 (t/zoned-date-time \"2019-11-04T10:57:00-08:00[America/Los_Angeles]\"))
467-
;; ->
468-
(t/zoned-date-time \"2020-11-04T10:57-08:00[America/Los_Angeles]\")"
469-
^java.time.temporal.Temporal [unit amount t]
470-
(if (zero? amount)
471-
t
472-
(t.core/plus t (case unit
473-
:millisecond (t.amount/millis amount)
474-
:second (t.amount/seconds amount)
475-
:minute (t.amount/minutes amount)
476-
:hour (t.amount/hours amount)
477-
:day (t.amount/days amount)
478-
:week (t.amount/days (* amount 7))
479-
:month (t.amount/months amount)
480-
:quarter (t.amount/months (* amount 3))
481-
:year (t.amount/years amount)))))
482-
483462
(s/defn add-datetime-units :- mbql.s/DateTimeValue
484463
"Return a `relative-datetime` clause with `n` units added to it."
485464
[absolute-or-relative-datetime :- mbql.s/DateTimeValue
486465
n :- s/Num]
487466
(if (is-clause? :relative-datetime absolute-or-relative-datetime)
488467
(let [[_ original-n unit] absolute-or-relative-datetime]
489468
[:relative-datetime (+ n original-n) unit])
490-
(let [[_ timestamp unit] absolute-or-relative-datetime]
491-
[:absolute-datetime (relative-date unit n timestamp) unit])))
469+
(let [[_ t unit] absolute-or-relative-datetime]
470+
[:absolute-datetime (u.date/add t unit n) unit])))
492471

493472
(defn dispatch-by-clause-name-or-class
494473
"Dispatch function perfect for use with multimethods that dispatch off elements of an MBQL query. If `x` is an MBQL
495474
clause, dispatches off the clause name; otherwise dispatches off `x`'s class."
496-
[x]
497-
(if (mbql-clause? x)
498-
(first x)
499-
(class x)))
475+
([x]
476+
(if (mbql-clause? x)
477+
(first x)
478+
(class x)))
479+
([x _]
480+
(dispatch-by-clause-name-or-class x)))
500481

501482
(s/defn expression-with-name :- mbql.s/FieldOrExpressionDef
502483
"Return the `Expression` referenced by a given `expression-name`."
@@ -728,9 +709,24 @@
728709
joins
729710
unique-aliases)))
730711

712+
(defn- remove-empty [x]
713+
(cond
714+
(map? x)
715+
(not-empty (into {} (for [[k v] x
716+
:let [v (remove-empty v)]
717+
:when (some? v)]
718+
[k v])))
719+
720+
(sequential? x)
721+
(not-empty (into (empty x) (filter some? (map remove-empty x))))
722+
723+
:else
724+
x))
725+
731726
(s/defn update-field-options :- mbql.s/field
732727
"Like `clojure.core/update`, but for the options in a `:field` clause."
733728
[[_ id-or-name opts] :- mbql.s/field f & args]
729+
;; TODO -- this should canonicalize the clause afterwards
734730
[:field id-or-name (not-empty (apply f opts args))])
735731

736732
(defn assoc-field-options

backend/mbql/test/metabase/mbql/util_test.clj

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
11
(ns metabase.mbql.util-test
22
(:require [clojure.test :refer :all]
3-
[java-time :as t]
43
[metabase.mbql.util :as mbql.u]
54
metabase.types))
65

76
;; fool cljr-refactor/the linter so it doesn't try to remove the unused dep on `metabase.types`
87
(comment metabase.types/keep-me)
98

10-
(deftest relative-date-test
11-
(let [t (t/zoned-date-time "2019-06-14T00:00:00.000Z[UTC]")]
12-
(doseq [[unit n expected] [[:second 5 "2019-06-14T00:00:05Z[UTC]"]
13-
[:minute 5 "2019-06-14T00:05:00Z[UTC]"]
14-
[:hour 5 "2019-06-14T05:00:00Z[UTC]"]
15-
[:day 5 "2019-06-19T00:00:00Z[UTC]"]
16-
[:week 5 "2019-07-19T00:00:00Z[UTC]"]
17-
[:month 5 "2019-11-14T00:00:00Z[UTC]"]
18-
[:quarter 5 "2020-09-14T00:00:00Z[UTC]"]
19-
[:year 5 "2024-06-14T00:00:00Z[UTC]"]]]
20-
(is (= (t/zoned-date-time expected)
21-
(mbql.u/relative-date unit n t))
22-
(format "%s plus %d %ss should be %s" t n unit expected)))))
23-
24-
259
;;; +----------------------------------------------------------------------------------------------------------------+
2610
;;; | match |
2711
;;; +----------------------------------------------------------------------------------------------------------------+
@@ -991,4 +975,8 @@
991975

992976
(testing "Should remove empty options"
993977
(is (= [:field 1 nil]
994-
(mbql.u/update-field-options [:field 1 {:a 1}] dissoc :a)))))
978+
(mbql.u/update-field-options [:field 1 {:a 1}] dissoc :a))))
979+
980+
(testing "Should normalize the clause"
981+
[:field 1 nil]
982+
(mbql.u/update-field-options [:field 1 {:a {:b 1}}] assoc-in [:a :b] nil)))

bin/lint-migrations-file/src/change/strict.clj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,18 @@
3131

3232
(s/def ::change
3333
(s/keys :opt-un [::addColumn ::createTable]))
34+
35+
(s/def :change.strict.dbms-qualified-sql-change.sql/dbms
36+
string?)
37+
38+
(s/def :change.strict.dbms-qualified-sql-change.sql/sql
39+
string?)
40+
41+
(s/def :change.strict.dbms-qualified-sql-change/sql
42+
(s/keys :req-un [:change.strict.dbms-qualified-sql-change.sql/dbms
43+
:change.strict.dbms-qualified-sql-change.sql/sql]))
44+
45+
(s/def ::dbms-qualified-sql-change
46+
(s/merge
47+
::change
48+
(s/keys :req-un [:change.strict.dbms-qualified-sql-change/sql])))

bin/lint-migrations-file/src/change_set/strict.clj

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
(ns change-set.strict
22
(:require change-set.common
33
change.strict
4-
[clojure.spec.alpha :as s]))
4+
[clojure.spec.alpha :as s]
5+
[clojure.string :as str]))
56

67
(comment change-set.common/keep-me
78
change.strict/keep-me)
@@ -12,9 +13,20 @@
1213
string?
1314
(partial re-find #"Added [\d.x]+")))
1415

15-
;; only one change allowed per change set for the strict schema.
1616
(s/def ::changes
17-
(s/alt :change :change.strict/change))
17+
(s/or
18+
;; only one change allowed per change set for the strict schema.
19+
:one-change
20+
(s/alt :change :change.strict/change)
21+
22+
;; unless it's SQL changes, in which case we'll let you specify more than one as long as they are qualified with
23+
;; different DBMSes
24+
:sql-changes-for-different-
25+
(s/and
26+
(s/+ :change.strict/dbms-qualified-sql-change)
27+
(fn [changes]
28+
(apply distinct? (mapcat #(str/split (-> % :sql :dbms) #",")
29+
changes))))))
1830

1931
(s/def ::change-set
2032
(s/merge

bin/lint-migrations-file/test/lint_migrations_file_test.clj

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,36 @@
119119
(mock-change-set
120120
:id 200
121121
:changes [(update (mock-create-table-changes) :createTable dissoc :remarks)]))))))
122+
123+
(deftest allow-multiple-sql-changes-if-dbmses-are-different
124+
(testing "Allow multiple SQL changes if DBMSes are different"
125+
(is (= :ok
126+
(validate
127+
(mock-change-set
128+
:id 200
129+
:changes
130+
[{:sql {:dbms "h2", :sql "1"}}
131+
{:sql {:dbms "postgresql", :sql "2"}}
132+
{:sql {:dbms "mysql,mariadb", :sql "3"}}])))))
133+
134+
(testing "should fail if *any* change is missing dbms"
135+
(is (thrown-with-msg?
136+
clojure.lang.ExceptionInfo
137+
#":dbms"
138+
(validate
139+
(mock-change-set
140+
:id 200
141+
:changes
142+
[{:sql {:dbms "h2", :sql "1"}}
143+
{:sql {:sql "2"}}])))))
144+
145+
(testing "should fail if a DBMS is repeated"
146+
(is (thrown-with-msg?
147+
clojure.lang.ExceptionInfo
148+
#":changes"
149+
(validate
150+
(mock-change-set
151+
:id 200
152+
:changes
153+
[{:sql {:dbms "h2", :sql "1"}}
154+
{:sql {:dbms "postgresql,h2", :sql "2"}}]))))))

jsconfig.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
1-
{"include": ["frontend/src/**", "enterprise/frontent/src/**"]}
1+
{
2+
"include": [
3+
"frontend/src/**/*",
4+
"enterprise/frontend/src/**/*",
5+
"frontend/test/**/*",
6+
"enterprise/frontend/test/**/*"
7+
],
8+
"compilerOptions": {
9+
"baseUrl": ".",
10+
"paths": {
11+
"*": [
12+
"./frontend/src/*",
13+
"./frontend/test/*",
14+
"./enterprise/frontend/src/*",
15+
"./enterprise/frontend/test/*"
16+
]
17+
}
18+
}
19+
}

modules/drivers/bigquery/test/metabase/driver/bigquery/query_processor_test.clj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,9 @@
604604
[:field (:id f) {:temporal-unit unit}]
605605
[:relative-datetime -1 unit]])}))))))))))
606606

607+
;; This is a table of different BigQuery column types -> temporal units we should be able to bucket them by for
608+
;; filtering purposes against RELATIVE-DATETIMES. `relative-datetime` only supports the unit below -- a subset of all
609+
;; the MBQL date bucketing units.
607610
(def ^:private filter-test-table
608611
[[nil :minute :hour :day :week :month :quarter :year]
609612
[:time true true false false false false false]
@@ -613,6 +616,8 @@
613616

614617
(defn- test-table-with-fn [table f]
615618
(let [units (rest (first table))]
619+
;; this is done in parallel because there are a lot of combinations and doing them one at a time would take the
620+
;; rest of our lives
616621
(dorun (pmap (fn [[field & vs]]
617622
(testing (format "\nfield = %s" field)
618623
(dorun (pmap (fn [[unit expected]]
@@ -631,6 +636,8 @@
631636
(mt/dataset attempted-murders
632637
(test-table-with-fn filter-test-table can-we-filter-against-relative-datetime?)))))
633638

639+
;; This is a table of different BigQuery column types -> temporal units we should be able to bucket them by for
640+
;; breakout purposes.
634641
(def ^:private breakout-test-table
635642
[[nil :default :minute :hour :day :week :month :quarter :year :minute-of-hour :hour-of-day :day-of-week :day-of-month :day-of-year :week-of-year :month-of-year :quarter-of-year]
636643
[:time true true true false false false false false true true false false false false false false]

0 commit comments

Comments
 (0)