diff --git a/doc/clause-reference.md b/doc/clause-reference.md index 22e77a2..becbb85 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -11,12 +11,45 @@ a space (e.g., `:left-join` is formatted as `LEFT JOIN`). Except as noted, these clauses apply to all the SQL dialects that HoneySQL supports. -## alter-table +## alter-table, add-column, drop-column, modify-column, rename-column -## create-table +## add-index, drop-index + +## create-table, with-columns + +`:create-table` can accept a single table name or a pair +containing a table name and a flag indicating the creation +should be conditional (`:if-not-exists` or the symbol `if-not-exists`, +although any truthy value will work). `:create-table` should +be used with `:with-columns` to specify the actual columns +in the table: + +```clojure +user=> (sql/format {:create-table :fruit + :with-columns + [[:id :int [:not nil]] + [:name [:varchar 32] [:not nil]] + [:cost :float :null]]}) +;; \n has been replaced by an actual newline here for clarity: +["CREATE TABLE fruit ( + id INT NOT NULL, + name VARCHAR(32) NOT NULL, + cost FLOAT NULL +)"] +``` ## create-view +`:create-view` accepts a single view name: + +```clojure +user=> (sql/format {:create-view :products + :select [:*] + :from [:items] + :where [:= :category "product"]}) +["CREATE VIEW products AS SELECT * FROM items WHERE category = ?" "product"] +``` + ## drop-table `:drop-table` can accept a single table name or a sequence of @@ -31,6 +64,8 @@ user=> (sql/format {:drop-table [:foo :bar]}) ["DROP TABLE foo, bar"] ``` +## rename-table + ## nest This is pseudo-syntax that lets you wrap a substatement diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 118274c..43afb9b 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -37,7 +37,9 @@ (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 :create-table :with-columns :create-view :drop-table + :alter-table :add-column :drop-column :rename-column + :add-index :drop-index :rename-table + :create-table :with-columns :create-view :drop-table ;; then SQL clauses in priority order: :nest :with :with-recursive :intersect :union :union-all :except :except-all :select :select-distinct :insert-into :update :delete :delete-from :truncate @@ -466,7 +468,19 @@ [(str (sql-kw k) " " e " = EXCLUDED." e)]) (format-set-exprs k x))) -(defn- format-alter-table [k [x]] ["ALTER TABLE"]) +(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)) + +(defn- format-alter-table [k x] + (if (sequential? x) + [(str (sql-kw k) " " (format-entity (first x)) + (when-let [clauses (next x)] + (str " " (str/join ", " (map #'format-simple-clause clauses)))))] + [(str (sql-kw k) " " (format-entity x))])) (defn- format-create-table [k table] (let [[table if-not-exists] (if (sequential? table) table [table])] @@ -485,21 +499,28 @@ (when if-exists (str (sql-kw :if-exists) " ")) (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)) + +(defn- format-single-column [xs] + (binding [*inline* true] + (str/join " " (let [[id & spec] (map #'format-simple-expr xs)] + (cons id (map upper-case spec)))))) + (defn- format-table-columns [k xs] - (let [simple-expr (fn [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] - [(str "(\n " - (str/join ",\n " - (map #(str/join " " - (let [[id & spec] (map simple-expr %)] - (cons id (map upper-case spec)))) - xs)) - "\n)")]))) + [(str "(\n " + (str/join ",\n " (map #'format-single-column xs)) + "\n)")]) + +(defn- format-add-item [k spec] + [(str (sql-kw k) " " (format-single-column spec))]) + +(defn- format-rename-item [k [x y]] + [(str (sql-kw k) " " (format-entity x) " TO " (format-entity y))]) (def ^:private base-clause-order "The (base) order for known clauses. Can have items added and removed. @@ -517,6 +538,12 @@ "The (default) behavior for each known clause. Can also have items added and removed." (atom {:alter-table #'format-alter-table + :add-column #'format-add-item + :drop-column #'format-selector + :rename-column #'format-rename-item + :add-index #'format-add-item + :drop-index #'format-selector + :rename-table #'format-rename-item :create-table #'format-create-table :with-columns #'format-table-columns :create-view #'format-create-view diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index 97c6bfe..13a2dc4 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -43,11 +43,17 @@ (assoc data k arg) (assoc {} k data))) -(defn alter-table [& args] (generic :nest args)) -(defn create-table [& args] (generic :nest args)) -(defn with-columns [& args] (generic :nest args)) -(defn create-view [& args] (generic :nest args)) -(defn drop-table [& args] (generic :nest args)) +(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 rename-column [& args] (generic :rename-column args)) +(defn add-index [& args] (generic :add-index args)) +(defn drop-index [& args] (generic-1 :drop-index args)) +(defn rename-table [& args] (generic :alter-table args)) +(defn create-table [& args] (generic :create-table args)) +(defn with-columns [& args] (generic :with-columns args)) +(defn create-view [& args] (generic-1 :create-view args)) +(defn drop-table [& args] (generic :drop-table args)) (defn nest [& args] (generic :nest args)) (defn with [& args] (generic :with args)) (defn with-recursive [& args] (generic :with-recursive args)) diff --git a/test/honey/sql/helpers_test.cljc b/test/honey/sql/helpers_test.cljc index 27a9f78..34dc372 100644 --- a/test/honey/sql/helpers_test.cljc +++ b/test/honey/sql/helpers_test.cljc @@ -6,13 +6,13 @@ :cljs [cljs.test :refer-macros [deftest is testing]]) [honey.sql :as sql] [honey.sql.helpers - :refer [columns create-view - cross-join do-update-set from full-join + :refer [add-column add-index alter-table columns create-table create-view + cross-join do-update-set drop-column drop-index drop-table from full-join group-by having insert-into join left-join limit offset on-conflict order-by over partition-by - returning right-join - select select-distinct values where window with]])) + rename-column rename-table returning right-join + select select-distinct values where window with with-columns]])) (deftest test-select (let [m1 (-> (with [:cte (-> (select :*) @@ -331,13 +331,38 @@ " MAX(salary) OVER () AS MaxSalary" " FROM employee")]))) -(deftest issue-293-ddl +(deftest issue-293-basic-ddl (is (= (sql/format {:create-view :metro :select [:*] :from [:cities] :where [:= :metroflag "y"]}) ["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"])) (is (= (sql/format {:create-table :films :with-columns [[:id :int :unsigned :auto-increment] [:name [:varchar 50] [:not nil]]]}) ["CREATE TABLE films (\n id INT UNSIGNED AUTO_INCREMENT,\n name VARCHAR(50) NOT NULL\n)"])) + (is (= (sql/format (-> (create-view :metro) + (select :*) + (from :cities) + (where [:= :metroflag "y"]))) + ["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"])) + (is (= (sql/format (-> (create-table :films) + (with-columns + [:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]))) + ["CREATE TABLE films (\n id INT UNSIGNED AUTO_INCREMENT,\n name VARCHAR(50) NOT NULL\n)"])) + (is (= (sql/format (-> (create-table :films :if-not-exists) + (with-columns + [:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]))) + ["CREATE TABLE IF NOT EXISTS films (\n id INT UNSIGNED AUTO_INCREMENT,\n name VARCHAR(50) NOT NULL\n)"])) + (is (= (sql/format (-> {:create-table :films + :with-columns + [[:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]]})) + ["CREATE TABLE films (\n id INT UNSIGNED AUTO_INCREMENT,\n name VARCHAR(50) NOT NULL\n)"])) + (is (= (sql/format (-> {:create-table [:films :if-not-exists] + :with-columns + [[:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]]})) + ["CREATE TABLE IF NOT EXISTS films (\n id INT UNSIGNED AUTO_INCREMENT,\n name VARCHAR(50) NOT NULL\n)"])) (is (= (sql/format {:drop-table :foo}) ["DROP TABLE foo"])) (is (= (sql/format {:drop-table [:if-exists :foo]}) @@ -347,8 +372,25 @@ (is (= (sql/format {:drop-table [:foo :bar]}) ["DROP TABLE foo, bar"])) (is (= (sql/format {:drop-table [:if-exists :foo :bar]}) + ["DROP TABLE IF EXISTS foo, bar"])) + (is (= (sql/format (drop-table :foo)) + ["DROP TABLE foo"])) + (is (= (sql/format (drop-table :if-exists :foo)) + ["DROP TABLE IF EXISTS foo"])) + (is (= (sql/format (drop-table :foo :bar)) + ["DROP TABLE foo, bar"])) + (is (= (sql/format (drop-table :if-exists :foo :bar)) ["DROP TABLE IF EXISTS foo, bar"]))) +(deftest issue-293-alter-table + (is (= (sql/format (-> (alter-table :fruit) + (add-column :id :int [:not nil]))) + ["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"])) + (is (= (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"]))) + (deftest issue-293-insert-into-data ;; insert into as (and other tests) based on :insert-into ;; examples in the clause reference docs: