lobos

1.0.0-SNAPSHOT


A library to create and manipulate SQL database schemas.

dependencies

org.clojure/clojure
1.3.0
org.clojure/java.jdbc
0.0.7
org.clojure/tools.macro
0.1.1

dev dependencies

lein-clojars
0.7.0
lein-marginalia
0.7.0-SNAPSHOT
cljss
0.1.1
hiccup
0.3.7
com.h2database/h2
1.3.160



(this space intentionally left almost blank)
 

The core namespace provide the basic interface to interact with a database. It contains a set of functions and actions (a special kind of functions acting on abstract schemas or their elements) used to manipulate database schemas in an implementation agnostic way.

To find out more about Lobos, check out:

(ns lobos.core
  {:author "Nicolas Buduroi"}
  (:refer-clojure :exclude [alter defonce drop])
  (:require (lobos [compiler :as compiler]
                   [connectivity :as conn]
                   [migration :as mig]
                   [schema :as schema]))
  (:use (clojure.tools [macro :only [name-with-attributes]])
        (clojure [pprint :only [pprint]])
        lobos.internal
        lobos.utils))

Helpers

(defmacro without-migration [& body]
  `(binding [mig/*record* nil]
     ~@body))

Debugging Interface

Set the current debugging level. The level argument can be one of :schema, :ast or :sql. Currently only :sql is supported for all actions. e.g.:

user> (set-debug-level :sql)
(defn set-debug-level
  [level]
  (swap! debug-level (constantly level)))

Prints useful information on the given combination protocol method and schema (or elements). For the available methods, see the lobos.schema namespace. For methods taking extra argument use the optional args argument, which takes a sequence. You can also supply a connection-info and level argument. Use the default connection and the :sql level when not specified. For debugging purpose. e.g.:

user> (debug build-create-statement
             (sample-schema))
(defn debug
  [method object-or-fn & [args connection-info level]]
  (let [level (or level @debug-level :sql)
        object (if (fn? object-or-fn)
                 (object-or-fn)
                 object-or-fn)
        db-spec (conn/get-db-spec connection-info)
        ast (when-not (= :schema level)
              (apply method object (conj args db-spec)))]
    (case level
      :sql (println (compiler/compile ast))
      :ast (do (println (type ast))
               (pprint ast))
      :schema (do (println (type object))
                  (pprint object)))))

Lobos Action

Action Macro

Defines an action applicable to an optional abstract schema or database connection. Actions are simply a special kind of functions. They will have an augmented argument list, which is the given one prepended by the optional cnx-or-schema argument.

All actions must return a built statement (or list of statements) using one of the protocol method available.

The defined actions will have access to two extra local variables. The schema variable will contain the given schema if cnx-or-schema is one else it be nil. The db-spec argument will contain the db-spec map found with the given connection or in the given schema. *For internal use*.

(defmacro defaction
  {:arglists '([name doc-string? attr-map? [params*] & body])}
  [name & args]
  (let [params (seq (first (filter vector? args)))
        name* (symbol (str name \*))
        [name args] (name-with-attributes name args)
        [params* & body] args]
    `(do
       (defn ~name* [self# & params#]
         (let [[~'db-spec ~'schema ~params*]
               (optional-cnx-or-schema params#)]
           (execute
            (do ~@body)
            ~'db-spec)
           (mig/record self#)))
       (defmacro ~name [~'& args#]
         `(~~name* (quote ~~'&form) ~@args#))
       (.setMeta #'~name
                 (merge (.meta #'~name)
                        {:arglists '(~(vec (conj params 'cnx-or-schema?)))})))))

Actions

Builds a create statement with the given schema element and execute it. See the lobos.schema namespace for more details on schema elements definition. e.g.:

user> (create (table :foo (integer :a)))
(defaction create
  [element]
  (schema/build-create-statement (or element schema) db-spec))

Builds an alter statement with the given schema element and execute it. There's four types of alter actions: :add, :drop, :modify and :rename. See the lobos.schema namespace for more details on schema elements definition. e.g.:

user> (alter :add (table :foo (integer :a)))
user> (alter :modify (table :foo (column :a [:default 0])))
user> (alter :rename (table :foo (column :a :to :b)))
(defaction alter
  [action element]
  (schema/build-alter-statement element action db-spec))

Builds a drop statement with the given schema element and execute it. It can take an optional behavior argument, when :cascade is specified drops all elements relying on the one being dropped. e.g.:

user> (drop (table :foo) :cascade)
(defaction drop
  [element & [behavior]]
  (schema/build-drop-statement element behavior db-spec))

Execute the given statements as an action.

(defaction exec
  [& statements]
  statements)

Lobos Migration

Migration Command Macro

(defmacro defcommand
  [name & args]
  (let [params (seq (first (filter vector? args)))
        [name args] (name-with-attributes name args)
        [params* & body] args]
    `(do
       (defn ~name [& params#]
         (let [[~'db-spec ~'sname ~params*]
               (optional-cnx-and-sname params#)]
           (mig/create-migrations-table ~'db-spec ~'sname)
           (do ~@body)))
       (.setMeta #'~name
                 (merge (.meta #'~name)
                        {:arglists
                         '(~(vec (conj params
                                       'sname?
                                       'cnx-or-schema?)))})))))

Migration Commands

(defn print-stash []
  (when (.exists mig/*stash-file*)
    (print (slurp mig/*stash-file*))))
(defcommand print-done []
  (doseq [name (mig/query-migrations-table db-spec sname)]
    (println name)))
(defcommand print-pending []
  (doseq [name (mig/pending-migrations db-spec sname)]
    (println name)))
(defcommand migrate [& names]
  (let [names (if (empty? names)
                (mig/pending-migrations db-spec sname)
                names)]
    (mig/do-migrations db-spec sname :up names)))
(defcommand rollback [& args]
  (let [names (cond
               (empty? args)
               [(last (mig/query-migrations-table db-spec sname))]
               (= 1 (count args))
               (let [arg (first args)
                     migs (mig/query-migrations-table db-spec sname)]
                 (cond
                  (integer? arg) (take arg migs)
                  (= arg :all) migs
                  :else args))
               :else args)]
    (mig/do-migrations db-spec sname :down names)))
(defcommand reset [& args]
  (apply rollback args)
  (migrate))
(defcommand generate-migration [name & [msg]]
  (let [name (symbol (if (string? name)
                       name
                       (clojure.core/name name)))]
    (when-not name
      (throw (IllegalArgumentException.
              "Migration must be named.")))
    (when ((set (mig/list-migrations-names)) (str name))
      (throw (IllegalArgumentException.
              "Migration name is already taken.")))
    (mig/generate-migration* db-spec sname name msg
                             (mig/read-stash-file))
    (mig/clear-stash-file)))
 

This namespace include the abstract schema data-structures, an handful of helpers to create them and the protocol to build the into an abstract syntax tree of implementation agnostic SQL statements. Abstract schema data-structures can be divided in two categories.

First, schema elements which include the schema and the table element definitions. Those can be created or dropped, also the table element can be altered.

Then there's the table elements that serves to define tables. There's the completly abstract column and constraint elements, which are only meant to be used directly with the alter action. Each of them have more specialized function to help you define tables, like the unique, primary-key, foreign-key and check constraints definitons and the typed data definitions.

(ns lobos.schema
  (:refer-clojure :exclude [defonce replace
                            bigint boolean char double float time])
  (:require (lobos [ast :as ast]))
  (:use (clojure [walk   :only [postwalk]]
                 [set    :only [union]]
                 [string :only [replace]])
        lobos.utils))
(ast/import-all)

Protocols

The Alterable protocol add the possibility of building alter statements from an object implementing it. For internal use.

(defprotocol Alterable
  (build-alter-statement [this action db-spec]))

The Buildable protocol is currently used only by table elements. For internal use.

(defprotocol Buildable
  (build-definition [this db-spec]))

The Creatable protocol add the possibility of building create statements from an object implementing it. For internal use.

(defprotocol Creatable
  (build-create-statement [this db-spec]))

The Dropable protocol add the possibility of building drop statements from an object implementing it. For internal use.

(defprotocol Dropable
  (build-drop-statement [this behavior db-spec]))

Common Exception

Throws an IllegalArgumentException when the given name is nil with a default message using the given type of elements.

(defn name-required
  [name etype]
  (when-not name
    (throw (IllegalArgumentException.
            (format "A % definition needs at least a name."
                    etype)))))

Definition Predicate

Returns true if the given object is an abstract schema element definition. For internal use.

(defn definition?
  [o]
  (isa? (type o) ::definition))

Expression Definitions

A set of symbol representing SQL infix operators.

(def 
  sql-infix-operators
  '#{;; math operators
     + - * /
     ;; boolean operators
     < > <= >= = != or and in like})

A set of symbol representing SQL prefix operators.

(def 
  sql-prefix-operators
  '#{not})

A set of symbol representing SQL functions.

(def 
  sql-functions
  '#{;; string functions
     length lower position replace str subs trim upper
     ;; numeric functions
     abs ceil floor mod
     ;; datetime functions
     extract now current_date current_time current_timestamp})
(def sql-symbols
  (union sql-infix-operators
         sql-prefix-operators
         sql-functions))
(defrecord Expression [value]
  Buildable
  (build-definition [this db-spec]
    (postwalk
     #(do
        (cond (vector? %)
              (let [[f & n] %]
                (if (keyword? f)
                  (condp contains? (-> f name symbol)
                    sql-infix-operators
                    (OperatorExpression. db-spec f (first n) (next n))
                    sql-prefix-operators
                    (OperatorExpression. db-spec f nil n)
                    sql-functions
                    (FunctionExpression. db-spec f n))
                  %))
              (and (keyword? %)
                   (not (contains? sql-symbols (-> % name symbol))))
              (IdentifierExpression. db-spec % nil)
              (not (keyword? %))
              (ScalarExpression. db-spec %)
              :else %))
     value)))
(defmacro expression [form]
  `(Expression.
    ~(postwalk
      #(if (and (seq? %)
                (sql-symbols (first %)))
         (apply vector
                (keyword (first %))
                (rest %))
         %)
      form)))

Index Definitions

(defrecord Index [iname tname columns options]
  Creatable Dropable
  (build-create-statement [this db-spec]
    (CreateIndexStatement. db-spec iname tname columns options))
  (build-drop-statement [this behavior db-spec]
    (DropStatement. db-spec :index iname nil {:tname tname})))
(defn index
  ([table columns] (index table nil columns))
  ([table name columns & options]
     (let [tname (if (keyword? table) table (:name table))
           cnames (map #(if (keyword? %) % (first %)) columns)
           name (or name (make-index-name tname :index cnames))]
       (if (keyword? table)
         (Index. name tname columns options)
         (update-in table [:indexes] conj
                    [name (Index. name tname columns options)])))))

Constraint Definitions

Constraint records are only used to define unspecified constraint. These type of constraints are useful with the alter drop action. They can be be construct using the constraint function. For internal use.

(defrecord Constraint [cname]
  Buildable
  (build-definition [this db-spec]
    (ConstraintDefinition. db-spec cname)))

Constructs an unspecified abstract constraint definition and add it to the given table. To be used with alter action while dropping a constraint.

(defn constraint
  [table name]
  (update-in table [:constraints] conj
             [name (Constraint. name)]))

UniqueConstraint records can be constructed using the primary-key or unique functions. It can represent either a unique or primary key constraint. For internal use.

(defrecord UniqueConstraint [cname ctype columns]
  Buildable
  (build-definition [this db-spec]
    (UniqueConstraintDefinition.
     db-spec
     cname
     ctype
     columns)))

Constructs an abstract unique (or primary-key depending on the given type) constraint definition and add it to the given table.

(defn unique-constraint
  [table name type columns]
  (let [name (or name (make-index-name table type columns))]
    (update-in table [:constraints] conj
               [name (UniqueConstraint. name type (vec columns))])))

Constructs an abstract primary key constraint definition and add it to the given table. If the name isn't specified, this constraint will be named using its specification.

(defn primary-key
  ([table columns] (primary-key table nil columns))
  ([table name columns]
     (unique-constraint table name :primary-key columns)))

Constructs an abstract unique constraint definition and add it to the given table. If the name isn't specified, this constraint will be named using its specification.

(defn unique
  ([table columns] (unique table nil columns))
  ([table name columns]
     (unique-constraint table name :unique columns)))

ForeignKeyConstraint record can be constructed using the foreign-key function. For internal use.

(defrecord ForeignKeyConstraint
  [cname columns parent-table parent-columns match triggered-actions]
  Buildable
  (build-definition [this db-spec]
    (ForeignKeyConstraintDefinition.
     db-spec
     cname
     columns
     parent-table
     parent-columns
     match
     triggered-actions)))

Constructs an abstract foreign key constraint definition and add it to the given table. The columns and parent-table arguments must be specified. If no parent-columns are specified, the columns will be used in its place.

The match optional argument can be one of :full, :partial or :simple, but note that this isn't supported by most databases.

You can specify triggered-actions with pairs of keyword, the first of the pairs must be one of :on-delete or :on-update, while the second one can be one of :cascade, :set-null, :restrict, :set-default or :no-action. The actions keywords are directly translated to SQL keywords, so you can specify custom ones if the database you're using provide more.

If the name isn't specified, this constraint will be named using its specification.

(defn foreign-key
  {:arglists '([table name? columns parent-table parent-columns? match?
                & triggered-actions])}
  [table & args]
  (let [[constraint-name args] (optional keyword? args)
        columns                (first args)
        parent-table           (second args)
        args                   (nnext args)
        [parent-columns args]  (optional vector? args)
        parent-columns         (or parent-columns columns)
        [match args]           (optional #{:full :partial :simple} args)
        triggered-actions      (apply hash-map args)
        constraint-name        (or constraint-name
                                   (make-index-name table "fkey" columns))]
    (update-in table [:constraints] conj
               [constraint-name
                (ForeignKeyConstraint. constraint-name
                                       columns
                                       parent-table
                                       parent-columns
                                       match
                                       triggered-actions)])))

CheckConstraint record can be constructed using the check macro or the chech* function. For internal use.

(defrecord CheckConstraint
  [cname condition]
  Buildable
  (build-definition [this db-spec]
    (CheckConstraintDefinition.
     db-spec
     cname
     (build-definition condition db-spec))))

Constructs an abstract check constraint definition and add it to the given table. The constraint-name argument is mandatory.

(defn check*
  [table constraint-name condition]
  (name-required constraint-name "check constraint")
  (update-in table [:constraints] conj
             [constraint-name
              (CheckConstraint. constraint-name
                                condition)]))

Constructs an abstract check constraint definition and add it to the given table.

(defmacro check
  [table constraint-name condition]
  `(check*
    ~table
    ~constraint-name
    (expression ~condition)))

Data-type Definition

DataType records can be constructed using the data-type function. For internal use.

(defrecord DataType [dtype args options])

Constructs an abstract data-type definition using the given keyword dtype. Can also take an options list of arguments (args) and options.

(defn data-type
  [dtype & [args options]]
  (DataType. dtype (vec args)
             (merge {:time-zone nil
                     :collate nil
                     :encoding nil}
                     options)))

Column Definition

If the given default value, it will be replaced by the standard function returning the current time, date or timestamp depending on the specified data-type. For internal use.

(defn datetime-now-alias
  [dtype default]
  (let [value (:value default)]
    (if (= value [:now])
      (Expression.
       (or ({:date [:current_date]
             :time [:current_time]
             :timestamp [:current_timestamp]} dtype) value))
      default)))

Column records can be constructed using the column function or the more specific typed column functions. The build-definition method will create the appropriate DataTypeClause for data-type definitions and *Expression AST for default values. For internal use.

(defrecord Column [cname data-type default auto-inc not-null others]
  Buildable
  (build-definition [this db-spec]
    (let [{:keys [dtype args options]} data-type]
      (ColumnDefinition.
       db-spec
       cname
       (DataTypeClause. db-spec dtype args options)
       (if (= default :drop)
         :drop
         (when default
           (build-definition
            (datetime-now-alias dtype default)
            db-spec)))
       (when auto-inc (AutoIncClause. db-spec))
       not-null
       others))))
(defmacro default [form]
  `[:default (expression ~form)])

Constructs an abstract column definition. It'll parse the column specific options. See the column function for more details. For internal use.

(defn column*
  [column-name data-type options]
  (let [{:keys [default encoding collate]}
        (into {} (filter vector? options))
        data-type (when data-type
                    (update-in data-type [:options]
                               (partial merge-with #(or %1 %2))
                               {:encoding encoding
                                :collate collate
                                :time-zone ((set options) :time-zone)}))
        others     (vec (filter string? options))
        option-set (set options)
        not-null   (clojure.core/boolean (:not-null option-set))
        auto-inc   (clojure.core/boolean (:auto-inc option-set))]
    (Column. column-name
             data-type
             default
             auto-inc
             not-null
             others)))

Constructs an abstract column definition and add it to the given table. Also creates and add the appropriate column constraints when these are specified as options. Here's a list of available options:

  • :unique which construct an unique constraint on that column
  • :primary-key which make the current column the primary key
  • [:refer tname & options] which add a foreign key constraint to the specified table. The options are the same as the foreign-key function with the expection that you can specify only one parent column.
  • :not-null prevents this column from being null
  • :auto-inc (for integers types) which makes it auto-populated with incremented integers
  • [:encoding enc] (for character types) determines which encoding to use if supported by the database. Also see the natianal character types.
  • [:collate type] (for character types) determines how equality is handled
  • :time-zone (for time types) determines if the type includes a time-zone

    It also can be used in alter modify and rename actions. In that case, if data-type is :to, it acts as a column rename clause and if data-type is :drop-default, it acts as a column drop default clause.

(defn column
  {:arglists '([table column-name data-type? & options])}
  [table column-name & options]
  (name-required column-name "column")
  (let [[data-type options] (optional #(instance? DataType %) options)
        reference? #(and (vector? %) (= (first %) :refer))
        [ptable pcol & others] (->> options (filter reference?) first next)
        options (filter (comp not reference?) options)
        option-set (when (seq? options) (set options))
        add-constraint #(cond (:primary-key option-set)
                              (primary-key % [column-name])
                              (:unique option-set)
                              (unique % [column-name])
                              ptable
                              (apply foreign-key % [column-name] ptable
                                     (when pcol [pcol]) others)
                              :else %)]
    (add-constraint
     (update-in table [:columns] conj
                [column-name
                 (case (first options)
                   :to (Column. column-name nil nil false false (second options))
                   :drop-default (Column. column-name nil :drop false false [])
                   (column* column-name data-type options))]))))

Typed Column Definitions

Instead of calling the column option directly and including the data-type argument, you can use typed column definition in which case each types have their own functions.

Typed Column Helpers

Helper for macros that create typed columns definitions. It takes a sequence of names and define a function for each of them, a vector of arguments for those functions, dargs must specify how to handle these arguement and options must specify the generic column options. The optional docs arguement is appended to the generic docstring. For internal use.

(defn def-typed-columns*
  [names args dargs options & [docs]]
  `(do
     ~@(for [n names]
         `(defn ~n
            ~(format (str "Constructs an abstract %s column definition and"
                          " add it to the given table." docs)
                     (name n))
            ~args
            (let [dargs# ~dargs
                  options# ~options]
              (apply column
                     ~'table
                     ~'column-name
                     (data-type ~(keyword n) dargs#)
                     options#))))))

Defines typed columns for simple data-types taking no arguments. For internal use.

(defmacro def-simple-typed-columns
  [& names]
  (def-typed-columns*
    names
    '[table column-name & options]
    '[]
    'options))

Defines numeric-like typed columns. These typed column funcitons can take an optional precision and scale argument. For internal use.

(defmacro def-numeric-like-typed-columns
  [& names]
  (def-typed-columns*
    names
    '[table column-name & [precision scale & options]]
    '(-> []
         (conj-when (integer? precision) precision)
         (conj-when (integer? scale) scale))
    '(-> options
         (conj-when (not (integer? precision)) precision)
         (conj-when (not (integer? scale)) scale))
    " Takes an optional `precision` and `scale` arguments."))

Defines typed columns with optional precision. Used by float and time data-types. For internal use.

(defmacro def-optional-precision-typed-columns
  [& names]
  (def-typed-columns*
    names
    '[table column-name & [precision & options]]
    '(conj-when [] (integer? precision) precision)
    '(conj-when options (not (integer? precision)) precision)
    " Takes an optional `precision` argument."))

Defines optionally length-bounded typed columns. Used by binary and character types. For internal use.

(defmacro def-optional-length-typed-columns
  [& names]
  (def-typed-columns*
    names
    '[table column-name & [length & options]]
    '(conj-when [] (integer? length) length)
    '(conj-when options (not (integer? length)) length)
    " Takes an optional `length` argument."))

Defines length-bounded typed columns. Used by variable binary and character types. For internal use.

(defmacro def-length-bounded-typed-columns
  [& names]
  (def-typed-columns*
    names
    '[table column-name length & options]
    '(conj-when [] (integer? length) length)
    '(conj-when options (not (integer? length)) length)
    " The `length` arguemnt is mandatory."))

Numeric Types

Constructs an abstract smallint column definition and add it to the given table.

(def-simple-typed-columns
  smallint
  integer
  bigint)

Constructs an abstract numeric column definition and add it to the given table. Takes an optional precision and scale arguments.

(def-numeric-like-typed-columns
  numeric
  decimal)

Constructs an abstract real column definition and add it to the given table.

(def-simple-typed-columns
  real
  double-precision)
(def double double-precision)

Constructs an abstract float column definition and add it to the given table. Takes an optional precision argument.

(def-optional-precision-typed-columns
  float)

Character Types

Constructs an abstract char column definition and add it to the given table. Takes an optional length argument.

(def-optional-length-typed-columns
  char
  nchar
  clob
  nclob)
(def text clob)
(def ntext nclob)

Constructs an abstract varchar column definition and add it to the given table. The length arguemnt is mandatory.

(def-length-bounded-typed-columns  
  varchar
  nvarchar)

Binary Types

Constructs an abstract binary column definition and add it to the given table. Takes an optional length argument.

(def-optional-length-typed-columns
  binary
  blob)

Constructs an abstract varbinary column definition and add it to the given table. The length arguemnt is mandatory.

(def-length-bounded-typed-columns
  varbinary)

Boolean Type

Constructs an abstract boolean column definition and add it to the given table.

(def-simple-typed-columns
  boolean)

Data/time Types

Constructs an abstract date column definition and add it to the given table.

(def-simple-typed-columns
  date)

Constructs an abstract time column definition and add it to the given table. Takes an optional precision argument.

(def-optional-precision-typed-columns
  time
  timestamp)

Table Definition

(defn- build-table-elements [db-spec method & elements]
  (->> (apply concat elements)
       (map #(when (second %)
               (method (second %) db-spec)))
       (filter identity)))

Table records can be constructed using the table* function or the table macro. For internal use.

(defrecord Table [name columns constraints indexes]
  Alterable Creatable Dropable
  (build-alter-statement [this action db-spec]
    (let [elements (build-table-elements db-spec
                                         build-definition
                                         columns
                                         constraints)]
      (for [element elements]
        (AlterTableStatement.
         db-spec
         name
         action
         element))))
  (build-create-statement [this db-spec]
    (conj
     (build-table-elements db-spec build-create-statement indexes)
     (CreateTableStatement.
      db-spec
      name
      (build-table-elements db-spec build-definition columns constraints))))
  (build-drop-statement [this behavior db-spec]
    (DropStatement. db-spec :table name behavior nil)))

Constructs an abstract table definition. The table-name is mandatory.

(defn table*
  [table-name & [columns constraints indexes]]
  (name-required table-name "table")
  (Table. table-name
          (or columns {})
          (or constraints {})
          (or indexes {})))

Constructs an abstract table definition containing the given elements. Takes an arbitrary number of table elements.

(defmacro table
  [name & elements]
  `(-> (table* ~name {} {} {}) ~@(reverse elements)))

Schema Definition

Schema records can be constructed using the schema function. For internal use.

(defrecord Schema [sname elements options]
  Creatable Dropable
  (build-create-statement [this db-spec]
    (CreateSchemaStatement.
     db-spec
     sname
     (flatten
      (map #(build-create-statement (second %) db-spec)
           elements))))
  (build-drop-statement [this behavior db-spec]
    (DropStatement. db-spec :schema sname behavior nil)))

Returns true if the given object is a Schema.

(defn schema?
  [o]
  (isa? (type o) Schema))

Constructs an abstract schema definition.

(defn schema
  {:arglists '([schema-name options? & elements])}
  [schema-name & args]
  (name-required schema-name "schema")
  (let [[options elements] (optional (comp not definition?) args)]
    (Schema.
     schema-name
     (into (sorted-map)
           (map #(vector (:name %) %) elements))
     (or options {}))))

Definitions Hierarchy

The definition hierarchy makes it easy to test if an object represent an abstract schema element definition. See the definition? predicate.

(derive Index                ::definition)
(derive Constraint           ::definition)
(derive UniqueConstraint     ::definition)
(derive ForeignKeyConstraint ::definition)
(derive CheckConstraint      ::definition)
(derive DataType             ::definition)
(derive Column               ::definition)
(derive Table                ::definition)
(derive Schema               ::definition)
 

A set of connectivity functions.

(ns lobos.connectivity
  (:refer-clojure :exclude [defonce])
  (:require (clojure.java.jdbc [internal :as sqlint]))
  (:use lobos.utils))

Globals

This atom contains a map of all opened global connections.

(defonce global-connections
  (atom {}))

Helpers

(def find-connection sqlint/find-connection*)
(def connection sqlint/connection*)

Returns the associated db-spec or itself. For internal use.

(defn get-db-spec
  [& [connection-info]]
  (let [connection-info (or connection-info :default-connection)]
    (or (:db-spec sqlint/*db*)
        (if (keyword? connection-info)
          (-> @global-connections connection-info :db-spec)
          connection-info))))

Replaces get-connection from contrib.sql.internal namespace. Dissociates the :schema key to prevent conflict.

(defn ^{:dynamic true} *get-cnx*
  [db-spec]
  (let [db-spec (dissoc db-spec :schema)]
    (sqlint/get-connection db-spec)))

Checks if the given argument is a named connection or a db-spec. As a db-spec is just a map, any map return true. For internal use.

(defn connection?
  [cnx]
  (or ((set (keys @global-connections)) cnx)
      (map? cnx)))

Global Connections

Supplied with a keyword identifying a global connection, that connection is closed and the reference dropped. If a truthful silent argument is supplied, don't throw an execption if there's no such connection.

(defn close-global
  [& [connection-name silent]]
  (let [connection-name (or connection-name :default-connection)
        cnx (connection-name @global-connections)]
    (if cnx
      (do
        (.close (:connection cnx))
        (swap! global-connections dissoc connection-name)
        true)
      (when-not silent
        (throw
         (Exception. (format "No global connection by that name is open: %s"
                             connection-name)))))))
(defn- open-global* [connection-name db-spec]
  (let [cnx (*get-cnx* db-spec)]
    (when-let [ac (-> db-spec :auto-commit)]
      (.setAutoCommit cnx ac))
    (swap! global-connections assoc
           (or connection-name :default-connection)
           {:connection cnx :db-spec db-spec})))

Opens a global connection to the database specified by db-spec. If no connection-name is given, opens a default global connection.

If a global connection by that name already exists and the db-spec is safe (see below), then an exeption will be thrown. When the db-spec is unsafe it will be closed if the old db-spec is different and the original connection is left untouched.

A safe db-spec is a map that does not contain an :unsafe key set to a truthful value.

(defn open-global
  ([db-spec] (open-global :default-connection db-spec))
  ([connection-name db-spec]
     (if-let [cnx (connection-name @global-connections)]
       (if (:unsafe (:db-spec cnx))
         (when-not (= (:db-spec cnx) db-spec)
           (close-global connection-name)
           (open-global* connection-name db-spec))
         (throw
          (Exception.
           (format "A global connection by that name already exists (%s)"
                   connection-name))))
       (open-global* connection-name db-spec))))

With Connections

Evaluates func in the context of a named global connection to a database.

(defn with-named-connection
  [connection-name func]
  (io!
   (if-let [cnx (@global-connections connection-name)]
     (binding [sqlint/*db*
               (assoc sqlint/*db*
                 :connection (:connection cnx)
                 :level 0
                 :rollback (atom false)
                 :db-spec (:db-spec cnx))]
       (func))
     (throw
      (Exception.
       (format "No such global connection currently open: %s, only got %s"
               connection-name
               (vec (keys @global-connections))))))))

Evaluates func in the context of a new connection to a database then closes the connection.

(defn with-spec-connection
  [db-spec func]
  (with-open [cnx (*get-cnx* db-spec)]
    (binding [sqlint/*db* (assoc sqlint/*db*
                            :connection cnx
                            :level 0
                            :rollback (atom false)
                            :db-spec db-spec)]
      (when-let [ac (-> db-spec :auto-commit)]
        (.setAutoCommit cnx ac))
      (func))))

Evaluates body in the context of a new connection or a named global connection to a database then closes the connection if it's a new one. The connection-info parameter can be a keyword denoting a global connection or 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
  • Options (for ClojureQL):

    • :auto-commit (optional) a Boolean
    • :fetch-size (optional) an integer
(defmacro with-connection
  [connection-info & body]
  `(let [connection-info# (or ~connection-info :default-connection)]
     ((if (keyword? connection-info#)
        with-named-connection
        with-spec-connection) connection-info# (fn [] ~@body))))

Returns the default connection if it exists.

(defn default-connection
  []
  (try
    (with-named-connection :default-connection
      connection)
    (catch Exception _ nil)))