Expand datafy/nav and metadata support (work in progress)

This commit is contained in:
Sean Corfield 2020-05-30 23:44:18 -07:00
parent 8f6844aa5d
commit 6a6e42e9af
4 changed files with 156 additions and 3 deletions

View file

@ -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

91
src/next/jdbc/datafy.clj Normal file
View file

@ -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))))))

View file

@ -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

View file

@ -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))))))