diff --git a/CHANGELOG.md b/CHANGELOG.md index b86d157..9560291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Only accretive/fixative changes will be made from now on. * 1.2.next in progress * _[Experimental! Will change in response to feedback!]_ Add `next.jdbc/with-logging` to create a wrapped connectable that will invoke logging functions with the SQL/parameters and optionally the result for each operation. + * Fix #167 by adding `:property-separator` to `next.jdbc.connection/dbtypes` and using it in `jdbc-url`. * Fix `:unit_count` references in **Getting Started** (were `:unit_cost`). * Update `test-runner`. diff --git a/doc/all-the-options.md b/doc/all-the-options.md index 5dcc7bb..5fc86ae 100644 --- a/doc/all-the-options.md +++ b/doc/all-the-options.md @@ -14,6 +14,7 @@ Although `get-datasource` does not accept options, the "db spec" hash map passed * `:host` -- an optional string that identifies the IP address or hostname of the server on which the database is running; the default is `"127.0.0.1"`; if `:none` is specified, `next.jdbc` will assume this is for a local database and will omit the host/port segment of the JDBC URL, * `:host-prefix` -- an optional string that can be used to override the `//` that is normally placed in front of the IP address or hostname in the JDBC URL, * `:port` -- an optional integer that identifies the port on which the database is running; for common database types, `next.jdbc` knows the default so this should only be needed for non-standard setups or "exotic" database types, +* `:property-separator` -- an optional string that can be used to override the separators used in `jdbc-url` for the properties (after the initial JDBC URL portion); by default `?` and `&` are used to build JDBC URLs with properties; for SQL Server drivers (both MS and jTDS) `:property-separator ";"` is used, so this option should only be necessary when you are specifying "unusual" databases that `next.jdbc` does not already know about, * `:classname` -- an optional string that identifies the name of the JDBC driver class to be used for the connection; for common database types, `next.jdbc` knows the default so this should only be needed for "exotic" database types, * `:user` -- an optional string that identifies the database username to be used when authenticating, * `:password` -- an optional string that identifies the database password to be used when authenticating. diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index f417897..33c51c4 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -103,6 +103,11 @@ the database name in the JDBC URL * `:host-prefix` -- override the `//` that normally precedes the IP address or hostname in the JDBC URL + * `:property-separator` -- an optional string that can be used to override + the separators used in `jdbc-url` for the properties (after the initial + JDBC URL portion); by default `?` and `&` are used to build JDBC URLs + with properties; for SQL Server drivers (both MS and jTDS) + `:property-separator \";\"` is used In the second format, this key is required: * `:jdbcUrl` -- a JDBC URL string diff --git a/src/next/jdbc/connection.clj b/src/next/jdbc/connection.clj index f7a1a3f..343a87a 100644 --- a/src/next/jdbc/connection.clj +++ b/src/next/jdbc/connection.clj @@ -22,7 +22,8 @@ string, this table includes `:dbname-separator` and/or `:host-prefix`. The default prefix for `:dbname` is either `/` or `:` and for `:host` it is `//`. For local databases, with no `:host`/`:port` segment in their JDBC URL, a - value of `:none` is provided for `:host` in this table. + value of `:none` is provided for `:host` in this table. In addition, + `:property-separator` can specify how you build the JDBC URL. For known database types, you can use `:dbtype` (and omit `:classname`). @@ -79,14 +80,17 @@ :host :none} "jtds" {:classname "net.sourceforge.jtds.jdbc.Driver" :alias-for "jtds:sqlserver" + :property-separator ";" :port 1433} "jtds:sqlserver" {:classname "net.sourceforge.jtds.jdbc.Driver" + :property-separator ";" :port 1433} "mariadb" {:classname "org.mariadb.jdbc.Driver" :port 3306} "mssql" {:classname "com.microsoft.sqlserver.jdbc.SQLServerDriver" :alias-for "sqlserver" :dbname-separator ";DATABASENAME=" + :property-separator ";" :port 1433} "mysql" {:classname ["com.mysql.cj.jdbc.Driver" "com.mysql.jdbc.Driver"] @@ -117,6 +121,7 @@ :host :none} "sqlserver" {:classname "com.microsoft.sqlserver.jdbc.SQLServerDriver" :dbname-separator ";DATABASENAME=" + :property-separator ";" :port 1433} "timesten:client" {:classname "com.timesten.jdbc.TimesTenClientDriver" :dbname-separator ":dsn=" @@ -151,7 +156,7 @@ As a special case, the database spec can contain jdbcUrl (just like ->pool), in which case it will return that URL as-is and a map of any other options." [{:keys [dbtype dbname host port classname - dbname-separator host-prefix + dbname-separator host-prefix property-separator jdbcUrl] :as db-spec}] (let [etc (dissoc db-spec @@ -208,7 +213,8 @@ (throw (ex-info (str "Unknown dbtype: " dbtype ", and :classname not provided.") db-spec))) - [url etc])))) + [url etc (or property-separator + (-> dbtype dbtypes :property-separator))])))) (defn jdbc-url "Given a database spec (as a hash map), return a JDBC URL with all the @@ -236,13 +242,15 @@ sure they are properly URL-encoded as values in the database spec hash map. This function does **not** attempt to URL-encode values for you!" [db-spec] - (let [[url etc] (spec->url+etc db-spec) - url-and (if (str/index-of url "?") "&" "?")] - (str url url-and (str/join "&" - (reduce-kv (fn [pairs k v] - (conj pairs (str (name k) "=" v))) - [] - etc))))) + (let [[url etc ps] (spec->url+etc db-spec) + url-and (or ps (if (str/index-of url "?") "&" "?"))] + (if (seq etc) + (str url url-and (str/join (or ps "&") + (reduce-kv (fn [pairs k v] + (conj pairs (str (name k) "=" v))) + [] + etc))) + url))) (defn ->pool "Given a (connection pooled datasource) class and a database spec, return a diff --git a/test/next/jdbc/connection_test.clj b/test/next/jdbc/connection_test.clj index 6fa0715..7af6b04 100644 --- a/test/next/jdbc/connection_test.clj +++ b/test/next/jdbc/connection_test.clj @@ -54,28 +54,72 @@ (#'c/spec->url+etc {:dbtype "sqlserver" :dbname db-name :port 1433}))))) (deftest custom-dbtypes - (is (= ["jdbc:acme:my-db" {}] + (is (= ["jdbc:acme:my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db" :host :none}))) - (is (= ["jdbc:acme://127.0.0.1/my-db" {}] + (is (= ["jdbc:acme://127.0.0.1/my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db"}))) - (is (= ["jdbc:acme://12.34.56.70:1234/my-db" {}] + (is (= ["jdbc:acme://12.34.56.70:1234/my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db" :host "12.34.56.70" :port 1234}))) - (is (= ["jdbc:acme:dsn=my-db" {}] + (is (= ["jdbc:acme:dsn=my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db" :host :none :dbname-separator ":dsn="}))) - (is (= ["jdbc:acme:(*)127.0.0.1/my-db" {}] + (is (= ["jdbc:acme:(*)127.0.0.1/my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db" :host-prefix "(*)"}))) - (is (= ["jdbc:acme:(*)12.34.56.70:1234/my-db" {}] + (is (= ["jdbc:acme:(*)12.34.56.70:1234/my-db" {} nil] (#'c/spec->url+etc {:dbtype "acme" :classname "java.lang.String" :dbname "my-db" :host "12.34.56.70" :port 1234 :host-prefix "(*)"})))) +(deftest jdbc-url-tests + (testing "basic URLs work" + (is (= "jdbc:acme:my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host :none}))) + (is (= "jdbc:acme://127.0.0.1/my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db"}))) + (is (= "jdbc:acme://12.34.56.70:1234/my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host "12.34.56.70" :port 1234}))) + (is (= "jdbc:acme:dsn=my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host :none + :dbname-separator ":dsn="}))) + (is (= "jdbc:acme:(*)127.0.0.1/my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" + :host-prefix "(*)"}))) + (is (= "jdbc:acme:(*)12.34.56.70:1234/my-db" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host "12.34.56.70" :port 1234 + :host-prefix "(*)"})))) + (testing "URLs with properties work" + (is (= "jdbc:acme:my-db?useSSL=true" + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host :none + :useSSL true}))) + (is (boolean (#{"jdbc:acme:my-db?useSSL=true&user=dba" + "jdbc:acme:my-db?user=dba&useSSL=true"} + (c/jdbc-url {:dbtype "acme" :classname "java.lang.String" + :dbname "my-db" :host :none + :useSSL true :user "dba"})))) + + (is (= "jdbc:jtds:sqlserver:my-db;useSSL=true" + (c/jdbc-url {:dbtype "jtds" + :dbname "my-db" :host :none + :useSSL true}))) + (is (boolean (#{"jdbc:jtds:sqlserver:my-db;useSSL=true;user=dba" + "jdbc:jtds:sqlserver:my-db;user=dba;useSSL=true"} + (c/jdbc-url {:dbtype "jtds" + :dbname "my-db" :host :none + :useSSL true :user "dba"})))))) + ;; these are the 'local' databases that we can always test against (def test-db-type ["derby" "h2" "h2:mem" "hsqldb" "sqlite"])