;; copyright (c) 2020-2021 Sean Corfield, all rights reserved

(ns next.jdbc.datafy
  "This namespace provides datafication of several JDBC object types,
  all within the `java.sql` package:

  * `Connection` -- datafies as a bean.
  * `DatabaseMetaData` -- datafies as a bean; six properties
        are navigable to produce fully-realized datafiable result sets.
  * `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 properties 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])
  (:import (java.sql Connection
                     DatabaseMetaData
                     ParameterMetaData
                     ResultSet ResultSetMetaData
                     Statement)))

(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))
   :type      (fn [^ResultSetMetaData o i] (.getColumnTypeName 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))})

(def ^:private parameter-meta
  {:class     (fn [^ParameterMetaData o i] (.getParameterClassName o i))
   :mode      (fn [^ParameterMetaData o i]
                (condp = (.getParameterMode o i)
                       ParameterMetaData/parameterModeIn    :in
                       ParameterMetaData/parameterModeInOut :in-out
                       ParameterMetaData/parameterModeOut   :out
                       :unknown))
   :precision (fn [^ParameterMetaData o i] (.getPrecision   o i))
   :scale     (fn [^ParameterMetaData o i] (.getScale       o i))
   :type      (fn [^ParameterMetaData o i] (.getParameterTypeName o i))
   ;; the is* fields:
   :nullability (fn [^ParameterMetaData o i]
                  (condp = (.isNullable o i)
                         ParameterMetaData/parameterNoNulls  :not-null
                         ParameterMetaData/parameterNullable :null
                         :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 :exceptions *datafy-failure*))
    (catch Throwable t
      (let [dex   (juxt type (comp str ex-message))
            cause (ex-cause t)]
        (with-meta (cond-> {:exception (dex t)}
                     cause (assoc :cause (dex cause)))
          {:exception t})))))

(defn- datafy-result-set-meta-data
  [^ResultSetMetaData this]
  (mapv #(reduce-kv (fn [m k f] (assoc m k (f this %)))
                    {}
                    column-meta)
        (range 1 (inc (.getColumnCount this)))))

(defn- datafy-parameter-meta-data
  [^ParameterMetaData this]
  (mapv #(reduce-kv (fn [m k f] (assoc m k (f this %)))
                    {}
                    parameter-meta)
        (range 1 (inc (.getParameterCount this)))))

(extend-protocol core-p/Datafiable
  Connection
  (datafy [this] (safe-bean this {}))
  DatabaseMetaData
  (datafy [this]
    (with-meta (let [data (safe-bean this {})]
                 (cond-> data
                   (not (:exception (meta data)))
                   ;; add an opaque object that nav will "replace"
                   (assoc :all-tables (Object.))))
      {`core-p/nav (fn [_ k v]
                     (condp = k
                       :all-tables
                       (rs/datafiable-result-set (.getTables this nil nil nil nil)
                                                 (.getConnection this)
                                                 {})
                       :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))}))
  ParameterMetaData
  (datafy [this] (datafy-parameter-meta-data this))
  ResultSet
  (datafy [this]
    ;; SQLite has a combination ResultSet/Metadata object...
    (if (instance? ResultSetMetaData this)
      (datafy-result-set-meta-data this)
      (let [s (.getStatement this)
            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}})))
