From 628a7c2c4ae3cf67e2f324d83601a887829d2f64 Mon Sep 17 00:00:00 2001
From: Cam Saul <1455846+camsaul@users.noreply.github.com>
Date: Tue, 4 Feb 2020 10:29:41 -0800
Subject: [PATCH] Fix temporal type reconciliation logic for relative moments
 in BigQuery (#11838)

[ci drivers]
---
 .../bigquery/src/metabase/driver/bigquery.clj |  15 +-
 .../driver/bigquery/query_processor.clj       | 175 ++++++++++++++----
 .../driver/bigquery/query_processor_test.clj  | 141 +++++++++++++-
 .../google/src/metabase/driver/google.clj     |  15 +-
 .../oracle/src/metabase/driver/oracle.clj     |  22 ++-
 .../presto/src/metabase/driver/presto.clj     |   4 +-
 .../redshift/src/metabase/driver/redshift.clj |   4 +-
 .../src/metabase/driver/snowflake.clj         |   4 +-
 .../src/metabase/driver/hive_like.clj         |   4 +-
 .../sqlite/src/metabase/driver/sqlite.clj     |   4 +-
 .../src/metabase/driver/sqlserver.clj         |   4 +-
 .../vertica/src/metabase/driver/vertica.clj   |   4 +-
 src/metabase/driver.clj                       |  11 +-
 src/metabase/driver/h2.clj                    |   6 +-
 src/metabase/driver/mysql.clj                 |   4 +-
 src/metabase/driver/postgres.clj              |   4 +-
 src/metabase/driver/sql/query_processor.clj   |  37 +++-
 src/metabase/util/date_2.clj                  |   2 +
 18 files changed, 365 insertions(+), 95 deletions(-)

diff --git a/modules/drivers/bigquery/src/metabase/driver/bigquery.clj b/modules/drivers/bigquery/src/metabase/driver/bigquery.clj
index 745286e2dea..e99388ae242 100644
--- a/modules/drivers/bigquery/src/metabase/driver/bigquery.clj
+++ b/modules/drivers/bigquery/src/metabase/driver/bigquery.clj
@@ -12,6 +12,7 @@
             [metabase.driver.google :as google]
             [metabase.driver.sql.util.unprepare :as unprepare]
             [metabase.query-processor
+             [error-type :as error-type]
              [store :as qp.store]
              [timezone :as qp.timezone]
              [util :as qputil]]
@@ -20,8 +21,7 @@
   (:import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
            com.google.api.client.http.HttpRequestInitializer
            [com.google.api.services.bigquery Bigquery Bigquery$Builder BigqueryScopes]
-           [com.google.api.services.bigquery.model QueryRequest QueryResponse Table TableCell TableFieldSchema TableList
-            TableList$Tables TableReference TableRow TableSchema]
+           [com.google.api.services.bigquery.model QueryRequest QueryResponse Table TableCell TableFieldSchema TableList TableList$Tables TableReference TableRow TableSchema]
            java.util.Collections))
 
 (driver/register! :bigquery, :parent #{:google :sql})
@@ -204,9 +204,14 @@
 (defn- process-native* [database query-string]
   {:pre [(map? database) (map? (:details database))]}
   ;; automatically retry the query if it times out or otherwise fails. This is on top of the auto-retry added by
-  ;; `execute` so operations going through `process-native*` may be retried up to 3 times.
-  (u/auto-retry 1
-    (post-process-native (execute-bigquery database query-string))))
+  ;; `execute`
+  (letfn [(thunk []
+            (post-process-native (execute-bigquery database query-string)))]
+    (try
+      (thunk)
+      (catch Throwable e
+        (when-not (error-type/client-error? (:type (u/all-ex-data e)))
+          (thunk))))))
 
 (defn- effective-query-timezone-id [database]
   (if (get-in database [:details :use-jvm-timezone])
diff --git a/modules/drivers/bigquery/src/metabase/driver/bigquery/query_processor.clj b/modules/drivers/bigquery/src/metabase/driver/bigquery/query_processor.clj
index 51440946dd9..54d7f2487da 100644
--- a/modules/drivers/bigquery/src/metabase/driver/bigquery/query_processor.clj
+++ b/modules/drivers/bigquery/src/metabase/driver/bigquery/query_processor.clj
@@ -3,6 +3,7 @@
             [clojure.tools.logging :as log]
             [honeysql
              [core :as hsql]
+             [format :as hformat]
              [helpers :as h]]
             [java-time :as t]
             [metabase
@@ -154,7 +155,10 @@
 (defmethod temporal-type :datetime-field
   [[_ field unit]]
   ;; date extraction operations result in integers, so the type of the expression shouldn't be a temporal type
-  (if (u.date/extract-units unit)
+  ;;
+  ;; `:year` is both an extract unit and a truncate unit in terms of `u.date` capabilities, but in MBQL it should be a
+  ;; truncation operation
+  (if ((disj u.date/extract-units :year) unit)
     nil
     (temporal-type field)))
 
@@ -166,7 +170,13 @@
       [:field-id id]               (temporal-type (qp.store/field id))
       [:field-literal _ base-type] (base-type->temporal-type base-type))))
 
+(defn- with-temporal-type [x new-type]
+  (if (= (temporal-type x) new-type)
+    x
+    (vary-meta x assoc :bigquery/temporal-type new-type)))
+
 (defmulti ^:private ->temporal-type
+  "Coerce `x` to target temporal type."
   {:arglists '([target-type x])}
   (fn [target-type x]
     [target-type (mbql.u/dispatch-by-clause-name-or-class x)])
@@ -212,7 +222,7 @@
     nil
 
     (= (temporal-type x) target-type)
-    (vary-meta x assoc :bigquery/temporal-type target-type)
+    (with-temporal-type x target-type)
 
     :else
     (let [hsql-form     (sql.qp/->honeysql :bigquery x)
@@ -227,13 +237,12 @@
         nil
 
         (= (temporal-type hsql-form) target-type)
-        (vary-meta hsql-form assoc :bigquery/temporal-type target-type)
+        (with-temporal-type hsql-form target-type)
 
         bigquery-type
         (do
-          (log/tracef "Casting %s (temporal type = %s) to %s" (binding [*print-meta* true] (pr-str x)) (temporal-type x) bigquery-type)
-          (with-meta (hx/cast bigquery-type (sql.qp/->honeysql :bigquery x))
-            {:bigquery/temporal-type target-type}))
+          (log/tracef "Coercing %s (temporal type = %s) to %s" (binding [*print-meta* true] (pr-str x)) (pr-str (temporal-type x)) bigquery-type)
+          (with-temporal-type (hx/cast bigquery-type (sql.qp/->honeysql :bigquery x)) target-type))
 
         :else
         x))))
