Merge pull request #346 from lread/lread-issue-290
Test code blocks in docs with test-doc-blocks
This commit is contained in:
commit
06e9917daa
11 changed files with 351 additions and 170 deletions
31
README.md
31
README.md
|
|
@ -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,7 +560,7 @@ 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"]]]}
|
||||||
```
|
```
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
34
build.clj
34
build.clj
|
|
@ -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]
|
||||||
|
|
|
||||||
31
build/honey/gen_doc_tests.clj
Normal file
31
build/honey/gen_doc_tests.clj
Normal 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"))))
|
||||||
17
deps.edn
17
deps.edn
|
|
@ -13,7 +13,7 @@
|
||||||
: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"}}
|
||||||
|
|
@ -22,7 +22,18 @@
|
||||||
;; 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\"]}"]}}}
|
||||||
|
|
|
||||||
|
|
@ -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`:
|
||||||
|
|
|
||||||
|
|
@ -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]]})
|
||||||
|
|
|
||||||
|
|
@ -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] ..))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
(require '[honey.sql.helpers :refer [select filter within-group from order-by where]])
|
||||||
|
|
||||||
|
(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]
|
||||||
```
|
```
|
||||||
|
|
||||||
## inline
|
## inline
|
||||||
|
|
@ -214,14 +221,14 @@ 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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue