2019-01-08 07:03:20 +00:00
|
|
|
;; copyright (c) 2018-2019 Sean Corfield, all rights reserved
|
2019-01-08 04:38:58 +00:00
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
(ns next.jdbc
|
|
|
|
|
""
|
2019-01-10 21:15:40 +00:00
|
|
|
(:require [clojure.set :as set])
|
2019-01-10 06:06:49 +00:00
|
|
|
(:import (java.lang AutoCloseable)
|
|
|
|
|
(java.sql Connection DriverManager
|
|
|
|
|
PreparedStatement
|
|
|
|
|
ResultSet ResultSetMetaData
|
|
|
|
|
SQLException Statement)
|
2019-01-08 07:03:20 +00:00
|
|
|
(javax.sql DataSource)
|
|
|
|
|
(java.util Properties)))
|
|
|
|
|
|
2019-01-10 06:06:49 +00:00
|
|
|
(comment
|
|
|
|
|
"Key areas of interaction:
|
|
|
|
|
1a. Making a DataSource -- turn everything connectable into a DataSource
|
|
|
|
|
1b. Given a DataSource, we can getConnection()
|
|
|
|
|
2. Preparing a Statement -- connection + SQL + params (+ options)
|
|
|
|
|
(multiple param groups means addBatch() calls)
|
|
|
|
|
3. Execute a (Prepared) Statement to produce a ResultSet (or update count)
|
|
|
|
|
(can execute batch of prepared statements and get multiple results)"
|
|
|
|
|
|
|
|
|
|
"Additional areas:
|
|
|
|
|
1. with-db-connection -- given 'something', get a connection, execute the
|
|
|
|
|
body, and close the connection (if we opened it).
|
|
|
|
|
2. with-db-transaction -- given 'something', get a connection, start a
|
|
|
|
|
transaction, execute the body, commit/rollback, and close the connection
|
|
|
|
|
(if we opened it else restore connection state)."
|
|
|
|
|
"Database metadata can tell us:
|
|
|
|
|
0. If get generated keys is supported!
|
|
|
|
|
1. If batch updates are supported
|
|
|
|
|
2. If save points are supported
|
|
|
|
|
3. If various concurrency/holdability/etc options are supported")
|
|
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
|
|
2019-01-10 07:05:21 +00:00
|
|
|
(defprotocol Sourceable
|
2019-01-11 02:30:05 +00:00
|
|
|
(get-datasource ^DataSource [this opts]))
|
2019-01-08 07:03:20 +00:00
|
|
|
(defprotocol Connectable
|
2019-01-10 06:06:49 +00:00
|
|
|
(get-connection ^AutoCloseable [this]))
|
2019-01-11 03:23:35 +00:00
|
|
|
(defprotocol Executable
|
|
|
|
|
(-execute ^clojure.lang.IReduceInit [this sql-params opts]))
|
2019-01-08 07:03:20 +00:00
|
|
|
(defprotocol Preparable
|
2019-01-10 06:06:49 +00:00
|
|
|
(prepare ^PreparedStatement [this sql-params opts]))
|
2019-01-11 02:30:05 +00:00
|
|
|
(defprotocol WithOptions
|
|
|
|
|
(get-options [this]))
|
|
|
|
|
|
2019-01-11 03:23:35 +00:00
|
|
|
(defn execute!
|
|
|
|
|
"General SQL execution function.
|
|
|
|
|
|
|
|
|
|
Returns a reducible that, when reduced, runs the SQL and yields the result."
|
|
|
|
|
([stmt] (-execute stmt [] {}))
|
|
|
|
|
([connectable sql-params & [opts]]
|
|
|
|
|
(-execute connectable sql-params opts)))
|
|
|
|
|
|
2019-01-11 02:30:05 +00:00
|
|
|
(extend-protocol
|
|
|
|
|
WithOptions
|
|
|
|
|
Object
|
|
|
|
|
(get-options [_] {})
|
|
|
|
|
nil
|
|
|
|
|
(get-options [_] {}))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
2019-01-10 19:32:46 +00:00
|
|
|
(defn set-parameters
|
|
|
|
|
""
|
|
|
|
|
^PreparedStatement
|
|
|
|
|
[^PreparedStatement ps params]
|
2019-01-11 02:46:24 +00:00
|
|
|
(when (seq params)
|
2019-01-11 02:47:48 +00:00
|
|
|
(loop [[p & more] params i 1]
|
|
|
|
|
(.setObject ps i p)
|
2019-01-11 02:46:24 +00:00
|
|
|
(when more
|
|
|
|
|
(recur more (inc i)))))
|
|
|
|
|
ps)
|
2019-01-10 19:32:46 +00:00
|
|
|
|
2019-01-10 06:06:49 +00:00
|
|
|
(defn- prepare*
|
|
|
|
|
"Given a connection, a SQL statement, its parameters, and some options,
|
|
|
|
|
return a PreparedStatement representing that."
|
|
|
|
|
[^Connection con [sql & params] opts]
|
2019-01-10 19:32:46 +00:00
|
|
|
(doto (.prepareStatement con sql)
|
|
|
|
|
(set-parameters params)))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
|
|
|
|
(def ^:private isolation-levels
|
|
|
|
|
"Transaction isolation levels."
|
2019-01-10 07:05:21 +00:00
|
|
|
{:none Connection/TRANSACTION_NONE
|
|
|
|
|
:read-committed Connection/TRANSACTION_READ_COMMITTED
|
|
|
|
|
:read-uncommitted Connection/TRANSACTION_READ_UNCOMMITTED
|
|
|
|
|
:repeatable-read Connection/TRANSACTION_REPEATABLE_READ
|
|
|
|
|
:serializable Connection/TRANSACTION_SERIALIZABLE})
|
2019-01-08 07:03:20 +00:00
|
|
|
|
|
|
|
|
(def ^:private isolation-kws
|
|
|
|
|
"Map transaction isolation constants to our keywords."
|
|
|
|
|
(set/map-invert isolation-levels))
|
|
|
|
|
|
|
|
|
|
(defn get-isolation-level
|
|
|
|
|
"Given an actual JDBC connection, return the current transaction
|
|
|
|
|
isolation level, if known. Return :unknown if we do not recognize
|
|
|
|
|
the isolation level."
|
|
|
|
|
[^Connection jdbc]
|
|
|
|
|
(isolation-kws (.getTransactionIsolation jdbc) :unknown))
|
|
|
|
|
|
|
|
|
|
(defn committable! [con commit?]
|
|
|
|
|
(when-let [state (:transacted con)]
|
|
|
|
|
(reset! state commit?))
|
|
|
|
|
con)
|
|
|
|
|
|
2019-01-11 02:33:12 +00:00
|
|
|
(defn transact*
|
2019-01-08 07:03:20 +00:00
|
|
|
""
|
2019-01-10 06:06:49 +00:00
|
|
|
[con transacted f opts]
|
|
|
|
|
(let [{:keys [isolation read-only? rollback-only?]} opts
|
2019-01-08 07:03:20 +00:00
|
|
|
committable? (not rollback-only?)]
|
|
|
|
|
(if transacted
|
|
|
|
|
;; should check isolation level; maybe implement save points?
|
|
|
|
|
(f con)
|
2019-01-10 06:06:49 +00:00
|
|
|
(with-open [^AutoCloseable t-con (assoc (get-connection con)
|
2019-01-11 02:24:55 +00:00
|
|
|
;; FIXME: not a record/map!
|
2019-01-10 06:06:49 +00:00
|
|
|
:transacted (atom committable?))]
|
2019-01-10 07:05:21 +00:00
|
|
|
(let [^Connection jdbc t-con
|
2019-01-08 07:03:20 +00:00
|
|
|
old-autocommit (.getAutoCommit jdbc)
|
|
|
|
|
old-isolation (.getTransactionIsolation jdbc)
|
|
|
|
|
old-readonly (.isReadOnly jdbc)]
|
|
|
|
|
(io!
|
|
|
|
|
(when isolation
|
|
|
|
|
(.setTransactionIsolation jdbc (isolation isolation-levels)))
|
|
|
|
|
(when read-only?
|
|
|
|
|
(.setReadOnly jdbc true))
|
|
|
|
|
(.setAutoCommit jdbc false)
|
|
|
|
|
(try
|
|
|
|
|
(let [result (f t-con)]
|
|
|
|
|
(if @(:transacted t-con)
|
|
|
|
|
(.commit jdbc)
|
|
|
|
|
(.rollback jdbc))
|
|
|
|
|
result)
|
|
|
|
|
(catch Throwable t
|
|
|
|
|
(try
|
|
|
|
|
(.rollback jdbc)
|
|
|
|
|
(catch Throwable rb
|
|
|
|
|
;; combine both exceptions
|
|
|
|
|
(throw (ex-info (str "Rollback failed handling \""
|
|
|
|
|
(.getMessage t)
|
|
|
|
|
"\"")
|
|
|
|
|
{:rollback rb
|
|
|
|
|
:handling t}))))
|
|
|
|
|
(throw t))
|
|
|
|
|
(finally ; tear down
|
|
|
|
|
(committable! t-con committable?)
|
|
|
|
|
;; the following can throw SQLExceptions but we do not
|
|
|
|
|
;; want those to replace any exception currently being
|
|
|
|
|
;; handled -- and if the connection got closed, we just
|
|
|
|
|
;; want to ignore exceptions here anyway
|
|
|
|
|
(try
|
|
|
|
|
(.setAutoCommit jdbc old-autocommit)
|
|
|
|
|
(catch Exception _))
|
|
|
|
|
(when isolation
|
|
|
|
|
(try
|
|
|
|
|
(.setTransactionIsolation jdbc old-isolation)
|
|
|
|
|
(catch Exception _)))
|
|
|
|
|
(when read-only?
|
|
|
|
|
(try
|
|
|
|
|
(.setReadOnly jdbc old-readonly)
|
|
|
|
|
(catch Exception _)))))))))))
|
|
|
|
|
|
|
|
|
|
(defmacro in-transaction
|
|
|
|
|
[[sym con opts] & body]
|
2019-01-11 02:33:12 +00:00
|
|
|
`(transact* ~con (fn [~sym] ~@body) ~opts))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
|
|
|
|
(def ^:private classnames
|
|
|
|
|
"Map of subprotocols to classnames. dbtype specifies one of these keys.
|
|
|
|
|
|
|
|
|
|
The subprotocols map below provides aliases for dbtype.
|
|
|
|
|
|
|
|
|
|
Most databases have just a single class name for their driver but we
|
|
|
|
|
support a sequence of class names to try in order to allow for drivers
|
|
|
|
|
that change their names over time (e.g., MySQL)."
|
|
|
|
|
{"derby" "org.apache.derby.jdbc.EmbeddedDriver"
|
|
|
|
|
"h2" "org.h2.Driver"
|
|
|
|
|
"h2:mem" "org.h2.Driver"
|
|
|
|
|
"hsqldb" "org.hsqldb.jdbcDriver"
|
|
|
|
|
"jtds:sqlserver" "net.sourceforge.jtds.jdbc.Driver"
|
|
|
|
|
"mysql" ["com.mysql.cj.jdbc.Driver"
|
|
|
|
|
"com.mysql.jdbc.Driver"]
|
|
|
|
|
"oracle:oci" "oracle.jdbc.OracleDriver"
|
|
|
|
|
"oracle:thin" "oracle.jdbc.OracleDriver"
|
|
|
|
|
"postgresql" "org.postgresql.Driver"
|
|
|
|
|
"pgsql" "com.impossibl.postgres.jdbc.PGDriver"
|
|
|
|
|
"redshift" "com.amazon.redshift.jdbc.Driver"
|
|
|
|
|
"sqlite" "org.sqlite.JDBC"
|
|
|
|
|
"sqlserver" "com.microsoft.sqlserver.jdbc.SQLServerDriver"})
|
|
|
|
|
|
|
|
|
|
(def ^:private aliases
|
|
|
|
|
"Map of schemes to subprotocols. Used to provide aliases for dbtype."
|
|
|
|
|
{"hsql" "hsqldb"
|
|
|
|
|
"jtds" "jtds:sqlserver"
|
|
|
|
|
"mssql" "sqlserver"
|
|
|
|
|
"oracle" "oracle:thin"
|
|
|
|
|
"oracle:sid" "oracle:thin"
|
|
|
|
|
"postgres" "postgresql"})
|
|
|
|
|
|
|
|
|
|
(def ^:private host-prefixes
|
|
|
|
|
"Map of subprotocols to non-standard host-prefixes.
|
|
|
|
|
Anything not listed is assumed to use //."
|
|
|
|
|
{"oracle:oci" "@"
|
|
|
|
|
"oracle:thin" "@"})
|
|
|
|
|
|
|
|
|
|
(def ^:private ports
|
|
|
|
|
"Map of subprotocols to ports."
|
|
|
|
|
{"jtds:sqlserver" 1433
|
|
|
|
|
"mysql" 3306
|
|
|
|
|
"oracle:oci" 1521
|
|
|
|
|
"oracle:sid" 1521
|
|
|
|
|
"oracle:thin" 1521
|
|
|
|
|
"postgresql" 5432
|
|
|
|
|
"sqlserver" 1433})
|
|
|
|
|
|
|
|
|
|
(def ^:private dbname-separators
|
|
|
|
|
"Map of schemes to separators. The default is / but a couple are different."
|
|
|
|
|
{"mssql" ";DATABASENAME="
|
|
|
|
|
"sqlserver" ";DATABASENAME="
|
|
|
|
|
"oracle:sid" ":"})
|
|
|
|
|
|
|
|
|
|
(defn- modify-connection
|
|
|
|
|
"Given a database connection and a map of options, update the connection
|
|
|
|
|
as specified by the options."
|
2019-01-10 07:05:21 +00:00
|
|
|
^Connection
|
|
|
|
|
[^Connection connection opts]
|
2019-01-08 07:03:20 +00:00
|
|
|
(when (and connection (contains? opts :auto-commit?))
|
|
|
|
|
(.setAutoCommit connection (boolean (:auto-commit? opts))))
|
|
|
|
|
(when (and connection (contains? opts :read-only?))
|
|
|
|
|
(.setReadOnly connection (boolean (:read-only? opts))))
|
|
|
|
|
connection)
|
|
|
|
|
|
|
|
|
|
(defn- ^Properties as-properties
|
|
|
|
|
"Convert any seq of pairs to a java.utils.Properties instance.
|
|
|
|
|
Uses as-sql-name to convert both keys and values into strings."
|
|
|
|
|
[m]
|
|
|
|
|
(let [p (Properties.)]
|
|
|
|
|
(doseq [[k v] m]
|
|
|
|
|
(.setProperty p (name k) (str v)))
|
|
|
|
|
p))
|
|
|
|
|
|
|
|
|
|
(defn- get-driver-connection
|
|
|
|
|
"Common logic for loading the DriverManager and the designed JDBC driver
|
|
|
|
|
class and obtaining the appropriate Connection object."
|
2019-01-10 07:05:21 +00:00
|
|
|
[url etc]
|
2019-01-10 07:30:46 +00:00
|
|
|
;; force DriverManager to be loaded
|
|
|
|
|
(DriverManager/getLoginTimeout)
|
2019-01-08 07:03:20 +00:00
|
|
|
(-> (DriverManager/getConnection url (as-properties etc))
|
2019-01-10 06:06:49 +00:00
|
|
|
(modify-connection etc)))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
2019-01-10 07:30:46 +00:00
|
|
|
(defn- spec->url+etc
|
2019-01-08 07:03:20 +00:00
|
|
|
""
|
2019-01-10 06:06:49 +00:00
|
|
|
[{:keys [dbtype dbname host port classname] :as db-spec}]
|
2019-01-08 07:03:20 +00:00
|
|
|
(let [;; allow aliases for dbtype
|
|
|
|
|
subprotocol (aliases dbtype dbtype)
|
|
|
|
|
host (or host "127.0.0.1")
|
|
|
|
|
port (or port (ports subprotocol))
|
|
|
|
|
db-sep (dbname-separators dbtype "/")
|
|
|
|
|
url (cond (= "h2:mem" dbtype)
|
|
|
|
|
(str "jdbc:" subprotocol ":" dbname ";DB_CLOSE_DELAY=-1")
|
|
|
|
|
(#{"derby" "h2" "hsqldb" "sqlite"} subprotocol)
|
|
|
|
|
(str "jdbc:" subprotocol ":" dbname)
|
|
|
|
|
:else
|
|
|
|
|
(str "jdbc:" subprotocol ":"
|
|
|
|
|
(host-prefixes subprotocol "//")
|
|
|
|
|
host
|
|
|
|
|
(when port (str ":" port))
|
|
|
|
|
db-sep dbname))
|
|
|
|
|
etc (dissoc db-spec :dbtype :dbname)]
|
2019-01-10 07:05:21 +00:00
|
|
|
;; verify the datasource is loadable
|
|
|
|
|
(if-let [class-name (or classname (classnames subprotocol))]
|
|
|
|
|
(do
|
|
|
|
|
(if (string? class-name)
|
|
|
|
|
(clojure.lang.RT/loadClassForName class-name)
|
|
|
|
|
(loop [[clazz & more] class-name]
|
|
|
|
|
(when-let [load-failure
|
|
|
|
|
(try
|
|
|
|
|
(clojure.lang.RT/loadClassForName clazz)
|
|
|
|
|
nil
|
|
|
|
|
(catch Exception e
|
|
|
|
|
e))]
|
|
|
|
|
(if (seq more)
|
|
|
|
|
(recur more)
|
|
|
|
|
(throw load-failure))))))
|
|
|
|
|
(throw (ex-info (str "Unknown dbtype: " dbtype) db-spec)))
|
2019-01-10 07:30:46 +00:00
|
|
|
[url etc]))
|
|
|
|
|
|
|
|
|
|
(defn- string->url+etc
|
2019-01-08 07:03:20 +00:00
|
|
|
""
|
|
|
|
|
[s]
|
2019-01-10 07:30:46 +00:00
|
|
|
[s {}])
|
|
|
|
|
|
|
|
|
|
(defn- url+etc->datasource
|
|
|
|
|
""
|
2019-01-11 02:30:05 +00:00
|
|
|
[[url etc] opts]
|
2019-01-11 03:23:35 +00:00
|
|
|
(reify DataSource
|
2019-01-11 02:30:05 +00:00
|
|
|
(getConnection [_]
|
2019-01-10 07:30:46 +00:00
|
|
|
(get-driver-connection url etc))
|
2019-01-11 02:30:05 +00:00
|
|
|
(getConnection [_ username password]
|
2019-01-10 07:30:46 +00:00
|
|
|
(get-driver-connection url
|
|
|
|
|
(assoc etc
|
|
|
|
|
:username username
|
2019-01-11 02:30:05 +00:00
|
|
|
:password password)))
|
|
|
|
|
WithOptions
|
|
|
|
|
(get-options [_] opts)))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
2019-01-11 03:23:35 +00:00
|
|
|
(extend-protocol Sourceable
|
|
|
|
|
clojure.lang.Associative
|
|
|
|
|
(get-datasource [this opts]
|
|
|
|
|
(url+etc->datasource (spec->url+etc this) opts))
|
|
|
|
|
DataSource
|
|
|
|
|
(get-datasource [this opts]
|
|
|
|
|
(reify DataSource
|
|
|
|
|
(getConnection [_]
|
|
|
|
|
(.getConnection this))
|
|
|
|
|
(getConnection [_ username password]
|
|
|
|
|
(.getConnection this username password))
|
|
|
|
|
WithOptions
|
|
|
|
|
(get-options [_] opts)))
|
|
|
|
|
String
|
|
|
|
|
(get-datasource [this opts]
|
|
|
|
|
(url+etc->datasource (string->url+etc this) opts)))
|
|
|
|
|
|
|
|
|
|
(extend-protocol Connectable
|
|
|
|
|
DataSource
|
|
|
|
|
(get-connection [this] (.getConnection this))
|
|
|
|
|
Object
|
|
|
|
|
(get-connection [this] (get-connection (get-datasource this {}))))
|
|
|
|
|
|
|
|
|
|
(extend-protocol Preparable
|
|
|
|
|
Connection
|
|
|
|
|
(prepare [this sql-params opts]
|
|
|
|
|
(prepare* this sql-params opts))
|
|
|
|
|
DataSource
|
|
|
|
|
(prepare [this sql-params opts]
|
|
|
|
|
(prepare (.getConnection this)
|
|
|
|
|
sql-params
|
|
|
|
|
(merge (get-options this) opts)))
|
|
|
|
|
Object
|
|
|
|
|
(prepare [this sql-params opts]
|
|
|
|
|
(prepare (get-datasource this opts) sql-params opts)))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
|
|
|
|
(defn- get-column-names
|
|
|
|
|
""
|
|
|
|
|
[^ResultSet rs]
|
|
|
|
|
(let [^ResultSetMetaData rsmeta (.getMetaData rs)
|
|
|
|
|
idxs (range 1 (inc (.getColumnCount rsmeta)))]
|
|
|
|
|
(mapv (fn [^Integer i]
|
2019-01-10 19:33:19 +00:00
|
|
|
(keyword (.getTableName rsmeta i)
|
|
|
|
|
(.getColumnLabel rsmeta i)))
|
2019-01-08 07:03:20 +00:00
|
|
|
idxs)))
|
|
|
|
|
|
|
|
|
|
(defn- mapify-result-set
|
|
|
|
|
"Given a result set, return an object that wraps the current row as a hash
|
|
|
|
|
map. Note that a result set is mutable and the current row will change behind
|
|
|
|
|
this wrapper so operations need to be eager (and fairly limited).
|
2019-01-10 06:06:49 +00:00
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
Supports ILookup (keywords are treated as strings).
|
2019-01-10 06:06:49 +00:00
|
|
|
|
2019-01-11 03:23:35 +00:00
|
|
|
Supports Associative (again, keywords are treated as strings).
|
2019-01-10 06:06:49 +00:00
|
|
|
|
|
|
|
|
Supports Seqable which realizes a full row of the data.
|
|
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
Later we may realize a new hash map when assoc (and other, future, operations
|
|
|
|
|
are performed on the result set row)."
|
2019-01-10 19:35:17 +00:00
|
|
|
[^ResultSet rs]
|
2019-01-08 07:03:20 +00:00
|
|
|
(let [cols (delay (get-column-names rs))]
|
|
|
|
|
(reify
|
2019-01-10 06:06:49 +00:00
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
clojure.lang.ILookup
|
|
|
|
|
(valAt [this k]
|
|
|
|
|
(try
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
(catch SQLException _)))
|
|
|
|
|
(valAt [this k not-found]
|
|
|
|
|
(try
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
(catch SQLException _
|
|
|
|
|
not-found)))
|
2019-01-10 06:06:49 +00:00
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
clojure.lang.Associative
|
|
|
|
|
(containsKey [this k]
|
|
|
|
|
(try
|
|
|
|
|
(.getObject rs (name k))
|
|
|
|
|
true
|
|
|
|
|
(catch SQLException _
|
|
|
|
|
false)))
|
|
|
|
|
(entryAt [this k]
|
|
|
|
|
(try
|
|
|
|
|
(clojure.lang.MapEntry. k (.getObject rs (name k)))
|
|
|
|
|
(catch SQLException _)))
|
2019-01-10 21:16:14 +00:00
|
|
|
(assoc [this k v]
|
|
|
|
|
(assoc (into {} (seq this)) k v))
|
2019-01-10 06:06:49 +00:00
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
clojure.lang.Seqable
|
|
|
|
|
(seq [this]
|
2019-01-10 06:06:49 +00:00
|
|
|
(seq (mapv (fn [^Integer i]
|
|
|
|
|
(clojure.lang.MapEntry. (nth @cols (dec i))
|
|
|
|
|
(.getObject rs i)))
|
|
|
|
|
(range 1 (inc (count @cols)))))))))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
2019-01-10 19:35:17 +00:00
|
|
|
(defn- reduce-stmt
|
|
|
|
|
""
|
|
|
|
|
[^PreparedStatement stmt f init]
|
|
|
|
|
(if (.execute stmt)
|
|
|
|
|
(let [rs (.getResultSet stmt)
|
|
|
|
|
rs-map (mapify-result-set rs)]
|
|
|
|
|
(loop [init' init]
|
|
|
|
|
(if (.next rs)
|
|
|
|
|
(let [result (f init' rs-map)]
|
|
|
|
|
(if (reduced? result)
|
|
|
|
|
@result
|
|
|
|
|
(recur result)))
|
|
|
|
|
init')))
|
2019-01-11 07:25:29 +00:00
|
|
|
(f init (.getUpdateCount stmt))))
|
2019-01-10 19:35:17 +00:00
|
|
|
|
2019-01-11 03:23:35 +00:00
|
|
|
(extend-protocol Executable
|
|
|
|
|
Connection
|
|
|
|
|
(-execute [this sql-params opts]
|
|
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
(reduce [_ f init]
|
|
|
|
|
(with-open [stmt (prepare this sql-params opts)]
|
|
|
|
|
(reduce-stmt stmt f init)))))
|
|
|
|
|
DataSource
|
|
|
|
|
(-execute [this sql-params opts]
|
|
|
|
|
(let [opts (merge (get-options this) opts)]
|
|
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
(reduce [_ f init]
|
|
|
|
|
(with-open [con (get-connection this)]
|
|
|
|
|
(with-open [stmt (prepare con sql-params opts)]
|
|
|
|
|
(reduce-stmt stmt f init)))))))
|
|
|
|
|
PreparedStatement
|
|
|
|
|
(-execute [this _ _]
|
|
|
|
|
(reify clojure.lang.IReduceInit
|
|
|
|
|
(reduce [_ f init] (reduce-stmt this f init))))
|
|
|
|
|
Object
|
|
|
|
|
(-execute [this sql-params opts]
|
|
|
|
|
(-execute (get-datasource this opts) sql-params opts)))
|
2019-01-08 07:03:20 +00:00
|
|
|
|
2019-01-10 19:44:19 +00:00
|
|
|
(defn query
|
|
|
|
|
""
|
|
|
|
|
[connectable sql-params & [opts]]
|
|
|
|
|
(into [] (map (partial into {})) (execute! connectable sql-params opts)))
|
|
|
|
|
|
2019-01-08 07:03:20 +00:00
|
|
|
(comment
|
2019-01-10 06:06:49 +00:00
|
|
|
(def db-spec {:dbtype "h2:mem" :dbname "perf"})
|
2019-01-10 07:30:46 +00:00
|
|
|
(def con db-spec)
|
2019-01-11 02:30:05 +00:00
|
|
|
(def con (get-datasource db-spec {}))
|
2019-01-11 07:25:29 +00:00
|
|
|
(get-connection con)
|
|
|
|
|
(def con (get-connection (get-datasource db-spec {})))
|
2019-01-10 06:06:49 +00:00
|
|
|
(def con (get-connection db-spec))
|
|
|
|
|
(reduce + 0 (execute! con ["DROP TABLE fruit"]))
|
|
|
|
|
(reduce + 0 (execute! con ["CREATE TABLE fruit (id int default 0, name varchar(32) primary key, appearance varchar(32), cost int, grade real)"]))
|
|
|
|
|
(reduce + 0 (execute! con ["INSERT INTO fruit (id,name,appearance,cost,grade) VALUES (1,'Apple','red',59,87), (2,'Banana','yellow',29,92.2), (3,'Peach','fuzzy',139,90.0), (4,'Orange','juicy',89,88.6)"]))
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-11 02:47:48 +00:00
|
|
|
(println con)
|
2019-01-10 06:06:49 +00:00
|
|
|
(close con)
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-10 06:06:49 +00:00
|
|
|
(require '[criterium.core :refer [bench quick-bench]])
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-10 07:05:21 +00:00
|
|
|
;; calibrate
|
2019-01-10 06:06:49 +00:00
|
|
|
(quick-bench (reduce + (take 10e6 (range))))
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-11 02:47:48 +00:00
|
|
|
;; same as the Java example in java.jdbc perf test
|
|
|
|
|
(quick-bench
|
|
|
|
|
(reduce (fn [rs m] (reduced (:name m)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! con ["select * from fruit where appearance = ?" "red"])))
|
|
|
|
|
|
|
|
|
|
;; simple query
|
2019-01-10 21:16:29 +00:00
|
|
|
(quick-bench
|
|
|
|
|
(query con ["select * from fruit where appearance = ?" "red"]))
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-11 02:47:48 +00:00
|
|
|
;; with a prepopulated prepared statement
|
|
|
|
|
(with-open [ps (prepare con ["select * from fruit where appearance = ?" "red"] {})]
|
2019-01-10 19:35:17 +00:00
|
|
|
(quick-bench
|
2019-01-11 02:47:48 +00:00
|
|
|
[(reduce (fn [_ row] (reduced (:name row)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! ps))]))
|
2019-01-11 02:30:05 +00:00
|
|
|
|
2019-01-11 02:47:48 +00:00
|
|
|
;; same as above but setting parameters inside the benchmark
|
2019-01-11 02:30:05 +00:00
|
|
|
(with-open [ps (prepare con ["select * from fruit where appearance = ?"] {})]
|
|
|
|
|
(quick-bench
|
|
|
|
|
[(reduce (fn [_ row] (reduced (:name row)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! (set-parameters ps ["red"])))]))
|
|
|
|
|
|
|
|
|
|
;; this takes more than twice the time of the one above which seems strange
|
2019-01-10 19:35:17 +00:00
|
|
|
(with-open [ps (prepare con ["select * from fruit where appearance = ?"] {})]
|
|
|
|
|
(quick-bench
|
|
|
|
|
[(reduce (fn [_ row] (reduced (:name row)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! (set-parameters ps ["red"])))
|
|
|
|
|
(reduce (fn [_ row] (reduced (:name row)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! (set-parameters ps ["fuzzy"])))]))
|
2019-01-11 02:47:48 +00:00
|
|
|
|
|
|
|
|
;; full first row
|
2019-01-10 06:06:49 +00:00
|
|
|
(quick-bench
|
|
|
|
|
(reduce (fn [rs m] (reduced (into {} m)))
|
|
|
|
|
nil
|
2019-01-10 21:16:14 +00:00
|
|
|
(execute! con ["select * from fruit where appearance = ?" "red"])))
|
2019-01-11 02:47:48 +00:00
|
|
|
|
|
|
|
|
;; test assoc works
|
2019-01-10 21:16:14 +00:00
|
|
|
(reduce (fn [rs m] (reduced (assoc m :test :value)))
|
|
|
|
|
nil
|
|
|
|
|
(execute! con ["select * from fruit where appearance = ?" "red"])))
|