From ecb42bbd777dc8f1d4bc187b3a9561613ccc721a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cam=20Sau=CC=88l?= <cameron@getluckybird.com>
Date: Mon, 29 Jun 2015 14:46:46 -0700
Subject: [PATCH] New driver method supports? to check if a driver supports a
 feature

---
 .dir-locals.el                                |  2 +-
 src/metabase/driver/generic_sql.clj           | 15 +++++++++-
 src/metabase/driver/interface.clj             | 30 +++++++++++++++++--
 src/metabase/driver/mongo.clj                 |  8 +++++
 src/metabase/driver/postgres.clj              |  3 +-
 src/metabase/driver/query_processor.clj       |  2 +-
 .../driver/query_processor/expand.clj         | 22 ++++++++++----
 test/metabase/driver/query_processor_test.clj | 21 +++++++++++++
 8 files changed, 90 insertions(+), 13 deletions(-)

diff --git a/.dir-locals.el b/.dir-locals.el
index 3120b81a99d..22c74497449 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -21,7 +21,7 @@
                               (expect-eval-actual-first 1)
                               (expect-expansion 0)
                               (expect-let 1)
-                              (expect-when-testing-against-dataset 1)
+                              (expect-when-testing-dataset 1)
                               (expect-when-testing-mongo 1)
                               (expect-with-all-drivers 1)
                               (expect-with-dataset 1)
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index a08274d9a00..505686f5724 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -8,7 +8,15 @@
             (metabase.driver.generic-sql [query-processor :as qp]
                                          [util :refer :all])))
 
