From 05b33c2e3ad67e3d9d4b4e612dc3ec2ac8f1ad2b Mon Sep 17 00:00:00 2001
From: metamben <103100869+metamben@users.noreply.github.com>
Date: Thu, 4 Aug 2022 22:56:56 +0300
Subject: [PATCH] Set timezone when truncating dates (#24247)

Breaking date values into parts should happen in the time zone expected by the user and the construction of the
truncated date should happen in the same timezone. This is especially important when the difference between the
expected timezone and UTC is not an integer number of hours and the truncation resolution is hour. (See #11149).
---
 ...ne-mongo-replace-missing-values.cy.spec.js |  4 +-
 .../metabase/driver/mongo/query_processor.clj | 18 ++++---
 .../driver/mongo/query_processor_test.clj     | 52 ++++++++++++++++---
 3 files changed, 56 insertions(+), 18 deletions(-)

diff --git a/frontend/test/metabase/scenarios/visualizations/reproductions/16170-line-mongo-replace-missing-values.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/reproductions/16170-line-mongo-replace-missing-values.cy.spec.js
index 890f210ed92..0fd028b0bc2 100644
--- a/frontend/test/metabase/scenarios/visualizations/reproductions/16170-line-mongo-replace-missing-values.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/reproductions/16170-line-mongo-replace-missing-values.cy.spec.js
@@ -34,8 +34,8 @@ describe("issue 16170", () => {
       cy.get(".dot").eq(-2).trigger("mousemove", { force: true });
 
       popover().within(() => {
-        testPairedTooltipValues("Created At", "2018");
-        testPairedTooltipValues("Count", "6,578");
+        testPairedTooltipValues("Created At", "2019");
+        testPairedTooltipValues("Count", "6,524");
       });
     });
   });
