Compare commits

...

97 commits

Author SHA1 Message Date
Sean Corfield
f5dbc274be
fixes #440 by supporting multiple tables in truncate
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-23 17:45:59 -04:00
Sean Corfield
df753e8635
update tools.build
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-27 23:06:49 -07:00
Sean Corfield
b4b2ca7d79
add general using-* index support
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-26 10:28:57 -07:00
Sean Corfield
78f7d5282f
update dev/build deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-25 17:19:42 -07:00
Sean Corfield
024d17b11e
fixes #571 by supporting empty order by clause
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-22 10:34:57 -07:00
Sean Corfield
7611871935
prep for 2.7.1295
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 16:02:47 -07:00
Sean Corfield
1b042687f4
Merge pull request #569 from seancorfield/issue-561
Fixes #561 by dropping support for clojure 1.9
2025-03-12 15:56:06 -07:00
Sean Corfield
a981ed9171
restore bb logic on sym/kw
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 15:52:44 -07:00
Sean Corfield
74cf16c134
fix kw->sym for cljs!
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 15:47:58 -07:00
Sean Corfield
2c8fc30b1d
restore some clojure-only optimizations on keywords
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 15:39:24 -07:00
Sean Corfield
3906aa53c0
remove accidentally duped test
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 15:30:30 -07:00
Sean Corfield
92e4e16b45
a couple of minor build script fixes for dropping 1.9
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 15:28:19 -07:00
Sean Corfield
30b5fabe58
Merge branch 'develop' into issue-561
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 14:51:18 -07:00
Sean Corfield
d74046c658
fix #570 by adding :at
there is also :.:.

Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 14:48:09 -07:00
Sean Corfield
44494e61c0
remove failing tests for #570
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 13:43:02 -07:00
Sean Corfield
d70e89ae3b
part of #570 -- colon path selection
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 13:40:40 -07:00
Sean Corfield
67ea477a5c
part of #570 -- colon path selection
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-12 13:36:49 -07:00
Sean Corfield
89f39be55c
clarify 2.7 versions
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-06 13:25:10 -08:00
Sean Corfield
0d1fd0e901
address #561 by dropping support for clojure 1.9
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-06 13:20:16 -08:00
Sean Corfield
675c94b294
prep for 2.6.1281
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-06 12:56:49 -08:00
Sean Corfield
03d96f5747
fixes #568
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-20 21:34:50 -08:00
Sean Corfield
f7dbfba57c
and exclude assert in readme refer
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-20 17:13:59 -08:00
Sean Corfield
f0eb68f151
add helper for #567 and helper-based tests
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-20 17:12:31 -08:00
Sean Corfield
4d1f5f83b7
fixes #567
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-20 17:03:44 -08:00
Sean Corfield
c2990597f1
fixes #566
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-02-20 16:47:27 -08:00
Sean Corfield
695351e33c
fix 2.6.1270/2.6.1267 reference
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-28 15:28:55 -08:00
Sean Corfield
0cd81b5d9b
note doc update in change log
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-28 12:50:04 -08:00
Sean Corfield
c295db44c0
add examples of :alias with :group-by
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-28 11:10:53 -08:00
Sean Corfield
44ca426b78
cleanup unused symbols
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-21 15:21:59 -08:00
Sean Corfield
e3fcb3e278
Delete .gitpod.yml 2025-01-20 14:36:05 -08:00
Sean Corfield
7e1fe8f558
prep for 2.6.1270
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-17 13:23:52 -08:00
Sean Corfield
13eb8fe859
Merge pull request #564 from alexander-yakushev/fix-warning
Fix autoboxing warning
2025-01-17 21:17:18 +00:00
Oleksandr Yakushev
4bc1d16f24 Fix autoboxing warning 2025-01-17 23:10:59 +02:00
Sean Corfield
206f980093
prep for 2.6.1267
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-16 16:14:54 -08:00
Sean Corfield
3beaa6b2bf
Merge pull request #562 from alexander-yakushev/opt
More small optimizations
2025-01-17 00:05:07 +00:00
Sean Corfield
30a04975f5
Merge branch 'develop' into opt 2025-01-17 00:01:35 +00:00
Sean Corfield
4f5b0ed256
fix default values clause bug
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-16 15:57:54 -08:00
Sean Corfield
1681764830
Merge branch 'develop' into opt 2025-01-16 23:52:40 +00:00
Sean Corfield
6c0c66e371
note pr #563 in change log
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-16 12:45:24 -08:00
Sean Corfield
2c793ce441
Merge pull request #563 from krevedkokun/expressions-in-with
Support expressions in WITH clauses
2025-01-16 20:43:36 +00:00
Nikita Domnitskii
f05c7051e2
Support expressions in WITH clauses 2025-01-16 18:25:52 +06:00
Oleksandr Yakushev
d086631e54 Refactor format-values to do fewer allocations 2025-01-08 18:08:13 +02:00
Oleksandr Yakushev
3ecac63bea Check for hyphen in dehyphen before calling str/replace 2025-01-08 18:01:43 +02:00
Sean Corfield
8e0d6984bd
note optimizations in changelog
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-01-01 11:02:58 -08:00
Sean Corfield
94fae3437f
Merge pull request #560 from alexander-yakushev/opt
More optimizations
2025-01-01 11:00:18 -08:00
Oleksandr Yakushev
316f36751f Add into* for concatenating multiple sequences without transient roundtrip 2024-12-30 13:19:02 +02:00
Oleksandr Yakushev
8ae93d91f6 Reuse keyword's symbol when in CLJ runtime 2024-12-30 13:19:02 +02:00
Oleksandr Yakushev
5fa85400f0 Replace clojure.string/split with a custom splitter 2024-12-30 00:29:20 +02:00
Sean Corfield
045634fd3c Merge branch 'develop' of github.com:seancorfield/honeysql into develop 2024-12-28 11:13:20 -08:00
Sean Corfield
81167cb77e
note pr #559 in change log
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-28 11:13:17 -08:00
Sean Corfield
a3b79215c4
Merge pull request #559 from whatacold/web-app-link
fix broken links
2024-12-28 11:12:45 -08:00
Ken Huang
0cbe76329e fix broken links 2024-12-28 23:25:44 +08:00
Sean Corfield
c93eef06f6
add logo to zulip badge
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-22 10:40:22 -08:00
Sean Corfield
60f5662d81
Add Zulip badge 2024-12-19 20:41:58 -08:00
Sean Corfield
6531413325
working examples for sql server auto-lifting true
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-15 16:03:08 -08:00
Sean Corfield
fce39548d0
fix mixed arg test for Clojure 1.12
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-15 16:02:28 -08:00
Sean Corfield
0f26e7d060
SQL Server has no TRUE / FALSE literals
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-14 12:35:41 -08:00
Sean Corfield
c98df6dd97
prep for 2.6.1243
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-13 22:05:19 -08:00
Sean Corfield
7a24fd0367
incorporate feedback from @jarohen
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-12 11:07:30 -08:00
Sean Corfield
30d177165d
fixes #556 by documenting all the xtdb stuff
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-11 15:37:23 -08:00
Sean Corfield
e4762a1a70
fixes #558 by implementing patch-into
also fix records helper and document a some more xtdb support

Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-11 11:50:47 -08:00
Sean Corfield
e0356bc9c5
example of alias with columns
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-10 11:45:22 -08:00
Sean Corfield
aa1f2bc0f6
update dev/test deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-02 19:53:23 -08:00
Sean Corfield
0272c7b9ed
note assertion change
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-01 13:47:44 -08:00
Sean Corfield
10ec823151
switch asserts to proper validation and exceptions
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-12-01 13:24:41 -08:00
Sean Corfield
fdfc6bc997
improve xtdb test
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-29 21:49:44 -08:00
Sean Corfield
3f1677bff2
fixes #555 by implementing SETTING clause
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-24 22:40:20 -08:00
Sean Corfield
782bc4b78a Merge branch 'develop' of github.com:seancorfield/honeysql into develop 2024-11-23 18:26:56 -08:00
Sean Corfield
f2763d5af5
remove experimental xtdb dialect - no longer needed
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 18:26:53 -08:00
Sean Corfield
573d6c75ca
new clj-kondo imports
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 15:31:58 -08:00
Sean Corfield
42d5f4baf1
prep for 2.6.1230
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 13:25:59 -08:00
Sean Corfield
8320571c4d
implement get-in #532
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 13:16:23 -08:00
Sean Corfield
559e71205d
add record/object special syntax #532
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 12:17:43 -08:00
Sean Corfield
1bac4352e3
add records support #532
still need to add record/object special syntax

Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 11:59:12 -08:00
Sean Corfield
b64ab9b0b0
remove auto-start repl/connect
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 11:03:12 -08:00
Sean Corfield
049fe5b68b
fix sqlize of maps in cljs
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-23 00:11:49 -08:00
Sean Corfield
b716d077c4
allow for inline hash maps (record syntax)
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 23:46:56 -08:00
Sean Corfield
09fa8afefe
add erase-from helper #532
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 23:17:16 -08:00
Sean Corfield
129239a742
add erase from #532
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 23:14:02 -08:00
Sean Corfield
ee53c54255
test xtdb dialect and quoted columns
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 23:03:28 -08:00
Sean Corfield
f4d212ae18
address #532 by supporting exclude/rename in alias position
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 22:54:42 -08:00
Sean Corfield
e2f7991ad8
fixes #552 by turning the assertion into a test
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 15:25:52 -08:00
Sean Corfield
c0c455358f
add #553 / #554 to change log
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-22 12:59:55 -08:00
Sean Corfield
488ddd4bcb
Merge pull request #554 from plooney81/issue-553
Fixes #553 by adding :not-between
2024-11-22 12:58:00 -08:00
Pete Looney
b2c1ae0068 Fixes #553 by adding :not-between 2024-11-22 13:24:31 -06:00
Sean Corfield
21ce3a2242
fixes #551 by supporting multiple window clauses
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-18 15:02:28 -08:00
Sean Corfield
6fa606ffd5
address #532 by adding exclude, rename
and tests for nest_one, nest_many

Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-12 15:58:32 -08:00
Sean Corfield
c1c7cba96a
new clj-kondo imports
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-11-05 09:08:30 -08:00
Sean Corfield
4992a3cb76
note support for Babashka and ClojureScript
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-10-29 09:28:52 -07:00
Sean Corfield
bbac863a2a
run bb tests on develop and PR; add #549 to changelog
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-10-29 09:19:25 -07:00
Sean Corfield
44399b1984
Merge pull request #550 from borkdude/bb-test-runner
Fix bb compat, fixes #549
2024-10-29 09:14:47 -07:00
Michiel Borkent
4288ceae56 Exclude core.cache from bb deps 2024-10-29 17:10:54 +01:00
Michiel Borkent
38080aff92 Fix java 2024-10-29 17:08:43 +01:00
Michiel Borkent
9999a90e62 Name of runner 2024-10-29 17:08:14 +01:00
Michiel Borkent
7892ec6006
Merge branch 'develop' into bb-test-runner 2024-10-29 17:07:21 +01:00
Michiel Borkent
362818530a Fix bb compat, fixes #549 2024-10-29 17:04:56 +01:00
Sean Corfield
643cea4930
address #549 by restoring bb support
Signed-off-by: Sean Corfield <sean@corfield.org>
2024-10-29 08:49:19 -07:00
35 changed files with 1281 additions and 320 deletions

View file

@ -0,0 +1 @@
{:lint-as {babashka.fs/with-temp-dir clojure.core/let}}

View file

@ -0,0 +1,3 @@
{:hooks
{:analyze-call {org.httpkit.server/with-channel httpkit.with-channel/with-channel}}}

View file

@ -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})))

View 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/->>}}

View file

@ -0,0 +1 @@
{:hooks {:analyze-call {taoensso.encore/defalias taoensso.encore/defalias}}}

View 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))}))

View file

@ -19,7 +19,7 @@ jobs:
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.12.0.1479'
cli: '1.12.0.1530'
- name: Cache All The Things
uses: actions/cache@v4
with:

View file

@ -17,7 +17,7 @@ jobs:
- name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.12.0.1479'
cli: '1.12.0.1530'
- name: Cache All The Things
uses: actions/cache@v4
with:
@ -49,7 +49,7 @@ jobs:
- name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.12.0.1479'
cli: '1.12.0.1530'
- name: Cache All The Things
uses: actions/cache@v4
with:

33
.github/workflows/test-bb.yml vendored Normal file
View 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

View file

@ -17,7 +17,7 @@ jobs:
- name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master
with:
cli: '1.12.0.1479'
cli: '1.12.0.1530'
- name: Cache All The Things
uses: actions/cache@v4
with:

View file

@ -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
View file

@ -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
}

View file

@ -1,5 +1,46 @@
# 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.
@ -36,7 +77,7 @@
* 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 [#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.
* 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).
@ -235,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 #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.
* 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.
* 2.0.0-rc5 (for testing; 2021-07-17)

View file

@ -4,17 +4,19 @@ SQL as Clojure data structures. Build queries programmatically -- even at runtim
## Build
[![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/honeysql_2.6.1203-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/honeysql)
[![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/honeysql?2.6.1203)](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
[![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/honeysql_2.7.1295-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/honeysql)
[![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/honeysql?2.7.1295)](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
[![Slack](https://img.shields.io/badge/slack-HoneySQL-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=honeysql)
[![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net)
[![Zulip](https://img.shields.io/badge/zulip-honeysql-orange.svg?logo=zulip)](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.
> 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).
@ -46,7 +48,7 @@ section of the documentation before trying to use HoneySQL to build your own que
From Clojure:
<!-- {:test-doc-blocks/reader-cond :clj} -->
```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]
;; CAUTION: this overwrites several clojure.core fns:
;;

8
bb.edn Normal file
View 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$"]}}}}

View file

@ -19,7 +19,7 @@
[deps-deploy.deps-deploy :as dd]))
(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 snapshot (the-version "9999-SNAPSHOT"))
(def class-dir "target/classes")
@ -47,8 +47,7 @@
"Generate and run doc tests.
Optionally specify :aliases vector:
[:1.9] -- test against Clojure 1.9 (the default)
[:1.10] -- test against Clojure 1.10.3
[:1.10] -- test against Clojure 1.10.3 (the default)
[:1.11] -- test against Clojure 1.11.0
[:1.12] -- test against Clojure 1.12.0
[:cljs] -- test against ClojureScript"
@ -63,6 +62,7 @@
(defn test "Run basic tests." [opts]
(run-task [:test :runner :1.11])
(run-task [:test :runner :cljs])
opts)
(defn- pom-template [version]
@ -98,10 +98,10 @@
(defn ci
"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."
[opts]
(let [aliases [:cljs :elide :1.10 :1.11 :1.12]
(let [aliases [:cljs :elide :1.11 :1.12]
opts (jar-opts opts)]
(b/delete {:path "target"})
(doseq [alias aliases]

View file

@ -16,7 +16,8 @@
;;"doc/operator-reference.md"
"doc/options.md"
"doc/postgresql.md"
"doc/special-syntax.md"]
"doc/special-syntax.md"
"doc/xtdb.md"]
regen-reason (if (not (fs/exists? success-marker))
"a previous successful gen result not found"
(let [newer-thans (fs/modified-since target

View file

@ -1,14 +1,13 @@
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.9.0"}}
:deps {org.clojure/clojure {:mvn/version "1.10.3"}}
:aliases
{;; 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"}}
:ns-default build}
;; 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.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
@ -31,7 +30,7 @@
:main-opts ["-m" "cljs-test-runner.main"]}
: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"}}
:main-opts ["-m" "honey.gen-doc-tests"]}

View file

@ -149,6 +149,14 @@ user=> (sql/format {:create-index [:my-idx [: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
Used with `:alter-table`,
@ -670,9 +678,9 @@ user=> (sql/format '{select * bulk-collect-into [arrv 100] from mytable})
["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
table name or a table/alias pair),
@ -690,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
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
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
statement but otherwise has identical syntax to `:insert-into`.
@ -794,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]
```
## delete, delete-from
## delete, delete-from, erase-from
`:delete-from` is the simple use case here, accepting just a
SQL entity (table name). `:delete` allows for deleting from
@ -811,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]
```
`: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` 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
user=> (sql/format '{truncate transport})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport)})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (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
@ -1062,6 +1088,9 @@ The `:where` clause can have a single SQL expression, or
a sequence of SQL expressions prefixed by either `:and`
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
tests several columns for equality, and you might have a Clojure
hash map containing those values. `honey.sql/map=` exists to
@ -1083,6 +1112,11 @@ user=> (sql/format '{select (*) from (table)
["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
The `:having` clause works identically to `:where` above
@ -1090,7 +1124,7 @@ but is rendered into the SQL later in precedence order.
## 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).
`:partition-by` accepts the same arguments as `:select` above
@ -1116,6 +1150,25 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
FROM employee
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!):
user=> (sql/format (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
@ -1128,6 +1181,18 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
FROM employee
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`:
@ -1166,12 +1231,15 @@ user=> (sql/format {:select [[[:over
## 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
SQL entity or a pair of a SQL expression and a direction
(which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`,
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
as a pair with a direction:
@ -1382,7 +1450,7 @@ user=> (sql/format {:insert-into :table
:values [{:a 1 :b 2 :c 3}
:default
{: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
:values [[1 2 3] :default [4 5 6]]})
["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]

View file

@ -7,6 +7,7 @@
["SQL Operator Reference" {:file "doc/operator-reference.md"}]
["SQL 'Special Syntax'" {:file "doc/special-syntax.md"}]
["PostgreSQL Support" {:file "doc/postgresql.md"}]
["XTDB Support" {:file "doc/xtdb.md"}]
["New Relic NRQL Support" {:file "doc/nrql.md"}]
["Other Databases" {:file "doc/databases.md"}]]
["All the Options" {:file "doc/options.md"}]

View file

@ -1,6 +1,8 @@
# 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
databases.

View file

@ -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
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
@ -63,7 +63,7 @@ Supported Clojure versions: 1.7 and later.
In `deps.edn`:
<!-- :test-doc-blocks/skip -->
```clojure
com.github.seancorfield/honeysql {:mvn/version "2.6.1203"}
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
```
Required as:
@ -90,7 +90,7 @@ The new namespaces are:
* `honey.sql` -- the primary API (just `format` now),
* `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

View file

@ -10,14 +10,14 @@ For the Clojure CLI, add the following dependency to your `deps.edn` file:
<!-- :test-doc-blocks/skip -->
```clojure
com.github.seancorfield/honeysql {:mvn/version "2.6.1203"}
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"}
```
For Leiningen, add the following dependency to your `project.clj` file:
<!-- :test-doc-blocks/skip -->
```clojure
[com.github.seancorfield/honeysql "2.6.1203"]
[com.github.seancorfield/honeysql "2.7.1295"]
```
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
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
@ -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,
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!

View file

@ -29,6 +29,14 @@ and strings.
:from :b
:order-by [[[:alias :'some-alias]]]})
;;=> ["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
@ -80,6 +88,29 @@ In the subquery case, produces `ARRAY(subquery)`:
;;=> ["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
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
cannot be an expression.
## between
## between and not-between
Accepts three arguments: an expression, a lower bound, and
an upper bound:
@ -101,6 +132,9 @@ an upper bound:
```clojure
(sql/format-expr [:between :id 1 100])
;;=> ["id BETWEEN ? AND ?" 1 100]
(sql/format-expr [:not-between :id 1 100])
;;=> ["id NOT BETWEEN ? AND ?" 1 100]
```
## case
@ -200,15 +234,23 @@ Accepts a single expression and prefixes it with `DISTINCT `:
;;=> ["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
(sql/format {: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:
```clojure
@ -216,6 +258,9 @@ Can be used with `:nest` for field selection from composites:
;;=> ["SELECT (v).*, (MYFUNC(x)).y"]
```
See also [`get-in`](xtdb.md#object-navigation-expressions)
and [`at`](#at) for additional path navigation functions.
## entity
Accepts a single keyword or symbol argument and produces a

220
doc/xtdb.md Normal file
View 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')"]
```

View file

@ -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
"Primary API for HoneySQL 2.x.
@ -31,7 +31,9 @@
(:require [clojure.string :as str]
#?(:clj [clojure.template])
[honey.sql.protocols :as p]
[honey.sql.util :refer [str join]]))
[honey.sql.util :refer [str join split-by-separator into*]]))
#?(:clj (set! *warn-on-reflection* true))
;; default formatting for known clauses
@ -55,12 +57,13 @@
:refresh-materialized-view
:create-index
;; then SQL clauses in priority order:
:setting
:raw :nest :with :with-recursive :intersect :union :union-all :except :except-all
:table
:table :assert ; #567 XTDB
:select :select-distinct :select-distinct-on :select-top :select-distinct-top
:distinct :expr
:distinct :expr :exclude :rename
:into :bulk-collect-into
:insert-into :replace-into :update :delete :delete-from :truncate
:insert-into :patch-into :replace-into :update :delete :delete-from :erase-from :truncate
:columns :set :from :using
:join-by
:join :left-join :right-join :inner-join :outer-join :full-join
@ -69,7 +72,7 @@
;; NRQL extension:
:facet
:window :partition-by
:order-by :limit :offset :fetch :for :lock :values
:order-by :limit :offset :fetch :for :lock :values :records
:on-conflict :on-constraint :do-nothing :do-update-set :on-duplicate-key-update
:returning
:with-data
@ -111,17 +114,15 @@
(assoc m k (assoc v :dialect k)))
{}
{:ansi {:quote #(strop "\"" % "\"")}
:sqlserver {:quote #(strop "[" % "]")}
:sqlserver {:quote #(strop "[" % "]")
:auto-lift-boolean true}
:mysql {:quote #(strop "`" % "`")
:clause-order-fn
#(add-clause-before % :set :where)}
:nrql {:quote #(strop "`" % "`")
:col-fn #(if (keyword? %) (subs (str %) 1) (str %))
:parts-fn vector}
:oracle {:quote #(strop "\"" % "\"") :as false}
:xtdb {:quote #(strop "\"" % "\"")
:col-fn #(if (keyword? %) (subs (str %) 1) (str %))
:parts-fn #(str/split % #"\.")}})))
:oracle {:quote #(strop "\"" % "\"") :as false}})))
; should become defonce
(def ^:private default-dialect (atom (:ansi @dialects)))
@ -228,7 +229,8 @@
Hyphens at the start or end of a string should not be touched."
[s]
(str/replace s #"(\w)-(?=\w)" "$1 "))
(cond-> s
(str/includes? s "-") (str/replace #"(\w)-(?=\w)" "$1 ")))
(defn- namespace-_
"Return the namespace portion of a symbol, with dashes converted."
@ -317,7 +319,7 @@
[n %]
(if aliased
[%]
(str/split % #"\."))))
(split-by-separator % "."))))
parts (parts-fn col-e)
entity (join "." (map #(cond-> % (not= "*" %) (quote-fn))) parts)]
(suspicious-entity-check entity)
@ -368,6 +370,29 @@
(keyword (name s)))
s))
(defn- kw->sym
"Given a keyword, produce a symbol, retaining the namespace
qualifier, if any."
[k]
(if (keyword? k)
#?(:bb (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k)))
:clj (.sym ^clojure.lang.Keyword k)
:default (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k))))
k))
(defn- inline-map [x & [open close]]
(str (or open "{")
(join ", " (map (fn [[k v]]
(str (format-entity k)
": "
(p/sqlize v))))
x)
(or close "}")))
(extend-protocol p/InlineValue
nil
(sqlize [_] "NULL")
@ -379,11 +404,17 @@
(sqlize [x] (sql-kw x))
#?(:cljs PersistentVector :default clojure.lang.IPersistentVector)
(sqlize [x] (str "[" (join ", " (map p/sqlize) x) "]"))
#?(:cljs PersistentArrayMap :default clojure.lang.IPersistentMap)
(sqlize [x] (inline-map x))
#?@(:cljs [PersistentHashMap
(sqlize [x] (inline-map x))])
#?@(:clj [java.util.UUID
;; issue 385: quoted UUIDs for PostgreSQL/ANSI
(sqlize [x] (str \' x \'))])
#?(:cljs default :default Object)
(sqlize [x] (str x)))
(sqlize [x] (if (string? x)
(str \' (str/replace x "'" "''") \')
(str x))))
(defn- sqlize-value [x] (p/sqlize x))
@ -419,7 +450,8 @@
(defn- format-simple-var
([x]
(let [c (if (keyword? x)
#?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon
#?(:bb (subs (str x) 1)
:clj (str (.sym ^clojure.lang.Keyword x))
:default (subs (str x) 1))
(str x))]
(format-simple-var x c {})))
@ -427,8 +459,8 @@
(if (str/starts-with? c "'")
(do
(reset! *formatted-column* true)
[(subs c 1)])
[(format-entity x opts)])))
(subs c 1))
(format-entity x opts))))
(defn- format-var
([x] (format-var x {}))
@ -437,11 +469,12 @@
;; for multiple / in the %fun.call case so that
;; qualified column names can be used:
(let [c (if (keyword? x)
#?(:clj (str (.sym ^clojure.lang.Keyword x)) ;; Omits leading colon
#?(:bb (subs (str x) 1)
:clj (str (.sym ^clojure.lang.Keyword x))
:default (subs (str x) 1))
(str x))]
(cond (str/starts-with? c "%")
(let [[f & args] (str/split (subs c 1) #"\.")]
(let [[f & args] (split-by-separator (subs c 1) ".")]
[(str (format-fn-name f) "("
(join ", " (map #(format-entity (keyword %) opts)) args)
")")])
@ -454,7 +487,7 @@
:else
["?" (->param k)]))
:else
(format-simple-var x c opts)))))
[(format-simple-var x c opts)]))))
(defn- format-entity-alias [x]
(cond (sequential? x)
@ -509,14 +542,10 @@
:else
(throw (ex-info "bigquery * only supports except and replace"
{:clause k :arg arg})))]
(-> [(cond->> sql' sql (str sql " "))]
(into params)
(into params'))))
(into* [(cond->> sql' sql (str sql " "))] params params')))
[]
(partition-all 2 x))]
(-> [(str sql " " sql')]
(into params)
(into params'))))
(into* [(str sql " " sql')] params params')))
(comment
(bigquery-*-except-replace? [:* :except [:a :b :c]])
@ -631,11 +660,18 @@
[sql & params] (if (map? selectable)
(format-dsl selectable {:nested true})
(format-expr selectable))
*-qualifier (and (map? alias)
(some #(contains? alias %)
[:exclude :rename
'exclude 'rename]))
[sql' & params'] (when alias
(if (sequential? alias)
(let [[sqls params] (format-expr-list alias {:aliased true})]
(into [(join " " sqls)] params))
(format-selectable-dsl alias {:aliased true})))
(cond (sequential? alias)
(let [[sqls params] (format-expr-list alias {:aliased true})]
(into [(join " " sqls)] params))
*-qualifier
(format-dsl alias)
:else
(format-selectable-dsl alias {:aliased true})))
[sql'' & params''] (when temporal
(format-temporal temporal))]
@ -644,17 +680,16 @@
(str " " sql''))
(when sql' ; alias
(str (if as
(if (and (contains? *dialect* :as)
(not (:as *dialect*)))
(if (or *-qualifier
(and (contains? *dialect* :as)
(not (:as *dialect*))))
" "
" AS ")
" ")
sql'))
(when hints
(str " WITH (" hints ")")))]
(into params)
(into params')
(into params'')))))
(into* params params' params'')))))
(defn- format-selectable-dsl
([x] (format-selectable-dsl x {}))
@ -728,9 +763,8 @@
(let [[cur & params] (peek result)
[sql & params'] (first exprs)]
(recur (rest exprs) args' false (conj (pop result)
(-> [(str cur " " sql)]
(into params)
(into params')))))
(into* [(str cur " " sql)]
params params'))))
(recur (rest exprs) args' false (conj result (first exprs))))))
(reduce-sql result)))))
@ -768,33 +802,40 @@
(defn- format-columns [k xs]
(if (and (= :columns k)
(or (contains-clause? :insert-into)
(contains-clause? :patch-into)
(contains-clause? :replace-into)))
[]
(let [[sqls params] (format-expr-list xs {:drop-ns true})]
(into [(str "(" (join ", " sqls) ")")] params))))
(defn- format-selects-common [prefix as xs]
(let [qualifier (format-meta xs)
prefix (if prefix
(cond-> prefix qualifier (str " " qualifier))
qualifier)]
(if (sequential? xs)
(let [[sqls params] (reduce-sql (map #(format-selectable-dsl % {:as as})) xs)]
(when-not (= :none *checking*)
(when (empty? xs)
(throw (ex-info (str prefix " empty column list is illegal")
{:clause (into [prefix] xs)}))))
(into [(str (when prefix (str prefix " ")) (join ", " sqls))] params))
(let [[sql & params] (format-selectable-dsl xs {:as as})]
(into [(str (when prefix (str prefix " ")) sql)] params)))))
(defn- format-selects-common
([prefix as xs] (format-selects-common prefix as xs nil))
([prefix as xs wrap]
(let [qualifier (format-meta xs)
prefix (if prefix
(cond-> prefix qualifier (str " " qualifier))
qualifier)
[pre post] (when (and wrap (sequential? xs) (< 1 (count xs)))
["(" ")"])]
(if (sequential? xs)
(let [[sqls params] (reduce-sql (map #(format-selectable-dsl % {:as as})) xs)]
(when-not (= :none *checking*)
(when (empty? xs)
(throw (ex-info (str prefix " empty column list is illegal")
{:clause (into [prefix] xs)}))))
(into [(str (when prefix (str prefix " ")) pre (join ", " sqls) post)] params))
(let [[sql & params] (format-selectable-dsl xs {:as as})]
(into [(str (when prefix (str prefix " ")) sql)] params))))))
(defn- format-selects [k xs]
(format-selects-common
(sql-kw k)
(#{:select :select-distinct :from :window :delete-from :facet
'select 'select-distinct 'from 'window 'delete-from 'facet}
(#{:select :select-distinct :rename :from :window :delete-from :facet
'select 'select-distinct 'rename 'from 'window 'delete-from 'facet
}
k)
xs))
xs
(#{:exclude :rename 'exclude 'rename} k)))
(defn- format-selects-on [_ xs]
(let [[on & cols] xs
@ -805,7 +846,7 @@
(str (sql-kw :select) " " sql)
true
cols)]
(-> [sql'] (into params) (into params'))))
(into* [sql'] params params')))
(defn- format-select-top [k xs]
(let [[top & cols] xs
@ -833,7 +874,7 @@
(join " " (map sql-kw) parts))
true
cols)]
(-> [sql'] (into params) (into params'))))
(into* [sql'] params params')))
(defn- format-select-into [k xs]
(let [[v e] (ensure-sequential xs)
@ -919,25 +960,20 @@
(map
(fn [[x expr & tail :as with]]
(let [[sql & params] (format-with-part x)
non-query-expr? (or (ident? expr) (string? expr))
non-query-expr? (not (map? expr))
[sql' & params'] (if non-query-expr?
(format-expr expr)
(format-dsl expr))
[sql'' & params'' :as sql-params'']
(if non-query-expr?
(cond-> [(str sql' " AS " sql)]
params' (into params')
params (into params))
(into* [(str sql' " AS " sql)] params' params)
;; according to docs, CTE should _always_ be wrapped:
(cond-> [(str sql " " (as-fn with) " " (str "(" sql' ")"))]
params (into params)
params' (into params')))
(into* [(str sql " " (as-fn with) " " (str "(" sql' ")"))]
params params'))
[tail-sql & tail-params]
(format-with-query-tail tail)]
(if (seq tail-sql)
(cond-> [(str sql'' " " tail-sql)]
params'' (into params'')
tail-params (into tail-params))
(into* [(str sql'' " " tail-sql)] params'' tail-params)
sql-params''))))
xs)]
(into [(str (sql-kw k) " " (join ", " sqls))] params)))
@ -945,6 +981,9 @@
(defn- format-selector [k xs]
(format-selects k [xs]))
(defn- format-window [k xs]
(format-selects k (into [] (partition-all 2 xs))))
(declare columns-from-values)
(defn- format-insert [k table]
@ -978,10 +1017,7 @@
(str cols-sql' " "))
overriding
sql)]
(into t-params)
(into c-params)
(into cols-params')
(into params)))
(into* t-params c-params cols-params' params)))
(sequential? (second table))
(let [[table cols] table
[t-sql & t-params] (format-entity-alias table)
@ -991,23 +1027,20 @@
(join ", " c-sqls)
")"
overriding)]
(into t-params)
(into c-params)))
(into* t-params c-params)))
:else
(let [[sql & params] (format-entity-alias table)]
(-> [(str (sql-kw k) " " sql
(when (seq cols')
(str " " cols-sql'))
overriding)]
(into cols-params')
(into params))))
(into* cols-params' params))))
(let [[sql & params] (format-entity-alias table)]
(-> [(str (sql-kw k) " " sql
(when (seq cols')
(str " " cols-sql'))
overriding)]
(into cols-params')
(into params))))))
(into* cols-params' params))))))
(comment
(format-insert :insert-into [[[:raw ":foo"]] {:select :bar}])
@ -1035,12 +1068,10 @@
(str "("
(join ", " u-sqls)
")"))
(-> params (into params-j) (into u-params))])
(into* params params-j u-params)])
(let [[sql & params'] (when e (format-expr e))]
[(cond-> sqls e (conj "ON" sql))
(-> params
(into params-j)
(into params'))])))))
(into* params params-j params')])))))
[[] []]
clauses)]
(into [(join " " sqls)] params)))
@ -1102,11 +1133,13 @@
dirs (map #(when (sequential? %) (second %)) xs)
[sqls params]
(format-expr-list (map #(if (sequential? %) (first %) %) xs))]
(into [(str (sql-kw k) " "
(join ", " (map (fn [sql dir]
(str sql " " (sql-kw (or dir :asc))))
sqls
dirs)))] params)))
(if (seq sqls)
(into [(str (sql-kw k) " "
(join ", " (map (fn [sql dir]
(str sql " " (sql-kw (or dir :asc))))
sqls
dirs)))] params)
[])))
(defn- format-lock-strength [k xs]
(let [[strength tables nowait] (ensure-sequential xs)]
@ -1162,17 +1195,17 @@
x))
xs)))
[sqls params]
(reduce (fn [[sql params] [sqls' params']]
[(conj sql
(if (sequential? sqls')
(str "(" (join ", " sqls') ")")
sqls'))
(into params params')])
(reduce (fn [[sql params] x]
(if (sequential? x)
(let [[sqls' params'] (format-expr-list x)]
[(conj sql
(if (sequential? sqls')
(str "(" (join ", " sqls') ")")
sqls'))
(into* params params')])
[(conj sql (sql-kw x)) params]))
[[] []]
(map #(if (sequential? %)
(format-expr-list %)
[(sql-kw %)])
xs'))
xs')
sqls (if row-ctr (map #(str "ROW" %) sqls) sqls)]
(into [(str (sql-kw k) " " (join ", " sqls))] params))
@ -1180,29 +1213,30 @@
;; [{:a 1 :b 2 :c 3}]
(let [[cols cols-sql]
(columns-from-values xs (or (contains-clause? :insert-into)
(contains-clause? :patch-into)
(contains-clause? :replace-into)
(contains-clause? :columns)))
[sqls params]
(reduce (fn [[sql params] [sqls' params']]
[(conj sql
(if (sequential? sqls')
(str "(" (join ", " sqls') ")")
sqls'))
(if params' (into params params') params')])
[[] []]
(map (fn [m]
(if (map? m)
(format-expr-list
(map #(get m
%
;; issue #366: use NULL or DEFAULT
;; for missing column values:
(if (contains? *values-default-columns* %)
[:default]
nil))
cols))
[(sql-kw m)]))
xs))]
(reduce
(fn [[sql params] x]
(if (map? x)
(let [[sqls' params']
(reduce-sql (map #(format-expr
(get x %
;; issue #366: use NULL or DEFAULT
;; for missing column values:
(if (contains? *values-default-columns* %)
[:default]
nil))))
cols)]
[(conj sql
(if (sequential? sqls')
(str "(" (join ", " sqls') ")")
sqls'))
(into* params params')])
[(conj sql (sql-kw x)) params]))
[[] []]
xs)]
(into [(str (when cols-sql
(str cols-sql " "))
(sql-kw k)
@ -1247,8 +1281,7 @@
(str " (" (join ", " sqls) ")"))
(when sql
(str " " sql)))]
(into expr-params)
(into clause-params)))
(into* expr-params clause-params)))
(format-on-conflict k [x])))
(defn- format-do-update-set [k x]
@ -1267,8 +1300,7 @@
where (or (:where x) ('where x))
[sql & params] (when where (format-dsl {:where where}))]
(-> [(str sets (when sql (str " " sql)))]
(into set-params)
(into params)))
(into* set-params params)))
(format-set-exprs k x))
(sequential? x)
(let [[cols clauses] (split-with (complement map?) x)]
@ -1356,20 +1388,17 @@
[(butlast coll) (last coll) nil]))]
(into [(join " " (map sql-kw) prequel)
(when table
(let [[v & more] (format-simple-var table)]
(when (seq more)
(throw (ex-info (str "DDL syntax error at: "
(pr-str table)
" - expected table name")
{:unexpected more})))
v))
(format-simple-var table))
(when ine (sql-kw ine))]
(when opts
(format-ddl-options opts context)))))
(defn- format-truncate [_ xs]
(let [[table & options] (ensure-sequential xs)
[pre table ine options] (destructure-ddl-item [table options] "truncate")]
table (if (or (ident? table) (string? table))
(format-simple-var table)
(join ", " (map format-simple-var table)))
[pre _ ine options] (destructure-ddl-item [nil options] "truncate")]
(when (seq pre) (throw (ex-info "TRUNCATE syntax error" {:unexpected pre})))
(when (seq ine) (throw (ex-info "TRUNCATE syntax error" {:unexpected ine})))
[(join " " (cond-> ["TRUNCATE TABLE" table]
@ -1382,6 +1411,11 @@
(destructure-ddl-item [:id [:int :unsigned :auto-increment]] "test")
(destructure-ddl-item [[[:foreign-key :bar]] :quux [[:wibble :wobble]]] "test")
(format-truncate :truncate [:foo])
(format-truncate :truncate ["foo, bar"])
(format-truncate :truncate "foo, bar")
(format-truncate :truncate [[:foo :bar]])
(format-truncate :truncate :foo)
(format {:truncate [[:foo] :x]})
)
(defn- format-create [q k item as]
@ -1400,10 +1434,12 @@
(defn- format-create-index [k clauses]
(let [[index-spec [table & exprs]] clauses
[pre entity ine & more] (destructure-ddl-item index-spec (str (sql-kw k) " options"))
[using & exprs] (if (contains? #{:using-gin 'using-gin}
(first exprs))
exprs
(cons nil exprs))
[using & exprs]
(let [item (first exprs)]
(if (and (ident? item)
(str/starts-with? (str (kw->sym item)) "using-"))
exprs
(cons nil exprs)))
[sqls params] (format-expr-list exprs)]
(into [(join " " (remove empty?)
(-> ["CREATE" pre "INDEX" ine entity
@ -1538,6 +1574,40 @@
(let [[sql & params] (format-expr n)]
(into [(str (sql-kw k) " " sql)] params)))))
[(str (sql-kw k) " " (sql-kw args))]))
(defn- format-records
"Records can take a single map or a sequence of maps.
A map will be inherently treated as a lifted parameter.
Records can be inlined [:inline some-map]"
[k args]
(if (sequential? args)
(let [args (if (every? map? args)
(map #(vector :lift %) args)
args)
[sqls params] (format-expr-list args)]
(into [(str (sql-kw k) " " (join ", " sqls))] params))
(format-records k [args])))
(defn- format-setting
[k args]
(if (and (sequential? args) (ident? (first args)))
(format-setting k [args])
(let [[sqls params]
(reduce-sql
(map (fn [arg]
(let [[sqls params]
(reduce-sql (map (fn [x]
(if (ident? x)
[(if (str/ends-with? (name x) "-time")
(format-fn-name x)
(sql-kw x))]
(format-expr x)))
arg))]
(into [(join " " sqls)] params)))
args))]
(into [(str (sql-kw k) " " (join ", " sqls))] params))))
(defn- check-where
"Given a formatter function, performs a pre-flight check that there is
a non-empty where clause if at least basic checking is enabled."
@ -1591,6 +1661,7 @@
:drop-materialized-view #'format-drop-items
:refresh-materialized-view (fn [_ x] (format-create :refresh :materialized-view x nil))
:create-index #'format-create-index
:setting #'format-setting
:raw (fn [_ x] (raw-render x))
:nest (fn [_ x]
(let [[sql & params] (format-dsl x {:nested true})]
@ -1603,20 +1674,27 @@
:except #'format-on-set-op
:except-all #'format-on-set-op
:table #'format-selector
:assert (fn [k xs]
(let [[sql & params] (format-expr xs)]
(into [(str (sql-kw k) " " sql)] params)))
:select #'format-selects
:select-distinct #'format-selects
:select-distinct-on #'format-selects-on
:select-top #'format-select-top
:select-distinct-top #'format-select-top
:exclude #'format-selects
:rename #'format-selects
:distinct (fn [k xs] (format-selects k [[xs]]))
:expr (fn [_ xs] (format-selects nil [[xs]]))
:into #'format-select-into
:bulk-collect-into #'format-select-into
:insert-into #'format-insert
:patch-into #'format-insert
:replace-into #'format-insert
:update (check-where #'format-selector)
:delete (check-where #'format-selects)
:delete-from (check-where #'format-selector)
:erase-from (check-where #'format-selector)
:truncate #'format-truncate
:columns #'format-columns
:set #'format-set-exprs
@ -1633,7 +1711,7 @@
:where #'format-on-expr
:group-by #'format-group-by
:having #'format-on-expr
:window #'format-selector
:window #'format-window
:partition-by #'format-selects
:order-by #'format-order-by
:limit #'format-on-expr
@ -1652,6 +1730,7 @@
:for #'format-lock-strength
:lock #'format-lock-strength
:values #'format-values
:records #'format-records
:on-conflict #'format-on-conflict
:on-constraint #'format-selector
:do-nothing (fn [k _] (vector (sql-kw k)))
@ -1671,16 +1750,6 @@
(set @current-clause-order)
(set (keys @clause-format))))
(defn- kw->sym
"Given a keyword, produce a symbol, retaining the namespace
qualifier, if any."
[k]
(if (keyword? k)
(if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k)))
k))
(defn format-dsl
"Given a hash map representing a SQL statement and a hash map
of options, return a vector containing a string -- the formatted
@ -1708,7 +1777,7 @@
(if (seq leftover)
(throw (ex-info (str "These SQL clauses are unknown or have nil values: "
(join ", " (keys leftover))
"(perhaps you need [:lift {"
" (perhaps you need [:lift {"
(first (keys leftover))
" ...}] here?)")
leftover))
@ -1729,6 +1798,7 @@
"like" "not-like" "regexp" "~" "&&"
"ilike" "not-ilike" "similar-to" "not-similar-to"
"is" "is-not" "not=" "!=" "regex"
"is-distinct-from" "is-not-distinct-from"
"with-ordinality"}
(into (map str "+-*%|&^=<>"))
(into (keys infix-aliases))
@ -1774,23 +1844,18 @@
(= 1 (count params-y))
(coll? v1))
(let [sql (str "(" (join ", " (repeat (count v1) "?")) ")")]
(-> [(str sql-x " " (sql-kw in) " " sql)]
(into params-x)
(into v1)))
(into* [(str sql-x " " (sql-kw in) " " sql)] params-x v1))
(and *numbered*
(= (str "$" (count @*numbered*)) sql-y)
(= 1 (count params-y))
(coll? v1))
(let [vs (for [v v1] (->numbered v))
sql (str "(" (join ", " (map first) vs) ")")]
(-> [(str sql-x " " (sql-kw in) " " sql)]
(into params-x)
(conj nil)
(into (map second vs))))
(into* [(str sql-x " " (sql-kw in) " " sql)]
params-x [nil] (map second vs)))
:else
(-> [(str sql-x " " (sql-kw in) " " sql-y)]
(into params-x)
(into (if *numbered* values params-y))))))
(into* [(str sql-x " " (sql-kw in) " " sql-y)]
params-x (if *numbered* values params-y)))))
(defn- function-0 [k xs]
[(str (sql-kw k)
@ -1834,7 +1899,7 @@
(let [[sql-e & params-e] (format-expr e)
[sql-c & params-c] (format-dsl c {:nested true})]
[(conj sqls (str sql-e " " (sql-kw k) " " sql-c))
(-> params (into params-e) (into params-c))]))
(into* params params-e params-c)]))
[[] []]
(partition 2 pairs))]
(into [(join ", " sqls)] params)))
@ -1853,7 +1918,7 @@
(= 'else condition))
(conj sqls (sql-kw :else) sqlv)
(conj sqls (sql-kw :when) sqlc (sql-kw :then) sqlv))
(-> params (into paramsc) (into paramsv))]))
(into* params paramsc paramsv)]))
[[] []]
(partition 2 (if case-expr? (rest clauses) clauses)))]
(-> [(str (sql-kw :case) " "
@ -1861,13 +1926,53 @@
(str sqlx " "))
(join " " sqls)
" " (sql-kw :end))]
(into paramsx)
(into params))))
(into* paramsx params))))
(defn ignore-respect-nulls [k [x]]
(defn- between-fn
"For both :between and :not-between"
[k [x a b]]
(let [[sql-x & params-x] (format-expr x {:nested true})
[sql-a & params-a] (format-expr a {:nested true})
[sql-b & params-b] (format-expr b {:nested true})]
(into* [(str sql-x " " (sql-kw k) " " sql-a " AND " sql-b)]
params-x params-a params-b)))
(defn- object-record-literal
[k [x]]
[(str (sql-kw k) " " (inline-map x "(" ")"))])
(defn- get-in-navigation
"[:get-in expr key-or-index1 key-or-index2 ...]"
[wrap [expr & kix]]
(let [[sql & params] (format-expr expr)
[sqls params']
(reduce-sql (map #(cond (number? %)
[(str "[" % "]")]
(string? %)
[(str "[" (sqlize-value %) "]")]
(ident? %)
[(str "." (format-entity %))]
:else
(let [[sql' & params'] (format-expr %)]
(cons (str "[" sql' "]") params')))
kix))]
(into* [(str (if wrap (str "(" sql ")") sql)
(join "" sqls))]
params
params')))
(defn- ignore-respect-nulls [k [x]]
(let [[sql & params] (format-expr x)]
(into [(str sql " " (sql-kw k))] params)))
(defn- dot-navigation [sep [expr col & subcols]]
(let [[sql & params] (format-expr expr)]
(into [(str sql sep (format-simple-expr col "dot navigation")
(when (seq subcols)
(str "." (join "." (map #(format-simple-expr % "dot navigation")
subcols)))))]
params)))
(def ^:private special-syntax
(atom
{;; these "functions" are mostly used in column
@ -1888,12 +1993,9 @@
:references #'function-1
:unique #'function-1-opt
;; dynamic dotted name creation:
:. (fn [_ [expr col subcol]]
(let [[sql & params] (format-expr expr)]
(into [(str sql "." (format-entity col)
(when subcol
(str "." (format-entity subcol))))]
params)))
:. (fn [_ data] (dot-navigation "." data))
;; snowflake variant #570:
:.:. (fn [_ data] (dot-navigation ":" data))
;; used in DDL to force rendering as a SQL entity instead
;; of a SQL keyword:
:entity (fn [_ [e]] [(format-entity e)])
@ -1918,6 +2020,7 @@
(let [[sqls params] (format-expr-list arr)
type-str (when type (str "::" (sql-kw type) "[]"))]
(into [(str "ARRAY[" (join ", " sqls) "]" type-str)] params))))
:at (fn [_ data] (get-in-navigation false data))
:at-time-zone
(fn [_ [expr tz]]
(let [[sql & params] (format-expr expr {:nested true})
@ -1925,15 +2028,8 @@
(binding [*inline* true]
(format-expr (if (ident? tz) (name tz) tz)))]
(into [(str sql " AT TIME ZONE " tz-sql)] params)))
:between
(fn [_ [x a b]]
(let [[sql-x & params-x] (format-expr x {:nested true})
[sql-a & params-a] (format-expr a {:nested true})
[sql-b & params-b] (format-expr b {:nested true})]
(-> [(str sql-x " BETWEEN " sql-a " AND " sql-b)]
(into params-x)
(into params-a)
(into params-b))))
:between #'between-fn
:not-between #'between-fn
:case #'case-clauses
:case-expr #'case-clauses
:cast
@ -1942,9 +2038,7 @@
[sql' & params'] (if (ident? type)
[(sql-kw type)]
(format-expr type))]
(-> [(str "CAST(" sql " AS " sql' ")")]
(into params)
(into params'))))
(into* [(str "CAST(" sql " AS " sql' ")")] params params')))
:composite
(fn [_ [& args]]
(let [[sqls params] (format-expr-list args)]
@ -1957,15 +2051,14 @@
(fn [_ [pattern escape-chars]]
(let [[sql-p & params-p] (format-expr pattern)
[sql-e & params-e] (format-expr escape-chars)]
(-> [(str sql-p " " (sql-kw :escape) " " sql-e)]
(into params-p)
(into params-e))))
(into* [(str sql-p " " (sql-kw :escape) " " sql-e)] params-p params-e)))
:filter expr-clause-pairs
:get-in (fn [_ data] (get-in-navigation true data))
:ignore-nulls ignore-respect-nulls
:inline
(fn [_ xs]
(binding [*inline* true]
[(join " " (mapcat format-expr) xs)]))
[(join " " (mapcat #(format-expr % {:record true})) xs)]))
:interval format-interval
:join
(fn [_ [e & js]]
@ -1999,13 +2092,13 @@
(fn [_ [x]]
(let [[sql & params] (format-expr x {:nested true})]
(into [(str "NOT " sql)] params)))
:object #'object-record-literal
:record #'object-record-literal
:order-by
(fn [k [e & qs]]
(let [[sql-e & params-e] (format-expr e)
[sql-q & params-q] (format-dsl {k qs})]
(-> [(str sql-e " " sql-q)]
(into params-e)
(into params-q))))
(into* [(str sql-e " " sql-q)] params-e params-q)))
:over
(fn [_ [& args]]
(let [[sqls params]
@ -2016,7 +2109,7 @@
[(format-entity p)])]
[(conj sqls (str sql-e " OVER " sql-p
(when a (str " AS " (format-entity a)))))
(-> params (into params-e) (into params-p))]))
(into* params params-e params-p)]))
[[] []]
args)]
(into [(join ", " sqls)] params)))
@ -2057,8 +2150,7 @@
(cond-> nested
(as-> s (str "(" s ")")))
(vector)
(into p1)
(into p2))))
(into* p1 p2))))
(defn- format-infix-expr [op' op expr nested]
(let [args (cond->> (rest expr)
@ -2104,11 +2196,11 @@
This is intended to be used when writing your own formatters to
extend the DSL supported by HoneySQL."
([expr] (format-expr expr {}))
([expr {:keys [nested] :as opts}]
([expr {:keys [nested record] :as opts}]
(cond (ident? expr)
(format-var expr opts)
(map? expr)
(and (map? expr) (not record))
(format-dsl expr (assoc opts :nested true))
(sequential? expr)
@ -2131,7 +2223,9 @@
(into [(str "(" (join ", " sqls) ")")] params))))
(boolean? expr)
[(upper-case (str expr))]
(if (:auto-lift-boolean *dialect*)
["?" expr]
[(upper-case (str expr))])
(nil? expr)
["NULL"]
@ -2322,7 +2416,8 @@
[clause formatter before]
(let [clause (sym->kw clause)
before (sym->kw before)]
(assert (keyword? clause))
(when-not (keyword? clause)
(throw (ex-info "The clause must be a keyword or symbol" {:clause clause})))
(let [k (sym->kw formatter)
f (if (keyword? k)
(get @clause-format k)
@ -2389,7 +2484,8 @@
DSL."
[function formatter]
(let [function (sym->kw function)]
(assert (keyword? function))
(when-not (keyword? function)
(throw (ex-info "The function must be a keyword or symbol" {:function function})))
(let [k (sym->kw formatter)
f (if (keyword? k)
(get @special-syntax k)
@ -2410,7 +2506,8 @@
construct the DSL)."
[op & {:keys [ignore-nil]}]
(let [op (sym->kw op)]
(assert (keyword? op))
(when-not (keyword? op)
(throw (ex-info "The operator must be a keyword or symbol" {:operator op})))
(swap! infix-ops conj op)
(when ignore-nil
(swap! op-ignore-nil conj op))))
@ -2438,6 +2535,22 @@
(first clauses)
(into [:and] clauses))))
(defn semicolon
"Given either a vector of formatted SQL+params vectors, or two or more
SQL+params vectors as arguments, merge them into a single SQL+params
vector with the SQL strings separated by semicolons."
([sql+params-vector]
(reduce into
[(str/join "; " (map first sql+params-vector))]
(map rest sql+params-vector)))
([sql+params & more]
(semicolon (cons sql+params more))))
(comment
(semicolon [ ["foo" 1 2 3] ["bar" 4 5 6] ])
(semicolon ["foo" 1 2 3] ["bar" 4 5 6] ["baz" 7 8 9] )
)
;; aids to migration from HoneySQL 1.x -- these are deliberately undocumented
;; so as not to encourage their use for folks starting fresh with 2.x!
@ -2445,6 +2558,10 @@
(comment
(format {:truncate :foo})
(format [:and])
(format [:and] {:dialect :sqlserver})
(format {:select :* :from :table :where (map= {})})
(format {:select :* :from :table :where (map= {})} {:dialect :sqlserver})
(format-expr [:= :id 1])
(format-expr [:+ :id 1])
(format-expr [:+ 1 [:+ 1 :quux]])
@ -2610,4 +2727,7 @@
:select [:*]
:from [(keyword "'`a-b.b-c.c-d`")]}
(sql/format))
(sql/format {:select :* :from [[[:json_to_recordset :my-json-column] [:b {:with-columns [[:c :int] [:d :text]]}]]]})
(sql/format {:select :* :from :my_publications :where [:= :is_published true]}
{:dialect :sqlserver})
)

View file

@ -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
"Helper functions for the built-in clauses in honey.sql.
@ -58,10 +58,12 @@
bulk-collect-info [& args]
(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]
[honey.sql :as h]))
#?(:clj (set! *warn-on-reflection* true))
;; implementation helpers:
(defn- default-merge [current args]
@ -399,6 +401,11 @@
[& args]
(generic :create-index args))
(defn setting
"Accepts one or more time settings for a query."
[& args]
(generic :setting args))
(defn with
"Accepts one or more CTE definitions.
@ -445,6 +452,14 @@
[& clauses]
(generic :except-all (cons {} clauses)))
(defn assert
"Accepts an expression (predicate).
Produces: ASSERT expression"
{:arglists '([expr])}
[& args]
(generic-1 :assert args))
(defn select
"Accepts any number of column names, or column/alias
pairs, or SQL expressions (optionally aliased):
@ -498,6 +513,12 @@
[& 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
"Like `select-distinct` but produces DISTINCT..."
[& args]
@ -508,6 +529,16 @@
[& 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
"Accepts table name, optionally followed a database name."
{:arglists '([table] [table dbname])}
@ -521,6 +552,14 @@
[& 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
"Accepts a table name or a table/alias pair. That
can optionally be followed by a collection of
@ -536,12 +575,20 @@
(-> (select :*) (from :other)))"
{:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args]
(let [[data & args :as args']
(if (map? (first args)) args (cons {} args))
[table cols statement] args]
(if (and (sequential? cols) (map? statement))
(generic :insert-into [data [table cols] statement])
(generic :insert-into args'))))
(stuff-into :insert-into args))
(defn patch-into
"Accepts a table name or a table/alias pair. That
can optionally be followed by a collection of
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
"Accepts a table name or a table/alias pair. That
@ -554,7 +601,7 @@
MySQL and SQLite."
{:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args]
(apply insert-into args))
(stuff-into :replace-into args))
(defn update
"Accepts either a table name or a table/alias pair.
@ -582,6 +629,15 @@
[& 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
"Accepts a single table name to truncate."
{:arglists '([table])}
@ -1176,20 +1232,6 @@
[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
(-> (delete-from :table)
(where [:in (composite :first :second)

View file

@ -25,6 +25,8 @@
(:refer-clojure :exclude [-> ->> -])
(:require [honey.sql :as sql]))
#?(:clj (set! *warn-on-reflection* true))
;; see https://www.postgresql.org/docs/current/functions-json.html
(def ->

View file

@ -4,5 +4,7 @@
"InlineValue -- a protocol that defines how to inline
values; (sqlize x) produces a SQL string for x.")
#?(:clj (set! *warn-on-reflection* true))
(defprotocol InlineValue :extend-via-metadata true
(sqlize [this] "Render value inline in a SQL string."))

View file

@ -3,6 +3,8 @@
(:refer-clojure :exclude [str])
(:require clojure.string))
#?(:clj (set! *warn-on-reflection* true))
(defn str
"More efficient implementation of `clojure.core/str` because it has more
non-variadic arities. Optimization is Clojure-only, on other platforms it
@ -75,3 +77,33 @@
:default
(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)))

View file

@ -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
(:refer-clojure :exclude [format])
@ -9,3 +9,11 @@
(is (= ["SELECT a - b - c AS x"]
(-> {:select [[[:- :a :b :c] :x]]}
(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)))))

View file

@ -2,24 +2,43 @@
(ns honey.sql.helpers-test
(: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.helpers :as h
:refer [add-column add-index alter-table columns create-table create-table-as create-view
create-materialized-view drop-view drop-materialized-view
:refer [add-column alter-table columns create-table create-table-as create-view
create-materialized-view
create-index
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
group-by having insert-into
join-by join lateral left-join limit offset on-conflict
group-by having insert-into replace-into
join-by join left-join limit offset on-conflict
on-duplicate-key-update
order-by over partition-by refresh-materialized-view
rename-column rename-table returning right-join
select select-distinct select-top select-distinct-top
returning right-join
select select-distinct select-top
values where window with with-columns
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
(testing "large helper expression"
(let [m1 (-> (with [:cte (-> (select :*)
@ -533,6 +552,33 @@
" MAX(salary) OVER w AS MaxSalary"
" FROM employee"
" 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:
(is (= (-> (select :id
(over [[:avg :salary] {} :Average]
@ -790,7 +836,10 @@
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
;; three arguments with an alias and columns:
(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
@ -989,11 +1038,15 @@
(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))"]
(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)"]
(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)"]
(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
(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"]

View 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}))))))

View file

@ -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
(:refer-clojure :exclude [format])
@ -22,6 +22,8 @@
(sut/format-expr [:is :id nil])))
(is (= ["id = TRUE"]
(sut/format-expr [:= :id true])))
(is (= ["[id] = ?" true]
(sut/format [:= :id true] {:dialect :sqlserver})))
(is (= ["id IS TRUE"]
(sut/format-expr [:is :id true])))
(is (= ["id <> TRUE"]
@ -219,7 +221,26 @@
: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
(is (= (format {:insert-into :foo})
@ -554,13 +575,15 @@
(-> {:delete-from :foo
:where [:= :foo.id 42]}
(format :dialect :mysql :pretty true)))))
(when (str/starts-with? #?(:cljs *clojurescript-version*
:default (clojure-version)) "1.11")
(testing "format can be called with mixed arguments"
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
(-> {:delete-from :foo
:where [:= :foo.id 42]}
(format :dialect :mysql {:pretty true})))))))
(let [version #?(:cljs *clojurescript-version*
: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"
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
(-> {:delete-from :foo
:where [:= :foo.id 42]}
(format :dialect :mysql {:pretty true}))))))))
(deftest delete-from-test
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
@ -591,6 +614,12 @@
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `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})))))
(deftest inlined-values-are-stringified-correctly
@ -1155,9 +1184,10 @@ ORDER BY id = ? DESC
(deftest issue-474-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]
(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]"]
(let [t :a c :d]
(sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}
@ -1171,8 +1201,54 @@ ORDER BY id = ? DESC
(sut/format '{select (((. (nest v) *))
((. (nest w) x))
((. (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})))))
(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
(testing "single argument :raw"
(is (= ["@foo := 42"]
@ -1399,6 +1475,41 @@ ORDER BY id = ? DESC
(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
;; partial (incorrect!) workaround for #407:
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})
@ -1411,4 +1522,9 @@ ORDER BY id = ? DESC
:select [:*]
:from [:a-b.b-c.c-d]}
(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)
)

View file

@ -43,3 +43,20 @@
(is (= "1, 2, 3, 4"
(sut/join ", " (remove nil?) [1 nil 2 nil 3 nil nil nil 4])))
(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]))))