From fa10fa24ec9a9d1c78b0367eaf2b833731606c51 Mon Sep 17 00:00:00 2001
From: Ryan Senior <ryan@metabase.com>
Date: Fri, 4 May 2018 10:00:09 -0500
Subject: [PATCH] Move date related functions from `util` to `util.date`

---
 .../metabase/sample_dataset/generate.clj      |  13 +-
 src/metabase/api/dataset.clj                  |   3 +-
 src/metabase/cmd.clj                          |   5 +-
 src/metabase/db/migrations.clj                |   3 +-
 src/metabase/driver.clj                       |   5 +-
 src/metabase/driver/bigquery.clj              |  13 +-
 src/metabase/driver/crate/util.clj            |   6 +-
 src/metabase/driver/druid/query_processor.clj |   7 +-
 .../driver/generic_sql/query_processor.clj    |   6 +-
 .../driver/generic_sql/util/unprepare.clj     |   6 +-
 .../googleanalytics/query_processor.clj       |  15 +-
 src/metabase/driver/mongo/query_processor.clj |  17 +-
 src/metabase/driver/mysql.clj                 |   3 +-
 src/metabase/driver/presto.clj                |  17 +-
 src/metabase/driver/sqlite.clj                |   8 +-
 src/metabase/driver/vertica.clj               |   3 +-
 src/metabase/email/messages.clj               |   5 +-
 src/metabase/events/last_login.clj            |   3 +-
 src/metabase/feature_extraction/async.clj     |   5 +-
 src/metabase/middleware.clj                   |   5 +-
 src/metabase/models/activity.clj              |   3 +-
 src/metabase/models/collection_revision.clj   |   3 +-
 src/metabase/models/dependency.clj            |   4 +-
 src/metabase/models/interface.clj             |   5 +-
 src/metabase/models/permissions_revision.clj  |   3 +-
 src/metabase/models/revision.clj              |   3 +-
 src/metabase/models/session.clj               |   3 +-
 src/metabase/models/user.clj                  |   3 +-
 src/metabase/models/view_log.clj              |   3 +-
 src/metabase/pulse/render.clj                 |   9 +-
 src/metabase/query_processor.clj              |   6 +-
 src/metabase/query_processor/interface.clj    |   8 +-
 .../query_processor/middleware/cache.clj      |   5 +-
 .../middleware/cache_backend/db.clj           |  12 +-
 .../query_processor/middleware/expand.clj     |   8 +-
 .../middleware/format_rows.clj                |  10 +-
 .../middleware/parameters/sql.clj             |  14 +-
 .../query_processor/middleware/resolve.clj    |   7 +-
 src/metabase/sync/analyze.clj                 |   3 +-
 .../sync/analyze/fingerprint/datetime.clj     |   3 +-
 src/metabase/sync/util.clj                    |   3 +-
 src/metabase/util.clj                         | 345 -----------------
 src/metabase/util/date.clj                    | 355 ++++++++++++++++++
 test/metabase/api/activity_test.clj           |   9 +-
 test/metabase/api/card_test.clj               |  11 +-
 test/metabase/driver/googleanalytics_test.clj |  21 +-
 test/metabase/driver/mysql_test.clj           |  16 +-
 test/metabase/http_client.clj                 |   5 +-
 test/metabase/middleware_test.clj             |   5 +-
 test/metabase/models/dependency_test.clj      |   7 +-
 test/metabase/models/session_test.clj         |  11 +-
 .../query_processor/expand_resolve_test.clj   |   5 +-
 .../middleware/fetch_source_query_test.clj    |   5 +-
 .../middleware/parameters/mbql_test.clj       |  11 +-
 test/metabase/sync/analyze/classify_test.clj  |   5 +-
 .../sync/analyze/fingerprint_test.clj         |   3 +-
 test/metabase/sync/analyze_test.clj           |  11 +-
 test/metabase/task/sync_databases_test.clj    |   3 +-
 test/metabase/test/data/bigquery.clj          |   3 +-
 test/metabase/test/data/generic_sql.clj       |   8 +-
 test/metabase/test/data/sqlite.clj            |   8 +-
 test/metabase/util/date_test.clj              |  85 +++++
 test/metabase/util_test.clj                   |  83 ----
 63 files changed, 680 insertions(+), 601 deletions(-)
 create mode 100644 src/metabase/util/date.clj
 create mode 100644 test/metabase/util/date_test.clj

diff --git a/sample_dataset/metabase/sample_dataset/generate.clj b/sample_dataset/metabase/sample_dataset/generate.clj
index ad8b6a82531..20da4b57e71 100644
--- a/sample_dataset/metabase/sample_dataset/generate.clj
+++ b/sample_dataset/metabase/sample_dataset/generate.clj
@@ -16,7 +16,8 @@
             [jdistlib.core :as dist]
             [medley.core :as m]
             [metabase.db.spec :as dbspec]
-            [metabase.util :as u])
+            [metabase.util :as u]
+            [metabase.util.date :as du])
   (:import java.util.Date))
 
 (def ^:private ^:const sample-dataset-filename
@@ -67,7 +68,7 @@
     {:name       (format "%s %s" first last)
      :email      (internet/free-email (format "%s.%s" first last))
      :password   (str (java.util.UUID/randomUUID))
-     :birth_date (random-date-between (u/relative-date :year -60) (u/relative-date :year -18))
+     :birth_date (random-date-between (du/relative-date :year -60) (du/relative-date :year -18))
      :address    (str (:house-number addr) " " (:street addr))
      :city       (:city addr)
      :zip        (:zip addr)
@@ -75,7 +76,7 @@
      :latitude   (:lat addr)
      :longitude  (:lon addr)
      :source     (rand-nth ["Google" "Twitter" "Facebook" "Organic" "Affiliate"])
-     :created_at (random-date-between (u/relative-date :year -2) (u/relative-date :year 1))}))
+     :created_at (random-date-between (du/relative-date :year -2) (du/relative-date :year 1))}))
 
 ;;; ## PRODUCTS
 
@@ -141,7 +142,7 @@
    :category   (rand-nth ["Widget" "Gizmo" "Gadget" "Doohickey"])
    :vendor     (random-company-name)
    :price      (random-price 12 100)
-   :created_at (random-date-between (u/relative-date :year -2) (u/relative-date :year 1))})
+   :created_at (random-date-between (du/relative-date :year -2) (du/relative-date :year 1))})
 
 
 ;;; ## ORDERS
@@ -240,7 +241,7 @@
                      (format "No tax rate found for state '%s'." state))
         created-at (random-date-between (min-date (:created_at person)
                                                   (:created_at product))
-                                        (u/relative-date :year 2))
+                                        (du/relative-date :year 2))
         price      (if (> (.getTime created-at) (.getTime (Date. 118 0 1)))
                      (* 1.5 price)
                      price)
@@ -266,7 +267,7 @@
                           4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4
                           5 5 5 5 5 5 5 5 5 5 5 5 5])
    :body       (first (lorem/paragraphs))
-   :created_at (random-date-between (:created_at product) (u/relative-date :year 2))})
+   :created_at (random-date-between (:created_at product) (du/relative-date :year 2))})
 
 (defn- add-ids [objs]
   (map-indexed
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index 4ead2c8ee2b..6c3c1ca6a10 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -18,6 +18,7 @@
             [metabase.query-processor.util :as qputil]
             [metabase.util :as util]
             [metabase.util
+             [date :as du]
              [export :as ex]
              [schema :as su]]
             [puppetlabs.i18n.core :refer [trs tru]]
@@ -108,7 +109,7 @@
       {:status  200
        :body    ((:export-fn export-conf) columns (maybe-modify-date-values cols rows))
        :headers {"Content-Type"        (str (:content-type export-conf) "; charset=utf-8")
-                 "Content-Disposition" (str "attachment; filename=\"query_result_" (u/date->iso-8601) "." (:ext export-conf) "\"")}}
+                 "Content-Disposition" (str "attachment; filename=\"query_result_" (du/date->iso-8601) "." (:ext export-conf) "\"")}}
       ;; failed query, send error message
       {:status 500
        :body   (:error response)})))
diff --git a/src/metabase/cmd.clj b/src/metabase/cmd.clj
index 0efcf920bfa..4dd3dcaadbc 100644
--- a/src/metabase/cmd.clj
+++ b/src/metabase/cmd.clj
@@ -19,7 +19,8 @@
             [metabase
              [config :as config]
              [db :as mdb]
-             [util :as u]]))
+             [util :as u]]
+            [metabase.util.date :as du]))
 
 (defn ^:command migrate
   "Run database migrations. Valid options for DIRECTION are `up`, `force`, `down-one`, `print`, or `release-locks`."
@@ -41,7 +42,7 @@
   ;; override env var that would normally make Jetty block forever
   (require 'environ.core)
   (intern 'environ.core 'env (assoc @(resolve 'environ.core/env) :mb-jetty-join "false"))
-  (u/profile "start-normally" ((resolve 'metabase.core/start-normally))))
+  (du/profile "start-normally" ((resolve 'metabase.core/start-normally))))
 
 (defn ^:command reset-password
   "Reset the password for a user with EMAIL-ADDRESS."
diff --git a/src/metabase/db/migrations.clj b/src/metabase/db/migrations.clj
index 69d3813bf16..afe2b56b9d2 100644
--- a/src/metabase/db/migrations.clj
+++ b/src/metabase/db/migrations.clj
@@ -31,6 +31,7 @@
              [table :as table :refer [Table]]
              [user :refer [User]]]
             [metabase.query-processor.util :as qputil]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -51,7 +52,7 @@
       (@migration-var)
       (db/insert! DataMigrations
         :id        migration-name
-        :timestamp (u/new-sql-timestamp)))))
+        :timestamp (du/new-sql-timestamp)))))
 
 (def ^:private data-migrations (atom []))
 
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 36dcbe3e927..7cc70b1f659 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -24,6 +24,7 @@
              table]
             [metabase.sync.interface :as si]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [puppetlabs.i18n.core :refer [tru]]
             [schema.core :as s]
             [puppetlabs.i18n.core :refer [trs tru]]
@@ -73,7 +74,7 @@
 
   (date-interval [this, ^Keyword unit, ^Number amount]
     "*OPTIONAL* Return an driver-appropriate representation of a moment relative to the current moment in time. By
-     default, this returns an `Timestamp` by calling `metabase.util/relative-date`; but when possible drivers should
+     default, this returns an `Timestamp` by calling `metabase.util.date/relative-date`; but when possible drivers should
      return a native form so we can be sure the correct timezone is applied. For example, SQL drivers should return a
      HoneySQL form to call the appropriate SQL fns:
 
@@ -235,7 +236,7 @@
 
 (def IDriverDefaultsMixin
   "Default implementations of `IDriver` methods marked *OPTIONAL*."
-  {:date-interval                     (u/drop-first-arg u/relative-date)
+  {:date-interval                     (u/drop-first-arg du/relative-date)
    :describe-table-fks                (constantly nil)
    :features                          (constantly nil)
    :format-custom-field-name          (u/drop-first-arg identity)
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 3a1d012cac1..634a907af09 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -15,6 +15,7 @@
              [config :as config]
              [driver :as driver]
              [util :as u]]
+            [metabase.util.date :as du]
             [metabase.driver
              [generic-sql :as sql]
              [google :as google]]
@@ -153,15 +154,15 @@
 (defn- parse-timestamp-str [s]
   ;; Timestamp strings either come back as ISO-8601 strings or Unix timestamps in µs, e.g. "1.3963104E9"
   (or
-   (u/->Timestamp s)
+   (du/->Timestamp s)
    ;; If parsing as ISO-8601 fails parse as a double then convert to ms. Add the appropriate number of milliseconds to
    ;; the number to convert it to the local timezone. We do this because the dates come back in UTC but we want the
    ;; grouping to match the local time (HUH?) This gives us the same results as the other
    ;; `has-questionable-timezone-support?` drivers. Not sure if this is actually desirable, but if it's not, it
    ;; probably means all of those other drivers are doing it wrong
-   (u/->Timestamp (- (* (Double/parseDouble s) 1000)
-                     (.getDSTSavings default-timezone)
-                     (.getRawOffset  default-timezone)))))
+   (du/->Timestamp (- (* (Double/parseDouble s) 1000)
+                      (.getDSTSavings default-timezone)
+                      (.getRawOffset  default-timezone)))))
 
 (def ^:private bigquery-time-format (tformat/formatter "HH:mm:SS" time/utc))
 
@@ -377,7 +378,7 @@
 
 (defmethod sqlqp/->honeysql [BigQueryDriver Date]
   [_ date]
-  (hsql/call :timestamp (hx/literal (u/date->iso-8601 date))))
+  (hsql/call :timestamp (hx/literal (du/date->iso-8601 date))))
 
 (defmethod sqlqp/->honeysql [BigQueryDriver TimeValue]
   [driver {:keys [value]}]
@@ -452,7 +453,7 @@
   (hsql/call :length field-key))
 
 (defn- date-interval [driver unit amount]
-  (sqlqp/->honeysql driver (u/relative-date unit amount)))
+  (sqlqp/->honeysql driver (du/relative-date unit amount)))
 
 
 ;; BigQuery doesn't return a timezone with it's time strings as it's always UTC, JodaTime parsing also defaults to UTC
diff --git a/src/metabase/driver/crate/util.clj b/src/metabase/driver/crate/util.clj
index 8d9c014cfaa..ebcd933ec88 100644
--- a/src/metabase/driver/crate/util.clj
+++ b/src/metabase/driver/crate/util.clj
@@ -4,7 +4,9 @@
                       [format :as hformat])
             [metabase.driver.generic-sql.query-processor :as qp]
             [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import java.sql.Timestamp))
 
 ;; register the try_cast function with HoneySQL
@@ -59,7 +61,7 @@
   "ISQLDriver `date` implementation"
   [_ unit expr]
   (let [v (if (instance? Timestamp expr)
-            (hx/literal (u/date->iso-8601 expr))
+            (hx/literal (du/date->iso-8601 expr))
             expr)]
     (case unit
       :default         (date-format (str "%Y-%m-%d %H:%i:%s") v)
diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj
index 3a4de54aea4..d46d0c40698 100644
--- a/src/metabase/driver/druid/query_processor.clj
+++ b/src/metabase/driver/druid/query_processor.clj
@@ -12,7 +12,8 @@
             [metabase.query-processor
              [annotate :as annotate]
              [interface :as i]]
-            [metabase.util :as u])
+            [metabase.util :as u]
+            [metabase.util.date :as du])
   (:import java.util.TimeZone
            [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Expression Field
             RelativeDateTimeValue Value]
@@ -64,8 +65,8 @@
   Field                 (->rvalue [this] (:field-name this))
   DateTimeField         (->rvalue [this] (->rvalue (:field this)))
   Value                 (->rvalue [this] (:value this))
-  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (u/date->iso-8601 (u/date-trunc unit value (get-timezone-id))))
-  RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (u/date->iso-8601 (u/date-trunc unit (u/relative-date unit amount) (get-timezone-id)))))
+  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (du/date->iso-8601 (du/date-trunc unit value (get-timezone-id))))
+  RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (du/date->iso-8601 (du/date-trunc unit (du/relative-date unit amount) (get-timezone-id)))))
 
 (defprotocol ^:private IDimensionOrMetric
   (^:private dimension-or-metric? [this]
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index d4d823418ce..409bfaa0c00 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -14,7 +14,9 @@
              [annotate :as annotate]
              [interface :as i]
              [util :as qputil]]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import clojure.lang.Keyword
            [java.sql PreparedStatement ResultSet ResultSetMetaData SQLException]
            [java.util Calendar Date TimeZone]
@@ -383,7 +385,7 @@
   land"
   [^TimeZone tz ^ResultSet rs ^Integer i]
   (let [date-string (.getString rs i)]
-    (if-let [parsed-date (u/str->date-time tz date-string)]
+    (if-let [parsed-date (du/str->date-time date-string tz)]
       parsed-date
       (throw (Exception. (format "Unable to parse date '%s'" date-string))))))
 
diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj
index 55b028dca91..1a3be243d6d 100644
--- a/src/metabase/driver/generic_sql/util/unprepare.clj
+++ b/src/metabase/driver/generic_sql/util/unprepare.clj
@@ -3,7 +3,9 @@
   (:require [clojure.string :as str]
             [honeysql.core :as hsql]
             [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import java.sql.Time
            java.util.Date))
 
@@ -11,7 +13,7 @@
   (^:private unprepare-arg ^String [this settings]))
 
 (defn- unprepare-date [date-or-time iso-8601-fn]
-  (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 date-or-time))))
+  (hsql/call iso-8601-fn (hx/literal (du/date->iso-8601 date-or-time))))
 
 (extend-protocol IUnprepare
   nil     (unprepare-arg [this _] "NULL")
diff --git a/src/metabase/driver/googleanalytics/query_processor.clj b/src/metabase/driver/googleanalytics/query_processor.clj
index 41e95cf52b8..07c6aaca94f 100644
--- a/src/metabase/driver/googleanalytics/query_processor.clj
+++ b/src/metabase/driver/googleanalytics/query_processor.clj
@@ -5,7 +5,8 @@
             [clojure.tools.reader.edn :as edn]
             [medley.core :as m]
             [metabase.query-processor.util :as qputil]
-            [metabase.util :as u])
+            [metabase.util :as u]
+            [metabase.util.date :as du])
   (:import [com.google.api.services.analytics.model GaData GaData$ColumnHeaders]
            [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Field RelativeDateTimeValue Value]))
 
