From 41ed38ea3847eec215cf4f08f3bd3ac36b72e023 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Fri, 12 Feb 2021 21:50:22 -0800 Subject: [PATCH] Finish and document DDL --- doc/clause-reference.md | 73 +++++++++++++++++++++++++++++++++++++ doc/special-syntax.md | 63 +++++++++++++++++++++++++++++--- src/honey/sql.cljc | 75 ++++++++++++++++++++++++++++---------- src/honey/sql/helpers.cljc | 1 + 4 files changed, 187 insertions(+), 25 deletions(-) diff --git a/doc/clause-reference.md b/doc/clause-reference.md index becbb85..3b9bc5d 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -13,8 +13,62 @@ dialects that HoneySQL supports. ## alter-table, add-column, drop-column, modify-column, rename-column +`:alter-table` can accept either a single table name or +a sequence that begins with a table name and is followed +by clauses that manipulate columns (or indices, see below). + +If a single table name is provided, a single column +(or index) operation can provided in the hash map DSL: + +```clojure +user=> (sql/format {:alter-table :fruit + :add-column [:id :int [:not nil]]}) +["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"] +user=> (sql/format {:alter-table :fruit + :drop-column :ident}) +["ALTER TABLE fruit DROP COLUMN ident"] +user=> (sql/format {:alter-table :fruit + :modify-column [:id :int :unsigned nil]}) +["ALTER TABLE fruit MODIFY COLUMN id INT UNSIGNED NULL"] +user=> (sql/format {:alter-table :fruit + :rename-column [:look :appearance]}) +["ALTER TABLE fruit RENAME COLUMN look TO appearance"] +``` + +If a sequence of a table name and various clauses is +provided, the generated `ALTER` statement will have +comma-separated clauses: + +```clojure +user=> (sql/format {:alter-table [:fruit + {:add-column [:id :int [:not nil]]} + {:drop-column :ident}]}) +["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident"] +``` + +As can be seen above, `:add-column` and `:modify-column` +both accept a column description (as a sequence of simple +expressions); `:drop-column` accepts a single column name, +and `:rename-column` accepts a sequence with two column +names: the "from" and the "to" names. + ## add-index, drop-index +`:add-index` accepts a single (function) expression +that describes an index, and `:drop-index` accepts a +single index name: + +```clojure +user=> (sql/format {:alter-table :fruit + :add-index [:index :look :appearance]}) +["ALTER TABLE fruit ADD INDEX look(appearance)"] +user=> (sql/format {:alter-table :fruit + :add-index [:unique nil :color :appearance]}) +["ALTER TABLE fruit ADD UNIQUE(color,appearance)"] +user=> (sql/format {:alter-table :fruit :drop-index :look}) +["ALTER TABLE fruit DROP INDEX look"] +``` + ## create-table, with-columns `:create-table` can accept a single table name or a pair @@ -38,6 +92,17 @@ user=> (sql/format {:create-table :fruit )"] ``` +The `:with-columns` clause is formatted as if `{:inline true}` +was specified so nothing is parameterized. In addition, +everything except the first element of a column description +will be uppercased (mostly to give the appearance of separating +the column name from the SQL keywords). + +Various function-like expressions can be specified, as shown +in the example above, but allow things like `CHECK` for a +constraint, `FOREIGN KEY` (with a column name), `REFERENCES` +(with a pair of column names). See [special-syntax.md#clause-descriptors](Clause Descriptors in Special Syntax) for more details. + ## create-view `:create-view` accepts a single view name: @@ -66,6 +131,14 @@ user=> (sql/format {:drop-table [:foo :bar]}) ## rename-table +`:rename-table` accepts a pair of the "from" table name +and the "to" table names: + +```clojure +user=> (sql/format {:rename-table [:fruit :vegetable]}) +["RENAME TABLE fruit TO vegetable"] +``` + ## nest This is pseudo-syntax that lets you wrap a substatement diff --git a/doc/special-syntax.md b/doc/special-syntax.md index df6f088..88e1a24 100644 --- a/doc/special-syntax.md +++ b/doc/special-syntax.md @@ -4,6 +4,8 @@ This section lists the function-like expressions that HoneySQL supports out of the box which are formatted as special syntactic forms. +The first group are used for SQL expressions. The second (last group) are used primarily in column definitions (as part of `:with-columns` and `:add-column` / `:modify-column`). + ## array Accepts a single argument, which is expected to evaluate to @@ -58,12 +60,6 @@ expression (comma-separated, wrapped in parentheses): ;;=> ["(a, b, ?, x + ?)" "red" 1] ``` -## default - -Takes no arguments and produces the SQL keyword `DEFAULT`. - -_[I expect this to be expanded for PostgreSQL]_ - ## inline Accepts a single argument and tries to render it as a @@ -186,3 +182,58 @@ parameters from them: (sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]}) ;;=> ["SELECT a, @var := ?" "foo"] ``` + +## Column Descriptors + +There are three types of descriptors that vary +in how they treat their first argument. All three +descriptors automatically try to inline any parameters +(and will throw an exception if they can't, since these +descriptors are meant to be used in column or index +specifications). + +### foreign-key, primary-key + +If no arguments are provided, these render as just SQL +keywords (uppercase): + +```clojure +[:foreign-key] ;=> FOREIGN KEY +[:primary-key] ;=> PRIMARY KEY +``` + +Otherwise, these render as regular function calls: + +```clojure +[:foreign-key :a] ;=> FOREIGN KEY(a) +[:primary-key :x :y] ;=> PRIMARY KEY(x,y) +``` + +## constraint, default, references + +Although these are grouped together, they are generally +used differently. This group renders as SQL keywords if +no arguments are provided. If a single argument is +provided, this renders as a SQL keyword followed by the +argument. If two or more arguments are provided, this +renders as a SQL keyword followed by the first argument, +followed by the rest as a regular argument list: + +```clojure +[:default] ;=> DEFAULT +[:default 42] ;=> DEFAULT 42 +[:default "str"] ;=> DEFAULT 'str' +[:constraint :name] ;=> CONSTRAINT name +[:references :foo :bar] ;=> REFERENCES foo(bar) +``` + +## index, unique + +These behave like the group above except that if the +first argument is `nil`, it is omitted: + +```clojure +[:index :foo :bar :quux] ;=> INDEX foo(bar,quux) +[:index nil :bar :quux] ;=> INDEX(bar,quux) +[:unique :a :b] ;=> UNIQUE a(b) +``` diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 43afb9b..a378d38 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -37,7 +37,7 @@ (def ^:private default-clause-order "The (default) order for known clauses. Can have items added and removed." [;; DDL comes first (these don't really have a precedence): - :alter-table :add-column :drop-column :rename-column + :alter-table :add-column :drop-column :modify-column :rename-column :add-index :drop-index :rename-table :create-table :with-columns :create-view :drop-table ;; then SQL clauses in priority order: @@ -469,11 +469,12 @@ (format-set-exprs k x))) (defn- format-simple-clause [c] - (let [[x & y] (format-dsl c)] - (when (seq y) - (throw (ex-info "column/index operations must be simple clauses" - {:clause c :params y}))) - x)) + (binding [*inline* true] + (let [[x & y] (format-dsl c)] + (when (seq y) + (throw (ex-info "column/index operations must be simple clauses" + {:clause c :params y}))) + x))) (defn- format-alter-table [k x] (if (sequential? x) @@ -500,16 +501,16 @@ (str/join ", " (map #'format-entity tables)))])) (defn- format-simple-expr [e] - (let [[x & y] (format-expr e)] - (when (seq y) - (throw (ex-info "column elements must be simple expressions" - {:expr e :params y}))) - x)) + (binding [*inline* true] + (let [[x & y] (format-expr e)] + (when (seq y) + (throw (ex-info "column elements must be simple expressions" + {:expr e :params y}))) + x))) (defn- format-single-column [xs] - (binding [*inline* true] - (str/join " " (let [[id & spec] (map #'format-simple-expr xs)] - (cons id (map upper-case spec)))))) + (str/join " " (let [[id & spec] (map #'format-simple-expr xs)] + (cons id (map upper-case spec))))) (defn- format-table-columns [k xs] [(str "(\n " @@ -540,8 +541,10 @@ (atom {:alter-table #'format-alter-table :add-column #'format-add-item :drop-column #'format-selector + :modify-column #'format-add-item :rename-column #'format-rename-item - :add-index #'format-add-item + ;; so :add-index works with both [:index] and [:unique] + :add-index (fn [_ x] (format-on-expr :add x)) :drop-index #'format-selector :rename-table #'format-rename-item :create-table #'format-create-table @@ -676,9 +679,46 @@ (into params-x) (into params-y))))) +(defn- function-0 [k xs] + [(str (sql-kw k) + (when (seq xs) + (str "(" (str/join "," (map #'format-simple-expr xs)) ")")))]) + +(defn- function-1 [k xs] + [(str (sql-kw k) + (when (seq xs) + (str " " (format-simple-expr (first xs)) + (when-let [args (next xs)] + (str "(" (str/join "," (map #'format-simple-expr args)) ")")))))]) + +(defn- function-1-opt [k xs] + [(str (sql-kw k) + (when (seq xs) + (str (when-let [e (first xs)] + (str " " (format-simple-expr e))) + (when-let [args (next xs)] + (str "(" (str/join "," (map #'format-simple-expr args)) ")")))))]) + (def ^:private special-syntax (atom - {:array + {;; these "functions" are mostly used in column + ;; descriptions so they generally have one of two forms: + ;; function-0 - with zero arguments, renders as a keyword, + ;; otherwise renders as a function call + ;; function-1 - with zero arguments, renders as a keyword, + ;; with one argument, as a keyword followed by an entity, + ;; otherwise renders as a keyword followed by a function + ;; call using the first entity as the function + ;; function-1-opt - like function-1 except if the first + ;; argument is nil, it is omitted + :constraint #'function-1 + :default #'function-1 + :foreign-key #'function-0 + :index #'function-1-opt + :primary-key #'function-0 + :references #'function-1 + :unique #'function-1-opt + :array (fn [_ [arr]] (let [[sqls params] (format-expr-list arr)] (into [(str "ARRAY[" (str/join ", " sqls) "]")] params))) @@ -720,9 +760,6 @@ (fn [_ [& args]] (let [[sqls params] (format-expr-list args)] (into [(str "(" (str/join ", " sqls) ")")] params))) - :default - (fn [_ []] - ["DEFAULT"]) :inline (fn [_ [x]] (if (sequential? x) diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index 13a2dc4..dd727a9 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -46,6 +46,7 @@ (defn alter-table [& args] (generic :alter-table args)) (defn add-column [& args] (generic :add-column args)) (defn drop-column [& args] (generic-1 :drop-column args)) +(defn modify-column [& args] (generic :modify-column args)) (defn rename-column [& args] (generic :rename-column args)) (defn add-index [& args] (generic :add-index args)) (defn drop-index [& args] (generic-1 :drop-index args))