@@ -242,21 +251,49 @@
   [target-type [_ t unit]]
   [:absolute-datetime (->temporal-type target-type t) unit])
 
+(def ^:private temporal-type->supported-units
+  {:timestamp #{:microsecond :millisecond :second :minute :hour :day}
+   :datetime  #{:microsecond :millisecond :second :minute :hour :day :week :month :quarter :year}
+   :date      #{:day :week :month :quarter :year}
+   :time      #{:microsecond :millisecond :second :minute :hour}})
+
+(defmethod ->temporal-type [:temporal-type :relative-datetime]
+  [target-type [_ _ unit :as clause]]
+  {:post [(= target-type (temporal-type %))]}
+  (with-temporal-type
+    ;; check and see whether we need to do a conversion. If so, use the parent method which will just wrap this in a
+    ;; cast statement.
+    (if ((temporal-type->supported-units target-type) unit)
+      clause
+      ((get-method ->temporal-type :default) target-type clause))
+    target-type))
+
+(defrecord ^:private TruncForm [hsql-form unit]
+  hformat/ToSql
+  (to-sql [_]
+    (let [t (or (temporal-type hsql-form) :datetime)
+          f (case t
+              :date      :date_trunc
+              :time      :time_trunc
+              :datetime  :datetime_trunc
+              :timestamp :timestamp_trunc)]
+      (hformat/to-sql (hsql/call f (->temporal-type t hsql-form) (hsql/raw (name unit)))))))
+
+(defmethod temporal-type TruncForm
+  [trunc-form]
+  (temporal-type (:hsql-form trunc-form)))
+
+(defmethod ->temporal-type [:temporal-type TruncForm]
+  [target-type trunc-form]
+  (map->TruncForm (update trunc-form :hsql-form (partial ->temporal-type target-type))))
+
 (defn- trunc
   "Generate a SQL call an appropriate truncation function, depending on the temporal type of `expr`."
-  [unit expr]
-  (let [expr-type (or (temporal-type expr) :datetime)
-        f         (case expr-type
-                    :date      :date_trunc
-                    :time      :time_trunc
-                    :datetime  :datetime_trunc
-                    :timestamp :timestamp_trunc)]
-    (with-meta (hsql/call f (->temporal-type expr-type expr) (hsql/raw (name unit)))
-      {:bigquery/temporal-type expr-type})))
+  [unit hsql-form]
+  (TruncForm. hsql-form unit))
 
 (defn- extract [unit expr]
-  (with-meta (hsql/call :extract unit (->temporal-type :timestamp expr))
-    {:bigquery/temporal-type nil}))
+  (with-temporal-type (hsql/call :extract unit (->temporal-type :timestamp expr)) nil))
 
 (defmethod sql.qp/date [:bigquery :minute]          [_ _ expr] (trunc   :minute    expr))
 (defmethod sql.qp/date [:bigquery :minute-of-hour]  [_ _ expr] (extract :minute    expr))
