Skip to content
Snippets Groups Projects
Unverified Commit 16ec3aba authored by Cam Saul's avatar Cam Saul
Browse files

Fix SQLServer datetime grouping for locales with yyyy-dd-MM dates

[ci sqlserver]
parent b9a1994c
No related branches found
No related tags found
No related merge requests found
...@@ -91,21 +91,23 @@ ...@@ -91,21 +91,23 @@
(defn- date-add [unit & exprs] (defn- date-add [unit & exprs]
(apply hsql/call :dateadd (hsql/raw (name unit)) exprs)) (apply hsql/call :dateadd (hsql/raw (name unit)) exprs))
;; See [this page](https://msdn.microsoft.com/en-us/library/ms187752.aspx) for details on the functions we're using. ;; See https://docs.microsoft.com/en-us/sql/t-sql/functions/date-and-time-data-types-and-functions-transact-sql for
;; details on the functions we're using.
(defmethod sql.qp/date [:sqlserver :default] [_ _ expr] expr)
(defmethod sql.qp/date [:sqlserver :minute] [_ _ expr] (hx/cast :smalldatetime expr)) (defmethod sql.qp/date [:sqlserver :default] [_ _ expr]
(defmethod sql.qp/date [:sqlserver :minute-of-hour] [_ _ expr] (date-part :minute expr)) expr)
(defmethod sql.qp/date [:sqlserver :hour] [_ _ expr] (hx/->datetime (hx/format "yyyy-MM-dd HH:00:00" expr)))
(defmethod sql.qp/date [:sqlserver :hour-of-day] [_ _ expr] (date-part :hour expr)) (defmethod sql.qp/date [:sqlserver :minute] [_ _ expr]
(defmethod sql.qp/date [:sqlserver :day-of-week] [_ _ expr] (date-part :weekday expr)) (hx/cast :smalldatetime expr))
(defmethod sql.qp/date [:sqlserver :day-of-month] [_ _ expr] (date-part :day expr))
(defmethod sql.qp/date [:sqlserver :day-of-year] [_ _ expr] (date-part :dayofyear expr)) (defmethod sql.qp/date [:sqlserver :minute-of-hour] [_ _ expr]
(defmethod sql.qp/date [:sqlserver :week-of-year] [_ _ expr] (date-part :iso_week expr)) (date-part :minute expr))
(defmethod sql.qp/date [:sqlserver :month] [_ _ expr] (hx/->datetime (hx/format "yyyy-MM-01" expr)))
(defmethod sql.qp/date [:sqlserver :month-of-year] [_ _ expr] (date-part :month expr)) (defmethod sql.qp/date [:sqlserver :hour] [_ _ expr]
(defmethod sql.qp/date [:sqlserver :quarter-of-year] [_ _ expr] (date-part :quarter expr)) (hsql/call :datetime2fromparts (hx/year expr) (hx/month expr) (hx/day expr) (date-part :hour expr) 0 0 0 0))
(defmethod sql.qp/date [:sqlserver :year] [_ _ expr] (date-part :year expr))
(defmethod sql.qp/date [:sqlserver :hour-of-day] [_ _ expr]
(date-part :hour expr))
;; jTDS is wack; I sense an ongoing theme here. It returns DATEs as strings instead of as java.sql.Dates like every ;; jTDS is wack; I sense an ongoing theme here. It returns DATEs as strings instead of as java.sql.Dates like every
;; other SQL DB we support. Work around that by casting to DATE for truncation then back to DATETIME so we get the ;; other SQL DB we support. Work around that by casting to DATE for truncation then back to DATETIME so we get the
...@@ -116,6 +118,15 @@ ...@@ -116,6 +118,15 @@
(defmethod sql.qp/date [:sqlserver :day] [_ _ expr] (defmethod sql.qp/date [:sqlserver :day] [_ _ expr]
(hx/->datetime (hx/->date expr))) (hx/->datetime (hx/->date expr)))
(defmethod sql.qp/date [:sqlserver :day-of-week] [_ _ expr]
(date-part :weekday expr))
(defmethod sql.qp/date [:sqlserver :day-of-month] [_ _ expr]
(date-part :day expr))
(defmethod sql.qp/date [:sqlserver :day-of-year] [_ _ expr]
(date-part :dayofyear expr))
;; Subtract the number of days needed to bring us to the first day of the week, then convert to date ;; Subtract the number of days needed to bring us to the first day of the week, then convert to date
;; The equivalent SQL looks like: ;; The equivalent SQL looks like:
;; CAST(DATEADD(day, 1 - DATEPART(weekday, %s), CAST(%s AS DATE)) AS DATETIME) ;; CAST(DATEADD(day, 1 - DATEPART(weekday, %s), CAST(%s AS DATE)) AS DATETIME)
...@@ -125,13 +136,29 @@ ...@@ -125,13 +136,29 @@
(hx/- 1 (date-part :weekday expr)) (hx/- 1 (date-part :weekday expr))
(hx/->date expr)))) (hx/->date expr))))
(defmethod sql.qp/date [:sqlserver :week-of-year] [_ _ expr]
(date-part :iso_week expr))
(defmethod sql.qp/date [:sqlserver :month] [_ _ expr]
(hsql/call :datefromparts (hx/year expr) (hx/month expr) 1))
(defmethod sql.qp/date [:sqlserver :month-of-year] [_ _ expr]
(date-part :month expr))
;; Format date as yyyy-01-01 then add the appropriate number of quarter ;; Format date as yyyy-01-01 then add the appropriate number of quarter
;; Equivalent SQL: ;; Equivalent SQL:
;; DATEADD(quarter, DATEPART(quarter, %s) - 1, FORMAT(%s, 'yyyy-01-01')) ;; DATEADD(quarter, DATEPART(quarter, %s) - 1, FORMAT(%s, 'yyyy-01-01'))
(defmethod sql.qp/date [:sqlserver :quarter] [_ _ expr] (defmethod sql.qp/date [:sqlserver :quarter] [_ _ expr]
(date-add :quarter (date-add :quarter
(hx/dec (date-part :quarter expr)) (hx/dec (date-part :quarter expr))
(hx/format "yyyy-01-01" expr))) (hsql/call :datefromparts (hx/year expr) 1 1)))
(defmethod sql.qp/date [:sqlserver :quarter-of-year] [_ _ expr]
(date-part :quarter expr))
(defmethod sql.qp/date [:sqlserver :year] [_ _ expr]
(date-part :year expr))
(defmethod driver/date-interval :sqlserver [_ unit amount] (defmethod driver/date-interval :sqlserver [_ unit amount]
(date-add unit amount :%getdate)) (date-add unit amount :%getdate))
......
(ns metabase.driver.sqlserver-test (ns metabase.driver.sqlserver-test
(:require [clojure.string :as str] (:require [clojure.java.jdbc :as jdbc]
[clojure.string :as str]
[expectations :refer [expect]] [expectations :refer [expect]]
[honeysql.core :as hsql]
[medley.core :as m] [medley.core :as m]
[metabase [metabase
[driver :as driver] [driver :as driver]
[query-processor :as qp] [query-processor :as qp]
[query-processor-test :as qp.test]] [query-processor-test :as qp.test]]
[metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
[metabase.driver.sql.query-processor :as sql.qp]
[metabase.query-processor.test-util :as qp.test-util] [metabase.query-processor.test-util :as qp.test-util]
[metabase.test [metabase.test
[data :as data] [data :as data]
[util :as tu :refer [obj->json->obj]]] [util :as tu :refer [obj->json->obj]]]
[metabase.test.data [metabase.test.data
[datasets :as datasets] [datasets :as datasets]
[interface :refer [def-database-definition]]])) [interface :as tx :refer [def-database-definition]]]))
;;; -------------------------------------------------- VARCHAR(MAX) -------------------------------------------------- ;;; -------------------------------------------------- VARCHAR(MAX) --------------------------------------------------
...@@ -142,3 +145,22 @@ ...@@ -142,3 +145,22 @@
:order-by [[:asc $id]] :order-by [[:asc $id]]
:limit 5} :limit 5}
:limit 3}})))) :limit 3}}))))
;; Make sure datetime bucketing functions work properly with languages that format dates like yyyy-dd-MM instead of
;; yyyy-MM-dd (i.e. not American English) (#9057)
(datasets/expect-with-driver :sqlserver
[{:my-date #inst "2019-02-01T00:00:00.000-00:00"}]
;; we're doing things here with low-level calls to HoneySQL (emulating what the QP does) instead of using normal QP
;; pathways because `SET LANGUAGE` doesn't seem to persist to subsequent executions so to test that things are
;; working we need to add to in from of the query we're trying to check
(jdbc/with-db-transaction [t-conn (sql-jdbc.conn/connection-details->spec :sqlserver
(tx/dbdef->connection-details :sqlserver :db {:database-name "test-data"}))]
(try
(jdbc/execute! t-conn "CREATE TABLE temp (d DATETIME2);")
(jdbc/execute! t-conn ["INSERT INTO temp (d) VALUES (?)" #inst "2019-02-08T00:00:00Z"])
(jdbc/query t-conn (let [[sql & args] (hsql/format {:select [[(sql.qp/date :sqlserver :month :temp.d) :my-date]]
:from [:temp]}
:quoting :ansi, :allow-dashed-names? true)]
(cons (str "SET LANGUAGE Italian; " sql) args)))
;; rollback transaction so `temp` table gets discarded
(finally (.rollback (jdbc/get-connection t-conn))))))
...@@ -68,6 +68,9 @@ ...@@ -68,6 +68,9 @@
;; Clean up any leftover DBs that weren't destroyed by the last test run (eg, if it failed for some reason). This is ;; Clean up any leftover DBs that weren't destroyed by the last test run (eg, if it failed for some reason). This is
;; important because we're limited to a quota of 30 DBs on RDS. ;; important because we're limited to a quota of 30 DBs on RDS.
;;
;; This doesn't kill databases with active connections (i.e. CI instances testing against them) -- `DROP DATABASE`
;; will fail if the DB has open connections
(defmethod tx/before-run :sqlserver [_] (defmethod tx/before-run :sqlserver [_]
(let [connection-spec (sql-jdbc.conn/connection-details->spec :sqlserver (let [connection-spec (sql-jdbc.conn/connection-details->spec :sqlserver
(tx/dbdef->connection-details :sqlserver :server nil)) (tx/dbdef->connection-details :sqlserver :server nil))
...@@ -80,6 +83,7 @@ ...@@ -80,6 +83,7 @@
(doseq [db leftover-dbs] (doseq [db leftover-dbs]
(u/ignore-exceptions (u/ignore-exceptions
(printf "Deleting leftover SQL Server DB '%s'...\n" db) (printf "Deleting leftover SQL Server DB '%s'...\n" db)
;; Don't try to kill other connections to this DB with SET SINGLE_USER -- some other instance (eg CI) might be using it ;; Don't try to kill other connections to this DB with SET SINGLE_USER -- some other instance (eg CI) might
;; be using it
(jdbc/execute! connection-spec [(format "DROP DATABASE \"%s\";" db)]) (jdbc/execute! connection-spec [(format "DROP DATABASE \"%s\";" db)])
(println "[ok]")))))) (println "[ok]"))))))
...@@ -141,6 +141,7 @@ ...@@ -141,6 +141,7 @@
(def ^{:arglists '([& exprs])} floor "SQL `floor` function." (partial hsql/call :floor)) (def ^{:arglists '([& exprs])} floor "SQL `floor` function." (partial hsql/call :floor))
(def ^{:arglists '([& exprs])} hour "SQL `hour` function." (partial hsql/call :hour)) (def ^{:arglists '([& exprs])} hour "SQL `hour` function." (partial hsql/call :hour))
(def ^{:arglists '([& exprs])} minute "SQL `minute` function." (partial hsql/call :minute)) (def ^{:arglists '([& exprs])} minute "SQL `minute` function." (partial hsql/call :minute))
(def ^{:arglists '([& exprs])} day "SQL `day` function." (partial hsql/call :day))
(def ^{:arglists '([& exprs])} week "SQL `week` function." (partial hsql/call :week)) (def ^{:arglists '([& exprs])} week "SQL `week` function." (partial hsql/call :week))
(def ^{:arglists '([& exprs])} month "SQL `month` function." (partial hsql/call :month)) (def ^{:arglists '([& exprs])} month "SQL `month` function." (partial hsql/call :month))
(def ^{:arglists '([& exprs])} quarter "SQL `quarter` function."(partial hsql/call :quarter)) (def ^{:arglists '([& exprs])} quarter "SQL `quarter` function."(partial hsql/call :quarter))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment