diff --git a/deps.edn b/deps.edn index 4342e5c..df5ed63 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.10.1"} - org.clojure/java.data {:mvn/version "1.0.73"}} + org.clojure/java.data {:mvn/version "1.0.78"}} :aliases {:test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "1.0.0"} diff --git a/src/next/jdbc/datafy.clj b/src/next/jdbc/datafy.clj index aec5585..57a1690 100644 --- a/src/next/jdbc/datafy.clj +++ b/src/next/jdbc/datafy.clj @@ -1,13 +1,33 @@ ;; copyright (c) 2020 Sean Corfield, all rights reserved (ns next.jdbc.datafy - "This namespace provides datafication of several JDBC object types: + "This namespace provides datafication of several JDBC object types, + all within the `java.sql` package: - * `java.sql.Connection` -- datafies as a bean; `:metaData` is navigable + * `Connection` -- datafies as a bean; `:metaData` is navigable and produces `java.sql.DatabaseMetaData`. - * `java.sql.DatabaseMetaData` -- datafies as a bean; five properties + * `DatabaseMetaData` -- datafies as a bean; five properties are navigable to produce fully-realized datafiable result sets. - * `java.sql.ResultSetMetaData` -- datafies as a vector of column descriptions." + * `ParameterMetaData` -- datafies as a vector of parameter descriptions. + * `ResultSet` -- datafies as a bean; if the `ResultSet` has an associated + `Statement` and that in turn has an associated `Connection` then an + additional key of `:rows` is provided which is a datafied result set, + from `next.jdbc.result-set/datafiable-result-set` with default options. + This is provided as a convenience, purely for datafication of other + JDBC data types -- in normal `next.jdbc` usage, result sets are + datafied under full user control. + * `ResultSetMetaData` -- datafies as a vector of column descriptions. + * `Statement` -- datafies as a bean. + + Because different database drivers may throw `SQLException` for various + unimplemented or unavailable propertiies on objects in various states, + the default behavior is to return those exceptions using the `:qualify` + option for `clojure.java.data/from-java-shallow`, so for a property + `:foo`, if its corresponding getter throws an exception, it would instead + be returned as `:foo/exception`. This behavior can be overridden by + `binding` `next.jdbc.datafy/*datafy-failure*` to any of the other options + supported: `:group`, `:omit`, or `:return`. See the `clojure.java.data` + documentation for more details." (:require [clojure.core.protocols :as core-p] [clojure.java.data :as j] [next.jdbc.result-set :as rs]) @@ -64,9 +84,15 @@ :unknown)) :signed (fn [^ParameterMetaData o i] (.isSigned o i))}) +(def ^:dynamic *datafy-failure* + "How datafication failures should be handled, based on `clojure.java.data`. + + Defaults to `:qualify`, but can be `:group`, `:omit`, `:qualify`, or `:return`." + :qualify) + (defn- safe-bean [o opts] (try - (j/from-java-shallow o (assoc opts :add-class true)) + (j/from-java-shallow o (assoc opts :add-class true :exceptions *datafy-failure*)) (catch Throwable t (let [dex (juxt type (comp str ex-message)) cause (ex-cause t)] @@ -96,7 +122,8 @@ (with-meta (let [data (safe-bean this {})] (cond-> data (not (:exception (meta data))) - (assoc :all-tables []))) + ;; add an opaque object that nav will "replace" + (assoc :all-tables (Object.)))) {`core-p/nav (fn [_ k v] (condp = k :all-tables @@ -124,8 +151,6 @@ (.getConnection this) {}) v))})) - ResultSetMetaData - (datafy [this] (datafy-result-set-meta-data this)) ParameterMetaData (datafy [this] (datafy-parameter-meta-data this)) ResultSet @@ -137,5 +162,7 @@ c (when s (.getConnection s))] (cond-> (safe-bean this {}) c (assoc :rows (rs/datafiable-result-set this c {})))))) + ResultSetMetaData + (datafy [this] (datafy-result-set-meta-data this)) Statement (datafy [this] (safe-bean this {:omit #{:moreResults}}))) diff --git a/test/next/jdbc/datafy_test.clj b/test/next/jdbc/datafy_test.clj index 9a42bcd..9be5452 100644 --- a/test/next/jdbc/datafy_test.clj +++ b/test/next/jdbc/datafy_test.clj @@ -24,20 +24,20 @@ :networkTimeout :schema :transactionIsolation :typeMap :warnings ;; boolean properties :closed :readOnly - ;; added by bean itself + ;; configured to be added as if by clojure.core/bean :class}) (deftest connection-datafy-tests (testing "connection datafication" (with-open [con (jdbc/get-connection (ds))] - (if (derby?) - (is (= #{:exception :cause} ; at least one property not supported - (set (keys (d/datafy con))))) - (let [data (set (keys (d/datafy con)))] - (when-let [diff (seq (set/difference data basic-connection-keys))] - (println (:dbtype (db)) :connection (sort diff))) - (is (= basic-connection-keys - (set/intersection basic-connection-keys data)))))))) + (let [reference-keys (cond-> basic-connection-keys + (derby?) (-> (disj :networkTimeout) + (conj :networkTimeout/exception))) + data (set (keys (d/datafy con)))] + (when-let [diff (seq (set/difference data reference-keys))] + (println (:dbtype (db)) :connection (sort diff))) + (is (= reference-keys + (set/intersection reference-keys data))))))) (def ^:private basic-database-metadata-keys "Generic JDBC Connection fields." @@ -63,28 +63,33 @@ :typeInfo :userName ;; boolean properties :catalogAtStart :readOnly - ;; added by bean itself - :class}) + ;; configured to be added as if by clojure.core/bean + :class + ;; added by next.jdbc.datafy if the datafication succeeds + :all-tables}) (deftest database-metadata-datafy-tests (testing "database metadata datafication" (with-open [con (jdbc/get-connection (ds))] - (if (or (postgres?) (sqlite?)) - (is (= #{:exception :cause} ; at least one property not supported - (set (keys (d/datafy (.getMetaData con)))))) - (let [data (set (keys (d/datafy (.getMetaData con))))] - (when-let [diff (seq (set/difference data basic-database-metadata-keys))] - (println (:dbtype (db)) :db-meta (sort diff))) - (is (= basic-database-metadata-keys - (set/intersection basic-database-metadata-keys data))))))) + (let [reference-keys (cond-> basic-database-metadata-keys + (postgres?) (-> (disj :rowIdLifetime) + (conj :rowIdLifetime/exception)) + (sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime) + (conj :clientInfoProperties/exception + :rowIdLifetime/exception))) + data (set (keys (d/datafy (.getMetaData con))))] + (when-let [diff (seq (set/difference data reference-keys))] + (println (:dbtype (db)) :db-meta (sort diff))) + (is (= reference-keys + (set/intersection reference-keys data)))))) (testing "nav to catalogs yields object" - (when-not (or (postgres?) (sqlite?)) - (with-open [con (jdbc/get-connection (ds))] - (let [data (d/datafy (.getMetaData con))] - (doseq [k [:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo]] - (let [rs (d/nav data k nil)] - (is (vector? rs)) - (is (every? map? rs))))))))) + (with-open [con (jdbc/get-connection (ds))] + (let [data (d/datafy (.getMetaData con))] + (doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo} + (sqlite?) (disj :clientInfoProperties))] + (let [rs (d/nav data k nil)] + (is (vector? rs)) + (is (every? map? rs)))))))) (deftest result-set-metadata-datafy-tests (testing "result set metadata datafication" @@ -100,7 +105,10 @@ (comment (def con (jdbc/get-connection (ds))) (rs/datafiable-result-set (.getTables (.getMetaData con) nil nil nil nil) con {}) - (def ps (jdbc/prepare con ["SELECT * FROM fruit"])) + (def ps (jdbc/prepare con ["SELECT * FROM fruit WHERE grade > ?"])) + (require '[next.jdbc.prepare :as prep]) + (prep/set-parameters ps [30]) (.execute ps) (.getResultSet ps) + (.close ps) (.close con))