diff --git a/modules/drivers/athena/test/metabase/test/data/athena.clj b/modules/drivers/athena/test/metabase/test/data/athena.clj index 69444f4ea224bd29879dd3f6c016ec6717200300..19d4fb25fa483832ff2bd9c013f1bbbf9df338e0 100644 --- a/modules/drivers/athena/test/metabase/test/data/athena.clj +++ b/modules/drivers/athena/test/metabase/test/data/athena.clj @@ -194,3 +194,7 @@ (defmethod tx/supports-time-type? :athena [_driver] false) + +(defmethod tx/supports-timestamptz-type? :athena + [_driver] + false) diff --git a/modules/drivers/mongo/test/metabase/test/data/mongo.clj b/modules/drivers/mongo/test/metabase/test/data/mongo.clj index 7c0e342ee5ae478ab366286b783f972124a8cfee..fd6d6ae1f7c9b277d1cdb15dc6b6c5bb6bcee551 100644 --- a/modules/drivers/mongo/test/metabase/test/data/mongo.clj +++ b/modules/drivers/mongo/test/metabase/test/data/mongo.clj @@ -13,6 +13,7 @@ (tx/add-test-extensions! :mongo) (defmethod tx/supports-time-type? :mongo [_driver] false) +(defmethod tx/supports-timestamptz-type? :mongo [_driver] false) (defn ssl-required? "Returns if the mongo server requires an SSL connection." diff --git a/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj b/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj index f9fd1f4a0bcbfe06c8c31ecc1532bd620f910715..6d7da7fcdb13dedf4311dabe9b2401d2eb9ba30c 100644 --- a/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj +++ b/modules/drivers/sqlite/test/metabase/test/data/sqlite.clj @@ -6,6 +6,8 @@ (sql-jdbc.tx/add-test-extensions! :sqlite) +(defmethod tx/supports-timestamptz-type? :sqlite [_driver] false) + (defmethod tx/dbdef->connection-details :sqlite [_driver _context dbdef] {:db (str (tx/escaped-database-name dbdef) ".sqlite")}) diff --git a/src/metabase/driver/h2.clj b/src/metabase/driver/h2.clj index 0a03f2048d3478ea5970708369e0e1cbfce19622..1e75f57a7be55a603e64e6f3940f18d276c9bb3b 100644 --- a/src/metabase/driver/h2.clj +++ b/src/metabase/driver/h2.clj @@ -239,22 +239,24 @@ (defn- format-datetime [format-str expr] (hsql/call :formatdatetime expr (hx/literal format-str))) (defn- parse-datetime [format-str expr] (hsql/call :parsedatetime expr (hx/literal format-str))) (defn- trunc-with-format [format-str expr] (parse-datetime format-str (format-datetime format-str expr))) +(defn- extract [unit expr] (hsql/call :extract unit (hx/cast :timestamp expr))) (defmethod sql.qp/date [:h2 :minute] [_ _ expr] (trunc-with-format "yyyyMMddHHmm" expr)) (defmethod sql.qp/date [:h2 :minute-of-hour] [_ _ expr] (hx/minute expr)) (defmethod sql.qp/date [:h2 :hour] [_ _ expr] (trunc-with-format "yyyyMMddHH" expr)) -(defmethod sql.qp/date [:h2 :hour-of-day] [_ _ expr] (hx/hour expr)) +(defmethod sql.qp/date [:h2 :hour-of-day] [_ _ expr] (extract :hour expr)) (defmethod sql.qp/date [:h2 :day] [_ _ expr] (hx/->date expr)) -(defmethod sql.qp/date [:h2 :day-of-month] [_ _ expr] (hsql/call :day_of_month expr)) -(defmethod sql.qp/date [:h2 :day-of-year] [_ _ expr] (hsql/call :day_of_year expr)) +(defmethod sql.qp/date [:h2 :day-of-month] [_ _ expr] (extract :day expr)) +(defmethod sql.qp/date [:h2 :day-of-year] [_ _ expr] (extract :day_of_year expr)) (defmethod sql.qp/date [:h2 :month] [_ _ expr] (trunc-with-format "yyyyMM" expr)) -(defmethod sql.qp/date [:h2 :month-of-year] [_ _ expr] (hx/month expr)) -(defmethod sql.qp/date [:h2 :quarter-of-year] [_ _ expr] (hx/quarter expr)) -(defmethod sql.qp/date [:h2 :year] [_ _ expr] (parse-datetime "yyyy" (hx/year expr))) +(defmethod sql.qp/date [:h2 :month-of-year] [_ _ expr] (extract :month expr)) +(defmethod sql.qp/date [:h2 :quarter-of-year] [_ _ expr] (extract :quarter expr)) +(defmethod sql.qp/date [:h2 :year] [_ _ expr] (parse-datetime "yyyy" (extract :year expr))) +(defmethod sql.qp/date [:h2 :year-of-era] [_ _ expr] (extract :year expr)) (defmethod sql.qp/date [:h2 :day-of-week] [_ _ expr] - (sql.qp/adjust-day-of-week :h2 (hsql/call :iso_day_of_week expr))) + (sql.qp/adjust-day-of-week :h2 (extract :iso_day_of_week expr))) (defmethod sql.qp/date [:h2 :week] [_ _ expr] @@ -262,7 +264,7 @@ (hx/- 1 (sql.qp/date :h2 :day-of-week expr)) :day)) -(defmethod sql.qp/date [:h2 :week-of-year-iso] [_ _ expr] (hsql/call :iso_week expr)) +(defmethod sql.qp/date [:h2 :week-of-year-iso] [_ _ expr] (extract :iso_week expr)) ;; Rounding dates to quarters is a bit involved but still doable. Here's the plan: ;; * extract the year and quarter from the date; diff --git a/test/metabase/driver/sql/query_processor_test.clj b/test/metabase/driver/sql/query_processor_test.clj index a3fd3e8784b9a7e9f2a14a0a2d224a4431abb30c..8738750bf4ee18ebdf132ab63b4fbd7d41e1e685 100644 --- a/test/metabase/driver/sql/query_processor_test.clj +++ b/test/metabase/driver/sql/query_processor_test.clj @@ -380,7 +380,7 @@ (mt/dataset sample-dataset (is (= '{:select [source.PRODUCTS__via__PRODUCT_ID__CATEGORY AS PRODUCTS__via__PRODUCT_ID__CATEGORY source.PEOPLE__via__USER_ID__SOURCE AS PEOPLE__via__USER_ID__SOURCE - parsedatetime (year (source.CREATED_AT) "yyyy") AS CREATED_AT + parsedatetime (extract (year from CAST (source.CREATED_AT AS timestamp)) "yyyy") AS CREATED_AT source.pivot-grouping AS pivot-grouping count (*) AS count] :from [{:select [ORDERS.ID AS ID @@ -408,16 +408,16 @@ AND (source.PRODUCTS__via__PRODUCT_ID__CATEGORY = ? OR source.PRODUCTS__via__PRODUCT_ID__CATEGORY = ?) AND - source.CREATED_AT >= parsedatetime (year (dateadd ("year" CAST (-2 AS long) now ())) "yyyy") + source.CREATED_AT >= parsedatetime (extract (year from CAST (dateadd ("year" CAST (-2 AS long) now ()) AS timestamp)) "yyyy") AND - source.CREATED_AT < parsedatetime (year (now ()) "yyyy"))] + source.CREATED_AT < parsedatetime (extract (year from CAST (now () AS timestamp)) "yyyy"))] :group-by [source.PRODUCTS__via__PRODUCT_ID__CATEGORY source.PEOPLE__via__USER_ID__SOURCE - parsedatetime (year (source.CREATED_AT) "yyyy") + parsedatetime (extract (year from CAST (source.CREATED_AT AS timestamp)) "yyyy") source.pivot-grouping] :order-by [source.PRODUCTS__via__PRODUCT_ID__CATEGORY ASC source.PEOPLE__via__USER_ID__SOURCE ASC - parsedatetime (year (source.CREATED_AT) "yyyy") ASC + parsedatetime (extract (year from CAST (source.CREATED_AT AS timestamp)) "yyyy") ASC source.pivot-grouping ASC]} (-> (mt/mbql-query orders {:aggregation [[:aggregation-options [:count] {:name "count"}]] diff --git a/test/metabase/query_processor_test/date_time_zone_functions_test.clj b/test/metabase/query_processor_test/date_time_zone_functions_test.clj index f57c070278e8178a70a0198b969d507c3b3de955..e0cbda44d9c63c2e2c152336d4bf080893053c60 100644 --- a/test/metabase/query_processor_test/date_time_zone_functions_test.clj +++ b/test/metabase/query_processor_test/date_time_zone_functions_test.clj @@ -112,7 +112,7 @@ (testing (format "extract %s function works as expected on %s column for driver %s" op col-type driver/*driver*) (is (= (set (expected-fn op)) (set (test-temporal-extract (query-fn op field-id))))))))) - ;; mongo doesn't supports cast string to date + ;; mongo doesn't supports cast string to date (mt/test-drivers (disj (mt/normal-drivers-with-feature :temporal-extract) :mongo) (testing "with date columns" (doseq [[col-type field-id] [[:date (mt/id :times :d)] [:text-as-date (mt/id :times :as_d)]] @@ -140,7 +140,54 @@ :fields (into [] (for [op ops] [:expression (name op)]))}) (mt/formatted-rows (repeat int)) first - (zipmap ops))))))))) + (zipmap ops))))))) + + (testing "with timestamptz columns" + (mt/test-drivers (filter mt/supports-timestamptz-type? (mt/normal-drivers-with-feature :temporal-extract)) + (mt/with-report-timezone-id "Asia/Kabul" + (is (= (if (or (= driver/*driver* :sqlserver) + (driver/supports? driver/*driver* :set-timezone)) + {:get-year 2004, + :get-quarter 1, + :get-month 1, + :get-day 1, + :get-day-of-week 5, + ;; TIMEZONE FIXME these drivers are returning the extracted hours in + ;; the timezone that they were inserted in + ;; maybe they need explicit convert-timezone to the report-tz before extraction? + :get-hour (case driver/*driver* + (:sqlserver :presto :presto-jdbc :snowflake :oracle) 5 + 2), + :get-minute (case driver/*driver* + (:sqlserver :presto :presto-jdbc :snowflake :oracle) 19 + 49), + :get-second 9} + {:get-year 2003, + :get-quarter 4, + :get-month 12, + :get-day 31, + :get-day-of-week 4, + :get-hour 22, + :get-minute 19, + :get-second 9}) + (let [ops [:get-year :get-quarter :get-month :get-day + :get-day-of-week :get-hour :get-minute :get-second]] + (->> (mt/mbql-query times {:expressions (into {"shifted-day" [:datetime-subtract $dt_tz 78 :day] + ;; the idea is to extract a column with value = 2004-01-01 02:49:09 +04:30 + ;; this way the UTC value is 2003-12-31 22:19:09 +00:00 which will make sure + ;; the year, quarter, month, day, week is extracted correctly + ;; TODO: it's better to use a literal for this, but the function is not working properly + ;; with OffsetDatetime for all drivers, so we'll go wit this for now + "shifted-hour" [:datetime-subtract [:expression "shifted-day"] 4 :hour]} + (for [op ops] + [(name op) [op [:expression "shifted-hour"]]])) + :fields (into [] (for [op ops] [:expression (name op)])) + :filter [:= $index 1] + :limit 1}) + mt/process-query + (mt/formatted-rows (repeat int)) + first + (zipmap ops)))))))))) (deftest temporal-extraction-with-filter-expresion-tests (mt/test-drivers (mt/normal-drivers-with-feature :temporal-extract) diff --git a/test/metabase/test.clj b/test/metabase/test.clj index 346fce7299b8a396eff591bbc50c1d9d7a0cd852..1da6de02175cc94ea34656f257fed5ff12a30acb 100644 --- a/test/metabase/test.clj +++ b/test/metabase/test.clj @@ -249,7 +249,8 @@ has-test-extensions? metabase-instance sorts-nil-first? - supports-time-type?] + supports-time-type? + supports-timestamptz-type?] [tx.env set-test-drivers! diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj index e9f42d40b18a5e279a63f22eb89de532d4426c1f..8499166b6760f663f49ce4d518ce109937240006 100644 --- a/test/metabase/test/data/interface.clj +++ b/test/metabase/test/data/interface.clj @@ -364,6 +364,14 @@ (defmethod supports-time-type? ::test-extensions [_driver] true) +(defmulti supports-timestamptz-type? + "Whether this database supports a `timestamp with time zone` data type or equivalent." + {:arglists '([driver])} + dispatch-on-driver-with-test-extensions + :hierarchy #'driver/hierarchy) + +(defmethod supports-timestamptz-type? ::test-extensions [_driver] true) + (defmulti aggregate-column-info "Return the expected type information that should come back for QP results as part of `:cols` for an aggregation of a given type (and applied to a given Field, when applicable)."