Merge pull request #346 from lread/lread-issue-290

Test code blocks in docs with test-doc-blocks
This commit is contained in:
Sean Corfield 2021-08-30 19:15:16 -07:00 committed by GitHub
commit 06e9917daa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 170 deletions

View file

@ -24,8 +24,8 @@ for copying and pasting directly into your SQL tool of choice!
## Note on code samples ## Note on code samples
All sample code in this README is automatically run as a unit test using Sample code in this documentation is verified via
[seancorfield/readme](https://github.com/seancorfield/readme). [lread/test-doc-blocks](https://github.com/lread/test-doc-blocks).
Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings. Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings.
@ -37,6 +37,8 @@ HoneySQL 1.x will continue to get critical security fixes but otherwise should b
## Usage ## Usage
From Clojure:
<!-- {:test-doc-blocks/reader-cond :clj} -->
```clojure ```clojure
(refer-clojure :exclude '[filter for group-by into partition-by set update]) (refer-clojure :exclude '[filter for group-by into partition-by set update])
(require '[honey.sql :as sql] (require '[honey.sql :as sql]
@ -51,6 +53,20 @@ HoneySQL 1.x will continue to get critical security fixes but otherwise should b
'[clojure.core :as c]) '[clojure.core :as c])
``` ```
From ClojureScript, we don't have `:refer :all`. If we want to use `:refer`, we have no choice but to be specific:
<!-- {:test-doc-blocks/reader-cond :cljs} -->
```Clojure
(refer-clojure :exclude '[filter for group-by into partition-by set update])
(require '[honey.sql :as sql]
'[honey.sql.helpers :refer [select select-distinct from
join left-join right-join
where for group-by having union
order-by limit offset values columns
update insert-into set composite
delete delete-from truncate] :as h]
'[clojure.core :as c])
```
Everything is built on top of maps representing SQL queries: Everything is built on top of maps representing SQL queries:
```clojure ```clojure
@ -78,7 +94,8 @@ HoneySQL is a relatively "pure" library, it does not manage your JDBC connection
or run queries for you, it simply generates SQL strings. You can then pass them or run queries for you, it simply generates SQL strings. You can then pass them
to a JDBC library, such as [`next.jdbc`](https://github.com/seancorfield/next-jdbc): to a JDBC library, such as [`next.jdbc`](https://github.com/seancorfield/next-jdbc):
```clj <!-- :test-doc-blocks/skip -->
```clojure
(jdbc/execute! conn (sql/format sqlmap)) (jdbc/execute! conn (sql/format sqlmap))
``` ```
@ -352,7 +369,7 @@ VALUES (?, (?, ?)), (?, (?, ?))
Updates are possible too: Updates are possible too:
```clojure ```clojure
(-> (h/update :films) (-> (update :films)
(set {:kind "dramatic" (set {:kind "dramatic"
:watched [:+ :watched 1]}) :watched [:+ :watched 1]})
(where [:= :kind "drama"]) (where [:= :kind "drama"])
@ -543,9 +560,9 @@ These can be combined to allow more fine-grained control over SQL generation:
``` ```
```clojure ```clojure
call-qualify-map call-qualify-map
=> '{:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]] => {:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]]
:from (:foo) :from (:foo)
:select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]} :select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]}
``` ```
```clojure ```clojure
(sql/format call-qualify-map {:params {:baz "BAZ"}}) (sql/format call-qualify-map {:params {:baz "BAZ"}})
@ -730,7 +747,9 @@ OFFSET ?
``` ```
```clojure ```clojure
;; Printable and readable ;; Printable and readable
(= big-complicated-map (read-string (pr-str big-complicated-map))) (require '[clojure.edn :as edn])
(= big-complicated-map (edn/read-string (pr-str big-complicated-map)))
=> true => true
``` ```

View file

@ -4,27 +4,53 @@
clojure -T:build run-tests clojure -T:build run-tests
clojure -T:build run-tests :aliases '[:master]' clojure -T:build run-tests :aliases '[:master]'
clojure -T:build run-doc-tests :aliases '[:cljs]'
clojure -T:build ci clojure -T:build ci
For more information, run: For more information, run:
clojure -A:deps -T:build help/doc" clojure -A:deps -T:build help/doc"
(:require [clojure.tools.build.api :as b] (:require [clojure.tools.build.api :as b]
[org.corfield.build :as bb])) [org.corfield.build :as bb]))
(def lib 'com.github.seancorfield/honeysql) (def lib 'com.github.seancorfield/honeysql)
(def version (format "2.0.%s" (b/git-count-revs nil))) (def version (format "2.0.%s" (b/git-count-revs nil)))
(defn readme "Run the README tests." [opts]
(-> opts (bb/run-task [:readme])))
(defn eastwood "Run Eastwood." [opts] (defn eastwood "Run Eastwood." [opts]
(-> opts (bb/run-task [:eastwood]))) (-> opts (bb/run-task [:eastwood])))
(defn gen-doc-tests "Generate tests from doc code blocks." [opts]
(-> opts (bb/run-task [:gen-doc-tests])))
(defn run-doc-tests
"Generate and run doc tests.
Optionally specify :aliases vector:
[:1.9] -- test against Clojure 1.9 (the default)
[:1.10] -- test against Clojure 1.10.3
[:master] -- test against Clojure 1.11 master snapshot
[:cljs] -- test against ClojureScript"
[{:keys [aliases] :as opts}]
(gen-doc-tests opts)
(bb/run-tests (assoc opts :aliases
(-> [:test-doc]
(into aliases)
(into (if (some #{:cljs} aliases)
[:test-doc-cljs]
[:test-doc-clj])))))
opts)
(defn ci "Run the CI pipeline of tests (and build the JAR)." [opts] (defn ci "Run the CI pipeline of tests (and build the JAR)." [opts]
(-> opts (-> opts
(bb/clean)
(assoc :lib lib :version version) (assoc :lib lib :version version)
(readme) (as-> opts
(reduce (fn [opts alias]
(run-doc-tests (assoc opts :aliases [alias])))
opts
[:cljs :1.9 :1.10 :master]))
(eastwood) (eastwood)
(as-> opts (as-> opts
(reduce (fn [opts alias] (reduce (fn [opts alias]

View file

@ -0,0 +1,31 @@
(ns honey.gen-doc-tests
(:require [babashka.fs :as fs]
[lread.test-doc-blocks :as tdb]))
(defn -main [& _args]
(let [target "target/test-doc-blocks"
success-marker (fs/file target "SUCCESS")
docs ["README.md"
"doc/clause-reference.md"
"doc/differences-from-1-x.md"
"doc/extending-honeysql.md"
"doc/general-reference.md"
"doc/getting-started.md"
"doc/postgresql.md"
"doc/special-syntax.md"]
regen-reason (if (not (fs/exists? success-marker))
"a previous successful gen result not found"
(let [newer-thans (fs/modified-since target
(concat docs
["build.clj" "deps.edn"]
(fs/glob "build" "**/*.*")
(fs/glob "src" "**/*.*")))]
(when (seq newer-thans)
(str "found files newer than last gen: " (mapv str newer-thans)))))]
(if regen-reason
(do
(fs/delete-if-exists success-marker)
(println "gen-doc-tests: Regenerating:" regen-reason)
(tdb/gen-tests {:docs docs})
(spit success-marker "SUCCESS"))
(println "gen-doc-tests: Tests already successfully generated"))))

View file

@ -13,16 +13,27 @@
:master {:override-deps {org.clojure/clojure {:mvn/version "1.11.1-master-SNAPSHOT"}}} :master {:override-deps {org.clojure/clojure {:mvn/version "1.11.1-master-SNAPSHOT"}}}
;; running tests/checks of various kinds: ;; running tests/checks of various kinds:
:test ; can also run clojure -X:test :test
{:extra-paths ["test"] {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner :extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.4.0" :git/sha "334f2e2"}} {:git/tag "v0.4.0" :git/sha "334f2e2"}}
:exec-fn cognitect.test-runner.api/test} :exec-fn cognitect.test-runner.api/test}
;; various "runners" for tests/CI: ;; various "runners" for tests/CI:
:cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}} :cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}}
:main-opts ["-m" "cljs-test-runner.main"]} :main-opts ["-m" "cljs-test-runner.main"]}
:readme {:extra-deps {seancorfield/readme {:mvn/version "1.0.16"}}
:main-opts ["-m" "seancorfield.readme"]} :gen-doc-tests {:replace-paths ["build"]
:extra-deps {babashka/fs {:mvn/version "0.0.5"}
com.github.lread/test-doc-blocks {:mvn/version "1.0.146-alpha"}}
:main-opts ["-m" "honey.gen-doc-tests"]}
:test-doc {:replace-paths ["src" "target/test-doc-blocks/test"]}
:test-doc-clj {:main-opts ["-m" "cognitect.test-runner"
"-d" "target/test-doc-blocks/test"]}
:test-doc-cljs {:main-opts ["-m" "cljs-test-runner.main"
"-c" "{:warnings,{:single-segment-namespace,false}}"
"-d" "target/test-doc-blocks/test"]}
:eastwood {:extra-deps {jonase/eastwood {:mvn/version "0.9.9"}} :eastwood {:extra-deps {jonase/eastwood {:mvn/version "0.9.9"}}
:main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}} :main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}}

View file

@ -14,6 +14,14 @@ dialects that HoneySQL supports.
DDL clauses are listed first, followed by SQL clauses. DDL clauses are listed first, followed by SQL clauses.
The examples herein assume:
```clojure
(refer-clojure :exclude '[partition-by])
(require '[honey.sql :as sql]
'[honey.sql.helpers :refer [select from join-by left-join join
where order-by over partition-by window]])
```
# DDL Clauses # DDL Clauses
HoneySQL supports the following DDL clauses as a data DSL. HoneySQL supports the following DDL clauses as a data DSL.
@ -76,7 +84,7 @@ user=> (sql/format {:alter-table :fruit
["ALTER TABLE fruit ADD INDEX look(appearance)"] ["ALTER TABLE fruit ADD INDEX look(appearance)"]
user=> (sql/format {:alter-table :fruit user=> (sql/format {:alter-table :fruit
:add-index [:unique nil :color :appearance]}) :add-index [:unique nil :color :appearance]})
["ALTER TABLE fruit ADD UNIQUE(color,appearance)"] ["ALTER TABLE fruit ADD UNIQUE(color, appearance)"]
user=> (sql/format {:alter-table :fruit :drop-index :look}) user=> (sql/format {:alter-table :fruit :drop-index :look})
["ALTER TABLE fruit DROP INDEX look"] ["ALTER TABLE fruit DROP INDEX look"]
``` ```
@ -108,12 +116,7 @@ user=> (sql/format {:create-table :fruit
[[:id :int [:not nil]] [[:id :int [:not nil]]
[:name [:varchar 32] [:not nil]] [:name [:varchar 32] [:not nil]]
[:cost :float :null]]}) [:cost :float :null]]})
;; reformatted for clarity: ["CREATE TABLE fruit (id INT NOT NULL, name VARCHAR(32) NOT NULL, cost FLOAT NULL)"]
["CREATE TABLE fruit (
id INT NOT NULL,
name VARCHAR(32) NOT NULL,
cost FLOAT NULL
)"]
``` ```
The `:with-columns` clause is formatted as if `{:inline true}` The `:with-columns` clause is formatted as if `{:inline true}`
@ -544,25 +547,30 @@ user=> (sql/format {:select [:t.ref :pp.code]
[:using :id]] [:using :id]]
:join [[:logtransaction :log] :join [[:logtransaction :log]
[:= :t.id :log.id]]] [:= :t.id :log.id]]]
:where [:= "settled" :pp.status]}) :where [:= "settled" :pp.status]}
;; newlines inserted for readability: {:pretty true})
["SELECT t.ref, pp.code FROM transaction AS t ["
LEFT JOIN paypal_tx AS pp USING (id) SELECT t.ref, pp.code
INNER JOIN logtransaction AS log ON t.id = log.id FROM transaction AS t
WHERE ? = pp.status" "settled"] LEFT JOIN paypal_tx AS pp USING (id) INNER JOIN logtransaction AS log ON t.id = log.id
;; or using helpers: WHERE ? = pp.status
" "settled"]
;; or the equivalent using helpers:
user=> (sql/format (-> (select :t.ref :pp.code) user=> (sql/format (-> (select :t.ref :pp.code)
(from [:transaction :t]) (from [:transaction :t])
(join-by (left-join [:paypal-tx :pp] (join-by (left-join [:paypal-tx :pp]
[:using :id]) [:using :id])
(join [:logtransaction :log] (join [:logtransaction :log]
[:= :t.id :log.id])) [:= :t.id :log.id]))
(where := "settled" :pp.status))) (where := "settled" :pp.status))
;; newlines inserted for readability: {:pretty true})
["SELECT t.ref, pp.code FROM transaction AS t ["
LEFT JOIN paypal_tx AS pp USING (id) SELECT t.ref, pp.code
INNER JOIN logtransaction AS log ON t.id = log.id FROM transaction AS t
WHERE ? = pp.status" "settled"] LEFT JOIN paypal_tx AS pp USING (id) INNER JOIN logtransaction AS log ON t.id = log.id
WHERE ? = pp.status
" "settled"]
``` ```
Without `:join-by`, a `:join` would normally be generated before a `:left-join`. Without `:join-by`, a `:join` would normally be generated before a `:left-join`.
@ -667,25 +675,25 @@ user=> (sql/format {:select [:id
:w :w
:MaxSalary]]]] :MaxSalary]]]]
:from [:employee] :from [:employee]
:window [:w {:partition-by [:department]}]}) :window [:w {:partition-by [:department]}]}
;; newlines inserted for readability: {:pretty true})
["SELECT id, ["
AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
MAX(salary) OVER w AS MaxSalary FROM employee
FROM employee WINDOW w AS (PARTITION BY department)
WINDOW w AS (PARTITION BY department)"] "]
;; easier to write with helpers (and easier to read!): ;; easier to write with helpers (and easier to read!):
user=> (sql/format (-> (select :id user=> (sql/format (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average] (over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
[[:max :salary] :w :MaxSalary])) [[:max :salary] :w :MaxSalary]))
(from :employee) (from :employee)
(window :w (partition-by :department)))) (window :w (partition-by :department)))
;; newlines inserted for readability: {:pretty true})
["SELECT id, ["
AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
MAX(salary) OVER w AS MaxSalary FROM employee
FROM employee WINDOW w AS (PARTITION BY department)
WINDOW w AS (PARTITION BY department)"] "]
``` ```
The window function in the `:over` expression may be `{}` or `nil`: The window function in the `:over` expression may be `{}` or `nil`:

View file

@ -19,17 +19,30 @@ In addition, HoneySQL 2.x contains different namespaces so you can have both ver
### HoneySQL 1.x ### HoneySQL 1.x
In `deps.edn`:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
;; in deps.edn:
honeysql {:mvn/version "1.0.461"} honeysql {:mvn/version "1.0.461"}
;; or, more correctly: ;; or, more correctly:
honeysql/honeysql {:mvn/version "1.0.461"} honeysql/honeysql {:mvn/version "1.0.461"}
```
;; in use: Required as:
<!-- :test-doc-blocks/skip -->
```clojure
(ns my.project (ns my.project
(:require [honeysql.core :as sql])) (:require [honeysql.core :as sql]))
```
... Or if in the REPL:
<!-- :test-doc-blocks/skip -->
```clojure
(require '[honeysql.core :as sq])
```
In use:
<!-- :test-doc-blocks/skip -->
```clojure
(sql/format {:select [:*] :from [:table] :where [:= :id 1]}) (sql/format {:select [:*] :from [:table] :where [:= :id 1]})
;;=> ["SELECT * FROM table WHERE id = ?" 1] ;;=> ["SELECT * FROM table WHERE id = ?" 1]
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql) (sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql)
@ -47,15 +60,26 @@ Supported Clojure versions: 1.7 and later.
### HoneySQL 2.x ### HoneySQL 2.x
In `deps.edn`:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
;; in deps.edn:
com.github.seancorfield/honeysql {:mvn/version "2.0.783"} com.github.seancorfield/honeysql {:mvn/version "2.0.783"}
```
;; in use: Required as:
<!-- :test-doc-blocks/skip -->
```clojure
(ns my.project (ns my.project
(:require [honey.sql :as sql])) (:require [honey.sql :as sql]))
```
... Or if in the REPL:
```clojure
(require '[honey.sql :as sql])
```
In use:
```clojure
(sql/format {:select [:*] :from [:table] :where [:= :id 1]}) (sql/format {:select [:*] :from [:table] :where [:= :id 1]})
;;=> ["SELECT * FROM table WHERE id = ?" 1] ;;=> ["SELECT * FROM table WHERE id = ?" 1]
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}) (sql/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql})
@ -158,7 +182,7 @@ it should have been a function, and in 2.x it is:
```clojure ```clojure
;; 1.x: EXISTS should never have been implemented as SQL syntax: it's an operator! ;; 1.x: EXISTS should never have been implemented as SQL syntax: it's an operator!
;; (sq/format {:exists {:select [:a] :from [:foo]}}) ;; (sq/format {:exists {:select [:a] :from [:foo]}})
;;=> ["EXISTS (SELECT a FROM foo)"] ;; -> ["EXISTS (SELECT a FROM foo)"]
;; 2.x: select function call with an alias: ;; 2.x: select function call with an alias:
user=> (sql/format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]}) user=> (sql/format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]})

