diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a3320..1dbfb55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Only accretive/fixative changes will be made from now on. * 1.3.next in progress + * Fix [#229](https://github.com/seancorfield/next-jdbc/issues/229) by adding `next.jdbc.connect/uri->db-spec` which converts a URI string to a db-spec hash map; in addition, if `DriverManager/getConnection` fails, it assumes it was passed a URI instead of a JDBC URL, and retries after calling that function and then recreating the JDBC URL (which should have the effect of moving the embedded user/password credentials into the properties structure instead of the URL). * Address [#228](https://github.com/seancorfield/next-jdbc/issues/228) by adding `PreparedStatement` caveat to the Oracle **Tips & Tricks** section. * 1.3.834 -- 2022-09-23 diff --git a/src/next/jdbc/connection.clj b/src/next/jdbc/connection.clj index 4b0bd39..8efd593 100644 --- a/src/next/jdbc/connection.clj +++ b/src/next/jdbc/connection.clj @@ -132,21 +132,6 @@ :dbname-separator ":dsn=" :host :none}}) -(defn- ^Properties as-properties - "Convert any seq of pairs to a `java.util.Properties` instance." - [m] - (let [p (Properties.)] - (doseq [[k v] m] - (.setProperty p (name k) (str v))) - p)) - -(defn- get-driver-connection - "Common logic for loading the designated JDBC driver class and - obtaining the appropriate `Connection` object." - [url timeout etc] - (when timeout (DriverManager/setLoginTimeout timeout)) - (DriverManager/getConnection url (as-properties etc))) - (def ^:private driver-cache "An optimization for repeated calls to get-datasource, or for get-connection called on a db-spec hash map, so that we only try to load the classes once." @@ -359,6 +344,53 @@ [s] [s {}]) +(defn- ^Properties as-properties + "Convert any seq of pairs to a `java.util.Properties` instance." + [m] + (let [p (Properties.)] + (doseq [[k v] m] + (.setProperty p (name k) (str v))) + p)) + +(defn uri->db-spec + "clojure.java.jdbc (and some users out there) considered the URI format + to be an acceptable JDBC URL, i.e., with credentials embdedded in the string, + rather than as query parameters. + + This function accepts a URI string, optionally prefixed with `jdbc:` and + returns a db-spec hash map." + [uri] + (let [{:keys [scheme userInfo host port path query]} + (j/from-java (java.net.URI. (str/replace uri #"^jdbc:" ""))) + [user password] (when (seq userInfo) (str/split userInfo #":")) + properties (when (seq query) + (into {} + (map #(str/split % #"=")) + (str/split query #"\&")))] + (cond-> (assoc properties + :dbtype scheme + :host host + :port port) + (seq path) (assoc :dbname (subs path 1)) + user (assoc :user user) + password (assoc :password password)))) + +(defn- get-driver-connection + "Common logic for loading the designated JDBC driver class and + obtaining the appropriate `Connection` object." + [url timeout etc] + (when timeout (DriverManager/setLoginTimeout timeout)) + (try + (DriverManager/getConnection url (as-properties etc)) + (catch Exception e + (try + (let [db-spec (uri->db-spec url) + [url' etc'] (spec->url+etc db-spec)] + (DriverManager/getConnection url' (as-properties (merge etc' etc)))) + (catch Exception _ + ;; if the fallback fails too, throw the original exception + (throw e)))))) + (defn- url+etc->datasource "Given a JDBC URL and a map of options, return a `DataSource` that can be used to obtain a new database connection." diff --git a/test/next/jdbc/connection_string_test.clj b/test/next/jdbc/connection_string_test.clj new file mode 100644 index 0000000..56a7e74 --- /dev/null +++ b/test/next/jdbc/connection_string_test.clj @@ -0,0 +1,41 @@ +;; copyright (c) 2019-2021 Sean Corfield, all rights reserved + +(ns next.jdbc.connection-string-test + "Tests for the main hash map spec to JDBC URL logic and the get-datasource + and get-connection protocol implementations. + + At some point, the datasource/connection tests should probably be extended + to accept EDN specs from an external source (environment variables?)." + (:require [clojure.string :as str] + [clojure.test :refer [deftest is testing use-fixtures]] + [next.jdbc.connection :as c] + [next.jdbc.protocols :as p] + [next.jdbc.specs :as specs] + [next.jdbc.test-fixtures :refer [with-test-db db]])) + +(set! *warn-on-reflection* true) + +(use-fixtures :once with-test-db) + +(specs/instrument) + +(deftest test-uri-strings + (testing "datasource via String" + (let [db-spec (db) + db-spec (if (= "embedded-postgres" (:dbtype db-spec)) + (assoc db-spec :dbtype "postgresql") + db-spec) + [url etc] (#'c/spec->url+etc db-spec) + {:keys [user password]} etc + etc (dissoc etc :user :password) + uri (-> url + ;; strip jdbc: prefix for fun + (str/replace #"^jdbc:" "") + (str/replace #";" "?") ; for SQL Server tests + (str/replace #":sqlserver" "") ; for SQL Server tests + (cond-> (and user password) + (str/replace #"://" (str "://" user ":" password "@")))) + ds (p/get-datasource (assoc etc :jdbcUrl uri))] + (when (and user password) + (with-open [con (p/get-connection ds {})] + (is (instance? java.sql.Connection con)))))))