-(defrecord SqlDriver [column->base-type
+(def ^:private ^:const sql-driver-features
+  "Features supported by *all* Generic SQL drivers."
+  #{:foreign-keys
+    :standard-deviation-aggregations
+    :unix-timestamp-special-type-fields})
+
+(defrecord SqlDriver [;; A set of additional features supported by a specific driver implmentation, e.g. :set-timezone for :postgres
+                      additional-supported-features
+                      column->base-type
                       connection-details->connection-spec
                       database->connection-details
                       sql-string-length-fn
@@ -20,6 +28,11 @@
                       ;; e.g. #"CAST\(TIMESTAMPADD\('(?:MILLI)?SECOND', ([^\s]+), DATE '1970-01-01'\) AS DATE\)" for H2
                       uncastify-timestamp-regex]
   IDriver
+  ;; Features
+  (supports? [_ feature]
+    (or (contains? sql-driver-features feature)
+        (contains? additional-supported-features feature)))
+
   ;; Connection
   (can-connect? [this database]
     (can-connect-with-details? this (database->connection-details database)))
diff --git a/src/metabase/driver/interface.clj b/src/metabase/driver/interface.clj
index cdd370f6ee4..d13bef75b43 100644
--- a/src/metabase/driver/interface.clj
+++ b/src/metabase/driver/interface.clj
@@ -1,21 +1,36 @@
 (ns metabase.driver.interface
-  "Protocols that DB drivers implement. Thus, the interface such drivers provide.")
+  "Protocols that DB drivers implement. Thus, the interface such drivers provide."
+  (:import (clojure.lang Keyword)))
+
+(def ^:const driver-optional-features
+  "A set on optional features (as keywords) that may or may not be supported by individual drivers."
+  #{:foreign-keys
+    :set-timezone
+    :standard-deviation-aggregations
+    :unix-timestamp-special-type-fields})
+
 
 ;; ## IDriver Protocol
 
 (defprotocol IDriver
   "Methods all drivers must implement."
+  ;; Features
+  (supports? [this ^Keyword feature]
+    "Does this driver support optional FEATURE?
+
+       (supports? metabase.driver.h2/driver :timestamps) -> false")
+
   ;; Connection
   (can-connect? [this database]
     "Check whether we can connect to DATABASE and perform a simple query.
      (To check whether we can connect to a database given only its details, use `can-connect-with-details?` instead).
 
-       (can-connect? (sel :one Database :id 1))")
+       (can-connect? driver (sel :one Database :id 1))")
   (can-connect-with-details? [this details-map]
     "Check whether we can connect to a database and performa a simple query.
      Returns true if we can, otherwise returns false or throws an Exception.
 
-       (can-connect-with-details? {:engine :postgres, :dbname \"book\", ...})")
+       (can-connect-with-details? driver {:engine :postgres, :dbname \"book\", ...})")
 
   ;; Syncing
   (sync-in-context [this database do-sync-fn]
@@ -80,3 +95,12 @@
    If a driver doesn't implement this protocol, it *must* implement `ISyncDriverFieldValues`."
   (field-percent-urls [this field]
     "Return the percentage of non-nil values of textual FIELD that are valid URLs."))
+
+
+;; ## Helper Functions
+
+(defn assert-driver-supports
+  "Helper fn. Assert that DRIVER supports FEATURE."
+  [driver ^Keyword feature]
+  (when-not (supports? driver feature)
+    (throw (Exception. (format "%s is not supported by this driver." (name feature))))))
diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj
index 20358519d30..37f3b1d0446 100644
--- a/src/metabase/driver/mongo.clj
+++ b/src/metabase/driver/mongo.clj
@@ -40,8 +40,16 @@
 
 ;;; ## MongoDriver
 
+(def ^:const ^:private mongo-driver-features
+  "Optional features supported by the Mongo driver."
+  #{}) ; nothing yet
+
 (deftype MongoDriver []
   IDriver
+;;; ### Features
+  (supports? [_ feature]
+    (contains? mongo-driver-features feature))
+
 ;;; ### Connection
   (can-connect? [_ database]
     (with-mongo-connection [^com.mongodb.DBApiLayer conn database]
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index df7332e4dcf..18a8763a077 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -125,7 +125,8 @@
 
 (def ^:const driver
   (generic-sql/map->SqlDriver
-   {:column->base-type                            column->base-type
+   {:additional-supported-features                #{:set-timezone}
+    :column->base-type                            column->base-type
     :connection-details->connection-spec          connection-details->connection-spec
     :database->connection-details                 database->connection-details
     :sql-string-length-fn                         :CHAR_LENGTH
diff --git a/src/metabase/driver/query_processor.clj b/src/metabase/driver/query_processor.clj
index 244fe2a71a7..d720d20230d 100644
--- a/src/metabase/driver/query_processor.clj
+++ b/src/metabase/driver/query_processor.clj
@@ -50,7 +50,7 @@
 
 (defn- pre-expand [qp]
   (fn [query]
-    (qp (expand/expand query))))
+    (qp (expand/expand *driver* query))))
 
 
 (defn- post-add-row-count-and-status
diff --git a/src/metabase/driver/query_processor/expand.clj b/src/metabase/driver/query_processor/expand.clj
index 78f52a93ade..4d3ec5b8fdc 100644
--- a/src/metabase/driver/query_processor/expand.clj
+++ b/src/metabase/driver/query_processor/expand.clj
@@ -42,6 +42,7 @@
             [medley.core :as m]
             [swiss.arrows :refer [-<>]]
             [metabase.db :refer [sel]]
+            [metabase.driver.interface :as i]
             (metabase.models [database :refer [Database]]
                              [field :as field]
                              [foreign-key :refer [ForeignKey]]
@@ -80,6 +81,12 @@
 
 ;; ## -------------------- Expansion - Impl --------------------
 
+(def ^:private ^:dynamic *driver* nil)
+
+(defn- assert-driver-supports [^Keyword feature]
+  {:pre [*driver*]}
+  (i/assert-driver-supports *driver* feature))
+
 (defn- non-empty-clause? [clause]
   (and clause
        (or (not (sequential? clause))
@@ -185,8 +192,9 @@
 
 (defn expand
   "Expand a QUERY-DICT."
-  [query-dict]
-  (binding [*field-ids*    (atom #{})
+  [driver query-dict]
+  (binding [*driver*       driver
+            *field-ids*    (atom #{})
             *fk-field-ids* (atom #{})
             *table-ids*    (atom #{})]
     (some-> query-dict
@@ -272,9 +280,10 @@
                                            (->FieldPlaceholder field-id))
      ["fk->"
       (fk-field-id :guard integer?)
-      (dest-field-id :guard integer?)] (do (swap! *field-ids* conj dest-field-id)
-                                            (swap! *fk-field-ids* conj fk-field-id)
-                                            (->FieldPlaceholder dest-field-id))))
+      (dest-field-id :guard integer?)] (do (assert-driver-supports :foreign-keys)
+                                           (swap! *field-ids* conj dest-field-id)
+                                           (swap! *fk-field-ids* conj fk-field-id)
+                                           (->FieldPlaceholder dest-field-id))))
   ([field-id value]
    (->ValuePlaceholder (:field-id (ph field-id)) value)))
 
@@ -300,7 +309,8 @@
   ["avg" field-id]      (->Aggregation :avg (ph field-id))
   ["count" field-id]    (->Aggregation :count (ph field-id))
   ["distinct" field-id] (->Aggregation :distinct (ph field-id))
-  ["stddev" field-id]   (->Aggregation :stddev (ph field-id))
+  ["stddev" field-id]   (do (assert-driver-supports :standard-deviation-aggregations)
+                            (->Aggregation :stddev (ph field-id)))
   ["sum" field-id]      (->Aggregation :sum (ph field-id))
   ["cum_sum" field-id]  (->Aggregation :cumulative-sum (ph field-id)))
 
diff --git a/test/metabase/driver/query_processor_test.clj b/test/metabase/driver/query_processor_test.clj
index 192bfcf1482..704fc114001 100644
--- a/test/metabase/driver/query_processor_test.clj
+++ b/test/metabase/driver/query_processor_test.clj
@@ -622,6 +622,15 @@
   {:source_table (id :venues)
    :aggregation  ["stddev" (id :venues :latitude)]})
 
+;; Make sure standard deviation fails for the Mongo driver since its not supported
+(datasets/expect-with-dataset :mongo
+  {:status :failed
+   :error  "standard-deviation-aggregations is not supported by this driver."}
+  (driver/process-query {:database (db-id)
+                         :type :query
+                         :query {:source_table (id :venues)
+                                 :aggregation  ["stddev" (id :venues :latitude)]}}))
+
 
 ;;; ## order_by aggregate fields (SQL-only for the time being)
 
@@ -891,3 +900,15 @@
                         [&sightings.id:id "ascending"]]
          :limit        10)
        :data :rows (map butlast) (map reverse))) ; drop timestamps. reverse ordering to make the results columns order match order_by
+
+
+;; Check that trying to use a Foreign Key fails for Mongo
+(datasets/expect-with-dataset :mongo
+  {:status :failed
+   :error "foreign-keys is not supported by this driver."}
+  (query-with-temp-db defs/tupac-sightings
+    :source_table &sightings:id
+    :order_by     [[["fk->" &sightings.city_id:id &cities.name:id] "ascending"]
+                   [["fk->" &sightings.category_id:id &categories.name:id] "descending"]
+                   [&sightings.id:id "ascending"]]
+    :limit        10))
-- 
GitLab