Functions for fetching the timezone for the current query.

(ns metabase.query-processor.timezone
  (:require
   [java-time.api :as t]
   [metabase.config :as config]
   [metabase.driver :as driver]
   [metabase.driver.util :as driver.u]
   [metabase.lib.metadata :as lib.metadata]
   [metabase.query-processor.store :as qp.store]
   [metabase.util.log :as log])
  (:import
   (java.time ZonedDateTime)))
(set! *warn-on-reflection* true)
(def ^:private ^:dynamic *report-timezone-id-override* nil)
(def ^:private ^:dynamic *database-timezone-id-override* nil)
(def ^:private ^:dynamic *results-timezone-id-override* nil)

TODO - consider making this metabase.util.date-2/the-timezone-id

(defn- valid-timezone-id [timezone-id]
  (when (and (string? timezone-id)
             (seq timezone-id))
    (try
      (t/zone-id timezone-id)
      timezone-id
      (catch Throwable _
        (log/warnf "Invalid timezone ID '%s'" timezone-id)
        nil))))
(defn- report-timezone-id* []
  (or *report-timezone-id-override*
      (driver/report-timezone)))

+----------------------------------------------------------------------------------------------------------------+ | Public Interface | +----------------------------------------------------------------------------------------------------------------+

Timezone ID for the report timezone, if the current driver and database supports it. (If the current driver supports it, this is bound by the bind-effective-timezone middleware.)

(defn report-timezone-id-if-supported
  (^String []
   (report-timezone-id-if-supported driver/*driver* (lib.metadata/database (qp.store/metadata-provider))))
  (^String [driver database]
   (when (driver.u/supports? driver :set-timezone database)
     (valid-timezone-id (report-timezone-id*)))))

The timezone that the current database is in, as determined by the most recent sync.

(defn database-timezone-id
  (^String []
   (database-timezone-id ::db-from-store))
  (^String [database]
   (valid-timezone-id
    (or *database-timezone-id-override*
        (let [database (if (= database ::db-from-store)
                         (lib.metadata/database (qp.store/metadata-provider))
                         database)]
          (:timezone database))))))

The system timezone of this Metabase instance.

(defn system-timezone-id
  ^String []
  (.. (t/system-clock) getZone getId))

The timezone that we would like to run a query in, regardless of whether we are actually able to do so. This is always equal to the value of the report-timezone Setting (if it is set), otherwise the database timezone (if known), otherwise the system timezone.

(defn requested-timezone-id
  ^String []
  (valid-timezone-id (report-timezone-id*)))

The timezone that a query is actually ran in ­ report timezone, if set and supported by the current driver; otherwise the timezone of the database (if known), otherwise the system timezone. Guaranteed to always return a timezone ID ­ never returns nil.

(defn results-timezone-id
  (^String []
   (results-timezone-id driver/*driver* ::db-from-store))
  (^String [database]
   (results-timezone-id (:engine database) database))
  (^String [driver database & {:keys [use-report-timezone-id-if-unsupported?]
                               :or   {use-report-timezone-id-if-unsupported? false}}]
   (valid-timezone-id
    (or *results-timezone-id-override*
        (if use-report-timezone-id-if-unsupported?
          (valid-timezone-id (report-timezone-id*))
          (report-timezone-id-if-supported driver database))
        ;; don't actually fetch DB from store unless needed — that way if `*results-timezone-id-override*` is set we
        ;; don't need to init a store during tests
        (database-timezone-id database)
        ;; NOTE: if we don't have an explicit report-timezone then use the JVM timezone
        ;;       this ensures alignment between the way dates are processed by JDBC and our returned data
        ;;       GH issues: #2282, #2035
        (system-timezone-id)))))

Get the current moment in time adjusted to the results timezone ID, e.g. for relative datetime calculations.

(def ^ZonedDateTime now
  (comp (fn [timezone-id]
          (t/with-zone-same-instant (t/zoned-date-time) (t/zone-id timezone-id)))
        results-timezone-id))

normally I'd do this inline with the def form above but it busts Eastwood

(when config/is-dev?
  (alter-meta! #'now assoc :arglists (:arglists (meta #'results-timezone-id))))