@@ -33,13 +34,13 @@
   Field                 (->rvalue [this] (:field-name this))
   DateTimeField         (->rvalue [this] (->rvalue (:field this)))
   Value                 (->rvalue [this] (:value this))
-  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (u/format-date "yyyy-MM-dd" (u/date-trunc unit value)))
+  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (du/format-date "yyyy-MM-dd" (du/date-trunc unit value)))
   RelativeDateTimeValue (->rvalue [{:keys [unit amount]}]
                           (cond
                             (and (= unit :day) (= amount 0))  "today"
                             (and (= unit :day) (= amount -1)) "yesterday"
                             (and (= unit :day) (< amount -1)) (str (- amount) "daysAgo")
-                            :else                             (u/format-date "yyyy-MM-dd" (u/date-trunc unit (u/relative-date unit amount))))))
+                            :else                             (du/format-date "yyyy-MM-dd" (du/date-trunc unit (du/relative-date unit amount))))))
 
 
 (defn- char-escape-map
@@ -206,14 +207,14 @@
 
 (def ^:private ga-dimension->date-format-fn
   {"ga:minute"         parse-number
-   "ga:dateHour"       (partial u/parse-date "yyyyMMddHH")
+   "ga:dateHour"       (partial du/parse-date "yyyyMMddHH")
    "ga:hour"           parse-number
-   "ga:date"           (partial u/parse-date "yyyyMMdd")
+   "ga:date"           (partial du/parse-date "yyyyMMdd")
    "ga:dayOfWeek"      (comp inc parse-number)
    "ga:day"            parse-number
-   "ga:isoYearIsoWeek" (partial u/parse-date "YYYYww")
+   "ga:isoYearIsoWeek" (partial du/parse-date "YYYYww")
    "ga:week"           parse-number
-   "ga:yearMonth"      (partial u/parse-date "yyyyMM")
+   "ga:yearMonth"      (partial du/parse-date "yyyyMM")
    "ga:month"          parse-number
    "ga:year"           parse-number})
 
diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 7ec04960d42..dbf43bac3c1 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -13,6 +13,7 @@
              [annotate :as annotate]
              [interface :as i]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [monger joda-time
              [collection :as mc]
              [operators :refer :all]])
@@ -181,10 +182,10 @@
                       ([format-string]
                        (stringify format-string value))
                       ([format-string v]
-                       {:___date (u/format-date format-string v)}))
-          extract   (u/rpartial u/date-extract value)]
+                       {:___date (du/format-date format-string v)}))
+          extract   (u/rpartial du/date-extract value)]
       (case (or unit :default)
-        :default         (some-> value u/->Date)
+        :default         (some-> value du/->Date)
         :minute          (stringify "yyyy-MM-dd'T'HH:mm:00")
         :minute-of-hour  (extract :minute)
         :hour            (stringify "yyyy-MM-dd'T'HH:00:00")
@@ -193,17 +194,17 @@
         :day-of-week     (extract :day-of-week)
         :day-of-month    (extract :day-of-month)
         :day-of-year     (extract :day-of-year)
-        :week            (stringify "yyyy-MM-dd" (u/date-trunc :week value))
+        :week            (stringify "yyyy-MM-dd" (du/date-trunc :week value))
         :week-of-year    (extract :week-of-year)
         :month           (stringify "yyyy-MM")
         :month-of-year   (extract :month)
-        :quarter         (stringify "yyyy-MM" (u/date-trunc :quarter value))
+        :quarter         (stringify "yyyy-MM" (du/date-trunc :quarter value))
         :quarter-of-year (extract :quarter-of-year)
         :year            (extract :year))))
 
   RelativeDateTimeValue
   (->rvalue [{:keys [amount unit field]}]
-    (->rvalue (i/map->DateTimeValue {:value (u/relative-date (or unit :day) amount)
+    (->rvalue (i/map->DateTimeValue {:value (du/relative-date (or unit :day) amount)
                                      :field field}))))
 
 
@@ -416,7 +417,7 @@
     (into {} (for [[k v] row]
                {k (if (and (map? v)
                            (:___date v))
-                    (u/->Timestamp (:___date v))
+                    (du/->Timestamp (:___date v))
                     v)}))))
 
 
@@ -439,7 +440,7 @@
    ;; it looks like Date() just ignores any arguments return a date string formatted the same way the Mongo console
    ;; does
    :Date       (fn [& _]
-                 (u/format-date "EEE MMM dd yyyy HH:mm:ss z"))
+                 (du/format-date "EEE MMM dd yyyy HH:mm:ss z"))
    :NumberLong (fn [^String s]
                  (Long/parseLong s))
    :NumberInt  (fn [^String s]
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index fcc46e09633..32df4c94906 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -15,6 +15,7 @@
             [metabase.driver.generic-sql :as sql]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
             [metabase.util
+             [date :as du]
              [honeysql-extensions :as hx]
              [ssh :as ssh]])
   (:import java.sql.Time
@@ -149,7 +150,7 @@
       ;; preferable to have timezones slightly wrong in these rare theoretical situations, instead of all the time, as
       ;; was the previous behavior.
       (hsql/call :convert_tz
-        (hx/literal (u/format-date :date-hour-minute-second-ms date))
+        (hx/literal (du/format-date :date-hour-minute-second-ms date))
         (hx/literal system-timezone-offset-str)
         (hx/literal report-timezone-offset-str))
       ;; otherwise if we don't have a report timezone we can continue to pass the object as-is, e.g. as a prepared
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
index eef652b53c9..7482002e05e 100644
--- a/src/metabase/driver/presto.clj
+++ b/src/metabase/driver/presto.clj
@@ -20,6 +20,7 @@
             [metabase.driver.generic-sql.util.unprepare :as unprepare]
             [metabase.query-processor.util :as qputil]
             [metabase.util
+             [date :as du]
              [honeysql-extensions :as hx]
              [ssh :as ssh]])
   (:import java.sql.Time
@@ -48,16 +49,16 @@
 
 (defn- parse-time-with-tz [s]
   ;; Try parsing with offset first then with full ZoneId
-  (or (u/ignore-exceptions (u/parse-date "HH:mm:ss.SSS ZZ" s))
-      (u/parse-date "HH:mm:ss.SSS ZZZ" s)))
+  (or (u/ignore-exceptions (du/parse-date "HH:mm:ss.SSS ZZ" s))
+      (du/parse-date "HH:mm:ss.SSS ZZZ" s)))
 
 (defn- parse-timestamp-with-tz [s]
   ;; Try parsing with offset first then with full ZoneId
-  (or (u/ignore-exceptions (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s))
-      (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s)))
+  (or (u/ignore-exceptions (du/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s))
+      (du/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s)))
 
 (def ^:private presto-date-time-formatter
-  (u/->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
+  (du/->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
 
 (defn- parse-presto-time
   "Parsing time from presto using a specific formatter rather than the
@@ -65,7 +66,7 @@
   performance is important"
   [time-str]
   (->> time-str
-       (u/parse-date :hour-minute-second-ms)
+       (du/parse-date :hour-minute-second-ms)
        tcoerce/to-long
        Time.))
 
@@ -74,7 +75,7 @@
     #"decimal.*"                bigdec
     #"time"                     parse-presto-time
     #"time with time zone"      parse-time-with-tz
-    #"timestamp"                (partial u/parse-date
+    #"timestamp"                (partial du/parse-date
                                          (if-let [report-tz (and report-timezone
                                                                  (time/time-zone-for-id report-timezone))]
                                            (tformat/with-zone presto-date-time-formatter report-tz)
@@ -215,7 +216,7 @@
 
 (defmethod sqlqp/->honeysql [PrestoDriver Date]
   [_ date]
-  (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 date))))
+  (hsql/call :from_iso8601_timestamp (hx/literal (du/date->iso-8601 date))))
 
 (def ^:private time-format (tformat/formatter "HH:mm:SS.SSS"))
 
diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj
index 557d6adc5fd..5d830f2e549 100644
--- a/src/metabase/driver/sqlite.clj
+++ b/src/metabase/driver/sqlite.clj
@@ -12,7 +12,9 @@
              [util :as u]]
             [metabase.driver.generic-sql :as sql]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import [java.sql Time Timestamp]))
 
 (defrecord SQLiteDriver []
@@ -67,7 +69,7 @@
   [unit expr]
   ;; Convert Timestamps to ISO 8601 strings before passing to SQLite, otherwise they don't seem to work correctly
   (let [v (if (instance? Timestamp expr)
-            (hx/literal (u/date->iso-8601 expr))
+            (hx/literal (du/date->iso-8601 expr))
             expr)]
     (case unit
       :default         v
@@ -147,7 +149,7 @@
     ;; for anything that's a Date (usually a java.sql.Timestamp) convert it to a yyyy-MM-dd formatted date literal
     ;; string For whatever reason the SQL generated from parameters ends up looking like `WHERE date(some_field) = ?`
     ;; sometimes so we need to use just the date rather than a full ISO-8601 string
-    (u/format-date "yyyy-MM-dd" obj)
+    (du/format-date "yyyy-MM-dd" obj)
     ;; every other prepared statement arg can be returned as-is
     obj))
 
diff --git a/src/metabase/driver/vertica.clj b/src/metabase/driver/vertica.clj
index 4e1d34addd9..f64c0dab0f0 100644
--- a/src/metabase/driver/vertica.clj
+++ b/src/metabase/driver/vertica.clj
@@ -8,6 +8,7 @@
              [util :as u]]
             [metabase.driver.generic-sql :as sql]
             [metabase.util
+             [date :as du]
              [honeysql-extensions :as hx]
              [ssh :as ssh]]))
 
@@ -52,7 +53,7 @@
   before date operations can be performed. This function will add that cast if it is a timestamp, otherwise this is a
   noop."
   [expr]
-  (if (u/is-temporal? expr)
+  (if (du/is-temporal? expr)
     (hx/cast :timestamp expr)
     expr))
 
diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj
index 4639f218e56..5d7f1c58e34 100644
--- a/src/metabase/email/messages.clj
+++ b/src/metabase/email/messages.clj
@@ -12,6 +12,7 @@
              [util :as u]]
             [metabase.pulse.render :as render]
             [metabase.util
+             [date :as du]
              [export :as export]
              [quotation :as quotation]
              [urls :as url]]
@@ -62,7 +63,7 @@
                                :invitorEmail (:email invitor)
                                :company      company
                                :joinUrl      join-url
-                               :today        (u/format-date "MMM'&nbsp;'dd,'&nbsp;'yyyy")
+                               :today        (du/format-date "MMM'&nbsp;'dd,'&nbsp;'yyyy")
                                :logoHeader   true}
                               (random-quote-context)))]
     (email/send-message!
@@ -97,7 +98,7 @@
                               :joinedUserName    (:first_name new-user)
                               :joinedViaSSO      google-auth?
                               :joinedUserEmail   (:email new-user)
-                              :joinedDate        (u/format-date "EEEE, MMMM d") ; e.g. "Wednesday, July 13". TODO - is this what we want?
+                              :joinedDate        (du/format-date "EEEE, MMMM d") ; e.g. "Wednesday, July 13". TODO - is this what we want?
                               :adminEmail        (first recipients)
                               :joinedUserEditUrl (str (public-settings/site-url) "/admin/people")}
                              (random-quote-context))))))
diff --git a/src/metabase/events/last_login.clj b/src/metabase/events/last_login.clj
index aef66c07f42..c5852694352 100644
--- a/src/metabase/events/last_login.clj
+++ b/src/metabase/events/last_login.clj
@@ -5,6 +5,7 @@
              [events :as events]
              [util :as u]]
             [metabase.models.user :refer [User]]
+            [metabase.util.date :as du]
             [toucan.db :as db]))
 
 (def ^:const last-login-topics
@@ -27,7 +28,7 @@
     (when-let [{object :item} last-login-event]
       ;; just make a simple attempt to set the `:last_login` for the given user to now
       (when-let [user-id (:user_id object)]
-        (db/update! User user-id, :last_login (u/new-sql-timestamp))))
+        (db/update! User user-id, :last_login (du/new-sql-timestamp))))
     (catch Throwable e
       (log/warn (format "Failed to process sync-database event. %s" (:topic last-login-event)) e))))
 
diff --git a/src/metabase/feature_extraction/async.clj b/src/metabase/feature_extraction/async.clj
index 8c82265b268..0588872725c 100644
--- a/src/metabase/feature_extraction/async.clj
+++ b/src/metabase/feature_extraction/async.clj
@@ -7,6 +7,7 @@
              [computation-job :refer [ComputationJob]]
              [computation-job-result :refer [ComputationJobResult]]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]))
 
 (defonce ^:private running-jobs (atom {}))
@@ -44,7 +45,7 @@
         :payload    payload)
       (db/update! ComputationJob id
         :status   :done
-        :ended_at (u/new-sql-timestamp)))
+        :ended_at (du/new-sql-timestamp)))
     (when callback
       (callback job payload)))
   (swap! running-jobs dissoc id)
@@ -63,7 +64,7 @@
           :payload    error)
         (db/update! ComputationJob id
           :status :error
-          :ended_at (u/new-sql-timestamp)))
+          :ended_at (du/new-sql-timestamp)))
       (when callback
         (callback job error)))
     (swap! running-jobs dissoc id)
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index 5376e25d3f3..fc3304cfc86 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -15,6 +15,7 @@
              [session :refer [Session]]
              [setting :refer [defsetting]]
              [user :as user :refer [User]]]
+            [metabase.util.date :as du]
             monger.json
             [puppetlabs.i18n.core :refer [tru]]
             [toucan
@@ -173,7 +174,7 @@
   []
   {"Cache-Control" "max-age=0, no-cache, must-revalidate, proxy-revalidate"
    "Expires"        "Tue, 03 Jul 2001 06:00:00 GMT"
-   "Last-Modified"  (u/format-date :rfc822)})
+   "Last-Modified"  (du/format-date :rfc822)})
 
 (def ^:private ^:const strict-transport-security-header
   "Tell browsers to only access this resource over HTTPS for the next year (prevent MTM attacks). (This only applies if
@@ -347,7 +348,7 @@
       (let [start-time (System/nanoTime)]
         (db/with-call-counting [call-count]
           (u/prog1 (handler request)
-            (log-response jetty-stats-fn request <> (u/format-nanoseconds (- (System/nanoTime) start-time)) (call-count))))))))
+            (log-response jetty-stats-fn request <> (du/format-nanoseconds (- (System/nanoTime) start-time)) (call-count))))))))
 
 
 ;;; ----------------------------------------------- EXCEPTION HANDLING -----------------------------------------------
