Compare commits

..

No commits in common. "develop" and "v2.6.1126" have entirely different histories.

43 changed files with 701 additions and 2280 deletions

View file

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

View file

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

View file

@ -1,16 +0,0 @@
(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

@ -1,5 +0,0 @@
{: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

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

View file

@ -1,16 +0,0 @@
(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

@ -12,16 +12,16 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v4 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'adopt'
java-version: '11' java-version: '11'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.11.1.1413'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository

View file

@ -10,16 +10,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'adopt'
java-version: '11' java-version: '11'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.11.1.1413'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository
@ -39,19 +39,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '8', '17', '21' ] java: [ '8', '14', '17', '19' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'adopt'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Clojure CLI - name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.11.1.1413'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository

View file

@ -1,33 +0,0 @@
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

@ -7,19 +7,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ '8', '11', '17', '21' ] java: [ '8', '11', '14', '17', '19' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'adopt'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
- name: Clojure CLI - name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.11.1.1413'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository

9
.gitignore vendored
View file

@ -1,9 +1,4 @@
*.class
*.jar
*.swp
*~
.calva/output-window/ .calva/output-window/
.calva/repl.calva-repl
.classpath .classpath
.clj-kondo/.cache .clj-kondo/.cache
.cpcache .cpcache
@ -23,6 +18,10 @@
.settings .settings
.socket-repl-port .socket-repl-port
.sw* .sw*
*.class
*.jar
*.swp
*~
/checkouts /checkouts
/classes /classes
/cljs-test-runner-out /cljs-test-runner-out

23
.gitpod.yml Normal file
View file

@ -0,0 +1,23 @@
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

18
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"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.autoConnectRepl": true
}

View file

@ -1,83 +1,10 @@
# Changes # Changes
* 2.7.next in progress
* Address [#440](https://github.com/seancorfield/honeysql/issues/440) by supporting multiple tables in `:truncate`.
* Support `USING HASH` as well as `USING GIN`.
* Fix [#571](https://github.com/seancorfield/honeysql/issues/571) by allowing `:order-by` to take an empty sequence of columns (and be omitted).
* Update dev/build deps.
* 2.7.1295 -- 2025-03-12
* Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax.
* Drop support for Clojure 1.9 [#561](https://github.com/seancorfield/honeysql/issues/561).
* 2.6.1281 -- 2025-03-06
* Address [#568](https://github.com/seancorfield/honeysql/issues/568) by adding `honey.sql/semicolon` to merge multiple SQL+params vectors into one (with semicolons separating the SQL statements).
* Address [#567](https://github.com/seancorfield/honeysql/issues/567) by adding support for `ASSERT` clause.
* Address [#566](https://github.com/seancorfield/honeysql/issues/566) by adding `IS [NOT] DISTINCT FROM` operators.
* Add examples of `:alias` with `:group-by` (syntax is slightly different to existing examples for `:order-by`).
* 2.6.1270 -- 2025-01-17
* Fix autoboxing introduced in 2.6.1267 via PR [#564](https://github.com/seancorfield/honeysql/pull/564) [@alexander-yakushev](https://github.com/alexander-yakushev).
* 2.6.1267 -- 2025-01-16
* Support expressions in `WITH` clauses via PR [#563](https://github.com/seancorfield/honeysql/pull/563) [@krevedkokun](https://github.com/krevedkokun).
* More performance optimizations via PRs [#560](https://github.com/seancorfield/honeysql/pull/560) and [#562](https://github.com/seancorfield/honeysql/pull/562) [@alexander-yakushev](https://github.com/alexander-yakushev).
* Fix two broken links to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) via PR [#559](https://github.com/seancorfield/honeysql/pull/559) [@whatacold](https://github.com/whatacold).
* Make SQL Server dialect auto-lift Boolean values to parameters since SQL Server has no `TRUE` / `FALSE` literals.
* Fix bug in `DEFAULT` values clause (that omitted some values).
* 2.6.1243 -- 2024-12-13
* Address [#558](https://github.com/seancorfield/honeysql/issues/558) by adding `:patch-into` (and `patch-into` helper) for XTDB (but in core).
* Address [#556](https://github.com/seancorfield/honeysql/issues/556) by adding an XTDB section to the documentation with examples.
* Address [#555](https://github.com/seancorfield/honeysql/issues/555) by supporting `SETTING` clause for XTDB.
* Replace `assert` calls with proper validation, throwing `ex-info` on failure (like other existing validation in HoneySQL).
* Experimental `:xtdb` dialect removed (since XTDB no longer supports qualified column names).
* Update dev/test deps.
* 2.6.1230 -- 2024-11-23
* Fix [#553](https://github.com/seancorfield/honeysql/issues/553) by adding `:not-between` as special syntax via PR [#554](https://github.com/seancorfield/honeysql/pull/554) [@plooney81](https://github.com/plooney81)
* Fix [#552](https://github.com/seancorfield/honeysql/issues/552) by changing the assert-on-load behavior into an explicit test in the test suite.
* Fix [#551](https://github.com/seancorfield/honeysql/issues/551) by supporting multiple `WINDOW` clauses.
* Fix [#549](https://github.com/seancorfield/honeysql/issues/549) by using `:bb` conditionals to support Babashka (and still support Clojure 1.9.0), and add testing against Babashka so it is fully-supported as a target via PR [#550](https://github.com/seancorfield/honeysql/pull/550) [@borkdude](https://github.com/borkdude)
* Address [#532](https://github.com/seancorfield/honeysql/issues/532) by adding support for XTDB SQL extensions `ERASE`, `EXCLUDE`, `OBJECT`, `RECORD`, `RECORDS`, and `RENAME`, along with inline hash maps (as records) and `:get-in` for object navigation, and starting to write tests for XTDB compatibility.
* 2.6.1203 -- 2024-10-22
* Fix [#548](https://github.com/seancorfield/honeysql/issues/548) which was a regression introduced in [#526](https://github.com/seancorfield/honeysql/issues/526) (in 2.6.1161).
* Address [#542](https://github.com/seancorfield/honeysql/issues/542) by adding support for `WITH` query tail options for PostgreSQL.
* Replace all optional argument destructuring with multiple arities to improve performance.
* 2.6.1196 -- 2024-10-06
* Address [#547](https://github.com/seancorfield/honeysql/issues/547) by adding examples of conditional SQL building with the helpers to the README and the `honey.sql.helpers` ns docstring.
* Performance optimizations via PRs [#545](https://github.com/seancorfield/honeysql/pull/545) and [#546](https://github.com/seancorfield/honeysql/pull/546) [@alexander-yakushev](https://github.com/alexander-yakushev).
* Address [#544](https://github.com/seancorfield/honeysql/issues/544) by adding support for MySQL's `VALUES ROW(..)` syntax.
* Fix [#543](https://github.com/seancorfield/honeysql/issues/543) by supporting both symbols and keywords in named parameters.
* Address [#541](https://github.com/seancorfield/honeysql/issues/541) by specifying the expected result of a formatter function passed to `register-clause!` and adding the example from the README to **Extending HoneySQL**.
* Getting Started updated based on feedback from Los Angeles Clojure meetup walkthrough [#539](https://github.com/seancorfield/honeysql/issues/539).
* Fix [#538](https://github.com/seancorfield/honeysql/issues/538) by removing `mod` from list of infix operators.
* Fixed a few symbol/keyword resolution bugs in the formatter. Thanks to [@irigarae](https://github.com/irigarae).
* Update Clojure version to 1.12.0; update dev/test/ci deps.
* 2.6.1161 -- 2024-08-29
* Address [#537](https://github.com/seancorfield/honeysql/issues/537) by ignoring non-scalar values in metadata, and expanding support to numbers, and checking strings for suspicious characters.
* Address [#536](https://github.com/seancorfield/honeysql/issues/536) by noting what will not work with PostgreSQL (but works with other databases).
* Address [#533](https://github.com/seancorfield/honeysql/issues/533) by adding `honey.sql/*escape-?*` which can be bound to `false` to prevent `?` being escaped to `??` when used as an operator or function.
* Address [#526](https://github.com/seancorfield/honeysql/issues/526) by using `format-var` in DDL, instead of `format-entity`.
* Update JDK test matrix (adopt -> temurin, 19 -> 21).
* Update Clojure versions (to 1.11.4 & 1.12.0-rc2).
* 2.6.1147 -- 2024-06-12
* Address [#531](https://github.com/seancorfield/honeysql/issues/531) and [#527](https://github.com/seancorfield/honeysql/issues/527) by adding tests and more documentation for `:composite`; fix bug in `set-dialect!` where clause order is not restored.
* Address [#530](https://github.com/seancorfield/honeysql/issues/530) by adding support for `:using-gin` to `:create-index`.
* Address [#529](https://github.com/seancorfield/honeysql/issues/529) by fixing `:join` special syntax to support aliases and to handle expressions the same way `select` / `from` etc handle them (extra `[...]` nesting).
* Add example of mixed `DO UPDATE SET` with `EXCLUDED` and regular SQL expressions.
* Improve exception message when un-`lift`-ed JSON expressions are used in the DSL.
* Update Clojure versions (to 1.11.3 and 1.12.0-alpha12); update other dev/test dependencies.
* 2.6.1126 -- 2024-03-04 * 2.6.1126 -- 2024-03-04
* Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs. * Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs.
* Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases. * Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases.
* Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server). * Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server).
* ~Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.~ _[This was removed in 2.6.1243 since XTDB no longer supports qualified column names]_ * Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.
* Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments. * Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments.
* Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias. * Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias.
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045). * Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045).
@ -276,7 +203,7 @@
* Fixes #344 by no longer dropping the qualifier on columns in a `SET` clause _for the `:mysql` dialect only_; the behavior is unchanged for all other dialects. * Fixes #344 by no longer dropping the qualifier on columns in a `SET` clause _for the `:mysql` dialect only_; the behavior is unchanged for all other dialects.
* Fixes #340 by making the "hyphen to space" logic more general so _operators_ containing `-` should retain the hyphen without special cases. * Fixes #340 by making the "hyphen to space" logic more general so _operators_ containing `-` should retain the hyphen without special cases.
* Documentation improvements: `:fetch`, `:lift`, `:limit`, `:offset`, `:param`, `:select`; also around JSON/PostgreSQL. * Documentation improvements: `:fetch`, `:lift`, `:limit`, `:offset`, `:param`, `:select`; also around JSON/PostgreSQL.
* Link to the [HoneySQL web app](https://john.shaffe.rs/honeysql/) in both the README and **Getting Started**. * Link to the [HoneySQL web app](https://www.john-shaffer.com/honeysql/) in both the README and **Getting Started**.
* Switch to `tools.build` for running tests and JAR building etc. * Switch to `tools.build` for running tests and JAR building etc.
* 2.0.0-rc5 (for testing; 2021-07-17) * 2.0.0-rc5 (for testing; 2021-07-17)

View file

@ -1,24 +1,22 @@
# Honey SQL [![Clojure CI Release](https://github.com/seancorfield/honeysql/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/seancorfield/honeysql/actions/workflows/test-and-release.yml) [![Clojure CI Develop](https://github.com/seancorfield/honeysql/actions/workflows/test-and-snapshot.yml/badge.svg)](https://github.com/seancorfield/honeysql/actions/workflows/test-and-snapshot.yml) [![Clojure CI Pull Request](https://github.com/seancorfield/honeysql/actions/workflows/test.yml/badge.svg)](https://github.com/seancorfield/honeysql/actions/workflows/test.yml) # Honey SQL [![Clojure CI](https://github.com/seancorfield/honeysql/actions/workflows/test.yml/badge.svg)](https://github.com/seancorfield/honeysql/actions/workflows/test.yml) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/seancorfield/honeysql)
SQL as Clojure data structures. Build queries programmatically -- even at runtime -- without having to bash strings together. SQL as Clojure data structures. Build queries programmatically -- even at runtime -- without having to bash strings together.
## Build ## Build
[![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) [![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/honeysql_2.6.1126-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) [![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/honeysql?2.6.1126)](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) [![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) [![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. This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of HoneySQL is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/honeysql) or via a git dependency on the latest SHA. > Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of HoneySQL is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/honeysql) or via a git dependency on the latest SHA.
HoneySQL 2.7.y requires Clojure 1.10.3 or later. HoneySQL 2.x requires Clojure 1.9 or later.
Earlier versions of HoneySQL support Clojure 1.9.0.
It also supports recent versions of ClojureScript and Babashka.
Compared to the [legacy 1.x version](#1.x), HoneySQL 2.x provides a streamlined codebase and a simpler method for extending the DSL. It also supports SQL dialects out-of-the-box and will be extended to support vendor-specific language features over time (unlike 1.x). Compared to 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).
> Note: you can use 1.x and 2.x side-by-side as they use different group IDs and different namespaces. This allows for a piecemeal migration. See this [summary of differences between 1.x and 2.x](doc/differences-from-1-x.md) if you are migrating from 1.x! > Note: you can use 1.x and 2.x side-by-side as they use different group IDs and different namespaces. This allows for a piecemeal migration. See this [summary of differences between 1.x and 2.x](doc/differences-from-1-x.md) if you are migrating from 1.x!
@ -37,6 +35,12 @@ Sample code in this documentation is verified via
Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings. Some of these samples show pretty-printed SQL: HoneySQL 2.x supports `:pretty true` which inserts newlines between clauses in the generated SQL strings.
### HoneySQL 1.x (legacy)
[![Clojars](https://img.shields.io/badge/clojars-honeysql_1.0.461-lightblue.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/honeysql/honeysql) [![cljdoc badge](https://cljdoc.org/badge/honeysql/honeysql?1.0.461)](https://cljdoc.org/d/honeysql/honeysql/CURRENT)
HoneySQL 1.x will continue to get critical security fixes but otherwise should be considered "legacy" at this point.
## Usage ## Usage
This section includes a number of usage examples but does not dive deep into the This section includes a number of usage examples but does not dive deep into the
@ -48,7 +52,7 @@ section of the documentation before trying to use HoneySQL to build your own que
From Clojure: From Clojure:
<!-- {:test-doc-blocks/reader-cond :clj} --> <!-- {:test-doc-blocks/reader-cond :clj} -->
```clojure ```clojure
(refer-clojure :exclude '[assert distinct filter for group-by into partition-by set update]) (refer-clojure :exclude '[distinct filter for group-by into partition-by set update])
(require '[honey.sql :as sql] (require '[honey.sql :as sql]
;; CAUTION: this overwrites several clojure.core fns: ;; CAUTION: this overwrites several clojure.core fns:
;; ;;
@ -217,24 +221,6 @@ If you want to replace a clause, you can `dissoc` the existing clause first, sin
=> ["SELECT * FROM foo WHERE (a = ?) AND (b < ?)" 1 100] => ["SELECT * FROM foo WHERE (a = ?) AND (b < ?)" 1 100]
``` ```
The power of this approach comes from the abiliity to programmatically and
conditionally build up queries:
<!-- :test-doc-blocks/skip -->
```clojure
(defn fetch-user [& {:keys [id name]}]
(-> (select :*)
(from :users)
(cond->
id (where [:= :id id])
name (where [:= :name name]))
sql/format))
```
You can call `fetch-user` with either `:id` or `:name` _or both_ and get back
a query with the appropriate `WHERE` clause, since the helpers will merge the
conditions into the query DSL.
Column and table names may be aliased by using a vector pair of the original Column and table names may be aliased by using a vector pair of the original
name and the desired alias: name and the desired alias:
@ -246,21 +232,6 @@ name and the desired alias:
=> ["SELECT a, b AS bar, c, d AS x FROM foo AS quux WHERE (quux.a = ?) AND (bar < ?)" 1 100] => ["SELECT a, b AS bar, c, d AS x FROM foo AS quux WHERE (quux.a = ?) AND (bar < ?)" 1 100]
``` ```
or conditionally:
<!-- :test-doc-blocks/skip -->
```clojure
(-> (select :a [:b :bar])
(cond->
need-c (select :c)
x-val (select [:d :x]))
(from [:foo :quux])
(where [:= :quux.a 1] [:< :bar 100])
(cond->
x-val (where [:> :x x-val]))
sql/format)
```
In particular, note that `(select [:a :b])` means `SELECT a AS b` rather than In particular, note that `(select [:a :b])` means `SELECT a AS b` rather than
`SELECT a, b` -- helpers like `select` are generally variadic and do not take `SELECT a, b` -- helpers like `select` are generally variadic and do not take
a collection of column names. a collection of column names.
@ -1044,15 +1015,8 @@ You can also register SQL clauses, specifying the keyword, the formatting functi
If you find yourself registering an operator, a function (syntax), or a new clause, consider submitting a [pull request to HoneySQL](https://github.com/seancorfield/honeysql/pulls) so others can use it, too. If it is dialect-specific, let me know in the pull request. If you find yourself registering an operator, a function (syntax), or a new clause, consider submitting a [pull request to HoneySQL](https://github.com/seancorfield/honeysql/pulls) so others can use it, too. If it is dialect-specific, let me know in the pull request.
<a name="1.x"/>
## HoneySQL 1.x (legacy)
[![Clojars](https://img.shields.io/badge/clojars-honeysql_1.0.461-lightblue.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/honeysql/honeysql) [![cljdoc badge](https://cljdoc.org/badge/honeysql/honeysql?1.0.461)](https://cljdoc.org/d/honeysql/honeysql/CURRENT)
HoneySQL 1.x will continue to get critical security fixes but otherwise should be considered "legacy" at this point.
## License ## License
Copyright (c) 2020-2024 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield. Copyright (c) 2020-2022 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield.
Distributed under the Eclipse Public License, the same as Clojure. Distributed under the Eclipse Public License, the same as Clojure.

8
bb.edn
View file

@ -1,8 +0,0 @@
{: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])) [deps-deploy.deps-deploy :as dd]))
(def lib 'com.github.seancorfield/honeysql) (def lib 'com.github.seancorfield/honeysql)
(defn- the-version [patch] (format "2.7.%s" patch)) (defn- the-version [patch] (format "2.6.%s" patch))
(def version (the-version (b/git-count-revs nil))) (def version (the-version (b/git-count-revs nil)))
(def snapshot (the-version "9999-SNAPSHOT")) (def snapshot (the-version "9999-SNAPSHOT"))
(def class-dir "target/classes") (def class-dir "target/classes")
@ -47,9 +47,10 @@
"Generate and run doc tests. "Generate and run doc tests.
Optionally specify :aliases vector: Optionally specify :aliases vector:
[:1.10] -- test against Clojure 1.10.3 (the default) [:1.9] -- test against Clojure 1.9 (the default)
[:1.10] -- test against Clojure 1.10.3
[:1.11] -- test against Clojure 1.11.0 [:1.11] -- test against Clojure 1.11.0
[:1.12] -- test against Clojure 1.12.0 [:1.12] -- test against Clojure 1.12 alpha
[:cljs] -- test against ClojureScript" [:cljs] -- test against ClojureScript"
[{:keys [aliases] :as opts}] [{:keys [aliases] :as opts}]
(gen-doc-tests opts) (gen-doc-tests opts)
@ -62,7 +63,6 @@
(defn test "Run basic tests." [opts] (defn test "Run basic tests." [opts]
(run-task [:test :runner :1.11]) (run-task [:test :runner :1.11])
(run-task [:test :runner :cljs])
opts) opts)
(defn- pom-template [version] (defn- pom-template [version]
@ -98,10 +98,10 @@
(defn ci (defn ci
"Run the CI pipeline of tests (and build the JAR). "Run the CI pipeline of tests (and build the JAR).
Default Clojure version is 1.10.3 (:1.10) so :elide Default Clojure version is 1.9.0 (:1.9) so :elide
tests for #409 on that version." tests for #409 on that version."
[opts] [opts]
(let [aliases [:cljs :elide :1.11 :1.12] (let [aliases [:cljs :elide :1.10 :1.11 :1.12]
opts (jar-opts opts)] opts (jar-opts opts)]
(b/delete {:path "target"}) (b/delete {:path "target"})
(doseq [alias aliases] (doseq [alias aliases]

View file

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

View file

@ -1,16 +1,17 @@
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}} {:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
:paths ["src"] :paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.10.3"}} :deps {org.clojure/clojure {:mvn/version "1.9.0"}}
:aliases :aliases
{;; for help: clojure -A:deps -T:build help/doc {;; for help: clojure -A:deps -T:build help/doc
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.8"} :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}
slipset/deps-deploy {:mvn/version "0.2.2"}} slipset/deps-deploy {:mvn/version "0.2.1"}}
:ns-default build} :ns-default build}
;; versions to test against: ;; versions to test against:
:1.9 {:override-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}} :1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}} :1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.1"}}}
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}} :1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha7"}}}
:elide ; to test #409 (assertion on helper docstrings) :elide ; to test #409 (assertion on helper docstrings)
{:jvm-opts ["-Dclojure.compiler.elide-meta=[:doc]"]} {:jvm-opts ["-Dclojure.compiler.elide-meta=[:doc]"]}
@ -26,12 +27,12 @@
{:main-opts ["-m" "cognitect.test-runner"]} {:main-opts ["-m" "cognitect.test-runner"]}
;; various "runners" for tests/CI: ;; various "runners" for tests/CI:
:cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.1"}} :cljs {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}}
:main-opts ["-m" "cljs-test-runner.main"]} :main-opts ["-m" "cljs-test-runner.main"]}
:gen-doc-tests {:replace-paths ["build"] :gen-doc-tests {:replace-paths ["build"]
:extra-deps {babashka/fs {:mvn/version "0.5.24"} :extra-deps {babashka/fs {:mvn/version "0.4.19"}
com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}} com.github.lread/test-doc-blocks {:mvn/version "1.0.166-alpha"}}
:main-opts ["-m" "honey.gen-doc-tests"]} :main-opts ["-m" "honey.gen-doc-tests"]}
:test-doc {:replace-paths ["src" "target/test-doc-blocks/test"]} :test-doc {:replace-paths ["src" "target/test-doc-blocks/test"]}
@ -41,5 +42,5 @@
"-c" "{:warnings,{:single-segment-namespace,false}}" "-c" "{:warnings,{:single-segment-namespace,false}}"
"-d" "target/test-doc-blocks/test"]} "-d" "target/test-doc-blocks/test"]}
:eastwood {:extra-deps {jonase/eastwood {:mvn/version "1.4.3"}} :eastwood {:extra-deps {jonase/eastwood {:mvn/version "1.4.0"}}
:main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}} :main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}}

View file

@ -141,22 +141,6 @@ user=> (sql/format (h/create-index [:unique :another-idx :if-not-exists] [:fruit
["CREATE UNIQUE INDEX IF NOT EXISTS another_idx ON fruit (color, LOWER(appearance))"] ["CREATE UNIQUE INDEX IF NOT EXISTS another_idx ON fruit (color, LOWER(appearance))"]
``` ```
As of 2.6.1147, `USING GIN` index creation is also possible using the keyword
`:using-gin` after the table name (or the symbol `using-gin`):
```clojure
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 ### rename-table
Used with `:alter-table`, Used with `:alter-table`,
@ -484,37 +468,6 @@ user=> (sql/format {:with [[[:stuff {:columns [:id :name]}]
> Note: you must use the vector-of-vectors format for `:values` here -- if you try to use the vector-of-maps format, `VALUES` will be preceded by the column names (keys from the maps) and the resultant SQL will be invalid. > Note: you must use the vector-of-vectors format for `:values` here -- if you try to use the vector-of-maps format, `VALUES` will be preceded by the column names (keys from the maps) and the resultant SQL will be invalid.
You can specify `MATERIALIZED`, `NOT MATERIALIZED` for the CTE:
```clojure
user=> (sql/format {:with [[:stuff {:select :*
:from :table} :not-materialized]]
:select :*
:from :stuff})
["WITH stuff AS NOT MATERIALIZED (SELECT * FROM table) SELECT * FROM stuff"]
```
As of 2.6.1203, you can specify `SEARCH` and/or `CYCLE` clauses, in place of
or following the `MATERIALIZED` marker:
```clojure
user=> (sql/format {:with-recursive [[:stuff {:select :*
:from :table}
:search-depth-first-by :col :set :search-col]]
:select :*
:from :stuff})
["WITH RECURSIVE stuff AS (SELECT * FROM table) SEARCH DEPTH FIRST BY col SET search_col SELECT * FROM stuff"]
```
```clojure
user=> (sql/format {:with-recursive [[:stuff {:select :*
:from :table}
:cycle [:a :b :c] :set :d :to [:abs :e] :default 42 :using :x]]
:select :*
:from :stuff})
["WITH RECURSIVE stuff AS (SELECT * FROM table) CYCLE a, b, c SET d TO ABS(e) DEFAULT ? USING x SELECT * FROM stuff" 42]
```
`:with-recursive` follows the same rules as `:with` and produces `WITH RECURSIVE` instead of just `WITH`. `:with-recursive` follows the same rules as `:with` and produces `WITH RECURSIVE` instead of just `WITH`.
## intersect, union, union-all, except, except-all ## intersect, union, union-all, except, except-all
@ -678,9 +631,9 @@ user=> (sql/format '{select * bulk-collect-into [arrv 100] from mytable})
["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100] ["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100]
``` ```
## insert-into, replace-into, patch-into ## insert-into, replace-into
There are three use cases with `:insert-into` etc. There are three use cases with `:insert-into`.
The first case takes just a table specifier (either a The first case takes just a table specifier (either a
table name or a table/alias pair), table name or a table/alias pair),
@ -698,10 +651,6 @@ For the first and second cases, you'll use the `:values` clause
to specify rows of values to insert. See [**values**](#values) below to specify rows of values to insert. See [**values**](#values) below
for more detail on the `:values` clause. for more detail on the `:values` clause.
`:patch-into` is only supported by XTDB but is
part of HoneySQL's "core" dialect anyway. It produces a `PATCH INTO`
statement but otherwise has identical syntax to `:insert-into`.
`:replace-into` is only supported by MySQL and SQLite but is `:replace-into` is only supported by MySQL and SQLite but is
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO` part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
statement but otherwise has identical syntax to `:insert-into`. statement but otherwise has identical syntax to `:insert-into`.
@ -795,18 +744,7 @@ user=> (sql/format {:update :transport
2] 2]
``` ```
You can also `UPDATE .. FROM (VALUES ..) ..` where you might also need `:composite`: ## delete, delete-from
```clojure
(sql/format {:update :table :set {:a :v.a}
:from [[{:values [[1 2 3]
[4 5 6]]}
[:v [:composite :a :b :c]]]]
:where [:and [:= :x :v.b] [:> :y :v.c]]})
["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, erase-from
`:delete-from` is the simple use case here, accepting just a `:delete-from` is the simple use case here, accepting just a
SQL entity (table name). `:delete` allows for deleting from SQL entity (table name). `:delete` allows for deleting from
@ -823,30 +761,16 @@ user=> (sql/format {:delete [:order :item]
["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42] ["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42]
``` ```
`:erase-from` is only supported by XTDB and produces an `ERASE FROM`
statement but otherwise has identical syntax to `:delete-from`. It
is a "hard" delete as opposed to a temporal delete.
## truncate ## truncate
`:truncate` accepts a simple SQL entity (table name) `:truncate` accepts a simple SQL entity (table name)
or a table name followed by various options, or a or a table name followed by various options:
sequence that starts with a sequence of one or more table names,
optionally followed by various options:
```clojure ```clojure
user=> (sql/format '{truncate transport}) user=> (sql/format '{truncate transport})
["TRUNCATE TABLE transport"] ["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport)})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport restart identity)}) user=> (sql/format '{truncate (transport restart identity)})
["TRUNCATE TABLE transport RESTART IDENTITY"] ["TRUNCATE TABLE transport RESTART IDENTITY"]
user=> (sql/format '{truncate ((transport))})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate ((transport other))})
["TRUNCATE TABLE transport, other"]
user=> (sql/format '{truncate ((transport other) restart identity)})
["TRUNCATE TABLE transport, other RESTART IDENTITY"]
``` ```
## columns ## columns
@ -891,17 +815,6 @@ user=> (sql/format {:select [:u.username :s.name]
["SELECT u.username, s.name FROM user AS u, status AS s WHERE (u.statusid = s.id) AND (u.id = ?)" 9] ["SELECT u.username, s.name FROM user AS u, status AS s WHERE (u.statusid = s.id) AND (u.id = ?)" 9]
``` ```
`:from` can also accept a `:values` clause:
```clojure
user=> (sql/format {:update :table :set {:a :v.a}
:from [[{:values [[1 2 3]
[4 5 6]]}
[:v [:composite :a :b :c]]]]
:where [:and [:= :x :v.b] [:> :y :v.c]]})
["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]
```
As of 2.4.1066, HoneySQL supports a temporal clause that starts with `:for`, As of 2.4.1066, HoneySQL supports a temporal clause that starts with `:for`,
followed by the time reference followed by the time reference
(e.g., `:system-time` or `:business-time`), followed by a temporal qualifier, (e.g., `:system-time` or `:business-time`), followed by a temporal qualifier,
@ -1088,9 +1001,6 @@ The `:where` clause can have a single SQL expression, or
a sequence of SQL expressions prefixed by either `:and` a sequence of SQL expressions prefixed by either `:and`
or `:or`. See examples of `:where` in various clauses above. or `:or`. See examples of `:where` in various clauses above.
If `:where` is given an empty sequence, the `WHERE` clause will
be omitted from the generated SQL.
Sometimes it is convenient to construct a `WHERE` clause that Sometimes it is convenient to construct a `WHERE` clause that
tests several columns for equality, and you might have a Clojure tests several columns for equality, and you might have a Clojure
hash map containing those values. `honey.sql/map=` exists to hash map containing those values. `honey.sql/map=` exists to
@ -1112,11 +1022,6 @@ user=> (sql/format '{select (*) from (table)
["SELECT * FROM table GROUP BY status, YEAR(created_date)"] ["SELECT * FROM table GROUP BY status, YEAR(created_date)"]
``` ```
You can `GROUP BY` expressions, column names (`:col1`), or table and column (`:table.col1`),
or aliases (`:some.alias`). Since there is ambiguity between the formatting
of those, you can use the special syntax `[:alias :some.thing]` to tell
HoneySQL to treat `:some.thing` as an alias instead of a table/column name.
## having ## having
The `:having` clause works identically to `:where` above The `:having` clause works identically to `:where` above
@ -1124,7 +1029,7 @@ but is rendered into the SQL later in precedence order.
## window, partition-by (and over) ## window, partition-by (and over)
`:window` accept alternating pairs of SQL entity (the window name) `:window` accepts a pair of SQL entity (the window name)
and the window "function" as a SQL clause (a hash map). and the window "function" as a SQL clause (a hash map).
`:partition-by` accepts the same arguments as `:select` above `:partition-by` accepts the same arguments as `:select` above
@ -1150,25 +1055,6 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
FROM employee FROM employee
WINDOW w AS (PARTITION BY department) WINDOW w AS (PARTITION BY department)
"] "]
;; multiple windows:
user=> (sql/format {:select [:id
[[:over
[[:avg :salary]
{:partition-by [:department]
:order-by [:designation]}
:Average]
[[:max :salary]
:w
:MaxSalary]]]]
:from [:employee]
:window [:w {:partition-by [:department]}
:x {:partition-by [:salary]}]}
{:pretty true})
["
SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department), x AS (PARTITION BY salary)
"]
;; easier to write with helpers (and easier to read!): ;; easier to write with helpers (and easier to read!):
user=> (sql/format (-> (select :id user=> (sql/format (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average] (over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
@ -1181,18 +1067,6 @@ SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) A
FROM employee FROM employee
WINDOW w AS (PARTITION BY department) WINDOW w AS (PARTITION BY department)
"] "]
;; multiple window clauses:
user=> (sql/format (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
[[:max :salary] :w :MaxSalary]))
(from :employee)
(window :w (partition-by :department))
(window :x (partition-by :salary))) {:pretty true})
["
SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary
FROM employee
WINDOW w AS (PARTITION BY department), x AS (PARTITION BY salary)
"]
``` ```
The window function in the `:over` expression may be `{}` or `nil`: The window function in the `:over` expression may be `{}` or `nil`:
@ -1231,15 +1105,12 @@ user=> (sql/format {:select [[[:over
## order-by ## order-by
`:order-by` accepts a sequence of zero or more ordering `:order-by` accepts a sequence of one or more ordering
expressions. Each ordering expression is either a simple expressions. Each ordering expression is either a simple
SQL entity or a pair of a SQL expression and a direction SQL entity or a pair of a SQL expression and a direction
(which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`, (which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`,
etc -- or the symbol equivalent). etc -- or the symbol equivalent).
If `:order-by` is given an empty sequence, the `ORDER BY` clause will
be omitted from the generated SQL.
If you want to order by an expression, you should wrap it If you want to order by an expression, you should wrap it
as a pair with a direction: as a pair with a direction:
@ -1394,21 +1265,6 @@ In addition, all of the rows are padded to the same length by adding `nil`
values if needed (since `:values` does not know how or if column values if needed (since `:values` does not know how or if column
names are being used in this case). names are being used in this case).
### values row (MySQL)
MySQL supports `VALUES` as a table expression in multiple
contexts, and it uses "row constructors" to represent the
rows of values.
HoneySQL supports this by using the keyword `:row` (or
symbol `'row`) as the first element of a sequence of values.
```clojure
user=> (sql/format {:values [:row [1 2] [3 4]]})
["VALUES ROW(?, ?), ROW(?, ?)" 1 2 3 4]
```
### values examples ### values examples
```clojure ```clojure
@ -1450,7 +1306,7 @@ user=> (sql/format {:insert-into :table
:values [{:a 1 :b 2 :c 3} :values [{:a 1 :b 2 :c 3}
:default :default
{:a 4 :b 5 :c 6}]}) {:a 4 :b 5 :c 6}]})
["INSERT INTO table (a, b, c) VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6] ["INSERT INTO table (a, b, c) VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 6 5 4]
user=> (sql/format {:insert-into :table user=> (sql/format {:insert-into :table
:values [[1 2 3] :default [4 5 6]]}) :values [[1 2 3] :default [4 5 6]]})
["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6] ["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]

View file

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

View file

@ -1,8 +1,6 @@
# Other Databases # Other Databases
There are dedicated sections for [New Relic Query Language Support](nrql.md), There is a dedicated section for [PostgreSQL Support](postgres.md).
[PostgreSQL Support](postgres.md), and
[XTDB Support](xtdb.md).
This section provides hints and tips for generating SQL for other This section provides hints and tips for generating SQL for other
databases. databases.

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 If you are using Clojure 1.11, you can invoke `format` with a mixture of named arguments and a trailing hash
map of additional options, if you wish. map of additional options, if you wish.
HoneySQL 1.x supported Clojure 1.7 and later. HoneySQL 2.7.y requires Clojure 1.10.3 or later. Earlier versions of HoneySQL 2.x support Clojure 1.9.0. HoneySQL 1.x supported Clojure 1.7 and later. HoneySQL 2.x requires Clojure 1.9 or later.
## Group, Artifact, and Namespaces ## Group, Artifact, and Namespaces
@ -63,7 +63,7 @@ Supported Clojure versions: 1.7 and later.
In `deps.edn`: In `deps.edn`:
<!-- :test-doc-blocks/skip --> <!-- :test-doc-blocks/skip -->
```clojure ```clojure
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"} com.github.seancorfield/honeysql {:mvn/version "2.6.1126"}
``` ```
Required as: Required as:
@ -90,7 +90,7 @@ The new namespaces are:
* `honey.sql` -- the primary API (just `format` now), * `honey.sql` -- the primary API (just `format` now),
* `honey.sql.helpers` -- helper functions to build the DSL. * `honey.sql.helpers` -- helper functions to build the DSL.
Supported Clojure versions: 1.10.3 and later. Supported Clojure versions: 1.9 and later.
## API Changes ## API Changes

View file

@ -10,7 +10,7 @@ register formatters or behavior corresponding to clauses,
operators, and functions. operators, and functions.
Built in clauses include: `:select`, `:from`, `:where` and Built in clauses include: `:select`, `:from`, `:where` and
many more. Built in operators include: `:=`, `:+`, `:%`. many more. Built in operators include: `:=`, `:+`, `:mod`.
Built in functions (special syntax) include: `:array`, `:case`, Built in functions (special syntax) include: `:array`, `:case`,
`:cast`, `:inline`, `:raw` and many more. `:cast`, `:inline`, `:raw` and many more.
@ -50,11 +50,6 @@ The formatter function will be called with:
* The clause name (always as a keyword), * The clause name (always as a keyword),
* The sequence of arguments provided. * The sequence of arguments provided.
The formatter function should return a vector whose first element is the
generated SQL string and whose remaining elements (if any) are the parameters
lifted from the DSL (for which the generated SQL string should contain `?`
placeholders).
The third argument to `register-clause!` allows you to The third argument to `register-clause!` allows you to
insert your new clause formatter so that clauses are insert your new clause formatter so that clauses are
formatted in the correct order for your SQL dialect. formatted in the correct order for your SQL dialect.
@ -62,26 +57,6 @@ For example, `:select` comes before `:from` which comes
before `:where`. You can call `clause-order` to see what the before `:where`. You can call `clause-order` to see what the
current ordering of clauses is. current ordering of clauses is.
<!-- :test-doc-blocks/skip -->
```clojure
;; the formatter will be passed your new clause and the value associated
;; with that clause in the DSL (which is often a sequence but does not
;; need to be -- it can be whatever syntax you desire in the DSL):
(sql/register-clause! :foobar
(fn [clause x]
(let [[sql & params]
(if (ident? x)
(sql/format-expr x)
(sql/format-dsl x))]
(c/into [(str (sql/sql-kw clause) " " sql)] params)))
:from) ; SELECT ... FOOBAR ... FROM ...
;; example usage:
(sql/format {:select [:a :b] :foobar :baz})
=> ["SELECT a, b FOOBAR baz"]
(sql/format {:select [:a :b] :foobar {:where [:= :id 1]}})
=> ["SELECT a, b FOOBAR WHERE id = ?" 1]
```
> Note: if you call `register-clause!` more than once for the same clause, the last call "wins". This allows you to correct an incorrect clause order insertion by simply calling `register-clause!` again with a different third argument. > Note: if you call `register-clause!` more than once for the same clause, the last call "wins". This allows you to correct an incorrect clause order insertion by simply calling `register-clause!` again with a different third argument.
## Defining a Helper Function for a New Clause ## Defining a Helper Function for a New Clause

View file

@ -10,14 +10,14 @@ For the Clojure CLI, add the following dependency to your `deps.edn` file:
<!-- :test-doc-blocks/skip --> <!-- :test-doc-blocks/skip -->
```clojure ```clojure
com.github.seancorfield/honeysql {:mvn/version "2.7.1295"} com.github.seancorfield/honeysql {:mvn/version "2.6.1126"}
``` ```
For Leiningen, add the following dependency to your `project.clj` file: For Leiningen, add the following dependency to your `project.clj` file:
<!-- :test-doc-blocks/skip --> <!-- :test-doc-blocks/skip -->
```clojure ```clojure
[com.github.seancorfield/honeysql "2.7.1295"] [com.github.seancorfield/honeysql "2.6.1126"]
``` ```
HoneySQL produces SQL statements but does not execute them. HoneySQL produces SQL statements but does not execute them.
@ -26,13 +26,13 @@ To execute SQL statements, you will also need a JDBC wrapper like
You can also experiment with HoneySQL directly in a browser -- no installation You can also experiment with HoneySQL directly in a browser -- no installation
required -- using [John Shaffer](https://github.com/john-shaffer)'s awesome required -- using [John Shaffer](https://github.com/john-shaffer)'s awesome
[HoneySQL web app](https://john.shaffe.rs/honeysql/), written in ClojureScript! [HoneySQL web app](https://www.john-shaffer.com/honeysql/), written in ClojureScript!
## Basic Concepts ## Basic Concepts
SQL statements are represented as hash maps, with keys that SQL statements are represented as hash maps, with keys that
represent clauses in SQL. SQL expressions are generally represent clauses in SQL. SQL expressions are generally
represented as vectors, where the first element identifies represented as sequences, where the first element identifies
the function or operator and the remaining elements are the the function or operator and the remaining elements are the
arguments or operands. arguments or operands.
@ -54,13 +54,11 @@ or symbols, are treated as positional parameters and replaced
by `?` in the SQL string and lifted out into the vector that by `?` in the SQL string and lifted out into the vector that
is returned from `format`. is returned from `format`.
Most clauses expect a vector as their value, containing Most clauses expect a sequence as their value, containing
either a list of SQL entities or the representation of a SQL either a list of SQL entities or the representation of a SQL
expression. Some clauses accept a single SQL entity. A few expression. Some clauses accept a single SQL entity. A few
accept a more specialized form (such as `:set` within an `:update` clause accept a more specialized form (such as `:set` accepting a
accepting a hash map of SQL entities and SQL expressions). hash map of SQL entities and SQL expressions).
> Note: clauses can have a list as their value, but literal vectors and keywords are easier to type without quoting.
A SQL entity can be a simple keyword (or symbol) or a pair A SQL entity can be a simple keyword (or symbol) or a pair
that represents a SQL entity and its alias (where aliases are allowed): that represents a SQL entity and its alias (where aliases are allowed):
@ -88,8 +86,6 @@ avoid evaluation:
;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1] ;;=> ["SELECT t.id, name AS item FROM table AS t WHERE id = ?" 1]
``` ```
> Note: these quoted forms may be appealing to users familiar with Datalog-family query languages, and they can be easier to type (and read) in some cases since you do not need to add `:` (shift-`;` on most keyboards) to the start of each SQL entity. The quoted forms do not work well in the [HoneySQL web app](https://john.shaffe.rs/honeysql/) so it's better to stick with vectors and keywords when using that.
If you wish, you can specify SQL entities as namespace-qualified If you wish, you can specify SQL entities as namespace-qualified
keywords (or symbols) and the namespace portion will treated as keywords (or symbols) and the namespace portion will treated as
the table name, i.e., `:foo/bar` instead of `:foo.bar`: the table name, i.e., `:foo/bar` instead of `:foo.bar`:
@ -105,8 +101,8 @@ the table name, i.e., `:foo/bar` instead of `:foo.bar`:
## SQL Expressions ## SQL Expressions
In addition to using hash maps to describe SQL clauses, In addition to using hash maps to describe SQL clauses,
HoneySQL uses vectors to describe SQL expressions. Any HoneySQL uses sequences to describe SQL expressions. Any
vector that begins with a keyword (or symbol) is considered sequence that begins with a keyword (or symbol) is considered
to be a kind of function invocation. Certain "functions" are to be a kind of function invocation. Certain "functions" are
considered to be "special syntax" and have custom rendering. considered to be "special syntax" and have custom rendering.
Some "functions" are considered to be operators. In general, Some "functions" are considered to be operators. In general,
@ -155,22 +151,22 @@ Some examples:
[:now] ;=> "NOW()" [:now] ;=> "NOW()"
[:count :*] ;=> "COUNT(*)" [:count :*] ;=> "COUNT(*)"
[:or [:<> :name nil] [:= :status-id 0]] ;=> "(name IS NOT NULL) OR (status_id = ?)" [:or [:<> :name nil] [:= :status-id 0]] ;=> "(name IS NOT NULL) OR (status_id = ?)"
;; the nil value is inlined as NULL but 0 is provided as a parameter ;; with a parameter of 0 -- the nil value is inlined as NULL
``` ```
`:inline` is an example of "special syntax" and it renders its `:inline` is an example of "special syntax" and it renders its
arguments as part of the SQL string generated by `format`. (single) argument as part of the SQL string generated by `format`.
Another form of special syntax that is treated as function calls Another form of special syntax that is treated as function calls
is keywords or symbols that begin with `%`. Such keywords (or quoted symbols) is keywords or symbols that begin with `%`. Such keywords (or symbols)
are split at `.` and turned into function calls: are split at `.` and turned into function calls:
<!-- :test-doc-blocks/skip --> <!-- :test-doc-blocks/skip -->
```clojure ```clojure
:%now ;=> NOW() %now ;=> NOW()
:%count.* ;=> COUNT(*) %count.* ;=> COUNT(*)
:%max.foo ;=> MAX(foo) %max.foo ;=> MAX(foo)
:%f.a.b ;=> F(a,b) %f.a.b ;=> F(a,b)
``` ```
If you need to reference a table or alias for a column, you can use If you need to reference a table or alias for a column, you can use
@ -204,8 +200,6 @@ expression requires an extra level of nesting:
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"] ;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
(sql/format {:select [:x [:y :d] [:%z.e] [:%z.f :g]]}) (sql/format {:select [:x [:y :d] [:%z.e] [:%z.f :g]]})
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"] ;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
(sql/format {:select [:x [:y :d] :%z.e [:%z.f :g]]})
;;=> ["SELECT x, y AS d, Z(e), Z(f) AS g"]
``` ```
## SQL Parameters ## SQL Parameters
@ -261,7 +255,7 @@ Or with `:numbered true`:
## Functional Helpers ## Functional Helpers
In addition to the hash map (and vectors) approach of building In addition to the hash map (and sequences) approach of building
SQL queries with raw Clojure data structures, a SQL queries with raw Clojure data structures, a
[namespace full of helper functions](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers) [namespace full of helper functions](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/api/honey.sql.helpers)
is also available. These functions are generally variadic and threadable: is also available. These functions are generally variadic and threadable:
@ -280,11 +274,8 @@ is also available. These functions are generally variadic and threadable:
There is a helper function for every single clause that HoneySQL There is a helper function for every single clause that HoneySQL
supports out of the box. In addition, there are helpers for supports out of the box. In addition, there are helpers for
`composite`, `lateral`, `over`, and `upsert` that make it easier to construct those `composite`, `lateral`, `over`, and `upsert` that make it easier to construct those
parts of the SQL DSL (examples of `composite` appear in the parts of the SQL DSL (examples of `composite` appear in the [README](/README.md),
[README](/README.md#composite-types) examples of `over` appear in the [Clause Reference](clause-reference.md))
and in the [General Reference](general-reference.md#tuples-and-composite-values);
examples of `over` appear in the
[Clause Reference](clause-reference.md#window-partition-by-and-over))
In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}` In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}`
(with a few exceptions -- see the docstring of the helper function (with a few exceptions -- see the docstring of the helper function
@ -357,7 +348,7 @@ The most visible difference between dialects is how SQL entities
should be quoted (if the `:quoted true` option is provided to `format`). should be quoted (if the `:quoted true` option is provided to `format`).
Most databases use `"` for quoting (the `:ansi` and `:oracle` dialects). Most databases use `"` for quoting (the `:ansi` and `:oracle` dialects).
The `:sqlserver` dialect uses `[`..`]` and the `:mysql` dialect uses The `:sqlserver` dialect uses `[`..`]` and the `:mysql` dialect uses
`` ` ``..`` ` ``. In addition, the `:oracle` dialect disables `AS` in aliases. ```..```. In addition, the `:oracle` dialect disables `AS` in aliases.
> Note: by default, quoting is **off** which produces cleaner-looking SQL and assumes you control all the symbols/keywords used as table, column, and function names -- the "SQL entities". If you are building any SQL or DDL where the table, column, or function names could be provided by an external source, **you should specify `:quoted true` to ensure all SQL entities are safely quoted**. As of 2.3.928, if you do _not_ specify `:quoted` as an option, HoneySQL will automatically quote any SQL entities that seem unusual, i.e., that contain any characters that are not alphanumeric or underscore. Purely alphanumeric entities will not be quoted (no entities were quoted by default prior to 2.3.928). You can prevent that auto-quoting by explicitly passing `:quoted false` into the `format` call but, from a security point of view, you should think very carefully before you do that: quoting entity names helps protect you from injection attacks! As of 2.4.947, you can change the default setting of `:quoted` from `nil` to `true` (or `false`) via the `set-options!` function. > Note: by default, quoting is **off** which produces cleaner-looking SQL and assumes you control all the symbols/keywords used as table, column, and function names -- the "SQL entities". If you are building any SQL or DDL where the table, column, or function names could be provided by an external source, **you should specify `:quoted true` to ensure all SQL entities are safely quoted**. As of 2.3.928, if you do _not_ specify `:quoted` as an option, HoneySQL will automatically quote any SQL entities that seem unusual, i.e., that contain any characters that are not alphanumeric or underscore. Purely alphanumeric entities will not be quoted (no entities were quoted by default prior to 2.3.928). You can prevent that auto-quoting by explicitly passing `:quoted false` into the `format` call but, from a security point of view, you should think very carefully before you do that: quoting entity names helps protect you from injection attacks! As of 2.4.947, you can change the default setting of `:quoted` from `nil` to `true` (or `false`) via the `set-options!` function.
@ -369,7 +360,7 @@ See [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect.
You can change the dialect globally using the `set-dialect!` function, You can change the dialect globally using the `set-dialect!` function,
passing in one of the keywords above. You need to call this function passing in one of the keywords above. You need to call this function
before you call `format` for the first time. See below for examples. before you call `format` for the first time.
You can change the dialect for a single `format` call by You can change the dialect for a single `format` call by
specifying the `:dialect` option in that call. specifying the `:dialect` option in that call.
@ -378,8 +369,7 @@ Alphanumeric SQL entities are not quoted by default but if you specify the
dialect in a `format` call, they will be quoted. If you don't dialect in a `format` call, they will be quoted. If you don't
specify a dialect in the `format` call, you can specify specify a dialect in the `format` call, you can specify
`:quoted true` to have SQL entities quoted. You can also enable quoting `:quoted true` to have SQL entities quoted. You can also enable quoting
globally via the `set-dialect!` function. See below for an example globally via the `set-dialect!` function.
with `:quoted true`.
If you want to use a dialect _and_ use the default quoting strategy (automatically quote any SQL entities that seem unusual), specify a `:dialect` option and set `:quoted nil`: If you want to use a dialect _and_ use the default quoting strategy (automatically quote any SQL entities that seem unusual), specify a `:dialect` option and set `:quoted nil`:
@ -415,8 +405,7 @@ If you want to use a dialect _and_ use the default quoting strategy (automatical
``` ```
Out of the box, as part of the extended ANSI SQL support, Out of the box, as part of the extended ANSI SQL support,
HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md) HoneySQL supports quite a few [PostgreSQL extensions](postgresql.md).
and [XTDB extensions](xtdb.md).
> Note: the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) library which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library (up to 0.4.112) out of the box! > Note: the [nilenso/honeysql-postgres](https://github.com/nilenso/honeysql-postgres) library which provided PostgreSQL support for HoneySQL 1.x does not work with HoneySQL 2.x. However, HoneySQL 2.x includes all of the functionality from that library (up to 0.4.112) out of the box!

View file

@ -110,7 +110,7 @@ Predicates for `NULL` and Boolean values:
;;=> ["...WHERE col IS NOT FALSE..."] ;;=> ["...WHERE col IS NOT FALSE..."]
``` ```
## xor, + - * / % | & ^ ## mod, xor, + - * / % | & ^
Mathematical and bitwise operators. Mathematical and bitwise operators.

View file

@ -293,24 +293,6 @@ DO UPDATE SET counter = table.counter + ?
" "id" 1 1] " "id" 1 1]
``` ```
You can use `:EXCLUDED.column` in a hash map to produce the
same effect as `:column` in a vector:
```clojure
user=> (-> (insert-into :table)
(values [{:id "id" :counter 1}])
(on-conflict :id)
(do-update-set {:name :EXCLUDED.name
:counter [:+ :table.counter 1]})
(sql/format {:pretty true}))
["
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name, counter = table.counter + ?
" "id" 1 1]
```
If you need to combine a `DO UPDATE SET` hash map expression If you need to combine a `DO UPDATE SET` hash map expression
with a `WHERE` clause, you need to explicitly use the `:fields` / with a `WHERE` clause, you need to explicitly use the `:fields` /
`:where` format explained above. Here's how those two examples `:where` format explained above. Here's how those two examples
@ -456,6 +438,11 @@ user=> (-> (alter-table :fruit)
(drop-column :skin) (drop-column :skin)
sql/format) sql/format)
["ALTER TABLE fruit DROP COLUMN skin"] ["ALTER TABLE fruit DROP COLUMN skin"]
;; alter table alter column:
user=> (-> (alter-table :fruit)
(alter-column :name [:varchar 64] [:not nil])
sql/format)
["ALTER TABLE fruit ALTER COLUMN name VARCHAR(64) NOT NULL"]
;; alter table rename column: ;; alter table rename column:
user=> (-> (alter-table :fruit) user=> (-> (alter-table :fruit)
(rename-column :cost :price) (rename-column :cost :price)
@ -468,29 +455,6 @@ user=> (-> (alter-table :fruit)
["ALTER TABLE fruit RENAME TO vegetable"] ["ALTER TABLE fruit RENAME TO vegetable"]
``` ```
The following does not work for PostgreSQL, but does work for several other databases:
```clojure
;; alter table alter column:
user=> (-> (alter-table :fruit)
(alter-column :name [:varchar 64] [:not nil])
sql/format)
["ALTER TABLE fruit ALTER COLUMN name VARCHAR(64) NOT NULL"]
```
For PostgreSQL, you need separate statements:
```clojure
user=> (-> (alter-table :fruit)
(alter-column :name :type [:varchar 64])
sql/format)
["ALTER TABLE fruit ALTER COLUMN name TYPE VARCHAR(64)"]
user=> (-> (alter-table :fruit)
(alter-column :name :set [:not nil])
sql/format)
["ALTER TABLE fruit ALTER COLUMN name SET NOT NULL"]
```
The following PostgreSQL-specific DDL statements are supported The following PostgreSQL-specific DDL statements are supported
(with the same syntax as the nilenso library but `sql/format` (with the same syntax as the nilenso library but `sql/format`
takes slightly different options): takes slightly different options):

View file

@ -29,14 +29,6 @@ and strings.
:from :b :from :b
:order-by [[[:alias :'some-alias]]]}) :order-by [[[:alias :'some-alias]]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"] ;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sql/format {:select [[:column-name "some-alias"]]
:from :b
:group-by [[:alias "some-alias"]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
(sql/format {:select [[:column-name "some-alias"]]
:from :b
:group-by [[:alias :'some-alias]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
``` ```
## array ## array
@ -88,29 +80,6 @@ In the subquery case, produces `ARRAY(subquery)`:
;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"] ;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"]
``` ```
## at
If addition to dot navigation (for JSON) -- see the `.` and `.:.` syntax below --
HoneySQL also supports bracket notation for JSON navigation.
The first argument to `:at` is treated as an expression that identifies
the column, and subsequent arguments are treated as field names or array
indices to navigate into that document.
```clojure
user=> (sql/format {:select [[[:at :col :field1 :field2]]]})
["SELECT col.field1.field2"]
user=> (sql/format {:select [[[:at :table.col 0 :field]]]})
["SELECT table.col[0].field"]
```
If you want an array index to be a parameter, use `:lift`:
```clojure
user=> (sql/format {:select [[[:at :col [:lift 0] :field]]]})
["SELECT col[?].field" 0]
```
## at time zone ## at time zone
Accepts two arguments: an expression (assumed to be a date/time of some sort) Accepts two arguments: an expression (assumed to be a date/time of some sort)
@ -124,7 +93,7 @@ and a time zone name or identifier (can be a string, a symbol, or a keyword):
The time zone name or identifier will be inlined (as a string) and therefore The time zone name or identifier will be inlined (as a string) and therefore
cannot be an expression. cannot be an expression.
## between and not-between ## between
Accepts three arguments: an expression, a lower bound, and Accepts three arguments: an expression, a lower bound, and
an upper bound: an upper bound:
@ -132,9 +101,6 @@ an upper bound:
```clojure ```clojure
(sql/format-expr [:between :id 1 100]) (sql/format-expr [:between :id 1 100])
;;=> ["id BETWEEN ? AND ?" 1 100] ;;=> ["id BETWEEN ? AND ?" 1 100]
(sql/format-expr [:not-between :id 1 100])
;;=> ["id NOT BETWEEN ? AND ?" 1 100]
``` ```
## case ## case
@ -208,23 +174,6 @@ expression (comma-separated, wrapped in parentheses):
;;=> ["(a, b, ?, x + ?)" "red" 1] ;;=> ["(a, b, ?, x + ?)" "red" 1]
``` ```
This can be useful in a number of situations where you want a composite
value, as above, or a composite based on or declaring columns names:
```clojure
(sql/format {:select [[[:composite :a :b] :c]] :from :table})
;;=> ["SELECT (a, b) AS c FROM table"]
```
```clojure
(sql/format {:update :table :set {:a :v.a}
:from [[{:values [[1 2 3]
[4 5 6]]}
[:v [:composite :a :b :c]]]]
:where [:and [:= :x :v.b] [:> :y :v.c]]})
;;=> ["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]
```
## distinct ## distinct
Accepts a single expression and prefixes it with `DISTINCT `: Accepts a single expression and prefixes it with `DISTINCT `:
@ -234,23 +183,15 @@ Accepts a single expression and prefixes it with `DISTINCT `:
;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"] ;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"]
``` ```
## dot . .:. ## dot .
Accepts an expression and one or more fields (or columns). Plain dot produces Accepts an expression and a field (or column) selection:
plain dotted selection:
```clojure ```clojure
(sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]}) (sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]})
;;=> ["SELECT t.c, s.t.c"] ;;=> ["SELECT t.c, s.t.c"]
``` ```
Dot colon dot produces Snowflake-style dotted selection:
```clojure
(sql/format {:select [ [[:.:. :t :c]] [[:.:. :s :t :c]] ]})
;;=> ["SELECT t:c, s:t.c"]
```
Can be used with `:nest` for field selection from composites: Can be used with `:nest` for field selection from composites:
```clojure ```clojure
@ -258,9 +199,6 @@ Can be used with `:nest` for field selection from composites:
;;=> ["SELECT (v).*, (MYFUNC(x)).y"] ;;=> ["SELECT (v).*, (MYFUNC(x)).y"]
``` ```
See also [`get-in`](xtdb.md#object-navigation-expressions)
and [`at`](#at) for additional path navigation functions.
## entity ## entity
Accepts a single keyword or symbol argument and produces a Accepts a single keyword or symbol argument and produces a
@ -414,20 +352,6 @@ a `JOIN` clause.
;;=> ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"] ;;=> ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"]
``` ```
An alias can be provided:
```clojure
(sql/format {:join [[[:join [:tbl1 :t] {:left-join [:tbl2 [:using :id]]}]]]})
;;=> ["INNER JOIN (tbl1 AS t LEFT JOIN tbl2 USING (id))"]
```
To provide an expression, an extra level of `[...]` is needed:
```clojure
(sql/format {:join [[[:join [[:make_thing 42] :t] {:left-join [:tbl2 [:using :id]]}]]]})
;;=> ["INNER JOIN (MAKE_THING(?) AS t LEFT JOIN tbl2 USING (id))" 42]
```
## lateral ## lateral
Accepts a single argument that can be a (`SELECT`) clause or Accepts a single argument that can be a (`SELECT`) clause or

View file

@ -1,220 +0,0 @@
# XTDB Support
As of 2.6.1230, HoneySQL provides support for most of XTDB's SQL
extensions, with additional support being added in subsequent releases.
For the most part, XTDB's SQL is based on
[SQL:2011](https://en.wikipedia.org/wiki/SQL:2011), including the
bitemporal features, but also includes a number of SQL extensions
to support additional XTDB-specific features.
HoneySQL attempts to support all of these XTDB features in the core
ANSI dialect, and this section documents most of those XTDB features.
For more details, see the XTDB documentation:
* [SQL Overview](https://docs.xtdb.com/quickstart/sql-overview.html)
* [SQL Queries](https://docs.xtdb.com/reference/main/sql/queries.html)
* [SQL Transactions/DML](https://docs.xtdb.com/reference/main/sql/txs.html)
## Code Examples
The code examples herein assume:
```clojure
(refer-clojure :exclude '[update set])
(require '[honey.sql :as sql]
'[honey.sql.helpers :refer [select from where
delete-from erase-from
insert-into patch-into values
records]])
```
Clojure users can opt for the shorter `(require '[honey.sql :as sql] '[honey.sql.helpers :refer :all])` but this syntax is not available to ClojureScript users.
## `select` Variations
XTDB allows you to omit `SELECT` in a query. `SELECT *` is assumed if
it is omitted. In HoneySQL, you can simply omit the `:select` clause
from the DSL to achieve this.
```clojure
user=> (sql/format '{select * from foo where (= status "active")})
["SELECT * FROM foo WHERE status = ?" "active"]
user=> (sql/format '{from foo where (= status "active")})
["FROM foo WHERE status = ?" "active"]
```
You can also `SELECT *` and then exclude columns and/or rename columns.
```clojure
user=> (sql/format {:select [[:* {:exclude :_id :rename [[:title, :name]]}]]})
["SELECT * EXCLUDE _id RENAME title AS name"]
user=> (sql/format '{select ((a.* {exclude _id})
(b.* {rename ((title, name))}))
from ((foo a))
join ((bar b) (= a._id b.foo_id))})
["SELECT a.* EXCLUDE _id, b.* RENAME title AS name FROM foo AS a INNER JOIN bar AS b ON a._id = b.foo_id"]
```
`:exclude` can accept a single column, or a sequence of columns.
`:rename` accepts a sequence of pairs (column name, new name).
```clojure
user=> (sql/format {:select [[:* {:exclude [:_id :upc]
:rename [[:title, :name]
[:price, :cost]]}]]})
["SELECT * EXCLUDE (_id, upc) RENAME (title AS name, price AS cost)"]
```
## Nested Sub-Queries
XTDB can produce structured results from `SELECT` queries containing
sub-queries, using `NEST_ONE` and `NEST_MANY`. In HoneySQL, these are
supported as regular function syntax in `:select` clauses.
```clojure
user=> (sql/format '{select (a.*
((nest_many {select * from bar where (= foo_id a._id)})
b))
from ((foo a))})
["SELECT a.*, NEST_MANY (SELECT * FROM bar WHERE foo_id = a._id) AS b FROM foo AS a"]
```
Remember that function calls in `:select` clauses need to be nested three
levels of parentheses (brackets):
`:select [:col-a [:col-b :alias-b] [[:fn-call :col-c] :alias-c]]`.
## `records` Clause
XTDB provides a `RECORDS` clause to specify a list of structured documents,
similar to `VALUES` but specifically for documents rather than a collection
of column values. HoneySQL supports a `:records` clauses and automatically
lifts hash map values to parameters (rather than treating them as DSL fragments).
You can inline a hash map to produce XTDB's inline document syntax.
See also `insert` and `patch` below.
```clojure
user=> (sql/format {:records [{:_id 1 :status "active"}]})
["RECORDS ?" {:_id 1, :status "active"}]
user=> (sql/format {:records [[:inline {:_id 1 :status "active"}]]})
["RECORDS {_id: 1, status: 'active'}"]
```
## `object` (`record`) Literals
While `RECORDS` exists in parallel to the `VALUES` clause, XTDB also provides
a syntax to construct documents in other contexts in SQL, via the `OBJECT`
literal syntax. `RECORD` is a synonym for `OBJECT`. HoneySQL supports both
`:object` and `:record` as special syntax:
```clojure
user=> (sql/format {:select [[[:object {:_id 1 :status "active"}]]]})
["SELECT OBJECT (_id: 1, status: 'active')"]
user=> (sql/format {:select [[[:record {:_id 1 :status "active"}]]]})
["SELECT RECORD (_id: 1, status: 'active')"]
```
A third option is to use `:inline` with a hash map:
```clojure
user=> (sql/format {:select [[[:inline {:_id 1 :status "active"}]]]})
["SELECT {_id: 1, status: 'active'}"]
```
## Object Navigation Expressions
In order to deal with nested documents, XTDB provides syntax to navigate
into them, via field names and/or array indices. HoneySQL supports this
via the `:get-in` special syntax, intended to be familiar to Clojure users.
The first argument to `:get-in` is treated as an expression that produces
the document, and subsequent arguments are treated as field names or array
indices to navigate into that document.
```clojure
user=> (sql/format {:select [[[:get-in :doc :field1 :field2]]]})
["SELECT (doc).field1.field2"]
user=> (sql/format {:select [[[:get-in :table.col 0 :field]]]})
["SELECT (table.col)[0].field"]
```
If you want an array index to be a parameter, use `:lift`:
```clojure
user=> (sql/format {:select [[[:get-in :doc [:lift 0] :field]]]})
["SELECT (doc)[?].field" 0]
```
## Temporal Queries
XTDB allows any query to be run in a temporal context via the `SETTING`
clause (ahead of the `SELECT` clause). HoneySQL supports this via the
`:setting` clause. It accepts a sequence of identifiers and expressions.
An identifier ending in `-time` is assumed to be a temporal identifier
(e.g., `:system-time` mapping to `SYSTEM_TIME`). Other identifiers are assumed to
be regular SQL (so `-` is mapped to a space, e.g., `:as-of` mapping to `AS OF`).
A timestamp literal, such as `DATE '2024-11-24'` can be specified in HoneySQL
using `[:inline [:DATE "2024-11-24"]]` (note the literal case of `:DATE`
to produce `DATE`).
See [XTDB's Top-level queries documentation](https://docs.xtdb.com/reference/main/sql/queries.html#_top_level_queries) for more details.
Here's one fairly complex example:
```clojure
user=> (sql/format {:setting [[:snapshot-time :to [:inline :DATE "2024-11-24"]]
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]]]})
["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023'"]
```
Table references (e.g., in a `FROM` clause) can also have temporal qualifiers.
See [HoneySQL's `from` clause documentation](clause-reference.md#from) for
examples of that, one of which is reproduced here:
```clojure
user=> (sql/format {:select [:username]
:from [[:user :for :system-time :as-of [:inline "2019-08-01 15:23:00"]]]
:where [:= :id 9]})
["SELECT username FROM user FOR SYSTEM_TIME AS OF '2019-08-01 15:23:00' WHERE id = ?" 9]
```
## `delete` and `erase`
In XTDB, `DELETE` is a temporal deletion -- the data remains in the database
but is no longer visible in queries that don't specify a time range prior to
the deletion. XTDB provides a similar `ERASE` operation that can permanently
delete the data. HoneySQL supports `:erase-from` with the same syntax as
`:delete-from`.
```clojure
user=> (sql/format {:delete-from :foo :where [:= :status "inactive"]})
["DELETE FROM foo WHERE status = ?" "inactive"]
user=> (sql/format {:erase-from :foo :where [:= :status "inactive"]})
["ERASE FROM foo WHERE status = ?" "inactive"]
```
## `insert` and `patch`
XTDB supports `PATCH` as an upsert operation: it will update existing
documents (via merging the new data) or insert new documents if they
don't already exist. HoneySQL supports `:patch-into` with the same syntax
as `:insert-into` with `:records`.
```clojure
user=> (sql/format {:insert-into :foo
:records [{:_id 1 :status "active"}]})
["INSERT INTO foo RECORDS ?" {:_id 1, :status "active"}]
user=> (sql/format {:patch-into :foo
:records [{:_id 1 :status "active"}]})
["PATCH INTO foo RECORDS ?" {:_id 1, :status "active"}]
```
## `assert`
XTDB supports an `ASSERT` operation that will throw an exception if the
asserted predicate is not true:
```clojure
user=> (sql/format '{assert (not-exists {select 1 from users where (= email "james @example.com")})}
:inline true)
["ASSERT NOT EXISTS (SELECT 1 FROM users WHERE email = 'james @example.com')"]
```

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
;; copyright (c) 2020-2025 sean corfield, all rights reserved ;; copyright (c) 2020-2023 sean corfield, all rights reserved
(ns honey.sql.helpers (ns honey.sql.helpers
"Helper functions for the built-in clauses in honey.sql. "Helper functions for the built-in clauses in honey.sql.
@ -17,16 +17,6 @@
(sql/format)) (sql/format))
``` ```
or conditionally like this:
```
(-> (select :a :b :c)
(from :table)
(cond->
id (where [:= :id id]))
(sql/format))
```
Therefore all helpers can take an existing DSL expression Therefore all helpers can take an existing DSL expression
as their first argument or, if the first argument is not as their first argument or, if the first argument is not
a hash map, an empty DSL is assumed -- an empty hash map. a hash map, an empty DSL is assumed -- an empty hash map.
@ -58,12 +48,10 @@
bulk-collect-info [& args] bulk-collect-info [& args]
(as they are for all helper functions)." (as they are for all helper functions)."
(:refer-clojure :exclude [assert distinct filter for group-by into partition-by set update]) (:refer-clojure :exclude [distinct filter for group-by into partition-by set update])
(:require [clojure.core :as c] (:require [clojure.core :as c]
[honey.sql :as h])) [honey.sql :as h]))
#?(:clj (set! *warn-on-reflection* true))
;; implementation helpers: ;; implementation helpers:
(defn- default-merge [current args] (defn- default-merge [current args]
@ -401,11 +389,6 @@
[& args] [& args]
(generic :create-index args)) (generic :create-index args))
(defn setting
"Accepts one or more time settings for a query."
[& args]
(generic :setting args))
(defn with (defn with
"Accepts one or more CTE definitions. "Accepts one or more CTE definitions.
@ -452,14 +435,6 @@
[& clauses] [& clauses]
(generic :except-all (cons {} clauses))) (generic :except-all (cons {} clauses)))
(defn assert
"Accepts an expression (predicate).
Produces: ASSERT expression"
{:arglists '([expr])}
[& args]
(generic-1 :assert args))
(defn select (defn select
"Accepts any number of column names, or column/alias "Accepts any number of column names, or column/alias
pairs, or SQL expressions (optionally aliased): pairs, or SQL expressions (optionally aliased):
@ -513,12 +488,6 @@
[& args] [& args]
(generic :select-distinct-top args)) (generic :select-distinct-top args))
(defn records
"Produces RECORDS {...}, {...}, ...
Like `values` so it accepts a collection of maps."
[& args]
(generic-1 :records args))
(defn distinct (defn distinct
"Like `select-distinct` but produces DISTINCT..." "Like `select-distinct` but produces DISTINCT..."
[& args] [& args]
@ -529,16 +498,6 @@
[& args] [& args]
(generic-1 :expr args)) (generic-1 :expr args))
(defn exclude
"Accepts one or more column names to exclude from a select list."
[& args]
(generic :exclude args))
(defn rename
"Accepts one or more column names with aliases to rename in a select list."
[& args]
(generic :rename args))
(defn into (defn into
"Accepts table name, optionally followed a database name." "Accepts table name, optionally followed a database name."
{:arglists '([table] [table dbname])} {:arglists '([table] [table dbname])}
@ -552,14 +511,6 @@
[& args] [& args]
(generic :bulk-collect-into args)) (generic :bulk-collect-into args))
(defn- stuff-into [k args]
(let [[data & args :as args']
(if (map? (first args)) args (cons {} args))
[table cols statement] args]
(if (and (sequential? cols) (map? statement))
(generic k [data [table cols] statement])
(generic k args'))))
(defn insert-into (defn insert-into
"Accepts a table name or a table/alias pair. That "Accepts a table name or a table/alias pair. That
can optionally be followed by a collection of can optionally be followed by a collection of
@ -575,20 +526,12 @@
(-> (select :*) (from :other)))" (-> (select :*) (from :other)))"
{:arglists '([table] [table cols] [table statement] [table cols statement])} {:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args] [& args]
(stuff-into :insert-into args)) (let [[data & args :as args']
(if (map? (first args)) args (cons {} args))
(defn patch-into [table cols statement] args]
"Accepts a table name or a table/alias pair. That (if (and (sequential? cols) (map? statement))
can optionally be followed by a collection of (generic :insert-into [data [table cols] statement])
column names. That can optionally be followed by (generic :insert-into args'))))
a (select) statement clause.
The arguments are identical to insert-into.
The PATCH INTO statement is only supported by
XTDB."
{:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args]
(stuff-into :patch-into args))
(defn replace-into (defn replace-into
"Accepts a table name or a table/alias pair. That "Accepts a table name or a table/alias pair. That
@ -601,7 +544,7 @@
MySQL and SQLite." MySQL and SQLite."
{:arglists '([table] [table cols] [table statement] [table cols statement])} {:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args] [& args]
(stuff-into :replace-into args)) (apply insert-into args))
(defn update (defn update
"Accepts either a table name or a table/alias pair. "Accepts either a table name or a table/alias pair.
@ -629,15 +572,6 @@
[& args] [& args]
(generic :delete-from args)) (generic :delete-from args))
(defn erase-from
"For erasing (hard delete) from a single table (XTDB).
Accepts a single table name to erase from.
(-> (erase-from :films) (where [:= :id 1]))"
{:arglists '([table])}
[& args]
(generic :erase-from args))
(defn truncate (defn truncate
"Accepts a single table name to truncate." "Accepts a single table name to truncate."
{:arglists '([table])} {:arglists '([table])}
@ -1232,6 +1166,20 @@
[k args] [k args]
(generic-1 k args)) (generic-1 k args))
#?(:clj
(do
;; #409 this assert is only valid when :doc metadata is not elided:
(when (-> #'generic-helper-unary meta :doc)
;; ensure #295 stays true (all public functions have docstring):
(assert (empty? (->> (ns-publics *ns*) (vals) (c/filter (comp not :doc meta))))))
;; ensure all public functions match clauses:
(assert (= (c/set (conj @#'honey.sql/default-clause-order
:composite :filter :lateral :over :within-group
:upsert
:generic-helper-variadic :generic-helper-unary))
(c/set (conj (map keyword (keys (ns-publics *ns*)))
:nest :raw))))))
(comment (comment
(-> (delete-from :table) (-> (delete-from :table)
(where [:in (composite :first :second) (where [:in (composite :first :second)

View file

@ -1,4 +1,4 @@
;; copyright (c) 2022-2024 sean corfield, all rights reserved ;; copyright (c) 2022 sean corfield, all rights reserved
(ns honey.sql.pg-ops (ns honey.sql.pg-ops
"Register all the PostgreSQL JSON/JSONB operators "Register all the PostgreSQL JSON/JSONB operators
@ -25,8 +25,6 @@
(:refer-clojure :exclude [-> ->> -]) (:refer-clojure :exclude [-> ->> -])
(:require [honey.sql :as sql])) (:require [honey.sql :as sql]))
#?(:clj (set! *warn-on-reflection* true))
;; see https://www.postgresql.org/docs/current/functions-json.html ;; see https://www.postgresql.org/docs/current/functions-json.html
(def -> (def ->
@ -51,16 +49,13 @@
(def - (def -
"The - operator: "The - operator:
- text value: deletes a key (and its value) from a JSON object, or matching string value(s) from a JSON array - text value: deletes a key (and its value) from a JSON object, or matching string value(s) from a JSON array
- text[] array value: as above, but for all the provided keys
- int value: deletes the array element with specified index (negative integers count from the end)" - int value: deletes the array element with specified index (negative integers count from the end)"
:-) :-)
(def hash- "The #- operator - deletes the field or array element at the specified path, where path elements can be either field keys or array indexes." :#-) (def hash- "The #- operator - deletes the field or array element at the specified path, where path elements can be either field keys or array indexes." :#-)
(def at? "The @? operator - does JSON path return any item for the specified JSON value?" (keyword "@?")) (def at? "The @? operator - does JSON path return any item for the specified JSON value?" (keyword "@?"))
(def atat (def atat
"The @@ operator: "The @@ operator - returns the result of a JSON path predicate check for the specified JSON value.
- returns the result of a JSON path predicate check for the specified JSON value. Only the first item of the result is taken into account. Only the first item of the result is taken into account. If the result is not Boolean, then NULL is returned."
If the result is not Boolean, then NULL is returned.
- checks if a text search vector (or a text value implicitly converted to a text search vector) matches a text search query. Returns a Boolean."
(keyword "@@")) (keyword "@@"))
(def tilde "The case-sensitive regex match operator." (keyword "~")) (def tilde "The case-sensitive regex match operator." (keyword "~"))

View file

@ -1,10 +1,8 @@
;; copyright (c) 2022-2024 sean corfield, all rights reserved ;; copyright (c) 2022 sean corfield, all rights reserved
(ns honey.sql.protocols (ns honey.sql.protocols
"InlineValue -- a protocol that defines how to inline "InlineValue -- a protocol that defines how to inline
values; (sqlize x) produces a SQL string for x.") values; (sqlize x) produces a SQL string for x.")
#?(:clj (set! *warn-on-reflection* true))
(defprotocol InlineValue :extend-via-metadata true (defprotocol InlineValue :extend-via-metadata true
(sqlize [this] "Render value inline in a SQL string.")) (sqlize [this] "Render value inline in a SQL string."))

View file

@ -1,109 +0,0 @@
(ns honey.sql.util
"Utility functions for the main honey.sql namespace."
(: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
reverts back to `clojure.core/str`."
(^String [] "")
(^String [^Object a]
#?(:clj (if (nil? a) "" (.toString a))
:default (clojure.core/str a)))
(^String [^Object a, ^Object b]
#?(:clj (if (nil? a)
(str b)
(if (nil? b)
(.toString a)
(.concat (.toString a) (.toString b))))
:default (clojure.core/str a b)))
(^String [a b c]
#?(:clj (let [sb (StringBuilder.)]
(.append sb (str a))
(.append sb (str b))
(.append sb (str c))
(.toString sb))
:default (clojure.core/str a b c)))
(^String [a b c d]
#?(:clj (let [sb (StringBuilder.)]
(.append sb (str a))
(.append sb (str b))
(.append sb (str c))
(.append sb (str d))
(.toString sb))
:default (clojure.core/str a b c d)))
(^String [a b c d e]
#?(:clj (let [sb (StringBuilder.)]
(.append sb (str a))
(.append sb (str b))
(.append sb (str c))
(.append sb (str d))
(.append sb (str e))
(.toString sb))
:default (clojure.core/str a b c d e)))
(^String [a b c d e & more]
#?(:clj (let [sb (StringBuilder.)]
(.append sb (str a))
(.append sb (str b))
(.append sb (str c))
(.append sb (str d))
(.append sb (str e))
(run! #(.append sb (str %)) more)
(.toString sb))
:default (apply clojure.core/str a b c d e more))))
(defn join
"More efficient implementation of `clojure.string/join`. May accept a transducer
`xform` to perform operations on each element before combining them together
into a string. Clojure-only, delegates to `clojure.string/join` on other
platforms."
([separator coll] (join separator identity coll))
([separator xform coll]
#?(:clj
(let [sb (StringBuilder.)
sep (str separator)]
(transduce xform
(fn
([] false)
([_] (.toString sb))
([add-sep? x]
(when add-sep? (.append sb sep))
(.append sb (str x))
true))
false coll))
: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) 2022-2024 sean corfield, all rights reserved ;; copyright (c) 2022 sean corfield, all rights reserved
(ns honey.bigquery-test (ns honey.bigquery-test
(:refer-clojure :exclude [format]) (:refer-clojure :exclude [format])

View file

@ -1,4 +1,4 @@
;; copyright (c) 2022-2024 sean corfield, all rights reserved ;; copyright (c) 2022 sean corfield, all rights reserved
(ns honey.cache-test (ns honey.cache-test
(:refer-clojure :exclude [format group-by]) (:refer-clojure :exclude [format group-by])

View file

@ -1,4 +1,4 @@
;; copyright (c) 2023-2025 sean corfield, all rights reserved ;; copyright (c) 2023 sean corfield, all rights reserved
(ns honey.ops-test (ns honey.ops-test
(:refer-clojure :exclude [format]) (:refer-clojure :exclude [format])
@ -9,11 +9,3 @@
(is (= ["SELECT a - b - c AS x"] (is (= ["SELECT a - b - c AS x"]
(-> {:select [[[:- :a :b :c] :x]]} (-> {:select [[[:- :a :b :c] :x]]}
(sut/format))))) (sut/format)))))
(deftest issue-566
(is (= ["SELECT * FROM table WHERE a IS DISTINCT FROM b"]
(-> {:select :* :from :table :where [:is-distinct-from :a :b]}
(sut/format))))
(is (= ["SELECT * FROM table WHERE a IS NOT DISTINCT FROM b"]
(-> {:select :* :from :table :where [:is-not-distinct-from :a :b]}
(sut/format)))))

View file

@ -1,44 +1,25 @@
;; copyright (c) 2020-2024 sean corfield, all rights reserved ;; copyright (c) 2020-2022 sean corfield, all rights reserved
(ns honey.sql.helpers-test (ns honey.sql.helpers-test
(:refer-clojure :exclude [filter for group-by partition-by set update]) (:refer-clojure :exclude [filter for group-by partition-by set update])
#_{:clj-kondo/ignore [:unused-namespace]} (:require [clojure.test :refer [deftest is testing]]
(:require [clojure.core :as c]
[clojure.test :refer [deftest is testing]]
[honey.sql :as sql] [honey.sql :as sql]
[honey.sql.helpers :as h [honey.sql.helpers :as h
:refer [add-column alter-table columns create-table create-table-as create-view :refer [add-column add-index alter-table columns create-table create-table-as create-view
create-materialized-view create-materialized-view drop-view drop-materialized-view
create-index create-index
bulk-collect-into bulk-collect-into
cross-join do-update-set drop-column drop-table cross-join do-update-set drop-column drop-index drop-table
filter from full-join filter from full-join
group-by having insert-into replace-into group-by having insert-into
join-by join left-join limit offset on-conflict join-by join lateral left-join limit offset on-conflict
on-duplicate-key-update on-duplicate-key-update
order-by over partition-by refresh-materialized-view order-by over partition-by refresh-materialized-view
returning right-join rename-column rename-table returning right-join
select select-distinct select-top select select-distinct select-top select-distinct-top
values where window with with-columns values where window with with-columns
with-data within-group]])) with-data within-group]]))
#?(:clj
(deftest helpers-are-complete
(let [helpers-ns (find-ns 'honey.sql.helpers)]
(testing "all public helpers have docstrings"
;; #409 this assert is only valid when :doc metadata is not elided:
(when (-> #'h/generic-helper-unary meta :doc)
;; ensure #295 stays true (all public functions have docstring):
(is (= [] (->> (ns-publics helpers-ns) (vals) (c/filter (comp not :doc meta)))))))
(testing "all clauses have public helpers"
;; ensure all public functions match clauses:
(is (= (c/set (conj @#'honey.sql/default-clause-order
:composite :filter :lateral :over :within-group
:upsert
:generic-helper-variadic :generic-helper-unary))
(c/set (conj (map keyword (keys (ns-publics helpers-ns)))
:nest :raw))))))))
(deftest test-select (deftest test-select
(testing "large helper expression" (testing "large helper expression"
(let [m1 (-> (with [:cte (-> (select :*) (let [m1 (-> (with [:cte (-> (select :*)
@ -355,15 +336,7 @@
{:params {:ids values} :numbered true}))))))) {:params {:ids values} :numbered true})))))))
(deftest test-case (deftest test-case
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo % ?) = ?) THEN foo / ? ELSE ? END FROM bar" (is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo MOD ?) = ?) THEN foo / ? ELSE ? END FROM bar"
0 -1 0 2 0 2 0]
(sql/format
{:select [[[:case
[:< :foo 0] -1
[:and [:> :foo 0] [:= [:% :foo 2] 0]] [:/ :foo 2]
:else 0]]]
:from [:bar]})))
(is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND (MOD(foo, ?) = ?) THEN foo / ? ELSE ? END FROM bar"
0 -1 0 2 0 2 0] 0 -1 0 2 0 2 0]
(sql/format (sql/format
{:select [[[:case {:select [[[:case
@ -552,33 +525,6 @@
" MAX(salary) OVER w AS MaxSalary" " MAX(salary) OVER w AS MaxSalary"
" FROM employee" " FROM employee"
" WINDOW w AS (PARTITION BY department)")])) " WINDOW w AS (PARTITION BY department)")]))
;; multiple window tests
(is (= (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
[[:max :salary] :w :MaxSalary]))
(from :employee)
(window :w (partition-by :department))
(window :x (partition-by :salary))
sql/format)
[(str "SELECT id,"
" AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,"
" MAX(salary) OVER w AS MaxSalary"
" FROM employee"
" WINDOW w AS (PARTITION BY department)"
", x AS (PARTITION BY salary)")]))
(is (= (-> (select :id
(over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average]
[[:max :salary] :w :MaxSalary]))
(from :employee)
(window :w (partition-by :department)
:x (partition-by :salary))
sql/format)
[(str "SELECT id,"
" AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average,"
" MAX(salary) OVER w AS MaxSalary"
" FROM employee"
" WINDOW w AS (PARTITION BY department)"
", x AS (PARTITION BY salary)")]))
;; test nil / empty window function clause: ;; test nil / empty window function clause:
(is (= (-> (select :id (is (= (-> (select :id
(over [[:avg :salary] {} :Average] (over [[:avg :salary] {} :Average]
@ -836,10 +782,7 @@
["INSERT INTO transport (id, name) SELECT * FROM cars"])) ["INSERT INTO transport (id, name) SELECT * FROM cars"]))
;; three arguments with an alias and columns: ;; three arguments with an alias and columns:
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)})) (is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"])) ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"])))
;; and again with replace-into:
(is (= (sql/format (replace-into '(transport t) '(id, name) '{select (*) from (cars)}))
["REPLACE INTO transport AS t (id, name) SELECT * FROM cars"])))
;; these tests are adapted from Cam Saul's PR #283 ;; these tests are adapted from Cam Saul's PR #283
@ -1037,24 +980,4 @@
(is (= ["CREATE UNIQUE INDEX IF NOT EXISTS my_column_idx ON my_table (my_column)"] (is (= ["CREATE UNIQUE INDEX IF NOT EXISTS my_column_idx ON my_table (my_column)"]
(sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column])))) (sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column]))))
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"] (is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column]))))) (sql/format (create-index :my-column-idx [:my-table :%lower.my-column]))))))
(testing "PostgreSQL extensions (USING GIN/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]))))
(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"]
(sql/format {:select :*
:from :foo
:left-join [[[:join [:populatons :pm]
{:join [[:customers :pc]
[:and
[:= :pm/id :pc/id]
[:= :pm/other-id :pc/other-id]]]}]]
[:= :foo/fk-id :pm/id]]}))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2022-2024 sean corfield, all rights reserved ;; copyright (c) 2022 sean corfield, all rights reserved
(ns honey.sql.pg-ops-test (ns honey.sql.pg-ops-test
(:require [clojure.test :refer [deftest is testing]] (:require [clojure.test :refer [deftest is testing]]

View file

@ -1,148 +0,0 @@
;; 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-2025 sean corfield, all rights reserved ;; copyright (c) 2021-2024 sean corfield, all rights reserved
(ns honey.sql-test (ns honey.sql-test
(:refer-clojure :exclude [format]) (:refer-clojure :exclude [format])
@ -6,8 +6,7 @@
[clojure.test :refer [deftest is testing]] [clojure.test :refer [deftest is testing]]
[honey.sql :as sut :refer [format]] [honey.sql :as sut :refer [format]]
[honey.sql.helpers :as h]) [honey.sql.helpers :as h])
#?(:clj (:import (clojure.lang ExceptionInfo) #?(:clj (:import (clojure.lang ExceptionInfo))))
(java.net URLEncoder))))
(deftest mysql-tests (deftest mysql-tests
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
@ -22,8 +21,6 @@
(sut/format-expr [:is :id nil]))) (sut/format-expr [:is :id nil])))
(is (= ["id = TRUE"] (is (= ["id = TRUE"]
(sut/format-expr [:= :id true]))) (sut/format-expr [:= :id true])))
(is (= ["[id] = ?" true]
(sut/format [:= :id true] {:dialect :sqlserver})))
(is (= ["id IS TRUE"] (is (= ["id IS TRUE"]
(sut/format-expr [:is :id true]))) (sut/format-expr [:is :id true])))
(is (= ["id <> TRUE"] (is (= ["id <> TRUE"]
@ -180,10 +177,8 @@
["WITH query AS MATERIALIZED (SELECT foo FROM bar)"])) ["WITH query AS MATERIALIZED (SELECT foo FROM bar)"]))
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :not-materialized]]}) (is (= (format {:with [[:query {:select [:foo] :from [:bar]} :not-materialized]]})
["WITH query AS NOT MATERIALIZED (SELECT foo FROM bar)"])) ["WITH query AS NOT MATERIALIZED (SELECT foo FROM bar)"]))
(is (= (format {:with [[:query {:select [:foo] :from [:bar]} :kw-1 :kw-2]]}) (is (= (format {:with [[:query {:select [:foo] :from [:bar]} :unknown]]})
["WITH query AS (SELECT foo FROM bar) KW 1 kw_2"])) ["WITH query AS (SELECT foo FROM bar)"]))
(is (= (format {:with-recursive [[:query {:select [:foo] :from [:bar]} :cycle [:a :b :c] :set :d :to [:abs :e] :default 42 :using :x]]})
["WITH RECURSIVE query AS (SELECT foo FROM bar) CYCLE a, b, c SET d TO ABS(e) DEFAULT ? USING x" 42]))
(is (= (format {:with [[:query1 {:select [:foo] :from [:bar]}] (is (= (format {:with [[:query1 {:select [:foo] :from [:bar]}]
[:query2 {:select [:bar] :from [:quux]}]] [:query2 {:select [:bar] :from [:quux]}]]
:select [:query1.id :query2.name] :select [:query1.id :query2.name]
@ -221,26 +216,7 @@
:from [:hits :stuff] :from [:hits :stuff]
:where [:= :EventDate :ts_upper_bound]}) :where [:= :EventDate :ts_upper_bound]})
["WITH ? AS ts_upper_bound, stuff AS (SELECT * FROM songs) SELECT * FROM hits, stuff WHERE EventDate = ts_upper_bound" ["WITH ? AS ts_upper_bound, stuff AS (SELECT * FROM songs) SELECT * FROM hits, stuff WHERE EventDate = ts_upper_bound"
"2019-08-01 15:23:00"]))) "2019-08-01 15:23:00"]))))
(testing "Use expression in a WITH clause"
(is (= (format
{:with [[:s [:sum :bytes]]]
:select [:s]
:from [:table]})
["WITH SUM(bytes) AS s SELECT s FROM table"]))
(is (= (format
{:with [[:v [:raw "m['k']"]]]
:select [:v]
:from [:table]})
["WITH m['k'] AS v SELECT v FROM table"]))
(is (= (format
{:with [[:cond [:and [:= :a 1] [:= :b 2] [:= :c 3]]]]
:select [:v]
:from [:table]
:where :cond})
["WITH (a = ?) AND (b = ?) AND (c = ?) AS cond SELECT v FROM table WHERE cond" 1 2 3]))))
(deftest insert-into (deftest insert-into
(is (= (format {:insert-into :foo}) (is (= (format {:insert-into :foo})
@ -380,11 +356,7 @@
(deftest compare-expressions-test (deftest compare-expressions-test
(testing "Sequences should be fns when in value/comparison spots" (testing "Sequences should be fns when in value/comparison spots"
(is (= ["SELECT foo FROM bar WHERE (col1 % ?) = (col2 + ?)" 4 4] (is (= ["SELECT foo FROM bar WHERE (col1 MOD ?) = (col2 + ?)" 4 4]
(format {:select [:foo]
:from [:bar]
:where [:= [:% :col1 4] [:+ :col2 4]]})))
(is (= ["SELECT foo FROM bar WHERE MOD(col1, ?) = (col2 + ?)" 4 4]
(format {:select [:foo] (format {:select [:foo]
:from [:bar] :from [:bar]
:where [:= [:mod :col1 4] [:+ :col2 4]]})))) :where [:= [:mod :col1 4] [:+ :col2 4]]}))))
@ -575,15 +547,13 @@
(-> {:delete-from :foo (-> {:delete-from :foo
:where [:= :foo.id 42]} :where [:= :foo.id 42]}
(format :dialect :mysql :pretty true))))) (format :dialect :mysql :pretty true)))))
(let [version #?(:cljs *clojurescript-version* (when (str/starts-with? #?(:cljs *clojurescript-version*
:default (clojure-version))] :default (clojure-version)) "1.11")
(when (or (str/starts-with? version "1.12") (testing "format can be called with mixed arguments"
(str/starts-with? version "1.11")) (is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
(testing "format can be called with mixed arguments" (-> {:delete-from :foo
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42] :where [:= :foo.id 42]}
(-> {:delete-from :foo (format :dialect :mysql {:pretty true})))))))
:where [:= :foo.id 42]}
(format :dialect :mysql {:pretty true}))))))))
(deftest delete-from-test (deftest delete-from-test
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42] (is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
@ -614,12 +584,6 @@
(format {:dialect :mysql})))) (format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"] (is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"]
(-> {:truncate [:foo :continue :identity]} (-> {:truncate [:foo :continue :identity]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2`"]
(-> {:truncate [[:t1 :t2]]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2` CONTINUE IDENTITY"]
(-> {:truncate [[:t1 :t2] :continue :identity]}
(format {:dialect :mysql}))))) (format {:dialect :mysql})))))
(deftest inlined-values-are-stringified-correctly (deftest inlined-values-are-stringified-correctly
@ -1184,10 +1148,9 @@ ORDER BY id = ? DESC
(deftest issue-474-dot-selection (deftest issue-474-dot-selection
(testing "basic dot selection" (testing "basic dot selection"
(is (= ["SELECT a.b, c.d, a.d.x, a.d.x.y"] (is (= ["SELECT a.b, c.d, a.d.x"]
(let [t :a c :d] (let [t :a c :d]
(sut/format {:select [[[:. t :b]] [[:. :c c]] (sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}))))
[[:. t c :x]] [[:. t c :x :y]]]}))))
(is (= ["SELECT [a].[b], [c].[d], [a].[d].[x]"] (is (= ["SELECT [a].[b], [c].[d], [a].[d].[x]"]
(let [t :a c :d] (let [t :a c :d]
(sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]} (sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]}
@ -1201,54 +1164,8 @@ ORDER BY id = ? DESC
(sut/format '{select (((. (nest v) *)) (sut/format '{select (((. (nest v) *))
((. (nest w) x)) ((. (nest w) x))
((. (nest (y z)) *)))} ((. (nest (y z)) *)))}
{:dialect :mysql})))
(is (= ["SELECT (v).*, (w).x, (Y(z)).*"]
(sut/format '{select (((get-in v *))
((get-in w x))
((get-in (y z) *)))})))
(is (= ["SELECT (`v`).*, (`w`).`x`, (Y(`z`)).*"]
(sut/format '{select (((get-in v *))
((get-in w x))
((get-in (y z) *)))}
{:dialect :mysql}))))) {:dialect :mysql})))))
(deftest issue-570-snowflake-dot-selection
(testing "basic colon selection"
(is (= ["SELECT a:b, c:d, a:d.x, a:d.x.y"]
(let [t :a c :d]
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]]
[[:.:. t c :x]] [[:.:. t c :x :y]]]}))))
(is (= ["SELECT [a]:[b], [c]:[d], [a]:[d].[x]"]
(let [t :a c :d]
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]] [[:.:. t c :x]]]}
{:dialect :sqlserver})))))
(testing "basic field selection from composite"
(is (= ["SELECT (v):*, (w):x, (Y(z)):*"]
(sut/format '{select (((.:. (nest v) *))
((.:. (nest w) x))
((.:. (nest (y z)) *)))})))
(is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"]
(sut/format '{select (((.:. (nest v) *))
((.:. (nest w) x))
((.:. (nest (y z)) *)))}
{:dialect :mysql}))))
(testing "bracket selection"
(is (= ["SELECT a['b'], c['b'], a['d'].x, a:e[0].name"]
(sut/format {:select [[[:at :a [:inline "b"]]]
[[:at :c "b"]]
[[:at :a [:inline "d"] :x]]
[[:.:. :a [:at :e [:inline 0]] :name]]]})))
(is (= ["SELECT a[?].name" 0]
(sut/format '{select (((at a (lift 0) name)))})))
;; sanity check, compare with get-in:
(is (= ["SELECT (a)[?].name" 0]
(sut/format '{select (((get-in a (lift 0) name)))})))
(is (= ["SELECT (a)['b'], (c)['b'], (a)['d'].x, a:(e)[0].name"]
(sut/format {:select [[[:get-in :a [:inline "b"]]]
[[:get-in :c "b"]]
[[:get-in :a [:inline "d"] :x]]
[[:.:. :a [:get-in :e [:inline 0]] :name]]]})))))
(deftest issue-476-raw (deftest issue-476-raw
(testing "single argument :raw" (testing "single argument :raw"
(is (= ["@foo := 42"] (is (= ["@foo := 42"]
@ -1424,107 +1341,10 @@ ORDER BY id = ? DESC
(= ["SELECT * FROM table WITH (DEF, ABC)"] (= ["SELECT * FROM table WITH (DEF, ABC)"]
(sut/format {:select [:*] :from [^{:abc true :def true} [:table]]})))))) (sut/format {:select [:*] :from [^{:abc true :def true} [:table]]}))))))
(deftest issue-527-composite
(is (= ["SELECT (a, b) AS c FROM table"]
(sut/format {:select [[[:composite :a :b] :c]] :from [:table]})))
(is (= ["SELECT a FROM table WHERE (b, c) = (?, ?)" 1 2]
(sut/format {:select :a :from :table :where [:= [:composite :b :c] [:composite 1 2]]})))
(is (= ["SELECT a, b, c FROM (VALUES (?, ?, ?), (?, ?, ?)) AS t (a, b, c)" 1 2 3 4 5 6]
(sut/format {:select [:a :b :c]
:from [[{:values [[1 2 3] [4 5 6]]}
[:t [:composite :a :b :c]]]]}))))
(deftest issue-543-param
(testing "quoted param with symbol param"
(is (= ["SELECT a FROM table WHERE x = ?" 42]
(sut/format '{select a from table where (= x (param y))}
{:params {'y 42}})))
(is (= ["SELECT a FROM table WHERE x = ?" 42]
(sut/format '{select a from table where (= x ?y)}
{:params {'y 42}}))))
(testing "quoted param with keyword param"
(is (= ["SELECT a FROM table WHERE x = ?" 42]
(sut/format '{select a from table where (= x (param y))}
{:params {:y 42}})))
(is (= ["SELECT a FROM table WHERE x = ?" 42]
(sut/format '{select a from table where (= x :?y)}
{:params {:y 42}}))))
(testing "all combinations"
(doseq [p1 [:y 'y] p2 [:y 'y]]
(is (= ["SELECT a FROM table WHERE x = ?" 42]
(sut/format {:select :a :from :table :where [:= :x [:param p1]]}
{:params {p2 42}}))))))
(deftest issue-n-using
(testing "all keywords"
(is (= ["SELECT * FROM `t1` INNER JOIN `t2` USING (`id`) WHERE `t1`.`id` = ?" 1]
(sut/format {:select :* :from :t1 :join [:t2 [:using :id]] :where [:= :t1/id 1]} {:dialect :mysql}))))
(testing "all symbols"
(is (= ["SELECT * FROM `t1` INNER JOIN `t2` USING (`id`) WHERE `t1`.`id` = ?" 1]
(sut/format '{select * from t1 join (t2 (using id)) where (= t1/id 1)} {:dialect :mysql}))))
(testing "mixed keywords and symbols"
(is (= ["SELECT * FROM `t1` INNER JOIN `t2` USING (`id`) WHERE `t1`.`id` = ?" 1]
(sut/format '{select * from t1 join (t2 (:using id)) where (= t1/id 1)} {:dialect :mysql})))))
(deftest issue-548-format-var-encoding
(is (= ["CREATE TABLE \"With%20Space\""]
(sut/format {:create-table "With%20Space"})))
(is (= ["CREATE TABLE \"%20WithLeadingSpace\""]
(sut/format {:create-table "%20WithLeadingSpace"})))
#?(:clj (let [table (URLEncoder/encode "привіт")]
(is (= [(str "CREATE TABLE \"" table "\"")]
(sut/format {:create-table table}))))))
(deftest issue-555-setting
(testing "setting default time"
(is (= ["SETTING DEFAULT SYSTEM_TIME AS OF DATE '2024-11-24'"]
(sut/format {:setting [:default :system-time :as-of [:inline :DATE "2024-11-24"]]})))
(is (= ["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023'"]
(sut/format {:setting [[:snapshot-time :to [:inline :DATE "2024-11-24"]]
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]]]})))
(is (= ["SETTING DEFAULT SYSTEM_TIME AS OF DATE '2024-11-24' SELECT * FROM table"]
(sut/format (-> (h/setting :default :system-time :as-of [:inline :DATE "2024-11-24"])
(h/select :*)
(h/from :table)))))
(is (= ["SETTING SNAPSHOT_TIME TO DATE '2024-11-24', DEFAULT VALID_TIME TO BETWEEN DATE '2022' AND DATE '2023' SELECT * FROM table"]
(sut/format (-> (h/setting [:snapshot-time :to [:inline :DATE "2024-11-24"]]
[:default :valid-time :to :between [:inline :DATE "2022"] :and [:inline :DATE "2023"]])
(h/select :*)
(h/from :table)))))))
(deftest issue-571
(testing "an empty where clause is omitted"
(is (= ["SELECT * FROM foo"]
(sut/format {:select :* :from :foo :where []})))
#?(:clj
(is (thrown? clojure.lang.ExceptionInfo
(sut/format {:select :* :from :foo :where nil}))))
(is (= ["SELECT * FROM foo WHERE 1 = 1"]
(sut/format {:select :* :from :foo :where [:= 1 1]} {:inline true}))))
(testing "an empty order by clause is omitted"
(is (= ["SELECT * FROM foo"]
(sut/format {:select :* :from :foo :order-by []})))
#?(:clj
(is (thrown? clojure.lang.ExceptionInfo
(sut/format {:select :* :from :foo :order-by nil}))))
(is (= ["SELECT * FROM foo ORDER BY bar ASC"]
(sut/format {:select :* :from :foo :order-by [:bar]})))))
(comment (comment
;; partial (incorrect!) workaround for #407: ;; partial (incorrect!) workaround for #407:
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]}) (sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})
;; correct version: ;; correct version:
(sut/format {:select :f.* :from [[:foo :f :for :system-time]] :where [:= :f.id 1]}) (sut/format {:select :f.* :from [[:foo :f :for :system-time]] :where [:= :f.id 1]})
(sut/format {:where [:= :x [:inline :DATE "2019-01-01"]]}) (sut/format {:where [:= :x [:inline :DATE "2019-01-01"]]})
;; https://github.com/seancorfield/honeysql/issues/526
(->
{:create-table-as [:a-b.b-c.c-d]
: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

@ -1,4 +1,4 @@
;; copyright (c) 2023-2024 sean corfield, all rights reserved ;; copyright (c) 2023 sean corfield, all rights reserved
(ns honey.union-test (ns honey.union-test
(:refer-clojure :exclude [format]) (:refer-clojure :exclude [format])

View file

@ -1,62 +0,0 @@
(ns honey.util-test
(:refer-clojure :exclude [str])
(:require [clojure.test :refer [deftest is are]]
[honey.sql.util :as sut]))
(deftest str-test
(are [arg1 result] (= result (sut/str arg1))
nil ""
1 "1"
"foo" "foo"
:foo ":foo")
(are [arg1 arg2 result] (= result (sut/str arg1 arg2))
nil nil ""
nil 1 "1"
1 nil "1"
1 2 "12"
:foo "bar" ":foobar")
(are [arg1 arg2 arg3 result] (= result (sut/str arg1 arg2 arg3))
nil nil nil ""
nil 1 nil "1"
1 nil nil "1"
1 nil 2 "12"
:foo "bar" 'baz ":foobarbaz")
(are [args result] (= result (apply sut/str args))
(range 10) "0123456789"
[] ""))
(deftest join-test
(is (= "0123456789" (sut/join "" (range 10))))
(is (= "1" (sut/join "" [1])))
(is (= "" (sut/join "" [])))
(is (= "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" (sut/join ", " (range 10))))
(is (= "1" (sut/join ", " [1])))
(is (= "" (sut/join ", " [])))
(is (= "0_0, 1_1, 2_2, 3_3, 4_4, 5_5, 6_6, 7_7, 8_8, 9_9"
(sut/join ", " (map #(sut/str % "_" %)) (range 10))))
(is (= "1_1"
(sut/join ", " (map #(sut/str % "_" %)) [1])))
(is (= ""
(sut/join ", " (map #(sut/str % "_" %)) [])))
(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]))))