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)
 

This namespace include the compile multimethod, a default implementation based on the SQL standard and some helpers functions. The compiler works on an AST defined in the lobos.ast namespace. Database specific implementations can be found in the backends directory.

(ns lobos.compiler
  (:refer-clojure :exclude [compile defonce replace])
  (:require (lobos [ast :as ast]))
  (:use (clojure [string :only [replace]])
        lobos.utils)
  (:import (java.lang UnsupportedOperationException)))
(ast/import-all)

Compiler

When no implementation specific method is found, fall back on the default standard compiler.

(def backends-hierarchy
  (atom (-> (make-hierarchy)
            (derive :h2 ::standard)
            (derive :mysql ::standard)
            (derive :postgresql ::standard)
            (derive :sqlite ::standard)
            (derive :sqlserver ::standard))))

Compiles the given object into SQL.

(defmulti compile
  (fn [o]
    [(-> o :db-spec :subprotocol (or ::standard) keyword)
     (type o)])
  :hierarchy backends-hierarchy)

Helpers

Constructs an IdentifierExpression AST form and compiles it. Takes an optional level argument to determine which qualification level to output, only support :schema for now.

(defn as-identifier
  [db-spec name & qualifiers]
  (compile (IdentifierExpression. db-spec name qualifiers)))

Throws an UnsupportedOperationException using the given message. Can be given a condition, in which case it throw an exception only if it's true.

(defn unsupported
  ([msg] (unsupported true msg))
  ([cond msg]
     (when cond
       (throw (UnsupportedOperationException. (str msg))))))

Constructs a Mode AST form with the specified db-spec.

(defn mode
  [db-spec]
  (Mode. db-spec))

Used by the extract-foreign-keys function to extract foreign key constraints from the given table.

