Compare commits
104 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5dbc274be | ||
|
|
df753e8635 | ||
|
|
b4b2ca7d79 | ||
|
|
78f7d5282f | ||
|
|
024d17b11e | ||
|
|
7611871935 | ||
|
|
1b042687f4 | ||
|
|
a981ed9171 | ||
|
|
74cf16c134 | ||
|
|
2c8fc30b1d | ||
|
|
3906aa53c0 | ||
|
|
92e4e16b45 | ||
|
|
30b5fabe58 | ||
|
|
d74046c658 | ||
|
|
44494e61c0 | ||
|
|
d70e89ae3b | ||
|
|
67ea477a5c | ||
|
|
89f39be55c | ||
|
|
0d1fd0e901 | ||
|
|
675c94b294 | ||
|
|
03d96f5747 | ||
|
|
f7dbfba57c | ||
|
|
f0eb68f151 | ||
|
|
4d1f5f83b7 | ||
|
|
c2990597f1 | ||
|
|
695351e33c | ||
|
|
0cd81b5d9b | ||
|
|
c295db44c0 | ||
|
|
44ca426b78 | ||
|
|
e3fcb3e278 | ||
|
|
7e1fe8f558 | ||
|
|
13eb8fe859 | ||
|
|
4bc1d16f24 | ||
|
|
206f980093 | ||
|
|
3beaa6b2bf | ||
|
|
30a04975f5 | ||
|
|
4f5b0ed256 | ||
|
|
1681764830 | ||
|
|
6c0c66e371 | ||
|
|
2c793ce441 | ||
|
|
f05c7051e2 | ||
|
|
d086631e54 | ||
|
|
3ecac63bea | ||
|
|
8e0d6984bd | ||
|
|
94fae3437f | ||
|
|
316f36751f | ||
|
|
8ae93d91f6 | ||
|
|
5fa85400f0 | ||
|
|
045634fd3c | ||
|
|
81167cb77e | ||
|
|
a3b79215c4 | ||
|
|
0cbe76329e | ||
|
|
c93eef06f6 | ||
|
|
60f5662d81 | ||
|
|
6531413325 | ||
|
|
fce39548d0 | ||
|
|
0f26e7d060 | ||
|
|
c98df6dd97 | ||
|
|
7a24fd0367 | ||
|
|
30d177165d | ||
|
|
e4762a1a70 | ||
|
|
e0356bc9c5 | ||
|
|
aa1f2bc0f6 | ||
|
|
0272c7b9ed | ||
|
|
10ec823151 | ||
|
|
fdfc6bc997 | ||
|
|
3f1677bff2 | ||
|
|
782bc4b78a | ||
|
|
f2763d5af5 | ||
|
|
573d6c75ca | ||
|
|
42d5f4baf1 | ||
|
|
8320571c4d | ||
|
|
559e71205d | ||
|
|
1bac4352e3 | ||
|
|
b64ab9b0b0 | ||
|
|
049fe5b68b | ||
|
|
b716d077c4 | ||
|
|
09fa8afefe | ||
|
|
129239a742 | ||
|
|
ee53c54255 | ||
|
|
f4d212ae18 | ||
|
|
e2f7991ad8 | ||
|
|
c0c455358f | ||
|
|
488ddd4bcb | ||
|
|
b2c1ae0068 | ||
|
|
21ce3a2242 | ||
|
|
6fa606ffd5 | ||
|
|
c1c7cba96a | ||
|
|
4992a3cb76 | ||
|
|
bbac863a2a | ||
|
|
44399b1984 | ||
|
|
4288ceae56 | ||
|
|
38080aff92 | ||
|
|
9999a90e62 | ||
|
|
7892ec6006 | ||
|
|
362818530a | ||
|
|
643cea4930 | ||
|
|
b271a898f5 | ||
|
|
35545facce | ||
|
|
170602e85f | ||
|
|
c6e6b54b8f | ||
|
|
9dba3860e2 | ||
|
|
a187ba98f1 | ||
|
|
203e923f99 |
35 changed files with 1592 additions and 506 deletions
1
.clj-kondo/imports/babashka/fs/config.edn
Normal file
1
.clj-kondo/imports/babashka/fs/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}
|
||||||
3
.clj-kondo/imports/http-kit/http-kit/config.edn
Normal file
3
.clj-kondo/imports/http-kit/http-kit/config.edn
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
{:hooks
|
||||||
|
{:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
(ns httpkit.with-channel
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(defn with-channel [{node :node}]
|
||||||
|
(let [[request channel & body] (rest (:children node))]
|
||||||
|
(when-not (and request channel) (throw (ex-info "No request or channel provided" {})))
|
||||||
|
(when-not (api/token-node? channel) (throw (ex-info "Missing channel argument" {})))
|
||||||
|
(let [new-node
|
||||||
|
(api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'let)
|
||||||
|
(api/vector-node [channel (api/vector-node [])])
|
||||||
|
request
|
||||||
|
body))]
|
||||||
|
|
||||||
|
{:node new-node})))
|
||||||
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal file
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{:lint-as
|
||||||
|
{rewrite-clj.zip/subedit-> clojure.core/->
|
||||||
|
rewrite-clj.zip/subedit->> clojure.core/->>
|
||||||
|
rewrite-clj.zip/edit-> clojure.core/->
|
||||||
|
rewrite-clj.zip/edit->> clojure.core/->>}}
|
||||||
1
.clj-kondo/imports/taoensso/encore/config.edn
Normal file
1
.clj-kondo/imports/taoensso/encore/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:hooks {:analyze-call {taoensso.encore/defalias taoensso.encore/defalias}}}
|
||||||
16
.clj-kondo/imports/taoensso/encore/taoensso/encore.clj
Normal file
16
.clj-kondo/imports/taoensso/encore/taoensso/encore.clj
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
(ns taoensso.encore
|
||||||
|
(:require
|
||||||
|
[clj-kondo.hooks-api :as hooks]))
|
||||||
|
|
||||||
|
(defn defalias [{:keys [node]}]
|
||||||
|
(let [[sym-raw src-raw] (rest (:children node))
|
||||||
|
src (if src-raw src-raw sym-raw)
|
||||||
|
sym (if src-raw
|
||||||
|
sym-raw
|
||||||
|
(symbol (name (hooks/sexpr src))))]
|
||||||
|
{:node (with-meta
|
||||||
|
(hooks/list-node
|
||||||
|
[(hooks/token-node 'def)
|
||||||
|
(hooks/token-node (hooks/sexpr sym))
|
||||||
|
(hooks/token-node (hooks/sexpr src))])
|
||||||
|
(meta src))}))
|
||||||
2
.github/workflows/test-and-release.yml
vendored
2
.github/workflows/test-and-release.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.12.0.1479'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
4
.github/workflows/test-and-snapshot.yml
vendored
4
.github/workflows/test-and-snapshot.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.12.0.1479'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -49,7 +49,7 @@ jobs:
|
||||||
- name: Clojure CLI
|
- name: Clojure CLI
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.12.0.1479'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
33
.github/workflows/test-bb.yml
vendored
Normal file
33
.github/workflows/test-bb.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Babashka tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 21
|
||||||
|
- name: Clojure CLI
|
||||||
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
|
with:
|
||||||
|
cli: '1.12.0.1530'
|
||||||
|
bb: latest
|
||||||
|
- name: Cache All The Things
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.m2/repository
|
||||||
|
~/.gitlibs
|
||||||
|
~/.clojure
|
||||||
|
~/.cpcache
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn', '**/bb.edn') }}
|
||||||
|
- name: Run Tests
|
||||||
|
run: bb test
|
||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: Clojure CLI
|
- name: Clojure CLI
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.12.0.1479'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
23
.gitpod.yml
23
.gitpod.yml
|
|
@ -1,23 +0,0 @@
|
||||||
image:
|
|
||||||
file: .gitpod.dockerfile
|
|
||||||
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- betterthantomorrow.calva
|
|
||||||
- mauricioszabo.clover
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Prepare deps/clover
|
|
||||||
init: |
|
|
||||||
clojure -A:test -P
|
|
||||||
echo 50505 > .socket-repl-port
|
|
||||||
mkdir ~/.config/clover
|
|
||||||
cp .clover/config.cljs ~/.config/clover/
|
|
||||||
- name: Start REPL
|
|
||||||
command: clojure -J-Dclojure.server.repl="{:address \"0.0.0.0\" :port 50505 :accept clojure.core.server/repl}" -A:test
|
|
||||||
- name: See Changes
|
|
||||||
command: code CHANGELOG.md
|
|
||||||
|
|
||||||
github:
|
|
||||||
prebuilds:
|
|
||||||
develop: true
|
|
||||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"calva.replConnectSequences": [
|
|
||||||
{
|
|
||||||
"name": "HoneySQL (Jack-In)",
|
|
||||||
"projectType": "deps.edn",
|
|
||||||
"autoSelectForJackIn": true,
|
|
||||||
"menuSelections": {
|
|
||||||
"cljAliases": ["1.12", "dev/repl", "portal", "test"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "HoneySQL (Connect)",
|
|
||||||
"projectType": "deps.edn",
|
|
||||||
"autoSelectForConnect": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"calva.autoStartRepl": true,
|
|
||||||
"calva.autoConnectRepl": true
|
|
||||||
}
|
|
||||||
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -1,5 +1,51 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
* 2.7.next in progress
|
||||||
|
* Address [#440](https://github.com/seancorfield/honeysql/issues/440) by supporting multiple tables in `:truncate`.
|
||||||
|
* Support `USING HASH` as well as `USING GIN`.
|
||||||
|
* Fix [#571](https://github.com/seancorfield/honeysql/issues/571) by allowing `:order-by` to take an empty sequence of columns (and be omitted).
|
||||||
|
* Update dev/build deps.
|
||||||
|
|
||||||
|
* 2.7.1295 -- 2025-03-12
|
||||||
|
* Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax.
|
||||||
|
* Drop support for Clojure 1.9 [#561](https://github.com/seancorfield/honeysql/issues/561).
|
||||||
|
|
||||||
|
* 2.6.1281 -- 2025-03-06
|
||||||
|
* Address [#568](https://github.com/seancorfield/honeysql/issues/568) by adding `honey.sql/semicolon` to merge multiple SQL+params vectors into one (with semicolons separating the SQL statements).
|
||||||
|
* Address [#567](https://github.com/seancorfield/honeysql/issues/567) by adding support for `ASSERT` clause.
|
||||||
|
* Address [#566](https://github.com/seancorfield/honeysql/issues/566) by adding `IS [NOT] DISTINCT FROM` operators.
|
||||||
|
* Add examples of `:alias` with `:group-by` (syntax is slightly different to existing examples for `:order-by`).
|
||||||
|
|
||||||
|
* 2.6.1270 -- 2025-01-17
|
||||||
|
* Fix autoboxing introduced in 2.6.1267 via PR [#564](https://github.com/seancorfield/honeysql/pull/564) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
|
||||||
|
* 2.6.1267 -- 2025-01-16
|
||||||
|
* Support expressions in `WITH` clauses via PR [#563](https://github.com/seancorfield/honeysql/pull/563) [@krevedkokun](https://github.com/krevedkokun).
|
||||||
|
* More performance optimizations via PRs [#560](https://github.com/seancorfield/honeysql/pull/560) and [#562](https://github.com/seancorfield/honeysql/pull/562) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
* Fix two broken links to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) via PR [#559](https://github.com/seancorfield/honeysql/pull/559) [@whatacold](https://github.com/whatacold).
|
||||||
|
* Make SQL Server dialect auto-lift Boolean values to parameters since SQL Server has no `TRUE` / `FALSE` literals.
|
||||||
|
* Fix bug in `DEFAULT` values clause (that omitted some values).
|
||||||
|
|
||||||
|
* 2.6.1243 -- 2024-12-13
|
||||||
|
* Address [#558](https://github.com/seancorfield/honeysql/issues/558) by adding `:patch-into` (and `patch-into` helper) for XTDB (but in core).
|
||||||
|
* Address [#556](https://github.com/seancorfield/honeysql/issues/556) by adding an XTDB section to the documentation with examples.
|
||||||
|
* Address [#555](https://github.com/seancorfield/honeysql/issues/555) by supporting `SETTING` clause for XTDB.
|
||||||
|
* Replace `assert` calls with proper validation, throwing `ex-info` on failure (like other existing validation in HoneySQL).
|
||||||
|
* Experimental `:xtdb` dialect removed (since XTDB no longer supports qualified column names).
|
||||||
|
* Update dev/test deps.
|
||||||
|
|
||||||
|
* 2.6.1230 -- 2024-11-23
|
||||||
|
* Fix [#553](https://github.com/seancorfield/honeysql/issues/553) by adding `:not-between` as special syntax via PR [#554](https://github.com/seancorfield/honeysql/pull/554) [@plooney81](https://github.com/plooney81)
|
||||||
|
* Fix [#552](https://github.com/seancorfield/honeysql/issues/552) by changing the assert-on-load behavior into an explicit test in the test suite.
|
||||||
|
* Fix [#551](https://github.com/seancorfield/honeysql/issues/551) by supporting multiple `WINDOW` clauses.
|
||||||
|
* Fix [#549](https://github.com/seancorfield/honeysql/issues/549) by using `:bb` conditionals to support Babashka (and still support Clojure 1.9.0), and add testing against Babashka so it is fully-supported as a target via PR [#550](https://github.com/seancorfield/honeysql/pull/550) [@borkdude](https://github.com/borkdude)
|
||||||
|
* Address [#532](https://github.com/seancorfield/honeysql/issues/532) by adding support for XTDB SQL extensions `ERASE`, `EXCLUDE`, `OBJECT`, `RECORD`, `RECORDS`, and `RENAME`, along with inline hash maps (as records) and `:get-in` for object navigation, and starting to write tests for XTDB compatibility.
|
||||||
|
|
||||||
|
* 2.6.1203 -- 2024-10-22
|
||||||
|
* Fix [#548](https://github.com/seancorfield/honeysql/issues/548) which was a regression introduced in [#526](https://github.com/seancorfield/honeysql/issues/526) (in 2.6.1161).
|
||||||
|
* Address [#542](https://github.com/seancorfield/honeysql/issues/542) by adding support for `WITH` query tail options for PostgreSQL.
|
||||||
|
* Replace all optional argument destructuring with multiple arities to improve performance.
|
||||||
|
|
||||||
* 2.6.1196 -- 2024-10-06
|
* 2.6.1196 -- 2024-10-06
|
||||||
* Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring.
|
* Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring.
|
||||||
* Performance optimizations via PRs [#545](https://github.com/seancorfield/honeysql/pull/545) and [#546](https://github.com/seancorfield/honeysql/pull/546) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
* Performance optimizations via PRs [#545](https://github.com/seancorfield/honeysql/pull/545) and [#546](https://github.com/seancorfield/honeysql/pull/546) [@alexander-yakushev](https://github.com/alexander-yakushev).
|
||||||
|
|
@ -31,7 +77,7 @@
|
||||||
* Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs.
|
* Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs.
|
||||||
* Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases.
|
* Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases.
|
||||||
* Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server).
|
* Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server).
|
||||||
* Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.
|
* ~Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.~ _[This was removed in 2.6.1243 since XTDB no longer supports qualified column names]_
|
||||||
* Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments.
|
* Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments.
|
||||||
* Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias.
|
* Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias.
|
||||||
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045).
|
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045).
|
||||||
|
|
@ -230,7 +276,7 @@
|
||||||
* Fixes #344 by no longer dropping the qualifier on columns in a `SET` clause _for the `:mysql` dialect only_; the behavior is unchanged for all other dialects.
|
* Fixes #344 by no longer dropping the qualifier on columns in a `SET` clause _for the `:mysql` dialect only_; the behavior is unchanged for all other dialects.
|
||||||
* Fixes #340 by making the "hyphen to space" logic more general so _operators_ containing `-` should retain the hyphen without special cases.
|
* Fixes #340 by making the "hyphen to space" logic more general so _operators_ containing `-` should retain the hyphen without special cases.
|
||||||
* Documentation improvements: `:fetch`, `:lift`, `:limit`, `:offset`, `:param`, `:select`; also around JSON/PostgreSQL.
|
* Documentation improvements: `:fetch`, `:lift`, `:limit`, `:offset`, `:param`, `:select`; also around JSON/PostgreSQL.
|
||||||
* Link to the [HoneySQL web app](https://www.john-shaffer.com/honeysql/) in both the README and **Getting Started**.
|
* Link to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) in both the README and **Getting Started**.
|
||||||
* Switch to `tools.build` for running tests and JAR building etc.
|
* Switch to `tools.build` for running tests and JAR building etc.
|
||||||
|
|
||||||
* 2.0.0-rc5 (for testing; 2021-07-17)
|
* 2.0.0-rc5 (for testing; 2021-07-17)
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -4,17 +4,19 @@ SQL as Clojure data structures. Build queries programmatically -- even at runtim
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
[](https://clojars.org/com.github.seancorfield/honeysql)
|
[](https://clojars.org/com.github.seancorfield/honeysql)
|
||||||
[](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
|
[](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
|
||||||
[](https://clojurians.slack.com/app_redirect?channel=honeysql)
|
[](https://clojurians.slack.com/app_redirect?channel=honeysql)
|
||||||
[](http://clojurians.net)
|
[](http://clojurians.net)
|
||||||
|
[](https://clojurians.zulipchat.com/#narrow/channel/152091-honeysql)
|
||||||
|
|
||||||
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
|
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
|
||||||
|
|
||||||
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of HoneySQL is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/honeysql) or via a git dependency on the latest SHA.
|
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of HoneySQL is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/honeysql) or via a git dependency on the latest SHA.
|
||||||
|
|
||||||
HoneySQL 2.x requires Clojure 1.9 or later.
|
HoneySQL 2.7.y requires Clojure 1.10.3 or later.
|
||||||
|
Earlier versions of HoneySQL support Clojure 1.9.0.
|
||||||
|
It also supports recent versions of ClojureScript and Babashka.
|
||||||
|
|
||||||
Compared to the [legacy 1.x version](#1.x), HoneySQL 2.x provides a streamlined codebase and a simpler method for extending the DSL. It also supports SQL dialects out-of-the-box and will be extended to support vendor-specific language features over time (unlike 1.x).
|
Compared to the [legacy 1.x version](#1.x), HoneySQL 2.x provides a streamlined codebase and a simpler method for extending the DSL. It also supports SQL dialects out-of-the-box and will be extended to support vendor-specific language features over time (unlike 1.x).
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ section of the documentation before trying to use HoneySQL to build your own que
|
||||||
From Clojure:
|
From Clojure:
|
||||||
<!-- {:test-doc-blocks/reader-cond :clj} -->
|
<!-- {:test-doc-blocks/reader-cond :clj} -->
|
||||||
```clojure
|
```clojure
|
||||||
(refer-clojure :exclude '[distinct filter for group-by into partition-by set update])
|
(refer-clojure :exclude '[assert distinct filter for group-by into partition-by set update])
|
||||||
(require '[honey.sql :as sql]
|
(require '[honey.sql :as sql]
|
||||||
;; CAUTION: this overwrites several clojure.core fns:
|
;; CAUTION: this overwrites several clojure.core fns:
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
8
bb.edn
Normal file
8
bb.edn
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{:paths ["src"]
|
||||||
|
:tasks
|
||||||
|
{test
|
||||||
|
{:extra-paths ["test"]
|
||||||
|
:extra-deps {io.github.cognitect-labs/test-runner
|
||||||
|
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
|
||||||
|
:task (exec 'cognitect.test-runner.api/test)
|
||||||
|
:exec-args {:patterns ["^(?!honey.cache).*-test$"]}}}}
|
||||||
10
build.clj
10
build.clj
|
|
@ -19,7 +19,7 @@
|
||||||
[deps-deploy.deps-deploy :as dd]))
|
[deps-deploy.deps-deploy :as dd]))
|
||||||
|
|
||||||
(def lib 'com.github.seancorfield/honeysql)
|
(def lib 'com.github.seancorfield/honeysql)
|
||||||
(defn- the-version [patch] (format "2.6.%s" patch))
|
(defn- the-version [patch] (format "2.7.%s" patch))
|
||||||
(def version (the-version (b/git-count-revs nil)))
|
(def version (the-version (b/git-count-revs nil)))
|
||||||
(def snapshot (the-version "9999-SNAPSHOT"))
|
(def snapshot (the-version "9999-SNAPSHOT"))
|
||||||
(def class-dir "target/classes")
|
(def class-dir "target/classes")
|
||||||
|
|
@ -47,8 +47,7 @@
|
||||||
"Generate and run doc tests.
|
"Generate and run doc tests.
|
||||||
|
|
||||||
Optionally specify :aliases vector:
|
Optionally specify :aliases vector:
|
||||||
[:1.9] -- test against Clojure 1.9 (the default)
|
[:1.10] -- test against Clojure 1.10.3 (the default)
|
||||||
[:1.10] -- test against Clojure 1.10.3
|
|
||||||
[:1.11] -- test against Clojure 1.11.0
|
[:1.11] -- test against Clojure 1.11.0
|
||||||
[:1.12] -- test against Clojure 1.12.0
|
[:1.12] -- test against Clojure 1.12.0
|
||||||
[:cljs] -- test against ClojureScript"
|
[:cljs] -- test against ClojureScript"
|
||||||
|
|
@ -63,6 +62,7 @@
|
||||||
|
|
||||||
(defn test "Run basic tests." [opts]
|
(defn test "Run basic tests." [opts]
|
||||||
(run-task [:test :runner :1.11])
|
(run-task [:test :runner :1.11])
|
||||||
|
(run-task [:test :runner :cljs])
|
||||||
opts)
|
opts)
|
||||||
|
|
||||||
(defn- pom-template [version]
|
(defn- pom-template [version]
|
||||||
|
|
@ -98,10 +98,10 @@
|
||||||
(defn ci
|
(defn ci
|
||||||
"Run the CI pipeline of tests (and build the JAR).
|
"Run the CI pipeline of tests (and build the JAR).
|
||||||
|
|
||||||
Default Clojure version is 1.9.0 (:1.9) so :elide
|
Default Clojure version is 1.10.3 (:1.10) so :elide
|
||||||
tests for #409 on that version."
|
tests for #409 on that version."
|
||||||
[opts]
|
[opts]
|
||||||
(let [aliases [:cljs :elide :1.10 :1.11 :1.12]
|
(let [aliases [:cljs :elide :1.11 :1.12]
|
||||||
opts (jar-opts opts)]
|
opts (jar-opts opts)]
|
||||||
(b/delete {:path "target"})
|
(b/delete {:path "target"})
|
||||||
(doseq [alias aliases]
|
(doseq [alias aliases]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
;;"doc/operator-reference.md"
|
;;"doc/operator-reference.md"
|
||||||
"doc/options.md"
|
"doc/options.md"
|
||||||
"doc/postgresql.md"
|
"doc/postgresql.md"
|
||||||
"doc/special-syntax.md"]
|
"doc/special-syntax.md"
|
||||||
|
"doc/xtdb.md"]
|
||||||
regen-reason (if (not (fs/exists? success-marker))
|
regen-reason (if (not (fs/exists? success-marker))
|
||||||
"a previous successful gen result not found"
|
"a previous successful gen result not found"
|
||||||
(let [newer-thans (fs/modified-since target
|
(let [newer-thans (fs/modified-since target
|
||||||
|
|
|
||||||
7
deps.edn
7
deps.edn
|
|
@ -1,14 +1,13 @@
|
||||||
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
||||||
:paths ["src"]
|
:paths ["src"]
|
||||||
:deps {org.clojure/clojure {:mvn/version "1.9.0"}}
|
:deps {org.clojure/clojure {:mvn/version "1.10.3"}}
|
||||||
:aliases
|
:aliases
|
||||||
{;; for help: clojure -A:deps -T:build help/doc
|
{;; for help: clojure -A:deps -T:build help/doc
|
||||||
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}
|
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.8"}
|
||||||
slipset/deps-deploy {:mvn/version "0.2.2"}}
|
slipset/deps-deploy {:mvn/version "0.2.2"}}
|
||||||
:ns-default build}
|
:ns-default build}
|
||||||
|
|
||||||
;; versions to test against:
|
;; versions to test against:
|
||||||
:1.9 {:override-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
|
|
||||||
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
|
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
|
||||||
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
|
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
|
||||||
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
|
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
|
||||||
|
|
@ -31,7 +30,7 @@
|
||||||
:main-opts ["-m" "cljs-test-runner.main"]}
|
:main-opts ["-m" "cljs-test-runner.main"]}
|
||||||
|
|
||||||
:gen-doc-tests {:replace-paths ["build"]
|
:gen-doc-tests {:replace-paths ["build"]
|
||||||
:extra-deps {babashka/fs {:mvn/version "0.5.22"}
|
:extra-deps {babashka/fs {:mvn/version "0.5.24"}
|
||||||
com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}}
|
com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}}
|
||||||
:main-opts ["-m" "honey.gen-doc-tests"]}
|
:main-opts ["-m" "honey.gen-doc-tests"]}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,14 @@ user=> (sql/format {:create-index [:my-idx [:fruit :using-gin :appearance]]})
|
||||||
["CREATE INDEX my_idx ON fruit USING GIN (appearance)"]
|
["CREATE INDEX my_idx ON fruit USING GIN (appearance)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As of 2.7.next, `USING HASH` index creation is also possible using the keyword
|
||||||
|
`:using-hash` after the table name (or the symbol `using-hash`):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:create-index [:my-idx [:fruit :using-hash :appearance]]})
|
||||||
|
["CREATE INDEX my_idx ON fruit USING HASH (appearance)"]
|
||||||
|
```
|
||||||
|
|
||||||
### rename-table
|
### rename-table
|
||||||
|
|
||||||
Used with `:alter-table`,
|
Used with `:alter-table`,
|
||||||
|
|
@ -486,6 +494,27 @@ user=> (sql/format {:with [[:stuff {:select :*
|
||||||
["WITH stuff AS NOT MATERIALIZED (SELECT * FROM table) SELECT * FROM stuff"]
|
["WITH stuff AS NOT MATERIALIZED (SELECT * FROM table) SELECT * FROM stuff"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As of 2.6.1203, you can specify `SEARCH` and/or `CYCLE` clauses, in place of
|
||||||
|
or following the `MATERIALIZED` marker:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:with-recursive [[:stuff {:select :*
|
||||||
|
:from :table}
|
||||||
|
:search-depth-first-by :col :set :search-col]]
|
||||||
|
:select :*
|
||||||
|
:from :stuff})
|
||||||
|
["WITH RECURSIVE stuff AS (SELECT * FROM table) SEARCH DEPTH FIRST BY col SET search_col SELECT * FROM stuff"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:with-recursive [[:stuff {:select :*
|
||||||
|
:from :table}
|
||||||
|
:cycle [:a :b :c] :set :d :to [:abs :e] :default 42 :using :x]]
|
||||||
|
:select :*
|
||||||
|
:from :stuff})
|
||||||
|
["WITH RECURSIVE stuff AS (SELECT * FROM table) CYCLE a, b, c SET d TO ABS(e) DEFAULT ? USING x SELECT * FROM stuff" 42]
|
||||||
|
```
|
||||||
|
|
||||||
`:with-recursive` follows the same rules as `:with` and produces `WITH RECURSIVE` instead of just `WITH`.
|
`:with-recursive` follows the same rules as `:with` and produces `WITH RECURSIVE` instead of just `WITH`.
|
||||||
|
|
||||||
## intersect, union, union-all, except, except-all
|
## intersect, union, union-all, except, except-all
|
||||||
|
|
@ -649,9 +678,9 @@ user=> (sql/format '{select * bulk-collect-into [arrv 100] from mytable})
|
||||||
["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100]
|
["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100]
|
||||||
```
|
```
|
||||||
|
|
||||||
## insert-into, replace-into
|
## insert-into, replace-into, patch-into
|
||||||
|
|
||||||
There are three use cases with `:insert-into`.
|
There are three use cases with `:insert-into` etc.
|
||||||
|
|
||||||
The first case takes just a table specifier (either a
|
The first case takes just a table specifier (either a
|
||||||
table name or a table/alias pair),
|
table name or a table/alias pair),
|
||||||
|
|
@ -669,6 +698,10 @@ For the first and second cases, you'll use the `:values` clause
|
||||||
to specify rows of values to insert. See [**values**](#values) below
|
to specify rows of values to insert. See [**values**](#values) below
|
||||||
for more detail on the `:values` clause.
|
for more detail on the `:values` clause.
|
||||||
|
|
||||||
|
`:patch-into` is only supported by XTDB but is
|
||||||
|
part of HoneySQL's "core" dialect anyway. It produces a `PATCH INTO`
|
||||||
|
statement but otherwise has identical syntax to `:insert-into`.
|
||||||
|
|
||||||
`:replace-into` is only supported by MySQL and SQLite but is
|
`:replace-into` is only supported by MySQL and SQLite but is
|
||||||
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
|
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
|
||||||
statement but otherwise has identical syntax to `:insert-into`.
|
statement but otherwise has identical syntax to `:insert-into`.
|
||||||
|
|
@ -773,7 +806,7 @@ You can also `UPDATE .. FROM (VALUES ..) ..` where you might also need `:composi
|
||||||
["UPDATE table SET a = v.a FROM (VALUES (?, ?, ?), (?, ?, ?)) AS v (a, b, c) WHERE (x = v.b) AND (y > v.c)" 1 2 3 4 5 6]
|
["UPDATE table SET a = v.a FROM (VALUES (?, ?, ?), (?, ?, ?)) AS v (a, b, c) WHERE (x = v.b) AND (y > v.c)" 1 2 3 4 5 6]
|
||||||
```
|
```
|
||||||
|
|
||||||
## delete, delete-from
|
## delete, delete-from, erase-from
|
||||||
|
|
||||||
`:delete-from` is the simple use case here, accepting just a
|
`:delete-from` is the simple use case here, accepting just a
|
||||||
SQL entity (table name). `:delete` allows for deleting from
|
SQL entity (table name). `:delete` allows for deleting from
|
||||||
|
|
@ -790,16 +823,30 @@ user=> (sql/format {:delete [:order :item]
|
||||||
["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42]
|
["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`:erase-from` is only supported by XTDB and produces an `ERASE FROM`
|
||||||
|
statement but otherwise has identical syntax to `:delete-from`. It
|
||||||
|
is a "hard" delete as opposed to a temporal delete.
|
||||||
|
|
||||||
## truncate
|
## truncate
|
||||||
|
|
||||||
`:truncate` accepts a simple SQL entity (table name)
|
`:truncate` accepts a simple SQL entity (table name)
|
||||||
or a table name followed by various options:
|
or a table name followed by various options, or a
|
||||||
|
sequence that starts with a sequence of one or more table names,
|
||||||
|
optionally followed by various options:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
user=> (sql/format '{truncate transport})
|
user=> (sql/format '{truncate transport})
|
||||||
["TRUNCATE TABLE transport"]
|
["TRUNCATE TABLE transport"]
|
||||||
|
user=> (sql/format '{truncate (transport)})
|
||||||
|
["TRUNCATE TABLE transport"]
|
||||||
user=> (sql/format '{truncate (transport restart identity)})
|
user=> (sql/format '{truncate (transport restart identity)})
|
||||||
["TRUNCATE TABLE transport RESTART IDENTITY"]
|
["TRUNCATE TABLE transport RESTART IDENTITY"]
|
||||||
|
user=> (sql/format '{truncate ((transport))})
|
||||||
|
["TRUNCATE TABLE transport"]
|
||||||
|
user=> (sql/format '{truncate ((transport other))})
|
||||||
|
["TRUNCATE TABLE transport, other"]
|
||||||
|
user=> (sql/format '{truncate ((transport other) restart identity)})
|
||||||
|
["TRUNCATE TABLE transport, other RESTART IDENTITY"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## columns
|
## columns
|
||||||
|
|
@ -1041,6 +1088,9 @@ The `:where` clause can have a single SQL expression, or
|
||||||
a sequence of SQL expressions prefixed by either `:and`
|
a sequence of SQL expressions prefixed by either `:and`
|
||||||
or `:or`. See examples of `:where` in various clauses above.
|
or `:or`. See examples of `:where` in various clauses above.
|
||||||
|
|
||||||
|
If `:where` is given an empty sequence, the `WHERE` clause will
|
||||||
|
be omitted from the generated SQL.
|
||||||
|
|
||||||
Sometimes it is convenient to construct a `WHERE` clause that
|
Sometimes it is convenient to construct a `WHERE` clause that
|
||||||
tests several columns for equality, and you might have a Clojure
|
tests several columns for equality, and you might have a Clojure
|
||||||
hash map containing those values. `honey.sql/map=` exists to
|
hash map containing those values. `honey.sql/map=` exists to
|
||||||
|
|
@ -1062,6 +1112,11 @@ user=> (sql/format '{select (*) from (table)
|
||||||
["SELECT * FROM table GROUP BY status, YEAR(created_date)"]
|
["SELECT * FROM table GROUP BY status, YEAR(created_date)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can `GROUP BY` expressions, column names (`:col1`), or table and column (`:table.col1`),
|
||||||
|
or aliases (`:some.alias`). Since there is ambiguity between the formatting
|
||||||
|
of those, you can use the special syntax `[:alias :some.thing]` to tell
|
||||||
|
HoneySQL to treat `:some.thing` as an alias instead of a table/column name.
|
||||||
|
|
||||||
## having
|
## having
|
||||||
|
|
||||||
The `:having` clause works identically to `:where` above
|
The `:having` clause works identically to `:where` above
|
||||||
|
|
@ -1069,7 +1124,7 @@ but is rendered into the SQL later in precedence order.
|
||||||
|
|
||||||
## window, partition-by (and over)
|
## window, partition-by (and over)
|
||||||
|
|
||||||
`:window` accepts a pair of SQL entity (the window name)
|
`:window` accept alternating pairs of SQL entity (the window name)
|
||||||
and the window "function" as a SQL clause (a hash map).
|
and the window "function" as a SQL clause (a hash map).
|
||||||
|
|
||||||
`:partition-by` accepts the same arguments as `:select` above
|
`:partition-by` accepts the same arguments as `:select` above
|
||||||
|
|
@ -1095,6 +1150,25 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
|
||||||
FROM employee
|
FROM employee
|
||||||
WINDOW w AS (PARTITION BY department)
|
WINDOW w AS (PARTITION BY department)
|
||||||
"]
|
"]
|
||||||
|
;; multiple windows:
|
||||||
|
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]}
|
||||||
|
:x {:partition-by [:salary]}]}
|
||||||
|
{: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), x AS (PARTITION BY salary)
|
||||||
|
"]
|
||||||
;; 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]
|
||||||
|
|
@ -1107,6 +1181,18 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
|
||||||
FROM employee
|
FROM employee
|
||||||
WINDOW w AS (PARTITION BY department)
|
WINDOW w AS (PARTITION BY department)
|
||||||
"]
|
"]
|
||||||
|
;; multiple window clauses:
|
||||||
|
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))
|
||||||
|
(window :x (partition-by :salary))) {: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), x AS (PARTITION BY salary)
|
||||||
|
"]
|
||||||
```
|
```
|
||||||
|
|
||||||
The window function in the `:over` expression may be `{}` or `nil`:
|
The window function in the `:over` expression may be `{}` or `nil`:
|
||||||
|
|
@ -1145,12 +1231,15 @@ user=> (sql/format {:select [[[:over
|
||||||
|
|
||||||
## order-by
|
## order-by
|
||||||
|
|
||||||
`:order-by` accepts a sequence of one or more ordering
|
`:order-by` accepts a sequence of zero or more ordering
|
||||||
expressions. Each ordering expression is either a simple
|
expressions. Each ordering expression is either a simple
|
||||||
SQL entity or a pair of a SQL expression and a direction
|
SQL entity or a pair of a SQL expression and a direction
|
||||||
(which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`,
|
(which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`,
|
||||||
etc -- or the symbol equivalent).
|
etc -- or the symbol equivalent).
|
||||||
|
|
||||||
|
If `:order-by` is given an empty sequence, the `ORDER BY` clause will
|
||||||
|
be omitted from the generated SQL.
|
||||||
|
|
||||||
If you want to order by an expression, you should wrap it
|
If you want to order by an expression, you should wrap it
|
||||||
as a pair with a direction:
|
as a pair with a direction:
|
||||||
|
|
||||||
|
|
@ -1361,7 +1450,7 @@ user=> (sql/format {:insert-into :table
|
||||||
:values [{:a 1 :b 2 :c 3}
|
:values [{:a 1 :b 2 :c 3}
|
||||||
:default
|
:default
|
||||||
{:a 4 :b 5 :c 6}]})
|
{:a 4 :b 5 :c 6}]})
|
||||||
["INSERT INTO table (a, b, c) VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 6 5 4]
|
["INSERT INTO table (a, b, c) VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]
|
||||||
user=> (sql/format {:insert-into :table
|
user=> (sql/format {:insert-into :table
|
||||||
:values [[1 2 3] :default [4 5 6]]})
|
:values [[1 2 3] :default [4 5 6]]})
|
||||||
["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]
|
["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
["SQL Operator Reference" {:file "doc/operator-reference.md"}]
|
["SQL Operator Reference" {:file "doc/operator-reference.md"}]
|
||||||
["SQL 'Special Syntax'" {:file "doc/special-syntax.md"}]
|
["SQL 'Special Syntax'" {:file "doc/special-syntax.md"}]
|
||||||
["PostgreSQL Support" {:file "doc/postgresql.md"}]
|
["PostgreSQL Support" {:file "doc/postgresql.md"}]
|
||||||
|
["XTDB Support" {:file "doc/xtdb.md"}]
|
||||||
["New Relic NRQL Support" {:file "doc/nrql.md"}]
|
["New Relic NRQL Support" {:file "doc/nrql.md"}]
|
||||||
["Other Databases" {:file "doc/databases.md"}]]
|
["Other Databases" {:file "doc/databases.md"}]]
|
||||||
["All the Options" {:file "doc/options.md"}]
|
["All the Options" {:file "doc/options.md"}]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# Other Databases
|
# Other Databases
|
||||||
|
|
||||||
There is a dedicated section for [PostgreSQL Support](postgres.md).
|
There are dedicated sections for [New Relic Query Language Support](nrql.md),
|
||||||
|
[PostgreSQL Support](postgres.md), and
|
||||||
|
[XTDB Support](xtdb.md).
|
||||||
This section provides hints and tips for generating SQL for other
|
This section provides hints and tips for generating SQL for other
|
||||||
databases.
|
databases.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ The DSL itself -- the data structures that both versions convert to SQL and para
|
||||||
If you are using Clojure 1.11, you can invoke `format` with a mixture of named arguments and a trailing hash
|
If you are using Clojure 1.11, you can invoke `format` with a mixture of named arguments and a trailing hash
|
||||||
map of additional options, if you wish.
|
map of additional options, if you wish.
|
||||||
|
|
||||||
HoneySQL 1.x supported Clojure 1.7 and later. HoneySQL 2.x requires Clojure 1.9 or later.
|
HoneySQL 1.x supported Clojure 1.7 and later. HoneySQL 2.7.y requires Clojure 1.10.3 or later. Earlier versions of HoneySQL 2.x support Clojure 1.9.0.
|
||||||
|
|
||||||
## Group, Artifact, and Namespaces
|
## Group, Artifact, and Namespaces
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ Supported Clojure versions: 1.7 and later.
|
||||||
In `deps.edn`:
|
In `deps.edn`:
|
||||||
<!-- :test-doc-blocks/skip -->
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
com.github.seancorfield/honeysql {:mvn/version "2.6.1196"}
|
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Required as:
|
Required as:
|
||||||
|
|
@ -90,7 +90,7 @@ The new namespaces are:
|
||||||
* `honey.sql` -- the primary API (just `format` now),
|
* `honey.sql` -- the primary API (just `format` now),
|
||||||
* `honey.sql.helpers` -- helper functions to build the DSL.
|
* `honey.sql.helpers` -- helper functions to build the DSL.
|
||||||
|
|
||||||
Supported Clojure versions: 1.9 and later.
|
Supported Clojure versions: 1.10.3 and later.
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ For the Clojure CLI, add the following dependency to your `deps.edn` file:
|
||||||
|
|
||||||
<!-- :test-doc-blocks/skip -->
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
com.github.seancorfield/honeysql {:mvn/version "2.6.1196"}
|
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
|
||||||
```
|
```
|
||||||
|
|
||||||
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 -->
|
<!-- :test-doc-blocks/skip -->
|
||||||
```clojure
|
```clojure
|
||||||
[com.github.seancorfield/honeysql "2.6.1196"]
|
[com.github.seancorfield/honeysql "2.7.1295"]
|
||||||
```
|
```
|
||||||
|
|
||||||
HoneySQL produces SQL statements but does not execute them.
|
HoneySQL produces SQL statements but does not execute them.
|
||||||
|
|
@ -26,7 +26,7 @@ To execute SQL statements, you will also need a JDBC wrapper like
|
||||||
|
|
||||||
You can also experiment with HoneySQL directly in a browser -- no installation
|
You can also experiment with HoneySQL directly in a browser -- no installation
|
||||||
required -- using [John Shaffer](https://github.com/john-shaffer)'s awesome
|
required -- using [John Shaffer](https://github.com/john-shaffer)'s awesome
|
||||||
[HoneySQL web app](https://www.john-shaffer.com/honeysql/), written in ClojureScript!
|
[HoneySQL web app](https://john.shaffe.rs/honeysql/), written in ClojureScript!
|
||||||
|
|
||||||
## Basic Concepts
|
## Basic Concepts
|
||||||
|
|
||||||
|
|
@ -415,7 +415,8 @@ If you want to use a dialect _and_ use the default quoting strategy (automatical
|
||||||
```
|
```
|
||||||
|
|
||||||
Out of the box, as part of the extended ANSI SQL support,
|
Out of the box, as part of the extended ANSI SQL support,
|
||||||
HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md).
|
HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md)
|
||||||
|
and [XTDB extensions](xtdb.md).
|
||||||
|
|
||||||
> Note: the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) library which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library (up to 0.4.112) out of the box!
|
> Note: the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) library which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library (up to 0.4.112) out of the box!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,14 @@ and strings.
|
||||||
:from :b
|
:from :b
|
||||||
:order-by [[[:alias :'some-alias]]]})
|
:order-by [[[:alias :'some-alias]]]})
|
||||||
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
|
||||||
|
(sql/format {:select [[:column-name "some-alias"]]
|
||||||
|
:from :b
|
||||||
|
:group-by [[:alias "some-alias"]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
|
||||||
|
(sql/format {:select [[:column-name "some-alias"]]
|
||||||
|
:from :b
|
||||||
|
:group-by [[:alias :'some-alias]]})
|
||||||
|
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
|
||||||
```
|
```
|
||||||
|
|
||||||
## array
|
## array
|
||||||
|
|
@ -80,6 +88,29 @@ In the subquery case, produces `ARRAY(subquery)`:
|
||||||
;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"]
|
;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## at
|
||||||
|
|
||||||
|
If addition to dot navigation (for JSON) -- see the `.` and `.:.` syntax below --
|
||||||
|
HoneySQL also supports bracket notation for JSON navigation.
|
||||||
|
|
||||||
|
The first argument to `:at` is treated as an expression that identifies
|
||||||
|
the column, and subsequent arguments are treated as field names or array
|
||||||
|
indices to navigate into that document.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:at :col :field1 :field2]]]})
|
||||||
|
["SELECT col.field1.field2"]
|
||||||
|
user=> (sql/format {:select [[[:at :table.col 0 :field]]]})
|
||||||
|
["SELECT table.col[0].field"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want an array index to be a parameter, use `:lift`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:at :col [:lift 0] :field]]]})
|
||||||
|
["SELECT col[?].field" 0]
|
||||||
|
```
|
||||||
|
|
||||||
## at time zone
|
## at time zone
|
||||||
|
|
||||||
Accepts two arguments: an expression (assumed to be a date/time of some sort)
|
Accepts two arguments: an expression (assumed to be a date/time of some sort)
|
||||||
|
|
@ -93,7 +124,7 @@ and a time zone name or identifier (can be a string, a symbol, or a keyword):
|
||||||
The time zone name or identifier will be inlined (as a string) and therefore
|
The time zone name or identifier will be inlined (as a string) and therefore
|
||||||
cannot be an expression.
|
cannot be an expression.
|
||||||
|
|
||||||
## between
|
## between and not-between
|
||||||
|
|
||||||
Accepts three arguments: an expression, a lower bound, and
|
Accepts three arguments: an expression, a lower bound, and
|
||||||
an upper bound:
|
an upper bound:
|
||||||
|
|
@ -101,6 +132,9 @@ an upper bound:
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format-expr [:between :id 1 100])
|
(sql/format-expr [:between :id 1 100])
|
||||||
;;=> ["id BETWEEN ? AND ?" 1 100]
|
;;=> ["id BETWEEN ? AND ?" 1 100]
|
||||||
|
|
||||||
|
(sql/format-expr [:not-between :id 1 100])
|
||||||
|
;;=> ["id NOT BETWEEN ? AND ?" 1 100]
|
||||||
```
|
```
|
||||||
|
|
||||||
## case
|
## case
|
||||||
|
|
@ -200,15 +234,23 @@ Accepts a single expression and prefixes it with `DISTINCT `:
|
||||||
;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"]
|
;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## dot .
|
## dot . .:.
|
||||||
|
|
||||||
Accepts an expression and a field (or column) selection:
|
Accepts an expression and one or more fields (or columns). Plain dot produces
|
||||||
|
plain dotted selection:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]})
|
(sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]})
|
||||||
;;=> ["SELECT t.c, s.t.c"]
|
;;=> ["SELECT t.c, s.t.c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Dot colon dot produces Snowflake-style dotted selection:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(sql/format {:select [ [[:.:. :t :c]] [[:.:. :s :t :c]] ]})
|
||||||
|
;;=> ["SELECT t:c, s:t.c"]
|
||||||
|
```
|
||||||
|
|
||||||
Can be used with `:nest` for field selection from composites:
|
Can be used with `:nest` for field selection from composites:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
|
|
@ -216,6 +258,9 @@ Can be used with `:nest` for field selection from composites:
|
||||||
;;=> ["SELECT (v).*, (MYFUNC(x)).y"]
|
;;=> ["SELECT (v).*, (MYFUNC(x)).y"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See also [`get-in`](xtdb.md#object-navigation-expressions)
|
||||||
|
and [`at`](#at) for additional path navigation functions.
|
||||||
|
|
||||||
## entity
|
## entity
|
||||||
|
|
||||||
Accepts a single keyword or symbol argument and produces a
|
Accepts a single keyword or symbol argument and produces a
|
||||||
|
|
|
||||||
220
doc/xtdb.md
Normal file
220
doc/xtdb.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# XTDB Support
|
||||||
|
|
||||||
|
As of 2.6.1230, HoneySQL provides support for most of XTDB's SQL
|
||||||
|
extensions, with additional support being added in subsequent releases.
|
||||||
|
|
||||||
|
For the most part, XTDB's SQL is based on
|
||||||
|
[SQL:2011](https://en.wikipedia.org/wiki/SQL:2011), including the
|
||||||
|
bitemporal features, but also includes a number of SQL extensions
|
||||||
|
to support additional XTDB-specific features.
|
||||||
|
|
||||||
|
HoneySQL attempts to support all of these XTDB features in the core
|
||||||
|
ANSI dialect, and this section documents most of those XTDB features.
|
||||||
|
|
||||||
|
For more details, see the XTDB documentation:
|
||||||
|
* [SQL Overview](https://docs.xtdb.com/quickstart/sql-overview.html)
|
||||||
|
* [SQL Queries](https://docs.xtdb.com/reference/main/sql/queries.html)
|
||||||
|
* [SQL Transactions/DML](https://docs.xtdb.com/reference/main/sql/txs.html)
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
The code examples herein assume:
|
||||||
|
```clojure
|
||||||
|
(refer-clojure :exclude '[update set])
|
||||||
|
(require '[honey.sql :as sql]
|
||||||
|
'[honey.sql.helpers :refer [select from where
|
||||||
|
delete-from erase-from
|
||||||
|
insert-into patch-into values
|
||||||
|
records]])
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## `select` Variations
|
||||||
|
|
||||||
|
XTDB allows you to omit `SELECT` in a query. `SELECT *` is assumed if
|
||||||
|
it is omitted. In HoneySQL, you can simply omit the `:select` clause
|
||||||
|
from the DSL to achieve this.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{select * from foo where (= status "active")})
|
||||||
|
["SELECT * FROM foo WHERE status = ?" "active"]
|
||||||
|
user=> (sql/format '{from foo where (= status "active")})
|
||||||
|
["FROM foo WHERE status = ?" "active"]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also `SELECT *` and then exclude columns and/or rename columns.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[:* {:exclude :_id :rename [[:title, :name]]}]]})
|
||||||
|
["SELECT * EXCLUDE _id RENAME title AS name"]
|
||||||
|
user=> (sql/format '{select ((a.* {exclude _id})
|
||||||
|
(b.* {rename ((title, name))}))
|
||||||
|
from ((foo a))
|
||||||
|
join ((bar b) (= a._id b.foo_id))})
|
||||||
|
["SELECT a.* EXCLUDE _id, b.* RENAME title AS name FROM foo AS a INNER JOIN bar AS b ON a._id = b.foo_id"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`:exclude` can accept a single column, or a sequence of columns.
|
||||||
|
`:rename` accepts a sequence of pairs (column name, new name).
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[:* {:exclude [:_id :upc]
|
||||||
|
:rename [[:title, :name]
|
||||||
|
[:price, :cost]]}]]})
|
||||||
|
["SELECT * EXCLUDE (_id, upc) RENAME (title AS name, price AS cost)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested Sub-Queries
|
||||||
|
|
||||||
|
XTDB can produce structured results from `SELECT` queries containing
|
||||||
|
sub-queries, using `NEST_ONE` and `NEST_MANY`. In HoneySQL, these are
|
||||||
|
supported as regular function syntax in `:select` clauses.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{select (a.*
|
||||||
|
((nest_many {select * from bar where (= foo_id a._id)})
|
||||||
|
b))
|
||||||
|
from ((foo a))})
|
||||||
|
["SELECT a.*, NEST_MANY (SELECT * FROM bar WHERE foo_id = a._id) AS b FROM foo AS a"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember that function calls in `:select` clauses need to be nested three
|
||||||
|
levels of parentheses (brackets):
|
||||||
|
`:select [:col-a [:col-b :alias-b] [[:fn-call :col-c] :alias-c]]`.
|
||||||
|
|
||||||
|
## `records` Clause
|
||||||
|
|
||||||
|
XTDB provides a `RECORDS` clause to specify a list of structured documents,
|
||||||
|
similar to `VALUES` but specifically for documents rather than a collection
|
||||||
|
of column values. HoneySQL supports a `:records` clauses and automatically
|
||||||
|
lifts hash map values to parameters (rather than treating them as DSL fragments).
|
||||||
|
You can inline a hash map to produce XTDB's inline document syntax.
|
||||||
|
See also `insert` and `patch` below.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:records [{:_id 1 :status "active"}]})
|
||||||
|
["RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
user=> (sql/format {:records [[:inline {:_id 1 :status "active"}]]})
|
||||||
|
["RECORDS {_id: 1, status: 'active'}"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `object` (`record`) Literals
|
||||||
|
|
||||||
|
While `RECORDS` exists in parallel to the `VALUES` clause, XTDB also provides
|
||||||
|
a syntax to construct documents in other contexts in SQL, via the `OBJECT`
|
||||||
|
literal syntax. `RECORD` is a synonym for `OBJECT`. HoneySQL supports both
|
||||||
|
`:object` and `:record` as special syntax:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:object {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT OBJECT (_id: 1, status: 'active')"]
|
||||||
|
user=> (sql/format {:select [[[:record {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT RECORD (_id: 1, status: 'active')"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A third option is to use `:inline` with a hash map:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:inline {:_id 1 :status "active"}]]]})
|
||||||
|
["SELECT {_id: 1, status: 'active'}"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Object Navigation Expressions
|
||||||
|
|
||||||
|
In order to deal with nested documents, XTDB provides syntax to navigate
|
||||||
|
into them, via field names and/or array indices. HoneySQL supports this
|
||||||
|
via the `:get-in` special syntax, intended to be familiar to Clojure users.
|
||||||
|
|
||||||
|
The first argument to `:get-in` is treated as an expression that produces
|
||||||
|
the document, and subsequent arguments are treated as field names or array
|
||||||
|
indices to navigate into that document.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:get-in :doc :field1 :field2]]]})
|
||||||
|
["SELECT (doc).field1.field2"]
|
||||||
|
user=> (sql/format {:select [[[:get-in :table.col 0 :field]]]})
|
||||||
|
["SELECT (table.col)[0].field"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want an array index to be a parameter, use `:lift`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [[[:get-in :doc [:lift 0] :field]]]})
|
||||||
|
["SELECT (doc)[?].field" 0]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Temporal Queries
|
||||||
|
|
||||||
|
XTDB allows any query to be run in a temporal context via the `SETTING`
|
||||||
|
clause (ahead of the `SELECT` clause). HoneySQL supports this via the
|
||||||
|
`:setting` clause. It accepts a sequence of identifiers and expressions.
|
||||||
|
An identifier ending in `-time` is assumed to be a temporal identifier
|
||||||
|
(e.g., `:system-time` mapping to `SYSTEM_TIME`). Other identifiers are assumed to
|
||||||
|
be regular SQL (so `-` is mapped to a space, e.g., `:as-of` mapping to `AS OF`).
|
||||||
|
A timestamp literal, such as `DATE '2024-11-24'` can be specified in HoneySQL
|
||||||
|
using `[:inline [:DATE "2024-11-24"]]` (note the literal case of `:DATE`
|
||||||
|
to produce `DATE`).
|
||||||
|
|
||||||
|
See [XTDB's Top-level queries documentation](https://docs.xtdb.com/reference/main/sql/queries.html#_top_level_queries) for more details.
|
||||||
|
|
||||||
|
Here's one fairly complex example:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:setting [[:snapshot-time :to [:inline :DATE "2024-11-24"]]
|
||||||
|
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]]]})
|
||||||
|
["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023'"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Table references (e.g., in a `FROM` clause) can also have temporal qualifiers.
|
||||||
|
See [HoneySQL's `from` clause documentation](clause-reference.md#from) for
|
||||||
|
examples of that, one of which is reproduced here:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:select [:username]
|
||||||
|
:from [[:user :for :system-time :as-of [:inline "2019-08-01 15:23:00"]]]
|
||||||
|
:where [:= :id 9]})
|
||||||
|
["SELECT username FROM user FOR SYSTEM_TIME AS OF '2019-08-01 15:23:00' WHERE id = ?" 9]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `delete` and `erase`
|
||||||
|
|
||||||
|
In XTDB, `DELETE` is a temporal deletion -- the data remains in the database
|
||||||
|
but is no longer visible in queries that don't specify a time range prior to
|
||||||
|
the deletion. XTDB provides a similar `ERASE` operation that can permanently
|
||||||
|
delete the data. HoneySQL supports `:erase-from` with the same syntax as
|
||||||
|
`:delete-from`.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:delete-from :foo :where [:= :status "inactive"]})
|
||||||
|
["DELETE FROM foo WHERE status = ?" "inactive"]
|
||||||
|
user=> (sql/format {:erase-from :foo :where [:= :status "inactive"]})
|
||||||
|
["ERASE FROM foo WHERE status = ?" "inactive"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `insert` and `patch`
|
||||||
|
|
||||||
|
XTDB supports `PATCH` as an upsert operation: it will update existing
|
||||||
|
documents (via merging the new data) or insert new documents if they
|
||||||
|
don't already exist. HoneySQL supports `:patch-into` with the same syntax
|
||||||
|
as `:insert-into` with `:records`.
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format {:insert-into :foo
|
||||||
|
:records [{:_id 1 :status "active"}]})
|
||||||
|
["INSERT INTO foo RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
user=> (sql/format {:patch-into :foo
|
||||||
|
:records [{:_id 1 :status "active"}]})
|
||||||
|
["PATCH INTO foo RECORDS ?" {:_id 1, :status "active"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert`
|
||||||
|
|
||||||
|
XTDB supports an `ASSERT` operation that will throw an exception if the
|
||||||
|
asserted predicate is not true:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
user=> (sql/format '{assert (not-exists {select 1 from users where (= email "james @example.com")})}
|
||||||
|
:inline true)
|
||||||
|
["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2020-2024 sean corfield, all rights reserved
|
;; copyright (c) 2020-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
(ns honey.sql.helpers
|
(ns honey.sql.helpers
|
||||||
"Helper functions for the built-in clauses in honey.sql.
|
"Helper functions for the built-in clauses in honey.sql.
|
||||||
|
|
@ -58,10 +58,12 @@
|
||||||
bulk-collect-info [& args]
|
bulk-collect-info [& args]
|
||||||
|
|
||||||
(as they are for all helper functions)."
|
(as they are for all helper functions)."
|
||||||
(:refer-clojure :exclude [distinct filter for group-by into partition-by set update])
|
(:refer-clojure :exclude [assert distinct filter for group-by into partition-by set update])
|
||||||
(:require [clojure.core :as c]
|
(:require [clojure.core :as c]
|
||||||
[honey.sql :as h]))
|
[honey.sql :as h]))
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
;; implementation helpers:
|
;; implementation helpers:
|
||||||
|
|
||||||
(defn- default-merge [current args]
|
(defn- default-merge [current args]
|
||||||
|
|
@ -399,6 +401,11 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic :create-index args))
|
(generic :create-index args))
|
||||||
|
|
||||||
|
(defn setting
|
||||||
|
"Accepts one or more time settings for a query."
|
||||||
|
[& args]
|
||||||
|
(generic :setting args))
|
||||||
|
|
||||||
(defn with
|
(defn with
|
||||||
"Accepts one or more CTE definitions.
|
"Accepts one or more CTE definitions.
|
||||||
|
|
||||||
|
|
@ -445,6 +452,14 @@
|
||||||
[& clauses]
|
[& clauses]
|
||||||
(generic :except-all (cons {} clauses)))
|
(generic :except-all (cons {} clauses)))
|
||||||
|
|
||||||
|
(defn assert
|
||||||
|
"Accepts an expression (predicate).
|
||||||
|
|
||||||
|
Produces: ASSERT expression"
|
||||||
|
{:arglists '([expr])}
|
||||||
|
[& args]
|
||||||
|
(generic-1 :assert args))
|
||||||
|
|
||||||
(defn select
|
(defn select
|
||||||
"Accepts any number of column names, or column/alias
|
"Accepts any number of column names, or column/alias
|
||||||
pairs, or SQL expressions (optionally aliased):
|
pairs, or SQL expressions (optionally aliased):
|
||||||
|
|
@ -498,6 +513,12 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic :select-distinct-top args))
|
(generic :select-distinct-top args))
|
||||||
|
|
||||||
|
(defn records
|
||||||
|
"Produces RECORDS {...}, {...}, ...
|
||||||
|
Like `values` so it accepts a collection of maps."
|
||||||
|
[& args]
|
||||||
|
(generic-1 :records args))
|
||||||
|
|
||||||
(defn distinct
|
(defn distinct
|
||||||
"Like `select-distinct` but produces DISTINCT..."
|
"Like `select-distinct` but produces DISTINCT..."
|
||||||
[& args]
|
[& args]
|
||||||
|
|
@ -508,6 +529,16 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic-1 :expr args))
|
(generic-1 :expr args))
|
||||||
|
|
||||||
|
(defn exclude
|
||||||
|
"Accepts one or more column names to exclude from a select list."
|
||||||
|
[& args]
|
||||||
|
(generic :exclude args))
|
||||||
|
|
||||||
|
(defn rename
|
||||||
|
"Accepts one or more column names with aliases to rename in a select list."
|
||||||
|
[& args]
|
||||||
|
(generic :rename args))
|
||||||
|
|
||||||
(defn into
|
(defn into
|
||||||
"Accepts table name, optionally followed a database name."
|
"Accepts table name, optionally followed a database name."
|
||||||
{:arglists '([table] [table dbname])}
|
{:arglists '([table] [table dbname])}
|
||||||
|
|
@ -521,6 +552,14 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic :bulk-collect-into args))
|
(generic :bulk-collect-into args))
|
||||||
|
|
||||||
|
(defn- stuff-into [k args]
|
||||||
|
(let [[data & args :as args']
|
||||||
|
(if (map? (first args)) args (cons {} args))
|
||||||
|
[table cols statement] args]
|
||||||
|
(if (and (sequential? cols) (map? statement))
|
||||||
|
(generic k [data [table cols] statement])
|
||||||
|
(generic k args'))))
|
||||||
|
|
||||||
(defn insert-into
|
(defn insert-into
|
||||||
"Accepts a table name or a table/alias pair. That
|
"Accepts a table name or a table/alias pair. That
|
||||||
can optionally be followed by a collection of
|
can optionally be followed by a collection of
|
||||||
|
|
@ -536,12 +575,20 @@
|
||||||
(-> (select :*) (from :other)))"
|
(-> (select :*) (from :other)))"
|
||||||
{:arglists '([table] [table cols] [table statement] [table cols statement])}
|
{:arglists '([table] [table cols] [table statement] [table cols statement])}
|
||||||
[& args]
|
[& args]
|
||||||
(let [[data & args :as args']
|
(stuff-into :insert-into args))
|
||||||
(if (map? (first args)) args (cons {} args))
|
|
||||||
[table cols statement] args]
|
(defn patch-into
|
||||||
(if (and (sequential? cols) (map? statement))
|
"Accepts a table name or a table/alias pair. That
|
||||||
(generic :insert-into [data [table cols] statement])
|
can optionally be followed by a collection of
|
||||||
(generic :insert-into args'))))
|
column names. That can optionally be followed by
|
||||||
|
a (select) statement clause.
|
||||||
|
|
||||||
|
The arguments are identical to insert-into.
|
||||||
|
The PATCH INTO statement is only supported by
|
||||||
|
XTDB."
|
||||||
|
{:arglists '([table] [table cols] [table statement] [table cols statement])}
|
||||||
|
[& args]
|
||||||
|
(stuff-into :patch-into args))
|
||||||
|
|
||||||
(defn replace-into
|
(defn replace-into
|
||||||
"Accepts a table name or a table/alias pair. That
|
"Accepts a table name or a table/alias pair. That
|
||||||
|
|
@ -554,7 +601,7 @@
|
||||||
MySQL and SQLite."
|
MySQL and SQLite."
|
||||||
{:arglists '([table] [table cols] [table statement] [table cols statement])}
|
{:arglists '([table] [table cols] [table statement] [table cols statement])}
|
||||||
[& args]
|
[& args]
|
||||||
(apply insert-into args))
|
(stuff-into :replace-into args))
|
||||||
|
|
||||||
(defn update
|
(defn update
|
||||||
"Accepts either a table name or a table/alias pair.
|
"Accepts either a table name or a table/alias pair.
|
||||||
|
|
@ -582,6 +629,15 @@
|
||||||
[& args]
|
[& args]
|
||||||
(generic :delete-from args))
|
(generic :delete-from args))
|
||||||
|
|
||||||
|
(defn erase-from
|
||||||
|
"For erasing (hard delete) from a single table (XTDB).
|
||||||
|
Accepts a single table name to erase from.
|
||||||
|
|
||||||
|
(-> (erase-from :films) (where [:= :id 1]))"
|
||||||
|
{:arglists '([table])}
|
||||||
|
[& args]
|
||||||
|
(generic :erase-from args))
|
||||||
|
|
||||||
(defn truncate
|
(defn truncate
|
||||||
"Accepts a single table name to truncate."
|
"Accepts a single table name to truncate."
|
||||||
{:arglists '([table])}
|
{:arglists '([table])}
|
||||||
|
|
@ -1176,20 +1232,6 @@
|
||||||
[k args]
|
[k args]
|
||||||
(generic-1 k args))
|
(generic-1 k args))
|
||||||
|
|
||||||
#?(:clj
|
|
||||||
(do
|
|
||||||
;; #409 this assert is only valid when :doc metadata is not elided:
|
|
||||||
(when (-> #'generic-helper-unary meta :doc)
|
|
||||||
;; ensure #295 stays true (all public functions have docstring):
|
|
||||||
(assert (empty? (->> (ns-publics *ns*) (vals) (c/filter (comp not :doc meta))))))
|
|
||||||
;; ensure all public functions match clauses:
|
|
||||||
(assert (= (c/set (conj @#'honey.sql/default-clause-order
|
|
||||||
:composite :filter :lateral :over :within-group
|
|
||||||
:upsert
|
|
||||||
:generic-helper-variadic :generic-helper-unary))
|
|
||||||
(c/set (conj (map keyword (keys (ns-publics *ns*)))
|
|
||||||
:nest :raw))))))
|
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(-> (delete-from :table)
|
(-> (delete-from :table)
|
||||||
(where [:in (composite :first :second)
|
(where [:in (composite :first :second)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
(:refer-clojure :exclude [-> ->> -])
|
(:refer-clojure :exclude [-> ->> -])
|
||||||
(:require [honey.sql :as sql]))
|
(:require [honey.sql :as sql]))
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
;; see https://www.postgresql.org/docs/current/functions-json.html
|
;; see https://www.postgresql.org/docs/current/functions-json.html
|
||||||
|
|
||||||
(def ->
|
(def ->
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,7 @@
|
||||||
"InlineValue -- a protocol that defines how to inline
|
"InlineValue -- a protocol that defines how to inline
|
||||||
values; (sqlize x) produces a SQL string for x.")
|
values; (sqlize x) produces a SQL string for x.")
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
(defprotocol InlineValue :extend-via-metadata true
|
(defprotocol InlineValue :extend-via-metadata true
|
||||||
(sqlize [this] "Render value inline in a SQL string."))
|
(sqlize [this] "Render value inline in a SQL string."))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
(:refer-clojure :exclude [str])
|
(:refer-clojure :exclude [str])
|
||||||
(:require clojure.string))
|
(:require clojure.string))
|
||||||
|
|
||||||
|
#?(:clj (set! *warn-on-reflection* true))
|
||||||
|
|
||||||
(defn str
|
(defn str
|
||||||
"More efficient implementation of `clojure.core/str` because it has more
|
"More efficient implementation of `clojure.core/str` because it has more
|
||||||
non-variadic arities. Optimization is Clojure-only, on other platforms it
|
non-variadic arities. Optimization is Clojure-only, on other platforms it
|
||||||
|
|
@ -75,3 +77,33 @@
|
||||||
|
|
||||||
:default
|
:default
|
||||||
(clojure.string/join separator (transduce xform conj [] coll)))))
|
(clojure.string/join separator (transduce xform conj [] coll)))))
|
||||||
|
|
||||||
|
(defn split-by-separator
|
||||||
|
"More efficient implementation of `clojure.string/split` for cases when a
|
||||||
|
literal string (not regex) is used as a separator, and for cases where the
|
||||||
|
separator is not present in the haystack at all."
|
||||||
|
[s sep]
|
||||||
|
(loop [start 0, res []]
|
||||||
|
(if-some [sep-idx (clojure.string/index-of s sep start)]
|
||||||
|
(let [sep-idx (long sep-idx)]
|
||||||
|
(recur (inc sep-idx) (conj res (subs s start sep-idx))))
|
||||||
|
(if (= start 0)
|
||||||
|
;; Fastpath - zero separators in s
|
||||||
|
[s]
|
||||||
|
(conj res (subs s start))))))
|
||||||
|
|
||||||
|
(defn into*
|
||||||
|
"An extension of `clojure.core/into` that accepts multiple \"from\" arguments.
|
||||||
|
Doesn't support `xform`."
|
||||||
|
([to from1] (into* to from1 nil nil nil))
|
||||||
|
([to from1 from2] (into* to from1 from2 nil nil))
|
||||||
|
([to from1 from2 from3] (into* to from1 from2 from3 nil))
|
||||||
|
([to from1 from2 from3 from4]
|
||||||
|
(if (or from1 from2 from3 from4)
|
||||||
|
(as-> (transient to) to'
|
||||||
|
(reduce conj! to' from1)
|
||||||
|
(reduce conj! to' from2)
|
||||||
|
(reduce conj! to' from3)
|
||||||
|
(reduce conj! to' from4)
|
||||||
|
(persistent! to'))
|
||||||
|
to)))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2023-2024 sean corfield, all rights reserved
|
;; copyright (c) 2023-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
(ns honey.ops-test
|
(ns honey.ops-test
|
||||||
(:refer-clojure :exclude [format])
|
(:refer-clojure :exclude [format])
|
||||||
|
|
@ -9,3 +9,11 @@
|
||||||
(is (= ["SELECT a - b - c AS x"]
|
(is (= ["SELECT a - b - c AS x"]
|
||||||
(-> {:select [[[:- :a :b :c] :x]]}
|
(-> {:select [[[:- :a :b :c] :x]]}
|
||||||
(sut/format)))))
|
(sut/format)))))
|
||||||
|
|
||||||
|
(deftest issue-566
|
||||||
|
(is (= ["SELECT * FROM table WHERE a IS DISTINCT FROM b"]
|
||||||
|
(-> {:select :* :from :table :where [:is-distinct-from :a :b]}
|
||||||
|
(sut/format))))
|
||||||
|
(is (= ["SELECT * FROM table WHERE a IS NOT DISTINCT FROM b"]
|
||||||
|
(-> {:select :* :from :table :where [:is-not-distinct-from :a :b]}
|
||||||
|
(sut/format)))))
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,43 @@
|
||||||
|
|
||||||
(ns honey.sql.helpers-test
|
(ns honey.sql.helpers-test
|
||||||
(:refer-clojure :exclude [filter for group-by partition-by set update])
|
(:refer-clojure :exclude [filter for group-by partition-by set update])
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
#_{:clj-kondo/ignore [:unused-namespace]}
|
||||||
|
(:require [clojure.core :as c]
|
||||||
|
[clojure.test :refer [deftest is testing]]
|
||||||
[honey.sql :as sql]
|
[honey.sql :as sql]
|
||||||
[honey.sql.helpers :as h
|
[honey.sql.helpers :as h
|
||||||
:refer [add-column add-index alter-table columns create-table create-table-as create-view
|
:refer [add-column alter-table columns create-table create-table-as create-view
|
||||||
create-materialized-view drop-view drop-materialized-view
|
create-materialized-view
|
||||||
create-index
|
create-index
|
||||||
bulk-collect-into
|
bulk-collect-into
|
||||||
cross-join do-update-set drop-column drop-index drop-table
|
cross-join do-update-set drop-column drop-table
|
||||||
filter from full-join
|
filter from full-join
|
||||||
group-by having insert-into
|
group-by having insert-into replace-into
|
||||||
join-by join lateral left-join limit offset on-conflict
|
join-by join left-join limit offset on-conflict
|
||||||
on-duplicate-key-update
|
on-duplicate-key-update
|
||||||
order-by over partition-by refresh-materialized-view
|
order-by over partition-by refresh-materialized-view
|
||||||
rename-column rename-table returning right-join
|
returning right-join
|
||||||
select select-distinct select-top select-distinct-top
|
select select-distinct select-top
|
||||||
values where window with with-columns
|
values where window with with-columns
|
||||||
with-data within-group]]))
|
with-data within-group]]))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(deftest helpers-are-complete
|
||||||
|
(let [helpers-ns (find-ns 'honey.sql.helpers)]
|
||||||
|
(testing "all public helpers have docstrings"
|
||||||
|
;; #409 this assert is only valid when :doc metadata is not elided:
|
||||||
|
(when (-> #'h/generic-helper-unary meta :doc)
|
||||||
|
;; ensure #295 stays true (all public functions have docstring):
|
||||||
|
(is (= [] (->> (ns-publics helpers-ns) (vals) (c/filter (comp not :doc meta)))))))
|
||||||
|
(testing "all clauses have public helpers"
|
||||||
|
;; ensure all public functions match clauses:
|
||||||
|
(is (= (c/set (conj @#'honey.sql/default-clause-order
|
||||||
|
:composite :filter :lateral :over :within-group
|
||||||
|
:upsert
|
||||||
|
:generic-helper-variadic :generic-helper-unary))
|
||||||
|
(c/set (conj (map keyword (keys (ns-publics helpers-ns)))
|
||||||
|
:nest :raw))))))))
|
||||||
|
|
||||||
(deftest test-select
|
(deftest test-select
|
||||||
(testing "large helper expression"
|
(testing "large helper expression"
|
||||||
(let [m1 (-> (with [:cte (-> (select :*)
|
(let [m1 (-> (with [:cte (-> (select :*)
|
||||||
|
|
@ -533,6 +552,33 @@
|
||||||
" 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)")]))
|
||||||
|
;; multiple window tests
|
||||||
|
(is (= (-> (select :id
|
||||||
|
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
|
||||||
|
[[:max :salary] :w :MaxSalary]))
|
||||||
|
(from :employee)
|
||||||
|
(window :w (partition-by :department))
|
||||||
|
(window :x (partition-by :salary))
|
||||||
|
sql/format)
|
||||||
|
[(str "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)"
|
||||||
|
", x AS (PARTITION BY salary)")]))
|
||||||
|
(is (= (-> (select :id
|
||||||
|
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
|
||||||
|
[[:max :salary] :w :MaxSalary]))
|
||||||
|
(from :employee)
|
||||||
|
(window :w (partition-by :department)
|
||||||
|
:x (partition-by :salary))
|
||||||
|
sql/format)
|
||||||
|
[(str "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)"
|
||||||
|
", x AS (PARTITION BY salary)")]))
|
||||||
;; test nil / empty window function clause:
|
;; test nil / empty window function clause:
|
||||||
(is (= (-> (select :id
|
(is (= (-> (select :id
|
||||||
(over [[:avg :salary] {} :Average]
|
(over [[:avg :salary] {} :Average]
|
||||||
|
|
@ -790,7 +836,10 @@
|
||||||
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
|
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
|
||||||
;; three arguments with an alias and columns:
|
;; three arguments with an alias and columns:
|
||||||
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
||||||
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"])))
|
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))
|
||||||
|
;; and again with replace-into:
|
||||||
|
(is (= (sql/format (replace-into '(transport t) '(id, name) '{select (*) from (cars)}))
|
||||||
|
["REPLACE INTO transport AS t (id, name) SELECT * FROM cars"])))
|
||||||
|
|
||||||
;; these tests are adapted from Cam Saul's PR #283
|
;; these tests are adapted from Cam Saul's PR #283
|
||||||
|
|
||||||
|
|
@ -989,11 +1038,15 @@
|
||||||
(sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column]))))
|
(sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column]))))
|
||||||
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
|
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
|
||||||
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column])))))
|
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column])))))
|
||||||
(testing "PostgreSQL extensions (USING GIN)"
|
(testing "PostgreSQL extensions (USING GIN/HASH)"
|
||||||
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
||||||
(sql/format {:create-index [:my-column-idx [:my-table :using-gin :my-column]]})))
|
(sql/format {:create-index [:my-column-idx [:my-table :using-gin :my-column]]})))
|
||||||
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
|
||||||
(sql/format (create-index :my-column-idx [:my-table :using-gin :my-column]))))))
|
(sql/format (create-index :my-column-idx [:my-table :using-gin :my-column]))))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
|
||||||
|
(sql/format {:create-index [:my-column-idx [:my-table :using-hash :my-column]]})))
|
||||||
|
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
|
||||||
|
(sql/format (create-index :my-column-idx [:my-table :using-hash :my-column]))))))
|
||||||
|
|
||||||
(deftest join-with-alias
|
(deftest join-with-alias
|
||||||
(is (= ["SELECT * FROM foo LEFT JOIN (populatons AS pm INNER JOIN customers AS pc ON (pm.id = pc.id) AND (pm.other_id = pc.other_id)) ON foo.fk_id = pm.id"]
|
(is (= ["SELECT * FROM foo LEFT JOIN (populatons AS pm INNER JOIN customers AS pc ON (pm.id = pc.id) AND (pm.other_id = pc.other_id)) ON foo.fk_id = pm.id"]
|
||||||
|
|
|
||||||
148
test/honey/sql/xtdb_test.cljc
Normal file
148
test/honey/sql/xtdb_test.cljc
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
;; copyright (c) 2020-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns honey.sql.xtdb-test
|
||||||
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
|
[honey.sql :as sql]
|
||||||
|
[honey.sql.helpers :as h
|
||||||
|
:refer [select exclude rename from]]))
|
||||||
|
|
||||||
|
(deftest select-tests
|
||||||
|
(testing "select, exclude, rename"
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME value AS foo_value FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id) (rename [:value :foo_value])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE (_id, a) RENAME value AS foo_value FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id :a) (rename [:value :foo_value])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME (value AS foo_value, a AS b) FROM foo"]
|
||||||
|
(sql/format (-> (select :*) (exclude :_id)
|
||||||
|
(rename [:value :foo_value]
|
||||||
|
[:a :b])
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME value AS foo_value, c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id) (rename [:value :foo_value]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE (_id, a) RENAME value AS foo_value, c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id :a) (rename [:value :foo_value]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo)))))
|
||||||
|
(is (= ["SELECT * EXCLUDE _id RENAME (value AS foo_value, a AS b), c.x FROM foo"]
|
||||||
|
(sql/format (-> (select [:* (-> (exclude :_id)
|
||||||
|
(rename [:value :foo_value]
|
||||||
|
[:a :b]))]
|
||||||
|
:c.x)
|
||||||
|
(from :foo))))))
|
||||||
|
(testing "select, nest_one, nest_many"
|
||||||
|
(is (= ["SELECT a._id, NEST_ONE (SELECT * FROM foo AS b WHERE b_id = a._id) FROM bar AS a"]
|
||||||
|
(sql/format '{select (a._id,
|
||||||
|
((nest_one {select * from ((foo b)) where (= b_id a._id)})))
|
||||||
|
from ((bar a))})))
|
||||||
|
(is (= ["SELECT a._id, NEST_MANY (SELECT * FROM foo AS b) FROM bar AS a"]
|
||||||
|
(sql/format '{select (a._id,
|
||||||
|
((nest_many {select * from ((foo b))})))
|
||||||
|
from ((bar a))})))))
|
||||||
|
|
||||||
|
(deftest dotted-array-access-tests
|
||||||
|
(is (= ["SELECT (a.b).c"] ; old, partial support:
|
||||||
|
(sql/format '{select (((. (nest :a.b) :c)))})))
|
||||||
|
(is (= ["SELECT (a.b).c"] ; new, complete support:
|
||||||
|
(sql/format '{select (((:get-in :a.b :c)))})))
|
||||||
|
(is (= ["SELECT (a).b.c"] ; the first expression is always parenthesized:
|
||||||
|
(sql/format '{select (((:get-in :a :b :c)))}))))
|
||||||
|
|
||||||
|
(deftest erase-from-test
|
||||||
|
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
|
||||||
|
(-> {:erase-from :foo
|
||||||
|
:where [:= :foo.id 42]}
|
||||||
|
(sql/format))))
|
||||||
|
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
|
||||||
|
(-> (h/erase-from :foo)
|
||||||
|
(h/where [:= :foo.id 42])
|
||||||
|
(sql/format)))))
|
||||||
|
|
||||||
|
(deftest inline-record-body
|
||||||
|
(is (= ["{_id: 1, name: 'foo', info: {contact: [{loc: 'home', tel: '123'}, {loc: 'work', tel: '456'}]}}"]
|
||||||
|
(sql/format [:inline {:_id 1 :name "foo"
|
||||||
|
:info {:contact [{:loc "home" :tel "123"}
|
||||||
|
{:loc "work" :tel "456"}]}}]))))
|
||||||
|
|
||||||
|
(deftest records-statement
|
||||||
|
(testing "auto-lift maps"
|
||||||
|
(is (= ["RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}))))
|
||||||
|
(testing "explicit inline"
|
||||||
|
(is (= ["RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]}))))
|
||||||
|
(testing "insert with records"
|
||||||
|
(is (= ["INSERT INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:insert-into :foo
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["INSERT INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:insert-into :foo
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["INSERT INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:insert-into [:foo ; as a sub-clause
|
||||||
|
{:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}]})))))
|
||||||
|
|
||||||
|
(deftest patch-statement
|
||||||
|
(testing "patch with records"
|
||||||
|
(is (= ["PATCH INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
|
||||||
|
(sql/format {:patch-into [:foo]
|
||||||
|
:records [[:inline {:_id 1 :name "cat"}]
|
||||||
|
[:inline {:_id 2 :name "dog"}]]})))
|
||||||
|
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format {:patch-into [:foo ; as a sub-clause
|
||||||
|
{:records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}]}]})))
|
||||||
|
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
|
||||||
|
(sql/format (h/patch-into :foo
|
||||||
|
(h/records [{:_id 1 :name "cat"}
|
||||||
|
{:_id 2 :name "dog"}])))))))
|
||||||
|
|
||||||
|
(deftest object-record-expr
|
||||||
|
(testing "object literal"
|
||||||
|
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format {:select [[[:object {:_id 1 :name "foo"}]]]})))
|
||||||
|
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format '{select (((:object {:_id 1 :name "foo"})))}))))
|
||||||
|
(testing "record literal"
|
||||||
|
(is (= ["SELECT RECORD (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format {:select [[[:record {:_id 1 :name "foo"}]]]})))
|
||||||
|
(is (= ["SELECT RECORD (_id: 1, name: 'foo')"]
|
||||||
|
(sql/format '{select (((:record {:_id 1 :name "foo"})))}))))
|
||||||
|
(testing "inline map literal"
|
||||||
|
(is (= ["SELECT {_id: 1, name: 'foo'}"]
|
||||||
|
(sql/format {:select [[[:inline {:_id 1 :name "foo"}]]]})))))
|
||||||
|
|
||||||
|
(deftest navigation-dot-index
|
||||||
|
(is (= ["SELECT (a.b).c[1].d"]
|
||||||
|
(sql/format '{select (((get-in a.b c 1 d)))})))
|
||||||
|
(is (= ["SELECT (a.b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in a.b c (lift 1) d)))})))
|
||||||
|
(is (= ["SELECT (a.b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in (. a b) c (lift 1) d)))})))
|
||||||
|
(is (= ["SELECT (OBJECT (_id: 1, b: 'thing').b).c[?].d" 1]
|
||||||
|
(sql/format '{select (((get-in (. (object {_id 1 b "thing"}) b) c (lift 1) d)))}))))
|
||||||
|
|
||||||
|
(deftest assert-statement
|
||||||
|
(testing "quoted sql"
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(sql/format '{assert (not-exists {select 1 from users where (= email "james @example.com")})}
|
||||||
|
:inline true)))
|
||||||
|
(is (= ["ASSERT TRUE"]
|
||||||
|
(sql/format '{assert true}
|
||||||
|
:inline true))))
|
||||||
|
(testing "helper"
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(-> (h/assert [:not-exists {:select 1 :from :users :where [:= :email "james @example.com"]}])
|
||||||
|
(sql/format {:inline true}))))
|
||||||
|
(is (= ["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
|
||||||
|
(-> {}
|
||||||
|
(h/assert [:not-exists {:select 1 :from :users :where [:= :email "james @example.com"]}])
|
||||||
|
(sql/format {:inline true}))))))
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2021-2024 sean corfield, all rights reserved
|
;; copyright (c) 2021-2025 sean corfield, all rights reserved
|
||||||
|
|
||||||
(ns honey.sql-test
|
(ns honey.sql-test
|
||||||
(:refer-clojure :exclude [format])
|
(:refer-clojure :exclude [format])
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
[clojure.test :refer [deftest is testing]]
|
[clojure.test :refer [deftest is testing]]
|
||||||
[honey.sql :as sut :refer [format]]
|
[honey.sql :as sut :refer [format]]
|
||||||
[honey.sql.helpers :as h])
|
[honey.sql.helpers :as h])
|
||||||
#?(:clj (:import (clojure.lang ExceptionInfo))))
|
#?(:clj (:import (clojure.lang ExceptionInfo)
|
||||||
|
(java.net URLEncoder))))
|
||||||
|
|
||||||
(deftest mysql-tests
|
(deftest mysql-tests
|
||||||
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
|
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
|
||||||
|
|
@ -21,6 +22,8 @@
|
||||||
(sut/format-expr [:is :id nil])))
|
(sut/format-expr [:is :id nil])))
|
||||||
(is (= ["id = TRUE"]
|
(is (= ["id = TRUE"]
|
||||||
(sut/format-expr [:= :id true])))
|
(sut/format-expr [:= :id true])))
|
||||||
|
(is (= ["[id] = ?" true]
|
||||||
|
(sut/format [:= :id true] {:dialect :sqlserver})))
|
||||||
(is (= ["id IS TRUE"]
|
(is (= ["id IS TRUE"]
|
||||||
(sut/format-expr [:is :id true])))
|
(sut/format-expr [:is :id true])))
|
||||||
(is (= ["id <> TRUE"]
|
(is (= ["id <> TRUE"]
|
||||||
|
|
@ -177,8 +180,10 @@
|
||||||
["WITH query AS MATERIALIZED (SELECT foo FROM bar)"]))
|
["WITH query AS MATERIALIZED (SELECT foo FROM bar)"]))
|
||||||
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :not-materialized]]})
|
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :not-materialized]]})
|
||||||
["WITH query AS NOT MATERIALIZED (SELECT foo FROM bar)"]))
|
["WITH query AS NOT MATERIALIZED (SELECT foo FROM bar)"]))
|
||||||
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :unknown]]})
|
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :kw-1 :kw-2]]})
|
||||||
["WITH query AS (SELECT foo FROM bar)"]))
|
["WITH query AS (SELECT foo FROM bar) KW 1 kw_2"]))
|
||||||
|
(is (= (format {:with-recursive [[:query {:select [:foo] :from [:bar]} :cycle [:a :b :c] :set :d :to [:abs :e] :default 42 :using :x]]})
|
||||||
|
["WITH RECURSIVE query AS (SELECT foo FROM bar) CYCLE a, b, c SET d TO ABS(e) DEFAULT ? USING x" 42]))
|
||||||
(is (= (format {:with [[:query1 {:select [:foo] :from [:bar]}]
|
(is (= (format {:with [[:query1 {:select [:foo] :from [:bar]}]
|
||||||
[:query2 {:select [:bar] :from [:quux]}]]
|
[:query2 {:select [:bar] :from [:quux]}]]
|
||||||
:select [:query1.id :query2.name]
|
:select [:query1.id :query2.name]
|
||||||
|
|
@ -216,7 +221,26 @@
|
||||||
:from [:hits :stuff]
|
:from [:hits :stuff]
|
||||||
:where [:= :EventDate :ts_upper_bound]})
|
:where [:= :EventDate :ts_upper_bound]})
|
||||||
["WITH ? AS ts_upper_bound, stuff AS (SELECT * FROM songs) SELECT * FROM hits, stuff WHERE EventDate = ts_upper_bound"
|
["WITH ? AS ts_upper_bound, stuff AS (SELECT * FROM songs) SELECT * FROM hits, stuff WHERE EventDate = ts_upper_bound"
|
||||||
"2019-08-01 15:23:00"]))))
|
"2019-08-01 15:23:00"])))
|
||||||
|
(testing "Use expression in a WITH clause"
|
||||||
|
(is (= (format
|
||||||
|
{:with [[:s [:sum :bytes]]]
|
||||||
|
:select [:s]
|
||||||
|
:from [:table]})
|
||||||
|
["WITH SUM(bytes) AS s SELECT s FROM table"]))
|
||||||
|
|
||||||
|
(is (= (format
|
||||||
|
{:with [[:v [:raw "m['k']"]]]
|
||||||
|
:select [:v]
|
||||||
|
:from [:table]})
|
||||||
|
["WITH m['k'] AS v SELECT v FROM table"]))
|
||||||
|
|
||||||
|
(is (= (format
|
||||||
|
{:with [[:cond [:and [:= :a 1] [:= :b 2] [:= :c 3]]]]
|
||||||
|
:select [:v]
|
||||||
|
:from [:table]
|
||||||
|
:where :cond})
|
||||||
|
["WITH (a = ?) AND (b = ?) AND (c = ?) AS cond SELECT v FROM table WHERE cond" 1 2 3]))))
|
||||||
|
|
||||||
(deftest insert-into
|
(deftest insert-into
|
||||||
(is (= (format {:insert-into :foo})
|
(is (= (format {:insert-into :foo})
|
||||||
|
|
@ -551,13 +575,15 @@
|
||||||
(-> {:delete-from :foo
|
(-> {:delete-from :foo
|
||||||
:where [:= :foo.id 42]}
|
:where [:= :foo.id 42]}
|
||||||
(format :dialect :mysql :pretty true)))))
|
(format :dialect :mysql :pretty true)))))
|
||||||
(when (str/starts-with? #?(:cljs *clojurescript-version*
|
(let [version #?(:cljs *clojurescript-version*
|
||||||
:default (clojure-version)) "1.11")
|
:default (clojure-version))]
|
||||||
|
(when (or (str/starts-with? version "1.12")
|
||||||
|
(str/starts-with? version "1.11"))
|
||||||
(testing "format can be called with mixed arguments"
|
(testing "format can be called with mixed arguments"
|
||||||
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
|
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
|
||||||
(-> {:delete-from :foo
|
(-> {:delete-from :foo
|
||||||
:where [:= :foo.id 42]}
|
:where [:= :foo.id 42]}
|
||||||
(format :dialect :mysql {:pretty true})))))))
|
(format :dialect :mysql {:pretty true}))))))))
|
||||||
|
|
||||||
(deftest delete-from-test
|
(deftest delete-from-test
|
||||||
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
|
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
|
||||||
|
|
@ -588,6 +614,12 @@
|
||||||
(format {:dialect :mysql}))))
|
(format {:dialect :mysql}))))
|
||||||
(is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"]
|
(is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"]
|
||||||
(-> {:truncate [:foo :continue :identity]}
|
(-> {:truncate [:foo :continue :identity]}
|
||||||
|
(format {:dialect :mysql}))))
|
||||||
|
(is (= ["TRUNCATE TABLE `t1`, `t2`"]
|
||||||
|
(-> {:truncate [[:t1 :t2]]}
|
||||||
|
(format {:dialect :mysql}))))
|
||||||
|
(is (= ["TRUNCATE TABLE `t1`, `t2` CONTINUE IDENTITY"]
|
||||||
|
(-> {:truncate [[:t1 :t2] :continue :identity]}
|
||||||
(format {:dialect :mysql})))))
|
(format {:dialect :mysql})))))
|
||||||
|
|
||||||
(deftest inlined-values-are-stringified-correctly
|
(deftest inlined-values-are-stringified-correctly
|
||||||
|
|
@ -1152,9 +1184,10 @@ ORDER BY id = ? DESC
|
||||||
|
|
||||||
(deftest issue-474-dot-selection
|
(deftest issue-474-dot-selection
|
||||||
(testing "basic dot selection"
|
(testing "basic dot selection"
|
||||||
(is (= ["SELECT a.b, c.d, a.d.x"]
|
(is (= ["SELECT a.b, c.d, a.d.x, a.d.x.y"]
|
||||||
(let [t :a c :d]
|
(let [t :a c :d]
|
||||||
(sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}))))
|
(sut/format {:select [[[:. t :b]] [[:. :c c]]
|
||||||
|
[[:. t c :x]] [[:. t c :x :y]]]}))))
|
||||||
(is (= ["SELECT [a].[b], [c].[d], [a].[d].[x]"]
|
(is (= ["SELECT [a].[b], [c].[d], [a].[d].[x]"]
|
||||||
(let [t :a c :d]
|
(let [t :a c :d]
|
||||||
(sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}
|
(sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}
|
||||||
|
|
@ -1168,8 +1201,54 @@ ORDER BY id = ? DESC
|
||||||
(sut/format '{select (((. (nest v) *))
|
(sut/format '{select (((. (nest v) *))
|
||||||
((. (nest w) x))
|
((. (nest w) x))
|
||||||
((. (nest (y z)) *)))}
|
((. (nest (y z)) *)))}
|
||||||
|
{:dialect :mysql})))
|
||||||
|
(is (= ["SELECT (v).*, (w).x, (Y(z)).*"]
|
||||||
|
(sut/format '{select (((get-in v *))
|
||||||
|
((get-in w x))
|
||||||
|
((get-in (y z) *)))})))
|
||||||
|
(is (= ["SELECT (`v`).*, (`w`).`x`, (Y(`z`)).*"]
|
||||||
|
(sut/format '{select (((get-in v *))
|
||||||
|
((get-in w x))
|
||||||
|
((get-in (y z) *)))}
|
||||||
{:dialect :mysql})))))
|
{:dialect :mysql})))))
|
||||||
|
|
||||||
|
(deftest issue-570-snowflake-dot-selection
|
||||||
|
(testing "basic colon selection"
|
||||||
|
(is (= ["SELECT a:b, c:d, a:d.x, a:d.x.y"]
|
||||||
|
(let [t :a c :d]
|
||||||
|
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]]
|
||||||
|
[[:.:. t c :x]] [[:.:. t c :x :y]]]}))))
|
||||||
|
(is (= ["SELECT [a]:[b], [c]:[d], [a]:[d].[x]"]
|
||||||
|
(let [t :a c :d]
|
||||||
|
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]] [[:.:. t c :x]]]}
|
||||||
|
{:dialect :sqlserver})))))
|
||||||
|
(testing "basic field selection from composite"
|
||||||
|
(is (= ["SELECT (v):*, (w):x, (Y(z)):*"]
|
||||||
|
(sut/format '{select (((.:. (nest v) *))
|
||||||
|
((.:. (nest w) x))
|
||||||
|
((.:. (nest (y z)) *)))})))
|
||||||
|
(is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"]
|
||||||
|
(sut/format '{select (((.:. (nest v) *))
|
||||||
|
((.:. (nest w) x))
|
||||||
|
((.:. (nest (y z)) *)))}
|
||||||
|
{:dialect :mysql}))))
|
||||||
|
(testing "bracket selection"
|
||||||
|
(is (= ["SELECT a['b'], c['b'], a['d'].x, a:e[0].name"]
|
||||||
|
(sut/format {:select [[[:at :a [:inline "b"]]]
|
||||||
|
[[:at :c "b"]]
|
||||||
|
[[:at :a [:inline "d"] :x]]
|
||||||
|
[[:.:. :a [:at :e [:inline 0]] :name]]]})))
|
||||||
|
(is (= ["SELECT a[?].name" 0]
|
||||||
|
(sut/format '{select (((at a (lift 0) name)))})))
|
||||||
|
;; sanity check, compare with get-in:
|
||||||
|
(is (= ["SELECT (a)[?].name" 0]
|
||||||
|
(sut/format '{select (((get-in a (lift 0) name)))})))
|
||||||
|
(is (= ["SELECT (a)['b'], (c)['b'], (a)['d'].x, a:(e)[0].name"]
|
||||||
|
(sut/format {:select [[[:get-in :a [:inline "b"]]]
|
||||||
|
[[:get-in :c "b"]]
|
||||||
|
[[:get-in :a [:inline "d"] :x]]
|
||||||
|
[[:.:. :a [:get-in :e [:inline 0]] :name]]]})))))
|
||||||
|
|
||||||
(deftest issue-476-raw
|
(deftest issue-476-raw
|
||||||
(testing "single argument :raw"
|
(testing "single argument :raw"
|
||||||
(is (= ["@foo := 42"]
|
(is (= ["@foo := 42"]
|
||||||
|
|
@ -1387,6 +1466,50 @@ ORDER BY id = ? DESC
|
||||||
(is (= ["SELECT * FROM `t1` INNER JOIN `t2` USING (`id`) WHERE `t1`.`id` = ?" 1]
|
(is (= ["SELECT * FROM `t1` INNER JOIN `t2` USING (`id`) WHERE `t1`.`id` = ?" 1]
|
||||||
(sut/format '{select * from t1 join (t2 (:using id)) where (= t1/id 1)} {:dialect :mysql})))))
|
(sut/format '{select * from t1 join (t2 (:using id)) where (= t1/id 1)} {:dialect :mysql})))))
|
||||||
|
|
||||||
|
(deftest issue-548-format-var-encoding
|
||||||
|
(is (= ["CREATE TABLE \"With%20Space\""]
|
||||||
|
(sut/format {:create-table "With%20Space"})))
|
||||||
|
(is (= ["CREATE TABLE \"%20WithLeadingSpace\""]
|
||||||
|
(sut/format {:create-table "%20WithLeadingSpace"})))
|
||||||
|
#?(:clj (let [table (URLEncoder/encode "привіт")]
|
||||||
|
(is (= [(str "CREATE TABLE \"" table "\"")]
|
||||||
|
(sut/format {:create-table table}))))))
|
||||||
|
|
||||||
|
(deftest issue-555-setting
|
||||||
|
(testing "setting default time"
|
||||||
|
(is (= ["SETTING DEFAULT SYSTEM_TIME AS OF DATE '2024-11-24'"]
|
||||||
|
(sut/format {:setting [:default :system-time :as-of [:inline :DATE "2024-11-24"]]})))
|
||||||
|
(is (= ["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023'"]
|
||||||
|
(sut/format {:setting [[:snapshot-time :to [:inline :DATE "2024-11-24"]]
|
||||||
|
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]]]})))
|
||||||
|
(is (= ["SETTING DEFAULT SYSTEM_TIME AS OF DATE '2024-11-24' SELECT * FROM table"]
|
||||||
|
(sut/format (-> (h/setting :default :system-time :as-of [:inline :DATE "2024-11-24"])
|
||||||
|
(h/select :*)
|
||||||
|
(h/from :table)))))
|
||||||
|
(is (= ["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023' SELECT * FROM table"]
|
||||||
|
(sut/format (-> (h/setting [:snapshot-time :to [:inline :DATE "2024-11-24"]]
|
||||||
|
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]])
|
||||||
|
(h/select :*)
|
||||||
|
(h/from :table)))))))
|
||||||
|
|
||||||
|
(deftest issue-571
|
||||||
|
(testing "an empty where clause is omitted"
|
||||||
|
(is (= ["SELECT * FROM foo"]
|
||||||
|
(sut/format {:select :* :from :foo :where []})))
|
||||||
|
#?(:clj
|
||||||
|
(is (thrown? clojure.lang.ExceptionInfo
|
||||||
|
(sut/format {:select :* :from :foo :where nil}))))
|
||||||
|
(is (= ["SELECT * FROM foo WHERE 1 = 1"]
|
||||||
|
(sut/format {:select :* :from :foo :where [:= 1 1]} {:inline true}))))
|
||||||
|
(testing "an empty order by clause is omitted"
|
||||||
|
(is (= ["SELECT * FROM foo"]
|
||||||
|
(sut/format {:select :* :from :foo :order-by []})))
|
||||||
|
#?(:clj
|
||||||
|
(is (thrown? clojure.lang.ExceptionInfo
|
||||||
|
(sut/format {:select :* :from :foo :order-by nil}))))
|
||||||
|
(is (= ["SELECT * FROM foo ORDER BY bar ASC"]
|
||||||
|
(sut/format {:select :* :from :foo :order-by [:bar]})))))
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
;; partial (incorrect!) workaround for #407:
|
;; partial (incorrect!) workaround for #407:
|
||||||
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})
|
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})
|
||||||
|
|
@ -1399,4 +1522,9 @@ ORDER BY id = ? DESC
|
||||||
:select [:*]
|
:select [:*]
|
||||||
:from [:a-b.b-c.c-d]}
|
:from [:a-b.b-c.c-d]}
|
||||||
(sut/format {:dialect :nrql}))
|
(sut/format {:dialect :nrql}))
|
||||||
|
(sut/format {:select :a:b.c}) ; quotes a:b
|
||||||
|
(sut/format [:. :a :b :c]) ; a.b.c
|
||||||
|
(sut/format [:. :a :b :c :d]) ; drops d ; a.b.c
|
||||||
|
(sut/format [:.:. :a :b :c]) ; .(a, b, c)
|
||||||
|
(sut/format '(.:. a b c)) ; .(a, b, c)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,20 @@
|
||||||
(is (= "1, 2, 3, 4"
|
(is (= "1, 2, 3, 4"
|
||||||
(sut/join ", " (remove nil?) [1 nil 2 nil 3 nil nil nil 4])))
|
(sut/join ", " (remove nil?) [1 nil 2 nil 3 nil nil nil 4])))
|
||||||
(is (= "" (sut/join ", " (remove nil?) [nil nil nil nil]))))
|
(is (= "" (sut/join ", " (remove nil?) [nil nil nil nil]))))
|
||||||
|
|
||||||
|
(deftest split-by-separator-test
|
||||||
|
(is (= [""] (sut/split-by-separator "" ".")))
|
||||||
|
(is (= ["" ""] (sut/split-by-separator "." ".")))
|
||||||
|
(is (= ["hello"] (sut/split-by-separator "hello" ".")))
|
||||||
|
(is (= ["h" "e" "l" "l" "o"] (sut/split-by-separator "h.e.l.l.o" ".")))
|
||||||
|
(is (= ["" "h" "e" "" "" "l" "" "l" "o" ""]
|
||||||
|
(sut/split-by-separator ".h.e...l..l.o." "."))))
|
||||||
|
|
||||||
|
(deftest into*-test
|
||||||
|
(is (= [1] (sut/into* [1] nil)))
|
||||||
|
(is (= [1] (sut/into* [1] [])))
|
||||||
|
(is (= [1] (sut/into* [1] nil [] nil [])))
|
||||||
|
(is (= [1 2 3] (sut/into* [1] [2 3])))
|
||||||
|
(is (= [1 2 3 4 5 6] (sut/into* [1] [2 3] [4 5 6])))
|
||||||
|
(is (= [1 2 3 4 5 6 7] (sut/into* [1] [2 3] [4 5 6] [7])))
|
||||||
|
(is (= [1 2 3 4 5 6 7 8 9] (sut/into* [1] [2 3] [4 5 6] [7] [8 9]))))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue