From 6c42d7ce67113df7a3e3c83b948d8fbdd5d1e650 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Fri, 27 Sep 2019 13:06:45 -0700 Subject: [PATCH] Fixes #66 by adding support for :jdbcUrl in db-spec hash map --- CHANGELOG.md | 1 + doc/getting-started.md | 4 +- src/next/jdbc.clj | 11 ++- src/next/jdbc/connection.clj | 111 +++++++++++++++-------------- test/next/jdbc/connection_test.clj | 12 ++++ 5 files changed, 84 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3b1dc..9d73cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Only accretive/fixative changes will be made from now on. The following changes have been committed to the **master** branch since the 1.0.7 release: +* Fix #66 by adding support for a db-spec hash map format containing a `:jdbcUrl` key (consistent with `->pool`) so that you can create a datasource from a JDBC URL string and additional options. * Address #65 by adding a HugSQL "quick start" to the Friendly SQL Functions section of the docs. * Add `next.jdbc.specs/unstrument`. PR #64 (@gerred). * Address #63 by improving documentation around qualified column names and `:qualifier` (`clojure.java.jdbc`) migration. diff --git a/doc/getting-started.md b/doc/getting-started.md index c0740a8..15c4e1d 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -68,6 +68,8 @@ We described the database with just `:dbtype` and `:dbname` because it is create > Note: You can see the full list of `:dbtype` values supported in [next.jdbc/get-datasource](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc#get-datasource)'s docstring. If you need this programmatically, you can get it from the [next.jdbc.connection/dbtypes](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes) hash map. If those lists differ, the hash map is the definitive list (and I'll need to fix the docstring!). The docstring of that Var explains how to tell `next.jdbc` about additional databases. +If you already have a JDBC URL (string), you can use that as-is instead of the db-spec hash map. If you have a JDBC URL and still need additional options passed into the JDBC driver, you can use a hash map with the `:jdbcUrl` key specifying the string and whatever additional options you need. + ### `execute!` & `execute-one!` We used `execute!` to create the `address` table, to insert a new row into it, and to query it. In all three cases, `execute!` returns a vector of hash maps with namespace-qualified keys, representing the result set from the operation, if available. When no result set is produced, `next.jdbc` returns a "result set" containing the "update count" from the operation (which is usually the number of rows affected). By default, H2 uses uppercase names and `next.jdbc` returns these as-is. @@ -172,7 +174,7 @@ Then import the appropriate classes into your code: (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource))) ``` -Finally, create the connection pooled datasource. `db-spec` here contains the regular `next.jdbc` options (`:dbtype`, `:dbname`, and maybe `:host`, `:port`, `:classname` etc). Those are used to construct the JDBC URL that is passed into the datasource object (by calling `.setJdbcUrl` on it). You can also specify any of the connection pooling library's options, as mixed case keywords corresponding to any simple setter methods on the class being passed in, e.g., `:connectionTestQuery`, `:maximumPoolSize` (HikariCP), `:maxPoolSize`, `:preferredTestQuery` (c3p0). +Finally, create the connection pooled datasource. `db-spec` here contains the regular `next.jdbc` options (`:dbtype`, `:dbname`, and maybe `:host`, `:port`, `:classname` etc -- or the `:jdbcUrl` format mentioned above). Those are used to construct the JDBC URL that is passed into the datasource object (by calling `.setJdbcUrl` on it). You can also specify any of the connection pooling library's options, as mixed case keywords corresponding to any simple setter methods on the class being passed in, e.g., `:connectionTestQuery`, `:maximumPoolSize` (HikariCP), `:maxPoolSize`, `:preferredTestQuery` (c3p0). Some important notes regarding HikariCP: diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 3569902..4ee8dff 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -58,7 +58,11 @@ "Given some sort of specification of a database, return a `DataSource`. A specification can be a JDBC URL string (which is passed to the JDBC - driver as-is), or a hash map. For the hash map, these keys are required: + driver as-is), or a hash map. + + For the hash map, there are two formats accepted: + + In the first format, these keys are required: * `:dbtype` -- a string indicating the type of the database * `:dbname` -- a string indicating the name of the database to be used @@ -81,10 +85,13 @@ * `:host-prefix` -- override the `//` that normally precedes the IP address or hostname in the JDBC URL + In the second format, this key is required: + * `:jdbcUrl` -- a JDBC URL string + Any additional options provided will be passed to the JDBC driver's `.getConnection` call as a `java.util.Properties` structure. - Database types supported, and their defaults: + Database types supported (for `:dbtype`), and their defaults: * `derby` -- `org.apache.derby.jdbc.EmbeddedDriver` -- also pass `:create true` if you want the database to be automatically created * `h2` -- `org.h2.Driver` -- for an on-disk database diff --git a/src/next/jdbc/connection.clj b/src/next/jdbc/connection.clj index aa7f43b..a7820c0 100644 --- a/src/next/jdbc/connection.clj +++ b/src/next/jdbc/connection.clj @@ -136,61 +136,68 @@ (atom {})) (defn- spec->url+etc - "Given a database spec, return a JDBC URL and a map of any additional options." + "Given a database spec, return a JDBC URL and a map of any additional options. + + 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 + jdbcUrl] :as db-spec}] - (let [;; allow aliases for dbtype - subprotocol (-> dbtype dbtypes :alias-for (or dbtype)) - host (or host (-> dbtype dbtypes :host) "127.0.0.1") - port (or port (-> dbtype dbtypes :port)) - db-sep (or dbname-separator (-> dbtype dbtypes :dbname-separator (or "/"))) - local-sep (or dbname-separator (-> dbtype dbtypes :dbname-separator (or ":"))) - url (cond (#{"h2"} subprotocol) - (str "jdbc:" subprotocol local-sep - (if (re-find #"^([A-Za-z]:)?[\./\\]" dbname) - ;; DB name starts with relative or absolute path - dbname - ;; otherwise make it local - (str "./" dbname))) - - (#{"h2:mem"} subprotocol) - (str "jdbc:" subprotocol local-sep dbname ";DB_CLOSE_DELAY=-1") - - (= :none host) - (str "jdbc:" subprotocol local-sep dbname) - - :else - (str "jdbc:" subprotocol ":" - (or host-prefix (-> dbtype dbtypes :host-prefix (or "//"))) - host - (when port (str ":" port)) - db-sep dbname)) - etc (dissoc db-spec + (let [etc (dissoc db-spec :dbtype :dbname :host :port :classname - :dbname-separator :host-prefix)] - ;; verify the datasource is loadable - (if-let [class-name (or classname (-> dbtype dbtypes :classname))] - (swap! driver-cache update class-name - #(if % % - (do - ;; force DriverManager to be loaded - (DriverManager/getLoginTimeout) - (if (string? class-name) - (clojure.lang.RT/loadClassForName class-name) - (loop [[clazz & more] class-name] - (let [loaded - (try - (clojure.lang.RT/loadClassForName clazz) - (catch Exception e - e))] - (if (instance? Throwable loaded) - (if (seq more) - (recur more) - (throw loaded)) - loaded))))))) - (throw (ex-info (str "Unknown dbtype: " dbtype) db-spec))) - [url etc])) + :dbname-separator :host-prefix + :jdbcUrl)] + (if jdbcUrl + [jdbcUrl etc] + (let [;; allow aliases for dbtype + subprotocol (-> dbtype dbtypes :alias-for (or dbtype)) + host (or host (-> dbtype dbtypes :host) "127.0.0.1") + port (or port (-> dbtype dbtypes :port)) + db-sep (or dbname-separator (-> dbtype dbtypes :dbname-separator (or "/"))) + local-sep (or dbname-separator (-> dbtype dbtypes :dbname-separator (or ":"))) + url (cond (#{"h2"} subprotocol) + (str "jdbc:" subprotocol local-sep + (if (re-find #"^([A-Za-z]:)?[\./\\]" dbname) + ;; DB name starts with relative or absolute path + dbname + ;; otherwise make it local + (str "./" dbname))) + + (#{"h2:mem"} subprotocol) + (str "jdbc:" subprotocol local-sep dbname ";DB_CLOSE_DELAY=-1") + + (= :none host) + (str "jdbc:" subprotocol local-sep dbname) + + :else + (str "jdbc:" subprotocol ":" + (or host-prefix (-> dbtype dbtypes :host-prefix (or "//"))) + host + (when port (str ":" port)) + db-sep dbname))] + ;; verify the datasource is loadable + (if-let [class-name (or classname (-> dbtype dbtypes :classname))] + (swap! driver-cache update class-name + #(if % % + (do + ;; force DriverManager to be loaded + (DriverManager/getLoginTimeout) + (if (string? class-name) + (clojure.lang.RT/loadClassForName class-name) + (loop [[clazz & more] class-name] + (let [loaded + (try + (clojure.lang.RT/loadClassForName clazz) + (catch Exception e + e))] + (if (instance? Throwable loaded) + (if (seq more) + (recur more) + (throw loaded)) + loaded))))))) + (throw (ex-info (str "Unknown dbtype: " dbtype) db-spec))) + [url etc])))) (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 6cfb873..0f6b9e4 100644 --- a/test/next/jdbc/connection_test.clj +++ b/test/next/jdbc/connection_test.clj @@ -103,6 +103,18 @@ (is (str/index-of (pr-str ds) url)) (with-open [con (p/get-connection ds {})] (is (instance? java.sql.Connection con))))) + (testing "datasource via jdbcUrl" + (let [[url etc] (#'c/spec->url+etc db) + ds (p/get-datasource (assoc etc :jdbcUrl url))] + (if (= "derby" (:dbtype db)) + (is {:create true} etc) + (is {} etc)) + (is (instance? javax.sql.DataSource ds)) + (is (str/index-of (pr-str ds) (str "jdbc:" (:dbtype db)))) + ;; checks get-datasource on a DataSource is identity + (is (identical? ds (p/get-datasource ds))) + (with-open [con (p/get-connection ds {})] + (is (instance? java.sql.Connection con))))) (testing "datasource via HikariCP" ;; the type hint is only needed because we want to call .close (with-open [^HikariDataSource ds (c/->pool HikariDataSource db)]