From 38a827ccd13f7ceeb7af8a5007e6ca6ea6a4f498 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cam=20Sa=C3=BCl?= <cammsaul@gmail.com>
Date: Mon, 30 Nov 2015 21:01:28 -0800
Subject: [PATCH] Redshift driver :yum:

---
 circle.yml                                    |  6 +-
 src/metabase/driver/generic_sql/util.clj      |  2 +-
 src/metabase/driver/postgres.clj              | 23 +++--
 src/metabase/driver/redshift.clj              | 97 +++++++++++++++++++
 test/metabase/driver/query_processor_test.clj | 75 +++++++-------
 test/metabase/test/data/datasets.clj          | 24 ++---
 test/metabase/test/data/generic_sql.clj       |  4 +-
 test/metabase/test/data/redshift.clj          | 95 ++++++++++++++++++
 8 files changed, 266 insertions(+), 60 deletions(-)
 create mode 100644 src/metabase/driver/redshift.clj
 create mode 100644 test/metabase/test/data/redshift.clj

diff --git a/circle.yml b/circle.yml
index 848bae45b43..89890712625 100644
--- a/circle.yml
+++ b/circle.yml
@@ -25,11 +25,11 @@ test:
     # 0) runs unit tests w/ H2 local DB. Runs against H2, Mongo, MySQL
     # 1) runs unit tests w/ Postgres local DB. Runs against H2, SQL Server
     # 2) runs unit tests w/ MySQL local DB. Runs against H2, Postgres, SQLite
-    # 3) runs Eastwood linter
-    # 4) Bikeshed linter
+    # 3) runs unit tests w/ H2 local DB. Runs against H2, Redshift
+    # 4) runs Eastwood linter & Bikeshed linter
     # 5) runs JS linter + JS test
     # 6) runs lein uberjar. (We don't run bin/build because we're not really concerned about `npm install` (etc) in this test, which runs elsewhere)
-    - case $CIRCLE_NODE_INDEX in 0) ENGINES=h2,mongo,mysql lein test ;; 1) ENGINES=h2,sqlserver MB_DB_TYPE=postgres MB_DB_DBNAME=circle_test MB_DB_PORT=5432 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 2) ENGINES=h2,postgres,sqlite MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 3) lein eastwood ;; 4) lein bikeshed --max-line-length 240 ;; 5) npm install && npm run lint && npm run build && npm run test ;; 6) lein uberjar ;; esac:
+    - case $CIRCLE_NODE_INDEX in 0) ENGINES=h2,mongo,mysql lein test ;; 1) ENGINES=h2,sqlserver MB_DB_TYPE=postgres MB_DB_DBNAME=circle_test MB_DB_PORT=5432 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 2) ENGINES=h2,postgres,sqlite MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost lein test ;; 3) ENGINES=h2,redshift lein test ;; 4) lein eastwood && lein bikeshed --max-line-length 240 ;; 5) npm install && npm run lint && npm run build && npm run test ;; 6) lein uberjar ;; esac:
         parallel: true
 deployment:
   master:
diff --git a/src/metabase/driver/generic_sql/util.clj b/src/metabase/driver/generic_sql/util.clj
index ba38b1ff5c9..040a9c63d6b 100644
--- a/src/metabase/driver/generic_sql/util.clj
+++ b/src/metabase/driver/generic_sql/util.clj
@@ -9,7 +9,7 @@
             [metabase.driver :as driver]
             [metabase.driver.query-processor :as qp]))
 
-(defn- db->connection-spec
+(defn db->connection-spec
   "Return a JDBC connection spec for a Metabase `Database`."
   [{{:keys [short-lived?]} :details, :as database}]
   (let [driver (driver/engine->driver (:engine database))]
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index 2ba78202043..53f216c4aed 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -78,7 +78,7 @@
     (keyword "timestamp with timezone")    :DateTimeField
     (keyword "timestamp without timezone") :DateTimeField} column-type))
 