diff --git a/src/metabase/models/activity.clj b/src/metabase/models/activity.clj
index 9d62f80f3bb..a1247eee02c 100644
--- a/src/metabase/models/activity.clj
+++ b/src/metabase/models/activity.clj
@@ -9,6 +9,7 @@
              [metric :refer [Metric]]
              [pulse :refer [Pulse]]
              [segment :refer [Segment]]]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -34,7 +35,7 @@
 (models/defmodel Activity :activity)
 
 (defn- pre-insert [activity]
-  (let [defaults {:timestamp (u/new-sql-timestamp)
+  (let [defaults {:timestamp (du/new-sql-timestamp)
                   :details   {}}]
     (merge defaults activity)))
 
diff --git a/src/metabase/models/collection_revision.clj b/src/metabase/models/collection_revision.clj
index c13317bad50..c367c5b14b1 100644
--- a/src/metabase/models/collection_revision.clj
+++ b/src/metabase/models/collection_revision.clj
@@ -1,5 +1,6 @@
 (ns metabase.models.collection-revision
   (:require [metabase.util :as u]
+            [metabase.util.date :as du]
             [puppetlabs.i18n.core :refer [tru]]
             [toucan
              [db :as db]
@@ -8,7 +9,7 @@
 (models/defmodel CollectionRevision :collection_revision)
 
 (defn- pre-insert [revision]
-  (assoc revision :created_at (u/new-sql-timestamp)))
+  (assoc revision :created_at (du/new-sql-timestamp)))
 
 (u/strict-extend (class CollectionRevision)
   models/IModel
diff --git a/src/metabase/models/dependency.clj b/src/metabase/models/dependency.clj
index 529ca83e26e..ee140d38b1c 100644
--- a/src/metabase/models/dependency.clj
+++ b/src/metabase/models/dependency.clj
@@ -1,6 +1,6 @@
 (ns metabase.models.dependency
   (:require [clojure.set :as set]
-            [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -52,7 +52,7 @@
         dependencies+    (set/difference dependencies-new dependencies-old)
         dependencies-    (set/difference dependencies-old dependencies-new)]
     (when (seq dependencies+)
-      (let [vs (map #(merge % {:model entity-name, :model_id id, :created_at (u/new-sql-timestamp)}) dependencies+)]
+      (let [vs (map #(merge % {:model entity-name, :model_id id, :created_at (du/new-sql-timestamp)}) dependencies+)]
         (db/insert-many! Dependency vs)))
     (when (seq dependencies-)
       (doseq [{:keys [dependent_on_model dependent_on_id]} dependencies-]
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 3b3dc7eb8ae..17e47a64b0b 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -4,6 +4,7 @@
             [metabase.util :as u]
             [metabase.util
              [cron :as cron-util]
+             [date :as du]
              [encryption :as encryption]]
             [schema.core :as s]
             [taoensso.nippy :as nippy]
@@ -82,10 +83,10 @@
 ;;; properties
 
 (defn- add-created-at-timestamp [obj & _]
-  (assoc obj :created_at (u/new-sql-timestamp)))
+  (assoc obj :created_at (du/new-sql-timestamp)))
 
 (defn- add-updated-at-timestamp [obj & _]
-  (assoc obj :updated_at (u/new-sql-timestamp)))
+  (assoc obj :updated_at (du/new-sql-timestamp)))
 
 (models/add-property! :timestamped?
   :insert (comp add-created-at-timestamp add-updated-at-timestamp)
diff --git a/src/metabase/models/permissions_revision.clj b/src/metabase/models/permissions_revision.clj
index 10892a6f3c5..07ea81ff83b 100644
--- a/src/metabase/models/permissions_revision.clj
+++ b/src/metabase/models/permissions_revision.clj
@@ -1,5 +1,6 @@
 (ns metabase.models.permissions-revision
   (:require [metabase.util :as u]
+            [metabase.util.date :as du]
             [puppetlabs.i18n.core :refer [tru]]
             [toucan
              [db :as db]
@@ -8,7 +9,7 @@
 (models/defmodel PermissionsRevision :permissions_revision)
 
 (defn- pre-insert [revision]
-  (assoc revision :created_at (u/new-sql-timestamp)))
+  (assoc revision :created_at (du/new-sql-timestamp)))
 
 (u/strict-extend (class PermissionsRevision)
   models/IModel
diff --git a/src/metabase/models/revision.clj b/src/metabase/models/revision.clj
index c96cfee15c6..c79fd82e03c 100644
--- a/src/metabase/models/revision.clj
+++ b/src/metabase/models/revision.clj
@@ -3,6 +3,7 @@
             [metabase.models.revision.diff :refer [diff-string]]
             [metabase.models.user :refer [User]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [puppetlabs.i18n.core :refer [tru]]
             [toucan
              [db :as db]
@@ -63,7 +64,7 @@
 (models/defmodel Revision :revision)
 
 (defn- pre-insert [revision]
-  (assoc revision :timestamp (u/new-sql-timestamp)))
+  (assoc revision :timestamp (du/new-sql-timestamp)))
 
 (u/strict-extend (class Revision)
   models/IModel
diff --git a/src/metabase/models/session.clj b/src/metabase/models/session.clj
index 7156847ce94..285218a277d 100644
--- a/src/metabase/models/session.clj
+++ b/src/metabase/models/session.clj
@@ -1,5 +1,6 @@
 (ns metabase.models.session
   (:require [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -7,7 +8,7 @@
 (models/defmodel Session :core_session)
 
 (defn- pre-insert [session]
-  (assoc session :created_at (u/new-sql-timestamp)))
+  (assoc session :created_at (du/new-sql-timestamp)))
 
 (u/strict-extend (class Session)
   models/IModel
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index ed3a769376c..2827fc8b163 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -9,6 +9,7 @@
             [metabase.models
              [permissions-group :as group]
              [permissions-group-membership :as perm-membership :refer [PermissionsGroupMembership]]]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]])
@@ -26,7 +27,7 @@
   (assert (not (:password_salt user))
     "Don't try to pass an encrypted password to (insert! User). Password encryption is handled by pre-insert.")
   (let [salt     (str (UUID/randomUUID))
-        defaults {:date_joined  (u/new-sql-timestamp)
+        defaults {:date_joined  (du/new-sql-timestamp)
                   :last_login   nil
                   :is_active    true
                   :is_superuser false}]
diff --git a/src/metabase/models/view_log.clj b/src/metabase/models/view_log.clj
index 8430b56d4b4..ec7a07eed8a 100644
--- a/src/metabase/models/view_log.clj
+++ b/src/metabase/models/view_log.clj
@@ -2,12 +2,13 @@
   "The ViewLog is used to log an event where a given User views a given object such as a Table or Card (Question)."
   (:require [metabase.models.interface :as i]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.models :as models]))
 
 (models/defmodel ViewLog :view_log)
 
 (defn- pre-insert [log-entry]
-  (let [defaults {:timestamp (u/new-sql-timestamp)}]
+  (let [defaults {:timestamp (du/new-sql-timestamp)}]
     (merge defaults log-entry)))
 
 (u/strict-extend (class ViewLog)
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index 5c38a745653..7bcabaa4ee6 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -13,6 +13,7 @@
              [util :as hutil]]
             [metabase.util :as u]
             [metabase.util
+             [date :as du]
              [ui-logic :as ui-logic]
              [urls :as urls]]
             [puppetlabs.i18n.core :refer [tru trs]]
@@ -212,7 +213,7 @@
 (defn- reformat-timestamp [timezone old-format-timestamp new-format-string]
   (f/unparse (f/with-zone (f/formatter new-format-string)
                (DateTimeZone/forTimeZone timezone))
-             (u/str->date-time old-format-timestamp timezone)))
+             (du/str->date-time old-format-timestamp timezone)))
 
 (defn- format-timestamp
   "Formats timestamps with human friendly absolute dates based on the column :unit"
@@ -221,7 +222,7 @@
     :hour          (reformat-timestamp timezone timestamp "h a - MMM YYYY")
     :week          (str "Week " (reformat-timestamp timezone timestamp "w - YYYY"))
     :month         (reformat-timestamp timezone timestamp "MMMM YYYY")
-    :quarter       (let [timestamp-obj (u/str->date-time timestamp timezone)]
+    :quarter       (let [timestamp-obj (du/str->date-time timestamp timezone)]
                      (str "Q"
                           (inc (int (/ (t/month timestamp-obj)
                                        3)))
@@ -249,7 +250,7 @@
 (defn- format-timestamp-relative
   "Formats timestamps with relative names (today, yesterday, this *, last *) based on column :unit, if possible, otherwie returns nil"
   [timezone timestamp, {:keys [unit]}]
-  (let [parsed-timestamp (u/str->date-time timestamp timezone)]
+  (let [parsed-timestamp (du/str->date-time timestamp timezone)]
     (case unit
       :day     (date->interval-name parsed-timestamp
                                     (t/date-midnight (year) (month) (day))
@@ -649,7 +650,7 @@
   [render-type timezone card {:keys [rows cols] :as data}]
   (let [[x-axis-rowfn y-axis-rowfn] (graphing-columns card data)
         ft-row (if (datetime-field? (x-axis-rowfn cols))
-                 #(.getTime ^Date (u/->Timestamp %))
+                 #(.getTime ^Date (du/->Timestamp %))
                  identity)
         rows   (if (> (ft-row (x-axis-rowfn (first rows)))
                       (ft-row (x-axis-rowfn (last rows))))
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index fe8b7012c50..6cd7a9150fd 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -34,7 +34,9 @@
              [resolve :as resolve]
              [source-table :as source-table]]
             [metabase.query-processor.util :as qputil]
-            [metabase.util.schema :as su]
+            [metabase.util
+             [date :as du]
+             [schema :as su]]
             [schema.core :as s]
             [toucan.db :as db]))
 
@@ -229,7 +231,7 @@
    :hash              (or query-hash (throw (Exception. "Missing query hash!")))
    :native            (= query-type "native")
    :json_query        (dissoc query :info)
-   :started_at        (u/new-sql-timestamp)
+   :started_at        (du/new-sql-timestamp)
    :running_time      0
    :result_rows       0
    :start_time_millis (System/currentTimeMillis)})
diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj
index 6fd4af9cd16..db2b7a8b0fe 100644
--- a/src/metabase/query_processor/interface.clj
+++ b/src/metabase/query_processor/interface.clj
@@ -7,7 +7,9 @@
              [field :as field]]
             [metabase.sync.interface :as i]
             [metabase.util :as u]
-            [metabase.util.schema :as su]
+            [metabase.util
+             [date :as du]
+             [schema :as su]]
             [schema.core :as s])
   (:import clojure.lang.Keyword
            [java.sql Time Timestamp]))
@@ -269,7 +271,7 @@
 
 (def LiteralDatetimeString
   "Schema for an MBQL datetime string literal, in ISO-8601 format."
-  (s/constrained su/NonBlankString u/date-string? "Valid ISO-8601 datetime string literal"))
+  (s/constrained su/NonBlankString du/date-string? "Valid ISO-8601 datetime string literal"))
 
 (def LiteralDatetime
   "Schema for an MBQL literal datetime value: and ISO-8601 string or `java.sql.Date`."
@@ -333,7 +335,7 @@
 (extend-protocol IDateTimeValue
   DateTimeValue
   (unit                [this]   (:unit (:field this)))
-  (add-date-time-units [this n] (assoc this :value (u/relative-date (unit this) n (:value this))))
+  (add-date-time-units [this n] (assoc this :value (du/relative-date (unit this) n (:value this))))
 
   RelativeDateTimeValue
   (unit                [this]   (:unit this))
diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj
index 384d18f6963..ce8db38cea6 100644
--- a/src/metabase/query_processor/middleware/cache.clj
+++ b/src/metabase/query_processor/middleware/cache.clj
@@ -18,7 +18,8 @@
              [public-settings :as public-settings]
              [util :as u]]
             [metabase.query-processor.middleware.cache-backend.interface :as i]
-            [metabase.query-processor.util :as qputil]))
+            [metabase.query-processor.util :as qputil]
+            [metabase.util.date :as du]))
 
 (def ^:dynamic ^Boolean *ignore-cached-results*
   "Should we force the query to run, ignoring cached results even if they're available?
@@ -70,7 +71,7 @@
 (defn- cached-results [query-hash max-age-seconds]
   (when-not *ignore-cached-results*
     (when-let [results (i/cached-results @backend-instance query-hash max-age-seconds)]
-      (assert (u/is-temporal? (:updated_at results))
+      (assert (du/is-temporal? (:updated_at results))
         "cached-results should include an `:updated_at` field containing the date when the query was last ran.")
       (log/info "Returning cached results for query" (u/emoji "💾"))
       (assoc results :cached true))))
diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj
index bb426ba5f3e..4e33785fc42 100644
--- a/src/metabase/query_processor/middleware/cache_backend/db.clj
+++ b/src/metabase/query_processor/middleware/cache_backend/db.clj
@@ -2,10 +2,12 @@
   (:require [metabase
              [public-settings :as public-settings]
              [util :as u]]
+            [metabase.util.date :as du]
             [metabase.models
              [interface :as models]
              [query-cache :refer [QueryCache]]]
             [metabase.query-processor.middleware.cache-backend.interface :as i]
+            [metabase.util.date :as du]
             [toucan.db :as db]))
 
 (defn- cached-results
@@ -13,16 +15,16 @@
   [query-hash max-age-seconds]
   (when-let [{:keys [results updated_at]} (db/select-one [QueryCache :results :updated_at]
                                             :query_hash query-hash
-                                            :updated_at [:>= (u/->Timestamp (- (System/currentTimeMillis)
-                                                                               (* 1000 max-age-seconds)))])]
+                                            :updated_at [:>= (du/->Timestamp (- (System/currentTimeMillis)
+                                                                                (* 1000 max-age-seconds)))])]
     (assoc results :updated_at updated_at)))
 
 (defn- purge-old-cache-entries!
   "Delete any cache entries that are older than the global max age `max-cache-entry-age-seconds` (currently 3 months)."
   []
   (db/simple-delete! QueryCache
-    :updated_at [:<= (u/->Timestamp (- (System/currentTimeMillis)
-                                       (* 1000 (public-settings/query-caching-max-ttl))))]))
+    :updated_at [:<= (du/->Timestamp (- (System/currentTimeMillis)
+                                        (* 1000 (public-settings/query-caching-max-ttl))))]))
 
 (defn- save-results!
   "Save the RESULTS of query with QUERY-HASH, updating an existing QueryCache entry
@@ -30,7 +32,7 @@
   [query-hash results]
   (purge-old-cache-entries!)
   (or (db/update-where! QueryCache {:query_hash query-hash}
-        :updated_at (u/new-sql-timestamp)
+        :updated_at (du/new-sql-timestamp)
         :results    (models/compress results)) ; have to manually call these here since Toucan doesn't call type conversion fns for update-where! (yet)
       (db/insert! QueryCache
         :query_hash query-hash
diff --git a/src/metabase/query_processor/middleware/expand.clj b/src/metabase/query_processor/middleware/expand.clj
index 0c1d5c968f0..59c949370d6 100644
--- a/src/metabase/query_processor/middleware/expand.clj
+++ b/src/metabase/query_processor/middleware/expand.clj
@@ -9,7 +9,9 @@
              [interface :as i]
              [util :as qputil]]
             [metabase.util :as u]
-            [metabase.util.schema :as su]
+            [metabase.util
+             [date :as du]
+             [schema :as su]]
             [schema.core :as s])
   (:import [metabase.query_processor.interface AgFieldRef BetweenFilter ComparisonFilter CompoundFilter DateTimeValue
             DateTimeField Expression ExpressionRef FieldLiteral FieldPlaceholder RelativeDatetime
@@ -110,9 +112,9 @@
     (instance? RelativeDateTimeValue v) v
     (instance? DateTimeValue v)         v
     (instance? RelativeDatetime v)      (i/map->RelativeDateTimeValue (assoc v :unit (datetime-unit f v), :field (datetime-field f (datetime-unit f v))))
-    (instance? DateTimeField f)         (i/map->DateTimeValue {:value (u/->Timestamp v), :field f})
+    (instance? DateTimeField f)         (i/map->DateTimeValue {:value (du/->Timestamp v), :field f})
     (instance? FieldLiteral f)          (if (isa? (:base-type f) :type/DateTime)
-                                          (i/map->DateTimeValue {:value (u/->Timestamp v)
+                                          (i/map->DateTimeValue {:value (du/->Timestamp v)
                                                                  :field (i/map->DateTimeField {:field f :unit :default})})
                                           (i/map->Value {:value v, :field f}))
     :else                               (i/map->ValuePlaceholder {:field-placeholder (field f), :value v})))
diff --git a/src/metabase/query_processor/middleware/format_rows.clj b/src/metabase/query_processor/middleware/format_rows.clj
index 114a60ed307..6e30c30ca9a 100644
--- a/src/metabase/query_processor/middleware/format_rows.clj
+++ b/src/metabase/query_processor/middleware/format_rows.clj
@@ -1,7 +1,7 @@
 (ns metabase.query-processor.middleware.format-rows
   "Middleware that formats the results of a query.
    Currently, the only thing this does is convert datetime types to ISO-8601 strings in the appropriate timezone."
-  (:require [metabase.util :as u]))
+  (:require [metabase.util.date :as du]))
 
 (defn- format-rows* [{:keys [report-timezone]} rows]
   (let [timezone (or report-timezone (System/getProperty "user.timezone"))]
@@ -11,11 +11,11 @@
         ;;       this ensures alignment between the way dates are processed by JDBC and our returned data
         ;;       GH issues: #2282, #2035
         (cond
-          (u/is-time? v)
-          (u/format-time v timezone)
+          (du/is-time? v)
+          (du/format-time v timezone)
 
-          (u/is-temporal? v)
-          (u/->iso-8601-datetime v timezone)
+          (du/is-temporal? v)
+          (du/->iso-8601-datetime v timezone)
 
           :else v)))))
 
diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj
index 759c3cf8e34..8734775fd8a 100644
--- a/src/metabase/query_processor/middleware/parameters/sql.clj
+++ b/src/metabase/query_processor/middleware/parameters/sql.clj
@@ -13,7 +13,9 @@
             [metabase.query-processor.middleware.parameters.dates :as date-params]
             [metabase.query-processor.middleware.expand :as ql]
             [metabase.util :as u]
-            [metabase.util.schema :as su]
+            [metabase.util
+             [date :as du]
+             [schema :as su]]
             [puppetlabs.i18n.core :refer [tru]]
             [schema.core :as s]
             [toucan.db :as db])
@@ -373,15 +375,15 @@
 
   Date
   (->replacement-snippet-info [{:keys [s]}]
-    (honeysql->replacement-snippet-info (u/->Timestamp s)))
+    (honeysql->replacement-snippet-info (du/->Timestamp s)))
 
   DateRange
   (->replacement-snippet-info [{:keys [start end]}]
     (cond
-      (= start end) {:replacement-snippet "= ?",             :prepared-statement-args [(u/->Timestamp start)]}
-      (nil? start)  {:replacement-snippet "< ?",             :prepared-statement-args [(u/->Timestamp end)]}
-      (nil? end)    {:replacement-snippet "> ?",             :prepared-statement-args [(u/->Timestamp start)]}
-      :else         {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(u/->Timestamp start) (u/->Timestamp end)]}))
+      (= start end) {:replacement-snippet "= ?",             :prepared-statement-args [(du/->Timestamp start)]}
+      (nil? start)  {:replacement-snippet "< ?",             :prepared-statement-args [(du/->Timestamp end)]}
+      (nil? end)    {:replacement-snippet "> ?",             :prepared-statement-args [(du/->Timestamp start)]}
+      :else         {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(du/->Timestamp start) (du/->Timestamp end)]}))
 
   ;; TODO - clean this up if possible!
   Dimension
diff --git a/src/metabase/query_processor/middleware/resolve.clj b/src/metabase/query_processor/middleware/resolve.clj
index f627d5afdec..683819eb619 100644
--- a/src/metabase/query_processor/middleware/resolve.clj
+++ b/src/metabase/query_processor/middleware/resolve.clj
@@ -20,6 +20,7 @@
             [metabase.query-processor
              [interface :as i]
              [util :as qputil]]
+            [metabase.util.date :as du]
             [schema.core :as s]
             [toucan
              [db :as db]
@@ -233,8 +234,8 @@
     (let [tz                 (when-let [tz-id ^String (setting/get :report-timezone)]
                                (TimeZone/getTimeZone tz-id))
           parsed-string-date (some-> value
-                                     (u/str->date-time tz)
-                                     u/->Timestamp)]
+                                     (du/str->date-time tz)
+                                     du/->Timestamp)]
       (cond
         parsed-string-date
         (s/validate DateTimeValue (i/map->DateTimeValue {:field this, :value parsed-string-date}))
@@ -256,7 +257,7 @@
           tz                 (when tz-id
                                (TimeZone/getTimeZone tz-id))
           parsed-string-time (some-> value
-                                     (u/str->time tz))]
+                                     (du/str->time tz))]
       (cond
         parsed-string-time
         (s/validate TimeValue (i/map->TimeValue {:field this, :value parsed-string-time :timezone-id tz-id}))
diff --git a/src/metabase/sync/analyze.clj b/src/metabase/sync/analyze.clj
index 9976fa930ea..956e6f03e09 100644
--- a/src/metabase/sync/analyze.clj
+++ b/src/metabase/sync/analyze.clj
@@ -13,6 +13,7 @@
              [fingerprint :as fingerprint]
              #_[table-row-count :as table-row-count]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [schema.core :as s]
             [toucan.db :as db]))
 
@@ -61,7 +62,7 @@
     (db/update-where! Field {:table_id            [:in ids]
                              :fingerprint_version i/latest-fingerprint-version
                              :last_analyzed       nil}
-      :last_analyzed (u/new-sql-timestamp))))
+      :last_analyzed (du/new-sql-timestamp))))
 
 (s/defn ^:private update-fields-last-analyzed!
   "Update the `last_analyzed` date for all the recently re-fingerprinted/re-classified Fields in TABLE."
diff --git a/src/metabase/sync/analyze/fingerprint/datetime.clj b/src/metabase/sync/analyze/fingerprint/datetime.clj
index 7c0bcded644..21c284ea5aa 100644
--- a/src/metabase/sync/analyze/fingerprint/datetime.clj
+++ b/src/metabase/sync/analyze/fingerprint/datetime.clj
@@ -6,13 +6,14 @@
             [medley.core :as m]
             [metabase.sync.interface :as i]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [redux.core :as redux]
             [schema.core :as s]))
 
 (s/defn datetime-fingerprint :- i/DateTimeFingerprint
   "Generate a fingerprint containing information about values that belong to a `DateTime` Field."
   [values :- i/FieldSample]
-  (transduce (map u/str->date-time)
+  (transduce (map du/str->date-time)
              (redux/post-complete
               (redux/fuse {:earliest t/min-date
                            :latest   t/max-date})
diff --git a/src/metabase/sync/util.clj b/src/metabase/sync/util.clj
index 5bab0ce1530..d5988306c8f 100644
--- a/src/metabase/sync/util.clj
+++ b/src/metabase/sync/util.clj
@@ -13,6 +13,7 @@
             [metabase.models.table :refer [Table]]
             [metabase.query-processor.interface :as qpi]
             [metabase.sync.interface :as i]
+            [metabase.util.date :as du]
             [ring.util.codec :as codec]
             [taoensso.nippy :as nippy]
             [toucan.db :as db]))
@@ -95,7 +96,7 @@
       (f)
       (log/info (u/format-color 'magenta "FINISHED: %s (%s)"
                   message
-                  (u/format-nanoseconds (- (System/nanoTime) start-time)))))))
+                  (du/format-nanoseconds (- (System/nanoTime) start-time)))))))
 
 
 (defn- with-db-logging-disabled
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index 05aa6728af0..3597631b370 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -41,286 +41,6 @@
   [& body]
   `(try ~@body (catch Throwable ~'_)))
 
-
-(defprotocol ITimestampCoercible
-  "Coerce object to a `java.sql.Timestamp`."
-  (->Timestamp ^java.sql.Timestamp [this]
-    "Coerce this object to a `java.sql.Timestamp`. Strings are parsed as ISO-8601."))
-
-(declare str->date-time)
-
-(extend-protocol ITimestampCoercible
-  nil       (->Timestamp [_]
-              nil)
-  Timestamp (->Timestamp [this]
-              this)
-  Date       (->Timestamp [this]
-               (Timestamp. (.getTime this)))
-  ;; Number is assumed to be a UNIX timezone in milliseconds (UTC)
-  Number    (->Timestamp [this]
-              (Timestamp. this))
-  Calendar  (->Timestamp [this]
-              (->Timestamp (.getTime this)))
-  ;; Strings are expected to be in ISO-8601 format. `YYYY-MM-DD` strings *are* valid ISO-8601 dates.
-  String    (->Timestamp [this]
-              (->Timestamp (str->date-time this)))
-  DateTime  (->Timestamp [this]
-              (->Timestamp (.getMillis this))))
-
-
-(defprotocol IDateTimeFormatterCoercible
-  "Protocol for converting objects to `DateTimeFormatters`."
-  (->DateTimeFormatter ^org.joda.time.format.DateTimeFormatter [this]
-    "Coerce object to a `DateTimeFormatter`."))
-
-(declare pprint-to-str)
-
-(extend-protocol IDateTimeFormatterCoercible
-  ;; Specify a format string like "yyyy-MM-dd"
-  String            (->DateTimeFormatter [this] (time/formatter this))
-  DateTimeFormatter (->DateTimeFormatter [this] this)
-  ;; Keyword will be used to get matching formatter from time/formatters
-  Keyword           (->DateTimeFormatter [this]
-                      (or (time/formatters this)
-                          (throw (Exception. (format "Invalid formatter name, must be one of:\n%s"
-                                                     (pprint-to-str (sort (keys time/formatters)))))))))
-
-
-(defn parse-date
-  "Parse a datetime string S with a custom DATE-FORMAT, which can be a format string, clj-time formatter keyword, or
-  anything else that can be coerced to a `DateTimeFormatter`.
-
-     (parse-date \"yyyyMMdd\" \"20160201\") -> #inst \"2016-02-01\"
-     (parse-date :date-time \"2016-02-01T00:00:00.000Z\") -> #inst \"2016-02-01\""
-  ^java.sql.Timestamp [date-format, ^String s]
-  (->Timestamp (time/parse (->DateTimeFormatter date-format) s)))
-
-
-(defprotocol ISO8601
-  "Protocol for converting objects to ISO8601 formatted strings."
-  (->iso-8601-datetime ^String [this timezone-id]
-    "Coerce object to an ISO8601 date-time string such as \"2015-11-18T23:55:03.841Z\" with a given TIMEZONE."))
-
-(def ^:private ^{:arglists '([timezone-id])} ISO8601Formatter
-  ;; memoize this because the formatters are static. They must be distinct per timezone though.
-  (memoize (fn [timezone-id]
-             (if timezone-id
-               (time/with-zone (time/formatters :date-time) (t/time-zone-for-id timezone-id))
-               (time/formatters :date-time)))))
-
-(extend-protocol ISO8601
-  nil                    (->iso-8601-datetime [_ _] nil)
-  java.util.Date         (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-date this)))
-  java.sql.Date          (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-date this)))
-  java.sql.Timestamp     (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-time this)))
-  org.joda.time.DateTime (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) this)))
-
-(def ^:private ^{:arglists '([timezone-id])} time-formatter
-  ;; memoize this because the formatters are static. They must be distinct per timezone though.
-  (memoize (fn [timezone-id]
-             (if timezone-id
-               (time/with-zone (time/formatters :time) (t/time-zone-for-id timezone-id))
-               (time/formatters :time)))))
-
-(defn format-time
-  "Returns a string representation of the time found in `T`"
-  [t time-zone-id]
-  (time/unparse (time-formatter time-zone-id) (coerce/to-date-time t)))
-
-(defn is-time?
-  "Returns true if `V` is a Time object"
-  [v]
-  (and v (instance? Time v)))
-
-;;; ## Date Stuff
-
-(defn is-temporal?
-  "Is VALUE an instance of a datetime class like `java.util.Date` or `org.joda.time.DateTime`?"
-  [v]
-  (or (instance? java.util.Date v)
-      (instance? org.joda.time.DateTime v)))
-
-(defn new-sql-timestamp
-  "`java.sql.Date` doesn't have an empty constructor so this is a convenience that lets you make one with the current
-  date. (Some DBs like Postgres will get snippy if you don't use a `java.sql.Timestamp`)."
-  ^java.sql.Timestamp []
-  (->Timestamp (System/currentTimeMillis)))
-
-(defn format-date
-  "Format DATE using a given DATE-FORMAT.
-
-   DATE is anything that can coerced to a `Timestamp` via `->Timestamp`, such as a `Date`, `Timestamp`,
-   `Long` (ms since the epoch), or an ISO-8601 `String`. DATE defaults to the current moment in time.
-
-   DATE-FORMAT is anything that can be passed to `->DateTimeFormatter`, such as `String`
-   (using [the usual date format args](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)),
-   `Keyword`, or `DateTimeFormatter`.
-
-
-     (format-date \"yyyy-MM-dd\")                        -> \"2015-11-18\"
-     (format-date :year (java.util.Date.))               -> \"2015\"
-     (format-date :date-time (System/currentTimeMillis)) -> \"2015-11-18T23:55:03.841Z\""
-  (^String [date-format]
-   (format-date date-format (System/currentTimeMillis)))
-  (^String [date-format date]
-   (time/unparse (->DateTimeFormatter date-format) (coerce/from-sql-time (->Timestamp date)))))
-
-(def ^{:arglists '([] [date])} date->iso-8601
-  "Format DATE a an ISO-8601 string."
-  (partial format-date :date-time))
-
-(defn date-string?
-  "Is S a valid ISO 8601 date string?"
-  [^String s]
-  (boolean (when (string? s)
-             (ignore-exceptions
-               (->Timestamp s)))))
-
-
-(defn ->Date
-  "Coerece DATE to a `java.util.Date`."
-  (^java.util.Date []
-   (java.util.Date.))
-  (^java.util.Date [date]
-   (java.util.Date. (.getTime (->Timestamp date)))))
-
-
-(defn ->Calendar
-  "Coerce DATE to a `java.util.Calendar`."
-  (^java.util.Calendar []
-   (doto (Calendar/getInstance)
-     (.setTimeZone (TimeZone/getTimeZone "UTC"))))
-  (^java.util.Calendar [date]
-   (doto (->Calendar)
-     (.setTime (->Timestamp date))))
-  (^java.util.Calendar [date, ^String timezone-id]
-   (doto (->Calendar date)
-     (.setTimeZone (TimeZone/getTimeZone timezone-id)))))
-
-
-(defn relative-date
-  "Return a new `Timestamp` relative to the current time using a relative date UNIT.
-
-     (relative-date :year -1) -> #inst 2014-11-12 ..."
-  (^java.sql.Timestamp [unit amount]
-   (relative-date unit amount (Calendar/getInstance)))
-  (^java.sql.Timestamp [unit amount date]
-   (let [cal               (->Calendar date)
-         [unit multiplier] (case unit
-                             :second  [Calendar/SECOND 1]
-                             :minute  [Calendar/MINUTE 1]
-                             :hour    [Calendar/HOUR   1]
-                             :day     [Calendar/DATE   1]
-                             :week    [Calendar/DATE   7]
-                             :month   [Calendar/MONTH  1]
-                             :quarter [Calendar/MONTH  3]
-                             :year    [Calendar/YEAR   1])]
-     (.set cal unit (+ (.get cal unit)
-                       (* amount multiplier)))
-     (->Timestamp cal))))
-
-
-(def ^:private ^:const date-extract-units
-  #{:minute-of-hour :hour-of-day :day-of-week :day-of-month :day-of-year :week-of-year :month-of-year :quarter-of-year
-    :year})
-
-(defn date-extract
-  "Extract UNIT from DATE. DATE defaults to now.
-
-     (date-extract :year) -> 2015"
-  ([unit]
-   (date-extract unit (System/currentTimeMillis) "UTC"))
-  ([unit date]
-   (date-extract unit date "UTC"))
-  ([unit date timezone-id]
-   (let [cal (->Calendar date timezone-id)]
-     (case unit
-       :minute-of-hour  (.get cal Calendar/MINUTE)
-       :hour-of-day     (.get cal Calendar/HOUR_OF_DAY)
-       ;; 1 = Sunday <-> 6 = Saturday
-       :day-of-week     (.get cal Calendar/DAY_OF_WEEK)
-       :day-of-month    (.get cal Calendar/DAY_OF_MONTH)
-       :day-of-year     (.get cal Calendar/DAY_OF_YEAR)
-       ;; 1 = First week of year
-       :week-of-year    (.get cal Calendar/WEEK_OF_YEAR)
-       :month-of-year   (inc (.get cal Calendar/MONTH))
-       :quarter-of-year (let [month (date-extract :month-of-year date timezone-id)]
-                          (int (/ (+ 2 month)
-                                  3)))
-       :year            (.get cal Calendar/YEAR)))))
-
-
-(def ^:private ^:const date-trunc-units
-  #{:minute :hour :day :week :month :quarter :year})
-
-(defn- trunc-with-format [format-string date timezone-id]
-  (->Timestamp (format-date (time/with-zone (time/formatter format-string)
-                              (t/time-zone-for-id timezone-id))
-                            date)))
-
-(defn- trunc-with-floor [date amount-ms]
-  (->Timestamp (* (math/floor (/ (.getTime (->Timestamp date))
-                                 amount-ms))
-                  amount-ms)))
-
-(defn- ->first-day-of-week [date timezone-id]
-  (let [day-of-week (date-extract :day-of-week date timezone-id)]
-    (relative-date :day (- (dec day-of-week)) date)))
-
-(defn- format-string-for-quarter ^String [date timezone-id]
-  (let [year    (date-extract :year date timezone-id)
-        quarter (date-extract :quarter-of-year date timezone-id)
-        month   (- (* 3 quarter) 2)]
-    (format "%d-%02d-01'T'ZZ" year month)))
-
-(defn date-trunc
-  "Truncate DATE to UNIT. DATE defaults to now.
-
-     (date-trunc :month).
-     ;; -> #inst \"2015-11-01T00:00:00\""
-  (^java.sql.Timestamp [unit]
-   (date-trunc unit (System/currentTimeMillis) "UTC"))
-  (^java.sql.Timestamp [unit date]
-   (date-trunc unit date "UTC"))
-  (^java.sql.Timestamp [unit date timezone-id]
-   (case unit
-     ;; For minute and hour truncation timezone should not be taken into account
-     :minute  (trunc-with-floor date (* 60 1000))
-     :hour    (trunc-with-floor date (* 60 60 1000))
-     :day     (trunc-with-format "yyyy-MM-dd'T'ZZ" date timezone-id)
-     :week    (trunc-with-format "yyyy-MM-dd'T'ZZ" (->first-day-of-week date timezone-id) timezone-id)
-     :month   (trunc-with-format "yyyy-MM-01'T'ZZ" date timezone-id)
-     :quarter (trunc-with-format (format-string-for-quarter date timezone-id) date timezone-id)
-     :year    (trunc-with-format "yyyy-01-01'T'ZZ" date timezone-id))))
-
-
-(defn date-trunc-or-extract
-  "Apply date bucketing with UNIT to DATE. DATE defaults to now."
-  ([unit]
-   (date-trunc-or-extract unit (System/currentTimeMillis) "UTC"))
-  ([unit date]
-   (date-trunc-or-extract unit date "UTC"))
-  ([unit date timezone-id]
-   (cond
-     (= unit :default) date
-
-     (contains? date-extract-units unit)
-     (date-extract unit date timezone-id)
-
-     (contains? date-trunc-units unit)
-     (date-trunc unit date timezone-id))))
-
-(defn format-nanoseconds
-  "Format a time interval in nanoseconds to something more readable (µs/ms/etc.)
-   Useful for logging elapsed time when using `(System/nanotime)`"
-  ^String [nanoseconds]
-  (loop [n nanoseconds, [[unit divisor] & more] [[:ns 1000] [:µs 1000] [:ms 1000] [:s 60] [:mins 60] [:hours Integer/MAX_VALUE]]]
-    (if (and (> n divisor)
-             (seq more))
-      (recur (/ n divisor) more)
-      (format "%.0f %s" (double n) (name unit)))))
-
-
 ;;; ## Etc
 
 (defprotocol ^:private IClobToStr
@@ -767,19 +487,6 @@
     (integer? object-or-id) object-or-id
     :else                   (throw (Exception. (str "Not something with an ID: " object-or-id)))))
 
-(defmacro profile
-  "Like `clojure.core/time`, but lets you specify a MESSAGE that gets printed with the total time,
-   and formats the time nicely using `format-nanoseconds`."
-  {:style/indent 1}
-  ([form]
-   `(profile ~(str form) ~form))
-  ([message & body]
-   `(let [start-time# (System/nanoTime)]
-      (prog1 (do ~@body)
-        (println (format-color '~'green "%s took %s"
-                   ~message
-                   (format-nanoseconds (- (System/nanoTime) start-time#))))))))
-
 (def metabase-namespace-symbols
   "Delay to a vector of symbols of all Metabase namespaces, excluding test namespaces.
    This is intended for use by various routines that load related namespaces, such as task and events initialization.
@@ -885,58 +592,6 @@
     (apply update-in m k f args)
     m))
 
-(defn- str->date-time-with-formatters
-  "Attempt to parse `DATE-STR` using `FORMATTERS`. First successful
-  parse is returned, or nil"
-  ([formatters date-str]
-   (str->date-time-with-formatters formatters date-str nil))
-  ([formatters ^String date-str ^TimeZone tz]
-   (let [dtz (some-> tz .getID t/time-zone-for-id)]
-     (first
-      (for [formatter formatters
-            :let [formatter-with-tz (time/with-zone formatter dtz)
-                  parsed-date (ignore-exceptions (time/parse formatter-with-tz date-str))]
-            :when parsed-date]
-        parsed-date)))))
-
-(def ^:private date-time-with-millis-no-t
-  "This primary use for this formatter is for Dates formatted by the built-in SQLite functions"
-  (->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
-
-(def ^:private ordered-date-parsers
-  "When using clj-time.format/parse without a formatter, it tries all default formatters, but not ordered by how
-  likely the date formatters will succeed. This leads to very slow parsing as many attempts fail before the right one
-  is found. Using this retains that flexibility but improves performance by trying the most likely ones first"
-  (let [most-likely-default-formatters [:mysql :date-hour-minute-second :date-time :date
-                                        :basic-date-time :basic-date-time-no-ms
-                                        :date-time :date-time-no-ms]]
-    (concat (map time/formatters most-likely-default-formatters)
-            [date-time-with-millis-no-t]
-            (vals (apply dissoc time/formatters most-likely-default-formatters)))))
-
-(defn str->date-time
-  "Like clj-time.format/parse but uses an ordered list of parsers to be faster. Returns the parsed date or nil if it
-  was unable to be parsed."
-  (^org.joda.time.DateTime [^String date-str]
-   (str->date-time date-str nil))
-  ([^String date-str ^TimeZone tz]
-   (str->date-time-with-formatters ordered-date-parsers date-str tz)))
-
-(def ^:private ordered-time-parsers
-  (let [most-likely-default-formatters [:hour-minute :hour-minute-second :hour-minute-second-fraction]]
-    (concat (map time/formatters most-likely-default-formatters)
-            [(time/formatter "HH:mmZ") (time/formatter "HH:mm:SSZ") (time/formatter "HH:mm:SS.SSSZ")])))
-
-(defn str->time
-  "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil
-  if `TIME-STR` can't be parsed."
-  ([^String date-str]
-   (str->time date-str nil))
-  ([^String date-str ^TimeZone tz]
-   (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz)
-           coerce/to-long
-           Time.)))
-
 (defn index-of
   "Return index of the first element in `coll` for which `pred` reutrns true."
   [pred coll]
diff --git a/src/metabase/util/date.clj b/src/metabase/util/date.clj
new file mode 100644
index 00000000000..8f987cf9a16
--- /dev/null
+++ b/src/metabase/util/date.clj
@@ -0,0 +1,355 @@
+(ns metabase.util.date
+  (:require [clj-time
+             [coerce :as coerce]
+             [core :as t]
+             [format :as time]]
+            [clojure.tools.logging :as log]
+            [clojure.math.numeric-tower :as math]
+            [metabase.util :as u]
+            [puppetlabs.i18n.core :refer [trs]])
+  (:import [clojure.lang Keyword]
+           [java.util Calendar Date TimeZone]
+           [java.sql Time Timestamp]
+           [org.joda.time DateTime DateTimeZone]
+           [org.joda.time.format DateTimeFormatter]))
+
+
+(defprotocol ITimestampCoercible
+  "Coerce object to a `java.sql.Timestamp`."
+  (->Timestamp ^java.sql.Timestamp [this]
+    "Coerce this object to a `java.sql.Timestamp`. Strings are parsed as ISO-8601."))
+
+(declare str->date-time)
+
+(extend-protocol ITimestampCoercible
+  nil       (->Timestamp [_]
+              nil)
+  Timestamp (->Timestamp [this]
+              this)
+  Date       (->Timestamp [this]
+               (Timestamp. (.getTime this)))
+  ;; Number is assumed to be a UNIX timezone in milliseconds (UTC)
+  Number    (->Timestamp [this]
+              (Timestamp. this))
+  Calendar  (->Timestamp [this]
+              (->Timestamp (.getTime this)))
+  ;; Strings are expected to be in ISO-8601 format. `YYYY-MM-DD` strings *are* valid ISO-8601 dates.
+  String    (->Timestamp [this]
+              (->Timestamp (str->date-time this)))
+  DateTime  (->Timestamp [this]
+              (->Timestamp (.getMillis this))))
+
+
+(defprotocol IDateTimeFormatterCoercible
+  "Protocol for converting objects to `DateTimeFormatters`."
+  (->DateTimeFormatter ^org.joda.time.format.DateTimeFormatter [this]
+    "Coerce object to a `DateTimeFormatter`."))
+
+(extend-protocol IDateTimeFormatterCoercible
+  ;; Specify a format string like "yyyy-MM-dd"
+  String            (->DateTimeFormatter [this] (time/formatter this))
+  DateTimeFormatter (->DateTimeFormatter [this] this)
+  ;; Keyword will be used to get matching formatter from time/formatters
+  Keyword           (->DateTimeFormatter [this]
+                      (or (time/formatters this)
+                          (throw (Exception. (format "Invalid formatter name, must be one of:\n%s"
+                                                     (u/pprint-to-str (sort (keys time/formatters)))))))))
+
+
+(defn parse-date
+  "Parse a datetime string S with a custom DATE-FORMAT, which can be a format string, clj-time formatter keyword, or
+  anything else that can be coerced to a `DateTimeFormatter`.
+
+     (parse-date \"yyyyMMdd\" \"20160201\") -> #inst \"2016-02-01\"
+     (parse-date :date-time \"2016-02-01T00:00:00.000Z\") -> #inst \"2016-02-01\""
+  ^java.sql.Timestamp [date-format, ^String s]
+  (->Timestamp (time/parse (->DateTimeFormatter date-format) s)))
+
+
+(defprotocol ISO8601
+  "Protocol for converting objects to ISO8601 formatted strings."
+  (->iso-8601-datetime ^String [this timezone-id]
+    "Coerce object to an ISO8601 date-time string such as \"2015-11-18T23:55:03.841Z\" with a given TIMEZONE."))
+
+(def ^:private ^{:arglists '([timezone-id])} ISO8601Formatter
+  ;; memoize this because the formatters are static. They must be distinct per timezone though.
+  (memoize (fn [timezone-id]
+             (if timezone-id
+               (time/with-zone (time/formatters :date-time) (t/time-zone-for-id timezone-id))
+               (time/formatters :date-time)))))
+
+(extend-protocol ISO8601
+  nil                    (->iso-8601-datetime [_ _] nil)
+  java.util.Date         (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-date this)))
+  java.sql.Date          (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-date this)))
+  java.sql.Timestamp     (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-time this)))
+  org.joda.time.DateTime (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) this)))
+
+(def ^:private ^{:arglists '([timezone-id])} time-formatter
+  ;; memoize this because the formatters are static. They must be distinct per timezone though.
+  (memoize (fn [timezone-id]
+             (if timezone-id
+               (time/with-zone (time/formatters :time) (t/time-zone-for-id timezone-id))
+               (time/formatters :time)))))
+
+(defn format-time
+  "Returns a string representation of the time found in `T`"
+  [t time-zone-id]
+  (time/unparse (time-formatter time-zone-id) (coerce/to-date-time t)))
+
+(defn is-time?
+  "Returns true if `V` is a Time object"
+  [v]
+  (and v (instance? Time v)))
+
+;;; ## Date Stuff
+
+(defn is-temporal?
+  "Is VALUE an instance of a datetime class like `java.util.Date` or `org.joda.time.DateTime`?"
+  [v]
+  (or (instance? java.util.Date v)
+      (instance? org.joda.time.DateTime v)))
+
+(defn new-sql-timestamp
+  "`java.sql.Date` doesn't have an empty constructor so this is a convenience that lets you make one with the current
+  date. (Some DBs like Postgres will get snippy if you don't use a `java.sql.Timestamp`)."
+  ^java.sql.Timestamp []
+  (->Timestamp (System/currentTimeMillis)))
+
+(defn format-date
+  "Format DATE using a given DATE-FORMAT.
+
+   DATE is anything that can coerced to a `Timestamp` via `->Timestamp`, such as a `Date`, `Timestamp`,
+   `Long` (ms since the epoch), or an ISO-8601 `String`. DATE defaults to the current moment in time.
+
+   DATE-FORMAT is anything that can be passed to `->DateTimeFormatter`, such as `String`
+   (using [the usual date format args](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)),
+   `Keyword`, or `DateTimeFormatter`.
+
+
+     (format-date \"yyyy-MM-dd\")                        -> \"2015-11-18\"
+     (format-date :year (java.util.Date.))               -> \"2015\"
+     (format-date :date-time (System/currentTimeMillis)) -> \"2015-11-18T23:55:03.841Z\""
+  (^String [date-format]
+   (format-date date-format (System/currentTimeMillis)))
+  (^String [date-format date]
+   (time/unparse (->DateTimeFormatter date-format) (coerce/from-sql-time (->Timestamp date)))))
+
+(def ^{:arglists '([] [date])} date->iso-8601
+  "Format DATE a an ISO-8601 string."
+  (partial format-date :date-time))
+
+(defn date-string?
+  "Is S a valid ISO 8601 date string?"
+  [^String s]
+  (boolean (when (string? s)
+             (u/ignore-exceptions
+               (->Timestamp s)))))
+
+(defn ->Date
+  "Coerece DATE to a `java.util.Date`."
+  (^java.util.Date []
+   (java.util.Date.))
+  (^java.util.Date [date]
+   (java.util.Date. (.getTime (->Timestamp date)))))
+
+
+(defn ->Calendar
+  "Coerce DATE to a `java.util.Calendar`."
+  (^java.util.Calendar []
+   (doto (Calendar/getInstance)
+     (.setTimeZone (TimeZone/getTimeZone "UTC"))))
+  (^java.util.Calendar [date]
+   (doto (->Calendar)
+     (.setTime (->Timestamp date))))
+  (^java.util.Calendar [date, ^String timezone-id]
+   (doto (->Calendar date)
+     (.setTimeZone (TimeZone/getTimeZone timezone-id)))))
+
+
+(defn relative-date
+  "Return a new `Timestamp` relative to the current time using a relative date UNIT.
+
+     (relative-date :year -1) -> #inst 2014-11-12 ..."
+  (^java.sql.Timestamp [unit amount]
+   (relative-date unit amount (Calendar/getInstance)))
+  (^java.sql.Timestamp [unit amount date]
+   (let [cal               (->Calendar date)
+         [unit multiplier] (case unit
+                             :second  [Calendar/SECOND 1]
+                             :minute  [Calendar/MINUTE 1]
+                             :hour    [Calendar/HOUR   1]
+                             :day     [Calendar/DATE   1]
+                             :week    [Calendar/DATE   7]
+                             :month   [Calendar/MONTH  1]
+                             :quarter [Calendar/MONTH  3]
+                             :year    [Calendar/YEAR   1])]
+     (.set cal unit (+ (.get cal unit)
+                       (* amount multiplier)))
+     (->Timestamp cal))))
+
+
+(def ^:private ^:const date-extract-units
+  #{:minute-of-hour :hour-of-day :day-of-week :day-of-month :day-of-year :week-of-year :month-of-year :quarter-of-year
+    :year})
+
+(defn date-extract
+  "Extract UNIT from DATE. DATE defaults to now.
+
+     (date-extract :year) -> 2015"
+  ([unit]
+   (date-extract unit (System/currentTimeMillis) "UTC"))
+  ([unit date]
+   (date-extract unit date "UTC"))
+  ([unit date timezone-id]
+   (let [cal (->Calendar date timezone-id)]
+     (case unit
+       :minute-of-hour  (.get cal Calendar/MINUTE)
+       :hour-of-day     (.get cal Calendar/HOUR_OF_DAY)
+       ;; 1 = Sunday <-> 6 = Saturday
+       :day-of-week     (.get cal Calendar/DAY_OF_WEEK)
+       :day-of-month    (.get cal Calendar/DAY_OF_MONTH)
+       :day-of-year     (.get cal Calendar/DAY_OF_YEAR)
+       ;; 1 = First week of year
+       :week-of-year    (.get cal Calendar/WEEK_OF_YEAR)
+       :month-of-year   (inc (.get cal Calendar/MONTH))
+       :quarter-of-year (let [month (date-extract :month-of-year date timezone-id)]
+                          (int (/ (+ 2 month)
+                                  3)))
+       :year            (.get cal Calendar/YEAR)))))
+
+
+(def ^:private ^:const date-trunc-units
+  #{:minute :hour :day :week :month :quarter :year})
+
+(defn- trunc-with-format [format-string date timezone-id]
+  (->Timestamp (format-date (time/with-zone (time/formatter format-string)
+                              (t/time-zone-for-id timezone-id))
+                            date)))
+
+(defn- trunc-with-floor [date amount-ms]
+  (->Timestamp (* (math/floor (/ (.getTime (->Timestamp date))
+                                 amount-ms))
+                  amount-ms)))
+
+(defn- ->first-day-of-week [date timezone-id]
+  (let [day-of-week (date-extract :day-of-week date timezone-id)]
+    (relative-date :day (- (dec day-of-week)) date)))
+
+(defn- format-string-for-quarter ^String [date timezone-id]
+  (let [year    (date-extract :year date timezone-id)
+        quarter (date-extract :quarter-of-year date timezone-id)
+        month   (- (* 3 quarter) 2)]
+    (format "%d-%02d-01'T'ZZ" year month)))
+
+(defn date-trunc
+  "Truncate DATE to UNIT. DATE defaults to now.
+
+     (date-trunc :month).
+     ;; -> #inst \"2015-11-01T00:00:00\""
+  (^java.sql.Timestamp [unit]
+   (date-trunc unit (System/currentTimeMillis) "UTC"))
+  (^java.sql.Timestamp [unit date]
+   (date-trunc unit date "UTC"))
+  (^java.sql.Timestamp [unit date timezone-id]
+   (case unit
+     ;; For minute and hour truncation timezone should not be taken into account
+     :minute  (trunc-with-floor date (* 60 1000))
+     :hour    (trunc-with-floor date (* 60 60 1000))
+     :day     (trunc-with-format "yyyy-MM-dd'T'ZZ" date timezone-id)
+     :week    (trunc-with-format "yyyy-MM-dd'T'ZZ" (->first-day-of-week date timezone-id) timezone-id)
+     :month   (trunc-with-format "yyyy-MM-01'T'ZZ" date timezone-id)
+     :quarter (trunc-with-format (format-string-for-quarter date timezone-id) date timezone-id)
+     :year    (trunc-with-format "yyyy-01-01'T'ZZ" date timezone-id))))
+
+
+(defn date-trunc-or-extract
+  "Apply date bucketing with UNIT to DATE. DATE defaults to now."
+  ([unit]
+   (date-trunc-or-extract unit (System/currentTimeMillis) "UTC"))
+  ([unit date]
+   (date-trunc-or-extract unit date "UTC"))
+  ([unit date timezone-id]
+   (cond
+     (= unit :default) date
+
+     (contains? date-extract-units unit)
+     (date-extract unit date timezone-id)
+
+     (contains? date-trunc-units unit)
+     (date-trunc unit date timezone-id))))
+
+(defn format-nanoseconds
+  "Format a time interval in nanoseconds to something more readable (µs/ms/etc.)
+   Useful for logging elapsed time when using `(System/nanotime)`"
+  ^String [nanoseconds]
+  (loop [n nanoseconds, [[unit divisor] & more] [[:ns 1000] [:µs 1000] [:ms 1000] [:s 60] [:mins 60] [:hours Integer/MAX_VALUE]]]
+    (if (and (> n divisor)
+             (seq more))
+      (recur (/ n divisor) more)
+      (format "%.0f %s" (double n) (name unit)))))
+
+(defmacro profile
+  "Like `clojure.core/time`, but lets you specify a MESSAGE that gets printed with the total time,
+   and formats the time nicely using `format-nanoseconds`."
+  {:style/indent 1}
+  ([form]
+   `(profile ~(str form) ~form))
+  ([message & body]
+   `(let [start-time# (System/nanoTime)]
+      (u/prog1 (do ~@body)
+        (println (u/format-color '~'green "%s took %s"
+                   ~message
+                   (format-nanoseconds (- (System/nanoTime) start-time#))))))))
+
+(defn- str->date-time-with-formatters
+  "Attempt to parse `DATE-STR` using `FORMATTERS`. First successful
+  parse is returned, or nil"
+  ([formatters date-str]
+   (str->date-time-with-formatters formatters date-str nil))
+  ([formatters ^String date-str ^TimeZone tz]
+   (let [dtz (some-> tz .getID t/time-zone-for-id)]
+     (first
+      (for [formatter formatters
+            :let [formatter-with-tz (time/with-zone formatter dtz)
+                  parsed-date (u/ignore-exceptions (time/parse formatter-with-tz date-str))]
+            :when parsed-date]
+        parsed-date)))))
+
+(def ^:private date-time-with-millis-no-t
+  "This primary use for this formatter is for Dates formatted by the built-in SQLite functions"
+  (->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
+
+(def ^:private ordered-date-parsers
+  "When using clj-time.format/parse without a formatter, it tries all default formatters, but not ordered by how
+  likely the date formatters will succeed. This leads to very slow parsing as many attempts fail before the right one
+  is found. Using this retains that flexibility but improves performance by trying the most likely ones first"
+  (let [most-likely-default-formatters [:mysql :date-hour-minute-second :date-time :date
+                                        :basic-date-time :basic-date-time-no-ms
+                                        :date-time :date-time-no-ms]]
+    (concat (map time/formatters most-likely-default-formatters)
+            [date-time-with-millis-no-t]
+            (vals (apply dissoc time/formatters most-likely-default-formatters)))))
+
+(defn str->date-time
+  "Like clj-time.format/parse but uses an ordered list of parsers to be faster. Returns the parsed date or nil if it
+  was unable to be parsed."
+  (^org.joda.time.DateTime [^String date-str]
+   (str->date-time date-str nil))
+  ([^String date-str ^TimeZone tz]
+   (str->date-time-with-formatters ordered-date-parsers date-str tz)))
+
+(def ^:private ordered-time-parsers
+  (let [most-likely-default-formatters [:hour-minute :hour-minute-second :hour-minute-second-fraction]]
+    (concat (map time/formatters most-likely-default-formatters)
+            [(time/formatter "HH:mmZ") (time/formatter "HH:mm:SSZ") (time/formatter "HH:mm:SS.SSSZ")])))
+
+(defn str->time
+  "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil
+  if `TIME-STR` can't be parsed."
+  ([^String date-str]
+   (str->time date-str nil))
+  ([^String date-str ^TimeZone tz]
+   (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz)
+           coerce/to-long
+           Time.)))
diff --git a/test/metabase/api/activity_test.clj b/test/metabase/api/activity_test.clj
index 4343e153f55..f3dd9990af5 100644
--- a/test/metabase/api/activity_test.clj
+++ b/test/metabase/api/activity_test.clj
@@ -10,6 +10,7 @@
             [metabase.test.data.users :refer :all]
             [metabase.test.util :as tu :refer [match-$]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -22,19 +23,19 @@
 ;; NOTE: timestamp matching was being a real PITA so I cheated a bit.  ideally we'd fix that
 (tt/expect-with-temp [Activity [activity1 {:topic     "install"
                                            :details   {}
-                                           :timestamp (u/->Timestamp "2015-09-09T12:13:14.888Z")}]
+                                           :timestamp (du/->Timestamp "2015-09-09T12:13:14.888Z")}]
                       Activity [activity2 {:topic     "dashboard-create"
                                            :user_id   (user->id :crowberto)
                                            :model     "dashboard"
                                            :model_id  1234
                                            :details   {:description  "Because I can!"
                                                        :name         "Bwahahaha"}
-                                           :timestamp (u/->Timestamp "2015-09-10T18:53:01.632Z")}]
+                                           :timestamp (du/->Timestamp "2015-09-10T18:53:01.632Z")}]
                       Activity [activity3 {:topic     "user-joined"
                                            :user_id   (user->id :rasta)
                                            :model     "user"
                                            :details   {}
-                                           :timestamp (u/->Timestamp "2015-09-10T05:33:43.641Z")}]]
+                                           :timestamp (du/->Timestamp "2015-09-10T05:33:43.641Z")}]]
   [(match-$ (Activity (:id activity2))
      {:id           $
       :topic        "dashboard-create"
@@ -116,7 +117,7 @@
     :user_id  user
     :model    model
     :model_id model-id
-    :timestamp (u/new-sql-timestamp))
+    :timestamp (du/new-sql-timestamp))
   ;; we sleep a bit to ensure no events have the same timestamp
   ;; sadly, MySQL doesn't support milliseconds so we have to wait a second
   ;; otherwise our records are out of order and this test fails :(
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 5ddc3f6333f..bff21c86d4d 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -29,6 +29,7 @@
              [data :as data :refer :all]
              [util :as tu :refer [match-$ random-name]]]
             [metabase.test.data.users :refer :all]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt])
   (:import java.io.ByteArrayInputStream
@@ -140,15 +141,15 @@
                       ;; 3 was viewed most recently, followed by 4, then 1. Card 2 was viewed by a different user so
                       ;; shouldn't be returned
                       ViewLog  [_ {:model "card", :model_id card-1-id, :user_id (user->id :rasta)
-                                   :timestamp (u/->Timestamp #inst "2015-12-01")}]
+                                   :timestamp (du/->Timestamp "2015-12-01")}]
                       ViewLog  [_ {:model "card", :model_id card-2-id, :user_id (user->id :trashbird)
-                                   :timestamp (u/->Timestamp #inst "2016-01-01")}]
+                                   :timestamp (du/->Timestamp "2016-01-01")}]
                       ViewLog  [_ {:model "card", :model_id card-3-id, :user_id (user->id :rasta)
-                                   :timestamp (u/->Timestamp #inst "2016-02-01")}]
+                                   :timestamp (du/->Timestamp "2016-02-01")}]
                       ViewLog  [_ {:model "card", :model_id card-4-id, :user_id (user->id :rasta)
-                                   :timestamp (u/->Timestamp #inst "2016-03-01")}]
+                                   :timestamp (du/->Timestamp "2016-03-01")}]
                       ViewLog  [_ {:model "card", :model_id card-3-id, :user_id (user->id :rasta)
-                                   :timestamp (u/->Timestamp #inst "2016-04-01")}]]
+                                   :timestamp (du/->Timestamp "2016-04-01")}]]
   [card-3-id card-4-id card-1-id]
   (mapv :id ((user->client :rasta) :get 200 "card", :f :recent)))
 
diff --git a/test/metabase/driver/googleanalytics_test.clj b/test/metabase/driver/googleanalytics_test.clj
index ceb95d91e17..b6cb1885e42 100644
--- a/test/metabase/driver/googleanalytics_test.clj
+++ b/test/metabase/driver/googleanalytics_test.clj
@@ -10,6 +10,7 @@
             [metabase.query-processor.interface :as qpi]
             [metabase.test.data.users :as users]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -128,8 +129,8 @@
 
 ;; relative date -- last month
 (expect
-  (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month -1))
-             :end-date   (u/format-date "yyyy-MM-01")})
+  (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month -1))
+             :end-date   (du/format-date "yyyy-MM-01")})
   (mbql->native {:query {:filter {:filter-type :=
                                   :field       (ga-date-field :month)
                                   :value       (qpi/map->RelativeDateTimeValue {:amount -1
@@ -138,8 +139,8 @@
 
 ;; relative date -- this month
 (expect
-  (ga-query {:start-date (u/format-date "yyyy-MM-01")
-             :end-date   (u/format-date "yyyy-MM-01" (u/relative-date :month 1))})
+  (ga-query {:start-date (du/format-date "yyyy-MM-01")
+             :end-date   (du/format-date "yyyy-MM-01" (du/relative-date :month 1))})
   (mbql->native {:query {:filter {:filter-type :=
                                   :field       (ga-date-field :month)
                                   :value       (qpi/map->RelativeDateTimeValue {:amount 0
@@ -148,8 +149,8 @@
 
 ;; relative date -- next month
 (expect
-  (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month 1))
-             :end-date   (u/format-date "yyyy-MM-01" (u/relative-date :month 2))})
+  (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month 1))
+             :end-date   (du/format-date "yyyy-MM-01" (du/relative-date :month 2))})
   (mbql->native {:query {:filter {:filter-type :=
                                   :field       (ga-date-field :month)
                                   :value       (qpi/map->RelativeDateTimeValue {:amount 1
@@ -158,8 +159,8 @@
 
 ;; relative date -- 2 months from now
 (expect
-  (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month 2))
-             :end-date   (u/format-date "yyyy-MM-01" (u/relative-date :month 3))})
+  (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month 2))
+             :end-date   (du/format-date "yyyy-MM-01" (du/relative-date :month 3))})
   (mbql->native {:query {:filter {:filter-type :=
                                   :field       (ga-date-field :month)
                                   :value       (qpi/map->RelativeDateTimeValue {:amount 2
@@ -168,8 +169,8 @@
 
 ;; relative date -- last year
 (expect
-  (ga-query {:start-date (u/format-date "yyyy-01-01" (u/relative-date :year -1))
-             :end-date   (u/format-date "yyyy-01-01")})
+  (ga-query {:start-date (du/format-date "yyyy-01-01" (du/relative-date :year -1))
+             :end-date   (du/format-date "yyyy-01-01")})
   (mbql->native {:query {:filter {:filter-type :=
                                   :field       (ga-date-field :year)
                                   :value       (qpi/map->RelativeDateTimeValue {:amount -1
diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj
index 7840327daf2..534f9418aac 100644
--- a/test/metabase/driver/mysql_test.clj
+++ b/test/metabase/driver/mysql_test.clj
@@ -16,7 +16,7 @@
              [datasets :refer [expect-with-engine]]
              [interface :refer [def-database-definition]]]
             [metabase.test.util :as tu]
-            [metabase.util :as u]
+            [metabase.util.date :as du]
             [honeysql.core :as hsql]
             [toucan.db :as db]
             [toucan.util.test :as tt])
@@ -99,8 +99,8 @@
     (tu/db-timezone-id)))
 
 
-(def before-daylight-savings (u/str->date-time "2018-03-10 10:00:00"))
-(def after-daylight-savings (u/str->date-time "2018-03-12 10:00:00"))
+(def before-daylight-savings (du/str->date-time "2018-03-10 10:00:00"))
+(def after-daylight-savings (du/str->date-time "2018-03-12 10:00:00"))
 
 (expect (#'mysql/timezone-id->offset-str "US/Pacific" before-daylight-savings) "-08:00")
 (expect (#'mysql/timezone-id->offset-str "US/Pacific" after-daylight-savings)  "-07:00")
@@ -114,18 +114,18 @@
 ;; make sure DateTime types generate appropriate SQL...
 ;; ...with no report-timezone set
 (expect
-  ["?" (u/->Timestamp "2018-01-03")]
+  ["?" (du/->Timestamp "2018-01-03")]
   (tu/with-temporary-setting-values [report-timezone nil]
-    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03")))))
+    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03")))))
 
 ;; ...with a report-timezone set
 (expect
   ["convert_tz('2018-01-03T00:00:00.000', '+00:00', '-08:00')"]
   (tu/with-temporary-setting-values [report-timezone "US/Pacific"]
-    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03")))))
+    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03")))))
 
 ;; ...with a report-timezone set to the same as the system timezone (shouldn't need to do TZ conversion)
 (expect
-  ["?" (u/->Timestamp "2018-01-03")]
+  ["?" (du/->Timestamp #inst "2018-01-03")]
   (tu/with-temporary-setting-values [report-timezone "UTC"]
-    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03")))))
+    (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03")))))
diff --git a/test/metabase/http_client.clj b/test/metabase/http_client.clj
index 9de0ef75217..81bb8c862b7 100644
--- a/test/metabase/http_client.clj
+++ b/test/metabase/http_client.clj
@@ -6,7 +6,8 @@
             [clojure.tools.logging :as log]
             [metabase
              [config :as config]
-             [util :as u]]))
+             [util :as u]]
+            [metabase.util.date :as du]))
 
 ;;; build-url
 
@@ -40,7 +41,7 @@
         (map? response) (->> response
                              (map (fn [[k v]]
                                     {k (cond
-                                         (contains? auto-deserialize-dates-keys k) (u/->Timestamp v)
+                                         (contains? auto-deserialize-dates-keys k) (du/->Timestamp v)
                                          (coll? v) (auto-deserialize-dates v)
                                          :else v)}))
                              (into {}))
diff --git a/test/metabase/middleware_test.clj b/test/metabase/middleware_test.clj
index 1e3b37e33a2..3d3f21bdd5f 100644
--- a/test/metabase/middleware_test.clj
+++ b/test/metabase/middleware_test.clj
@@ -13,6 +13,7 @@
             [metabase.models.session :refer [Session]]
             [metabase.test.data.users :refer :all]
             [metabase.test.async :refer [while-with-timeout]]
+            [metabase.util.date :as du]
             [ring.mock.request :as mock]
             [ring.util.response :as resp]
             [toucan.db :as db]
@@ -75,7 +76,7 @@
 (expect
   (user->id :rasta)
   (let [session-id (random-session-id)]
-    (db/simple-insert! Session, :id session-id, :user_id (user->id :rasta), :created_at (u/new-sql-timestamp))
+    (db/simple-insert! Session, :id session-id, :user_id (user->id :rasta), :created_at (du/new-sql-timestamp))
     (-> (auth-enforced-handler (request-with-session-id session-id))
         :metabase-user-id)))
 
@@ -96,7 +97,7 @@
 ;; NOTE that :trashbird is our INACTIVE test user
 (expect response-unauthentic
   (let [session-id (random-session-id)]
-    (db/simple-insert! Session, :id session-id, :user_id (user->id :trashbird), :created_at (u/new-sql-timestamp))
+    (db/simple-insert! Session, :id session-id, :user_id (user->id :trashbird), :created_at (du/new-sql-timestamp))
     (auth-enforced-handler (request-with-session-id session-id))))
 
 
diff --git a/test/metabase/models/dependency_test.clj b/test/metabase/models/dependency_test.clj
index 07eb81cb70c..661625c3498 100644
--- a/test/metabase/models/dependency_test.clj
+++ b/test/metabase/models/dependency_test.clj
@@ -3,6 +3,7 @@
             [metabase.models.dependency :refer :all]
             [metabase.test.data :refer :all]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan
              [db :as db]
              [models :as models]]
@@ -49,12 +50,12 @@
                                  :model_id           4
                                  :dependent_on_model "test"
                                  :dependent_on_id    1
-                                 :created_at         (u/new-sql-timestamp)}]
+                                 :created_at         (du/new-sql-timestamp)}]
                   Dependency [_ {:model              "Mock"
                                  :model_id           4
                                  :dependent_on_model "foobar"
                                  :dependent_on_id    13
-                                 :created_at         (u/new-sql-timestamp)}]]
+                                 :created_at         (du/new-sql-timestamp)}]]
     (format-dependencies (retrieve-dependencies Mock 4))))
 
 
@@ -101,6 +102,6 @@
       :model_id           1
       :dependent_on_model "test"
       :dependent_on_id    5
-      :created_at         (u/new-sql-timestamp))
+      :created_at         (du/new-sql-timestamp))
     (update-dependencies! Mock 1 {:test [1 2]})
     (format-dependencies (db/select Dependency, :model "Mock", :model_id 1))))
diff --git a/test/metabase/models/session_test.clj b/test/metabase/models/session_test.clj
index 116d3755631..b77375357ef 100644
--- a/test/metabase/models/session_test.clj
+++ b/test/metabase/models/session_test.clj
@@ -5,6 +5,7 @@
              [user :refer [User]]]
             [metabase.test.util :as tu]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -18,17 +19,17 @@
     (db/simple-insert-many! Session
       [{:id         "the-greatest-day-ever"
         :user_id    user-id
-        :created_at (u/->Timestamp "1980-10-19T05:05:05.000Z")}
+        :created_at (du/->Timestamp "1980-10-19T05:05:05.000Z")}
        {:id         "even-more-greatness"
         :user_id    user-id
-        :created_at (u/->Timestamp "1980-10-19T05:08:05.000Z")}
+        :created_at (du/->Timestamp "1980-10-19T05:08:05.000Z")}
        {:id         "the-world-of-bi-changes-forever"
         :user_id    user-id
-        :created_at (u/->Timestamp "2015-10-21")}
+        :created_at (du/->Timestamp "2015-10-21")}
        {:id         "something-could-have-happened"
         :user_id    user-id
-        :created_at (u/->Timestamp "1999-12-31")}
+        :created_at (du/->Timestamp "1999-12-31")}
        {:id         "now"
         :user_id    user-id
-        :created_at (u/new-sql-timestamp)}])
+        :created_at (du/new-sql-timestamp)}])
     (first-session-for-user user-id)))
diff --git a/test/metabase/query_processor/expand_resolve_test.clj b/test/metabase/query_processor/expand_resolve_test.clj
index 5bd8b51f0a0..4d6ae514890 100644
--- a/test/metabase/query_processor/expand_resolve_test.clj
+++ b/test/metabase/query_processor/expand_resolve_test.clj
@@ -12,7 +12,8 @@
             [metabase.test
              [data :as data :refer :all]
              [util :as tu]]
-            [metabase.test.data.dataset-definitions :as defs]))
+            [metabase.test.data.dataset-definitions :as defs]
+            [metabase.util.date :as du]))
 
 ;; this is here because expectations has issues comparing and object w/ a map and most of the output
 ;; below has objects for the various place holders in the expanded/resolved query
@@ -239,7 +240,7 @@
                                                                                     :type   {:type/DateTime {:earliest "2014-01-01T00:00:00.000Z"
                                                                                                              :latest   "2014-12-05T00:00:00.000Z"}}}})
                                                 :unit  :year}
-                                  :value       {:value (u/->Timestamp "1980-01-01")
+                                  :value       {:value (du/->Timestamp "1980-01-01")
                                                 :field {:field
                                                         (merge field-defaults
                                                                {:field-id           (id :users :last_login)
diff --git a/test/metabase/query_processor/middleware/fetch_source_query_test.clj b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
index 569716cf3be..0b9d4936ddb 100644
--- a/test/metabase/query_processor/middleware/fetch_source_query_test.clj
+++ b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
@@ -10,6 +10,7 @@
              [database :as database]]
             [metabase.query-processor.middleware.fetch-source-query :as fetch-source-query]
             [metabase.test.data :as data]
+            [metabase.util.date :as du]
             [toucan.util.test :as tt]))
 
 (def ^:private ^{:arglists '([query])} fetch-source-query (fetch-source-query/fetch-source-query identity))
@@ -79,9 +80,9 @@
    {:source-query {:source-table {:schema "PUBLIC" :name "CHECKINS" :id (data/id :checkins)}, :join-tables nil}
     :filter {:filter-type :between,
              :field {:field-name "date", :base-type :type/Date},
-             :min-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-01-01"))
+             :min-val {:value (tcoerce/to-timestamp (du/str->date-time "2015-01-01"))
                        :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}},
-             :max-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-02-01"))
+             :max-val {:value (tcoerce/to-timestamp (du/str->date-time "2015-02-01"))
                        :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}}}})
   (tt/with-temp Card [card {:dataset_query {:database (data/id)
                                             :type     :query
diff --git a/test/metabase/query_processor/middleware/parameters/mbql_test.clj b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
index 5193b192a54..9dfd8caa46f 100644
--- a/test/metabase/query_processor/middleware/parameters/mbql_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
@@ -8,7 +8,8 @@
             [metabase.query-processor.middleware.expand :as ql]
             [metabase.query-processor.middleware.parameters.mbql :as mbql-params :refer :all]
             [metabase.test.data :as data]
-            [metabase.test.data.datasets :as datasets]))
+            [metabase.test.data.datasets :as datasets]
+            [metabase.util.date :as du]))
 
 (defn- expand-parameters [query]
   (expand (dissoc query :parameters) (:parameters query)))
@@ -232,10 +233,10 @@
   {:query  (str "SELECT count(*) AS \"count\" FROM \"PUBLIC\".\"CHECKINS\" "
                 "WHERE (CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date) "
                 "OR CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date))")
-   :params [(u/->Timestamp #inst "2014-06-01")
-            (u/->Timestamp #inst "2014-06-30")
-            (u/->Timestamp #inst "2015-06-01")
-            (u/->Timestamp #inst "2015-06-30")]}
+   :params [(du/->Timestamp #inst "2014-06-01")
+            (du/->Timestamp #inst "2014-06-30")
+            (du/->Timestamp #inst "2015-06-01")
+            (du/->Timestamp #inst "2015-06-30")]}
   (let [inner-query (data/query checkins
                       (ql/aggregation (ql/count)))
         outer-query (-> (data/wrap-inner-query inner-query)
diff --git a/test/metabase/sync/analyze/classify_test.clj b/test/metabase/sync/analyze/classify_test.clj
index 3ba4aec617c..ba6e087a2e2 100644
--- a/test/metabase/sync/analyze/classify_test.clj
+++ b/test/metabase/sync/analyze/classify_test.clj
@@ -6,6 +6,7 @@
             [metabase.sync.analyze.classify :as classify]
             [metabase.sync.interface :as i]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.util.test :as tt]))
 
 ;; Check that only the right Fields get classified
@@ -21,7 +22,7 @@
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Current fingerprint, already analzed"
                               :fingerprint_version Short/MAX_VALUE
-                              :last_analyzed       (u/->Timestamp "2017-08-09")}]
+                              :last_analyzed       (du/->Timestamp "2017-08-09")}]
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Old fingerprint, not analyzed"
                               :fingerprint_version (dec Short/MAX_VALUE)
@@ -29,6 +30,6 @@
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Old fingerprint, already analzed"
                               :fingerprint_version (dec Short/MAX_VALUE)
-                              :last_analyzed       (u/->Timestamp "2017-08-09")}]]
+                              :last_analyzed       (du/->Timestamp "2017-08-09")}]]
       (for [field (#'classify/fields-to-classify table)]
         (:name field)))))
diff --git a/test/metabase/sync/analyze/fingerprint_test.clj b/test/metabase/sync/analyze/fingerprint_test.clj
index caa8daaf8f6..0bef3751bce 100644
--- a/test/metabase/sync/analyze/fingerprint_test.clj
+++ b/test/metabase/sync/analyze/fingerprint_test.clj
@@ -10,6 +10,7 @@
             [metabase.test.data :as data]
             [metabase.test.util]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -212,7 +213,7 @@
                               :table_id            (data/id :venues)
                               :fingerprint         nil
                               :fingerprint_version 1
-                              :last_analyzed       (u/->Timestamp "2017-08-09")}]
+                              :last_analyzed       (du/->Timestamp "2017-08-09")}]
     (with-redefs [i/latest-fingerprint-version 3
                   sample/sample-fields         (constantly [[field [1 2 3 4 5]]])
                   fingerprint/fingerprint      (constantly {:experimental {:fake-fingerprint? true}})]
diff --git a/test/metabase/sync/analyze_test.clj b/test/metabase/sync/analyze_test.clj
index 04b0ae514c8..df55f111cd0 100644
--- a/test/metabase/sync/analyze_test.clj
+++ b/test/metabase/sync/analyze_test.clj
@@ -10,11 +10,12 @@
              [sync-metadata :as sync-metadata]]
             [metabase.test.data :as data]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
 (def ^:private fake-analysis-completion-date
-  (u/->Timestamp "2017-08-01"))
+  (du/->Timestamp "2017-08-01"))
 
 ;; Check that Fields do *not* get analyzed if they're not newly created and fingerprint version is current
 (expect
@@ -61,7 +62,7 @@
 (expect
   #{"Current fingerprint, not analyzed"}
   (with-redefs [i/latest-fingerprint-version Short/MAX_VALUE
-                u/new-sql-timestamp          (constantly (u/->Timestamp "1999-01-01"))]
+                du/new-sql-timestamp         (constantly (du/->Timestamp "1999-01-01"))]
     (tt/with-temp* [Table [table]
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Current fingerprint, not analyzed"
@@ -70,7 +71,7 @@
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Current fingerprint, already analzed"
                               :fingerprint_version Short/MAX_VALUE
-                              :last_analyzed       (u/->Timestamp "2017-08-09")}]
+                              :last_analyzed       (du/->Timestamp "2017-08-09")}]
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Old fingerprint, not analyzed"
                               :fingerprint_version (dec Short/MAX_VALUE)
@@ -78,6 +79,6 @@
                     Field [_ {:table_id            (u/get-id table)
                               :name                "Old fingerprint, already analzed"
                               :fingerprint_version (dec Short/MAX_VALUE)
-                              :last_analyzed       (u/->Timestamp "2017-08-09")}]]
+                              :last_analyzed       (du/->Timestamp "2017-08-09")}]]
       (#'analyze/update-fields-last-analyzed! table)
-      (db/select-field :name Field :last_analyzed (u/new-sql-timestamp)))))
+      (db/select-field :name Field :last_analyzed (du/new-sql-timestamp)))))
diff --git a/test/metabase/task/sync_databases_test.clj b/test/metabase/task/sync_databases_test.clj
index 10b7969357a..0f032d795b7 100644
--- a/test/metabase/task/sync_databases_test.clj
+++ b/test/metabase/task/sync_databases_test.clj
@@ -8,6 +8,7 @@
             [metabase.task.sync-databases :as sync-db]
             [metabase.test.util :as tu]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [toucan.db :as db]
             [toucan.util.test :as tt])
   (:import [metabase.task.sync_databases SyncAndAnalyzeDatabase UpdateFieldValues]))
@@ -155,7 +156,7 @@
            :ran-update-field-values? (not (zero? @update-field-values-counter))})))))
 
 (defn- cron-schedule-for-next-year []
-  (format "0 15 10 * * ? %d" (inc (u/date-extract :year))))
+  (format "0 15 10 * * ? %d" (inc (du/date-extract :year))))
 
 ;; Make sure that a database that *is* marked full sync *will* get analyzed
 (expect
diff --git a/test/metabase/test/data/bigquery.clj b/test/metabase/test/data/bigquery.clj
index b6318d19b17..aee81562e0d 100644
--- a/test/metabase/test/data/bigquery.clj
+++ b/test/metabase/test/data/bigquery.clj
@@ -11,6 +11,7 @@
              [datasets :as datasets]
              [interface :as i]]
             [metabase.util :as u]
+            [metabase.util.date :as du]
             [metabase.util.schema :as su]
             [schema.core :as s])
   (:import com.google.api.client.util.DateTime
@@ -94,7 +95,7 @@
   "Convert the HoneySQL form we normally use to wrap a `Timestamp` to a Google `DateTime`."
   [{[{s :literal}] :args}]
   {:pre [(string? s) (seq s)]}
-  (DateTime. (u/->Timestamp (str/replace s #"'" ""))))
+  (DateTime. (du/->Timestamp (str/replace s #"'" ""))))
 
 
 (defn- insert-data! [^String dataset-id, ^String table-id, row-maps]
diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj
index b21330c2e07..e6c4522d770 100644
--- a/test/metabase/test/data/generic_sql.clj
+++ b/test/metabase/test/data/generic_sql.clj
@@ -11,7 +11,9 @@
             [metabase.driver.generic-sql.query-processor :as sqlqp]
             [metabase.test.data.interface :as i]
             [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import clojure.lang.Keyword
            java.sql.SQLException
            [metabase.test.data.interface DatabaseDefinition FieldDefinition TableDefinition]))
@@ -173,7 +175,7 @@
       (zipmap fields-for-insert (for [v row]
                                   (if (and (not (instance? java.sql.Time v))
                                            (instance? java.util.Date v))
-                                    (u/->Timestamp v)
+                                    (du/->Timestamp v)
                                     v))))))
 
 (defn load-data-add-ids
@@ -322,7 +324,7 @@
     (execute-sql! driver :db dbdef (s/join ";\n" (map hx/unescape-dots @statements))))
   ;; Now load the data for each Table
   (doseq [tabledef table-definitions]
-    (u/profile (format "load-data for %s %s %s" (name driver) (:database-name dbdef) (:table-name tabledef))
+    (du/profile (format "load-data for %s %s %s" (name driver) (:database-name dbdef) (:table-name tabledef))
       (load-data! driver dbdef tabledef))))
 
 (def IDriverTestExtensionsMixin
diff --git a/test/metabase/test/data/sqlite.clj b/test/metabase/test/data/sqlite.clj
index 0c2f3839f44..c637165c413 100644
--- a/test/metabase/test/data/sqlite.clj
+++ b/test/metabase/test/data/sqlite.clj
@@ -4,7 +4,9 @@
              [generic-sql :as generic]
              [interface :as i]]
             [metabase.util :as u]
-            [metabase.util.honeysql-extensions :as hx])
+            [metabase.util
+             [date :as du]
+             [honeysql-extensions :as hx]])
   (:import metabase.driver.sqlite.SQLiteDriver))
 
 (defn- database->connection-details [context dbdef]
@@ -29,10 +31,10 @@
                (into {} (for [[k v] row]
                           [k (cond
                                (instance? java.sql.Time v)
-                               (hsql/call :time (hx/literal (u/format-time v "UTC")))
+                               (hsql/call :time (hx/literal (du/format-time v "UTC")))
 
                                (instance? java.util.Date v)
-                               (hsql/call :datetime (hx/literal (u/date->iso-8601 v)))
+                               (hsql/call :datetime (hx/literal (du/date->iso-8601 v)))
 
                                :else v)]))))))
 
diff --git a/test/metabase/util/date_test.clj b/test/metabase/util/date_test.clj
new file mode 100644
index 00000000000..2cacfd3af7d
--- /dev/null
+++ b/test/metabase/util/date_test.clj
@@ -0,0 +1,85 @@
+(ns metabase.util.date-test
+  (:require [expectations :refer :all]
+            [metabase.util.date :refer :all]))
+
+;;; Date stuff
+
+(def ^:private ^:const saturday-the-31st   #inst "2005-12-31T19:05:55")
+(def ^:private ^:const sunday-the-1st #inst "2006-01-01T04:18:26")
+
+(expect false (is-temporal? nil))
+(expect false (is-temporal? 123))
+(expect false (is-temporal? "abc"))
+(expect false (is-temporal? [1 2 3]))
+(expect false (is-temporal? {:a "b"}))
+(expect true (is-temporal? saturday-the-31st))
+
+(expect saturday-the-31st (->Timestamp (->Date saturday-the-31st)))
+(expect saturday-the-31st (->Timestamp (->Calendar saturday-the-31st)))
+(expect saturday-the-31st (->Timestamp (->Calendar (.getTime saturday-the-31st))))
+(expect saturday-the-31st (->Timestamp (.getTime saturday-the-31st)))
+(expect saturday-the-31st (->Timestamp "2005-12-31T19:05:55+00:00"))
+
+(expect nil (->iso-8601-datetime nil nil))
+(expect "2005-12-31T19:05:55.000Z" (->iso-8601-datetime saturday-the-31st nil))
+(expect "2005-12-31T11:05:55.000-08:00" (->iso-8601-datetime saturday-the-31st "US/Pacific"))
+(expect "2006-01-01T04:05:55.000+09:00" (->iso-8601-datetime saturday-the-31st "Asia/Tokyo"))
+
+
+(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "UTC"))
+(expect 19   (date-extract :hour-of-day     saturday-the-31st   "UTC"))
+(expect 7    (date-extract :day-of-week     saturday-the-31st   "UTC"))
+(expect 1    (date-extract :day-of-week     sunday-the-1st      "UTC"))
+(expect 31   (date-extract :day-of-month    saturday-the-31st   "UTC"))
+(expect 365  (date-extract :day-of-year     saturday-the-31st   "UTC"))
+(expect 53   (date-extract :week-of-year    saturday-the-31st   "UTC"))
+(expect 12   (date-extract :month-of-year   saturday-the-31st   "UTC"))
+(expect 4    (date-extract :quarter-of-year saturday-the-31st   "UTC"))
+(expect 2005 (date-extract :year            saturday-the-31st   "UTC"))
+
+(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "US/Pacific"))
+(expect 11   (date-extract :hour-of-day     saturday-the-31st   "US/Pacific"))
+(expect 7    (date-extract :day-of-week     saturday-the-31st   "US/Pacific"))
+(expect 7    (date-extract :day-of-week     sunday-the-1st      "US/Pacific"))
+(expect 31   (date-extract :day-of-month    saturday-the-31st   "US/Pacific"))
+(expect 365  (date-extract :day-of-year     saturday-the-31st   "US/Pacific"))
+(expect 53   (date-extract :week-of-year    saturday-the-31st   "US/Pacific"))
+(expect 12   (date-extract :month-of-year   saturday-the-31st   "US/Pacific"))
+(expect 4    (date-extract :quarter-of-year saturday-the-31st   "US/Pacific"))
+(expect 2005 (date-extract :year            saturday-the-31st   "US/Pacific"))
+
+(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "Asia/Tokyo"))
+(expect 4    (date-extract :hour-of-day     saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :day-of-week     saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :day-of-week     sunday-the-1st      "Asia/Tokyo"))
+(expect 1    (date-extract :day-of-month    saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :day-of-year     saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :week-of-year    saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :month-of-year   saturday-the-31st   "Asia/Tokyo"))
+(expect 1    (date-extract :quarter-of-year saturday-the-31st   "Asia/Tokyo"))
+(expect 2006 (date-extract :year            saturday-the-31st   "Asia/Tokyo"))
+
+
+(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "UTC"))
+(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "UTC"))
+(expect #inst "2005-12-31"       (date-trunc :day     saturday-the-31st   "UTC"))
+(expect #inst "2005-12-25"       (date-trunc :week    saturday-the-31st   "UTC"))
+(expect #inst "2006-01-01"       (date-trunc :week    sunday-the-1st      "UTC"))
+(expect #inst "2005-12-01"       (date-trunc :month   saturday-the-31st   "UTC"))
+(expect #inst "2005-10-01"       (date-trunc :quarter saturday-the-31st   "UTC"))
+
+(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "Asia/Tokyo"))
+(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "Asia/Tokyo"))
+(expect #inst "2006-01-01+09:00" (date-trunc :day     saturday-the-31st   "Asia/Tokyo"))
+(expect #inst "2006-01-01+09:00" (date-trunc :week    saturday-the-31st   "Asia/Tokyo"))
+(expect #inst "2006-01-01+09:00" (date-trunc :week    sunday-the-1st      "Asia/Tokyo"))
+(expect #inst "2006-01-01+09:00" (date-trunc :month   saturday-the-31st   "Asia/Tokyo"))
+(expect #inst "2006-01-01+09:00" (date-trunc :quarter saturday-the-31st   "Asia/Tokyo"))
+
+(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "US/Pacific"))
+(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "US/Pacific"))
+(expect #inst "2005-12-31-08:00" (date-trunc :day     saturday-the-31st   "US/Pacific"))
+(expect #inst "2005-12-25-08:00" (date-trunc :week    saturday-the-31st   "US/Pacific"))
+(expect #inst "2005-12-25-08:00" (date-trunc :week    sunday-the-1st      "US/Pacific"))
+(expect #inst "2005-12-01-08:00" (date-trunc :month   saturday-the-31st   "US/Pacific"))
+(expect #inst "2005-10-01-08:00" (date-trunc :quarter saturday-the-31st   "US/Pacific"))
diff --git a/test/metabase/util_test.clj b/test/metabase/util_test.clj
index 90647a34cc8..a618a245c91 100644
--- a/test/metabase/util_test.clj
+++ b/test/metabase/util_test.clj
@@ -3,89 +3,6 @@
   (:require [expectations :refer :all]
             [metabase.util :refer :all]))
 
-
-;;; Date stuff
-
-(def ^:private ^:const saturday-the-31st   #inst "2005-12-31T19:05:55")
-(def ^:private ^:const sunday-the-1st #inst "2006-01-01T04:18:26")
-
-(expect false (is-temporal? nil))
-(expect false (is-temporal? 123))
-(expect false (is-temporal? "abc"))
-(expect false (is-temporal? [1 2 3]))
-(expect false (is-temporal? {:a "b"}))
-(expect true (is-temporal? saturday-the-31st))
-
-(expect saturday-the-31st (->Timestamp (->Date saturday-the-31st)))
-(expect saturday-the-31st (->Timestamp (->Calendar saturday-the-31st)))
-(expect saturday-the-31st (->Timestamp (->Calendar (.getTime saturday-the-31st))))
-(expect saturday-the-31st (->Timestamp (.getTime saturday-the-31st)))
-(expect saturday-the-31st (->Timestamp "2005-12-31T19:05:55+00:00"))
-
-(expect nil (->iso-8601-datetime nil nil))
-(expect "2005-12-31T19:05:55.000Z" (->iso-8601-datetime saturday-the-31st nil))
-(expect "2005-12-31T11:05:55.000-08:00" (->iso-8601-datetime saturday-the-31st "US/Pacific"))
-(expect "2006-01-01T04:05:55.000+09:00" (->iso-8601-datetime saturday-the-31st "Asia/Tokyo"))
-
-
-(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "UTC"))
-(expect 19   (date-extract :hour-of-day     saturday-the-31st   "UTC"))
-(expect 7    (date-extract :day-of-week     saturday-the-31st   "UTC"))
-(expect 1    (date-extract :day-of-week     sunday-the-1st      "UTC"))
-(expect 31   (date-extract :day-of-month    saturday-the-31st   "UTC"))
-(expect 365  (date-extract :day-of-year     saturday-the-31st   "UTC"))
-(expect 53   (date-extract :week-of-year    saturday-the-31st   "UTC"))
-(expect 12   (date-extract :month-of-year   saturday-the-31st   "UTC"))
-(expect 4    (date-extract :quarter-of-year saturday-the-31st   "UTC"))
-(expect 2005 (date-extract :year            saturday-the-31st   "UTC"))
-
-(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "US/Pacific"))
-(expect 11   (date-extract :hour-of-day     saturday-the-31st   "US/Pacific"))
-(expect 7    (date-extract :day-of-week     saturday-the-31st   "US/Pacific"))
-(expect 7    (date-extract :day-of-week     sunday-the-1st      "US/Pacific"))
-(expect 31   (date-extract :day-of-month    saturday-the-31st   "US/Pacific"))
-(expect 365  (date-extract :day-of-year     saturday-the-31st   "US/Pacific"))
-(expect 53   (date-extract :week-of-year    saturday-the-31st   "US/Pacific"))
-(expect 12   (date-extract :month-of-year   saturday-the-31st   "US/Pacific"))
-(expect 4    (date-extract :quarter-of-year saturday-the-31st   "US/Pacific"))
-(expect 2005 (date-extract :year            saturday-the-31st   "US/Pacific"))
-
-(expect 5    (date-extract :minute-of-hour  saturday-the-31st   "Asia/Tokyo"))
-(expect 4    (date-extract :hour-of-day     saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :day-of-week     saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :day-of-week     sunday-the-1st      "Asia/Tokyo"))
-(expect 1    (date-extract :day-of-month    saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :day-of-year     saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :week-of-year    saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :month-of-year   saturday-the-31st   "Asia/Tokyo"))
-(expect 1    (date-extract :quarter-of-year saturday-the-31st   "Asia/Tokyo"))
-(expect 2006 (date-extract :year            saturday-the-31st   "Asia/Tokyo"))
-
-
-(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "UTC"))
-(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "UTC"))
-(expect #inst "2005-12-31"       (date-trunc :day     saturday-the-31st   "UTC"))
-(expect #inst "2005-12-25"       (date-trunc :week    saturday-the-31st   "UTC"))
-(expect #inst "2006-01-01"       (date-trunc :week    sunday-the-1st      "UTC"))
-(expect #inst "2005-12-01"       (date-trunc :month   saturday-the-31st   "UTC"))
-(expect #inst "2005-10-01"       (date-trunc :quarter saturday-the-31st   "UTC"))
-
-(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "Asia/Tokyo"))
-(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "Asia/Tokyo"))
-(expect #inst "2006-01-01+09:00" (date-trunc :day     saturday-the-31st   "Asia/Tokyo"))
-(expect #inst "2006-01-01+09:00" (date-trunc :week    saturday-the-31st   "Asia/Tokyo"))
-(expect #inst "2006-01-01+09:00" (date-trunc :week    sunday-the-1st      "Asia/Tokyo"))
-(expect #inst "2006-01-01+09:00" (date-trunc :month   saturday-the-31st   "Asia/Tokyo"))
-(expect #inst "2006-01-01+09:00" (date-trunc :quarter saturday-the-31st   "Asia/Tokyo"))
-
-(expect #inst "2005-12-31T19:05" (date-trunc :minute  saturday-the-31st   "US/Pacific"))
-(expect #inst "2005-12-31T19:00" (date-trunc :hour    saturday-the-31st   "US/Pacific"))
-(expect #inst "2005-12-31-08:00" (date-trunc :day     saturday-the-31st   "US/Pacific"))
-(expect #inst "2005-12-25-08:00" (date-trunc :week    saturday-the-31st   "US/Pacific"))
-(expect #inst "2005-12-25-08:00" (date-trunc :week    sunday-the-1st      "US/Pacific"))
-(expect #inst "2005-12-01-08:00" (date-trunc :month   saturday-the-31st   "US/Pacific"))
-(expect #inst "2005-10-01-08:00" (date-trunc :quarter saturday-the-31st   "US/Pacific"))
-
 ;;; ## tests for HOST-UP?
 
 (expect true
-- 
GitLab