From 793fa3989e5cbd42d1c2f8a4576b6a0965625013 Mon Sep 17 00:00:00 2001 From: Cam Saul <cammsaul@gmail.com> Date: Wed, 23 Aug 2017 12:28:21 -0700 Subject: [PATCH] Switch to official M$ SQL Server JDBC driver [ci drivers] --- project.clj | 2 +- src/metabase/config.clj | 11 ++++- src/metabase/driver/generic_sql.clj | 19 ++++++-- src/metabase/driver/mongo/util.clj | 2 +- src/metabase/driver/sqlserver.clj | 62 ++++++++++++++++--------- src/metabase/middleware.clj | 3 +- src/metabase/util.clj | 5 -- test/metabase/driver/sqlserver_test.clj | 34 +++++++++++++- 8 files changed, 97 insertions(+), 41 deletions(-) diff --git a/project.clj b/project.clj index 0622a9174c4..17d57a85809 100644 --- a/project.clj +++ b/project.clj @@ -52,6 +52,7 @@ [com.h2database/h2 "1.4.194"] ; embedded SQL database [com.mattbertolini/liquibase-slf4j "2.0.0"] ; Java Migrations lib [com.mchange/c3p0 "0.9.5.2"] ; connection pooling library + [com.microsoft.sqlserver/mssql-jdbc "6.2.1.jre7"] ; SQLServer JDBC driver. TODO - Switch this to `.jre8` once we officially switch to Java 8 [com.novemberain/monger "3.1.0"] ; MongoDB Driver [com.taoensso/nippy "2.13.0"] ; Fast serialization (i.e., GZIP) library for Clojure [compojure "1.5.2"] ; HTTP Routing library built on Ring @@ -73,7 +74,6 @@ [mysql/mysql-connector-java "5.1.39"] ; !!! Don't upgrade to 6.0+ yet -- that's Java 8 only !!! [net.sf.cssbox/cssbox "4.12" ; HTML / CSS rendering :exclusions [org.slf4j/slf4j-api]] - [net.sourceforge.jtds/jtds "1.3.1"] ; Open Source SQL Server driver [com.clearspring.analytics/stream "2.9.5" ; Various sketching algorithms :exclusions [org.slf4j/slf4j-api it.unimi.dsi/fastutil]] diff --git a/src/metabase/config.clj b/src/metabase/config.clj index 5a3d3b4c660..423e25325f8 100644 --- a/src/metabase/config.clj +++ b/src/metabase/config.clj @@ -82,7 +82,14 @@ (version-info-from-properties-file) (version-info-from-shell-script))) -(def ^:const mb-version-string - "A formatted version string representing the currently running application." +(def ^:const ^String mb-version-string + "A formatted version string representing the currently running application. + Looks something like `v0.25.0-snapshot (1de6f3f nested-queries-icon)`." (let [{:keys [tag hash branch]} mb-version-info] (format "%s (%s %s)" tag hash branch))) + +(def ^:const ^String mb-app-id-string + "A formatted version string including the word 'Metabase' appropriate for passing along + with database connections so admins can identify them as Metabase ones. + Looks something like `Metabase v0.25.0.RC1`." + (str "Metabase " (mb-version-info :tag))) diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj index b0ec6470a25..d01d391bb1b 100644 --- a/src/metabase/driver/generic_sql.clj +++ b/src/metabase/driver/generic_sql.clj @@ -178,16 +178,25 @@ (defn handle-additional-options "If DETAILS contains an `:addtional-options` key, append those options to the connection string in CONNECTION-SPEC. - (Some drivers like MySQL provide this details field to allow special behavior where needed)." - {:arglists '([connection-spec] [connection-spec details])} + (Some drivers like MySQL provide this details field to allow special behavior where needed). + + Optionally specify SEPERATOR-STYLE, which defaults to `:url` (e.g. `?a=1&b=2`). You may instead set it to `:semicolon`, + which will separate different options with semicolons instead (e.g. `;a=1;b=2`). (While most drivers require the former + style, some require the latter.)" + {:arglists '([connection-spec] [connection-spec details & {:keys [seperator-style]}])} ;; single arity provided for cases when `connection-spec` is built by applying simple transformations to `details` ([connection-spec] (handle-additional-options connection-spec connection-spec)) - ;; two-arity version provided for when `connection-spec` is being built up separately from `details` source - ([{connection-string :subname, :as connection-spec} {additional-options :additional-options, :as details}] + ;; two-arity+options version provided for when `connection-spec` is being built up separately from `details` source + ([{connection-string :subname, :as connection-spec} {additional-options :additional-options, :as details} & {:keys [seperator-style] + :or {seperator-style :url}}] (-> (dissoc connection-spec :additional-options) (assoc :subname (str connection-string (when (seq additional-options) - (str (if (str/includes? connection-string "?") "&" "?") + (str (case seperator-style + :semicolon ";" + :url (if (str/includes? connection-string "?") + "&" + "?")) additional-options))))))) diff --git a/src/metabase/driver/mongo/util.clj b/src/metabase/driver/mongo/util.clj index c7a153a1a7c..e2162c1ba77 100644 --- a/src/metabase/driver/mongo/util.clj +++ b/src/metabase/driver/mongo/util.clj @@ -64,7 +64,7 @@ :or {ssl? false}}] (-> (client-options-for-url-params additional-options) client-options->builder - (.description (str "Metabase " (config/mb-version-info :tag))) + (.description config/mb-app-id-string) (.connectTimeout connection-timeout-ms) (.serverSelectionTimeout connection-timeout-ms) (.sslEnabled ssl?) diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj index 4a99ca24a3e..2ec0c420f0e 100644 --- a/src/metabase/driver/sqlserver.clj +++ b/src/metabase/driver/sqlserver.clj @@ -1,14 +1,18 @@ (ns metabase.driver.sqlserver + "Driver for SQLServer databases. Uses the official Microsoft JDBC driver under the hood (pre-0.25.0, used jTDS)." (:require [honeysql.core :as hsql] [metabase + [config :as config] [driver :as driver] [util :as u]] [metabase.driver.generic-sql :as sql] - [metabase.util.honeysql-extensions :as hx] - [metabase.util.ssh :as ssh])) + [metabase.util + [honeysql-extensions :as hx] + [ssh :as ssh]])) (defn- column->base-type - "See [this page](https://msdn.microsoft.com/en-us/library/ms187752.aspx) for details." + "Mappings for SQLServer types to Metabase types. + See the list here: https://docs.microsoft.com/en-us/sql/connect/jdbc/using-basic-data-types" [column-type] ({:bigint :type/BigInteger :binary :type/* @@ -48,24 +52,31 @@ (keyword "int identity") :type/Integer} column-type)) ; auto-incrementing integer (ie pk) field -(defn- connection-details->spec [{:keys [user password db host port instance domain ssl] - :or {user "dbuser", password "dbpassword", db "", host "localhost", port 1433} - :as details}] - {:classname "net.sourceforge.jtds.jdbc.Driver" - :subprotocol "jtds:sqlserver" - :loginTimeout 5 ; Wait up to 10 seconds for connection success. If we get no response by then, consider the connection failed - :subname (str "//" host ":" port "/" db) - ;; everything else gets passed as `java.util.Properties` to the JDBC connection. See full list of properties here: `http://jtds.sourceforge.net/faq.html#urlFormat` - ;; (passing these as Properties instead of part of the `:subname` is preferable because they support things like passwords with special characters) - :user user - :password password - :instance instance - :domain domain - :useNTLMv2 (boolean domain) ; if domain is specified, send LMv2/NTLMv2 responses when using Windows authentication - ;; for whatever reason `ssl=request` doesn't work with RDS (it hangs indefinitely), so just set ssl=off (disabled) if SSL isn't being used - :ssl (if ssl - "require" - "off")}) +(defn- connection-details->spec + "Build the connection spec for a SQL Server database from the DETAILS set in the admin panel. + Check out the full list of options here: `https://technet.microsoft.com/en-us/library/ms378988(v=sql.105).aspx`" + [{:keys [user password db host port instance domain ssl] + :or {user "dbuser", password "dbpassword", db "", host "localhost", port 1433} + :as details}] + (-> {:applicationName config/mb-app-id-string + :classname "com.microsoft.sqlserver.jdbc.SQLServerDriver" + :subprotocol "sqlserver" + ;; it looks like the only thing that actually needs to be passed as the `subname` is the host; everything else can be passed as part of the Properties + :subname (str "//" host) + ;; everything else gets passed as `java.util.Properties` to the JDBC connection. + ;; (passing these as Properties instead of part of the `:subname` is preferable because they support things like passwords with special characters) + :database db + :port port + :password password + ;; Wait up to 10 seconds for connection success. If we get no response by then, consider the connection failed + :loginTimeout 10 + ;; apparently specifying `domain` with the official SQLServer driver is done like `user:domain\user` as opposed to specifying them seperately as with jTDS + ;; see also: https://social.technet.microsoft.com/Forums/sqlserver/en-US/bc1373f5-cb40-479d-9770-da1221a0bc95/connecting-to-sql-server-in-a-different-domain-using-jdbc-driver?forum=sqldataaccess + :user (str (when domain (str domain "\\")) + user) + :instanceName instance + :encrypt (boolean ssl)} + (sql/handle-additional-options details, :seperator-style :semicolon))) (defn- date-part [unit expr] @@ -75,7 +86,8 @@ (apply hsql/call :dateadd (hsql/raw (name unit)) exprs)) (defn- date - "See also the [jTDS SQL <-> Java types table](http://jtds.sourceforge.net/typemap.html)" + "Wrap a HoneySQL datetime EXPRession in appropriate forms to cast/bucket it as UNIT. + See [this page](https://msdn.microsoft.com/en-us/library/ms187752.aspx) for details on the functions we're using." [unit expr] (case unit :default expr @@ -85,6 +97,7 @@ :hour-of-day (date-part :hour expr) ;; jTDS is retarded; 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 type we want + ;; TODO - I'm not sure we still need to do this now that we're using the official Microsoft JDBC driver. Maybe we can simplify this now? :day (hx/->datetime (hx/->date expr)) :day-of-week (date-part :weekday expr) :day-of-month (date-part :day expr) @@ -175,7 +188,10 @@ {:name "ssl" :display-name "Use a secure connection (SSL)?" :type :boolean - :default false}]))}) + :default false} + {:name "additional-options" + :display-name "Additional JDBC connection string options" + :placeholder "trustServerCertificate=false"}]))}) sql/ISQLDriver (merge (sql/ISQLDriverDefaultsMixin) diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj index 5511be6662c..b4ea7aaa0ba 100644 --- a/src/metabase/middleware.clj +++ b/src/metabase/middleware.clj @@ -278,7 +278,7 @@ ;; ## Custom JSON encoders ;; Always fall back to `.toString` instead of barfing. -;; In some cases we should be able to improve upon this behavior; `.toString` may just return the Class and address, e.g. `net.sourceforge.jtds.jdbc.ClobImpl@72a8b25e` +;; In some cases we should be able to improve upon this behavior; `.toString` may just return the Class and address, e.g. `some.Class@72a8b25e` ;; The following are known few classes where `.toString` is the optimal behavior: ;; * `org.postgresql.jdbc4.Jdbc4Array` (Postgres arrays) ;; * `org.bson.types.ObjectId` (Mongo BSON IDs) @@ -290,7 +290,6 @@ ;; stringify JDBC clobs (add-encoder org.h2.jdbc.JdbcClob encode-jdbc-clob) ; H2 -(add-encoder net.sourceforge.jtds.jdbc.ClobImpl encode-jdbc-clob) ; SQLServer (add-encoder org.postgresql.util.PGobject encode-jdbc-clob) ; Postgres ;; Encode BSON undefined like `nil` diff --git a/src/metabase/util.clj b/src/metabase/util.clj index a8d3dbb0888..b94e8fcbbca 100644 --- a/src/metabase/util.clj +++ b/src/metabase/util.clj @@ -332,11 +332,6 @@ ;; H2 -- See also http://h2database.com/javadoc/org/h2/jdbc/JdbcClob.html org.h2.jdbc.JdbcClob - (jdbc-clob->str [this] - (jdbc-clob->str (.getCharacterStream this))) - - ;; SQL Server -- See also http://jtds.sourceforge.net/doc/net/sourceforge/jtds/jdbc/ClobImpl.html - net.sourceforge.jtds.jdbc.ClobImpl (jdbc-clob->str [this] (jdbc-clob->str (.getCharacterStream this)))) diff --git a/test/metabase/driver/sqlserver_test.clj b/test/metabase/driver/sqlserver_test.clj index fd492d874c2..bf92b699674 100644 --- a/test/metabase/driver/sqlserver_test.clj +++ b/test/metabase/driver/sqlserver_test.clj @@ -1,5 +1,10 @@ (ns metabase.driver.sqlserver-test - (:require [metabase.test + (:require [clojure.string :as str] + [expectations :refer [expect]] + [metabase.driver + [generic-sql :as sql] + [sqlserver :as sqlserver]] + [metabase.test [data :as data] [util :refer [obj->json->obj]]] [metabase.test.data @@ -7,7 +12,7 @@ [interface :refer [def-database-definition]]])) ;;; ------------------------------------------------------------ VARCHAR(MAX) ------------------------------------------------------------ -;; VARCHAR(MAX) comes back from jTDS as a "ClobImpl" so make sure it gets encoded like a normal string by Cheshire +;; Make sure something long doesn't come back as some weird type like `ClobImpl` (def ^:private ^:const a-gene "Really long string representing a gene like \"GGAGCACCTCCACAAGTGCAGGCTATCCTGTCGAGTAAGGCCT...\"" (apply str (repeatedly 1000 (partial rand-nth [\A \G \C \T])))) @@ -22,3 +27,28 @@ (-> (data/dataset metabase.driver.sqlserver-test/genetic-data (data/run-query genetic-data)) :data :rows obj->json->obj)) ; convert to JSON + back so the Clob gets stringified + + +;;; Test that additional connection string options work (#5296) +(expect + {:classname "com.microsoft.sqlserver.jdbc.SQLServerDriver" + :subprotocol "sqlserver" + :applicationName "Metabase <version>" + :subname "//localhost;trustServerCertificate=false" + :database "birddb" + :port 1433 + :instanceName nil + :user "cam" + :password "toucans" + :encrypt false + :loginTimeout 10} + (-> (sql/connection-details->spec + (sqlserver/->SQLServerDriver) + {:user "cam" + :password "toucans" + :db "birddb" + :host "localhost" + :port 1433 + :additional-options "trustServerCertificate=false"}) + ;; the MB version Is subject to change between test runs, so replace the part like `v.0.25.0` with `<version>` + (update :applicationName #(str/replace % #"\s.*$" " <version>")))) -- GitLab