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
All sample code in this README is automatically run as a unit test using
[seancorfield/readme](https://github.com/seancorfield/readme).
Sample code in this documentation is verified via
[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.
@ -37,6 +37,8 @@ HoneySQL 1.x will continue to get critical security fixes but otherwise should b
## Usage
From Clojure:
<!-- {:test-doc-blocks/reader-cond :clj} -->
```clojure
(refer-clojure :exclude '[filter for group-by into partition-by set update])
(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])
```
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:
```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
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))
```
@ -352,7 +369,7 @@ VALUES (?, (?, ?)), (?, (?, ?))
Updates are possible too:
```clojure
(-> (h/update :films)
(-> (update :films)
(set {:kind "dramatic"
:watched [:+ :watched 1]})
(where [:= :kind "drama"])
@ -543,9 +560,9 @@ These can be combined to allow more fine-grained control over SQL generation:
```
```clojure
call-qualify-map
=> '{:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]]
:from (:foo)
:select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]}
=> {:where [:and [:= :a [:param :baz]] [:= :b [:inline 42]]]
:from (:foo)
:select [[[:foo :bar]] [[:raw "@var := foo.bar"]]]}
```
```clojure
(sql/format call-qualify-map {:params {:baz "BAZ"}})
@ -730,7 +747,9 @@ OFFSET ?
```
```clojure
;; 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
```

View file

@ -4,27 +4,53 @@
clojure -T:build run-tests
clojure -T:build run-tests :aliases '[:master]'
clojure -T:build run-doc-tests :aliases '[:cljs]'
clojure -T:build ci
For more information, run:
clojure -A:deps -T:build help/doc"
(:require [clojure.tools.build.api :as b]
[org.corfield.build :as bb]))
(def lib 'com.github.seancorfield/honeysql)
(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]
(-> 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]
(-> opts
(bb/clean)
(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)
(as-> opts
(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"}}}
;; running tests/checks of various kinds:
:test ; can also run clojure -X:test
:test
{:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.4.0" :git/sha "334f2e2"}}
:exec-fn cognitect.test-runner.api/test}
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.4.0" :git/sha "334f2e2"}}
:exec-fn cognitect.test-runner.api/test}
;; various "runners" for tests/CI:
:cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}}
: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"}}
: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.
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
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)"]
user=> (sql/format {:alter-table :fruit
: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})
["ALTER TABLE fruit DROP INDEX look"]
```
@ -108,12 +116,7 @@ user=> (sql/format {:create-table :fruit
[[:id :int [:not nil]]
[:name [:varchar 32] [:not nil]]
[: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}`
@ -544,25 +547,30 @@ user=> (sql/format {:select [:t.ref :pp.code]
[:using :id]]
:join [[:logtransaction :log]
[:= :t.id :log.id]]]
:where [:= "settled" :pp.status]})
;; newlines inserted for readability:
["SELECT t.ref, pp.code FROM transaction AS t
LEFT JOIN paypal_tx AS pp USING (id)
INNER JOIN logtransaction AS log ON t.id = log.id
WHERE ? = pp.status" "settled"]
;; or using helpers:
:where [:= "settled" :pp.status]}
{:pretty true})
["
SELECT t.ref, pp.code
FROM transaction AS t
LEFT JOIN paypal_tx AS pp USING (id) INNER JOIN logtransaction AS log ON t.id = log.id
WHERE ? = pp.status
" "settled"]
;; or the equivalent using helpers:
user=> (sql/format (-> (select :t.ref :pp.code)
(from [:transaction :t])
(join-by (left-join [:paypal-tx :pp]
[:using :id])
(join [:logtransaction :log]
[:= :t.id :log.id]))
(where := "settled" :pp.status)))
;; newlines inserted for readability:
["SELECT t.ref, pp.code FROM transaction AS t
LEFT JOIN paypal_tx AS pp USING (id)
INNER JOIN logtransaction AS log ON t.id = log.id
WHERE ? = pp.status" "settled"]
(where := "settled" :pp.status))
{:pretty true})
["
SELECT t.ref, pp.code
FROM transaction AS t
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`.
@ -667,25 +675,25 @@ user=> (sql/format {:select [:id
:w
:MaxSalary]]]]
:from [:employee]
:window [:w {:partition-by [:department]}]})
;; newlines inserted for readability:
["SELECT id,
AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,
MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department)"]
:window [:w {:partition-by [:department]}]}
{:pretty true})
["
SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department)
"]
;; easier to write with helpers (and easier to read!):
user=> (sql/format (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
[[:max :salary] :w :MaxSalary]))
(from :employee)
(window :w (partition-by :department))))
;; newlines inserted for readability:
["SELECT id,
AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,
MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department)"]
(window :w (partition-by :department)))
{:pretty true})
["
SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department)
"]
```
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
In `deps.edn`:
<!-- :test-doc-blocks/skip -->
```clojure
;; in deps.edn:
honeysql {:mvn/version "1.0.461"}
;; or, more correctly:
honeysql/honeysql {:mvn/version "1.0.461"}
```
;; in use:
Required as:
<!-- :test-doc-blocks/skip -->
```clojure
(ns my.project
(: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]})
;;=> ["SELECT * FROM table WHERE id = ?" 1]
(sql/format {:select [:*] :from [:table] :where [:= :id 1]} :quoting :mysql)
@ -47,15 +60,26 @@ Supported Clojure versions: 1.7 and later.
### HoneySQL 2.x
In `deps.edn`:
<!-- :test-doc-blocks/skip -->
```clojure
;; in deps.edn:
com.github.seancorfield/honeysql {:mvn/version "2.0.783"}
```
;; in use:
Required as:
<!-- :test-doc-blocks/skip -->
```clojure
(ns my.project
(: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]})
;;=> ["SELECT * FROM table WHERE id = ?" 1]
(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
;; 1.x: EXISTS should never have been implemented as SQL syntax: it's an operator!
;; (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:
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`:
```clojure
(require '[honey.sql :as sql])
(sql/register-op! :<=> :variadic true)
;; and then use the new operator:
(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]})
@ -86,6 +88,7 @@ The formatter function will be called with:
For example:
<!-- :test-doc-blocks/skip -->
```clojure
(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:
```clojure
(require '[honey.sql :as sql])
(sql/format {:select :foo-bar} {:quoted true})
;;=> ["SELECT \"foo-bar\""]
(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:
<!-- :test-doc-blocks/skip -->
```clojure
com.github.seancorfield/honeysql {:mvn/version "2.0.783"}
```
For Leiningen, add the following dependency to your `project.clj` file:
<!-- :test-doc-blocks/skip -->
```clojure
[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:
```clojure
(ns my.example
(:require [honey.sql :as sql]))
(require '[honey.sql :as sql])
(sql/format {:select [:*], :from [:table], :where [:= :id 1]})
;; produces:
;;=> ["SELECT * FROM table WHERE id = ?" 1]
```
@ -65,7 +65,6 @@ that represents a SQL entity and its alias (where aliases are allowed):
```clojure
(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]
```
@ -80,9 +79,10 @@ avoid evaluation:
```clojure
(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)})
;; also produces:
;;=> ["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`:
```clojure
;; notice the following both produce the same result:
(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]})
;; both produce:
;;=> ["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:
<!-- :test-doc-blocks/skip -->
```clojure
[:= :a 42] ;=> "a = ?" 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)
are split at `.` and turned into function calls:
<!-- :test-doc-blocks/skip -->
```clojure
%now ;=> NOW()
%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
qualified names in a function invocation:
<!-- :test-doc-blocks/skip -->
```clojure
%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
parameter list in order:
<!-- :test-doc-blocks/skip -->
```clojure
[: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]
:where [:= :a :?x]}
{:params {:x 42}})
["SELECT * FROM table WHERE a = ?" 42]
;;=> ["SELECT * FROM table WHERE a = ?" 42]
(sql/format {:select [:*] :from [:table]
:where [:= :a [:param :x]]}
{:params {:x 42}})
["SELECT * FROM table WHERE a = ?" 42]
;;=> ["SELECT * FROM table WHERE a = ?" 42]
```
## Functional Helpers
@ -210,15 +214,13 @@ SQL queries with raw Clojure data structures, a
is also available. These functions are generally variadic and threadable:
```clojure
(ns my.example
(:require [honey.sql :as sql]
[honey.sql.helpers :refer [select from where]]))
(require '[honey.sql :as sql]
'[honey.sql.helpers :refer [select from where]])
(-> (select :t/id [:name :item])
(from [:table :t])
(where [:= :id 1])
(sql/format))
;; produces:
;;=> ["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])
(select [:name :item])
(sql/format))
;; produces:
;;=> ["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)
(select [:name :item])
(sql/format))
;; produces:
;;=> ["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
`: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
(sql/format '{select (id) from (table)} {:quoted true})
;;=> ["SELECT \"id\" FROM \"table\""]
@ -323,6 +325,11 @@ specify a dialect in the `format` call, you can specify
;;=> nil
(sql/format '{select (id) from (table)} {:quoted true})
;;=> ["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,

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
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
Upserting data is relatively easy in PostgreSQL
@ -34,11 +51,16 @@ user=> (-> (insert-into :distributors)
(upsert (-> (on-conflict :did)
(do-update-set :dname)))
(returning :*)
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?)
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *"
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?), (?, ?)
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
@ -51,11 +73,16 @@ user=> (-> (insert-into :distributors)
(on-conflict :did)
(do-update-set :dname)
(returning :*)
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?)
ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *"
5 "Gizmo Transglobal" 6 "Associated Computing, Inc"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?), (?, ?)
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
@ -66,11 +93,14 @@ user=> (-> (insert-into :distributors)
(values [{:did 7 :dname "Redline GmbH"}])
(upsert (-> (on-conflict :did)
do-nothing))
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?)
ON CONFLICT (did) DO NOTHING"
7 "Redline GmbH"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
ON CONFLICT (did)
DO NOTHING
"
7 "Redline GmbH"]
```
As above, the nested `upsert` helper is no longer needed:
@ -80,11 +110,14 @@ user=> (-> (insert-into :distributors)
(values [{:did 7 :dname "Redline GmbH"}])
(on-conflict :did)
do-nothing
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?)
ON CONFLICT (did) DO NOTHING"
7 "Redline GmbH"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
ON CONFLICT (did)
DO NOTHING
"
7 "Redline GmbH"]
```
`ON CONSTRAINT` is handled slightly differently to the nilenso library,
@ -96,22 +129,29 @@ user=> (-> (insert-into :distributors)
;; can specify as a nested clause...
(on-conflict (on-constraint :distributors_pkey))
do-nothing
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?)
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING"
9 "Antwerp Design"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
ON CONFLICT ON CONSTRAINT distributors_pkey
DO NOTHING
"
9 "Antwerp Design"]
user=> (-> (insert-into :distributors)
(values [{:did 9 :dname "Antwerp Design"}])
;; ...or as two separate clauses
on-conflict
(on-constraint :distributors_pkey)
do-nothing
sql/format)
;; newlines inserted for readability:
["INSERT INTO distributors (did, dname) VALUES (?, ?)
ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING"
9 "Antwerp Design"]
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
ON CONFLICT
ON CONSTRAINT distributors_pkey
DO NOTHING
"
9 "Antwerp Design"]
```
As above, the `upsert` helper has been omitted here.
@ -124,12 +164,14 @@ user=> (-> (insert-into :user)
(values [{:phone "5555555" :name "John"}])
(on-conflict :phone (where [:<> :phone nil]))
(do-update-set :phone :name (where [:= :user.active false]))
sql/format)
;; newlines inserted for readability:
["INSERT INTO user (phone, name) VALUES (?, ?)
ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name
WHERE user.active = FALSE" "5555555" "John"]
(sql/format {:pretty true}))
["
INSERT INTO user
(phone, name) VALUES (?, ?)
ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
"
"5555555" "John"]
;; using the DSL directly:
user=> (sql/format
{:insert-into :user
@ -137,16 +179,20 @@ user=> (sql/format
:on-conflict [:phone
{:where [:<> :phone nil]}]
:do-update-set {:fields [:phone :name]
:where [:= :user.active false]}})
;; newlines inserted for readability:
["INSERT INTO user (phone, name) VALUES (?, ?)
ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name
WHERE user.active = FALSE" "5555555" "John"]
:where [:= :user.active false]}}
{:pretty true})
["
INSERT INTO user
(phone, name) VALUES (?, ?)
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:
<!-- :test-doc-blocks/skip -->
```clojure
;; NOT VALID FOR HONEYSQL!
{:insert-into :user
@ -230,12 +276,12 @@ user=> (-> (create-table :distributors)
;; "serial" is inlined as 'SERIAL':
[:default [:nextval "serial"]]]
[:name [:varchar 40] [:not nil]]])
sql/format)
(sql/format {:pretty true}))
;; newlines inserted for readability:
["CREATE TABLE distributors (
did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'),
name VARCHAR(40) NOT NULL
)"]
["
CREATE TABLE distributors
(did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)
"]
;; PostgreSQL CHECK constraint is supported:
user=> (-> (create-table :products)
(with-columns [[:product_no :integer]
@ -243,20 +289,16 @@ user=> (-> (create-table :products)
[:price :numeric [:check [:> :price 0]]]
[:discounted_price :numeric]
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
sql/format)
;; newlines inserted for readability:
["CREATE TABLE products (
product_no INTEGER,
name TEXT,
price NUMERIC CHECK(PRICE > 0),
discounted_price NUMERIC,
CHECK((discounted_price > 0) AND (price > discounted_price))
)"]
(sql/format {:pretty true}))
["
CREATE TABLE products
(product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))
"]
;; conditional creation:
user=> (-> (create-table :products :if-not-exists)
...
(with-columns [[:name :text]])
sql/format)
["CREATE TABLE IF NOT EXISTS products (...)"]
["CREATE TABLE IF NOT EXISTS products (name TEXT)"]
;; drop table:
user=> (sql/format (drop-table :cities))
["DROP TABLE cities"]
@ -339,10 +381,7 @@ user=> (-> (alter-table :fruit)
user=> (sql/format (alter-table :fruit
(add-column :skin [:varchar 16] nil)
(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

View file

@ -13,6 +13,8 @@ a sequence, and produces `ARRAY[?, ?, ..]` for the elements
of that sequence (as SQL parameters):
```clojure
(require '[honey.sql :as sql])
(sql/format-expr [:array (range 5)])
;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4]
```
@ -36,8 +38,7 @@ may be `:else` (or `'else`) to produce `ELSE`, otherwise
```clojure
(sql/format-expr [:case [:< :a 10] "small" [:> :a 100] "big" :else "medium"])
;;=> ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END"
;; 10 "small" 100 "big" "medium"]
;; => ["CASE WHEN a < ? THEN ? WHEN a > ? THEN ? ELSE ? END" 10 "small" 100 "big" "medium"]
```
## 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
constructing DDL statements.
<!-- :test-doc-blocks/skip -->
```clojure
[:tablespace :quux]
;;=> TABLESPACE QUUX
@ -89,9 +91,9 @@ Intended to be used with regular expression patterns to
specify the escape characters (if any).
```clojure
(format {:select :* :from :foo
:where [:similar-to :foo [:escape "bar" [:inline "*"]]]})
;;=> ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"]))))
(sql/format {:select :* :from :foo
:where [:similar-to :foo [:escape "bar" [:inline "*"]]]})
;;=> ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"]
```
## 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.
```clojure
(format {:select [:a :b [[:filter :%count.* {:where [:< :x 100]}] :c]
[[:within-group [:percentile_disc [:inline 0.25]]
{:order-by [:a]}] :inter_max]
[[:within-group [:percentile_cont [:inline 0.25]]
{:order-by [:a]}] :abs_max]]
:from :aa})
;; newlines added for readability:
;;=> ["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]
(sql/format {:select [:a :b [[:filter :%count.* {:where [:< :x 100]}] :c]
[[:within-group [:percentile_disc [:inline 0.25]]
{:order-by [:a]}] :inter_max]
[[:within-group [:percentile_cont [:inline 0.25]]
{:order-by [:a]}] :abs_max]]
:from :aa}
{:pretty true})
;;=> ["
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]
```
There are helpers for both `filter` and `within-group`. Be careful with `filter`
since it shadows `clojure.core/filter`:
```clojure
(format (-> (select :a :b [(filter :%count.* (where :< :x 100)) :c]
[(within-group [:percentile_disc [:inline 0.25]]
(order-by :a)) :inter_max]
[(within-group [:percentile_cont [:inline 0.25]]
(order-by :a)) :abs_max])
(from :aa)))
;; newlines added for readability:
;;=> ["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]
(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]]
(order-by :a)) :inter_max]
[(within-group [:percentile_cont [:inline 0.25]]
(order-by :a)) :abs_max])
(from :aa))
{:pretty true})
;;=> ["
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
@ -214,15 +221,15 @@ by an ordering specifier, which can be an expression or a pair of expression
and direction (`:asc` or `:desc`):
```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"]
(format (-> (select [[:array_agg [:order-by :a [:b :desc]]]])
(sql/format (-> (select [[:array_agg [:order-by :a [: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"]
(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"]
```
@ -285,7 +292,7 @@ parameters from them:
(sql/format {:select [:a [[:raw ["@var := " [:inline "foo"]]]]]})
;;=> ["SELECT a, @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.
@ -304,6 +311,7 @@ specifications).
If no arguments are provided, these render as just SQL
keywords (uppercase):
<!-- :test-doc-blocks/skip -->
```clojure
[:foreign-key] ;=> FOREIGN KEY
[:primary-key] ;=> PRIMARY KEY
@ -311,6 +319,7 @@ keywords (uppercase):
Otherwise, these render as regular function calls:
<!-- :test-doc-blocks/skip -->
```clojure
[:foreign-key :a] ;=> FOREIGN KEY(a)
[: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,
followed by the rest as a regular argument list:
<!-- :test-doc-blocks/skip -->
```clojure
[:default] ;=> DEFAULT
[: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
first argument is `nil`, it is omitted:
<!-- :test-doc-blocks/skip -->
```clojure
[:index :foo :bar :quux] ;=> INDEX foo(bar, quux)
[:index nil :bar :quux] ;=> INDEX(bar, quux)