diff --git a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
index a9aac397543..533904c435d 100644
--- a/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
+++ b/modules/drivers/mongo/src/metabase/driver/mongo/query_processor.clj
@@ -123,13 +123,13 @@
   (let [field-name (str \$ (field->name field "."))]
     (cond
       (isa? coercion :Coercion/UNIXMicroSeconds->DateTime)
-      {:$dateFromParts {:millisecond {$divide [field-name 1000]}, :year 1970}}
+      {:$dateFromParts {:millisecond {$divide [field-name 1000]}, :year 1970, :timezone "UTC"}}
 
       (isa? coercion :Coercion/UNIXMilliSeconds->DateTime)
-      {:$dateFromParts {:millisecond field-name, :year 1970}}
+      {:$dateFromParts {:millisecond field-name, :year 1970, :timezone "UTC"}}
 
       (isa? coercion :Coercion/UNIXSeconds->DateTime)
-      {:$dateFromParts {:second field-name, :year 1970}}
+      {:$dateFromParts {:second field-name, :year 1970, :timezone "UTC"}}
 
       (isa? coercion :Coercion/YYYYMMDDHHMMSSString->Temporal)
       {"$dateFromString" {:dateString field-name
@@ -181,11 +181,13 @@
                           (* 24 60 60 1000)]}]})
 
 (defn- truncate-to-resolution [column resolution]
-  (mongo-let [parts {:$dateToParts {:date column}}]
-    {:$dateFromParts (into {} (for [part (concat (take-while (partial not= resolution)
-                                                             [:year :month :day :hour :minute :second :millisecond])
-                                                 [resolution])]
-                                [part (str (name parts) \. (name part))]))}))
+  (mongo-let [parts {:$dateToParts {:timezone (qp.timezone/results-timezone-id)
+                                    :date column}}]
+    {:$dateFromParts (into {:timezone (qp.timezone/results-timezone-id)}
+                           (for [part (concat (take-while (partial not= resolution)
+                                                          [:year :month :day :hour :minute :second :millisecond])
+                                              [resolution])]
+                             [part (str (name parts) \. (name part))]))}))
 
 (defn- with-rvalue-temporal-bucketing
   [field unit]
diff --git a/modules/drivers/mongo/test/metabase/driver/mongo/query_processor_test.clj b/modules/drivers/mongo/test/metabase/driver/mongo/query_processor_test.clj
index 73e00722849..d72446e2973 100644
--- a/modules/drivers/mongo/test/metabase/driver/mongo/query_processor_test.clj
+++ b/modules/drivers/mongo/test/metabase/driver/mongo/query_processor_test.clj
@@ -5,6 +5,7 @@
             [metabase.driver.mongo.query-processor :as mongo.qp]
             [metabase.models :refer [Field Table]]
             [metabase.query-processor :as qp]
+            [metabase.query-processor.timezone :as qp.timezone]
             [metabase.test :as mt]
             [metabase.util :as u]
             [schema.core :as s]
@@ -87,16 +88,22 @@
                         :query       [{"$match"
                                        {:$expr
                                         {"$eq"
-                                         [{:$let {:vars {:parts {:$dateToParts {:date "$datetime"}}}
-                                                  :in   {:$dateFromParts {:year "$$parts.year", :month "$$parts.month"}}}}
+                                         [{:$let {:vars {:parts {:$dateToParts {:date "$datetime"
+                                                                                :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}
+                                                  :in   {:$dateFromParts {:year "$$parts.year", :month "$$parts.month"
+                                                                                :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}}
                                           {:$dateFromString {:dateString "2021-01-01T00:00Z"}}]}}}
-                                      {"$group" {"_id"   {"datetime~~~month" {:$let {:vars {:parts {:$dateToParts {:date "$datetime"}}}
+                                      {"$group" {"_id"   {"datetime~~~month" {:$let {:vars {:parts {:$dateToParts {:date "$datetime"
+                                                                                                                   :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}
                                                                                      :in   {:$dateFromParts {:year  "$$parts.year"
-                                                                                                             :month "$$parts.month"}}}},
-                                                          "datetime~~~day"   {:$let {:vars {:parts {:$dateToParts {:date "$datetime"}}}
+                                                                                                             :month "$$parts.month"
+                                                                                                             :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}},
+                                                          "datetime~~~day"   {:$let {:vars {:parts {:$dateToParts {:date "$datetime"
+                                                                                                                   :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}
                                                                                      :in   {:$dateFromParts {:year  "$$parts.year"
                                                                                                              :month "$$parts.month"
-                                                                                                             :day   "$$parts.day"}}}}}
+                                                                                                             :day   "$$parts.day"
+                                                                                                             :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}}}
                                                  "count" {"$sum" 1}}}
                                       {"$sort" {"_id" 1}}
                                       {"$project" {"_id"              false
@@ -111,6 +118,33 @@
                                 s/Keyword s/Any}
                                (qp/process-query (mt/native-query query)))))))))))))
 
+(deftest grouping-with-timezone-test
+  (mt/test-driver :mongo
+    (testing "Result timezone is respected when grouping by hour (#11149)"
+      (mt/dataset attempted-murders
+        (testing "Querying in UTC works"
+          (mt/with-system-timezone-id "UTC"
+            (is (= [["2019-11-20T20:00:00Z" 1]
+                    ["2019-11-19T00:00:00Z" 1]
+                    ["2019-11-18T20:00:00Z" 1]
+                    ["2019-11-17T14:00:00Z" 1]]
+                   (mt/rows (mt/run-mbql-query attempts
+                              {:aggregation [[:count]]
+                               :breakout [[:field %datetime {:temporal-unit :hour}]]
+                               :order-by [[:desc [:field %datetime {:temporal-unit :hour}]]]
+                               :limit 4}))))))
+        (testing "Querying in Kathmandu works"
+          (mt/with-system-timezone-id "Asia/Kathmandu"
+            (is (= [["2019-11-21T01:00:00+05:45" 1]
+                    ["2019-11-19T06:00:00+05:45" 1]
+                    ["2019-11-19T02:00:00+05:45" 1]
+                    ["2019-11-17T19:00:00+05:45" 1]]
+                   (mt/rows (mt/run-mbql-query attempts
+                              {:aggregation [[:count]]
+                               :breakout [[:field %datetime {:temporal-unit :hour}]]
+                               :order-by [[:desc [:field %datetime {:temporal-unit :hour}]]]
+                               :limit 4}))))))))))
+
 (deftest nested-columns-test
   (mt/test-driver :mongo
     (testing "Should generate correct queries against nested columns"
@@ -262,8 +296,10 @@
                    {"_id"
                     {"date~~~day"
                      {:$let
-                      {:vars {:parts {:$dateToParts {:date "$date"}}},
-                       :in   {:$dateFromParts {:year "$$parts.year", :month "$$parts.month", :day "$$parts.day"}}}}}}}
+                      {:vars {:parts {:$dateToParts {:date "$date"
+                                                     :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}},
+                       :in   {:$dateFromParts {:year "$$parts.year", :month "$$parts.month", :day "$$parts.day"
+                                               :timezone (qp.timezone/results-timezone-id :mongo mt/db)}}}}}}}
                   {"$sort" {"_id" 1}}
                   {"$project" {"_id" false, "date~~~day" "$_id.date~~~day"}}
                   {"$sort" {"date~~~day" 1}}
-- 
GitLab