From a7663762ce7a0b770173138b4b2e7bebcd7412d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cam=20Sa=C3=BCl?= <cammsaul@gmail.com>
Date: Tue, 15 Nov 2016 16:39:30 -0800
Subject: [PATCH] Allow custom MySQL JDBC connection string params

---
 src/metabase/db/spec.clj            |  2 +-
 src/metabase/driver/mysql.clj       | 29 +++++++++++----
 test/metabase/driver/mysql_test.clj | 55 ++++++++++++++++++++++++++++-
 3 files changed, 77 insertions(+), 9 deletions(-)

diff --git a/src/metabase/db/spec.clj b/src/metabase/db/spec.clj
index 969dfbd97f9..1f55e85c2a9 100644
--- a/src/metabase/db/spec.clj
+++ b/src/metabase/db/spec.clj
@@ -35,7 +35,7 @@
           :subprotocol "mysql"
           :subname (str "//" host ":" port "/" db)
           :delimiters "`"}
-         (dissoc opts :host :port :db)))
+         (dissoc opts :host :port :db :additional-options)))
 
 
 ;; TODO - These other ones can acutally be moved directly into their respective drivers themselves since they're not supported as backing DBs
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 9b7745cd575..66e51fab8ec 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -43,7 +43,7 @@
     :VARCHAR    :type/Text
     :YEAR       :type/Integer} (keyword (s/replace (name column-type) #"\sUNSIGNED$" "")))) ; strip off " UNSIGNED" from end if present
 
-(def ^:private ^:const connection-args
+(def ^:private ^:const default-connection-args
   "Map of args for the MySQL JDBC connection string.
    Full list of is options is available here: http://dev.mysql.com/doc/connector-j/6.0/en/connector-j-reference-configuration-properties.html"
   {:zeroDateTimeBehavior :convertToNull ; 0000-00-00 dates are valid in MySQL; convert these to `null` when they come back because they're illegal in Java
@@ -51,16 +51,28 @@
    :characterEncoding    :UTF8
    :characterSetResults  :UTF8})
 
-(def ^:private ^:const connection-args-string
-  (apply str "?" (interpose \& (for [[k v] connection-args]
-                                 (str (name k) \= (name v))))))
+(def ^:private ^:const ^String default-connection-args-string
+  (s/join \& (for [[k v] default-connection-args]
+               (str (name k) \= (name v)))))
+
+(defn- append-connection-args
+  {:argslist '([connection-spec details])}
+  [{connection-string :subname, :as connection-spec} {ssl? :ssl, additional-options :additional-options, :as details}]
+  (assoc connection-spec
+    :subname (str connection-string
+                  "?"
+                  default-connection-args-string
+                  ;; newer versions of MySQL will complain if you don't explicitly disable SSL
+                  (when-not ssl?
+                    "&useSSL=false")
+                  (when (seq additional-options)
+                    (str "&" additional-options)))))
 
 (defn- connection-details->spec [details]
   (-> details
       (set/rename-keys {:dbname :db})
       dbspec/mysql
-      (update :subname #(str % connection-args-string (when-not (:ssl details)
-                                                        "&useSSL=false"))))) ; newer versions of MySQL will complain if you don't explicitly disable SSL
+      (append-connection-args details)))
 
 (defn- unix-timestamp->timestamp [expr seconds-or-milliseconds]
   (hsql/call :from_unixtime (case seconds-or-milliseconds
@@ -161,7 +173,10 @@
                                                           {:name         "password"
                                                            :display-name "Database password"
                                                            :type         :password
-                                                           :placeholder  "*******"}])
+                                                           :placeholder  "*******"}
+                                                          {:name         "additional-options"
+                                                           :display-name "Additional JDBC connection string options"
+                                                           :placeholder  "tinyInt1isBit=false"}])
           :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)})
 
   sql/ISQLDriver
diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj
index a4376b99c85..9c046a629e1 100644
--- a/test/metabase/driver/mysql_test.clj
+++ b/test/metabase/driver/mysql_test.clj
@@ -1,8 +1,16 @@
 (ns metabase.driver.mysql-test
   (:require [expectations :refer :all]
+            [metabase.db :as db]
+            (metabase.driver [generic-sql :as generic-sql]
+                             mysql)
+            [metabase.models.database :refer [Database]]
+            [metabase.sync-database :as sync-db]
             [metabase.test.data :as data]
             (metabase.test.data [datasets :refer [expect-with-engine]]
-                                [interface :refer [def-database-definition]])))
+                                [interface :refer [def-database-definition]])
+            [metabase.test.util :as tu]
+            [metabase.util :as u])
+  (:import metabase.driver.mysql.MySQLDriver))
 
 ;; MySQL allows 0000-00-00 dates, but JDBC does not; make sure that MySQL is converting them to NULL when returning them like we asked
 (def-database-definition ^:private ^:const all-zero-dates
@@ -12,6 +20,51 @@
 
 (expect-with-engine :mysql
   [[1 nil]]
+  ;; TODO - use the `rows` function from `metabse.query-processor-test`. Preferrably after it's moved to some sort of shared test util namespace
   (-> (data/dataset metabase.driver.mysql-test/all-zero-dates
         (data/run-query exciting-moments-in-history))
       :data :rows))
+
+
+;; make sure connection details w/ extra params work as expected
+(expect
+  "//localhost:3306/cool?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF8&characterSetResults=UTF8&useSSL=false&tinyInt1isBit=false"
+  (:subname (generic-sql/connection-details->spec (MySQLDriver.) {:host               "localhost"
+                                                                  :port               "3306"
+                                                                  :dbname             "cool"
+                                                                  :additional-options "tinyInt1isBit=false"})))
+
+
+;; Test how TINYINT(1) columns are interpreted. By default, they should be interpreted as integers,
+;; but with the correct additional options, we should be able to change that -- see https://github.com/metabase/metabase/issues/3506
+(def-database-definition ^:private ^:const tiny-int-ones
+  ["number-of-cans"
+   [{:field-name "thing",          :base-type :type/Text}
+    {:field-name "number-of-cans", :base-type {:native "tinyint(1)"}}]
+   [["Six Pack"              6]
+    ["Toucan"                2]
+    ["Empty Vending Machine" 0]]])
+
+(defn- db->fields [db]
+  (let [table-ids (db/select-ids 'Table :db_id (u/get-id db))]
+    (set (map (partial into {}) (db/select ['Field :name :base_type :special_type] :table_id [:in table-ids])))))
+
+;; By default TINYINT(1) should be a boolean
+(expect-with-engine :mysql
+  #{{:name "number-of-cans", :base_type :type/Boolean, :special_type :type/Category}
+    {:name "id",             :base_type :type/Integer, :special_type :type/PK}
+    {:name "thing",          :base_type :type/Text,    :special_type :type/Category}}
+  (data/with-temp-db [db tiny-int-ones]
+    (db->fields db)))
+
+;; if someone says specifies `tinyInt1isBit=false`, it should come back as a number instead
+(expect-with-engine :mysql
+  #{{:name "number-of-cans", :base_type :type/Integer, :special_type :type/Category}
+    {:name "id",             :base_type :type/Integer, :special_type :type/PK}
+    {:name "thing",          :base_type :type/Text,    :special_type :type/Category}}
+  (data/with-temp-db [db tiny-int-ones]
+    (tu/with-temp Database [db {:engine "mysql"
+                                :details (assoc (:details db)
+                                           :additional-options "tinyInt1isBit=false")}]
+      (sync-db/sync-database! db)
+      (db->fields db))))
-- 
GitLab