(defn- extract-foreign-keys*
  [table]
  (let [fkey? #(instance? ForeignKeyConstraintDefinition %)
        foreign-keys (vector (:tname table)
                             (->> (:elements table)
                                  (filter fkey?)))
        table (update-in table [:elements]
               (fn [es] (filter #(not (fkey? %)) es)))]
    [table foreign-keys]))

Given a collection of AST statements, extract all foreign key constraints for all tables and returns a vector composed of the statements (without the foreign keys) and a map of the foreign keys grouped by table name.

(defn extract-foreign-keys
  [elements]
  (let [tables (filter #(instance? CreateTableStatement %) elements)
        others (filter #(not (instance? CreateTableStatement %)) elements)
        results (map extract-foreign-keys* tables)
        tables (map first results)
        foreign-keys (apply hash-map (apply concat (map second results)))]
    [(concat tables others)
     foreign-keys]))

Helper used when compiling a create schema statement that require the foreign keys to be extracted and added individually using alter add statements.

(defn build-alter-add-statements
  [db-spec m]
  (for [[tname elements] m element elements]
    (AlterTableStatement. db-spec tname :add element)))

Default compiler

The default compiler is based on the SQL standard and does not prefer any particular implementation.

Compiling Mode instance returns nil by default.

(defmethod compile [::standard Mode] [_] nil)

Expressions

Keywords will be made into SQL keywords using the as-sql-keyword function while strings will be properly delimited by single quotes.

(defmethod compile [::standard ScalarExpression]
  [expression]
  (let [{:keys [scalar]} expression]
    (cond (keyword? scalar) (str (as-sql-keyword scalar))
          (string? scalar) (str "'" scalar "'")
          :else scalar)))
(defmethod compile [::standard IdentifierExpression]
  [identifier]
  (let [{:keys [db-spec name qualifiers]} identifier]
    (join* \. (->> (concat qualifiers [name])
                   (filter identity)
                   (map #(when % (as-str \" % \")))))))
(defmethod compile [::standard FunctionExpression]
  [function]
  (let [{:keys [db-spec name args]} function]
    (str (as-sql-keyword name)
         (as-list (map compile args)))))
(defmethod compile [::standard OperatorExpression]
  [operator]
  (let [{:keys [db-spec op left right]} operator]
    (if (vector? (first right))
      (str "("
           (compile left) " "
           (as-sql-keyword op) " "
           (as-list (map compile (first right)))
           ")")
      (str "(" (apply join
                      (str " " (as-sql-keyword op) " ")
                      (map compile (conj right left))) ")"))))

DataTypeClause instances are compiled with their dtype property made into SQL keywords using the as-sql-keyword function. If the data-type has an argument list, it will be passed through the as-list function.

(defmethod compile [::standard DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression
        {:keys [encoding collate time-zone]} options]
    (join \space
      (str (as-sql-keyword dtype) (as-list args))
      (when encoding ["CHARACTER SET" (as-str encoding)])
      (when collate ["COLLATE" (as-str collate)])
      (when time-zone ["WITH TIME ZONE"]))))

Clauses

Standard AutoIncClause are using the ALWAYS variant.

(defmethod compile [::standard AutoIncClause]
  [_]
  "GENERATED ALWAYS AS IDENTITY")

Definitions

ColumnDefinition instance will get their names made into SQL identifiers using the as-identifier function. It's data-type will get compiled with the appropriate method. The options will be added in this order: default clause, auto-inc option and the not-null constraint. Strings found in the others property will be added as they are.

(defmethod compile [::standard ColumnDefinition]
  [definition]
  (let [{:keys [db-spec cname data-type default
                auto-inc not-null others]} definition]
    (apply join \space
      (as-identifier db-spec cname)
      (compile data-type)
      (when default  (str "DEFAULT " (compile default)))
      (when auto-inc (compile auto-inc))
      (when not-null "NOT NULL")
      others)))

UniqueConstraintDefinition instances will get their names made into SQL identifiers using the as-identifier function. The ctype property must one of :primary-key or :unique.

(defmethod compile [::standard UniqueConstraintDefinition]
  [definition]
  (let [{:keys [db-spec cname ctype columns]} definition]
    (join \space
      "CONSTRAINT"
      (as-identifier db-spec cname)
      (as-sql-keyword ctype)
      (as-list (map (partial as-identifier db-spec) columns)))))

ForeignKeyConstraintDefinition instances are always compiled with their full references clause. The options will be added in this order: the match clause, the on-delete and the on-update triggered actions.

(defmethod compile [::standard ForeignKeyConstraintDefinition]
  [definition]
  (let [{:keys [db-spec cname columns parent-table parent-columns match
                triggered-actions]} definition]
    (join \space
      "CONSTRAINT"
      (as-identifier db-spec cname)
      "FOREIGN KEY"
      (as-list (map (partial as-identifier db-spec) columns))
      "REFERENCES"
      (as-identifier db-spec parent-table (:schema db-spec))
      (as-list (map (partial as-identifier db-spec) parent-columns))
      (when match (str "MATCH " (as-sql-keyword match)))
      (when (contains? triggered-actions :on-delete)
        (str "ON DELETE " (as-sql-keyword (:on-delete triggered-actions))))
      (when (contains? triggered-actions :on-update)
        (str "ON UPDATE " (as-sql-keyword (:on-update triggered-actions)))))))
(defmethod compile [::standard CheckConstraintDefinition]
  [definition]
  (let [{:keys [db-spec cname condition]} definition]
    (join \space
      "CONSTRAINT"
      (as-identifier db-spec cname)
      "CHECK"
      (compile condition))))

Create and Drop Statements

The default create statement will extract all foreign key constraint from its create table statements to be added afterward using alter add statements.

(defmethod compile [::standard CreateSchemaStatement]
  [statement]
  (let [{:keys [db-spec sname elements]} statement
        [elements foreign-keys] (extract-foreign-keys elements)
        alters (map compile (build-alter-add-statements
                             (assoc db-spec :schema sname)
                             foreign-keys))]
    (conj alters
          (str "CREATE SCHEMA "
               (apply join "\n" (conj (map compile elements)
                                      (as-identifier db-spec sname)))))))

CreateTableStatement instances will properly compile all their elements.

(defmethod compile [::standard CreateTableStatement]
  [statement]
  (let [{:keys [db-spec tname elements]} statement]
    (format "CREATE TABLE %s %s"
            (as-identifier db-spec tname (:schema db-spec))
            (or (as-list (map compile elements))
                "()"))))
(defmethod compile [::standard CreateIndexStatement]
  [statement]
  (let [{:keys [db-spec iname tname columns options]} statement
        index-column #(if (keyword? %)
                        (as-identifier db-spec %)
                        (let [col (first %)
                              options (set (rest %))]
                          (apply join \space
                            (as-identifier db-spec (first %))
                            (map as-sql-keyword options))))]
    (format "CREATE %sINDEX %s ON %s %s"
            (str (when ((set options) :unique) "UNIQUE "))
            (as-identifier db-spec iname)
            (as-identifier db-spec tname (:schema db-spec))
            (as-list (map index-column columns)))))
(defmethod compile [::standard DropStatement]
  [statement]
  (let [{:keys [db-spec otype oname behavior]} statement]
    (join \space
      "DROP"
      (as-sql-keyword otype)
      (as-identifier db-spec oname (:schema db-spec))
      (as-sql-keyword behavior))))

Alter Statement and Actions

(defmethod compile [::standard AlterAddAction]
  [action]
  (let [{:keys [db-spec element]} action
        element (assoc element :db-spec db-spec)]
    (join \space
          "ADD"
          (compile element))))
(defmethod compile [::standard AlterDropAction]
  [action]
  (let [{:keys [db-spec element]} action
        is-column (instance? ColumnDefinition element)]
    (join \space
          "DROP"
          (if is-column
            "COLUMN"
            "CONSTRAINT")
          (as-identifier db-spec (:cname element)))))
(defmethod compile [::standard AlterModifyAction]
  [action]
  (let [{:keys [db-spec element]} action
        default (:default element)]
    (if default
      (join \space
            "ALTER COLUMN"
            (as-identifier db-spec (:cname element))
            (if (= default :drop)
              "DROP DEFAULT"
              (str "SET DEFAULT " (compile default))))
      (unsupported "Only set/drop default supported."))))

AlterRenameAction instances aren't supported by the default compiler.

(defmethod compile [::standard AlterRenameAction]
  [action]
  (let [{:keys [db-spec element]} action]
     (unsupported "Rename action not supported.")))

AlterTableStatement instances will get dispatched further into one of the action supported: AlterAddAction, AlterDropAction, AlterModifyAction or AlterRenameAction.

(defmethod compile [::standard AlterTableStatement]
  [statement]
  (let [{:keys [db-spec tname action element]} statement
        element (assoc element :sname (:schema db-spec) :tname tname)]
    (join \space
          "ALTER TABLE"
          (as-identifier db-spec tname (:schema db-spec))
          (case action
            :add    (compile (AlterAddAction. db-spec element))
            :drop   (compile (AlterDropAction. db-spec element))
            :modify (compile (AlterModifyAction. db-spec element))
            :rename (compile (AlterRenameAction. db-spec element))))))
 

Compiler implementation for H2.

(ns lobos.backends.h2
  (:refer-clojure :exclude [compile defonce])
  (:require (lobos [schema :as schema]))
  (:use (clojure [string :only [split]])
        (lobos analyzer compiler connectivity internal metadata utils))
  (:import (lobos.ast AlterRenameAction
                      AutoIncClause
                      CreateSchemaStatement
                      DataTypeClause
                      DropStatement)
           (lobos.schema ForeignKeyConstraint
                         UniqueConstraint)))

Analyzer

(defmethod analyze [:h2 UniqueConstraint]
  [_ sname tname cname meta]
  (let [columns (split (:column_list meta) #",")
        ctype (-> meta :constraint_type as-keyword)]
    (UniqueConstraint.
     (make-index-name tname ctype columns)
     ctype
     (map as-keyword columns))))
(defmethod analyze [:h2 :constraints]
  [_ sname tname]
  (let [db-spec (db-meta-spec)]
    (concat
     (map (fn [meta] (analyze UniqueConstraint sname tname
                              (-> meta :constraint_name keyword)
                              meta))
          (query db-spec
                 :INFORMATION_SCHEMA
                 :CONSTRAINTS
                 (and (or (= :CONSTRAINT_TYPE "UNIQUE")
                          (= :CONSTRAINT_TYPE "PRIMARY KEY"))
                      (= :TABLE_SCHEMA (as-str sname))
                      (= :TABLE_NAME (as-str tname)))))
     (map (fn [[cname meta]] (analyze ForeignKeyConstraint cname meta))
          (references-meta sname tname)))))

Compiler

(defmethod compile [:h2 DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression]
    (unsupported (= dtype :binary)
      "Use varbinary instead.")
    (unsupported (:time-zone options)
      "Time zones not supported.")
    (str (as-sql-keyword dtype) (as-list args))))
(defmethod compile [:h2 AutoIncClause]
  [_]
  "AUTO_INCREMENT")
(defmethod compile [::standard CreateSchemaStatement]
  [statement]
  (let [{:keys [db-spec sname elements]} statement
        [elements foreign-keys] (extract-foreign-keys elements)
        alters (map compile (build-alter-add-statements
                             (assoc db-spec :schema sname)
                             foreign-keys))]
    (conj alters
          (str "CREATE SCHEMA "
               (apply join "\n" (conj (map compile elements)
                                      (as-identifier db-spec sname)))))))
(defmethod compile [:h2 CreateSchemaStatement]
  [statement]
  (let [{:keys [db-spec sname elements]} statement
        [elements foreign-keys] (extract-foreign-keys elements)
        alters (map compile (build-alter-add-statements
                             (assoc db-spec :schema sname)
                             foreign-keys))]
    (conj (concat (map (comp compile
                             #(assoc-in % [:db-spec :schema] sname))
                       elements)
                  alters)
          (str "CREATE SCHEMA "
               (as-identifier db-spec sname)))))
(defmethod compile [:h2 DropStatement]
  [statement]
  (let [{:keys [db-spec otype oname behavior]} statement]
    (join \space
      "DROP"
      (as-sql-keyword otype)
      (as-identifier db-spec oname (:schema db-spec))
      (when (and behavior (#{:table} otype))
        (as-sql-keyword behavior)))))
(defmethod compile [:h2 AlterRenameAction]
  [action]
  (let [{:keys [db-spec element]} action
        old-name (:cname element)
        new-name (:others element)]
    (format "ALTER COLUMN %s RENAME TO %s"
            (as-identifier db-spec old-name)
            (as-identifier db-spec new-name))))
 

Compiler implementation for MySQL.

(ns lobos.backends.mysql
  (:refer-clojure :exclude [compile defonce])
  (:require (lobos [schema :as schema]
                   [ast :as ast]))
  (:use (lobos [schema :only [build-definition]]
               analyzer
               compiler
               metadata
               utils))
  (:import (lobos.schema Column
                         DataType
                         Schema
                         UniqueConstraint)))
(ast/import-all)

Analyzer

(def ^{:private true} analyzer-data-type-aliases
  {:bit :boolean
   :int :integer
   :text :clob
   :tinyblob :blob
   :tinytext :clob})
(defmethod analyze [:mysql DataType]
  [_ column-meta]
  (let [dtype (-> column-meta :type_name as-keyword)
        dtype (first (replace analyzer-data-type-aliases [dtype]))]
    (schema/data-type
     dtype
     (if (#{:time :timestamp} dtype)
       []
       (analyze-data-type-args dtype column-meta)))))
(defmethod analyze [:mysql UniqueConstraint]
  [_ sname tname cname index-meta]
  (let [pkeys (primary-keys sname tname)
        pkey (pkeys (keyword cname))
        columns (vec (map #(-> % :column_name keyword)
                          index-meta))]
    (UniqueConstraint.
     (if pkey
       (make-index-name tname :primary-key columns)
       (keyword cname))
     (if pkey
       :primary-key
       :unique)
     columns)))
(defn- analyze-column [sname tname cname]
  (analyze Column
    (first
     (resultset-seq
      (.getColumns (db-meta) (name sname) nil (name tname) (name cname))))))
(defmethod analyze [:mysql Schema]
  [_ sname]
  (let [db-spec (db-meta-spec)
        sname (or sname
                  (->> db-spec
                       :subname
                       (re-find #"//.*/(.+)$")
                       second))]
    (when-not sname
      (throw (java.sql.SQLException. "No database selected")))
    (analyze [:lobos.analyzer/standard Schema] sname)))

Compiler

(defmethod compile [:mysql IdentifierExpression]
  [identifier]
  (let [{:keys [db-spec name qualifiers]} identifier]
    (join* \. (->> (concat qualifiers [name])
                   (filter identity)
                   (map #(when % (as-str \` % \`)))))))
(def ^{:private true} compiler-data-type-aliases
  {:clob :text
   :nclob :text})
(defmethod compile [:mysql DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression
        {:keys [encoding collate]} options
        encoding (when (= dtype :nclob) "UTF8")
        dtype (first (replace compiler-data-type-aliases [dtype]))
        args (if (#{:time :timestamp} dtype) [] args)]
    (unsupported (= dtype :real)
      "Use double instead.")
    (unsupported (:time-zone options)
      "Time zones not supported.")
    (join \space
      (str (as-sql-keyword dtype) (as-list args))
      (when encoding (str "CHARACTER SET " (as-str encoding)))
      (when collate (str "COLLATE " (as-str collate))))))
(defmethod compile [:mysql AutoIncClause]
  [_]
  "AUTO_INCREMENT")
(defmethod compile [:mysql CreateSchemaStatement]
  [statement]
  (let [{:keys [db-spec sname elements]} statement
        [elements foreign-keys] (extract-foreign-keys elements)
        alters (map compile (build-alter-add-statements
                             (assoc db-spec :schema sname)
                             foreign-keys))]
    (conj (concat (map (comp compile
                             #(assoc-in % [:db-spec :schema] sname))
                       elements)
                  alters)
          (str "CREATE SCHEMA "
               (as-identifier db-spec sname)))))
(defmethod compile [:mysql DropStatement]
  [statement]
  (let [{:keys [db-spec otype oname behavior options]} statement]
    (if (= otype :index)
      (join \space
        "DROP INDEX"
        (as-identifier db-spec oname)
        "ON"
        (as-identifier db-spec (:tname options) (:schema db-spec)))
      (join \space
        "DROP"
        (as-sql-keyword otype)
        (as-identifier db-spec oname (:schema db-spec))
        (when (and behavior (#{:table} otype))
          [(as-sql-keyword behavior)])))))
(defmethod compile [:mysql AlterDropAction]
  [action]
  (let [{:keys [db-spec element]} action
        is-unique (instance? UniqueConstraintDefinition element)
        is-pkey (and is-unique (= (:type element) :primary-key))]
    (join \space
          "DROP"
          (cond (instance? ColumnDefinition element) "COLUMN"
                (instance? ForeignKeyConstraintDefinition element) "FOREIGN KEY"
                (and is-unique (= (:ctype element) :unique)) "INDEX"
                is-pkey "PRIMARY KEY")
          (when-not is-pkey
            (as-identifier db-spec (:cname element))))))
(defmethod compile [:mysql AlterRenameAction]
  [action]
  (let [{:keys [db-spec element]} action
        old-name (:cname element)
        new-name (:others element)
        column (with-db-meta db-spec
                 (assoc (analyze-column (:sname element)
                                        (:tname element)
                                        old-name)
                   :cname new-name))]
    (join \space
          "CHANGE"
          (as-identifier db-spec old-name)
          (compile (build-definition column db-spec)))))
 

Compiler implementation for PostgreSQL.

(ns lobos.backends.postgresql
  (:refer-clojure :exclude [compile defonce])
  (:require (lobos [schema :as schema]))
  (:use lobos.analyzer
        lobos.compiler
        lobos.utils)
  (:import (lobos.ast AlterRenameAction
                      ColumnDefinition
                      DataTypeClause)
           (lobos.schema DataType
                         Schema)))

Analyzer

(def ^{:private true} analyzer-data-type-aliases
  {:bool :boolean
   :bpchar :char
   :bytea :blob
   :float4 :real
   :float8 :double
   :int2 :smallint
   :int4 :integer
   :int8 :bigint
   :text :nclob
   :timestamptz :timestamp
   :timetz :time})
(defmethod analyze [:postgresql DataType]
  [_ column-meta]
  (let [dtype (-> column-meta :type_name as-keyword)
        options {:time-zone (#{:timetz :timestamptz} dtype)}
        dtype (first (replace analyzer-data-type-aliases
                              [dtype]))]
    (schema/data-type
     dtype
     (analyze-data-type-args dtype column-meta)
     options)))
(defmethod analyze [:postgresql Schema]
  [_ sname]
  (analyze [:lobos.analyzer/standard Schema] (or sname :public)))

Compiler

(def ^{:private true} compiler-data-type-aliases
  {:blob :bytea
   :clob :text
   :double :double-precision
   :nclob :text
   :nvarchar :varchar})
(defmethod compile [:postgresql DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression
        {:keys [time-zone]} options
        dtype (first (replace compiler-data-type-aliases [dtype]))
        args (if (#{:bytea :text} dtype) [] args)]
    (unsupported (#{:binary :varbinary} dtype)
      "Use blob instead.")
    (join \space
      (str (as-sql-keyword dtype) (as-list args))
      (when time-zone "WITH TIME ZONE"))))
(defmethod compile [:postgresql ColumnDefinition]
  [definition]
  (let [{:keys [db-spec cname data-type default
                auto-inc not-null others]} definition]
    (apply join \space
      (as-identifier db-spec cname)
      (if auto-inc "SERIAL" (compile data-type))
      (when default (str "DEFAULT " (compile default)))
      (when not-null "NOT NULL")
      others)))
(defmethod compile [:postgresql AlterRenameAction]
  [action]
  (let [{:keys [db-spec element]} action
        old-name (:cname element)
        new-name (:others element)]
    (format "RENAME COLUMN %s TO %s"
            (as-identifier db-spec old-name)
            (as-identifier db-spec new-name))))
 

Compiler implementation for SQLite.

(ns lobos.backends.sqlite
  (:refer-clojure :exclude [compile defonce])
  (:require (lobos [schema :as schema]))
  (:use (lobos analyzer compiler connectivity internal metadata utils))
  (:import (lobos.ast AlterTableStatement
                      AutoIncClause
                      CreateSchemaStatement
                      CreateTableStatement
                      DataTypeClause
                      DropStatement
                      IdentifierExpression)
           (lobos.schema DataType
                         ForeignKeyConstraint
                         Schema
                         UniqueConstraint)))

Analyzer

(def ^{:private true} analyzer-data-type-aliases
  {:time-with-time-zone :time
   :timestamp-with-time-zone :timestamp})
(defmethod analyze [:sqlite DataType]
  [_ column-meta]
  (let [dtype (-> column-meta :type_name as-keyword)
        tz? #{:time-with-time-zone :timestamp-with-time-zone}
        [dtype options] (if (tz? dtype)
                          [dtype {:time-zone true}]
                          [dtype nil])
        dtype (first (replace analyzer-data-type-aliases [dtype]))
        args (analyze-data-type-args dtype column-meta)]
    (schema/data-type
     dtype
     (if (#{:decimal :numeric} dtype)
       [(first args)]
       args)
     options)))
(defmethod analyze [:sqlite UniqueConstraint]
  [_ sname tname cname index-meta]
  (let [columns (vec (map #(-> % :column_name keyword)
                          index-meta))]
    (UniqueConstraint.
     (make-index-name tname :unique columns)
     :unique
     columns)))
(defn- analyze-primary-keys [tname]
  (let [columns (reduce
                 #(conj %1 (-> %2 :column_name keyword))
                 []
                 (resultset-seq
                  (.getPrimaryKeys (db-meta) nil nil (name tname))))]
    (when (not-empty columns)
      [(UniqueConstraint.
        (make-index-name tname :primary-key columns)
        :primary-key
        columns)])))
(defn- analyze-foreign-keys [tname]
  (let [fks (group-by :id  (try
                             (raw-query (format "pragma foreign_key_list(%s);"
                                                (name tname)))
                             (catch Exception _)))]
    (for [fk fks]
      (let [fk (second fk)
            pcolumns (reduce #(conj %1 (-> %2 :to keyword)) [] fk)
            fcolumns (reduce #(conj %1 (-> %2 :from keyword)) [] fk)
            fk (first fk)
            ptable (keyword (:table fk))
            match (as-keyword (:match fk))
            match (when-not (= match :none) match)
            on-delete (as-keyword (:on_delete fk))
            on-delete (when-not (= on-delete :no-action) on-delete)
            on-delete (when on-delete [:on-delete on-delete])
            on-update (as-keyword (:on_update fk))
            on-update (when-not (= on-update :no-action) on-update)
            on-update (when on-delete [:on-update on-update])]
        (ForeignKeyConstraint.
         (make-index-name tname :fkey fcolumns)
         fcolumns
         ptable
         pcolumns
         match
         (into {} [on-delete on-update]))))))
(defn- analyze-unique [sname tname]
  (map (fn [[cname meta]] (analyze UniqueConstraint sname tname cname meta))
       (indexes-meta sname tname #(let [nu (:non_unique %)]
                                    (or (false? nu) (= nu 0))))))
(defmethod analyze [:sqlite :constraints]
  [_ sname tname]
  (concat (analyze-unique sname tname)
          (analyze-primary-keys tname)
          (analyze-foreign-keys tname)))
(defmethod analyze [:sqlite Schema]
  [_ sname]
  (let [db-spec (db-meta-spec)
        sname (or sname
                  (->> db-spec
                       :subname
                       (re-find #"^\./(.*)\.\w+$")
                       second))]
    (analyze [:lobos.analyzer/standard Schema] sname)))

Compiler

(defmethod compile [:sqlite IdentifierExpression]
  [identifier]
  (as-str (:name identifier)))
(defmethod compile [:sqlite DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression
        {:keys [collate time-zone]} options]
    (unsupported (and (#{:decimal :numeric} dtype) (= (count args) 2))
      "Doesn't support scale argument.")
    (join \space
      (str (as-sql-keyword dtype) (as-list args))
      (when collate (str "COLLATE " (as-str collate)))
      (when time-zone "WITH TIME ZONE"))))
(defmethod compile [:sqlite AutoIncClause]
  [_]
  nil)
(defmethod compile [:sqlite CreateSchemaStatement]
  [statement]
  (let [{:keys [db-spec sname elements]} statement]
    (map compile elements)))
(defn- drop-schema [db-spec]
  (map (comp compile
             #(schema/build-drop-statement % :cascade db-spec)
             #(schema/table %)
             :name)
       (with-connection db-spec
         (raw-query
          (format "select name from sqlite_master where type <> 'index';")))))
(defmethod compile [:sqlite DropStatement]
  [statement]
  (let [{:keys [db-spec otype oname behavior]} statement]
    (if (= otype :schema)
      (drop-schema db-spec)
      (join \space
        "DROP"
        (as-sql-keyword otype)
        (as-identifier db-spec oname)))))
(defmethod compile [:sqlite AlterTableStatement]
  [statement]
  (unsupported "Alter statement unsupported."))
 

Compiler implementation for SQL Server.

(ns lobos.backends.sqlserver
  (:refer-clojure :exclude [compile defonce])
  (:require clojure.string
            (lobos [schema :as schema]))
  (:use lobos.analyzer
        lobos.compiler
        lobos.metadata
        lobos.utils)
  (:import (lobos.ast AlterAddAction
                      AlterDropAction
                      AlterModifyAction
                      AlterRenameAction
                      AlterTableStatement
                      AutoIncClause
                      DataTypeClause
                      DropStatement
                      FunctionExpression
                      IdentifierExpression)
           (lobos.schema DataType
                         Expression
                         Schema)))

Analyzer

(defmethod analyze [:microsoft-sql-server Expression]
  [_ expr]
  (when expr
    (Expression.
     (cond (re-find #"^\(\((\d+)\)\)$" expr)
           (let [[[_ n]] (re-seq #"(\w+)(\(\))?" expr)]
             (Integer/parseInt n))))))
(def ^{:private true} analyzer-data-type-aliases
  {:bit :boolean
   :datetime2 :timestamp
   :image :blob
   :int :integer
   :ntext :nclob
   :text :clob})
(defmethod analyze [:microsoft-sql-server DataType]
  [_ column-meta]
  (let [dtype (-> column-meta :type_name as-keyword)
        [dtype options] (if (= dtype :datetimeoffset)
                          [:timestamp {:time-zone true}]
                          [dtype nil])
        dtype (first (replace analyzer-data-type-aliases
                              [dtype]))]
    (schema/data-type
     dtype
     (analyze-data-type-args dtype column-meta)
     options)))
(defmethod analyze [:microsoft-sql-server Schema]
  [_ sname]
  (analyze [:lobos.analyzer/standard Schema] (or sname :dbo)))

Compiler

(defmethod compile [:sqlserver IdentifierExpression]
  [identifier]
  (let [{:keys [db-spec name qualifiers]} identifier]
    (join* \. (->> (concat qualifiers [name])
                   (filter identity)
                   (map #(when % (as-str \[ % \])))))))
(defmethod compile [:sqlserver FunctionExpression]
  [function]
  (let [{:keys [db-spec name args]} function
        name (if (= name :length)
               :len
               name)]
    (str (as-sql-keyword name)
         (as-list (map compile args)))))
(def ^{:private true} compiler-data-type-aliases
  {:blob      :image
   :boolean   :bit
   :clob      :text
   :double    :float
   :nclob     :ntext
   :timestamp :datetime2})
(defmethod compile [:sqlserver DataTypeClause]
  [expression]
  (let [{:keys [dtype args options]} expression
        {:keys [collate time-zone]} options
        dtype (if (and (= dtype :timestamp) time-zone)
                :datetimeoffset
                (first (replace compiler-data-type-aliases [dtype])))
        args (if (#{:image :ntext :text} dtype) [] args)]
    (unsupported (and (= dtype :time) time-zone)
      "Time zone unsupported for time data type.")
    (join \space
      (str (as-sql-keyword dtype) (as-list args))
      (when collate (str "COLLATE " (as-str collate))))))
(defmethod compile [:sqlserver AutoIncClause]
  [_]
  "IDENTITY")
(defn- drop-schema-cascade [db-spec sname]
  (vec (for [element (with-db-meta db-spec
                       (-> (analyze-schema sname) :elements keys))]
         (compile (schema/build-drop-statement
                   (schema/table element)
                   :cascade
                   (assoc db-spec :schema sname))))))
(defmethod compile [:sqlserver DropStatement]
  [statement]
  (let [{:keys [db-spec otype oname behavior options]} statement
        sql-string (join \space
                     "DROP"
                     (as-sql-keyword otype)
                     (as-identifier db-spec oname
                                    (when (not= otype :schema)
                                      (:schema db-spec))))]
    (if (= otype :index)
      (join \space
        "DROP INDEX"
        (as-identifier db-spec oname)
        "ON"
        (as-identifier db-spec (:tname options) (:schema db-spec)))
      (apply join \;
             (conj
              (when (and (= otype :schema)
                         (= behavior :cascade))
                (drop-schema-cascade db-spec oname))
              sql-string)))))
(defmethod compile [:sqlserver AlterModifyAction]
  [action]
  (let [{:keys [db-spec element]} action
        default (:default element)
        cname (when default
                (make-index-name (:tname element)
                                 "default"
                                 (list (:cname element))))]
    (cond (= default :drop)
          (join \space
                "DROP CONSTRAINT"
                (as-identifier db-spec cname))
          default
          (join \space
                "ADD CONSTRAINT"
                (as-identifier db-spec cname)
                "DEFAULT"
                (compile default)
                "FOR"
                (as-identifier db-spec (:cname element)))
          :else (unsupported "Only set/drop default supported."))))
(defmethod compile [:sqlserver AlterRenameAction]
  [action]
  (let [{:keys [db-spec element]} action]
    (format "EXEC sp_rename '%s', '%s', 'COLUMN';"
            (join \.
                  (as-identifier db-spec (:sname element))
                  (as-identifier db-spec (:tname element))
                  (as-identifier db-spec (:cname element)))
            (as-str (:others element)))))
(defmethod compile [:sqlserver AlterTableStatement]
  [statement]
  (let [{:keys [db-spec tname action element]} statement
        element (assoc element :sname (:schema db-spec) :tname tname)]
    (if (= action :rename)
      (compile (AlterRenameAction. db-spec element))
      (join \space
            "ALTER TABLE"
            (as-identifier db-spec tname (:schema db-spec))
            (case action
                  :add    (compile (AlterAddAction. db-spec element))
                  :drop   (compile (AlterDropAction. db-spec element))
                  :modify (compile (AlterModifyAction. db-spec element)))))))
 

Abstract SQL syntax tree for the DDL part of the language.

(ns lobos.ast)

Special Records

(defrecord Mode
  [db-spec])

Expression Records

(defrecord ScalarExpression
  [db-spec scalar])
(defrecord IdentifierExpression
  [db-spec name qualifiers])
(defrecord FunctionExpression
  [db-spec name args])
(defrecord OperatorExpression
  [db-spec op left right])

Clause Records

(defrecord AutoIncClause
  [db-spec])
(defrecord DataTypeClause
  [db-spec dtype args options])

Definition Records

(defrecord ColumnDefinition
  [db-spec cname data-type default auto-inc not-null others])
(defrecord ConstraintDefinition
  [db-spec cname])
(defrecord UniqueConstraintDefinition
  [db-spec cname ctype columns])
(defrecord ForeignKeyConstraintDefinition
  [db-spec cname columns parent-table parent-columns match triggered-actions])
(defrecord CheckConstraintDefinition
  [db-spec cname condition])

Statement Records

(defrecord CreateSchemaStatement
  [db-spec sname elements])
(defrecord CreateTableStatement
  [db-spec tname elements])
(defrecord CreateIndexStatement
  [db-spec iname tname columns options])
(defrecord DropStatement
  [db-spec otype oname behavior options])
(defrecord AlterTableStatement
  [db-spec tname action element])

Alter Action Records

(defrecord AlterAddAction
  [db-spec element])
(defrecord AlterDropAction
  [db-spec element])
(defrecord AlterModifyAction
  [db-spec element])
(defrecord AlterRenameAction
  [db-spec element])

Helpers

Import all expression AST records into the calling namespace.

(defn import-expressions
  []
  (import
   '(lobos.ast ScalarExpression
               IdentifierExpression
               FunctionExpression
               OperatorExpression)))

Import all clause AST records into the calling namespace.

(defn import-clauses
  []
  (import
   '(lobos.ast AutoIncClause
               DataTypeClause)))

Import all definition AST records into the calling namespace.

(defn import-definitions
  []
  (import
   '(lobos.ast ColumnDefinition
               ConstraintDefinition
               UniqueConstraintDefinition
               ForeignKeyConstraintDefinition
               CheckConstraintDefinition)))

Import all statement AST records into the calling namespace.

(defn import-statements
  []
  (import
   '(lobos.ast CreateSchemaStatement
               CreateTableStatement
               CreateIndexStatement
               DropStatement
               AlterTableStatement)))

Import all action AST records into the calling namespace.

(defn import-actions
  []
  (import
   '(lobos.ast AlterAddAction
               AlterDropAction
               AlterModifyAction
               AlterRenameAction)))

Import all AST records into the calling namespace.

(defn import-all
  []
  (import '(lobos.ast Mode))
  (import-expressions)
  (import-clauses)
  (import-definitions)
  (import-statements)
  (import-actions))