From 6e08557d92fa73bfa1d5296f135067e48693cfb1 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Sun, 31 Mar 2019 23:17:12 -0700 Subject: [PATCH] Add docstrings to everything --- doc/intro.md | 3 - src/next/jdbc.clj | 105 ++++++++++++++++++++++++++++++---- src/next/jdbc/connection.clj | 24 +++++--- src/next/jdbc/prepare.clj | 11 +++- src/next/jdbc/protocols.clj | 26 ++++++++- src/next/jdbc/result_set.clj | 45 +++++++++++++-- src/next/jdbc/sql.clj | 47 ++++++++++++--- src/next/jdbc/transaction.clj | 8 +-- test/next/jdbc_test.clj | 20 +++---- 9 files changed, 233 insertions(+), 56 deletions(-) delete mode 100644 doc/intro.md diff --git a/doc/intro.md b/doc/intro.md deleted file mode 100644 index 85177b0..0000000 --- a/doc/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Introduction to next.jdbc - -TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 96613a3..5e7c4d0 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -1,7 +1,41 @@ ;; copyright (c) 2018-2019 Sean Corfield, all rights reserved (ns next.jdbc - "" + "The public API of the next generation java.jdbc library. + + The basic building blocks are the java.sql/javax.sql classes: + * DataSource -- something to get connections from, + * Connection -- an active connection to the database, + * PreparedStatement -- SQL and parameters combined, from a connection, + * reducible! -- given a connectable and SQL + parameters or a statement, + return a reducible that, when reduced will execute the SQL and consume + the ResultSet produced, + * execute! -- given a connectable and SQL + parameters or a statement, + execute the SQL, consume the ResultSet produced, and return a vector + of hash maps representing the rows; this can be datafied to allow + navigation of foreign keys into other tables (either by convention or + via a schema definition). + * with-transaction -- execute a series of SQL operations within a transaction. + + In addition, there are some utility functions that make common operations + easier by providing some syntactic sugar over 'execute!'. + + The following options are supported generally: + * :entities -- specify a function used to convert strings to SQL entity names + (to turn table and column names into appropriate SQL names), + * :identifiers -- specify a function used to convert SQL entity (column) + names to Clojure names (that are then turned into keywords), + * :row-fn -- when consuming a ResultSet, apply this function to each row of + data; defaults to a function that produces a datafiable hash map. + + The following options are supported where a PreparedStatement is created: + * :concurrency -- :read-only, :updatable, + * :cursors -- :close, :hold + * :fetch-size -- the fetch size value, + * :max-rows -- the maximum number of rows to return, + * :result-type -- :forward-only, :scroll-insensitive, :scroll-sensitive, + * :return-keys -- either true or a vector of key names to return, + * :timeout -- the query timeout." (:require [next.jdbc.connection] ; used to extend protocols [next.jdbc.prepare :as prepare] ; used to extend protocols [next.jdbc.protocols :as p] @@ -11,11 +45,31 @@ (set! *warn-on-reflection* true) -(defn get-datasource [spec] (p/get-datasource spec)) +(defn get-datasource + "Given some sort of specification of a database, return a DataSource." + [spec] + (p/get-datasource spec)) -(defn get-connection [spec opts] (p/get-connection spec opts)) +(defn get-connection + "Given some sort of specification of a database, return a new Connection. -(defn prepare [spec sql-params opts] (p/prepare spec sql-params opts)) + In general, this should be used via with-open: + + (with-open [con (get-connection spec opts)] + (run-some-ops con))" + [spec opts] + (p/get-connection spec opts)) + +(defn prepare + "Given some sort of specification of a database, and a vector containing + SQL and any parameters it needs, return a new PreparedStatement. + + In general, this should be used via with-open: + + (with-open [stmt (prepare spec sql-params opts)] + (run-some-ops stmt))" + [spec sql-params opts] + (p/prepare spec sql-params opts)) (defn reducible! "General SQL execution function. @@ -26,7 +80,9 @@ (p/-execute connectable sql-params opts))) (defn execute! - "" + "General SQL execution function. + + Invokes 'reducible!' and then reduces that into a vector of hash maps." ([stmt] (rs/execute! stmt [] {})) ([connectable sql-params] @@ -35,7 +91,9 @@ (rs/execute! connectable sql-params opts))) (defn execute-one! - "" + "General SQL execution function that returns just the first row of a result. + + Invokes 'reducible!' but immediately returns the first row." ([stmt] (rs/execute-one! stmt [] {})) ([connectable sql-params] @@ -44,11 +102,22 @@ (rs/execute-one! connectable sql-params opts))) (defmacro with-transaction + "Given a connectable object, gets a new connection and binds it to 'sym', + then executes the 'body' in that context, committing any changes if the body + completes successfully, otherwise rolling back any changes made. + + The options map supports: + * isolation -- :none, :read-committed, :read-uncommitted, :repeatable-read, + :serializable, + * :read-only -- true / false, + * :rollback-only -- true / false." [[sym connectable opts] & body] `(p/-transact ~connectable (fn [~sym] ~@body) ~opts)) (defn insert! - "" + "Given a connectable object, a table name, and a data hash map, inserts the + data as a single row in the database and attempts to return a map of generated + keys." ([connectable table key-map] (rs/execute! connectable (sql/for-insert table key-map {}) @@ -59,7 +128,10 @@ (merge {:return-keys true} opts)))) (defn insert-multi! - "" + "Given a connectable object, a table name, a sequence of column names, and + a vector of rows of data (vectors of column values), inserts the data as + multiple rows in the database and attempts to return a vector of maps of + generated keys." ([connectable table cols rows] (rs/execute! connectable (sql/for-insert-multi table cols rows {}) @@ -70,14 +142,19 @@ (merge {:return-keys true} opts)))) (defn find-by-keys - "" + "Given a connectable object, a table name, and a hash map of columns and + their values, returns a vector of hash maps of rows that match." ([connectable table key-map] (rs/execute! connectable (sql/for-query table key-map {}) {})) ([connectable table key-map opts] (rs/execute! connectable (sql/for-query table key-map opts) opts))) (defn get-by-id - "" + "Given a connectable object, a table name, and a primary key value, returns + a hash map of the first row that matches. + + By default, the primary key is assumed to be 'id' but that can be overridden + in the five-argument call." ([connectable table pk] (rs/execute-one! connectable (sql/for-query table {:id pk} {}) {})) ([connectable table pk opts] @@ -86,14 +163,18 @@ (rs/execute-one! connectable (sql/for-query table {pk-name pk} opts) opts))) (defn update! - "" + "Given a connectable object, a table name, a hash map of columns and values + to set, and either a hash map of columns and values to search on or a vector + of a SQL where clause and parameters, perform an update on the table." ([connectable table key-map where-params] (rs/execute! connectable (sql/for-update table key-map where-params {}) {})) ([connectable table key-map where-params opts] (rs/execute! connectable (sql/for-update table key-map where-params opts) opts))) (defn delete! - "" + "Given a connectable object, a table name, and either a hash map of columns + and values to search on or a vector of a SQL where clause and parameters, + perform a delete on the table." ([connectable table where-params] (rs/execute! connectable (sql/for-delete table where-params {}) {})) ([connectable table where-params opts] diff --git a/src/next/jdbc/connection.clj b/src/next/jdbc/connection.clj index ea3f8c3..d73dbb4 100644 --- a/src/next/jdbc/connection.clj +++ b/src/next/jdbc/connection.clj @@ -1,7 +1,7 @@ ;; copyright (c) 2018-2019 Sean Corfield, all rights reserved (ns next.jdbc.connection - "" + "Standard implementations of get-datasource and get-connection." (:require [next.jdbc.protocols :as p]) (:import (java.sql Connection DriverManager) (javax.sql DataSource) @@ -81,7 +81,7 @@ (DriverManager/getConnection url (as-properties etc))) (defn- spec->url+etc - "" + "Given a database spec, return a JDBC URL and a map of any additional options." [{:keys [dbtype dbname host port classname] :as db-spec}] (let [;; allow aliases for dbtype subprotocol (aliases dbtype dbtype) @@ -118,12 +118,13 @@ [url etc])) (defn- string->url+etc - "" + "Given a JDBC URL, return it with an empty set of options with no parsing." [s] [s {}]) (defn- url+etc->datasource - "" + "Given a JDBC URL and a map of options, return a DataSource that can be + used to obtain a new database connection." [[url etc]] (reify DataSource (getConnection [_] @@ -136,14 +137,19 @@ (defn- make-connection "Given a DataSource and a map of options, get a connection and update it - as specified by the options." + as specified by the options. + + The options supported are: + * :auto-commit -- whether the connection should be set to auto-commit or not; + without this option, the defaut is true -- connections will auto-commit, + * :read-only -- whether the connection should be set to read-only mode." ^Connection [^DataSource datasource opts] (let [^Connection connection (.getConnection datasource)] - (when (contains? opts :auto-commit?) - (.setAutoCommit connection (boolean (:auto-commit? opts)))) - (when (contains? opts :read-only?) - (.setReadOnly connection (boolean (:read-only? opts)))) + (when (contains? opts :auto-commit) + (.setAutoCommit connection (boolean (:auto-commit opts)))) + (when (contains? opts :read-only) + (.setReadOnly connection (boolean (:read-only opts)))) connection)) (extend-protocol p/Sourceable diff --git a/src/next/jdbc/prepare.clj b/src/next/jdbc/prepare.clj index c5f24df..57df43d 100644 --- a/src/next/jdbc/prepare.clj +++ b/src/next/jdbc/prepare.clj @@ -1,7 +1,11 @@ ;; copyright (c) 2018-2019 Sean Corfield, all rights reserved (ns next.jdbc.prepare - "" + "Mostly an implementation namespace for how PreparedStatement objects are + created by the next generation java.jdbc library. + + set-parameters is public and may be useful if you have a PreparedStatement + that you wish to reuse and (re)set the parameters on it." (:require [next.jdbc.protocols :as p]) (:import (java.sql Connection PreparedStatement @@ -11,7 +15,10 @@ (set! *warn-on-reflection* true) (defn set-parameters - "" + "Given a PreparedStatement and a vector of parameter values, update the + PreparedStatement with those parameters and return it. + + Currently uses .setObject with no possibility of an override." ^java.sql.PreparedStatement [^PreparedStatement ps params] (when (seq params) diff --git a/src/next/jdbc/protocols.clj b/src/next/jdbc/protocols.clj index 63bbaa9..7640937 100644 --- a/src/next/jdbc/protocols.clj +++ b/src/next/jdbc/protocols.clj @@ -1,7 +1,31 @@ ;; copyright (c) 2018-2019 Sean Corfield, all rights reserved (ns next.jdbc.protocols - "") + "This is the extensible core of the next generation java.jdbc library. + + get-datasource -- turn something into a javax.sql.DataSource; implementations + are provided for strings, hash maps (db-spec structures), and also a + DataSource (which just returns itself). + + get-connection -- create a new JDBC connection that should be closed when you + are finished with it; implementations are provided for DataSource and + Object, on the assumption that an Object can possibly be turned into a + DataSource. + + -execute -- given SQL and parameters, produce a 'reducible' that, when + reduced, executes the SQL and produces a ResultSet that can be processed; + implementations are provided for Connection, DataSource, + PreparedStatement, and Object (on the assumption that an Object can be + turned into a DataSource and therefore used to get a Connection). + + prepare -- given SQL and parameters, produce a PreparedStatement that can + be executed (by -execute above); implementation is provided for + Connection. + + -transact -- given a function (presumably containing SQL operations), + run the function inside a SQL transaction; implementations are provided + for Connection, DataSource, and Object (on the assumption that an Object + can be turned into a DataSource).") (set! *warn-on-reflection* true) diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index 7e204de..6f23d1f 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -1,7 +1,7 @@ ;; copyright (c) 2018-2019 Sean Corfield, all rights reserved (ns next.jdbc.result-set - "" + "An implementation of ResultSet handling functions." (:require [clojure.core.protocols :as core-p] [next.jdbc.prepare :as prepare] [next.jdbc.protocols :as p]) @@ -12,7 +12,11 @@ (set! *warn-on-reflection* true) (defn- get-column-names - "" + "Given a ResultSet, return a vector of columns names, each qualified by + the table from which it came. + + If :identifiers was specified, apply that to both the table qualifier + and the column name." [^ResultSet rs opts] (let [^ResultSetMetaData rsmeta (.getMetaData rs) idxs (range 1 (inc (.getColumnCount rsmeta)))] @@ -75,7 +79,13 @@ (range 1 (inc (count @cols))))))))) (defn- reduce-stmt - "" + "Execute the PreparedStatement, attempt to get either its ResultSet or + its generated keys (as a ResultSet), and reduce that using the supplied + function and initial value. + + If the statement yields neither a ResultSet nor generated keys, return + a hash map containing ::update-count and the number of rows updated, + with the supplied function and initial value applied." [^PreparedStatement stmt f init opts] (if-let [^ResultSet rs (if (.execute stmt) (.getResultSet stmt) @@ -130,19 +140,27 @@ (declare navize-row) (defn datafiable-row + "Given a connectable object, return a function that knows how to turn a row + into a datafiable object that can be 'nav'igated." [connectable opts] (fn [row] (into (with-meta {} {`core-p/datafy (navize-row connectable opts)}) row))) (defn execute! - "" + "Given a connectable object and SQL and parameters, execute it and reduce it + into a vector of processed hash maps (rows). + + By default, this will create datafiable rows but :row-fn can override that." [connectable sql-params opts] (into [] (map (or (:row-fn opts) (datafiable-row connectable opts))) (p/-execute connectable sql-params opts))) (defn execute-one! - "" + "Given a connectable object and SQL and parameters, execute it and return + just the first processed hash map (row). + + By default, this will create a datafiable row but :row-fn can override that." [connectable sql-params opts] (let [row-fn (or (:row-fn opts) (datafiable-row connectable opts))] (reduce (fn [_ row] @@ -161,7 +179,22 @@ [(keyword table) :id]))) (defn- navize-row - "" + "Given a connectable object, return a function that knows how to turn a row + into a navigable object. + + A :schema option can provide a map of qualified column names (:table/column) + to tuples that indicate which table they are a foreign key for, the name of + the key within that table, and (optionality) the cardinality of that + relationship (:many, :one). + + If no :schema item is provided for a column, the convention of id or +
_id is used, and the assumption is that such columns are foreign keys + in the
portion of their name, the key is called 'id', and the + cardinality is :one. + + Rows are looked up using 'execute!' or 'execute-one!' and the :entities + function, if provided, is applied to both the assumed table name and the + assumed foreign key column name." [connectable opts] (fn [row] (with-meta row diff --git a/src/next/jdbc/sql.clj b/src/next/jdbc/sql.clj index ac2f780..4cc9e47 100644 --- a/src/next/jdbc/sql.clj +++ b/src/next/jdbc/sql.clj @@ -6,11 +6,16 @@ This is intended to provide a minimal level of parity with clojure.java.jdbc (insert!, update!, delete!, etc). For anything more complex, use a library - like HoneySQL https://github.com/jkk/honeysql to generate SQL + parameters." + like HoneySQL https://github.com/jkk/honeysql to generate SQL + parameters. + + This is primarily intended to be an implementation detail." (:require [clojure.string :as str])) (defn by-keys - "" + "Given a hash map of column names and values and a clause type (:set, :where), + return a vector of a SQL clause and its parameters. + + Applies any :entities function supplied in the options." [key-map clause opts] (let [entity-fn (:entities opts identity) [where params] (reduce-kv (fn [[conds params] k v] @@ -25,17 +30,25 @@ params))) (defn as-keys - "" + "Given a hash map of column names and values, return a string of all the + column names. + + Applies any :entities function supplied in the options." [key-map opts] (str/join ", " (map (comp (:entities opts identity) name) (keys key-map)))) (defn as-? - "" + "Given a hash map of column names and values, or a vector of column names, + return a string of ? placeholders for them." [key-map opts] (str/join ", " (repeat (count key-map) "?"))) (defn for-query - "" + "Given a table name and either a hash map of column names and values or a + vector of SQL (where clause) and its parameters, return a vector of the + full SELECT SQL string and its parameters. + + Applies any :entities function supplied in the options." [table where-params opts] (let [entity-fn (:entities opts identity) where-params (if (map? where-params) @@ -47,7 +60,11 @@ (rest where-params)))) (defn for-delete - "" + "Given a table name and either a hash map of column names and values or a + vector of SQL (where clause) and its parameters, return a vector of the + full DELETE SQL string and its parameters. + + Applies any :entities function supplied in the options." [table where-params opts] (let [entity-fn (:entities opts identity) where-params (if (map? where-params) @@ -59,7 +76,12 @@ (rest where-params)))) (defn for-update - "" + "Given a table name, a vector of column names to set and their values, and + either a hash map of column names and values or a vector of SQL (where clause) + and its parameters, return a vector of the full UPDATE SQL string and its + parameters. + + Applies any :entities function supplied in the options." [table key-map where-params opts] (let [entity-fn (:entities opts identity) set-params (by-keys key-map :set opts) @@ -74,7 +96,10 @@ (into (rest where-params))))) (defn for-insert - "" + "Given a table name and a hash map of column names and their values, + return a vector of the full INSERT SQL string and its parameters. + + Applies any :entities function supplied in the options." [table key-map opts] (let [entity-fn (:entities opts identity) params (as-keys key-map opts) @@ -85,7 +110,11 @@ (vals key-map)))) (defn for-insert-multi - "" + "Given a table name, a vector of column names, and a vector of row values + (each row is a vector of its values), return a vector of the full INSERT + SQL string and its parameters. + + Applies any :entities function supplied in the options." [table cols rows opts] (assert (apply = (count cols) (map count rows))) (let [entity-fn (:entities opts identity) diff --git a/src/next/jdbc/transaction.clj b/src/next/jdbc/transaction.clj index fb30943..ca6cfc2 100644 --- a/src/next/jdbc/transaction.clj +++ b/src/next/jdbc/transaction.clj @@ -19,19 +19,19 @@ (defn- transact* "" [^Connection con f opts] - (let [{:keys [isolation read-only? rollback-only?]} opts + (let [{:keys [isolation read-only rollback-only]} opts old-autocommit (.getAutoCommit con) old-isolation (.getTransactionIsolation con) old-readonly (.isReadOnly con)] (io! (when isolation (.setTransactionIsolation con (isolation isolation-levels))) - (when read-only? + (when read-only (.setReadOnly con true)) (.setAutoCommit con false) (try (let [result (f con)] - (if rollback-only? + (if rollback-only (.rollback con) (.commit con)) result) @@ -58,7 +58,7 @@ (try (.setTransactionIsolation con old-isolation) (catch Exception _))) - (when read-only? + (when read-only (try (.setReadOnly con old-readonly) (catch Exception _)))))))) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 5a1c557..0fbfa2e 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -1,4 +1,5 @@ (ns next.jdbc-test + "Not exactly a test suite -- more a series of examples." (:require [clojure.test :refer [deftest is testing]] [next.jdbc :refer :all] [next.jdbc.result-set :as rs])) @@ -9,17 +10,15 @@ (comment (def db-spec {:dbtype "h2:mem" :dbname "perf"}) - (def db-spec {:dbtype "derby" :dbname "perf" :create true}) - (def db-spec {:dbtype "mysql" :dbname "worldsingles" :user "root" :password "visual"}) - (def con db-spec) - (def con (get-datasource db-spec)) - (get-connection con {}) + ;; these should be equivalent (def con (get-connection (get-datasource db-spec) {})) (def con (get-connection db-spec {})) (execute! con ["DROP TABLE fruit"]) ;; h2 (execute! con ["CREATE TABLE fruit (id int default 0, name varchar(32) primary key, appearance varchar(32), cost int, grade real)"]) + ;; either this... (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)"]) + ;; ...or this (insert-multi! con :fruit [:id :name :appearance :cost :grade] [[1 "Apple" "red" 59 87] [2,"Banana","yellow",29,92.2] @@ -30,7 +29,7 @@ (execute! con ["CREATE TABLE fruit (id int auto_increment, name varchar(32), appearance varchar(32), cost int, grade real, primary key (id))"]) (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)"] {:return-keys true}) - + ;; when you're done (.close con) (require '[criterium.core :refer [bench quick-bench]]) @@ -64,7 +63,9 @@ (execute! con ["select * from fruit where appearance = ?" "red"])) (execute! con ["select * from fruit"]) + ;; this is not quite equivalent (into [] (map (partial into {})) (reducible! con ["select * from fruit"])) + ;; but this is (equivalent to execute!) (into [] (map (rs/datafiable-row con {})) (reducible! con ["select * from fruit"])) ;; with a prepopulated prepared statement @@ -105,11 +106,10 @@ ["select * from fruit where appearance = ?" "red"] {:row-fn #(assoc % :test :value)}) - (with-transaction [t con {:rollback-only? true}] + (with-transaction [t con {:rollback-only true}] (execute! t ["INSERT INTO fruit (id,name,appearance,cost,grade) VALUES (5,'Pear','green',49,47)"]) (execute! t ["select * from fruit where name = ?" "Pear"])) (execute! con ["select * from fruit where name = ?" "Pear"]) - (delete! con :fruit {:id 1}) - (update! con :fruit {:appearance "Brown"} {:name "Banana"}) - (execute! con ["select * from membership"])) + (delete! con :fruit {:id 1}) + (update! con :fruit {:appearance "Brown"} {:name "Banana"}))