aboutsummaryrefslogtreecommitdiff
path: root/modules/sql
diff options
context:
space:
mode:
authorStuart Sierra <mail@stuartsierra.com>2010-08-07 16:41:53 -0400
committerStuart Sierra <mail@stuartsierra.com>2010-08-07 16:41:53 -0400
commita6a92b9b3d2bfd9a56e1e5e9cfba706d1aeeaae5 (patch)
treef1f3da9887dc2dc557df3282b0bcbd4d701ec593 /modules/sql
parente7930c85290f77815cdb00a60604feedfa2d0194 (diff)
Split all namespaces into sub-modules.
* Examples and tests have not been copied over. * Clojure test/compile phases are commented out in parent POM. * May require installing parent POM before full build.
Diffstat (limited to 'modules/sql')
-rw-r--r--modules/sql/pom.xml26
-rw-r--r--modules/sql/src/main/clojure/clojure/contrib/sql.clj201
-rw-r--r--modules/sql/src/main/clojure/clojure/contrib/sql/internal.clj194
3 files changed, 421 insertions, 0 deletions
diff --git a/modules/sql/pom.xml b/modules/sql/pom.xml
new file mode 100644
index 00000000..de94c477
--- /dev/null
+++ b/modules/sql/pom.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http//www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.clojure.contrib</groupId>
+ <artifactId>parent</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <relativePath>../parent</relativePath>
+ </parent>
+ <artifactId>sql</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>org.clojure.contrib</groupId>
+ <artifactId>def</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.clojure.contrib</groupId>
+ <artifactId>string</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+</project> \ No newline at end of file
diff --git a/modules/sql/src/main/clojure/clojure/contrib/sql.clj b/modules/sql/src/main/clojure/clojure/contrib/sql.clj
new file mode 100644
index 00000000..c6946078
--- /dev/null
+++ b/modules/sql/src/main/clojure/clojure/contrib/sql.clj
@@ -0,0 +1,201 @@
+;; Copyright (c) Stephen C. Gilardi. All rights reserved. The use and
+;; distribution terms for this software are covered by the Eclipse Public
+;; License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can
+;; be found in the file epl-v10.html at the root of this distribution. By
+;; using this software in any fashion, you are agreeing to be bound by the
+;; terms of this license. You must not remove this notice, or any other,
+;; from this software.
+;;
+;; sql.clj
+;;
+;; A Clojure interface to sql databases via jdbc
+;;
+;; See clojure.contrib.sql.test for an example
+;;
+;; scgilardi (gmail)
+;; Created 2 April 2008
+
+(ns
+ ^{:author "Stephen C. Gilardi",
+ :doc "A Clojure interface to sql databases via jdbc."
+ :see-also [["http://github.com/richhickey/clojure-contrib/blob/master/src/test/clojure/clojure/contrib/test_sql.clj"
+ "Example code"]]}
+ clojure.contrib.sql
+ (:use (clojure.contrib
+ [def :only (defalias)]
+ [string :only (as-str)])
+ clojure.contrib.sql.internal))
+
+(defalias find-connection find-connection*)
+(defalias connection connection*)
+
+(defmacro with-connection
+ "Evaluates body in the context of a new connection to a database then
+ closes the connection. db-spec is a map containing values for one of the
+ following parameter sets:
+
+ Factory:
+ :factory (required) a function of one argument, a map of params
+ (others) (optional) passed to the factory function in a map
+
+ DriverManager:
+ :classname (required) a String, the jdbc driver class name
+ :subprotocol (required) a String, the jdbc subprotocol
+ :subname (required) a String, the jdbc subname
+ (others) (optional) passed to the driver as properties.
+
+ DataSource:
+ :datasource (required) a javax.sql.DataSource
+ :username (optional) a String
+ :password (optional) a String, required if :username is supplied
+
+ JNDI:
+ :name (required) a String or javax.naming.Name
+ :environment (optional) a java.util.Map"
+ [db-spec & body]
+ `(with-connection* ~db-spec (fn [] ~@body)))
+
+(defmacro transaction
+ "Evaluates body as a transaction on the open database connection. Any
+ nested transactions are absorbed into the outermost transaction. By
+ default, all database updates are committed together as a group after
+ evaluating the outermost body, or rolled back on any uncaught
+ exception. If set-rollback-only is called within scope of the outermost
+ transaction, the entire transaction will be rolled back rather than
+ committed when complete."
+ [& body]
+ `(transaction* (fn [] ~@body)))
+
+(defn set-rollback-only
+ "Marks the outermost transaction such that it will rollback rather than
+ commit when complete"
+ []
+ (rollback true))
+
+(defn is-rollback-only
+ "Returns true if the outermost transaction will rollback rather than
+ commit when complete"
+ []
+ (rollback))
+
+(defn do-commands
+ "Executes SQL commands on the open database connection."
+ [& commands]
+ (with-open [stmt (.createStatement (connection))]
+ (doseq [cmd commands]
+ (.addBatch stmt cmd))
+ (transaction
+ (seq (.executeBatch stmt)))))
+
+(defn do-prepared
+ "Executes an (optionally parameterized) SQL prepared statement on the
+ open database connection. Each param-group is a seq of values for all of
+ the parameters."
+ [sql & param-groups]
+ (with-open [stmt (.prepareStatement (connection) sql)]
+ (doseq [param-group param-groups]
+ (doseq [[index value] (map vector (iterate inc 1) param-group)]
+ (.setObject stmt index value))
+ (.addBatch stmt))
+ (transaction
+ (seq (.executeBatch stmt)))))
+
+(defn create-table
+ "Creates a table on the open database connection given a table name and
+ specs. Each spec is either a column spec: a vector containing a column
+ name and optionally a type and other constraints, or a table-level
+ constraint: a vector containing words that express the constraint. All
+ words used to describe the table may be supplied as strings or keywords."
+ [name & specs]
+ (do-commands
+ (format "CREATE TABLE %s (%s)"
+ (as-str name)
+ (apply str
+ (map as-str
+ (apply concat
+ (interpose [", "]
+ (map (partial interpose " ") specs))))))))
+
+(defn drop-table
+ "Drops a table on the open database connection given its name, a string
+ or keyword"
+ [name]
+ (do-commands
+ (format "DROP TABLE %s" (as-str name))))
+
+(defn insert-values
+ "Inserts rows into a table with values for specified columns only.
+ column-names is a vector of strings or keywords identifying columns. Each
+ value-group is a vector containing a values for each column in
+ order. When inserting complete rows (all columns), consider using
+ insert-rows instead."
+ [table column-names & value-groups]
+ (let [column-strs (map as-str column-names)
+ n (count (first value-groups))
+ template (apply str (interpose "," (replicate n "?")))
+ columns (if (seq column-names)
+ (format "(%s)" (apply str (interpose "," column-strs)))
+ "")]
+ (apply do-prepared
+ (format "INSERT INTO %s %s VALUES (%s)"
+ (as-str table) columns template)
+ value-groups)))
+
+(defn insert-rows
+ "Inserts complete rows into a table. Each row is a vector of values for
+ each of the table's columns in order."
+ [table & rows]
+ (apply insert-values table nil rows))
+
+(defn insert-records
+ "Inserts records into a table. records are maps from strings or
+ keywords (identifying columns) to values."
+ [table & records]
+ (doseq [record records]
+ (insert-values table (keys record) (vals record))))
+
+(defn delete-rows
+ "Deletes rows from a table. where-params is a vector containing a string
+ providing the (optionally parameterized) selection criteria followed by
+ values for any parameters."
+ [table where-params]
+ (let [[where & params] where-params]
+ (do-prepared
+ (format "DELETE FROM %s WHERE %s"
+ (as-str table) where)
+ params)))
+
+(defn update-values
+ "Updates values on selected rows in a table. where-params is a vector
+ containing a string providing the (optionally parameterized) selection
+ criteria followed by values for any parameters. record is a map from
+ strings or keywords (identifying columns) to updated values."
+ [table where-params record]
+ (let [[where & params] where-params
+ column-strs (map as-str (keys record))
+ columns (apply str (concat (interpose "=?, " column-strs) "=?"))]
+ (do-prepared
+ (format "UPDATE %s SET %s WHERE %s"
+ (as-str table) columns where)
+ (concat (vals record) params))))
+
+(defn update-or-insert-values
+ "Updates values on selected rows in a table, or inserts a new row when no
+ existing row matches the selection criteria. where-params is a vector
+ containing a string providing the (optionally parameterized) selection
+ criteria followed by values for any parameters. record is a map from
+ strings or keywords (identifying columns) to updated values."
+ [table where-params record]
+ (transaction
+ (let [result (update-values table where-params record)]
+ (if (zero? (first result))
+ (insert-values table (keys record) (vals record))
+ result))))
+
+(defmacro with-query-results
+ "Executes a query, then evaluates body with results bound to a seq of the
+ results. sql-params is a vector containing a string providing
+ the (optionally parameterized) SQL query followed by values for any
+ parameters."
+ [results sql-params & body]
+ `(with-query-results* ~sql-params (fn [~results] ~@body)))
diff --git a/modules/sql/src/main/clojure/clojure/contrib/sql/internal.clj b/modules/sql/src/main/clojure/clojure/contrib/sql/internal.clj
new file mode 100644
index 00000000..59a05205
--- /dev/null
+++ b/modules/sql/src/main/clojure/clojure/contrib/sql/internal.clj
@@ -0,0 +1,194 @@
+;; Copyright (c) Stephen C. Gilardi. All rights reserved. The use and
+;; distribution terms for this software are covered by the Eclipse Public
+;; License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can
+;; be found in the file epl-v10.html at the root of this distribution. By
+;; using this software in any fashion, you are agreeing to be bound by the
+;; terms of this license. You must not remove this notice, or any other,
+;; from this software.
+;;
+;; internal definitions for clojure.contrib.sql
+;;
+;; scgilardi (gmail)
+;; Created 3 October 2008
+
+(ns clojure.contrib.sql.internal
+ (:use
+ (clojure.contrib
+ [except :only (throwf throw-arg)]
+ [properties :only (as-properties)]
+ [seq :only (indexed)]))
+ (:import
+ (clojure.lang RT)
+ (java.sql BatchUpdateException DriverManager SQLException Statement)
+ (java.util Hashtable Map)
+ (javax.naming InitialContext Name)
+ (javax.sql DataSource)))
+
+(def *db* {:connection nil :level 0})
+
+(def special-counts
+ {Statement/EXECUTE_FAILED "EXECUTE_FAILED"
+ Statement/SUCCESS_NO_INFO "SUCCESS_NO_INFO"})
+
+(defn find-connection*
+ "Returns the current database connection (or nil if there is none)"
+ []
+ (:connection *db*))
+
+(defn connection*
+ "Returns the current database connection (or throws if there is none)"
+ []
+ (or (find-connection*)
+ (throwf "no current database connection")))
+
+(defn rollback
+ "Accessor for the rollback flag on the current connection"
+ ([]
+ (deref (:rollback *db*)))
+ ([val]
+ (swap! (:rollback *db*) (fn [_] val))))
+
+(defn get-connection
+ "Creates a connection to a database. db-spec is a map containing values
+ for one of the following parameter sets:
+
+ Factory:
+ :factory (required) a function of one argument, a map of params
+ (others) (optional) passed to the factory function in a map
+
+ DriverManager:
+ :classname (required) a String, the jdbc driver class name
+ :subprotocol (required) a String, the jdbc subprotocol
+ :subname (required) a String, the jdbc subname
+ (others) (optional) passed to the driver as properties.
+
+ DataSource:
+ :datasource (required) a javax.sql.DataSource
+ :username (optional) a String
+ :password (optional) a String, required if :username is supplied
+
+ JNDI:
+ :name (required) a String or javax.naming.Name
+ :environment (optional) a java.util.Map"
+ [{:keys [factory
+ classname subprotocol subname
+ datasource username password
+ name environment]
+ :as db-spec}]
+ (cond
+ factory
+ (factory (dissoc db-spec :factory))
+ (and classname subprotocol subname)
+ (let [url (format "jdbc:%s:%s" subprotocol subname)
+ etc (dissoc db-spec :classname :subprotocol :subname)]
+ (RT/loadClassForName classname)
+ (DriverManager/getConnection url (as-properties etc)))
+ (and datasource username password)
+ (.getConnection datasource username password)
+ datasource
+ (.getConnection datasource)
+ name
+ (let [env (and environment (Hashtable. environment))
+ context (InitialContext. env)
+ datasource (.lookup context name)]
+ (.getConnection datasource))
+ :else
+ (throw-arg "db-spec %s is missing a required parameter" db-spec)))
+
+(defn with-connection*
+ "Evaluates func in the context of a new connection to a database then
+ closes the connection."
+ [db-spec func]
+ (with-open [con (get-connection db-spec)]
+ (binding [*db* (assoc *db*
+ :connection con :level 0 :rollback (atom false))]
+ (func))))
+
+(defn print-sql-exception
+ "Prints the contents of an SQLException to stream"
+ [stream exception]
+ (.println
+ stream
+ (format (str "%s:" \newline
+ " Message: %s" \newline
+ " SQLState: %s" \newline
+ " Error Code: %d")
+ (.getSimpleName (class exception))
+ (.getMessage exception)
+ (.getSQLState exception)
+ (.getErrorCode exception))))
+
+(defn print-sql-exception-chain
+ "Prints a chain of SQLExceptions to stream"
+ [stream exception]
+ (loop [e exception]
+ (when e
+ (print-sql-exception stream e)
+ (recur (.getNextException e)))))
+
+(defn print-update-counts
+ "Prints the update counts from a BatchUpdateException to stream"
+ [stream exception]
+ (.println stream "Update counts:")
+ (doseq [[index count] (indexed (.getUpdateCounts exception))]
+ (.println stream (format " Statement %d: %s"
+ index
+ (get special-counts count count)))))
+
+(defn throw-rollback
+ "Sets rollback and throws a wrapped exception"
+ [e]
+ (rollback true)
+ (throwf e "transaction rolled back: %s" (.getMessage e)))
+
+(defn transaction*
+ "Evaluates func as a transaction on the open database connection. Any
+ nested transactions are absorbed into the outermost transaction. By
+ default, all database updates are committed together as a group after
+ evaluating the outermost body, or rolled back on any uncaught
+ exception. If rollback is set within scope of the outermost transaction,
+ the entire transaction will be rolled back rather than committed when
+ complete."
+ [func]
+ (binding [*db* (update-in *db* [:level] inc)]
+ (if (= (:level *db*) 1)
+ (let [con (connection*)
+ auto-commit (.getAutoCommit con)]
+ (io!
+ (.setAutoCommit con false)
+ (try
+ (func)
+ (catch BatchUpdateException e
+ (print-update-counts *err* e)
+ (print-sql-exception-chain *err* e)
+ (throw-rollback e))
+ (catch SQLException e
+ (print-sql-exception-chain *err* e)
+ (throw-rollback e))
+ (catch Exception e
+ (throw-rollback e))
+ (finally
+ (if (rollback)
+ (.rollback con)
+ (.commit con))
+ (rollback false)
+ (.setAutoCommit con auto-commit)))))
+ (func))))
+
+(defn with-query-results*
+ "Executes a query, then evaluates func passing in a seq of the results as
+ an argument. The first argument is a vector containing the (optionally
+ parameterized) sql query string followed by values for any parameters."
+ [[sql & params :as sql-params] func]
+ (when-not (vector? sql-params)
+ (throw-arg "\"%s\" expected %s %s, found %s %s"
+ "sql-params"
+ "vector"
+ "[sql param*]"
+ (.getName (class sql-params))
+ (pr-str sql-params)))
+ (with-open [stmt (.prepareStatement (connection*) sql)]
+ (doseq [[index value] (map vector (iterate inc 1) params)]
+ (.setObject stmt index value))
+ (with-open [rset (.executeQuery stmt)]
+ (func (resultset-seq rset)))))