@@ -279,7 +316,7 @@
                                            :milliseconds :timestamp_millis}]
   (defmethod sql.qp/unix-timestamp->timestamp [:bigquery unix-timestamp-type]
     [_ _ expr]
-    (vary-meta (hsql/call bigquery-fn expr) assoc :bigquery/temporal-type :timestamp)))
+    (with-temporal-type (hsql/call bigquery-fn expr) :timestamp)))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -316,7 +353,7 @@
   [driver field]
   (let [parent-method (get-method sql.qp/->honeysql [:sql (class Field)])
         identifier    (parent-method driver field)]
-    (vary-meta identifier assoc :bigquery/temporal-type (temporal-type field))))
+    (with-temporal-type identifier (temporal-type field))))
 
 (defmethod sql.qp/->honeysql [:bigquery Identifier]
   [_ identifier]
@@ -332,7 +369,14 @@
   (defmethod sql.qp/->honeysql [:bigquery clause-type]
     [driver clause]
     (let [hsql-form ((get-method sql.qp/->honeysql [:sql clause-type]) driver clause)]
-      (vary-meta hsql-form assoc :bigquery/temporal-type (temporal-type clause)))))
+      (with-temporal-type hsql-form (temporal-type clause)))))
+
+(defmethod sql.qp/->honeysql [:bigquery :relative-datetime]
+  [driver clause]
+  ;; wrap the parent method, converting the result if `clause` itself is typed
+  (let [t (temporal-type clause)]
+    (cond->> ((get-method sql.qp/->honeysql [:sql :relative-datetime]) driver clause)
+      t (->temporal-type t))))
 
 (s/defn ^:private honeysql-form->sql :- s/Str
   [driver, honeysql-form :- su/Map]
@@ -403,8 +447,7 @@
   ;;
   ;; TODO - we should make sure these are in the QP store somewhere and then could at least batch the calls
   (let [table-name (db/select-one-field :name table/Table :id (u/get-id table-id))]
-    (with-meta (hx/identifier :field table-name field-name)
-      {:bigquery/temporal-type (temporal-type field)})))
+    (with-temporal-type (hx/identifier :field table-name field-name) (temporal-type field))))
 
 (defmethod sql.qp/apply-top-level-clause [:bigquery :breakout]
   [driver _ honeysql-form {breakouts :breakout, fields :fields}]
@@ -437,8 +480,9 @@
   (if-let [target-type (or (temporal-type f) (some temporal-type args))]
     (do
       (log/tracef "Coercing args in %s to temporal type %s" (binding [*print-meta* true] (pr-str clause)) target-type)
-      (u/prog1 (into [clause-type] (map (partial ->temporal-type target-type) (cons f args)))
-        (when-not (= clause <>)
+      (u/prog1 (into [clause-type] (map (partial ->temporal-type target-type)
+                                        (cons f args)))
+        (when (not= [clause (meta clause)] [<> (meta <>)])
           (log/tracef "Coerced -> %s" (binding [*print-meta* true] (pr-str <>))))))
     clause))
 
@@ -454,18 +498,55 @@
 ;;; |                                Other Driver / SQLDriver Method Implementations                                 |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(defmethod driver/date-add :bigquery
-  [driver expr amount unit]
-  (let [add-fn (case (temporal-type expr)
-                 :timestamp :timestamp_add
-                 :datetime  :datetime_add
-                 :date      :date_add
-                 :time      :time_add
-                 nil)]
-    (if-not add-fn
-      (driver/date-add driver (->temporal-type :datetime expr) amount unit)
-      (with-meta (hsql/call add-fn expr (hsql/raw (format "INTERVAL %d %s" (int amount) (name unit))))
-        {:bigquery/temporal-type (temporal-type expr)}))))
+(defn- interval [amount unit]
+  (hsql/raw (format "INTERVAL %d %s" (int amount) (name unit))))
+
+(defn- assert-addable-unit [t-type unit]
+  (when-not (contains? (temporal-type->supported-units t-type) unit)
+    ;; trying to add an `hour` to a `date` or a `year` to a `time` is something we shouldn't be allowing in the UI in
+    ;; the first place
+    (throw (ex-info (tru "Invalid query: you cannot add a {0} to a {1} column."
+                         (name unit) (name t-type))
+             {:type error-type/invalid-query}))))
+
+;; We can coerce the HoneySQL form this wraps to whatever we want and generate the appropriate SQL.
+;; Thus for something like filtering against a relative datetime
+;;
+;; [:time-interval <datetime field> -1 :day]
+;;
+;;
+(defrecord ^:private AddIntervalForm [hsql-form amount unit]
+  hformat/ToSql
+  (to-sql [_]
+    (loop [hsql-form hsql-form]
+      (let [t      (temporal-type hsql-form)
+            add-fn (case t
+                     :timestamp :timestamp_add
+                     :datetime  :datetime_add
+                     :date      :date_add
+                     :time      :time_add
+                     nil)]
+        (if-not add-fn
+          (recur (->temporal-type :datetime hsql-form))
+          (do
+            (assert-addable-unit t unit)
+            (hformat/to-sql (hsql/call add-fn hsql-form (interval amount unit)))))))))
+
+(defmethod temporal-type AddIntervalForm
+  [add-interval]
+  (temporal-type (:hsql-form add-interval)))
+
+(defmethod ->temporal-type [:temporal-type AddIntervalForm]
+  [target-type add-interval-form]
+  (let [current-type (temporal-type (:hsql-form add-interval-form))]
+    (when (#{[:date :time] [:time :date]} [current-type target-type])
+      (throw (ex-info (tru "It doesn''t make sense to convert between DATEs and TIMEs!")
+               {:type error-type/invalid-query}))))
+  (map->AddIntervalForm (update add-interval-form :hsql-form (partial ->temporal-type target-type))))
+
+(defmethod sql.qp/add-interval-honeysql-form :bigquery
+  [_ hsql-form amount unit]
+  (AddIntervalForm. hsql-form amount unit))
 
 (defmethod driver/mbql->native :bigquery
   [driver
@@ -484,9 +565,27 @@
                          sql.qp/source-query-alias))
        :mbql?      true})))
 
