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)
 

Helpers to query the database's meta-data.

(ns lobos.metadata
  (:require (clojure.java.jdbc [internal :as sqlint])
            (lobos [compiler :as compiler]
                   [connectivity :as conn]
                   [schema :as schema]))
  (:import (java.sql DatabaseMetaData)))

Database Metadata

(def ^{:dynamic true :private true} *db-meta* nil)
(def ^{:dynamic true :private true} *db-meta-spec* nil)

Returns the binded DatabaseMetaData object found in db-meta or get one from the default connection if not available.

(defn db-meta
  []
  (or (when (conn/connection)
        (.getMetaData (conn/connection)))
      *db-meta*
      (conn/with-connection :default-connection
        (.getMetaData (conn/connection)))))
(defn db-meta-spec
  []
  (or (:db-spec sqlint/*db*)
      *db-meta-spec*
      (conn/get-db-spec :default-connection)))

Evaluates body in the context of a new connection or a named global connection to a database then closes the connection while binding its DatabaseMetaData object to db-meta.

(defmacro with-db-meta
  [connection-info & body]
  `(if ~connection-info
     (conn/with-connection ~connection-info
       (binding [*db-meta-spec* (conn/get-db-spec ~connection-info)
                 *db-meta* (.getMetaData (conn/connection))]
         ~@body))
     (do ~@body)))

Predicates

Returns the term used for catalogs if the underlying database supports that concept.

(defn supports-catalogs
  []
  (when (.supportsCatalogsInDataManipulation (db-meta))
    (.getCatalogTerm (db-meta))))

Returns the term used for schemas if the underlying database supports that concept.

(defn supports-schemas
  []
  (when (.supportsSchemasInDataManipulation (db-meta))
    (.getSchemaTerm (db-meta))))
(defmacro meta-call [method sname & args]
  `(doall
    (resultset-seq
     (~method (db-meta)
              (when (and ~sname (not (supports-schemas))) (name ~sname))
              (when (and ~sname (supports-schemas)) (name ~sname))
              ~@args))))

Database Objects

Returns a list of catalog names as keywords.

(defn catalogs
  []
  (map #(-> % :table_cat keyword)
       (doall (resultset-seq (.getCatalogs (db-meta))))))

Returns a list of schema names as keywords.

(defn schemas
  []
  (cond (supports-schemas)
        (map #(-> % :table_schem keyword)
             (doall (resultset-seq (.getSchemas (db-meta)))))
        (supports-catalogs) (catalogs)))
(defn default-schema []
  (when (supports-schemas)
    (->> (doall (resultset-seq (.getSchemas (db-meta))))
         (filter :is_default)
         first
         :table_schem
         keyword)))

Returns a list of table names as keywords for the specified schema.

(defn tables
  [& [sname]]
  (map #(-> % :table_name keyword)
       (meta-call .getTables sname nil (into-array ["TABLE"]))))

Returns primary key names as a set of keywords for the specified table.

(defn primary-keys
  [sname tname]
  (set
   (map #(-> % :pk_name keyword)
        (meta-call .getPrimaryKeys sname (name tname)))))

Returns metadata maps for indexes of the specified table. Results are sorted by ordinal position, grouped by index name and can be filtered by the given function f. Doesn't returns indexes that have tableIndexStatistic type.

(defn indexes-meta
  [sname tname & [f]]
  (try
    (group-by :index_name
      (filter #(and (> (:type %) DatabaseMetaData/tableIndexStatistic)
                    ((or f identity) %))
              (meta-call .getIndexInfo sname (name tname) false false)))
    (catch java.sql.SQLException _ nil)))

Returns metadata maps for cross reference of the specified foreign table. Results are sorted by their ordinal position and grouped by foreign key name.

(defn references-meta
  [sname tname]
  (group-by :fk_name
    (meta-call .getImportedKeys sname (name tname))))

Returns metadata maps for each columns of the specified table.

(defn columns-meta
  [sname tname]
  (meta-call .getColumns sname (name tname) nil))
 

Analyze a database's meta-data to contruct an abstract schema.

(ns lobos.analyzer
  (:refer-clojure :exclude [defonce replace])
  (:require (lobos [connectivity :as conn]
                   [schema :as schema]))
  (:use (clojure [string :only [replace]])
        lobos.internal
        lobos.metadata
        lobos.utils)
  (:import (java.sql DatabaseMetaData)
           (lobos.schema Column
                         DataType
                         Expression
                         ForeignKeyConstraint
                         Index
                         Schema
                         Table
                         UniqueConstraint)))

Analyzer

(def db-hierarchy
  (atom (-> (make-hierarchy)
            (derive :h2 ::standard)
            (derive :mysql ::standard)
            (derive :postgresql ::standard)
            (derive :sqlite ::standard)
            (derive :microsoft-sql-server ::standard))))

Analyzes the specified part of a schema and returns its abstract equivalent.

(defmulti analyze
  (fn [dispatch-val & args]
    (if (vector? dispatch-val)
      dispatch-val
      [(as-keyword (.getDatabaseProductName (db-meta)))
       dispatch-val]))
  :hierarchy db-hierarchy)

Default Analyzer

(defmethod analyze [::standard Expression]
  [_ expr]
  (when expr
    (Expression.
     (cond (re-find #"^(.*)::(.*)$" expr)
           (let [[_ & [value dtype]] (first (re-seq #"(.*)::(.*)" expr))]
             (read-string (replace (str value) \' \"))) ;; HACK: to replace!
           (re-find #"^\d+$" expr) (Integer/parseInt expr)
           (re-find #"^(\w+)(\(\))?$" expr)
           (let [[[_ func]] (re-seq #"(\w+)(\(\))?" expr)]
             (keyword func))
           :else (str expr)))))
(defmethod analyze [::standard UniqueConstraint]
  [_ sname tname cname index-meta]
  (let [pkeys (primary-keys sname tname)]
    (UniqueConstraint.
     (keyword cname)
     (if (pkeys (keyword cname))
       :primary-key
       :unique)
     (vec (map #(-> % :column_name keyword)
               index-meta)))))
(def action-rules
  {DatabaseMetaData/importedKeyCascade    :cascade
   DatabaseMetaData/importedKeySetNull    :set-null
   DatabaseMetaData/importedKeyRestrict   :restrict
   DatabaseMetaData/importedKeySetDefault :set-default})
(defmethod analyze [::standard ForeignKeyConstraint]
  [_ cname ref-meta]
  (let [pcolumns (vec (map #(-> % :pkcolumn_name keyword)
                           ref-meta))
        fcolumns (vec (map #(-> % :fkcolumn_name keyword)
                           ref-meta))
        ptable (-> ref-meta first :pktable_name keyword)
        on-delete (-> ref-meta first :delete_rule action-rules)
        on-delete (when on-delete [:on-delete on-delete])
        on-update (-> ref-meta first :update_rule action-rules)
        on-update (when on-delete [:on-update on-update])]
    (ForeignKeyConstraint.
     (keyword cname)
     fcolumns
     ptable
     pcolumns
     nil
     (into {} [on-delete on-update]))))
(defmethod analyze [::standard :constraints]
  [_ sname tname]
  (concat
   (map (fn [[cname meta]] (analyze UniqueConstraint sname tname cname meta))
        (indexes-meta sname tname #(let [nu (:non_unique %)]
                                     (or (false? nu) (= nu 0)))))
   (map (fn [[cname meta]] (analyze ForeignKeyConstraint cname meta))
        (references-meta sname tname))))
(defmethod analyze [::standard Index]
  [_ sname tname iname index-meta]
  (let [pkeys (primary-keys sname tname)]
    (Index.
     (keyword iname)
     tname
     (vec (map #(-> % :column_name keyword)
               index-meta))
     (when (-> index-meta first :non_unique not)
       (list :unique)))))
(defmethod analyze [::standard :indexes]
  [_ sname tname]
  (map (fn [[iname meta]] (analyze Index sname tname iname meta))
       (indexes-meta sname tname)))

Returns a vector containing the data type arguments for the given column meta data.

(defn analyze-data-type-args
  [dtype column-meta]
  (condp contains? dtype
    #{:nvarchar :varbinary :varchar} [(:column_size column-meta)]
    #{:binary :blob :char :clob :nchar :nclob
      :float :time :timestamp} [(:column_size column-meta)]
    #{:decimal :numeric} (let [scale (:decimal_digits column-meta)]
                           (if (= scale 0)
                           [(:column_size column-meta)]
                           [(:column_size column-meta) scale]))
    []))
(defmethod analyze [::standard DataType]
  [_ column-meta]
  (let [dtype (-> column-meta :type_name as-keyword)]
    (schema/data-type
     dtype
     (analyze-data-type-args dtype column-meta))))
(defmethod analyze [::standard Column]
  [_ column-meta]
  (let [auto-inc (= (:is_autoincrement column-meta) "YES")]
    (Column. (-> column-meta :column_name keyword)
             (analyze DataType column-meta)
             (when-not auto-inc
               (analyze Expression (:column_def column-meta)))
             auto-inc
             (= (:is_nullable column-meta) "NO")
             [])))
(defmethod analyze [::standard Table]
  [_ sname tname]
  (schema/table* tname
                 (into {} (map #(let [c (analyze Column %)]
                                  [(:cname c) c])
                               (columns-meta sname tname)))
                 (into {} (map #(vector (:cname %) %)
                               (analyze :constraints sname tname)))
                 (into {} (map #(vector (:iname %) %)
                               (analyze :indexes sname tname)))))
(defmethod analyze [::standard Schema]
  [_ sname]
  (apply schema/schema sname {:db-spec (db-meta-spec)}
         (map #(analyze Table sname %)
              (tables sname))))
(defn analyze-schema
  [& args]
  {:arglists '([connection-info? sname?])}
  (let [[db-spec sname _] (optional-cnx-and-sname args)]
    (with-db-meta db-spec
      (autorequire-backend db-spec)
      (let [sname (or (keyword sname)
                      (default-schema)
                      (first _))]
        (if-let [schemas (schemas)]
          (when (or (nil? sname)
                    ((set schemas) sname))
            (analyze Schema sname))
          (analyze Schema sname))))))