diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bc98f9..f912028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Only accretive/fixative changes will be made from now on. +Changes made since release 1.0.445: +* Addition of `next.jdbc.datafy` to provide more `datafy`/`nav` introspection (work in progress; documentation pending). +* Addition of `next.jdbc.result-set/metadata` to provide (datafied) result set metadata within `plan`. + ## Stable Builds * 2020-05-23 -- 1.0.445 diff --git a/src/next/jdbc/datafy.clj b/src/next/jdbc/datafy.clj new file mode 100644 index 0000000..d151258 --- /dev/null +++ b/src/next/jdbc/datafy.clj @@ -0,0 +1,91 @@ +;; copyright (c) 2018-2020 Sean Corfield, all rights reserved + +(ns next.jdbc.datafy + "This namespace provides datafication of several JDBC object types: + + * `java.sql.Connection` -- datafies as a bean; `:metaData` is navigable + and produces `java.sql.DatabaseMetaData`. + * `java.sql.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." + (:require [clojure.core.protocols :as core-p] + [next.jdbc.result-set :as rs]) + (:import (java.sql Connection + DatabaseMetaData + ResultSetMetaData))) + +(set! *warn-on-reflection* true) + +(def ^:private column-meta + {:catalog (fn [^ResultSetMetaData o i] (.getCatalogName o i)) + :class (fn [^ResultSetMetaData o i] (.getColumnClassName o i)) + :display-size (fn [^ResultSetMetaData o i] (.getColumnDisplaySize o i)) + :label (fn [^ResultSetMetaData o i] (.getColumnLabel o i)) + :name (fn [^ResultSetMetaData o i] (.getColumnName o i)) + :precision (fn [^ResultSetMetaData o i] (.getPrecision o i)) + :scale (fn [^ResultSetMetaData o i] (.getScale o i)) + :schema (fn [^ResultSetMetaData o i] (.getSchemaName o i)) + :table (fn [^ResultSetMetaData o i] (.getTableName o i)) + ;; the is* fields: + :nullability (fn [^ResultSetMetaData o i] + (condp = (.isNullable o i) + ResultSetMetaData/columnNoNulls :not-null + ResultSetMetaData/columnNullable :null + :unknown)) + :auto-increment (fn [^ResultSetMetaData o i] (.isAutoIncrement o i)) + :case-sensitive (fn [^ResultSetMetaData o i] (.isCaseSensitive o i)) + :currency (fn [^ResultSetMetaData o i] (.isCurrency o i)) + :definitely-writable (fn [^ResultSetMetaData o i] (.isDefinitelyWritable o i)) + :read-only (fn [^ResultSetMetaData o i] (.isReadOnly o i)) + :searchable (fn [^ResultSetMetaData o i] (.isSearchable o i)) + :signed (fn [^ResultSetMetaData o i] (.isSigned o i)) + :writable (fn [^ResultSetMetaData o i] (.isWritable o i))}) + +(defn- safe-bean [o] + (try + ;; ensure we return a basic hash map: + (merge {} (bean o)) + (catch Throwable t + {:exception (ex-message t) + :cause (ex-message (ex-cause t))}))) + +(extend-protocol core-p/Datafiable + Connection + (datafy [this] + (with-meta (safe-bean this) + {`core-p/nav (fn [_ k v] + (if (= :metaData k) + (.getMetaData this) + v))})) + DatabaseMetaData + (datafy [this] + (with-meta (safe-bean this) + {`core-p/nav (fn [_ k v] + (condp = k + :catalogs + (rs/datafiable-result-set (.getCatalogs this) + (.getConnection this) + {}) + :clientInfoProperties + (rs/datafiable-result-set (.getClientInfoProperties this) + (.getConnection this) + {}) + :schemas + (rs/datafiable-result-set (.getSchemas this) + (.getConnection this) + {}) + :tableTypes + (rs/datafiable-result-set (.getTableTypes this) + (.getConnection this) + {}) + :typeInfo + (rs/datafiable-result-set (.getTypeInfo this) + (.getConnection this) + {}) + v))})) + ResultSetMetaData + (datafy [this] + (mapv #(reduce-kv (fn [m k f] (assoc m k (f this %))) + {} + column-meta) + (range 1 (inc (.getColumnCount this)))))) diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index 1f0a1cd..d97edef 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -397,7 +397,14 @@ (row-number [this] "Return the current 1-based row number, if available.") (column-names [this] - "Return a vector of the column names from the result set.")) + "Return a vector of the column names from the result set.") + (metadata [this] + "Return the raw `ResultSetMetaData` object from the result set. + + If `next.jdbc.datafy` has been required, this will be fully-realized + as a Clojure data structure, otherwise this should not be allowed to + 'leak' outside of the reducing function as it may depend on the + connection remaining open, in order to be valid.")) (defn- mapify-result-set "Given a `ResultSet`, return an object that wraps the current row as a hash @@ -420,6 +427,7 @@ InspectableMapifiedResultSet (row-number [this] (.getRow rs)) (column-names [this] (:cols @builder)) + (metadata [this] (core-p/datafy (:rsmeta @builder))) clojure.lang.IPersistentMap (assoc [this k v] @@ -500,7 +508,8 @@ ;; since we have to call these eagerly, we trap any exceptions so ;; that they can be thrown when the actual functions are called (let [row (try (.getRow rs) (catch Throwable t t)) - cols (try (:cols @builder) (catch Throwable t t))] + cols (try (:cols @builder) (catch Throwable t t)) + meta (try (core-p/datafy (:rsmeta @builder)) (catch Throwable t t))] (with-meta (row-builder @builder) {`core-p/datafy @@ -508,7 +517,9 @@ `row-number (fn [_] (if (instance? Throwable row) (throw row) row)) `column-names - (fn [_] (if (instance? Throwable cols) (throw cols) cols))}))) + (fn [_] (if (instance? Throwable cols) (throw cols) cols)) + `metadata + (fn [_] (if (instance? Throwable meta) (throw meta) meta))}))) (toString [_] (try diff --git a/test/next/jdbc/datafy_test.clj b/test/next/jdbc/datafy_test.clj new file mode 100644 index 0000000..5709bca --- /dev/null +++ b/test/next/jdbc/datafy_test.clj @@ -0,0 +1,47 @@ +;; copyright (c) 2019-2020 Sean Corfield, all rights reserved + +(ns next.jdbc.datafy-test + "Tests for the datafy extensions over JDBC types." + (:require [clojure.core.protocols :as core-p] + [clojure.set :as set] + [clojure.string :as str] + [clojure.test :refer [deftest is testing use-fixtures]] + [next.jdbc :as jdbc] + [next.jdbc.datafy] + [next.jdbc.test-fixtures :refer [with-test-db db ds + derby? + mssql?]] + [next.jdbc.specs :as specs]) + (:import (java.sql ResultSet))) + +(set! *warn-on-reflection* true) + +(use-fixtures :once with-test-db) + +(specs/instrument) + +(def ^:private basic-connection-keys + "Generic JDBC Connection fields." + #{:autoCommit :catalog :clientInfo :holdability :metaData + :networkTimeout :schema :transactionIsolation :typeMap :warnings + ;; boolean properties + :closed :readOnly + ;; added by bean itself + :class}) + +(deftest connection-datafy-tests + (testing "basic datafication" + (if (derby?) + (is (= #{:exception :cause} ; at least one property not supported + (set (keys (core-p/datafy (jdbc/get-connection (ds))))))) + (let [data (set (keys (core-p/datafy (jdbc/get-connection (ds)))))] + (when-let [diff (seq (set/difference data basic-connection-keys))] + (println (:dbtype (db)) (sort diff))) + (is (= basic-connection-keys + (set/intersection basic-connection-keys data)))))) + (testing "nav to metadata yields object" + (when-not (derby?) + (is (instance? java.sql.DatabaseMetaData + (core-p/nav (core-p/datafy (jdbc/get-connection (ds))) + :metaData + nil))))))