-(defmethod sql.qp/current-datetime-fn :bigquery
+(defrecord ^:private CurrentMomentForm [t]
+  hformat/ToSql
+  (to-sql [_]
+    (hformat/to-sql
+     (case (or t :timestamp)
+       :time      :%current_time
+       :date      :%current_date
+       :datetime  :%current_datetime
+       :timestamp :%current_timestamp))))
+
+(defmethod temporal-type CurrentMomentForm
+  [^CurrentMomentForm current-moment]
+  (.t current-moment))
+
+(defmethod ->temporal-type [:temporal-type CurrentMomentForm]
+  [t _]
+  (CurrentMomentForm. t))
+
+(defmethod sql.qp/current-datetime-honeysql-form :bigquery
   [_]
-  (with-meta (hsql/call :current_timestamp) {:bigquery/temporal-type :timestamp}))
+  (CurrentMomentForm. nil))
 
 (defmethod sql.qp/quote-style :bigquery
   [_]
diff --git a/modules/drivers/bigquery/test/metabase/driver/bigquery/query_processor_test.clj b/modules/drivers/bigquery/test/metabase/driver/bigquery/query_processor_test.clj
index 0d4715288a7..d288029ee9d 100644
--- a/modules/drivers/bigquery/test/metabase/driver/bigquery/query_processor_test.clj
+++ b/modules/drivers/bigquery/test/metabase/driver/bigquery/query_processor_test.clj
@@ -1,6 +1,10 @@
 (ns metabase.driver.bigquery.query-processor-test
-  (:require [clojure.test :refer :all]
-            [honeysql.core :as hsql]
+  (:require [clojure
+             [string :as str]
+             [test :refer :all]]
+            [honeysql
+             [core :as hsql]
+             [format :as hformat]]
             [java-time :as t]
             [metabase
              [driver :as driver]
@@ -305,6 +309,36 @@
               (is (= [:= (hsql/call :extract :dayofweek expected-identifier) 1]
                      (sql.qp/->honeysql :bigquery [:= [:datetime-field [:field-id (:id field)] :day-of-week] 1]))))))))))
 
+(deftest reconcile-relative-datetimes-test
+  (testing "relative-datetime clauses on their own"
+    (doseq [[t [unit expected-sql]]
+            {:time      [:hour "time_trunc(time_add(current_time(), INTERVAL -1 hour), hour)"]
+             :date      [:year "date_trunc(date_add(current_date(), INTERVAL -1 year), year)"]
+             :datetime  [:year "datetime_trunc(datetime_add(current_datetime(), INTERVAL -1 year), year)"]
+             ;; timestamp_add doesn't support `year` so this should cast a datetime instead
+             :timestamp [:year "CAST(datetime_trunc(datetime_add(current_datetime(), INTERVAL -1 year), year) AS timestamp)"]}]
+      (testing t
+        (let [reconciled-clause (#'bigquery.qp/->temporal-type t [:relative-datetime -1 unit])]
+          (is (= t
+                 (#'bigquery.qp/temporal-type reconciled-clause))
+              "Should have correct type metadata after reconciliation")
+          (is (= [(str "WHERE " expected-sql)]
+                 (sql.qp/format-honeysql :bigquery
+                   {:where (sql.qp/->honeysql :bigquery reconciled-clause)}))
+              "Should get converted to the correct SQL")))))
+
+  (testing "relative-datetime clauses inside filter clauses"
+    (doseq [[expected-type t] {:date      #t "2020-01-31"
+                               :datetime  #t "2020-01-31T20:43:00.000"
+                               :timestamp #t "2020-01-31T20:43:00.000-08:00"}]
+      (testing expected-type
+        (let [[_ _ relative-datetime] (sql.qp/->honeysql :bigquery
+                                        [:=
+                                         t
+                                         [:relative-datetime -1 :year]])]
+          (is (= expected-type
+                 (#'bigquery.qp/temporal-type relative-datetime))))))))
+
 (deftest between-test
   (testing "Make sure :between clauses reconcile the temporal types of their args"
     (letfn [(between->sql [clause]
@@ -413,8 +447,8 @@
                              :filter       filter-clause})))))))))))))
 
 (deftest datetime-parameterized-sql-test
-  (testing "Make sure Field filters against temporal fields generates correctly-typed SQL (#11578)"
-    (mt/test-driver :bigquery
+  (mt/test-driver :bigquery
+    (testing "Make sure Field filters against temporal fields generates correctly-typed SQL (#11578)"
       (mt/dataset attempted-murders
         (doseq [field              [:datetime
                                     :date
@@ -440,3 +474,102 @@
                                         :name   "d"
                                         :target [:dimension [:template-tag "d"]]
                                         :value  value}]})))))))))))
+
+(deftest current-datetime-honeysql-form-test
+  (testing (str "The object returned by `current-datetime-honeysql-form` should be a magic object that can take on "
+                "whatever temporal type we want.")
+    (let [form (sql.qp/current-datetime-honeysql-form :bigquery)]
+      (is (= nil
+             (#'bigquery.qp/temporal-type form))
+          "When created the temporal type should be unspecified. The world's your oyster!")
+      (is (= ["current_timestamp()"]
+             (hformat/format form))
+          "Should fall back to acting like a timestamp if we don't coerce it to something else first")
+      (doseq [[temporal-type expected-sql] {:date      "current_date()"
+                                            :time      "current_time()"
+                                            :datetime  "current_datetime()"
+                                            :timestamp "current_timestamp()"}]
+        (testing (format "temporal type = %s" temporal-type)
+          (is (= temporal-type
+                 (#'bigquery.qp/temporal-type (#'bigquery.qp/->temporal-type temporal-type form)))
+              "Should be possible to convert to another temporal type/should report its type correctly")
+          (is (= [expected-sql]
+                 (hformat/format (#'bigquery.qp/->temporal-type temporal-type form)))
+              "Should convert to the correct SQL"))))))
+
+(deftest add-interval-honeysql-form-test
+  ;; this doesn't test conversion to/from time because there's no unit we can use that works for all for. So we'll
+  ;; just test the 3 that support `:day` and that should be proof the logic is working. (The code that actually uses
+  ;; this is tested e2e by `filter-by-relative-date-ranges-test` anyway.)
+  (doseq [initial-type [:date :datetime :timestamp]
+          :let         [form (sql.qp/add-interval-honeysql-form
+                              :bigquery
+                              (#'bigquery.qp/->temporal-type
+                               initial-type
+                               (sql.qp/current-datetime-honeysql-form :bigquery))
+                              -1
+                              :day)]]
+    (testing (format "initial form = %s" (pr-str form))
+      (is (= initial-type
+             (#'bigquery.qp/temporal-type form))
+          "Should have the temporal-type of the form it wraps when created.")
+      (doseq [[new-type expected-sql] {:date      "date_add(current_date(), INTERVAL -1 day)"
+                                       :datetime  "datetime_add(current_datetime(), INTERVAL -1 day)"
+                                       :timestamp "timestamp_add(current_timestamp(), INTERVAL -1 day)"}]
+        (testing (format "\nconvert from %s -> %s" initial-type new-type)
+          (is (= new-type
+                 (#'bigquery.qp/temporal-type (#'bigquery.qp/->temporal-type new-type form)))
+              "Should be possible to convert to another temporal type/should report its type correctly")
+          (is (= [expected-sql]
+                 (hformat/format (#'bigquery.qp/->temporal-type new-type form)))
+              "Should convert to the correct SQL"))))))
+
+(defn- can-we-filter-against-relative-datetime? [field unit]
+  (let [{:keys [error]} (mt/run-mbql-query attempts
+                          {:aggregation [[:count]]
+                           :filter      [:time-interval (mt/id :attempts field) :last unit]})]
+    (not error)))
+
+(deftest filter-by-relative-date-ranges-test
+  (testing "Make sure the SQL we generate for filters against relative-datetimes is typed correctly"
+    (mt/with-everything-store
+      (binding [sql.qp/*table-alias* "ABC"]
+        (doseq [[field-type [unit expected-sql]]
+                {:type/Time                [:hour (str "WHERE time_trunc(ABC.time, hour)"
+                                                       " = time_trunc(time_add(current_time(), INTERVAL -1 hour), hour)")]
+                 :type/Date                [:year (str "WHERE date_trunc(ABC.date, year)"
+                                                       " = date_trunc(date_add(current_date(), INTERVAL -1 year), year)")]
+                 :type/DateTime            [:year (str "WHERE datetime_trunc(ABC.datetime, year)"
+                                                       " = datetime_trunc(datetime_add(current_datetime(), INTERVAL -1 year), year)")]
+                 ;; `timestamp_add` doesn't support `year` so it should cast a `datetime_trunc` instead
+                 :type/DateTimeWithLocalTZ [:year (str "WHERE timestamp_trunc(ABC.datetimewithlocaltz, year)"
+                                                       " = CAST(datetime_trunc(datetime_add(current_datetime(), INTERVAL -1 year), year) AS timestamp)")]}]
+          (mt/with-temp Field [f {:name (str/lower-case (name field-type)), :base_type field-type}]
+            (testing (format "%s field" field-type)
+              (is (= [expected-sql]
+                     (hsql/format {:where (sql.qp/->honeysql :bigquery [:=
+                                                                        [:datetime-field [:field-id (:id f)] unit]
+                                                                        [:relative-datetime -1 unit]])}))))))))))
+
+(deftest filter-by-relative-date-ranges-e2e-test
+  (mt/test-driver :bigquery
+    (testing (str "Make sure filtering against relative date ranges works correctly regardless of underlying column "
+                  "type (#11725)")
+      (mt/dataset attempted-murders
+        (is (= [[nil          :minute :hour :day  :week :month :quarter :year]
+                [:time        true    true  false false false  false    false]
+                [:datetime    true    true  true  true  true   true     true]
+                [:date        false   false true  true  true   true     true]
+                [:datetime_tz true    true  true  true  true   true     true]]
+               (let [units  [:minute :hour :day :week :month :quarter :year]
+                     fields [:time :datetime :date :datetime_tz]]
+
+                 (into
+                  [(into [nil] units)]
+                  (pmap
+                   (fn [field]
+                     (into [field] (pmap
+                                    (fn [unit]
+                                      (boolean (can-we-filter-against-relative-datetime? field unit)))
+                                    units)))
+                   fields)))))))))
diff --git a/modules/drivers/google/src/metabase/driver/google.clj b/modules/drivers/google/src/metabase/driver/google.clj
index 176cc78f2e4..8ca1bebe8d7 100644
--- a/modules/drivers/google/src/metabase/driver/google.clj
+++ b/modules/drivers/google/src/metabase/driver/google.clj
@@ -6,10 +6,10 @@
              [driver :as driver]
              [util :as u]]
             [metabase.models.database :refer [Database]]
+            [metabase.query-processor.error-type :as error-type]
             [ring.util.codec :as codec]
             [toucan.db :as db])
-  (:import [com.google.api.client.googleapis.auth.oauth2 GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder
-            GoogleCredential GoogleCredential$Builder GoogleTokenResponse]
+  (:import [com.google.api.client.googleapis.auth.oauth2 GoogleAuthorizationCodeFlow GoogleAuthorizationCodeFlow$Builder GoogleCredential GoogleCredential$Builder GoogleTokenResponse]
            com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
            [com.google.api.client.googleapis.json GoogleJsonError GoogleJsonResponseException]
            com.google.api.client.googleapis.services.AbstractGoogleClientRequest
@@ -30,7 +30,7 @@
 (def ^:private ^:const ^String redirect-uri "urn:ietf:wg:oauth:2.0:oob")
 
 (defn execute-no-auto-retry
-  "`execute` REQUEST, and catch any `GoogleJsonResponseException` is throws, converting them to `ExceptionInfo` and
+  "Execute `request`, and catch any `GoogleJsonResponseException` is throws, converting them to `ExceptionInfo` and
   rethrowing them."
   [^AbstractGoogleClientRequest request]
   (try (.execute request)
@@ -44,10 +44,13 @@
   "Execute `request`, and catch any `GoogleJsonResponseException` is throws, converting them to `ExceptionInfo` and
   rethrowing them.
 
-  This automatically retries any failed requests up to 2 times."
+  This automatically retries any failed requests."
   [^AbstractGoogleClientRequest request]
-  (u/auto-retry 2
-    (execute-no-auto-retry request)))
+  (try
+    (execute-no-auto-retry request)
+    (catch Throwable e
+      (when-not (error-type/client-error? (:type (u/all-ex-data e)))
+        (execute-no-auto-retry request)))))
 
 (defn- create-application-name
   "Creates the application name string, separated out from the `def` below so it's testable with different values"
diff --git a/modules/drivers/oracle/src/metabase/driver/oracle.clj b/modules/drivers/oracle/src/metabase/driver/oracle.clj
index 27f4be986e5..bfb0b35547b 100644
--- a/modules/drivers/oracle/src/metabase/driver/oracle.clj
+++ b/modules/drivers/oracle/src/metabase/driver/oracle.clj
@@ -134,16 +134,18 @@
 (defn- num-to-ym-interval [unit v] (hsql/call :numtoyminterval v (hx/literal unit)))
 
 (defmethod driver/date-add :oracle
-  [_ dt amount unit]
-  (hx/+ (hx/->timestamp dt) (case unit
-                              :second  (num-to-ds-interval :second amount)
-                              :minute  (num-to-ds-interval :minute amount)
-                              :hour    (num-to-ds-interval :hour   amount)
-                              :day     (num-to-ds-interval :day    amount)
-                              :week    (num-to-ds-interval :day    (hx/* amount (hsql/raw 7)))
-                              :month   (num-to-ym-interval :month  amount)
-                              :quarter (num-to-ym-interval :month  (hx/* amount (hsql/raw 3)))
-                              :year    (num-to-ym-interval :year   amount))))
+  [_ hsql-form amount unit]
+  (hx/+
+   (hx/->timestamp hsql-form)
+   (case unit
+     :second  (num-to-ds-interval :second amount)
+     :minute  (num-to-ds-interval :minute amount)
+     :hour    (num-to-ds-interval :hour   amount)
+     :day     (num-to-ds-interval :day    amount)
+     :week    (num-to-ds-interval :day    (hx/* amount (hsql/raw 7)))
+     :month   (num-to-ym-interval :month  amount)
+     :quarter (num-to-ym-interval :month  (hx/* amount (hsql/raw 3)))
+     :year    (num-to-ym-interval :year   amount))))
 
 (defmethod sql.qp/unix-timestamp->timestamp [:oracle :seconds]
   [_ _ field-or-value]
diff --git a/modules/drivers/presto/src/metabase/driver/presto.clj b/modules/drivers/presto/src/metabase/driver/presto.clj
index 9a2cf94f60e..9cf915499c6 100644
--- a/modules/drivers/presto/src/metabase/driver/presto.clj
+++ b/modules/drivers/presto/src/metabase/driver/presto.clj
@@ -140,8 +140,8 @@
     (= v "information_schema")))
 
 (defmethod driver/date-add :presto
-  [_ dt amount unit]
-  (hsql/call :date_add (hx/literal unit) amount dt))
+  [_ hsql-form amount unit]
+  (hsql/call :date_add (hx/literal unit) amount hsql-form))
 
 (s/defn ^:private database->all-schemas :- #{su/NonBlankString}
   "Return a set of all schema names in this `database`."
diff --git a/modules/drivers/redshift/src/metabase/driver/redshift.clj b/modules/drivers/redshift/src/metabase/driver/redshift.clj
index 7e40a31b610..44d976f1e26 100644
--- a/modules/drivers/redshift/src/metabase/driver/redshift.clj
+++ b/modules/drivers/redshift/src/metabase/driver/redshift.clj
@@ -82,8 +82,8 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defmethod driver/date-add :redshift
-  [_ dt amount unit]
-  (hsql/call :dateadd (hx/literal unit) amount (hx/->timestamp dt)))
+  [_ hsql-form amount unit]
+  (hsql/call :dateadd (hx/literal unit) amount (hx/->timestamp hsql-form)))
 
 (defmethod sql.qp/unix-timestamp->timestamp [:redshift :seconds]
   [_ _ expr]
diff --git a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj
index 5bf66119366..756755625da 100644
--- a/modules/drivers/snowflake/src/metabase/driver/snowflake.clj
+++ b/modules/drivers/snowflake/src/metabase/driver/snowflake.clj
@@ -106,11 +106,11 @@
   :%current_timestamp)
 
 (defmethod driver/date-add :snowflake
-  [_ dt amount unit]
+  [_ hsql-form amount unit]
   (hsql/call :dateadd
     (hsql/raw (name unit))
     (hsql/raw (int amount))
-    (hx/->timestamp dt)))
+    (hx/->timestamp hsql-form)))
 
 (defn- extract    [unit expr] (hsql/call :date_part unit (hx/->timestamp expr)))
 (defn- date-trunc [unit expr] (hsql/call :date_trunc unit (hx/->timestamp expr)))
diff --git a/modules/drivers/sparksql/src/metabase/driver/hive_like.clj b/modules/drivers/sparksql/src/metabase/driver/hive_like.clj
index 6b679330ab8..fd05e378fe8 100644
--- a/modules/drivers/sparksql/src/metabase/driver/hive_like.clj
+++ b/modules/drivers/sparksql/src/metabase/driver/hive_like.clj
@@ -113,8 +113,8 @@
           3)))
 
 (defmethod driver/date-add :hive-like
-  [_ dt amount unit]
-  (hx/+ (hx/->timestamp dt) (hsql/raw (format "(INTERVAL '%d' %s)" (int amount) (name unit)))))
+  [_ hsql-form amount unit]
+  (hx/+ (hx/->timestamp hsql-form) (hsql/raw (format "(INTERVAL '%d' %s)" (int amount) (name unit)))))
 
 ;; ignore the schema when producing the identifier
 (defn qualified-name-components
diff --git a/modules/drivers/sqlite/src/metabase/driver/sqlite.clj b/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
index c4ce0e037f8..4a0d941c054 100644
--- a/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
+++ b/modules/drivers/sqlite/src/metabase/driver/sqlite.clj
@@ -162,7 +162,7 @@
   (->date (sql.qp/->honeysql driver expr) (hx/literal "start of year")))
 
 (defmethod driver/date-add :sqlite
-  [driver dt amount unit]
+  [driver hsql-form amount unit]
   (let [[multiplier sqlite-unit] (case unit
                                    :second  [1 "seconds"]
                                    :minute  [1 "minutes"]
@@ -184,7 +184,7 @@
     ;; The SQL we produce instead (for "last month") ends up looking something like:
     ;; DATE(DATETIME(DATE('2015-03-30', 'start of month'), '-1 month'), 'start of month').
     ;; It's a little verbose, but gives us the correct answer (Feb 1st).
-    (->datetime (sql.qp/date driver unit dt)
+    (->datetime (sql.qp/date driver unit hsql-form)
                 (hx/literal (format "%+d %s" (* amount multiplier) sqlite-unit)))))
 
 (defmethod sql.qp/unix-timestamp->timestamp [:sqlite :seconds]
diff --git a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
index a5e8e65b875..82919180d29 100644
--- a/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
+++ b/modules/drivers/sqlserver/src/metabase/driver/sqlserver.clj
@@ -180,8 +180,8 @@
   (hsql/call :datefromparts (hx/year expr) 1 1))
 
 (defmethod driver/date-add :sqlserver
-  [_ dt amount unit]
-  (date-add unit amount dt))
+  [_ hsql-form amount unit]
+  (date-add unit amount hsql-form))
 
 (defmethod sql.qp/unix-timestamp->timestamp [:sqlserver :seconds]
   [_ _ expr]
diff --git a/modules/drivers/vertica/src/metabase/driver/vertica.clj b/modules/drivers/vertica/src/metabase/driver/vertica.clj
index fbec1bde969..79d70d25a38 100644
--- a/modules/drivers/vertica/src/metabase/driver/vertica.clj
+++ b/modules/drivers/vertica/src/metabase/driver/vertica.clj
@@ -96,8 +96,8 @@
         one-day))
 
 (defmethod driver/date-add :vertica
-  [_ dt amount unit]
-  (hx/+ (hx/->timestamp dt) (hsql/raw (format "(INTERVAL '%d %s')" (int amount) (name unit)))))
+  [_ hsql-form amount unit]
+  (hx/+ (hx/->timestamp hsql-form) (hsql/raw (format "(INTERVAL '%d %s')" (int amount) (name unit)))))
 
 (defn- materialized-views
   "Fetch the Materialized Views for a Vertica `database`.
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 7a7933e944c..06aa491f06a 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -230,12 +230,13 @@
   dispatch-on-initialized-driver
   :hierarchy #'hierarchy)
 
+(defmulti ^{:deprecated "0.34.2"} date-add
+  "DEPRECATED -- this method is only used or implemented by `:sql` drivers. It has been superseded by
+  `metabase.driver.sql.query-processor/add-interval-honeysql-form`. Use/implement that method instead. DO NOT use or
+  implement this method for non-`:sql` drivers.
 
-;; TODO - this is only used (or implemented for that matter) by SQL drivers. This should probably be moved into the
-;; `:sql` driver. Don't bother to implement this for non-SQL drivers.
-(defmulti date-add
-  "Return an driver-appropriate representation of a moment relative to the given time."
-  {:arglists '([driver dt amount unit])}
+  This method will be removed at some point in the future."
+  {:arglists '([driver hsql-form amount unit])}
   dispatch-on-initialized-driver
   :hierarchy #'hierarchy)
 
diff --git a/src/metabase/driver/h2.clj b/src/metabase/driver/h2.clj
index b08da9b2fb1..025e0fdfa6f 100644
--- a/src/metabase/driver/h2.clj
+++ b/src/metabase/driver/h2.clj
@@ -70,10 +70,10 @@
 (defmethod driver/process-query-in-context :h2 [_ qp]
   (comp qp check-native-query-not-using-default-user))
 
-(defmethod driver/date-add :h2 [driver dt amount unit]
+(defmethod driver/date-add :h2 [driver hsql-form amount unit]
   (if (= unit :quarter)
-    (recur driver dt (hx/* amount 3) :month)
-    (hsql/call :dateadd (hx/literal unit) amount dt)))
+    (recur driver hsql-form (hx/* amount 3) :month)
+    (hsql/call :dateadd (hx/literal unit) amount hsql-form)))
 
 (defmethod driver/humanize-connection-error-message :h2 [_ message]
   (condp re-matches message
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index cc0fcd6e663..bcd14409f14 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -72,8 +72,8 @@
        :placeholder  "tinyInt1isBit=false")]))
 
 (defmethod driver/date-add :mysql
-  [_ dt amount unit]
-  (hsql/call :date_add dt (hsql/raw (format "INTERVAL %d %s" (int amount) (name unit)))))
+  [_ hsql-form amount unit]
+  (hsql/call :date_add hsql-form (hsql/raw (format "INTERVAL %d %s" (int amount) (name unit)))))
 
 (defmethod driver/humanize-connection-error-message :mysql
   [_ message]
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index 0727f7b5416..482d090fc35 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -41,8 +41,8 @@
 (defmethod driver/display-name :postgres [_] "PostgreSQL")
 
 (defmethod driver/date-add :postgres
-  [_ dt amount unit]
-  (hx/+ (hx/->timestamp dt)
+  [_ hsql-form amount unit]
+  (hx/+ (hx/->timestamp hsql-form)
         (hsql/raw (format "(INTERVAL '%d %s')" (int amount) (name unit)))))
 
 (defmethod driver/humanize-connection-error-message :postgres
diff --git a/src/metabase/driver/sql/query_processor.clj b/src/metabase/driver/sql/query_processor.clj
index 4d320f4805c..43780aa777b 100644
--- a/src/metabase/driver/sql/query_processor.clj
+++ b/src/metabase/driver/sql/query_processor.clj
@@ -60,14 +60,28 @@
 ;;; |                                            Interface (Multimethods)                                            |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(defmulti current-datetime-fn
-  "HoneySQL form that should be used to get the current `datetime` (or equivalent). Defaults to `:%now`."
+(defmulti ^{:deprecated "0.34.2"} current-datetime-fn
+  "HoneySQL form that should be used to get the current `datetime` (or equivalent). Defaults to `:%now`.
+
+  DEPRECATED: `current-datetime-fn` is a misnomer, since the result can actually be any valid HoneySQL form.
+  `current-datetime-honeysql-form` replaces this method; implement and call that method instead. This method will be
+  removed in favor of `current-datetime-honeysql-form` at some point in the future."
   {:arglists '([driver])}
   driver/dispatch-on-initialized-driver
   :hierarchy #'driver/hierarchy)
 
 (defmethod current-datetime-fn :sql [_] :%now)
 
+(defmulti current-datetime-honeysql-form
+  "HoneySQL form that should be used to get the current `datetime` (or equivalent). Defaults to `:%now`."
+  {:arglists '([driver])}
+  driver/dispatch-on-initialized-driver
+  :hierarchy #'driver/hierarchy)
+
+(defmethod current-datetime-honeysql-form :sql
+  [driver]
+  (current-datetime-fn driver))
+
 ;; TODO - rename this to `date-bucket` or something that better describes what it actually does
 (defmulti date
   "Return a HoneySQL form for truncating a date or timestamp field or value to a given resolution, or extracting a date
@@ -80,6 +94,17 @@
 (defmethod date [:sql :default] [_ _ expr] expr)
 
 
+(defmulti add-interval-honeysql-form
+  "Return a HoneySQL form that performs represents addition of some temporal interval to the original `hsql-form`.
+
+    (add-interval-honeysql-form :my-driver hsql-form 1 :day) -> (hsql/call :date_add hsql-form 1 (hx/literal 'day'))"
+  {:arglists '([driver hsql-form amount unit])}
+  driver/dispatch-on-initialized-driver
+  :hierarchy #'driver/hierarchy)
+
+(defmethod add-interval-honeysql-form :sql [driver hsql-form amount unit]
+  (driver/date-add driver hsql-form amount unit))
+
 (defmulti field->identifier
   "Return a HoneySQL form that should be used as the identifier for `field`, an instance of the Field model. The default
   implementation returns a keyword generated by from the components returned by `field/qualified-name-components`.
@@ -266,8 +291,8 @@
 (defmethod ->honeysql [:sql :+] [driver [_ & args]]
   (if (mbql.u/datetime-arithmetics? args)
     (let [[field & intervals] args]
-      (reduce (fn [result [_ amount unit]]
-                (driver/date-add driver result amount unit))
+      (reduce (fn [hsql-form [_ amount unit]]
+                (add-interval-honeysql-form driver hsql-form amount unit))
               (->honeysql driver field)
               intervals))
     (apply hsql/call :+ (map (partial ->honeysql driver) args))))
@@ -343,8 +368,8 @@
 (defmethod ->honeysql [:sql :relative-datetime]
   [driver [_ amount unit]]
   (date driver unit (if (zero? amount)
-                      (current-datetime-fn driver)
-                      (driver/date-add driver (current-datetime-fn driver) amount unit))))
+                      (current-datetime-honeysql-form driver)
+                      (add-interval-honeysql-form driver (current-datetime-honeysql-form driver) amount unit))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
diff --git a/src/metabase/util/date_2.clj b/src/metabase/util/date_2.clj
index 9eca9c6e161..66122314cad 100644
--- a/src/metabase/util/date_2.clj
+++ b/src/metabase/util/date_2.clj
@@ -115,6 +115,8 @@
     :iso-week-of-year
     :month-of-year
     :quarter-of-year
+    ;; TODO - in this namespace `:year` is something you can both extract and truncate to. In MBQL `:year` is a truncation
+    ;; operation. Maybe we should rename this unit to clear up the potential confusion (?)
     :year})
 
 (def ^:private week-fields*
-- 
GitLab