-(def ^:private ^:const ssl-params
+(def ^:const ssl-params
   "Params to include in the JDBC connection spec for an SSL connection."
   {:ssl        true
    :sslmode    "require"
@@ -162,13 +162,23 @@
   clojure.lang.Named
   (getName [_] "PostgreSQL"))
 
+(def PostgresISQLDriverMixin
+  "Implementations of `ISQLDriver` methods for `PostgresDriver`."
+  (merge (sql/ISQLDriverDefaultsMixin)
+         {:column->base-type         column->base-type
+          :connection-details->spec  connection-details->spec
+          :date                      date
+          :set-timezone-sql          (constantly "UPDATE pg_settings SET setting = ? WHERE name ILIKE 'timezone';")
+          :string-length-fn          (constantly :CHAR_LENGTH)
+          :unix-timestamp->timestamp unix-timestamp->timestamp}))
+
 (extend PostgresDriver
   driver/IDriver
   (merge (sql/IDriverSQLDefaultsMixin)
          {:date-interval                     date-interval
           :details-fields                    (constantly [{:name         "host"
                                                            :display-name "Host"
-                                                           :default "localhost"}
+                                                           :default      "localhost"}
                                                           {:name         "port"
                                                            :display-name "Port"
                                                            :type         :integer
@@ -192,13 +202,6 @@
           :driver-specific-sync-field!       driver-specific-sync-field!
           :humanize-connection-error-message humanize-connection-error-message})
 
-  sql/ISQLDriver
-  (merge (sql/ISQLDriverDefaultsMixin)
-         {:column->base-type         column->base-type
-          :connection-details->spec  connection-details->spec
-          :date                      date
-          :set-timezone-sql          (constantly "UPDATE pg_settings SET setting = ? WHERE name ILIKE 'timezone';")
-          :string-length-fn          (constantly :CHAR_LENGTH)
-          :unix-timestamp->timestamp unix-timestamp->timestamp}))
+  sql/ISQLDriver PostgresISQLDriverMixin)
 
 (driver/register-driver! :postgres (PostgresDriver.))
diff --git a/src/metabase/driver/redshift.clj b/src/metabase/driver/redshift.clj
new file mode 100644
index 00000000000..546413f1121
--- /dev/null
+++ b/src/metabase/driver/redshift.clj
@@ -0,0 +1,97 @@
+(ns metabase.driver.redshift
+  "Amazon Redshift Driver."
+  (:require [clojure.java.jdbc :as jdbc]
+            (korma [core :as k]
+                   [db :as kdb])
+            [korma.sql.utils :as kutils]
+            (metabase [config :as config]
+                      [driver :as driver])
+            [metabase.driver.generic-sql :as sql]
+            [metabase.driver.generic-sql.util :as sqlutil]
+            [metabase.driver.postgres :as postgres]))
+
+(defn- connection-details->spec [_ details]
+  (kdb/postgres (merge details postgres/ssl-params))) ; always connect to redshift over SSL
+
+(defn- date-interval [_ unit amount]
+  (kutils/generated (format "(GETDATE() + INTERVAL '%d %s')" amount (name unit))))
+
+(defn- unix-timestamp->timestamp [_ field-or-value seconds-or-milliseconds]
+  (kutils/func (case seconds-or-milliseconds
+                 :seconds      "(TIMESTAMP '1970-01-01T00:00:00Z' + (%s * INTERVAL '1 second'))"
+                 :milliseconds "(TIMESTAMP '1970-01-01T00:00:00Z' + ((%s / 1000) * INTERVAL '1 second'))")
+               [field-or-value]))
+
+;; The Postgres JDBC .getImportedKeys method doesn't work for Redshift, and we're not allowed to access information_schema.constraint_column_usage,
+;; so we'll have to use this custome query instead
+;; See also: [Related Postgres JDBC driver issue on GitHub](https://github.com/pgjdbc/pgjdbc/issues/79)
+;;           [How to access the equivalent of information_schema.constraint_column_usage in Redshift](https://forums.aws.amazon.com/thread.jspa?threadID=133514)
+(defn- table-fks [_ table]
+  (set (jdbc/query (sqlutil/db->connection-spec @(:db table))
+                   ["SELECT source_column.attname AS \"fk-column-name\",
+                       dest_table.relname  AS \"dest-table-name\",
+                       dest_column.attname AS \"dest-column-name\"
+                     FROM pg_constraint c
+                       JOIN pg_namespace n             ON c.connamespace = n.oid
+                       JOIN pg_class source_table      ON c.conrelid     = source_table.oid
+                       JOIN pg_attribute source_column ON c.conrelid     = source_column.attrelid
+                       JOIN pg_class dest_table        ON c.confrelid    = dest_table.oid
+                       JOIN pg_attribute dest_column   ON c.confrelid    = dest_column.attrelid
+                     WHERE c.contype            = 'f'::char
+                       AND source_table.relname = ?
+                       AND n.nspname            = ?
+                       AND source_column.attnum = ANY(c.conkey)
+                       AND dest_column.attnum   = ANY(c.confkey)"
+                    (:name table)
+                    (:schema table)])))
+
+(defrecord RedshiftDriver []
+  clojure.lang.Named
+  (getName [_] "Amazon Redshift"))
+
+(extend RedshiftDriver
+  driver/IDriver
+  (merge (sql/IDriverSQLDefaultsMixin)
+         {:date-interval  date-interval
+          :details-fields (constantly [{:name         "host"
+                                        :display-name "Host"
+                                        :placeholder  "my-cluster-name.abcd1234.us-east-1.redshift.amazonaws.com"
+                                        :required     true}
+                                       {:name         "port"
+                                        :display-name "Port"
+                                        :type         :integer
+                                        :default      5439}
+                                       {:name         "db"
+                                        :display-name "Database name"
+                                        :placeholder  "toucan_sightings"
+                                        :required     true}
+                                       {:name         "user"
+                                        :display-name "Master username"
+                                        :placeholder  "cam"
+                                        :required     true}
+                                       {:name         "password"
+                                        :display-name "Master user password"
+                                        :type         :password
+                                        :placeholder  "*******"
+                                        :required     true}])
+          :table-fks      table-fks})
+
+  sql/ISQLDriver
+  (merge postgres/PostgresISQLDriverMixin
+         {:connection-details->spec  connection-details->spec
+          :current-datetime-fn       (constantly (k/sqlfn* :GETDATE))
+          :set-timezone-sql          (constantly nil)
+          :unix-timestamp->timestamp unix-timestamp->timestamp}
+         ;; HACK ! When we test against Redshift we use a session-unique schema so we can run simultaneous tests against a single remote host;
+         ;; when running tests tell the sync process to ignore all the other schemas
+         (when config/is-test?
+           {:excluded-schemas (memoize
+                               (fn [_]
+                                 (require 'metabase.test.data.redshift)
+                                 (let [session-schema-number @(resolve 'metabase.test.data.redshift/session-schema-number)]
+                                   (set (conj (for [i (range 240)
+                                                    :when (not= i session-schema-number)]
+                                                (str "schema_" i))
+                                              "public")))))})))
+
+(driver/register-driver! :redshift (RedshiftDriver.))
diff --git a/test/metabase/driver/query_processor_test.clj b/test/metabase/driver/query_processor_test.clj
index 0527262f4d8..f3424ec869b 100644
--- a/test/metabase/driver/query_processor_test.clj
+++ b/test/metabase/driver/query_processor_test.clj
@@ -30,7 +30,7 @@
   (set/difference datasets/all-valid-engines (engines-that-support feature)))
 
 (defmacro if-questionable-timezone-support [then else]
-  `(if (contains? #{:sqlserver :mongo :sqlite} *engine*)
+  `(if (contains? #{:mongo :redshift :sqlite :sqlserver} *engine*)
      ~then
      ~else))
 
@@ -269,7 +269,9 @@
 
 ;; ## "AVG" AGGREGATION
 (qp-expect-with-all-engines
-    {:rows    [[35.50589199999998]]
+    {:rows    [[(if (= *engine* :redshift)
+                  35.505892
+                  35.50589199999998)]]
      :columns ["avg"]
      :cols    [(aggregate-col :avg (venues-col :latitude))]}
   (Q aggregate avg latitude of venues))
@@ -389,7 +391,8 @@
      :columns (venues-columns)
      :cols    (venues-cols)}
   (Q aggregate rows of venues
-     filter between id 21 22))
+     filter between id 21 22
+     order id+))
 
 ;; ### FILTER -- "BETWEEN" with dates
 (qp-expect-with-all-engines
@@ -408,7 +411,8 @@
      :columns (venues-columns)
      :cols    (venues-cols)}
   (Q aggregate rows of venues
-     filter or <= id 3 = id 5))
+     filter or <= id 3 = id 5
+     order id+))
 
 ;; ### FILTER -- "INSIDE"
 (qp-expect-with-all-engines
@@ -596,13 +600,14 @@
    :cols    [(aggregate-col :stddev (venues-col :latitude))]
    :rows    [[(datasets/engine-case
                 :h2        3.43467255295115      ; annoying :/
-                :postgres  3.4346725529512736
                 :mysql     3.417456040761316
+                :postgres  3.4346725529512736
+                :redshift  3.43467255295115
                 :sqlserver 3.43467255295126)]]}
   (Q aggregate stddev latitude of venues))
 
 ;; Make sure standard deviation fails for the Mongo driver since its not supported
-(datasets/expect-with-engines (engines-that-dont-support :foreign-keys)
+(datasets/expect-with-engines (engines-that-dont-support :standard-deviation-aggregations)
   {:status :failed
    :error  "standard-deviation-aggregations is not supported by this driver."}
   (select-keys (Q aggregate stddev latitude of venues) [:status :error]))
@@ -661,32 +666,36 @@
              "avg"]
    :rows    [[3 (datasets/engine-case
                   :h2        22
-                  :postgres  22.0000000000000000M
-                  :mysql     22.0000M
-                  :sqlserver 22
                   :mongo     22.0
-                  :sqlite    22.0)]
+                  :mysql     22.0000M
+                  :postgres  22.0000000000000000M
+                  :redshift  22
+                  :sqlite    22.0
+                  :sqlserver 22)]
              [2 (datasets/engine-case
                   :h2        28
-                  :postgres  28.2881355932203390M
-                  :mysql     28.2881M
-                  :sqlserver 28
                   :mongo     28.28813559322034
-                  :sqlite    28.28813559322034)]
+                  :mysql     28.2881M
+                  :postgres  28.2881355932203390M
+                  :redshift  28
+                  :sqlite    28.28813559322034
+                  :sqlserver 28)]
              [1 (datasets/engine-case
                   :h2        32
-                  :postgres  32.8181818181818182M
-                  :mysql     32.8182M
-                  :sqlserver 32
                   :mongo     32.81818181818182
-                  :sqlite    32.81818181818182)]
+                  :mysql     32.8182M
+                  :postgres  32.8181818181818182M
+                  :redshift  32
+                  :sqlite    32.81818181818182
+                  :sqlserver 32)]
              [4 (datasets/engine-case
                   :h2        53
-                  :postgres  53.5000000000000000M
-                  :mysql     53.5000M
-                  :sqlserver 53
                   :mongo     53.5
-                  :sqlite    53.5)]]
+                  :mysql     53.5000M
+                  :postgres  53.5000000000000000M
+                  :redshift  53
+                  :sqlite    53.5
+                  :sqlserver 53)]]
    :cols    [(venues-col :price)
              (aggregate-col :avg (venues-col :category_id))]}
   (Q return :data
@@ -701,10 +710,10 @@
 (datasets/expect-with-engines (engines-that-support :standard-deviation-aggregations)
   {:columns [(format-name "price")
              "stddev"]
-   :rows    [[3 (datasets/engine-case :h2 26, :postgres 26, :mysql 25, :sqlserver 26)]
+   :rows    [[3 (datasets/engine-case :h2 26, :mysql 25, :postgres 26, :redshift 26, :sqlserver 26)]
              [1 24]
              [2 21]
-             [4 (datasets/engine-case :h2 15, :postgres 15, :mysql 14, :sqlserver 15)]]
+             [4 (datasets/engine-case :h2 15, :mysql 14, :postgres 15, :redshift 15, :sqlserver 15)]]
    :cols    [(venues-col :price)
              (aggregate-col :stddev (venues-col :category_id))]}
   (-> (Q return :data
@@ -784,8 +793,8 @@
 
 (datasets/expect-with-all-engines
   (cond
-    ;; SQL Server and Mongo don't have a concept of timezone so results are all grouped by UTC
-    (contains? #{:sqlserver :mongo} *engine*)
+    ;; SQL Server, Mongo, and Redshift don't have a concept of timezone so results are all grouped by UTC
+    (contains? #{:mongo :redshift :sqlserver} *engine*)
     [[#inst "2015-06-01T07" 6]
      [#inst "2015-06-02T07" 10]
      [#inst "2015-06-03T07" 4]
@@ -809,7 +818,7 @@
      ["2015-06-09" 7]
      ["2015-06-10" 9]]
 
-    ;; Postgres, MySQL, and H2 -- grouped by DB timezone, US/Pacific in this case
+    ;; Postgres, Redshift, MySQL, and H2 -- grouped by DB timezone, US/Pacific in this case
     :else
     [[#inst "2015-06-01T07" 8]
      [#inst "2015-06-02T07" 9]
@@ -1134,7 +1143,7 @@
 
 (datasets/expect-with-all-engines
   (cond
-    (= *engine* :sqlserver)
+    (contains? #{:redshift :sqlserver} *engine*)
     [[#inst "2015-06-01T17:31" 1]
      [#inst "2015-06-01T23:06" 1]
      [#inst "2015-06-02T00:23" 1]
@@ -1173,7 +1182,7 @@
 
 (datasets/expect-with-all-engines
   (cond
-    (contains? #{:sqlserver :mongo} *engine*)
+    (contains? #{:mongo :redshift :sqlserver} *engine*)
     [[#inst "2015-06-01T17:31" 1]
      [#inst "2015-06-01T23:06" 1]
      [#inst "2015-06-02T00:23" 1]
@@ -1225,7 +1234,7 @@
 
 (datasets/expect-with-all-engines
   (cond
-    (contains? #{:sqlserver :mongo} *engine*)
+    (contains? #{:mongo :redshift :sqlserver} *engine*)
     [[#inst "2015-06-01T17" 1]
      [#inst "2015-06-01T23" 1]
      [#inst "2015-06-02T00" 1]
@@ -1270,7 +1279,7 @@
 
 (datasets/expect-with-all-engines
   (cond
-    (contains? #{:sqlserver :mongo} *engine*)
+    (contains? #{:mongo :redshift :sqlserver} *engine*)
     [[#inst "2015-06-01T07" 6]
      [#inst "2015-06-02T07" 10]
      [#inst "2015-06-03T07" 4]
@@ -1327,7 +1336,7 @@
 
 (datasets/expect-with-all-engines
   (cond
-    (contains? #{:sqlserver :mongo} *engine*)
+    (contains? #{:mongo :redshift :sqlserver} *engine*)
     [[#inst "2015-05-31T07" 46]
      [#inst "2015-06-07T07" 47]
      [#inst "2015-06-14T07" 40]
@@ -1354,7 +1363,7 @@
     (contains? #{:sqlserver :sqlite} *engine*)
     [[23 54] [24 46] [25 39] [26 61]]
 
-    (= *engine* :mongo)
+    (contains? #{:mongo :redshift} *engine*)
     [[23 46] [24 47] [25 40] [26 60] [27 7]]
 
     :else
diff --git a/test/metabase/test/data/datasets.clj b/test/metabase/test/data/datasets.clj
index 8560e8a38cf..70d70435dc4 100644
--- a/test/metabase/test/data/datasets.clj
+++ b/test/metabase/test/data/datasets.clj
@@ -11,6 +11,7 @@
                              [mongo :refer [map->MongoDriver]]
                              [mysql :refer [map->MySQLDriver]]
                              [postgres :refer [map->PostgresDriver]]
+                             [redshift :refer [map->RedshiftDriver]]
                              [sqlite :refer [map->SQLiteDriver]]
                              [sqlserver :refer [map->SQLServerDriver]])
             (metabase.models [field :refer [Field]]
@@ -20,6 +21,7 @@
                                 [mongo :as mongo]
                                 [mysql :as mysql]
                                 [postgres :as postgres]
+                                [redshift :as redshift]
                                 [sqlite :as sqlite]
                                 [sqlserver :as sqlserver])
             [metabase.util :as u])
@@ -27,6 +29,7 @@
            metabase.driver.mongo.MongoDriver
            metabase.driver.mysql.MySQLDriver
            metabase.driver.postgres.PostgresDriver
+           metabase.driver.redshift.RedshiftDriver
            metabase.driver.sqlite.SQLiteDriver
            metabase.driver.sqlserver.SQLServerDriver))
 
@@ -76,7 +79,7 @@
           :timestamp-field-type (constantly :DateField)}))
 
 
-;; ## Generic SQL
+;; ## SQL Drivers
 
 (def ^:private GenericSQLIDatasetMixin
   (merge IDatasetDefaultsMixin
@@ -85,8 +88,6 @@
           :timestamp-field-type (constantly :DateTimeField)}))
 
 
-;;; ### H2
-
 (extend H2Driver
   IDataset
   (merge GenericSQLIDatasetMixin
@@ -97,7 +98,11 @@
           :sum-field-type (constantly :BigIntegerField)}))
 
 
-;;; ### Postgres
+(extend MySQLDriver
+  IDataset
+  (merge GenericSQLIDatasetMixin
+         {:sum-field-type (constantly :BigIntegerField)}))
+
 
 (extend PostgresDriver
   IDataset
@@ -105,23 +110,17 @@
          {:default-schema (constantly "public")}))
 
 
-;;; ### MySQL
-
-(extend MySQLDriver
+(extend RedshiftDriver
   IDataset
   (merge GenericSQLIDatasetMixin
-         {:sum-field-type (constantly :BigIntegerField)}))
+         {:default-schema (constantly redshift/session-schema-name)}))
 
 
-;;; ### SQLite
-
 (extend SQLiteDriver
   IDataset
   GenericSQLIDatasetMixin)
 
 
-;;; ### SQLServer
-
 (extend SQLServerDriver
   IDataset
   (merge GenericSQLIDatasetMixin
@@ -137,6 +136,7 @@
    :mongo     (map->MongoDriver     {:dbpromise (promise)})
    :mysql     (map->MySQLDriver     {:dbpromise (promise)})
    :postgres  (map->PostgresDriver  {:dbpromise (promise)})
+   :redshift  (map->RedshiftDriver  {:dbpromise (promise)})
    :sqlite    (map->SQLiteDriver    {:dbpromise (promise)})
    :sqlserver (map->SQLServerDriver {:dbpromise (promise)})})
 
diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj
index cc0cc4eb044..333de035446 100644
--- a/test/metabase/test/data/generic_sql.clj
+++ b/test/metabase/test/data/generic_sql.clj
@@ -173,7 +173,9 @@
 
 (defn default-execute-sql! [loader context dbdef sql]
   (let [sql (some-> sql s/trim)]
-    (when (seq sql)
+    (when (and (seq sql)
+               ;; make sure SQL isn't just semicolons
+               (not (s/blank? (s/replace sql #";" ""))))
       (try
         (jdbc/execute! (database->spec loader context dbdef) [sql] :transaction? false, :multi? true)
         (catch java.sql.SQLException e
diff --git a/test/metabase/test/data/redshift.clj b/test/metabase/test/data/redshift.clj
new file mode 100644
index 00000000000..795a4eb0222
--- /dev/null
+++ b/test/metabase/test/data/redshift.clj
@@ -0,0 +1,95 @@
+(ns metabase.test.data.redshift
+  (:require [clojure.java.jdbc :as jdbc]
+            [clojure.tools.logging :as log]
+            [clojure.string :as s]
+            [environ.core :refer [env]]
+            [metabase.driver.generic-sql :as sql]
+            (metabase.test.data [generic-sql :as generic]
+                                [interface :as i]
+                                [postgres :as postgres])
+            [metabase.util :as u])
+  (:import metabase.driver.redshift.RedshiftDriver))
+
+;; Time, UUID types aren't supported by redshift
+(def ^:private ^:const field-base-type->sql-type
+  {:BigIntegerField "BIGINT"
+   :BooleanField    "BOOL"
+   :CharField       "VARCHAR(254)"
+   :DateField       "DATE"
+   :DateTimeField   "TIMESTAMP"
+   :DecimalField    "DECIMAL"
+   :FloatField      "FLOAT8"
+   :IntegerField    "INTEGER"
+   :TextField       "TEXT"})
+
+(defn- get-db-env-var
+  "Look up the relevant env var for AWS connection details or throw an exception if it's not set.
+
+     (get-db-env-var :user) ; Look up `MB_REDSHIFT_USER`"
+  [env-var & [default]]
+  (or (env (keyword (format "mb-redshift-%s" (name env-var))))
+      default
+      (throw (Exception. (format "In order to test Redshift, you must specify the env var MB_REDSHIFT_%s."
+                                 (s/upper-case (name env-var)))))))
+
+(def ^:private db-connection-details
+  (delay {:host     (get-db-env-var :host)
+          :port     (Integer/parseInt (get-db-env-var :port "5439"))
+          :db       (get-db-env-var :db)
+          :user     (get-db-env-var :user)
+          :password (get-db-env-var :password)}))
+
+
+;; Redshift is tested remotely, which means we need to support multiple tests happening against the same remote host at the same time.
+;; Since Redshift doesn't let us create and destroy databases (we must re-use the same database throughout the tests) we'll just fake it
+;; by creating a new schema when tests start running and re-use the same schema for each test
+(defonce ^:const session-schema-number
+  (rand-int 240)) ; there's a maximum of 256 schemas per DB so make sure we don't go over that limit
+
+(defonce ^:const session-schema-name
+  (str "schema_" session-schema-number))
+
+(defn- qualified-name-components
+  ([_ db-name]
+   [db-name])
+  ([_ _ table-name]
+   [session-schema-name table-name])
+  ([_ _ table-name field-name]
+   [session-schema-name table-name field-name]))
+
+(extend RedshiftDriver
+  generic/IGenericSQLDatasetLoader
+  (merge generic/DefaultsMixin
+         {:create-db-sql             (constantly nil)
+          :drop-db-if-exists-sql     (constantly nil)
+          :drop-table-if-exists-sql  generic/drop-table-if-exists-cascade-sql
+          :field-base-type->sql-type (fn [_ base-type]
+                                       (field-base-type->sql-type base-type))
+          :pk-sql-type               (constantly "INTEGER IDENTITY(1,1)")
+          :qualified-name-components qualified-name-components})
+
+  i/IDatasetLoader
+  (merge generic/IDatasetLoaderMixin
+         {:database->connection-details (fn [& _]
+                                          @db-connection-details)
+          :engine                       (constantly :redshift)}))
+
+
+;;; Create + destroy the schema used for this test session
+
+(defn- execute-when-testing-redshift! [format-str & args]
+  (when (contains? @(resolve 'metabase.test.data.datasets/test-engines) :redshift)
+    (let [sql (apply format format-str args)]
+      (log/info (u/format-color 'blue "[Redshift] %s" sql))
+      (jdbc/execute! (sql/connection-details->spec (RedshiftDriver.) @db-connection-details)
+                     [sql]))))
+
+(defn- create-session-schema!
+  {:expectations-options :before-run}
+  []
+  (execute-when-testing-redshift! "DROP SCHEMA IF EXISTS %s CASCADE; CREATE SCHEMA %s;" session-schema-name session-schema-name))
+
+(defn- destroy-session-schema!
+  {:expectations-options :after-run}
+  []
+  (execute-when-testing-redshift! "DROP SCHEMA IF EXISTS %s CASCADE;" session-schema-name))
-- 
GitLab