View file

@ -50,6 +50,8 @@ two arguments. You can optionally specify that an operator
can take any number of arguments with `:variadic true`: can take any number of arguments with `:variadic true`:
```clojure ```clojure
(require '[honey.sql :as sql])
(sql/register-op! :<=> :variadic true) (sql/register-op! :<=> :variadic true)
;; and then use the new operator: ;; and then use the new operator:
(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]}) (sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]})
@ -86,6 +88,7 @@ The formatter function will be called with:
For example: For example:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
(sql/register-fn! :foo (fn [f args] ..)) (sql/register-fn! :foo (fn [f args] ..))

View file

@ -12,6 +12,8 @@ because `:quoted true` was specified, the literal name of an unqualified,
single-segment keyword or symbol is used as-is and quoted: single-segment keyword or symbol is used as-is and quoted:
```clojure ```clojure
(require '[honey.sql :as sql])
(sql/format {:select :foo-bar} {:quoted true}) (sql/format {:select :foo-bar} {:quoted true})
;;=> ["SELECT \"foo-bar\""] ;;=> ["SELECT \"foo-bar\""]
(sql/format {:select :foo-bar} {:dialect :mysql}) (sql/format {:select :foo-bar} {:dialect :mysql})

View file

@ -8,12 +8,14 @@ data to a SQL statement (string) and any parameters it needs.
For the Clojure CLI, add the following dependency to your `deps.edn` file: For the Clojure CLI, add the following dependency to your `deps.edn` file:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
com.github.seancorfield/honeysql {:mvn/version "2.0.783"} com.github.seancorfield/honeysql {:mvn/version "2.0.783"}
``` ```
For Leiningen, add the following dependency to your `project.clj` file: For Leiningen, add the following dependency to your `project.clj` file:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[com.github.seancorfield/honeysql "2.0.783"] [com.github.seancorfield/honeysql "2.0.783"]
``` ```
@ -41,11 +43,9 @@ SQL string as the first element followed by any parameter
values identified in the SQL expressions: values identified in the SQL expressions:
```clojure ```clojure
(ns my.example (require '[honey.sql :as sql])
(:require [honey.sql :as sql]))
(sql/format {:select [:*], :from [:table], :where [:= :id 1]}) (sql/format {:select [:*], :from [:table], :where [:= :id 1]})
;; produces:
;;=> ["SELECT * FROM table WHERE id = ?" 1] ;;=> ["SELECT * FROM table WHERE id = ?" 1]
``` ```
@ -65,7 +65,6 @@ that represents a SQL entity and its alias (where aliases are allowed):
```clojure ```clojure
(sql/format {:select [:t.id [:name :item]], :from [[:table :t]], :where [:= :id 1]}) (sql/format {:select [:t.id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
;; produces:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -80,9 +79,10 @@ avoid evaluation:
```clojure ```clojure
(sql/format '{select [t.id [name item]], from [[table t]], where [= id 1]}) (sql/format '{select [t.id [name item]], from [[table t]], where [= id 1]})
;; or you can use (..) instead of [..] when quoted: ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
;; or you can use (..) instead of [..] when quoted to produce the same result:
(sql/format '{select (t.id (name item)), from ((table t)), where (= id 1)}) (sql/format '{select (t.id (name item)), from ((table t)), where (= id 1)})
;; also produces:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -91,10 +91,10 @@ keywords (or symbols) and the namespace portion will treated as
the table name, i.e., `:foo/bar` instead of `:foo.bar`: the table name, i.e., `:foo/bar` instead of `:foo.bar`:
```clojure ```clojure
;; notice the following both produce the same result:
(sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]}) (sql/format {:select [:t/id [:name :item]], :from [[:table :t]], :where [:= :id 1]})
;; and ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
(sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]}) (sql/format '{select [t/id [name item]], from [[table t]], where [= id 1]})
;; both produce:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -116,6 +116,7 @@ described in the [Special Syntax](special-syntax.md) section.
Some examples: Some examples:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:= :a 42] ;=> "a = ?" with a parameter of 42 [:= :a 42] ;=> "a = ?" with a parameter of 42
[:+ 42 :a :b] ;=> "? + a + b" with a parameter of 42 [:+ 42 :a :b] ;=> "? + a + b" with a parameter of 42
@ -133,6 +134,7 @@ Another form of special syntax that is treated as function calls
is keywords or symbols that begin with `%`. Such keywords (or symbols) is keywords or symbols that begin with `%`. Such keywords (or symbols)
are split at `.` and turned into function calls: are split at `.` and turned into function calls:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
%now ;=> NOW() %now ;=> NOW()
%count.* ;=> COUNT(*) %count.* ;=> COUNT(*)
@ -143,6 +145,7 @@ are split at `.` and turned into function calls:
If you need to reference a table or alias for a column, you can use If you need to reference a table or alias for a column, you can use
qualified names in a function invocation: qualified names in a function invocation:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
%max.foo/bar ;=> MAX(foo.bar) %max.foo/bar ;=> MAX(foo.bar)
``` ```
@ -179,6 +182,7 @@ that are not keywords or symbols are lifted out as positional parameters.
They are replaced by `?` in the generated SQL string and added to the They are replaced by `?` in the generated SQL string and added to the
parameter list in order: parameter list in order:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20 [:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20
``` ```
@ -195,11 +199,11 @@ call as the `:params` key of the options hash map.
(sql/format {:select [:*] :from [:table] (sql/format {:select [:*] :from [:table]
:where [:= :a :?x]} :where [:= :a :?x]}
{:params {:x 42}}) {:params {:x 42}})
["SELECT * FROM table WHERE a = ?" 42] ;;=> ["SELECT * FROM table WHERE a = ?" 42]
(sql/format {:select [:*] :from [:table] (sql/format {:select [:*] :from [:table]
:where [:= :a [:param :x]]} :where [:= :a [:param :x]]}
{:params {:x 42}}) {:params {:x 42}})
["SELECT * FROM table WHERE a = ?" 42] ;;=> ["SELECT * FROM table WHERE a = ?" 42]
``` ```
## Functional Helpers ## Functional Helpers
@ -210,15 +214,13 @@ SQL queries with raw Clojure data structures, a
is also available. These functions are generally variadic and threadable: is also available. These functions are generally variadic and threadable:
```clojure ```clojure
(ns my.example (require '[honey.sql :as sql]
(:require [honey.sql :as sql] '[honey.sql.helpers :refer [select from where]])
[honey.sql.helpers :refer [select from where]]))
(-> (select :t/id [:name :item]) (-> (select :t/id [:name :item])
(from [:table :t]) (from [:table :t])
(where [:= :id 1]) (where [:= :id 1])
(sql/format)) (sql/format))
;; produces:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -238,7 +240,6 @@ can make it easier to build queries programmatically:
(where [:= :id 1]) (where [:= :id 1])
(select [:name :item]) (select [:name :item])
(sql/format)) (sql/format))
;; produces:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -252,7 +253,6 @@ you need to explicitly remove the prior value:
(dissoc :select) (dissoc :select)
(select [:name :item]) (select [:name :item])
(sql/format)) (sql/format))
;; produces:
;;=> ["SELECT name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT name AS item FROM table AS t WHERE id = ?" 1]
``` ```
@ -314,6 +314,8 @@ dialect in a `format` call, they will be quoted. If you don't
specify a dialect in the `format` call, you can specify specify a dialect in the `format` call, you can specify
`:quoted true` to have SQL entities quoted. `:quoted true` to have SQL entities quoted.
<!-- Reminder to doc author:
Reset dialect to default so other blocks are not affected for test-doc-blocks -->
```clojure ```clojure
(sql/format '{select (id) from (table)} {:quoted true}) (sql/format '{select (id) from (table)} {:quoted true})
;;=> ["SELECT \"id\" FROM \"table\""] ;;=> ["SELECT \"id\" FROM \"table\""]
@ -323,6 +325,11 @@ specify a dialect in the `format` call, you can specify
;;=> nil ;;=> nil
(sql/format '{select (id) from (table)} {:quoted true}) (sql/format '{select (id) from (table)} {:quoted true})
;;=> ["SELECT [id] FROM [table]"] ;;=> ["SELECT [id] FROM [table]"]
;; and to the default of :ansi
(sql/set-dialect! :ansi)
;;=> nil
(sql/format '{select (id) from (table)} {:quoted true})
;;=> ["SELECT \"id\" FROM \"table\""]
``` ```
Out of the box, as part of the extended ANSI SQL support, Out of the box, as part of the extended ANSI SQL support,

View file

@ -18,6 +18,23 @@ HoneySQL not to do that. There are two possible approaches:
1. Use named parameters (e.g., `[:param :myval]`) instead of having the values directly in the DSL structure and then pass `{:params {:myval some-json}}` as part of the options in the call to `format`, or 1. Use named parameters (e.g., `[:param :myval]`) instead of having the values directly in the DSL structure and then pass `{:params {:myval some-json}}` as part of the options in the call to `format`, or
2. Use `[:lift ..]` wrapped around any structured values which tells HoneySQL not to interpret the vector or hash map value as a DSL: `[:lift some-json]`. 2. Use `[:lift ..]` wrapped around any structured values which tells HoneySQL not to interpret the vector or hash map value as a DSL: `[:lift some-json]`.
The code example herein assume:
```clojure
(refer-clojure :exclude '[update set])
(require '[honey.sql :as sql]
'[honey.sql.helpers :refer [select from where
update set
insert-into values
create-table with-columns create-view create-extension
add-column alter-table add-index
modify-column rename-column rename-table
drop-table drop-column drop-index drop-extension
upsert returning on-conflict on-constraint
do-update-set do-nothing]])
```
Clojure users can opt for the shorter `(require '[honey.sql :as sql] '[honey.sql.helpers :refer :all])` but this syntax is not available to ClojureScript users.
## Upsert ## Upsert
Upserting data is relatively easy in PostgreSQL Upserting data is relatively easy in PostgreSQL
@ -34,11 +51,16 @@ user=> (-> (insert-into :distributors)
(upsert (-> (on-conflict :did) (upsert (-> (on-conflict :did)
(do-update-set :dname))) (do-update-set :dname)))
(returning :*) (returning :*)
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) INSERT INTO distributors
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *" (did, dname) VALUES (?, ?), (?, ?)
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"] ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname
RETURNING *
"
5 "Gizmo Transglobal"
6 "Associated Computing, Inc"]
``` ```
However, the nested `upsert` helper is no longer needed However, the nested `upsert` helper is no longer needed
@ -51,11 +73,16 @@ user=> (-> (insert-into :distributors)
(on-conflict :did) (on-conflict :did)
(do-update-set :dname) (do-update-set :dname)
(returning :*) (returning :*)
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) INSERT INTO distributors
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *" (did, dname) VALUES (?, ?), (?, ?)
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"] ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname
RETURNING *
"
5 "Gizmo Transglobal"
6 "Associated Computing, Inc"]
``` ```
Similarly, the `do-nothing` helper behaves just the same Similarly, the `do-nothing` helper behaves just the same
@ -66,11 +93,14 @@ user=> (-> (insert-into :distributors)
(values [{:did 7 :dname "Redline GmbH"}]) (values [{:did 7 :dname "Redline GmbH"}])
(upsert (-> (on-conflict :did) (upsert (-> (on-conflict :did)
do-nothing)) do-nothing))
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?) INSERT INTO distributors
ON CONFLICT (did) DO NOTHING" (did, dname) VALUES (?, ?)
7 "Redline GmbH"] ON CONFLICT (did)
DO NOTHING
"
7 "Redline GmbH"]
``` ```
As above, the nested `upsert` helper is no longer needed: As above, the nested `upsert` helper is no longer needed:
@ -80,11 +110,14 @@ user=> (-> (insert-into :distributors)
(values [{:did 7 :dname "Redline GmbH"}]) (values [{:did 7 :dname "Redline GmbH"}])
(on-conflict :did) (on-conflict :did)
do-nothing do-nothing
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?) INSERT INTO distributors
ON CONFLICT (did) DO NOTHING" (did, dname) VALUES (?, ?)
7 "Redline GmbH"] ON CONFLICT (did)
DO NOTHING
"
7 "Redline GmbH"]
``` ```
`ON CONSTRAINT` is handled slightly differently to the nilenso library, `ON CONSTRAINT` is handled slightly differently to the nilenso library,
@ -96,22 +129,29 @@ user=> (-> (insert-into :distributors)
;; can specify as a nested clause... ;; can specify as a nested clause...
(on-conflict (on-constraint :distributors_pkey)) (on-conflict (on-constraint :distributors_pkey))
do-nothing do-nothing
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?) INSERT INTO distributors
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" (did, dname) VALUES (?, ?)
9 "Antwerp Design"] ON CONFLICT ON CONSTRAINT distributors_pkey
DO NOTHING
"
9 "Antwerp Design"]
user=> (-> (insert-into :distributors) user=> (-> (insert-into :distributors)
(values [{:did 9 :dname "Antwerp Design"}]) (values [{:did 9 :dname "Antwerp Design"}])
;; ...or as two separate clauses ;; ...or as two separate clauses
on-conflict on-conflict
(on-constraint :distributors_pkey) (on-constraint :distributors_pkey)
do-nothing do-nothing
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO distributors (did, dname) VALUES (?, ?) INSERT INTO distributors
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" (did, dname) VALUES (?, ?)
9 "Antwerp Design"] ON CONFLICT
ON CONSTRAINT distributors_pkey
DO NOTHING
"
9 "Antwerp Design"]
``` ```
As above, the `upsert` helper has been omitted here. As above, the `upsert` helper has been omitted here.
@ -124,12 +164,14 @@ user=> (-> (insert-into :user)
(values [{:phone "5555555" :name "John"}]) (values [{:phone "5555555" :name "John"}])
(on-conflict :phone (where [:<> :phone nil])) (on-conflict :phone (where [:<> :phone nil]))
(do-update-set :phone :name (where [:= :user.active false])) (do-update-set :phone :name (where [:= :user.active false]))
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["INSERT INTO user (phone, name) VALUES (?, ?) INSERT INTO user
ON CONFLICT (phone) WHERE phone IS NOT NULL (phone, name) VALUES (?, ?)
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name ON CONFLICT (phone) WHERE phone IS NOT NULL
WHERE user.active = FALSE" "5555555" "John"] DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
"
"5555555" "John"]
;; using the DSL directly: ;; using the DSL directly:
user=> (sql/format user=> (sql/format
{:insert-into :user {:insert-into :user
@ -137,16 +179,20 @@ user=> (sql/format
:on-conflict [:phone :on-conflict [:phone
{:where [:<> :phone nil]}] {:where [:<> :phone nil]}]
:do-update-set {:fields [:phone :name] :do-update-set {:fields [:phone :name]
:where [:= :user.active false]}}) :where [:= :user.active false]}}
;; newlines inserted for readability: {:pretty true})
["INSERT INTO user (phone, name) VALUES (?, ?) ["
ON CONFLICT (phone) WHERE phone IS NOT NULL INSERT INTO user
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name (phone, name) VALUES (?, ?)
WHERE user.active = FALSE" "5555555" "John"] ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
"
"5555555" "John"]
``` ```
By comparison, this is the DSL structure that nilenso would have required: By comparison, this is the DSL structure that nilenso would have required:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
;; NOT VALID FOR HONEYSQL! ;; NOT VALID FOR HONEYSQL!
{:insert-into :user {:insert-into :user
@ -230,12 +276,12 @@ user=> (-> (create-table :distributors)
;; "serial" is inlined as 'SERIAL': ;; "serial" is inlined as 'SERIAL':
[:default [:nextval "serial"]]] [:default [:nextval "serial"]]]
[:name [:varchar 40] [:not nil]]]) [:name [:varchar 40] [:not nil]]])
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ;; newlines inserted for readability:
["CREATE TABLE distributors ( ["
did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), CREATE TABLE distributors
name VARCHAR(40) NOT NULL (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)
)"] "]
;; PostgreSQL CHECK constraint is supported: ;; PostgreSQL CHECK constraint is supported:
user=> (-> (create-table :products) user=> (-> (create-table :products)
(with-columns [[:product_no :integer] (with-columns [[:product_no :integer]
@ -243,20 +289,16 @@ user=> (-> (create-table :products)
[:price :numeric [:check [:> :price 0]]] [:price :numeric [:check [:> :price 0]]]
[:discounted_price :numeric] [:discounted_price :numeric]
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]]) [[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
sql/format) (sql/format {:pretty true}))
;; newlines inserted for readability: ["
["CREATE TABLE products ( CREATE TABLE products
product_no INTEGER, (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))
name TEXT, "]
price NUMERIC CHECK(PRICE > 0),
discounted_price NUMERIC,
CHECK((discounted_price > 0) AND (price > discounted_price))
)"]
;; conditional creation: ;; conditional creation:
user=> (-> (create-table :products :if-not-exists) user=> (-> (create-table :products :if-not-exists)
... (with-columns [[:name :text]])
sql/format) sql/format)
["CREATE TABLE IF NOT EXISTS products (...)"] ["CREATE TABLE IF NOT EXISTS products (name TEXT)"]
;; drop table: ;; drop table:
user=> (sql/format (drop-table :cities)) user=> (sql/format (drop-table :cities))
["DROP TABLE cities"] ["DROP TABLE cities"]
@ -339,10 +381,7 @@ user=> (-> (alter-table :fruit)
user=> (sql/format (alter-table :fruit user=> (sql/format (alter-table :fruit
(add-column :skin [:varchar 16] nil) (add-column :skin [:varchar 16] nil)
(add-index :unique :fruit-name :name))) (add-index :unique :fruit-name :name)))
;; newlines inserted for readability: ["ALTER TABLE fruit ADD COLUMN skin VARCHAR(16) NULL, ADD UNIQUE fruit_name(name)"]
["ALTER TABLE fruit
ADD COLUMN skin VARCHAR(16) NULL,
ADD UNIQUE fruit_name(name)"]
``` ```
## Filter / Within Group ## Filter / Within Group

View file

@ -13,6 +13,8 @@ a sequence, and produces `ARRAY[?, ?, ..]` for the elements
of that sequence (as SQL parameters): of that sequence (as SQL parameters):
```clojure ```clojure
(require '[honey.sql :as sql])
(sql/format-expr [:array (range 5)]) (sql/format-expr [:array (range 5)])
;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4] ;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4]
``` ```
@ -36,8 +38,7 @@ may be `:else` (or `'else`) to produce `ELSE`, otherwise
```clojure ```clojure
(sql/format-expr [:case [:< :a 10] "small" [:> :a 100] "big" :else "medium"]) (sql/format-expr [:case [:< :a 10] "small" [:> :a 100] "big" :else "medium"])
;;=> ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END" ;; => ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END" 10 "small" 100 "big" "medium"]
;; 10 "small" 100 "big" "medium"]
``` ```
## cast ## cast
@ -76,6 +77,7 @@ SQL entity. This is intended for use in contexts that would
otherwise produce a sequence of SQL keywords, such as when otherwise produce a sequence of SQL keywords, such as when
constructing DDL statements. constructing DDL statements.
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:tablespace :quux] [:tablespace :quux]
;;=> TABLESPACE QUUX ;;=> TABLESPACE QUUX
@ -89,9 +91,9 @@ Intended to be used with regular expression patterns to
specify the escape characters (if any). specify the escape characters (if any).
```clojure ```clojure
(format {:select :* :from :foo (sql/format {:select :* :from :foo
:where [:similar-to :foo [:escape "bar" [:inline "*"]]]}) :where [:similar-to :foo [:escape "bar" [:inline "*"]]]})
;;=> ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"])))) ;;=> ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"]
``` ```
## filter, within-group ## filter, within-group
@ -104,34 +106,39 @@ Filter generally expects an aggregate expression and a `WHERE` clause.
Within group generally expects an aggregate expression and an `ORDER BY` clause. Within group generally expects an aggregate expression and an `ORDER BY` clause.
```clojure ```clojure
(format {:select [:a :b [[:filter :%count.* {:where [:< :x 100]}] :c] (sql/format {:select [:a :b [[:filter :%count.* {:where [:< :x 100]}] :c]
[[:within-group [:percentile_disc [:inline 0.25]] [[:within-group [:percentile_disc [:inline 0.25]]
{:order-by [:a]}] :inter_max] {:order-by [:a]}] :inter_max]
[[:within-group [:percentile_cont [:inline 0.25]] [[:within-group [:percentile_cont [:inline 0.25]]
{:order-by [:a]}] :abs_max]] {:order-by [:a]}] :abs_max]]
:from :aa}) :from :aa}
;; newlines added for readability: {:pretty true})
;;=> ["SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, ;;=> ["
;;=> PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max
;;=> PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max FROM aa
;;=> FROM aa" 100] "
100]
``` ```
There are helpers for both `filter` and `within-group`. Be careful with `filter` There are helpers for both `filter` and `within-group`. Be careful with `filter`
since it shadows `clojure.core/filter`: since it shadows `clojure.core/filter`:
```clojure ```clojure
(format (-> (select :a :b [(filter :%count.* (where :< :x 100)) :c] (refer-clojure :exclude '[filter])
[(within-group [:percentile_disc [:inline 0.25]] (require '[honey.sql.helpers :refer [select filter within-group from order-by where]])
(order-by :a)) :inter_max]
[(within-group [:percentile_cont [:inline 0.25]] (sql/format (-> (select :a :b [(filter :%count.* (where :< :x 100)) :c]
(order-by :a)) :abs_max]) [(within-group [:percentile_disc [:inline 0.25]]
(from :aa))) (order-by :a)) :inter_max]
;; newlines added for readability: [(within-group [:percentile_cont [:inline 0.25]]
;;=> ["SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, (order-by :a)) :abs_max])
;;=> PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, (from :aa))
;;=> PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max {:pretty true})
;;=> FROM aa" 100] ;;=> ["
SELECT a, b, COUNT(*) FILTER (WHERE x < ?) AS c, PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY a ASC) AS inter_max, PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY a ASC) AS abs_max
FROM aa
"
100]
``` ```
## inline ## inline
@ -214,15 +221,15 @@ by an ordering specifier, which can be an expression or a pair of expression
and direction (`:asc` or `:desc`): and direction (`:asc` or `:desc`):
```clojure ```clojure
(format {:select [[[:array_agg [:order-by :a [:b :desc]]]]] :from :table}) (sql/format {:select [[[:array_agg [:order-by :a [:b :desc]]]]] :from :table})
;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"] ;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"]
(format (-> (select [[:array_agg [:order-by :a [:b :desc]]]]) (sql/format (-> (select [[:array_agg [:order-by :a [:b :desc]]]])
(from :table))) (from :table)))
;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"] ;;=> ["SELECT ARRAY_AGG(a ORDER BY b DESC) FROM table"]
(format {:select [[[:string_agg :a [:order-by [:inline ","] :a]]]] :from :table}) (sql/format {:select [[[:string_agg :a [:order-by [:inline ","] :a]]]] :from :table})
;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"] ;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"]
(format (-> (select [[:string_agg :a [:order-by [:inline ","] :a]]]) (sql/format (-> (select [[:string_agg :a [:order-by [:inline ","] :a]]])
(from :table))) (from :table)))
;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"] ;;=> ["SELECT STRING_AGG(a, ',' ORDER BY a ASC) FROM table"]
``` ```
@ -285,7 +292,7 @@ parameters from them:
(sql/format {:select [:a [[:raw ["@var := " [:inline "foo"]]]]]}) (sql/format {:select [:a [[:raw ["@var := " [:inline "foo"]]]]]})
;;=> ["SELECT a, @var := 'foo'"] ;;=> ["SELECT a, @var := 'foo'"]
(sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]}) (sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]})
;;=> ["SELECT a, @var := ?" "foo"] ;;=> ["SELECT a, @var := (?)" "foo"]
``` ```
`:raw` is also supported as a SQL clause for the same reason. `:raw` is also supported as a SQL clause for the same reason.
@ -304,6 +311,7 @@ specifications).
If no arguments are provided, these render as just SQL If no arguments are provided, these render as just SQL
keywords (uppercase): keywords (uppercase):
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:foreign-key] ;=> FOREIGN KEY [:foreign-key] ;=> FOREIGN KEY
[:primary-key] ;=> PRIMARY KEY [:primary-key] ;=> PRIMARY KEY
@ -311,6 +319,7 @@ keywords (uppercase):
Otherwise, these render as regular function calls: Otherwise, these render as regular function calls:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:foreign-key :a] ;=> FOREIGN KEY(a) [:foreign-key :a] ;=> FOREIGN KEY(a)
[:primary-key :x :y] ;=> PRIMARY KEY(x, y) [:primary-key :x :y] ;=> PRIMARY KEY(x, y)
@ -326,6 +335,7 @@ argument. If two or more arguments are provided, this
renders as a SQL keyword followed by the first argument, renders as a SQL keyword followed by the first argument,
followed by the rest as a regular argument list: followed by the rest as a regular argument list:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:default] ;=> DEFAULT [:default] ;=> DEFAULT
[:default 42] ;=> DEFAULT 42 [:default 42] ;=> DEFAULT 42
@ -339,6 +349,7 @@ followed by the rest as a regular argument list:
These behave like the group above except that if the These behave like the group above except that if the
first argument is `nil`, it is omitted: first argument is `nil`, it is omitted:
<!-- :test-doc-blocks/skip -->
```clojure ```clojure
[:index :foo :bar :quux] ;=> INDEX foo(bar, quux) [:index :foo :bar :quux] ;=> INDEX foo(bar, quux)
[:index nil :bar :quux] ;=> INDEX(bar, quux) [:index nil :bar :quux] ;=> INDEX(bar, quux)