Compare commits

..

No commits in common. "develop" and "issue-422-quoting" have entirely different histories.

48 changed files with 1045 additions and 5262 deletions

View file

@ -1 +1,2 @@
{} {:lint-as
{honeysql.helpers/defhelper clojure.core/defn}}

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

@ -9,19 +9,19 @@ jobs:
build-and-release: build-and-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-java@v4 - uses: actions/setup-java@v2
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.10.3.1053'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository

View file

@ -9,17 +9,17 @@ jobs:
build-and-snapshot: build-and-snapshot:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: actions/setup-java@v4 - uses: actions/setup-java@v2
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.10.3.1053'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v2
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' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: actions/setup-java@v4 - uses: actions/setup-java@v2
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.10.3.1053'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v2
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' ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: actions/setup-java@v4 - uses: actions/setup-java@v2
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.10.3.1053'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: | path: |
~/.m2/repository ~/.m2/repository

10
.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,11 @@
.settings .settings
.socket-repl-port .socket-repl-port
.sw* .sw*
.vscode
*.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

View file

@ -1,201 +1,9 @@
# Changes # Changes
* 2.7.next in progress * 2.3.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
* Address [#524](https://github.com/seancorfield/honeysql/issues/524) by adding example of `{:nest ..}` in `:union` clause reference docs.
* Address [#523](https://github.com/seancorfield/honeysql/issues/523) by expanding examples in README **Functions** to show aliases.
* Address [#522](https://github.com/seancorfield/honeysql/issues/522) by supporting metadata on table specifications in `:from` and `:join` clauses to provide index hints (SQL Server).
* ~Address [#521](https://github.com/seancorfield/honeysql/issues/521) by adding initial experimental support for an XTDB dialect.~ _[This was removed in 2.6.1243 since XTDB no longer supports qualified column names]_
* Address [#520](https://github.com/seancorfield/honeysql/issues/520) by expanding how `:inline` works, to support a sequence of arguments.
* Fix [#518](https://github.com/seancorfield/honeysql/issues/518) by moving temporal clause before alias.
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding `formatv` macro (`.clj` only!) -- and removing the experimental `formatf` function (added for discussion in 2.4.1045).
* Implemented `CREATE INDEX` [#348](https://github.com/seancorfield/honeysql/issues/348) via PR [#517](https://github.com/seancorfield/honeysql/pull/517) [@dancek](https://github.com/dancek).
* Mention `:not-in` explicitly in the documentation.
* Code cleanup per `clj-kondo`.
* 2.5.1103 -- 2023-12-03
* Address [#515](https://github.com/seancorfield/honeysql/issues/515) by:
* Quoting entities that start with a digit but are otherwise alphanumeric. Note that entities that are all digits (optionally including underscores) will still not be quoted as in previous releases,
* Adding a new `:quoted-always` option allows users to specify a regex that matches entities that should always be quoted (stropped) regardless of the value of `:quoted` (such as reserved words that you have used as column or table names).
* Address [#513](https://github.com/seancorfield/honeysql/issues/513) by:
* Ignoring `:file`, `:line`, `:column`, `:end-line`, and `:end-column` metadata keys (previously only `:line` and `:column` were ignored),
* Adding an `:ignored-metadata` option to allow additional keys to be ignored.
* 2.5.1091 -- 2023-10-28
* Address [#512](https://github.com/seancorfield/honeysql/issues/512) by adding support for subqueries in the `:array` special syntax (for BigQuery and PostgreSQL). This also adds support for metadata on the `:select` value to produce `AS STRUCT` or `DISTINCT`.
* Address [#511](https://github.com/seancorfield/honeysql/issues/511) by adding support for BigQuery `CREATE OR REPLACE`.
* Address [#510](https://github.com/seancorfield/honeysql/issues/510) by adding initial support for an NRQL dialect.
* Fix [#509](https://github.com/seancorfield/honeysql/issues/509) by checking for `ident?` before checking keyword/symbol.
* 2.4.1078 -- 2023-10-07
* Address [#507](https://github.com/seancorfield/honeysql/issues/507) by clarifying formatting of `:cast` in **Special Syntax**.
* Fix [#505](https://github.com/seancorfield/honeysql/issues/505) by rewriting the helper merge function to handle both keywords and symbols properly.
* Address [#503](https://github.com/seancorfield/honeysql/issues/503) by adding `:at-time-zone` special syntax.
* Address [#504](https://github.com/seancorfield/honeysql/issues/504) for BigQuery support, by adding special syntax for ignore/respect nulls, as well as new `:distinct` and `:expr` clauses to allow expressions to be qualified with SQL clauses. The latter will probably be useful for other dialects too.
* Update `tools.build` to 0.9.6 (and get rid of `template/pom.xml` in favor of new `:pom-data` option to `b/write-pom`).
* 2.4.1066 -- 2023-08-27
* Add `:select` with function call and alias example to README (PR [#502](https://github.com/seancorfield/honeysql/pull/502) [@markbastian](https://github.com/markbastian)).
* Address [#501](https://github.com/seancorfield/honeysql/issues/501) by making `INSERT INTO` (and `REPLACE INTO`) use the `:columns` or `:values` clauses to produce column names (which are then omitted from those other clauses).
* Address [#497](https://github.com/seancorfield/honeysql/issues/497) by adding `:alias` special syntax.
* Address [#496](https://github.com/seancorfield/honeysql/issues/496) by adding `:overriding-value` option to `:insert` clause.
* Address [#407](https://github.com/seancorfield/honeysql/issues/407) by adding support for temporal queries (see `FROM` in [SQL Clause Reference](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#from)).
* Address [#389](https://github.com/seancorfield/honeysql/issues/389) by adding examples of `[:only :table]` producing `ONLY(table)`.
* Add `:create-or-replace-view` to support PostgreSQL's lack of `IF NOT EXISTS` for `CREATE VIEW`.
* Attempt to clarify the formatting behavior of the `:values` clause when used to produce column names.
* Update `tools.build` to 0.9.5 (and remove `:java-opts` setting from `build/run-task`)
* 2.4.1045 -- 2023-06-25
* Address [#495](https://github.com/seancorfield/honeysql/issues/495) by adding (experimental) `formatf` function -- _note: this was removed in 2.6.1126, in favor of the `formatv` macro._
* Fix [#494](https://github.com/seancorfield/honeysql/issues/494) by supporting expressions in `:on-conflict` instead of just entities.
* Address [#493](https://github.com/seancorfield/honeysql/issues/493) by clarifying use of `:values` in CTEs (using `:with`).
* Address [#489](https://github.com/seancorfield/honeysql/issues/489) by adding more examples around `:update`.
* Attempt to improve `honey.sql.helpers` namespace docstring (by adding a note from the relevant **Getting Started** section).
* Update dev/test dependencies.
* 2.4.1033 -- 2023-05-22
* Tentative [ClojureCLR](https://github.com/clojure/clojure-clr) support.
* Improve `on-conflict` helper docstring [#490](https://github.com/seancorfield/honeysql/pull/490) [@holyjak](https://github.com/holyjak).
* 2.4.1026 -- 2023-04-15
* Fix [#486](https://github.com/seancorfield/honeysql/issues/486) by supporting ANSI-style `INTERVAL` syntax.
* Fix [#485](https://github.com/seancorfield/honeysql/issues/485) by adding `:with-ordinality` "operator".
* Fix [#484](https://github.com/seancorfield/honeysql/issues/484) by adding `TABLE` to `TRUNCATE`.
* Fix [#483](https://github.com/seancorfield/honeysql/issues/483) by adding a function-like `:join` syntax to produce nested `JOIN` expressions.
* Update `tools.build`; split alias `:test`/`:runner` for friendlier jack-in UX while developing.
* 2.4.1011 -- 2023-03-23
* Address [#481](https://github.com/seancorfield/honeysql/issues/481) by adding more examples around `:do-update-set`.
* Address [#480](https://github.com/seancorfield/honeysql/issues/480) by clarifying the general relationship between clauses and helpers.
* Address [#448](https://github.com/seancorfield/honeysql/issues/448) by adding a new section with hints and tips for database-specific syntax and solutions.
* 2.4.1006 -- 2023-03-17
* Fix [#478](https://github.com/seancorfield/honeysql/issues/478) by handling `:do-update-set` correctly in the `upsert` helper and by handling parameters correctly in the `:do-update-set` formatter.
* Fix [#476](https://github.com/seancorfield/honeysql/issues/476) by adding support for multiple arguments to `:raw`, essentially restoring 1.x functionality (while still allowing for embedded vectors as expressions, introduced in 2.x).
* 2.4.1002 -- 2023-03-03
* Address [#474](https://github.com/seancorfield/honeysql/issues/474) by adding dot-selection special syntax.
* Improve docstrings for PostgreSQL operators via PR [#473](https://github.com/seancorfield/honeysql/pull/473) [@holyjak](https://github.com/holyjak).
* Address [#471](https://github.com/seancorfield/honeysql/issues/471) by supporting interspersed SQL keywords in function calls.
* Fix [#467](https://github.com/seancorfield/honeysql/issues/467) by allowing single keywords (symbols) as a short hand for a single-element sequence in more constructs via PR [#470](https://github.com/seancorfield/honeysql/pull/470) [@p-himik](https://github.com/p-himik).
* Address [#466](https://github.com/seancorfield/honeysql/issues/466) by treating `[:and]` as `TRUE` and `[:or]` as `FALSE`.
* Fix [#465](https://github.com/seancorfield/honeysql/issues/465) to allow multiple columns in `:order-by` special syntax via PR [#468](https://github.com/seancorfield/honeysql/pull/468) [@p-himik](https://github.com/p-himik).
* Fix [#464](https://github.com/seancorfield/honeysql/issues/464) by adding an optional type argument to `:array` via PR [#469](https://github.com/seancorfield/honeysql/pull/469) [@p-himik](https://github.com/p-himik).
* Address [#463](https://github.com/seancorfield/honeysql/issues/463) by explaining `:quoted nil` via PR [#475](https://github.com/seancorfield/honeysql/pull/475) [@nharsch](https://github.com/nharsch).
* Address [#462](https://github.com/seancorfield/honeysql/issues/462) by adding a note in the documentation for set operations, clarifying precedence issues.
* 2.4.980 -- 2023-02-15
* Fix [#461](https://github.com/seancorfield/honeysql/issues/461) -- a regression introduced in 2.4.979 -- by restricting unary operators to just `+`, `-`, and `~` (bitwise negation).
* 2.4.979 -- 2023-02-11
* Address [#459](https://github.com/seancorfield/honeysql/issues/459) by making all operators variadic (except `:=` and `:<>`).
* Address [#458](https://github.com/seancorfield/honeysql/issues/458) by adding `registered-*?` predicates.
* 2.4.972 -- 2023-02-02
* Address [#456](https://github.com/seancorfield/honeysql/issues/456) by allowing `format` to handle expressions (like 1.x could) as well as statements. This should aid with migration from 1.x to 2.x.
* 2.4.969 -- 2023-01-14
* Fix [#454](https://github.com/seancorfield/honeysql/issues/454) by allowing `-` to be variadic.
* Address [#452](https://github.com/seancorfield/honeysql/pull/452) by adding `:replace-into` to the core SQL supported, instead of just for the MySQL and SQLite dialects (so the latter is not needed yet).
* Address [#451](https://github.com/seancorfield/honeysql/issues/451) by adding a test for it, showing how `:nest` produces the desired result.
* Address [#447](https://github.com/seancorfield/honeysql/issues/447) by updating GitHub Actions and dependencies.
* Address [#445](https://github.com/seancorfield/honeysql/issues/445) and [#453](https://github.com/seancorfield/honeysql/issues/453) by adding key/constraint examples to `CREATE TABLE` docs.
* 2.4.962 -- 2022-12-17
* Fix `set-options!` (only `:checking` worked in 2.4.947).
* Fix `:cast` formatting when quoting is enabled, via PR [#443](https://github.com/seancorfield/honeysql/pull/443) [duddlf23](https://github.com/duddlf23). **This changes how type names containing `-` are formatted in a cast.** See [`cast` Special Syntax](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-special-syntax-#cast) for more details.
* Fix [#441](https://github.com/seancorfield/honeysql/issues/441) by adding `:replace-into` to in-flight clause order (as well as registering it for the `:mysql` dialect).
* Fix [#434](https://github.com/seancorfield/honeysql/issues/434) by special-casing `:'ARRAY`.
* Fix [#433](https://github.com/seancorfield/honeysql/issues/433) by supporting additional `WITH` syntax, via PR [#432](https://github.com/seancorfield/honeysql/issues/432), [@MawiraIke](https://github.com/MawiraIke). _[Technically, this was in 2.4.947, but I kept the issue open while I wordsmithed the documentation]_
* Address [#405](https://github.com/seancorfield/honeysql/issues/405) by adding `:numbered` option, which can also be set globally using `set-options!`.
* 2.4.947 -- 2022-11-05
* Fix [#439](https://github.com/seancorfield/honeysql/issues/439) by rewriting how DDL options are processed; also fixes [#386](https://github.com/seancorfield/honeysql/issues/386) and [#437](https://github.com/seancorfield/honeysql/issues/437); **Whilst this is intended to be purely a bug fix, it has the potential to be a breaking change -- hence the version jump to 2.4!**
* Fix [#438](https://github.com/seancorfield/honeysql/issues/438) by
supporting options on `TRUNCATE`.
* Address [#435](https://github.com/seancorfield/honeysql/issues/435) by showing `CREATE TEMP TABLE` etc.
* Fix [#431](https://github.com/seancorfield/honeysql/issues/431) -- `WHERE false` differed between the DSL and the `where` helper.
* Address [#430](https://github.com/seancorfield/honeysql/issues/430) by treating `:'` as introducing a name that should be treated literally and not formatted as a SQL entity (which respects quoting, dot-splitting, etc); this effectively expands the "escape hatch" introduced via [#352](https://github.com/seancorfield/honeysql/issues/352) in 2.2.868. _Note that the function context behavior formats as a SQL entity, rather than the usual SQL "keyword", whereas this new context is a literal transcription rather than as a SQL entity!_
* Address [#427](https://github.com/seancorfield/honeysql/issues/427) by adding `set-options!`.
* Address [#415](https://github.com/seancorfield/honeysql/issues/415) by supporting multiple column names in `ADD COLUMN`, `ALTER COLUMN`, `DROP COLUMN`, and `MODIFY COLUMN`.
* 2.3.928 -- 2022-09-04
* Address [#425](https://github.com/seancorfield/honeysql/issues/425) by clarifying that `INTERVAL` as special syntax may be MySQL-specific and PostgreSQL uses difference syntax (because `INTERVAL` is a data type there). * Address [#425](https://github.com/seancorfield/honeysql/issues/425) by clarifying that `INTERVAL` as special syntax may be MySQL-specific and PostgreSQL uses difference syntax (because `INTERVAL` is a data type there).
* Address [#423](https://github.com/seancorfield/honeysql/issues/423) by supporting `DEFAULT` values and `DEFAULT` rows in `VALUES`. * Address [#423](https://github.com/seancorfield/honeysql/issues/423) by supporting `DEFAULT` values and `DEFAULT` rows in `VALUES` clause -- NEEDS DOCUMENTATION!
* Address [#422](https://github.com/seancorfield/honeysql/issues/422) by auto-quoting unusual entity names when `:quoted` (and `:dialect`) are not specified, making HoneySQL more secure by default. * **WIP** Address [#422](https://github.com/seancorfield/honeysql/issues/422) by auto-quoting unusual entity names when `:quoted` (and `:dialect`) are not specified, making HoneySQL more secure by default.
* Fix [#421](https://github.com/seancorfield/honeysql/issues/421) by adding `:replace-into` for `:mysql` dialect.
* Address [#419](https://github.com/seancorfield/honeysql/issues/419) by adding `honey.sql.protocols` and `InlineValue` with a `sqlize` function. * Address [#419](https://github.com/seancorfield/honeysql/issues/419) by adding `honey.sql.protocols` and `InlineValue` with a `sqlize` function.
* Address [#413](https://github.com/seancorfield/honeysql/issues/413) by flagging a lack of `WHERE` clause for `DELETE`, `DELETE FROM`, and `UPDATE` when `:checking :basic` (or `:checking :strict`). * Address [#413](https://github.com/seancorfield/honeysql/issues/413) by flagging a lack of `WHERE` clause for `DELETE`, `DELETE FROM`, and `UPDATE` when `:checking :basic` (or `:checking :strict`).
* Fix [#392](https://github.com/seancorfield/honeysql/issues/392) by adding support for `WITH` / (`NOT`) `MATERIALIZED` -- via PR [#420](https://github.com/seancorfield/honeysql/issues/420) [@robhanlon22](https://github.com/robhanlon22). * Fix [#392](https://github.com/seancorfield/honeysql/issues/392) by adding support for `WITH` / (`NOT`) `MATERIALIZED` -- via PR [#420](https://github.com/seancorfield/honeysql/issues/420) [@robhanlon22](https://github.com/robhanlon22).
@ -225,7 +33,7 @@
* 2.2.868 -- 2022-02-21 * 2.2.868 -- 2022-02-21
* Address [#387](https://github.com/seancorfield/honeysql/issues/387) by making the function simpler. * Address [#387](https://github.com/seancorfield/honeysql/issues/387) by making the function simpler.
* Fix [#385](https://github.com/seancorfield/honeysql/issues/385) by quoting inlined UUIDs. * Fix [#385](https://github.com/seancorfield/honeysql/issues/385) by quoting inlined UUIDs.
* Address [#352](https://github.com/seancorfield/honeysql/issues/352) by treating `:'` as introducing a function name that should be formatted as a SQL entity (which respects quoting, dot-splitting, etc), rather than as a SQL "keyword". * Address [#352](https://github.com/seancorfield/honeysql/issues/352) by treating `:'` as introducing a function name that should be formatted as a SQL entity (respects quoting, dot-splitting, etc).
* 2.2.861 -- 2022-01-30 * 2.2.861 -- 2022-01-30
* Address [#382](https://github.com/seancorfield/honeysql/issues/382) by adding `:case-expr` for BigQuery support. * Address [#382](https://github.com/seancorfield/honeysql/issues/382) by adding `:case-expr` for BigQuery support.
@ -276,7 +84,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)

234
README.md
View file

@ -1,24 +1,18 @@
# 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 Project](https://clojars.org/com.github.seancorfield/honeysql/latest-version.svg)](https://clojars.org/com.github.seancorfield/honeysql) [![cljdoc badge](https://cljdoc.org/badge/com.github.seancorfield/honeysql?2.3.911)](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
[![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/honeysql?2.7.1295)](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT)
[![Slack](https://img.shields.io/badge/slack-HoneySQL-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=honeysql)
[![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net)
[![Zulip](https://img.shields.io/badge/zulip-honeysql-orange.svg?logo=zulip)](https://clojurians.zulipchat.com/#narrow/channel/152091-honeysql)
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository. 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.999-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 +31,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
[![Clojars Project](https://clojars.org/honeysql/honeysql/latest-version.svg)](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,11 +48,11 @@ 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 '[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:
;; ;;
;; distinct, filter, for, group-by, into, partition-by, set, and update ;; filter, for, group-by, into, partition-by, set, and update
;; ;;
;; you should generally only refer in the specific ;; you should generally only refer in the specific
;; helpers that you want to use! ;; helpers that you want to use!
@ -116,13 +116,6 @@ If you want to format the query as a string with no parameters (e.g. to use the
=> ["SELECT a, b, c FROM foo WHERE foo.a = 'baz'"] => ["SELECT a, b, c FROM foo WHERE foo.a = 'baz'"]
``` ```
As seen above, the default parameterization uses positional parameters (`?`) with the order of values in the generated vector matching the order of those placeholders in the SQL. As of 2.4.962, you can specified `:numbered true` as an option to produce numbered parameters (`$1`, `$2`, etc):
```clojure
(sql/format sqlmap {:numbered true})
=> ["SELECT a, b, c FROM foo WHERE foo.a = $1" "baz"]
```
Namespace-qualified keywords (and symbols) are generally treated as table-qualified columns: `:foo/bar` becomes `foo.bar`, except in contexts where that would be illegal (such as the list of columns in an `INSERT` statement). This approach is likely to be more compatible with code that uses libraries like [`next.jdbc`](https://github.com/seancorfield/next-jdbc) and [`seql`](https://github.com/exoscale/seql), as well as being more convenient in a world of namespace-qualified keywords, following the example of `clojure.spec` etc. Namespace-qualified keywords (and symbols) are generally treated as table-qualified columns: `:foo/bar` becomes `foo.bar`, except in contexts where that would be illegal (such as the list of columns in an `INSERT` statement). This approach is likely to be more compatible with code that uses libraries like [`next.jdbc`](https://github.com/seancorfield/next-jdbc) and [`seql`](https://github.com/exoscale/seql), as well as being more convenient in a world of namespace-qualified keywords, following the example of `clojure.spec` etc.
```clojure ```clojure
@ -139,24 +132,6 @@ Namespace-qualified keywords (and symbols) are generally treated as table-qualif
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"] => ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
``` ```
As of 2.6.1126, there is a helper macro you can use with quoted symbolic
queries (that are purely literal, not programmatically constructed) to
provide "escape hatches" for certain symbols that you want to be treated
as locally bound symbols (and, hence, their values):
<!-- :test-doc-blocks/skip -->
```clojure
;; quoted symbolic query with local substitution:
(let [search-value "baz"]
(sql/formatv [search-value]
'{select (foo/a, foo/b, foo/c)
from (foo)
where (= foo/a search-value)}))
=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
```
> Note: this is a Clojure-only feature and is not available in ClojureScript, and it is intended for literal, inline symbolic queries only, not for programmatically constructed queries (where you would be able to substitute the values directly, as you build the query).
Documentation for the entire data DSL can be found in the Documentation for the entire data DSL can be found in the
[Clause Reference](doc/clause-reference.md), the [Clause Reference](doc/clause-reference.md), the
[Operator Reference](doc/operator-reference.md), and the [Operator Reference](doc/operator-reference.md), and the
@ -175,10 +150,6 @@ function in the `honey.sql.helpers` namespace:
=> {:select [:a :b :c] :from [:foo] :where [:= :foo.a "baz"]} => {:select [:a :b :c] :from [:foo] :where [:= :foo.a "baz"]}
``` ```
In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}`
(with a few exceptions -- see the docstring of the helper function
for details).
Order doesn't matter (for independent clauses): Order doesn't matter (for independent clauses):
```clojure ```clojure
@ -217,24 +188,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 +199,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.
@ -288,7 +226,8 @@ then provide a collection of rows, each a collection of column values:
["Jane" "Daniels" 56]]) ["Jane" "Daniels" 56]])
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
(name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
" "
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56] "Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
@ -300,7 +239,8 @@ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
["Jane" "Daniels" 56]]} ["Jane" "Daniels" 56]]}
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
(name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
" "
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56] "Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
@ -317,8 +257,8 @@ Alternately, you can simply specify the values as maps:
{:name "Jane" :surname "Daniels" :age 56}]) {:name "Jane" :surname "Daniels" :age 56}])
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) (name, surname, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
" "
"John" "Smith" 34 "John" "Smith" 34
"Andrew" "Cooper" 12 "Andrew" "Cooper" 12
@ -330,8 +270,8 @@ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
{:name "Jane", :surname "Daniels", :age 56}]} {:name "Jane", :surname "Daniels", :age 56}]}
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) (name, surname, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
" "
"John" "Smith" 34 "John" "Smith" 34
"Andrew" "Cooper" 12 "Andrew" "Cooper" 12
@ -351,8 +291,8 @@ a set of column names that should get the value `DEFAULT` instead of `NULL`:
{:name "Jane" :surname "Daniels"}]) {:name "Jane" :surname "Daniels"}])
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL) (name, surname, age) VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL)
" "
"John" "Smith" 34 "John" "Smith" 34
"Andrew" 12 "Andrew" 12
@ -363,8 +303,8 @@ VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL)
{:name "Jane" :surname "Daniels"}]) {:name "Jane" :surname "Daniels"}])
(sql/format {:pretty true :values-default-columns #{:age}})) (sql/format {:pretty true :values-default-columns #{:age}}))
=> [" => ["
INSERT INTO properties (name, surname, age) INSERT INTO properties
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, DEFAULT) (name, surname, age) VALUES (?, ?, ?), (?, NULL, ?), (?, ?, DEFAULT)
" "
"John" "Smith" 34 "John" "Smith" 34
"Andrew" 12 "Andrew" 12
@ -386,8 +326,8 @@ The column values do not have to be literals, they can be nested queries:
(sql/format {:pretty true}))) (sql/format {:pretty true})))
=> [" => ["
INSERT INTO user_profile_to_role (user_profile_id, role_id) INSERT INTO user_profile_to_role
VALUES (?, (SELECT id FROM role WHERE name = ?)) (user_profile_id, role_id) VALUES (?, (SELECT id FROM role WHERE name = ?))
" "
12345 12345
"user"] "user"]
@ -401,8 +341,8 @@ VALUES (?, (SELECT id FROM role WHERE name = ?))
:where [:= :name "user"]}}]} :where [:= :name "user"]}}]}
(sql/format {:pretty true}))) (sql/format {:pretty true})))
=> [" => ["
INSERT INTO user_profile_to_role (user_profile_id, role_id) INSERT INTO user_profile_to_role
VALUES (?, (SELECT id FROM role WHERE name = ?)) (user_profile_id, role_id) VALUES (?, (SELECT id FROM role WHERE name = ?))
" "
12345 12345
"user"] "user"]
@ -443,22 +383,11 @@ Composite types are supported:
["large" (composite 10 "feet")]]) ["large" (composite 10 "feet")]])
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO comp_table (name, comp_column) INSERT INTO comp_table
(name, comp_column)
VALUES (?, (?, ?)), (?, (?, ?)) VALUES (?, (?, ?)), (?, (?, ?))
" "
"small" 1 "inch" "large" 10 "feet"] "small" 1 "inch" "large" 10 "feet"]
;; with numbered parameters:
(-> (insert-into :comp_table)
(columns :name :comp_column)
(values
[["small" (composite 1 "inch")]
["large" (composite 10 "feet")]])
(sql/format {:pretty true :numbered true}))
=> ["
INSERT INTO comp_table (name, comp_column)
VALUES ($1, ($2, $3)), ($4, ($5, $6))
"
"small" 1 "inch" "large" 10 "feet"]
;; or as pure data DSL: ;; or as pure data DSL:
(-> {:insert-into [:comp_table], (-> {:insert-into [:comp_table],
:columns [:name :comp_column], :columns [:name :comp_column],
@ -466,7 +395,8 @@ VALUES ($1, ($2, $3)), ($4, ($5, $6))
["large" [:composite 10 "feet"]]]} ["large" [:composite 10 "feet"]]]}
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO comp_table (name, comp_column) INSERT INTO comp_table
(name, comp_column)
VALUES (?, (?, ?)), (?, (?, ?)) VALUES (?, (?, ?)), (?, (?, ?))
" "
"small" 1 "inch" "large" 10 "feet"] "small" 1 "inch" "large" 10 "feet"]
@ -563,11 +493,11 @@ If you want to delete everything from a table, you can use `truncate`:
```clojure ```clojure
(-> (truncate :films) (-> (truncate :films)
(sql/format)) (sql/format))
=> ["TRUNCATE TABLE films"] => ["TRUNCATE films"]
;; or as pure data DSL: ;; or as pure data DSL:
(-> {:truncate :films} (-> {:truncate :films}
(sql/format)) (sql/format))
=> ["TRUNCATE TABLE films"] => ["TRUNCATE films"]
``` ```
### Set operations ### Set operations
@ -588,8 +518,6 @@ There are also helpers for each of those:
=> ["SELECT * FROM foo UNION SELECT * FROM bar"] => ["SELECT * FROM foo UNION SELECT * FROM bar"]
``` ```
> Note: different databases have different precedence rules for these set operations when used in combination -- you may need to use `:nest` to add `(` .. `)` in order to combine these operations in a single SQL statement, if the natural order produced by HoneySQL does not work "as expected" for your database.
### Functions ### Functions
Function calls (and expressions with operators) can be specified as Function calls (and expressions with operators) can be specified as
@ -612,11 +540,6 @@ keywords that begin with `%` are interpreted as SQL function calls:
=> ["SELECT COUNT(*) FROM foo"] => ["SELECT COUNT(*) FROM foo"]
``` ```
```clojure ```clojure
;; with an alias:
(-> (select [:%count.* :total]) (from :foo) sql/format)
=> ["SELECT COUNT(*) AS total FROM foo"]
```
```clojure
(-> (select :%max.id) (from :foo) sql/format) (-> (select :%max.id) (from :foo) sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
``` ```
@ -630,10 +553,6 @@ regular function calls in a select:
=> ["SELECT COUNT(*) FROM foo"] => ["SELECT COUNT(*) FROM foo"]
``` ```
```clojure ```clojure
(-> (select [[:count :*] :total]) (from :foo) sql/format)
=> ["SELECT COUNT(*) AS total FROM foo"]
```
```clojure
(-> (select [:%count.*]) (from :foo) sql/format) (-> (select [:%count.*]) (from :foo) sql/format)
=> ["SELECT COUNT(*) FROM foo"] => ["SELECT COUNT(*) FROM foo"]
;; or even: ;; or even:
@ -643,40 +562,20 @@ regular function calls in a select:
```clojure ```clojure
(-> (select [[:max :id]]) (from :foo) sql/format) (-> (select [[:max :id]]) (from :foo) sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
(-> (select [[:max :id] :highest]) (from :foo) sql/format)
=> ["SELECT MAX(id) AS highest FROM foo"]
;; the pure data DSL requires an extra level of brackets: ;; the pure data DSL requires an extra level of brackets:
(-> {:select [[[:max :id]]], :from [:foo]} sql/format) (-> {:select [[[:max :id]]], :from [:foo]} sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
(-> {:select [[[:max :id] :highest]], :from [:foo]} sql/format)
=> ["SELECT MAX(id) AS highest FROM foo"]
;; the shorthand makes this simpler: ;; the shorthand makes this simpler:
(-> {:select [[:%max.id]], :from [:foo]} sql/format) (-> {:select [[:%max.id]], :from [:foo]} sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
(-> {:select [[:%max.id :highest]], :from [:foo]} sql/format) ;; or even:
=> ["SELECT MAX(id) AS highest FROM foo"]
;; or even (no alias):
(-> {:select [:%max.id], :from [:foo]} sql/format) (-> {:select [:%max.id], :from [:foo]} sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
;; or even (no alias, no other columns): ;; or even:
(-> {:select :%max.id, :from :foo} sql/format) (-> {:select :%max.id, :from :foo} sql/format)
=> ["SELECT MAX(id) FROM foo"] => ["SELECT MAX(id) FROM foo"]
``` ```
Custom columns using functions are built with the same vector format.
Be sure to properly nest the vectors so that the first element in the selection
is the custom function and the second is the column alias.
```clojure
(sql/format
{:select [:job_name ;; A bare field selection
[[:avg [:/ [:- :end_time :start_time] 1000.0]] ;; A custom function
:avg_exec_time_seconds ;; The column alias
]]
:from [:job_data]
:group-by :job_name})
=> ["SELECT job_name, AVG((end_time - start_time) / ?) AS avg_exec_time_seconds FROM job_data GROUP BY job_name" 1000.0]
```
If a keyword begins with `'`, the function name is formatted as a SQL If a keyword begins with `'`, the function name is formatted as a SQL
entity rather than being converted to uppercase and having hyphens `-` entity rather than being converted to uppercase and having hyphens `-`
converted to spaces). That means that hyphens `-` will become underscores `_` converted to spaces). That means that hyphens `-` will become underscores `_`
@ -697,8 +596,6 @@ unless you have quoting enabled:
=> ["SELECT * FROM `foo` WHERE `my-schema`.`SomeFunction`(`bar`, ?)" 0] => ["SELECT * FROM `foo` WHERE `my-schema`.`SomeFunction`(`bar`, ?)" 0]
``` ```
> Note: in non-function contexts, if a keyword begins with `'`, it is transcribed into the SQL exactly as-is, with no case or character conversion at all.
### Bindable parameters ### Bindable parameters
Keywords that begin with `?` are interpreted as bindable parameters: Keywords that begin with `?` are interpreted as bindable parameters:
@ -709,12 +606,6 @@ Keywords that begin with `?` are interpreted as bindable parameters:
(where [:= :a :?baz]) (where [:= :a :?baz])
(sql/format {:params {:baz "BAZ"}})) (sql/format {:params {:baz "BAZ"}}))
=> ["SELECT id FROM foo WHERE a = ?" "BAZ"] => ["SELECT id FROM foo WHERE a = ?" "BAZ"]
;; or with numbered parameters:
(-> (select :id)
(from :foo)
(where [:= :a :?baz])
(sql/format {:params {:baz "BAZ"} :numbered true}))
=> ["SELECT id FROM foo WHERE a = $1" "BAZ"]
;; or as pure data DSL: ;; or as pure data DSL:
(-> {:select [:id], :from [:foo], :where [:= :a :?baz]} (-> {:select [:id], :from [:foo], :where [:= :a :?baz]}
(sql/format {:params {:baz "BAZ"}})) (sql/format {:params {:baz "BAZ"}}))
@ -808,8 +699,8 @@ have a lot of function calls needed in code:
[:cast 4325 :integer]]}]) [:cast 4325 :integer]]}])
(sql/format {:pretty true})) (sql/format {:pretty true}))
=> [" => ["
INSERT INTO sample (location) INSERT INTO sample
VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS INTEGER))) (location) VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS integer)))
" "
0.291 32.621 4325] 0.291 32.621 4325]
``` ```
@ -821,7 +712,7 @@ be quoted according to the selected dialect. If you override the dialect in a
`format` call, by passing the `:dialect` option, SQL entity names will be automatically `format` call, by passing the `:dialect` option, SQL entity names will be automatically
quoted. You can override the dialect and turn off quoting by passing `:quoted false`. quoted. You can override the dialect and turn off quoting by passing `:quoted false`.
Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL), Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL),
`:mysql`, `:oracle`, or `:sqlserver`. As of 2.5.1091, `:nrql` is also supported: `:mysql`, `:oracle`, or `:sqlserver`:
```clojure ```clojure
(-> (select :foo.a) (-> (select :foo.a)
@ -830,15 +721,6 @@ Valid `:dialect` options are `:ansi` (the default, use this for PostgreSQL),
(sql/format {:dialect :mysql})) (sql/format {:dialect :mysql}))
=> ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"] => ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"]
``` ```
```clojure
(-> (select :foo.a)
(from :foo)
(where [:= :foo.a "baz"])
(sql/format {:dialect :nrql}))
=> ["SELECT `foo.a` FROM foo WHERE `foo.a` = 'baz'"]
```
See [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect.
#### Locking #### Locking
@ -948,24 +830,6 @@ LIMIT ?
OFFSET ? OFFSET ?
" "
"bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
;; with numbered parameters:
(sql/format big-complicated-map
{:params {:param1 "gabba" :param2 2}
:pretty true :numbered true})
=> ["
SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10
FROM foo AS f, baz AS b
INNER JOIN draq ON f.b = draq.x INNER JOIN eldr ON f.e = eldr.t
LEFT JOIN clod AS c ON f.a = c.d
RIGHT JOIN bock ON bock.z = c.e
WHERE ((f.a = $1) AND (b.baz <> $2)) OR (($3 < $4) AND ($5 < $6)) OR (f.e IN ($7, $8, $9)) OR f.e BETWEEN $10 AND $11
GROUP BY f.a, c.e
HAVING $12 < f.e
ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST
LIMIT $13
OFFSET $14
"
"bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
``` ```
```clojure ```clojure
;; Printable and readable ;; Printable and readable
@ -983,9 +847,11 @@ If your database supports `<=>` as an operator, you can tell HoneySQL about it u
```clojure ```clojure
(sql/register-op! :<=>) (sql/register-op! :<=>)
;; all operators are assumed to be variadic: ;; default is a binary operator:
(-> (select :a) (where [:<=> :a "foo"]) sql/format) (-> (select :a) (where [:<=> :a "foo"]) sql/format)
=> ["SELECT a WHERE a <=> ?" "foo"] => ["SELECT a WHERE a <=> ?" "foo"]
;; you can declare that an operator is variadic:
(sql/register-op! :<=> :variadic true)
(-> (select :a) (where [:<=> "food" :a "fool"]) sql/format) (-> (select :a) (where [:<=> "food" :a "fool"]) sql/format)
=> ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"] => ["SELECT a WHERE ? <=> a <=> ?" "food" "fool"]
``` ```
@ -1014,13 +880,8 @@ Or perhaps your database supports syntax like `a BETWIXT b AND c`, in which case
;; example usage: ;; example usage:
(-> (select :a) (where [:betwixt :a 1 10]) sql/format) (-> (select :a) (where [:betwixt :a 1 10]) sql/format)
=> ["SELECT a WHERE a BETWIXT ? AND ?" 1 10] => ["SELECT a WHERE a BETWIXT ? AND ?" 1 10]
;; with numbered parameters:
(-> (select :a) (where [:betwixt :a 1 10]) (sql/format {:numbered true}))
=> ["SELECT a WHERE a BETWIXT $1 AND $2" 1 10]
``` ```
> Note: the generation of positional placeholders (`?`) or numbered placeholders (`$1`, `$2`, etc) is handled automatically by `format-expr` so you get this behavior "for free" in your extensions, as long as you use the public API for `honey.sql`. You should avoid writing extensions that generate placeholders directly if you want them to work with numbered parameters.
You can also register SQL clauses, specifying the keyword, the formatting function, and an existing clause that this new clause should be processed before: You can also register SQL clauses, specifying the keyword, the formatting function, and an existing clause that this new clause should be processed before:
```clojure ```clojure
@ -1044,15 +905,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-2021 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$"]}}}}

118
build.clj
View file

@ -7,119 +7,75 @@
Run tests: Run tests:
clojure -X:test clojure -X:test
clojure -X:test:1.12 clojure -X:test:master
For more information, run: For more information, run:
clojure -A:deps -T:build help/doc" clojure -A:deps -T:build help/doc"
(:refer-clojure :exclude [test]) (:refer-clojure :exclude [test])
(:require [clojure.string :as str] (:require [clojure.tools.build.api :as b]
[clojure.tools.build.api :as b] [org.corfield.build :as bb]))
[clojure.tools.deps :as t]
[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.3.%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 "999-SNAPSHOT"))
(def class-dir "target/classes")
(defn- run-task [aliases]
(println "\nRunning task for" (str/join "," (map name aliases)))
(let [basis (b/create-basis {:aliases aliases})
combined (t/combine-aliases basis aliases)
cmds (b/java-command
{:basis basis
:main 'clojure.main
:main-args (:main-opts combined)})
{:keys [exit]} (b/process cmds)]
(when-not (zero? exit) (throw (ex-info "Task failed" {})))))
(defn eastwood "Run Eastwood." [opts] (defn eastwood "Run Eastwood." [opts]
(run-task [:eastwood]) (-> opts (bb/run-task [:eastwood])))
opts)
(defn gen-doc-tests "Generate tests from doc code blocks." [opts] (defn gen-doc-tests "Generate tests from doc code blocks." [opts]
(run-task [:gen-doc-tests]) (-> opts (bb/run-task [:gen-doc-tests])))
opts)
(defn run-doc-tests (defn run-doc-tests
"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 [:master] -- test against Clojure 1.12 master snapshot
[:cljs] -- test against ClojureScript" [:cljs] -- test against ClojureScript"
[{:keys [aliases] :as opts}] [{:keys [aliases] :as opts}]
(gen-doc-tests opts) (gen-doc-tests opts)
(run-task (-> [:test :runner :test-doc] (bb/run-tests (assoc opts :aliases
(-> [:test-doc]
(into aliases) (into aliases)
(into (if (some #{:cljs} aliases) (into (if (some #{:cljs} aliases)
[:test-doc-cljs] [:test-doc-cljs]
[:test-doc-clj])))) [:test-doc-clj])))))
opts) opts)
(defn test "Run basic tests." [opts] (defn test "Run basic tests." [opts]
(run-task [:test :runner :1.11]) (-> opts
(run-task [:test :runner :cljs]) (update :aliases (fnil conj []) :1.11)
opts) (bb/run-tests)))
(defn- pom-template [version]
[[:description "SQL as Clojure data structures."]
[:url "https://github.com/seancorfield/honeysql"]
[:licenses
[:license
[:name "Eclipse Public License"]
[:url "http://www.eclipse.org/legal/epl-v10.html"]]]
[:developers
[:developer
[:name "Sean Corfield"]]
[:developer
[:name "Justin Kramer"]]]
[:scm
[:url "https://github.com/seancorfield/honeysql"]
[:connection "scm:git:https://github.com/seancorfield/honeysql.git"]
[:developerConnection "scm:git:ssh:git@github.com:seancorfield/honeysql.git"]
[:tag (str "v" version)]]])
(defn- jar-opts [opts]
(let [version (if (:snapshot opts) snapshot version)]
(println "\nVersion:" version)
(assoc opts
:lib lib :version version
:jar-file (format "target/%s-%s.jar" lib version)
:basis (b/create-basis {})
:class-dir class-dir
:target "target"
:src-dirs ["src"]
:pom-data (pom-template version))))
(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] (-> opts
opts (jar-opts opts)] (bb/clean)
(b/delete {:path "target"}) (assoc :lib lib :version (if (:snapshot opts) snapshot version))
(doseq [alias aliases] (as-> opts
(run-doc-tests {:aliases [alias]})) (reduce (fn [opts alias]
(eastwood opts) (run-doc-tests (assoc opts :aliases [alias])))
(doseq [alias aliases] opts
(run-task [:test :runner alias])) [:cljs :elide :1.10 :1.11 :master]))
(b/delete {:path "target"}) (eastwood)
(println "\nWriting pom.xml...") (as-> opts
(b/write-pom opts) (reduce (fn [opts alias]
(println "\nCopying source...") (bb/run-tests (assoc opts :aliases [alias])))
(b/copy-dir {:src-dirs ["src"] :target-dir class-dir}) opts
(println "\nBuilding" (:jar-file opts) "...") [:cljs :elide :1.10 :1.11 :master]))
(b/jar opts)) (bb/clean)
opts) (assoc :src-pom "template/pom.xml")
(bb/jar)))
(defn deploy "Deploy the JAR to Clojars." [opts] (defn deploy "Deploy the JAR to Clojars." [opts]
(let [{:keys [jar-file] :as opts} (jar-opts opts)] (-> opts
(dd/deploy {:installer :remote :artifact (b/resolve-path jar-file) (assoc :lib lib :version (if (:snapshot opts) snapshot version))
:pom-file (b/pom-path (select-keys opts [:lib :class-dir]))})) (bb/deploy)))
opts)

View file

@ -7,17 +7,14 @@
success-marker (fs/file target "SUCCESS") success-marker (fs/file target "SUCCESS")
docs ["README.md" docs ["README.md"
"doc/clause-reference.md" "doc/clause-reference.md"
"doc/databases.md"
"doc/differences-from-1-x.md" "doc/differences-from-1-x.md"
"doc/extending-honeysql.md" "doc/extending-honeysql.md"
"doc/general-reference.md" "doc/general-reference.md"
"doc/getting-started.md" "doc/getting-started.md"
"doc/nrql.md"
;;"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.seancorfield/build-clj
slipset/deps-deploy {:mvn/version "0.2.2"}} {:git/tag "v0.8.0" :git/sha "9bd8b8a"}}
: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"}}} :master {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-master-SNAPSHOT"}}}
: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]"]}
@ -19,19 +20,17 @@
:test :test
{:extra-paths ["test"] {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner :extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"} {:git/tag "v0.5.0" :git/sha "48c3c67"}
org.clojure/core.cache {:mvn/version "RELEASE"}} org.clojure/core.cache {:mvn/version "RELEASE"}}
:exec-fn cognitect.test-runner.api/test} :exec-fn cognitect.test-runner.api/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.1.2"}
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 +40,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.0.0"}}
:main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}} :main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}}}

View file

@ -10,8 +10,7 @@ Clauses can be specified as keywords or symbols. Use
a space (e.g., `:left-join` is formatted as `LEFT JOIN`). a space (e.g., `:left-join` is formatted as `LEFT JOIN`).
Except as noted, these clauses apply to all the SQL Except as noted, these clauses apply to all the SQL
dialects that HoneySQL supports. See also the section on dialects that HoneySQL supports.
[database-specific hints and tips](databases.md).
DDL clauses are listed first, followed by SQL clauses. DDL clauses are listed first, followed by SQL clauses.
@ -23,11 +22,6 @@ The examples herein assume:
where order-by over partition-by window]]) where order-by over partition-by window]])
``` ```
Every DDL and SQL clause has a corresponding helper function
in `honey.sql.helpers`. In general, `(helper :foo expr)` will
produce `{:helper [:foo expr]}` (with a few exceptions -- see
the docstring of the helper function for details).
# DDL Clauses # DDL Clauses
HoneySQL supports the following DDL clauses as a data DSL. HoneySQL supports the following DDL clauses as a data DSL.
@ -91,14 +85,13 @@ user=> (sql/format {:alter-table [:fruit
As can be seen above, `:add-column` and `:alter-column` As can be seen above, `:add-column` and `:alter-column`
both accept a column description (as a sequence of simple both accept a column description (as a sequence of simple
expressions); `:drop-column` accepts one or more column names expressions); `:drop-column` accepts a single column name,
optionally prefixed by `:if-exists`,
and `:rename-column` accepts a sequence with two column and `:rename-column` accepts a sequence with two column
names: the "from" and the "to" names. names: the "from" and the "to" names.
> Note: `:modify-column` is MySQL-specific and should be considered legacy and deprecated. `:alter-column` will produce `MODIFY COLUMN` when the MySQL dialect is selected. > Note: `:modify-column` is MySQL-specific and should be considered legacy and deprecated. `:alter-column` will produce `MODIFY COLUMN` when the MySQL dialect is selected.
### add-index, drop-index, create-index ### add-index, drop-index
Used with `:alter-table`, Used with `:alter-table`,
`:add-index` accepts a single (function) expression `:add-index` accepts a single (function) expression
@ -125,38 +118,6 @@ user=> (-> (h/alter-table :fruit)
["ALTER TABLE fruit ADD PRIMARY KEY(id)"] ["ALTER TABLE fruit ADD PRIMARY KEY(id)"]
``` ```
Some databases treat the standalone `:create-index` differently (e.g. PostgreSQL) while some treat it as an alias to `:alter-table` `:add-index` (e.g. MySQL). It accepts a pair of index specification and column specification:
```clojure
user=> (sql/format {:create-index [:my-idx [:fruit :appearance]]})
["CREATE INDEX my_idx ON fruit (appearance)"]
user=> (sql/format {:create-index [[:unique :another-idx] [:fruit :color :appearance]]})
["CREATE UNIQUE INDEX another_idx ON fruit (color, appearance)"]
```
PostgreSQL supports IF NOT EXISTS and expressions instead of columns. This may make `:create-index` more useful than `:add-index`:
```clojure
user=> (sql/format (h/create-index [:unique :another-idx :if-not-exists] [: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`,
@ -171,9 +132,10 @@ user=> (sql/format {:alter-table :fruit :rename-table :vegetable})
## create-table, with-columns ## create-table, with-columns
`:create-table` can accept a single table name or a sequence `:create-table` can accept a single table name or a pair
containing a table name and a flag indicating the creation containing a table name and a flag indicating the creation
should be conditional (`:if-not-exists` or the symbol `if-not-exists`). `:create-table` should should be conditional (`:if-not-exists` or the symbol `if-not-exists`,
although any truthy value will work). `:create-table` should
be used with `:with-columns` to specify the actual columns be used with `:with-columns` to specify the actual columns
in the table: in the table:
@ -186,83 +148,23 @@ user=> (sql/format {:create-table :fruit
["CREATE TABLE fruit (id INT NOT NULL, name VARCHAR(32) NOT NULL, cost FLOAT NULL)"] ["CREATE TABLE fruit (id INT NOT NULL, name VARCHAR(32) NOT NULL, cost FLOAT NULL)"]
``` ```
Any keywords (or symbols) preceding the table name will be
turned into SQL keywords (this is true for all of the `create-*`
DSL identifiers):
```clojure
user=> (sql/format {:create-table [:my :fancy :fruit :if-not-exists]
:with-columns
[[:id :int [:not nil]]
[:name [:varchar 32] [:not nil]]
[:cost :float :null]]})
["CREATE MY FANCY TABLE IF NOT EXISTS fruit (id INT NOT NULL, name VARCHAR(32) NOT NULL, cost FLOAT NULL)"]
```
This lets you write SQL like `CREATE TEMP TABLE foo ...` etc.
The `:with-columns` clause is formatted as if `{:inline true}` The `:with-columns` clause is formatted as if `{:inline true}`
was specified so nothing is parameterized. In addition, was specified so nothing is parameterized. In addition,
everything except the first element of a column description everything except the first element of a column description
will be uppercased (mostly to give the appearance of separating will be uppercased (mostly to give the appearance of separating
the column name from the SQL keywords) -- except for keywords the column name from the SQL keywords).
that start with `'` which will be transcribed into the SQL exactly
as-is, with no case or character conversion at all. This
"escape hatch" is intended to allow for SQL dialects that are
case sensitive and/or have other unusual syntax constraints.
Various function-like expressions can be specified, as shown Various function-like expressions can be specified, as shown
in the example above, that allow things like `CHECK` for a in the example above, that allow things like `CHECK` for a
constraint, `FOREIGN KEY` (with a column name), `REFERENCES` constraint, `FOREIGN KEY` (with a column name), `REFERENCES`
(with a pair of column names). See [Column Descriptors in Special Syntax](special-syntax.md#column-descriptors) for more details. (with a pair of column names). See [Column Descriptors in Special Syntax](special-syntax.md#column-descriptors) for more details.
For example:
```clojure
user=> (-> {:create-table :foo
:with-columns
[[:a :int]
[:b :int]
[[:primary-key :a :b]]]}
(sql/format))
["CREATE TABLE foo (a INT, b INT, PRIMARY KEY(a, b))"]
```
or:
```clojure
user=> (-> {:create-table [:bar]
:with-columns
[[:a :integer]
[:b :integer]
[[:constraint :foo_natural_key] :unique [:composite :a :b]]]}
(sql/format))
["CREATE TABLE bar (a INTEGER, b INTEGER, CONSTRAINT foo_natural_key UNIQUE (a, b))"]
```
or a mix of column constraints and table constraints:
```clojure
user=> (-> '{create-table quux
with-columns
((a integer (constraint a_pos) (check (> a 0)))
(b integer)
((constraint a_bigger) (check (< b a))))}
(sql/format {:pretty true}))
["
CREATE TABLE quux
(a INTEGER CONSTRAINT a_pos CHECK(a > 0), b INTEGER, CONSTRAINT a_bigger CHECK(b < a))
"]
```
## create-table-as ## create-table-as
`:create-table-as` can accept a single table name or a sequence `:create-table-as` can accept a single table name or a sequence
that starts with a table name, optionally followed by that starts with a table name, optionally followed by
a flag indicating the creation should be conditional a flag indicating the creation should be conditional
(`:if-not-exists` or the symbol `if-not-exists` or, (`:if-not-exists` or the symbol `if-not-exists`),
for BigQuery `:or-replace` or the symbol `or-replace`),
optionally followed by a `{:columns ..}` clause to specify optionally followed by a `{:columns ..}` clause to specify
the columns to use in the created table, optionally followed the columns to use in the created table, optionally followed
by special syntax to specify `TABLESPACE` etc. by special syntax to specify `TABLESPACE` etc.
@ -294,7 +196,7 @@ A more concise version of the above can use the `TABLE` clause:
```clojure ```clojure
user=> (sql/format {:create-table-as [:metro :or-replace user=> (sql/format {:create-table-as [:metro :if-not-exists
{:columns [:foo :bar :baz]} {:columns [:foo :bar :baz]}
[:tablespace [:entity :quux]]], [:tablespace [:entity :quux]]],
:table :cities, :table :cities,
@ -302,33 +204,17 @@ user=> (sql/format {:create-table-as [:metro :or-replace
:with-data false} :with-data false}
{:pretty true}) {:pretty true})
[" ["
CREATE OR REPLACE TABLE metro (foo, bar, baz) TABLESPACE quux AS CREATE TABLE IF NOT EXISTS metro (foo, bar, baz) TABLESPACE quux AS
TABLE cities TABLE cities
WHERE metroflag = ? WHERE metroflag = ?
WITH NO DATA WITH NO DATA
" "y"] " "y"]
``` ```
As above, any keywords (or symbols) preceding the table name
will be turned into SQL keywords (this is true for all of the
`create-*` DSL identifiers) so you can write:
```
{:create-table-as [:temp :metro :if-not-exists [..]] ..}
```
to produce `CREATE TEMP TABLE IF NOT EXISTS metro ..`, or:
```
{:create-table-as [:temp :metro :or-replace [..]] ..}
```
to produce `CREATE OR REPLACE TEMP TABLE metro ..`.
## create-extension ## create-extension
`:create-extension` can accept a single extension name or a `:create-extension` can accept a single extension name or a pair
sequence of the extension name, followed by of the extension name, followed by
a flag indicating the creation should be conditional a flag indicating the creation should be conditional
(`:if-not-exists` or the symbol `if-not-exists`). (`:if-not-exists` or the symbol `if-not-exists`).
See the [PostgreSQL](postgresql.md) section for examples. See the [PostgreSQL](postgresql.md) section for examples.
@ -357,18 +243,6 @@ user=> (sql/format {:refresh-materialized-view [:concurrently :products]
["REFRESH MATERIALIZED VIEW CONCURRENTLY products WITH NO DATA"] ["REFRESH MATERIALIZED VIEW CONCURRENTLY products WITH NO DATA"]
``` ```
PostgreSQL does not support `IF NOT EXISTS` on `CREATE VIEW` (it supports it on
`CREATE MATERIALIZED VIEW`!) so, as of 2.4.1066, HoneySQL also has
`:create-or-replace-view` for this case:
```clojure
user=> (sql/format {:create-or-replace-view [:products]
:select [:*]
:from [:items]
:where [:= :category "product"]})
["CREATE OR REPLACE VIEW products AS SELECT * FROM items WHERE category = ?" "product"]
```
## drop-table, drop-extension, drop-view, drop-materialized-view ## drop-table, drop-extension, drop-view, drop-materialized-view
`:drop-table` et al can accept a single table (extension, view) name or a sequence of `:drop-table` et al can accept a single table (extension, view) name or a sequence of
@ -416,11 +290,9 @@ order they would appear in a valid SQL statement).
## with, with-recursive ## with, with-recursive
These provide CTE support for several databases. These provide CTE support for SQL Server. The argument to
In the most common form, the argument to
`:with` (or `:with-recursive`) is a sequences of pairs, each of `:with` (or `:with-recursive`) is a sequences of pairs, each of
a result set name (or description) and either of; a basic SQL a result set name (or description) and a basic SQL statement.
statement, a string, a keyword or a symbol.
The result set can either be a SQL entity (a simple name) The result set can either be a SQL entity (a simple name)
or a pair of a SQL entity and a set of column names. or a pair of a SQL entity and a set of column names.
@ -433,34 +305,6 @@ user=> (sql/format '{with ((stuff {select (:*) from (foo)}),
["WITH stuff AS (SELECT * FROM foo), nonsense AS (SELECT * FROM bar) SELECT foo.id, bar.name FROM stuff, nonsense WHERE status = ?" 0] ["WITH stuff AS (SELECT * FROM foo), nonsense AS (SELECT * FROM bar) SELECT foo.id, bar.name FROM stuff, nonsense WHERE status = ?" 0]
``` ```
When the expression is a basic SQL statement in any of the pairs,
the resulting syntax of the pair is `WITH ident AS expr` as shown above.
However, when the expression is a string, a keyword or a symbol, the resulting
syntax of the pair is of the form `WITH expr AS ident` like this:
```clojure
user=> (sql/format '{with ((ts_upper_bound "2019-08-01 15:23:00"))
select :*
from (hits)
where (= EventDate ts_upper_bound)})
["WITH ? AS ts_upper_bound SELECT * FROM hits WHERE EventDate = ts_upper_bound" "2019-08-01 15:23:00"]
```
The syntax only varies for each pair and so you can use both SQL statements
and keywords/strings/symbols in the same `WITH` clause like this:
```clojure
user=> (sql/format '{with ((ts_upper_bound "2019-08-01 15:23:00")
(review :awesome)
(stuff {select (:*) from (songs)}))
select :*
from (hits, stuff)
where (and (= EventDate ts_upper_bound)
(= EventReview review))})
["WITH ? AS ts_upper_bound, awesome AS review, stuff AS (SELECT * FROM songs) SELECT * FROM hits, stuff WHERE (EventDate = ts_upper_bound) AND (EventReview = review)"
"2019-08-01 15:23:00"]
```
You can specify a list of columns for the CTE like this: You can specify a list of columns for the CTE like this:
```clojure ```clojure
@ -482,39 +326,6 @@ user=> (sql/format {:with [[[:stuff {:columns [:id :name]}]
["WITH stuff (id, name) AS (VALUES (?, ?), (?, ?)) SELECT id, name FROM stuff" 1 "Sean" 2 "Jay"] ["WITH stuff (id, name) AS (VALUES (?, ?), (?, ?)) SELECT id, name FROM stuff" 1 "Sean" 2 "Jay"]
``` ```
> 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
@ -529,15 +340,6 @@ user=> (sql/format '{union [{select (id,status) from (table-a)}
["SELECT id, status FROM table_a UNION SELECT id, event AS status, from, table_b"] ["SELECT id, status FROM table_a UNION SELECT id, event AS status, from, table_b"]
``` ```
> Note: different databases have different precedence rules for these set operations when used in combination -- you may need to use `:nest` to add `(` .. `)` in order to combine these operations in a single SQL statement, if the natural order produced by HoneySQL does not work "as expected" for your database.
```clojure
;; BigQuery requires UNION clauses be parenthesized:
user=> (sql/format '{union [{:nest {select (id,status) from (table-a)}}
{:nest {select (id,(event status) from (table-b))}}]})
["(SELECT id, status FROM table_a) UNION (SELECT id, event AS status, from, table_b)"]
```
## select, select-distinct, table ## select, select-distinct, table
`:select` and `:select-distinct` expect a sequence of SQL entities (column names `:select` and `:select-distinct` expect a sequence of SQL entities (column names
@ -567,40 +369,8 @@ Here, `:select` has a three expressions as its argument. The first is
a simple column name. The second is an expression and its alias. The a simple column name. The second is an expression and its alias. The
third is a simple column name and its alias. third is a simple column name and its alias.
An alias can be a simple name (a keyword or a symbol) or a string. An alias
containing a dot (`.`) is treated as a single name for quoting purposes.
Otherwise, a simple name will be formatted using table and column name rules
(including `-` to `_` translation). An alias specified as a string will not get
the `-` to `_` translation. There may be other contexts where you need to
refer to an alias but don't want the table/column rules applied to it, e.g.,
in an `:order-by` clause. 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 reference.
`:select-distinct` works the same way but produces `SELECT DISTINCT`. `:select-distinct` works the same way but produces `SELECT DISTINCT`.
As of 2.5.1091, you can use metadata on the argument to `:select` to
provide qualifiers for the `SELECT` clause:
```clojure
user=> (sql/format {:select ^:distinct [:id :name] :from :table})
["SELECT DISTINCT id, name FROM table"]
```
The metadata can also be a map, with `true` values ignored (which is why
`^:distinct` produces just `DISTINCT` even though it is short for
`^{:distinct true}`):
```clojure
user=> (sql/format {:select ^{:as :struct} [:id :name] :from :table})
["SELECT AS STRUCT id, name FROM table"]
```
As of 2.5.1103, HoneySQL ignores the following metadata: `:file`, `:line`,
`:column`, `:end-line`, and `:end-column` (2.5.1091 only ignored `:line`
and `:column`). You can ask HoneySQL to ignore other metadata by specifying
the `:ignored-metadata` option to `honey.sql/format`.
> Google BigQuery support: to provide `SELECT * EXCEPT ..` and `SELECT * REPLACE ..` syntax, HoneySQL supports a vector starting with `:*` or the symbol `*` followed by except columns and/or replace expressions as columns: > Google BigQuery support: to provide `SELECT * EXCEPT ..` and `SELECT * REPLACE ..` syntax, HoneySQL supports a vector starting with `:*` or the symbol `*` followed by except columns and/or replace expressions as columns:
```clojure ```clojure
@ -613,16 +383,7 @@ user=> (sql/format {:select [[:* :except [:a :b] :replace [[[:inline 2] :c]]]] :
``` ```
The `:table` clause is equivalent to `:select :* :from` and accepts just The `:table` clause is equivalent to `:select :* :from` and accepts just
a simple table name -- see `:create-table-as` above for an example. a simple table name -- `:create-table-as` above for an example.
Some databases support inheritance and you can `SELECT .. FROM ONLY ..` or
`.. JOIN ONLY ..` to restrict the query to just the specified table. You can
use function syntax for this `[:only table]` will produce `ONLY(table)`. This
is the ANSI SQL syntax (but PostgreSQL allows the parentheses to be omitted,
if you are writing SQL by hand).
Some databases support temporal queries -- see the `:for` clause section
of the `FROM` clause below.
## select-distinct-on ## select-distinct-on
@ -678,14 +439,13 @@ 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
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),
and then you can optionally specify the columns (via a `:columns` clause, and then you can optionally specify the columns (via a `:columns` clause).
or via a `:values` clause using hash maps).
The second case takes a pair of a table specifier (either a The second case takes a pair of a table specifier (either a
table name or table/alias pair) and a sequence of column table name or table/alias pair) and a sequence of column
@ -695,16 +455,7 @@ The third case takes a pair of either a table specifier
or a table/column specifier and a SQL query. or a table/column specifier and a SQL query.
For the first and second cases, you'll use the `:values` clause 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.
for more detail on the `:values` clause.
`:patch-into` is only supported by XTDB but is
part of HoneySQL's "core" dialect anyway. It produces a `PATCH INTO`
statement but otherwise has identical syntax to `:insert-into`.
`:replace-into` is only supported by MySQL and SQLite but is
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
statement but otherwise has identical syntax to `:insert-into`.
```clojure ```clojure
;; first case -- table specifier: ;; first case -- table specifier:
@ -745,33 +496,7 @@ user=> (sql/format '{insert-into (((transport t) (id, name)) {select (*) from (c
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"] ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]
``` ```
Some databases do not let you override (insert) values that would override > Note: if you specify `:columns` for an `:insert-into` that also includes column names, you will get invalid SQL. Similarly, if you specify `:columns` when `:values` is based on hash maps, you will get invalid SQL. Since clauses are generated independently, there is no cross-checking performed if you provide an illegal combination of clauses.
generated column values, unless your SQL specifies `OVERRIDING SYSTEM VALUE`
or `OVERRIDING USER VALUE`. As of 2.4.1066, you can use `:overriding-value` as
an option to `:insert-into` to specify this, with either `:system` or `:user`
as the option's value. The options can be specified as a hash map in the
first position of the `:insert-into` clause, prior to the table specifier.
```clojure
user=> (sql/format {:insert-into [{:overriding-value :system}
[:transport :t] [:id :name]]
:values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}
{:pretty true})
["
INSERT INTO transport AS t (id, name) OVERRIDING SYSTEM VALUE
VALUES (?, ?), (?, ?), (?, ?)
" 1 "Car" 2 "Boat" 3 "Bike"]
user=> (sql/format {:insert-into [{:overriding-value :user}
[:transport :t] [:id :name]]
:values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}
{:pretty true})
["
INSERT INTO transport AS t (id, name) OVERRIDING USER VALUE
VALUES (?, ?), (?, ?), (?, ?)
" 1 "Car" 2 "Boat" 3 "Bike"]
```
> Note: as of 2.4.1066, if you specify `:columns` for an `:insert-into` that also includes column names, or with a `:values` clause based on hash maps (which imply column names), then an order of precedence is applied: the columns specified directly in `:insert-into` take precedence, then the `:columns` clause, then the implied column names from the `:values` clause. Prior to 2.4.1066, you would get invalid SQL generated.
## update ## update
@ -785,28 +510,7 @@ user=> (sql/format {:update :transport
["UPDATE transport SET name = ? WHERE id = ?" "Yacht" 2] ["UPDATE transport SET name = ? WHERE id = ?" "Yacht" 2]
``` ```
You can also set columns to `NULL` or to their default values: ## delete, delete-from
```clojure
user=> (sql/format {:update :transport
:set {:owner nil, :date_built [:default]}
:where [:= :id 2]})
["UPDATE transport SET owner = NULL, date_built = DEFAULT WHERE id = ?"
2]
```
You can also `UPDATE .. FROM (VALUES ..) ..` where you might also need `:composite`:
```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 +527,13 @@ 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
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 transport"]
user=> (sql/format '{truncate (transport)})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport restart identity)})
["TRUNCATE TABLE transport RESTART IDENTITY"]
user=> (sql/format '{truncate ((transport))})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate ((transport other))})
["TRUNCATE TABLE transport, other"]
user=> (sql/format '{truncate ((transport other) restart identity)})
["TRUNCATE TABLE transport, other RESTART IDENTITY"]
``` ```
## columns ## columns
@ -874,9 +561,8 @@ user=> (sql/format {:update :order
`:from` accepts a single sequence argument that lists `:from` accepts a single sequence argument that lists
one or more SQL entities. Each entity can either be a one or more SQL entities. Each entity can either be a
simple table name (keyword or symbol) or a sequence of a simple table name (keyword or symbol) or a pair of a
table name, followed by an optional alias, followed by an table name and an alias:
optional temporal clause:
```clojure ```clojure
user=> (sql/format {:select [:username :name] user=> (sql/format {:select [:username :name]
@ -891,59 +577,8 @@ 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`,
followed by the time reference
(e.g., `:system-time` or `:business-time`), followed by a temporal qualifier,
one of:
* `:all`
* `:as-of timestamp`
* `:from timestamp1 :to timestamp2`
* `:between timestamp1 :and timestamp2`
```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]
user=> (sql/format {:select [:u.username]
:from [[:user :u :for :system-time :from [:inline "2019-08-01 15:23:00"] :to [:inline "2019-08-01 15:24:00"]]]
:where [:= :u.id 9]})
["SELECT u.username FROM user FOR SYSTEM_TIME FROM '2019-08-01 15:23:00' TO '2019-08-01 15:24:00' AS u WHERE u.id = ?" 9]
```
As of 2.6.1126, HoneySQL supports metadata on a table expression to provide
database-specific hints, such as SQL Server's `WITH (..)` clause:
```clojure
user=> (sql/format {:select [:col]
:from [^:nolock [:table]]
:where [:= :id 9]})
["SELECT col FROM table WITH (NOLOCK) WHERE id = ?" 9]
user=> (sql/format {:select [:col]
:from [^:nolock [:table :t]]
:where [:= :id 9]})
["SELECT col FROM table AS t WITH (NOLOCK) WHERE id = ?" 9]
```
Since you cannot put metadata on a keyword, the table name must be written as
a vector even when you have no alias.
> Note: the actual formatting of a `:from` clause is currently identical to the formatting of a `:select` clause. > Note: the actual formatting of a `:from` clause is currently identical to the formatting of a `:select` clause.
If you are using inheritance, you can specify `ONLY(table)` as a function
call: `[:only :table]`.
## using ## using
`:using` accepts a single sequence argument that lists `:using` accepts a single sequence argument that lists
@ -951,7 +586,7 @@ one or more SQL entities. Each entity can either be a
simple table name (keyword or symbol) or a pair of a simple table name (keyword or symbol) or a pair of a
table name and an alias. table name and an alias.
`:using` is intended to be used as a simple join, for example with a `:delete-from` `:using` is intended to be used as a simple join with a `:delete-from`
clause (see [PostgreSQL DELETE statement](https://www.postgresql.org/docs/12/sql-delete.html) clause (see [PostgreSQL DELETE statement](https://www.postgresql.org/docs/12/sql-delete.html)
for more detail). for more detail).
@ -1029,31 +664,6 @@ user=> (sql/format {:select [:t.ref :pp.code]
["SELECT t.ref, pp.code FROM transaction AS t LEFT JOIN paypal_tx AS pp USING (id) WHERE ? = pp.status" "settled"] ["SELECT t.ref, pp.code FROM transaction AS t LEFT JOIN paypal_tx AS pp USING (id) WHERE ? = pp.status" "settled"]
``` ```
As of 2.6.1126, HoneySQL supports metadata on a table expression to provide
database-specific hints, such as SQL Server's `WITH (..)` clause:
```clojure
user=> (sql/format {:select [:col]
:from [:table]
:join [^:nolock [:extra] [:= :table.extra_id :extra.id]]
:where [:= :id 9]})
["SELECT col FROM table INNER JOIN extra WITH (NOLOCK) ON table.extra_id = extra.id WHERE id = ?" 9]
user=> (sql/format {:select [:col]
:from [[:table :t]]
:join [^:nolock [:extra :x] [:= :t.extra_id :x.id]]
:where [:= :id 9]})
["SELECT col FROM table AS t INNER JOIN extra AS x WITH (NOLOCK) ON t.extra_id = x.id WHERE id = ?" 9]
```
Since you cannot put metadata on a keyword, the table name must be written as
a vector even when you have no alias.
If you are using inheritance, you can specify `ONLY(table)` as a function
call: `[:only :table]`.
See also the [`:join` special syntax](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-special-syntax-#join)
for nested `JOIN` expressions.
## cross-join ## cross-join
`:cross-join` accepts a single sequence argument that lists `:cross-join` accepts a single sequence argument that lists
@ -1088,9 +698,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 +719,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 +726,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 +752,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 +764,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`:
@ -1216,30 +787,14 @@ user=> (sql/format (-> (select :id
["SELECT id, AVG(salary) OVER () AS Average, MAX(salary) OVER () AS MaxSalary FROM employee"] ["SELECT id, AVG(salary) OVER () AS Average, MAX(salary) OVER () AS MaxSalary FROM employee"]
``` ```
## distinct, expr
Related to the windowing clauses above, `:distinct` and `:expr` are
intended to let you mix clauses with expressions, such as in BigQuery's
`ARRAY_AGG` function:
```clojure
user=> (sql/format {:select [[[:over
[[:array_agg {:distinct [:ignore-nulls :col] :order-by :x}]
{:partition-by :y}]]]]})
["SELECT ARRAY_AGG (DISTINCT col IGNORE NULLS ORDER BY x ASC) OVER (PARTITION BY y)"]
```
## 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:
@ -1270,11 +825,6 @@ user=> (sql/format {:select [:*] :from :table
["SELECT * FROM table ORDER BY CASE WHEN NOW() < expiry_date THEN created_date ELSE expiry_date END DESC"] ["SELECT * FROM table ORDER BY CASE WHEN NOW() < expiry_date THEN created_date ELSE expiry_date END DESC"]
``` ```
You can `ORDER BY` 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.
## limit, offset, fetch ## limit, offset, fetch
Some databases, including MySQL, support `:limit` and `:offset` Some databases, including MySQL, support `:limit` and `:offset`
@ -1358,59 +908,15 @@ same as the `:for` clause above.
row values or a sequence of sequences, also representing row row values or a sequence of sequences, also representing row
values. values.
### values with hash maps In the former case, all of the rows are augmented to have
If you provide a sequence of hash maps, the `:values` clause
will generate a `VALUES` clause, and will also generate the column names
as part of the `INSERT INTO` (or `REPLACE INTO`) statement.
If there is no `INSERT INTO` (or `REPLACE INTO`) statement in the context
of the `:values` clause, the column names will be generated as a part of
the `VALUES` clause itself.
```clojure
user=> (sql/format {:values [{:col-a 1 :col-b 2}]})
["(col_a, col_b) VALUES (?, ?)" 1 2]
```
In addition, all of the rows are augmented to have
either `NULL` or `DEFAULT` values for any missing keys (columns). either `NULL` or `DEFAULT` values for any missing keys (columns).
By default, `NULL` is used but you can specify a set of columns By default, `NULL` is used but you can specify a set of columns
to get `DEFAULT` values, via the `:values-default-columns` option. to get `DEFAULT` values, via the `:values-default-columns` option.
You can also be explicit and use `[:default]` as a value to generate `DEFAULT`. In the latter case -- a sequence of sequences --
all of the rows are padded to the same length by adding `nil`
### values with sequences
If you provide a sequence of sequences, the `:values` clause
will generate a `VALUES` clause with no column names and the
row values following.
```clojure
user=> (sql/format {:values [[1 2]]})
["VALUES (?, ?)" 1 2]
```
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
```clojure ```clojure
user=> (sql/format {:insert-into :table user=> (sql/format {:insert-into :table
:values [[1 2] [2 3 4 5] [3 4 5]]}) :values [[1 2] [2 3 4 5] [3 4 5]]})
@ -1430,32 +936,6 @@ user=> (sql/format '{insert-into table
> Note: the `:values-default-columns` option must match how the columns are specified, i.e., as symbols or keywords. > Note: the `:values-default-columns` option must match how the columns are specified, i.e., as symbols or keywords.
For databases that allow it, you can insert an entire row of default values,
if appropriate, using one of the following syntaxes:
```clojure
user=> (sql/format {:insert-into :table :values []})
["INSERT INTO table VALUES ()"]
user=> (sql/format {:insert-into :table :values :default})
["INSERT INTO table DEFAULT VALUES"]
```
Some databases support the empty `VALUES` clause, some support `DEFAULT VALUES`, some support neither. Consult your database's documentation to see which approach to use.
For databases that allow it, when specifying multiple rows you use `:default` in
place of a row to insert default values for that row:
```clojure
user=> (sql/format {:insert-into :table
:values [{:a 1 :b 2 :c 3}
:default
{:a 4 :b 5 :c 6}]})
["INSERT INTO table (a, b, c) VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]
user=> (sql/format {:insert-into :table
:values [[1 2 3] :default [4 5 6]]})
["INSERT INTO table VALUES (?, ?, ?), DEFAULT, (?, ?, ?)" 1 2 3 4 5 6]
```
## on-conflict, on-constraint, do-nothing, do-update-set ## on-conflict, on-constraint, do-nothing, do-update-set
These are grouped together because they are handled These are grouped together because they are handled
@ -1463,11 +943,10 @@ as if they are separate clauses but they will appear
in pairs: `ON ... DO ...`. in pairs: `ON ... DO ...`.
`:on-conflict` accepts a sequence of zero or more `:on-conflict` accepts a sequence of zero or more
SQL expressions, optionally SQL entities (keywords or symbols), optionally
followed by a single SQL clause (hash map). It can also followed by a single SQL clause (hash map). It can also
accept either a single SQL entity or a single SQL clause. accept either a single SQL entity or a single SQL clause.
The SQL expressions can be just column names or function calls etc, The SQL entities are column names and the SQL clause can be an
and the SQL clause can be an
`:on-constraint` clause or a`:where` clause. `:on-constraint` clause or a`:where` clause.
_[For convenience of use with the `on-conflict` helper, this clause can also accept any of those arguments, wrapped in a sequence; it can also accept an empty sequence, and just produce `ON CONFLICT`, so that it can be combined with other clauses directly]_ _[For convenience of use with the `on-conflict` helper, this clause can also accept any of those arguments, wrapped in a sequence; it can also accept an empty sequence, and just produce `ON CONFLICT`, so that it can be combined with other clauses directly]_
@ -1510,12 +989,6 @@ user=> (sql/format {:insert-into :companies
:do-update-set {:fields [:name] :do-update-set {:fields [:name]
:where [:<> :name nil]}}) :where [:<> :name nil]}})
["INSERT INTO companies (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name WHERE name IS NOT NULL" "Microsoft"] ["INSERT INTO companies (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name WHERE name IS NOT NULL" "Microsoft"]
user=> (sql/format {:insert-into :companies
:values [{:name "Microsoft"}]
:on-conflict :name
:do-update-set {:fields {:name [:+ :table.name 1]}
:where [:<> :name nil]}})
["INSERT INTO companies (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = table.name + ? WHERE name IS NOT NULL" "Microsoft" 1]
user=> (sql/format {:insert-into :companies user=> (sql/format {:insert-into :companies
:values [{:name "Microsoft"}] :values [{:name "Microsoft"}]
:on-conflict {:on-constraint :name-idx} :on-conflict {:on-constraint :name-idx}

View file

@ -6,10 +6,7 @@
["SQL Clause Reference" {:file "doc/clause-reference.md"}] ["SQL Clause Reference" {:file "doc/clause-reference.md"}]
["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"}]
["Other Databases" {:file "doc/databases.md"}]]
["All the Options" {:file "doc/options.md"}] ["All the Options" {:file "doc/options.md"}]
["Extending HoneySQL" {:file "doc/extending-honeysql.md"}] ["Extending HoneySQL" {:file "doc/extending-honeysql.md"}]
["Differences from 1.x" {:file "doc/differences-from-1-x.md"}]]} ["Differences from 1.x" {:file "doc/differences-from-1-x.md"}]]}

View file

@ -1,86 +0,0 @@
# Other Databases
There are dedicated sections for [New Relic Query Language Support](nrql.md),
[PostgreSQL Support](postgres.md), and
[XTDB Support](xtdb.md).
This section provides hints and tips for generating SQL for other
databases.
As a reminder, HoneySQL supports the following dialects out of the box:
* `:ansi` -- which is the default and provides broad support for PostgreSQL as well
* `:mysql` -- which includes MariaDB and Percona
* `:nrql` -- as of 2.5.1091
* `:oracle`
* `:sqlserver` -- Microsoft SQL Server
For the most part, these dialects only change the "stropping" --
how SQL entities are quoted in the generated SQL -- but dialects
can change clause order and/or add dialect-specific clauses.
This section is a work-in-progress and more hints and tips will be
added over time for more databases.
## Precedence
The biggest difference between database dialects tends to be
precedence. MySQL actually has different precedence in the `SET`
clause but several databases disagree on the precedence of actual
"set" operations: `UNION`, `EXCEPT`, `INTERSECT`, etc.
HoneySQL tries to be fairly neutral in this area and follows ANSI SQL
precedence. This means that some databases may have problems with
complex SQL operations that combine multiple clauses with contentious
precedence. In general, you can solve this using the `:nest`
pseudo-clause in the DSL:
<!-- :test-doc-blocks/skip -->
```clojure
{:nest DSL}
;; will produce DSL wrapped in ( .. )
```
This should allow you to cater to various databases' precedence
peculiarities.
## BigQuery (Google)
Function names can be case-sensitive: you can use the "as-is" notation
for SQL entities to avoid conversion to upper-case: `[:'domain :ref]`
produces `domain(ref)` rather than `DOMAIN(ref)`.
## ClickHouse
This is another case-sensitive database than requires the "as-is"
notation described for **BigQuery** above.
`WITH expr AS ident` is supported as a core part of the DSL,
as of 2.4.962.
## MySQL
When you select the `:mysql` dialect, the precedence of `:set` is
changed. All the other databases get this correct.
`REPLACE INTO`, while specific to MySQL and SQLite, is supported as
a core part of the DSL, as `:replace-into`, as of 2.4.969.
## SQLite
Precedence of "set" operations: SQLite differs from other databases
in handling compound SQL operations that use multiple `UNION`,
`EXCEPT`, `INTERSECT` clauses. Use `:nest` to disambiguate your
intentions.
See issue [#462](https://github.com/seancorfield/honeysql/issues/462)
for some background on this.
`INSERT OR IGNORE INTO`: this syntax is specific to SQLite for
performing upserts. However, SQLite supports the PostgreSQL-style
upsert with `ON CONFLICT` so you can use that syntax instead, for
`DO NOTHING` and `DO UPDATE SET`. In addition,
`INSERT OR REPLACE INTO` can be written using just `REPLACE INTO`
(see below).
Issue [#448](https://github.com/seancorfield/honeysql/issues/448)
has more background on this.
`REPLACE INTO`, while specific to MySQL and SQLite, is supported as
a core part of the DSL, as `:replace-into`, as of 2.4.969.

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.3.911"}
``` ```
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
@ -98,15 +98,13 @@ The primary API is just `honey.sql/format`. The `array`, `call`, `inline`, `para
Other `honeysql.core` functions that no longer exist include: `build`, `qualify`, and `quote-identifier`. Many other public functions were essentially undocumented (neither mentioned in the README nor in the tests) and also no longer exist. Other `honeysql.core` functions that no longer exist include: `build`, `qualify`, and `quote-identifier`. Many other public functions were essentially undocumented (neither mentioned in the README nor in the tests) and also no longer exist.
> As of 2.4.1002, the functionality of `qualify` can be achieved through the `:.` dot-selection special syntax.
You can now select a non-ANSI dialect of SQL using the new `honey.sql/set-dialect!` function (which sets a default dialect for all `format` operations) or by passing the new `:dialect` option to the `format` function. `:ansi` is the default dialect (which will mostly incorporate PostgreSQL usage over time). Other dialects supported are `:mysql` (which has a different quoting strategy and uses a different ranking for the `:set` clause), `:oracle` (which is essentially the `:ansi` dialect but will control other things over time), and `:sqlserver` (which is essentially the `:ansi` dialect but with a different quoting strategy). Other dialects and changes may be added over time. You can now select a non-ANSI dialect of SQL using the new `honey.sql/set-dialect!` function (which sets a default dialect for all `format` operations) or by passing the new `:dialect` option to the `format` function. `:ansi` is the default dialect (which will mostly incorporate PostgreSQL usage over time). Other dialects supported are `:mysql` (which has a different quoting strategy and uses a different ranking for the `:set` clause), `:oracle` (which is essentially the `:ansi` dialect but will control other things over time), and `:sqlserver` (which is essentially the `:ansi` dialect but with a different quoting strategy). Other dialects and changes may be added over time.
> Note: in general, all clauses are available in all dialects in HoneySQL unless the syntax of the clauses conflict between dialects (currently, no such clauses exist). The `:mysql` dialect is the only one so far that changes the priority ordering of a few clauses. > Note: in general, all clauses are available in all dialects in HoneySQL unless the syntax of the clauses conflict between dialects (currently, no such clauses exist). The `:mysql` dialect is the only one so far that changes the priority ordering of a few clauses.
## Option Changes ## Option Changes
The `:quoting <dialect>` option has been superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `:dialect <dialect>` instead (which turns on quoting by default) or set a default dialect (via `set-dialect!`) and then use `:quoted true` in `format` calls where you want quoting. The `:quoting <dialect>` option has superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `:dialect <dialect>` instead or set a default dialect (via `set-dialect!`) and then use `:quoted true` in `format` calls where you want quoting.
SQL entity names are automatically quoted if you specify a `:dialect` option to `format`, unless you also specify `:quoted false`. SQL entity names are automatically quoted if you specify a `:dialect` option to `format`, unless you also specify `:quoted false`.
@ -114,7 +112,7 @@ The following options are no longer supported:
* `:allow-dashed-names?` -- if you provide dashed-names in 2.x, they will be left as-is if quoting is enabled, else they will be converted to snake_case (so you will either get `"dashed-names"` with quoting or `dashed_names` without). If you want dashed-names to be converted to snake_case when `:quoted true`, you also need to specify `:quoted-snake true`. * `:allow-dashed-names?` -- if you provide dashed-names in 2.x, they will be left as-is if quoting is enabled, else they will be converted to snake_case (so you will either get `"dashed-names"` with quoting or `dashed_names` without). If you want dashed-names to be converted to snake_case when `:quoted true`, you also need to specify `:quoted-snake true`.
* `:allow-namespaced-names?` -- this supported `foo/bar` column names in SQL which I'd like to discourage. * `:allow-namespaced-names?` -- this supported `foo/bar` column names in SQL which I'd like to discourage.
* `:namespace-as-table?` -- this is the default in 2.x: `:foo/bar` will be treated as `foo.bar` which is more in keeping with `next.jdbc`. * `:namespace-as-table?` -- this is the default in 2.x: `:foo/bar` will be treated as `foo.bar` which is more in keeping with `next.jdbc`.
* `:parameterizer` -- this would add a lot of complexity to the formatting engine and I do not know how widely it was used (especially in its arbitrarily extensible form). _[As of 2.4.962, the ability to generated SQL with numbered parameters, i.e., `$1` instead of positional parameters, `?`, has been added via the `:numbered true` option]_ * `:parameterizer` -- this would add a lot of complexity to the formatting engine and I do not know how widely it was used (especially in its arbitrarily extensible form).
* `:return-param-names` -- this was added to 1.x back in 2013 without an associated issue or PR so I've no idea what use case this was intended to support. * `:return-param-names` -- this was added to 1.x back in 2013 without an associated issue or PR so I've no idea what use case this was intended to support.
> Note: I expect some push back on those first three options and the associated behavior changes. > Note: I expect some push back on those first three options and the associated behavior changes.
@ -133,7 +131,7 @@ The following new syntax has been added:
* `:default` -- for `DEFAULT` values (in inserts) and for declaring column defaults in table definitions, * `:default` -- for `DEFAULT` values (in inserts) and for declaring column defaults in table definitions,
* `:escape` -- used to wrap a regular expression so that non-standard escape characters can be provided, * `:escape` -- used to wrap a regular expression so that non-standard escape characters can be provided,
* `:inline` -- used as a function to replace the `sql/inline` / `#sql/inline` machinery, * `:inline` -- used as a function to replace the `sql/inline` / `#sql/inline` machinery,
* `:interval` -- used as a function to support `INTERVAL <n> <units>`, e.g., `[:interval 30 :days]` for databases that support it (e.g., MySQL) and, as of 2.4.1026, for `INTERVAL 'n units'`, e.g., `[:interval "24 hours"]` for ANSI/PostgreSQL. * `:interval` -- used as a function to support `INTERVAL <n> <units>`, e.g., `[:interval 30 :days]` for databases that support it (e.g., MySQL),
* `:lateral` -- used to wrap a statement or expression, to provide a `LATERAL` join, * `:lateral` -- used to wrap a statement or expression, to provide a `LATERAL` join,
* `:lift` -- used as a function to prevent interpretation of a Clojure data structure as DSL syntax (e.g., when passing a vector or hash map as a parameter value) -- this should mostly be a replacement for `honeysql.format/value`, * `:lift` -- used as a function to prevent interpretation of a Clojure data structure as DSL syntax (e.g., when passing a vector or hash map as a parameter value) -- this should mostly be a replacement for `honeysql.format/value`,
* `:nest` -- used as a function to add an extra level of nesting (parentheses) around an expression, * `:nest` -- used as a function to add an extra level of nesting (parentheses) around an expression,
@ -191,33 +189,13 @@ user=> (sql/format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]})
["SELECT EXISTS (SELECT a FROM foo) AS x"] ["SELECT EXISTS (SELECT a FROM foo) AS x"]
``` ```
### `ORDER BY` with `NULLS FIRST` or `NULLS LAST`
In HoneySQL 1.x, if you wanted to generate SQL like
```sql
ORDER BY ... DESC NULLS LAST
```
you needed to pass `:nulls-last` as a separate keyword, after `:asc` or `:desc`:
```clj
{:order-by [[:my-column :desc :nulls-last]]}
```
In HoneySQL 2.x, the direction and the null ordering rule are now combined into a single keyword:
```clj
{:order-by [[:my-column :desc-nulls-last]]}
```
## Extensibility ## Extensibility
The protocols and multimethods in 1.x have all gone away. The primary extension point is `honey.sql/register-clause!` which lets you specify the new clause (keyword), the formatter function for it, and the existing clause that it should be ranked before (`format` processes the DSL in clause order). The protocols and multimethods in 1.x have all gone away. The primary extension point is `honey.sql/register-clause!` which lets you specify the new clause (keyword), the formatter function for it, and the existing clause that it should be ranked before (`format` processes the DSL in clause order).
You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept. You can also register new "functions" that can implement special syntax (such as `:array`, `:inline`, `:raw` etc above) via `honey.sql/register-fn!`. This accepts a "function" name as a keyword and a formatter which will generally be a function of two arguments: the function name (so formatters can be reused across different names) and a vector of the arguments the function should accept.
And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and an optional named parameter to indicate whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). That can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values. And, finally, you can register new operators that will be recognized in expressions via `honey.sql/register-op!`. This accepts an operator name as a keyword and optional named parameters to indicate whether the operator is `:variadic` (the default is strictly binary) and whether it should ignore operands that evaluate to `nil` (via `:ignore-nil`). The latter can make it easier to construct complex expressions programmatically without having to worry about conditionally removing "optional" (`nil`) values.
> Note: because of the changes in the extension machinery between 1.x and 2.x, it is not possible to use the [nilenso/honeysql-postgress](https://github.com/nilenso/honeysql-postgres) library with HoneySQL 2.x but the goal is to incorporate all of the syntax from that library into the core of HoneySQL. > Note: because of the changes in the extension machinery between 1.x and 2.x, it is not possible to use the [nilenso/honeysql-postgress](https://github.com/nilenso/honeysql-postgres) library with HoneySQL 2.x but the goal is to incorporate all of the syntax from that library into the core of HoneySQL.

View file

@ -10,14 +10,10 @@ 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.
See also the section on
[database-specific hints and tips](databases.md), which may
let you avoid extending HoneySQL.
## Extending what `:inline` can do ## Extending what `:inline` can do
By default, the `:inline` option can convert a fairly By default, the `:inline` option can convert a fairly
@ -50,11 +46,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 +53,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
@ -111,13 +82,16 @@ You might have:
`honey.sql/register-op!` accepts a keyword (or a symbol) that `honey.sql/register-op!` accepts a keyword (or a symbol) that
should be treated as a new infix operator. should be treated as a new infix operator.
All operators are treated as variadic and an exception will be By default, operators are treated as strictly binary --
thrown if they are provided no arguments: accepting just two arguments -- and an exception will be
thrown if they are provided less than two or more than
two arguments. You can optionally specify that an operator
can take any number of arguments with `:variadic true`:
```clojure ```clojure
(require '[honey.sql :as sql]) (require '[honey.sql :as sql])
(sql/register-op! :<=>) (sql/register-op! :<=> :variadic true)
;; and then use the new operator: ;; and then use the new operator:
(sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]}) (sql/format {:select [:*], :from [:table], :where [:<=> 13 :x 42]})
;; will produce: ;; will produce:
@ -132,7 +106,7 @@ such `nil` expressions. You can specify `:ignore-nil true`
to achieve that: to achieve that:
```clojure ```clojure
(sql/register-op! :<=> :ignore-nil true) (sql/register-op! :<=> :variadic true :ignore-nil true)
;; and then use the new operator: ;; and then use the new operator:
(sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]}) (sql/format {:select [:*], :from [:table], :where [:<=> nil :x 42]})
;; will produce: ;; will produce:
@ -207,7 +181,6 @@ _New in HoneySQL 2.3.x_
The built-in dialects that HoneySQL supports are: The built-in dialects that HoneySQL supports are:
* `:ansi` -- the default, that quotes SQL entity names with double-quotes, like `"this"` * `:ansi` -- the default, that quotes SQL entity names with double-quotes, like `"this"`
* `:mysql` -- quotes SQL entity names with backticks, and changes the precedence of `SET` in `UPDATE` * `:mysql` -- quotes SQL entity names with backticks, and changes the precedence of `SET` in `UPDATE`
* `:nrql` -- as of 2.5.1091, see [New Relic NRQL Support](nrsql.md) for more details of the NRQL dialect
* `:oracle` -- quotes SQL entity names like `:ansi`, and does not use `AS` in aliases * `:oracle` -- quotes SQL entity names like `:ansi`, and does not use `AS` in aliases
* `:sqlserver` -- quotes SQL entity names with brackets, like `[this]` * `:sqlserver` -- quotes SQL entity names with brackets, like `[this]`

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.3.911"}
``` ```
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.3.911"]
``` ```
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,
@ -114,34 +110,7 @@ Some "functions" are considered to be operators. In general,
`42` and `"c"` lifted out into the overall vector result `42` and `"c"` lifted out into the overall vector result
(with a SQL string followed by all its parameters). (with a SQL string followed by all its parameters).
> Note: you can use the `:numbered true` option to `format` to produce SQL containing numbered placeholders, like `FOO(a, $1, $2)`, instead of positional placeholders (`?`). Operators can be strictly binary or variadic (most are strictly binary).
As of 2.4.1002, function calls with "named" arguments are supported
which some databases support, e.g., MySQL and PostgreSQL both have
`SUBSTRING()`:
<!-- :test-doc-blocks/skip -->
```clojure
[:substring :col 3 4] ;=> SUBSTRING(col, 3, 4)
;; can also be written:
[:substring :col :!from 3 :!for 4] ;=> SUBSTRING(col FROM 3 FOR 4)
```
In a function call, any keywords (or symbols) that begin with `!` followed
by a letter are treated as inline SQL keywords to be used instead of `,`
between arguments -- or in front of arguments, such as for `TRIM()`:
<!-- :test-doc-blocks/skip -->
```clojure
[:trim :!leading "x" :!from :col] ;=> TRIM(LEADING ? FROM col), with "x" parameter
[:trim :!both :!from :col] ;=> TRIM(BOTH FROM col), trims spaces
;; adjacent inline SQL keywords can be combined with a hyphen:
[:trim :!both-from :col] ;=> TRIM(BOTH FROM col)
;; (because - in a SQL keyword is replaced by a space)
```
Operators are all treated as variadic (except for `:=` and
`:<>` / `:!=` / `:not=` which are binary and require exactly two operands).
Special syntax can have zero or more arguments and each form is Special syntax can have zero or more arguments and each form is
described in the [Special Syntax](special-syntax.md) section. described in the [Special Syntax](special-syntax.md) section.
@ -155,22 +124,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,15 +173,13 @@ 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
As indicated in the preceding sections, values found in the DSL data structure As indicated in the preceding sections, values found in the DSL data structure
that are not keywords or symbols are lifted out as positional parameters. that are not keywords or symbols are lifted out as positional parameters.
By default, they are replaced by `?` in the generated SQL string and added to the They are replaced by `?` in the generated SQL string and added to the
parameter list in order: parameter list in order:
<!-- :test-doc-blocks/skip --> <!-- :test-doc-blocks/skip -->
@ -220,14 +187,6 @@ parameter list in order:
[:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20 [:between :size 10 20] ;=> "size BETWEEN ? AND ?" with parameters 10 and 20
``` ```
If you specify the `:numbered true` option to `format`, numbered placeholders (`$1`, `$2`, etc) will be used instead of positional placeholders (`?`).
<!-- :test-doc-blocks/skip -->
```clojure
;; with :numbered true option:
[:between :size 10 20] ;=> "size BETWEEN $1 AND $2" with parameters 10 and 20
```
HoneySQL also supports named parameters. There are two ways HoneySQL also supports named parameters. There are two ways
of identifying a named parameter: of identifying a named parameter:
* a keyword or symbol that begins with `?` * a keyword or symbol that begins with `?`
@ -247,21 +206,9 @@ call as the `:params` key of the options hash map.
;;=> ["SELECT * FROM table WHERE a = ?" 42] ;;=> ["SELECT * FROM table WHERE a = ?" 42]
``` ```
Or with `:numbered true`:
```clojure
(sql/format {:select [:*] :from [:table]
:where [:= :a :?x]}
{:params {:x 42} :numbered true})
;;=> ["SELECT * FROM table WHERE a = $1" 42]
(sql/format {:select [:*] :from [:table]
:where [:= :a [:param :x]]}
{:params {:x 42} :numbered true})
;;=> ["SELECT * FROM table WHERE a = $1" 42]
```
## 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,15 +227,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]}`
(with a few exceptions -- see the docstring of the helper function
for details).
In addition to being variadic -- which often lets you omit one In addition to being variadic -- which often lets you omit one
level of `[`..`]` -- the helper functions merge clauses, which level of `[`..`]` -- the helper functions merge clauses, which
@ -350,26 +290,23 @@ The dialects supported by HoneySQL 2.x are:
* `:ansi` -- the default, including most PostgreSQL extensions * `:ansi` -- the default, including most PostgreSQL extensions
* `:sqlserver` -- Microsoft SQL Server * `:sqlserver` -- Microsoft SQL Server
* `:mysql` -- MySQL (and Percona and MariaDB) * `:mysql` -- MySQL (and Percona and MariaDB)
* `:nrql` -- as of 2.5.1091
* `:oracle` -- Oracle * `:oracle` -- Oracle
The most visible difference between dialects is how SQL entities 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.next, 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.next). 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!
Currently, the only dialect that has substantive differences from Currently, the only dialect that has substantive differences from
the others is `:mysql` for which the `:set` clause the others is `:mysql` for which the `:set` clause
has a different precedence than ANSI SQL. has a different precedence than ANSI SQL.
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,10 +315,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`:
<!-- Reminder to doc author: <!-- Reminder to doc author:
Reset dialect to default so other blocks are not affected for test-doc-blocks --> Reset dialect to default so other blocks are not affected for test-doc-blocks -->
@ -405,26 +339,15 @@ If you want to use a dialect _and_ use the default quoting strategy (automatical
;; and reset back to the default of :ansi ;; and reset back to the default of :ansi
(sql/set-dialect! :ansi) (sql/set-dialect! :ansi)
;;=> nil ;;=> nil
;; which also resets the quoting default (back to nil)
;; so only unusual entity names get quoted:
(sql/format '{select (id) from (table)} {:quoted true}) (sql/format '{select (id) from (table)} {:quoted true})
;;=> ["SELECT \"id\" FROM \"table\""] ;;=> ["SELECT \"id\" FROM \"table\""]
;; use default quoting strategy with dialect specific quotes, only unusual entities quoted
(sql/format '{select (id, iffy##field ) from (table)} {:dialect :sqlserver :quoted nil})
;; => ["SELECT id, [iffy##field] FROM table"]
``` ```
Out of the box, as part of the extended ANSI SQL support, Out of the box, as part of the extended ANSI SQL support,
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!
See also the section on
[database-specific hints and tips](databases.md) which may
provide ways to satisfy your database's needs without changing
the dialect or extending HoneySQL.
## Reference Documentation ## Reference Documentation
The full list of supported SQL clauses is documented in the The full list of supported SQL clauses is documented in the

View file

@ -1,44 +0,0 @@
# New Relic NRQL Support
As of 2.5.1091, HoneySQL provides some support for New Relic's NRQL query language.
At present, the following additional SQL clauses (and their corresponding
helper functions) are supported:
* `:facet` - implemented just like `:select`
* `:since` - implemented like `:interval`
* `:until` - implemented like `:interval`
* `:compare-with` - implemented like `:interval`
* `:timeseries` - implemented like `:interval`
> Note: `:timeseries :auto` is the shortest way to specify a timeseries.
When you select the `:nrql` dialect, SQL formatting assumes `:inline true`
so that the generated SQL string can be used directly in NRQL queries.
In addition, stropping (quoting) is done using backticks, like MySQL,
but entities are not split at `/` or `.` characters, so that:
```
:foo/bar.baz ;;=> `foo/bar.baz`
```
```clojure
user=> (require '[honey.sql :as sql])
nil
```
```clojure
user=> (sql/format {:select [:mulog/timestamp :mulog/event-name]
:from :Log
:where [:= :mulog/data.account "foo-account-id"]
:since [2 :days :ago]
:limit 2000}
{:dialect :nrql :pretty true})
["
SELECT `mulog/timestamp`, `mulog/event-name`
FROM Log
WHERE `mulog/data.account` = 'foo-account-id'
LIMIT 2000
SINCE 2 DAYS AGO
"]
```

View file

@ -32,14 +32,15 @@ can simply evaluate to `nil` instead).
;;=> ["...WHERE (id = ?) OR (type = ?)..." 42 "match"] ;;=> ["...WHERE (id = ?) OR (type = ?)..." 42 "match"]
``` ```
## in, not-in ## in
Predicates for checking an expression is or is not a member of a specified set of values. Binary predicate for checking an expression is
is a member of a specified set of values.
The two most common forms are: The two most common forms are:
* `[:in :col [val1 val2 ...]]` or `[:not-in :col [val1 val2 ...]]` where the `valN` can be arbitrary expressions, * `[:in :col [val1 val2 ...]]` where the `valN` can be arbitrary expressions,
* `[:in :col {:select ...}]` or `[:not-in :col {:select ...}]` where the `SELECT` specifies a single column. * `[:in :col {:select ...}]` where the `SELECT` specifies a single column.
`:col` could be an arbitrary SQL expression (but is most `:col` could be an arbitrary SQL expression (but is most
commonly just a column name). commonly just a column name).
@ -47,17 +48,15 @@ commonly just a column name).
The former produces an inline vector expression with the The former produces an inline vector expression with the
values resolved as regular SQL expressions (i.e., with values resolved as regular SQL expressions (i.e., with
literal values lifted out as parameters): `col IN [?, ?, ...]` literal values lifted out as parameters): `col IN [?, ?, ...]`
or `col NOT IN [?, ?, ...]`
The latter produces a sub-select, as expected: `col IN (SELECT ...)` The latter produces a sub-select, as expected: `col IN (SELECT ...)`
or `col NOT IN (SELECT ...)`
You can also specify the set of values via a named parameter: You can also specify the set of values via a named parameter:
* `[:in :col :?values]` or `[:not-in :col :?values]` where `:params {:values [1 2 ...]}` is provided to `format` in the options. * `[:in :col :?values]` where `:params {:values [1 2 ...]}` is provided to `format` in the options.
In this case, the named parameter is expanded directly when In this case, the named parameter is expanded directly when
`:in` (or `:not-in`) is formatted to obtain the sequence of values (which `:in` is formatted to obtain the sequence of values (which
must be _sequential_, not a Clojure set). That means you must be _sequential_, not a Clojure set). That means you
cannot use this approach and also specify `:cache` -- see cannot use this approach and also specify `:cache` -- see
[cache in All the Options](options.md#cache) for more details. [cache in All the Options](options.md#cache) for more details.
@ -68,27 +67,22 @@ of columns, producing `(col1, col2) IN (SELECT ...)`, but
you need to specify the columns (or expressions) using the you need to specify the columns (or expressions) using the
`:composite` special syntax: `:composite` special syntax:
* `[:in [:composite :col1 :col2] ...]` or `[:not-in [:composite :col1 :col2] ...]` * `[:in [:composite :col1 :col2] ...]`
This produces `(col1, col2) IN ...` or `(col1, col2) NOT IN ...` This produces `(col1, col2) IN ...`
> Note: This is a change from HoneySQL 1.x which accepted a sequence of column names but required more work for arbitrary expressions. > Note: This is a change from HoneySQL 1.x which accepted a sequence of column names but required more work for arbitrary expressions.
## = <> ## = <> < > <= >=
Binary comparison operators. These expect exactly Binary comparison operators. These expect exactly
two arguments. two arguments.
`not=` and `!=` are accepted as aliases for `<>`. `not=` and `!=` are accepted as aliases for `<>`.
## < > <= >=
Comparison operators. These expect exactly
two arguments.
## is, is-not ## is, is-not
Predicates for `NULL` and Boolean values: Binary predicates for `NULL` and Boolean values:
```clojure ```clojure
{... {...
@ -110,28 +104,18 @@ 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. `+` and `*` are
variadic; the rest are strictly binary operators.
## like, not like, ilike, not ilike, regexp ## like, not like, ilike, not ilike, regexp
Pattern matching operators. `regex` is accepted Pattern matching binary operators. `regex` is accepted
as an alias for `regexp`. as an alias for `regexp`.
`similar-to` and `not-similar-to` are also supported. `similar-to` and `not-similar-to` are also supported.
## with ordinality
The ANSI SQL `WITH ORDINALITY` expression is supported as an infix operator:
```clojure
{...
[:with-ordinality [:jsonb_array_elements :j] [:arr :item :index]]
...}
;;=> ["...JSONB_ARRAY_ELEMENTS(j) WITH ORDINALITY ARR(item, index)..."]
```
## || ## ||
String concatenation operator. Variadic string concatenation operator.

View file

@ -18,27 +18,12 @@ All options may be omitted. The default behavior of each option is described in
* `:cache` -- an atom containing a [clojure.core.cache](https://github.com/clojure/core.cache) cache used to cache generated SQL; the default behavior is to generate SQL on each call to `format`, * `:cache` -- an atom containing a [clojure.core.cache](https://github.com/clojure/core.cache) cache used to cache generated SQL; the default behavior is to generate SQL on each call to `format`,
* `:checking` -- `:none` (default), `:basic`, or `:strict` to control the amount of lint-like checking that HoneySQL performs, * `:checking` -- `:none` (default), `:basic`, or `:strict` to control the amount of lint-like checking that HoneySQL performs,
* `:dialect` -- a keyword that identifies a dialect to be used for this specific call to `format`; the default is to use what was specified in `set-dialect!` or `:ansi` if no other dialect has been set, * `:dialect` -- a keyword that identifies a dialect to be used for this specific call to `format`; the default is to use what was specified in `set-dialect!` or `:ansi` if no other dialect has been set,
* `:ignored-metadata` -- a sequence of metadata keys that should be ignored when formatting (in addition to `:file`, `:line`, `:column`, `:end-line` and `:end-column` which are always ignored); the default is `[]` -- no additional metadata is ignored (since 2.5.1103),
* `:inline` -- a Boolean indicating whether or not to inline parameter values, rather than use `?` placeholders and a sequence of parameter values; the default is `false` -- values are not inlined, * `:inline` -- a Boolean indicating whether or not to inline parameter values, rather than use `?` placeholders and a sequence of parameter values; the default is `false` -- values are not inlined,
* `:numbered` -- a Boolean indicating whether to generate numbered placeholders in the generated SQL (`$1`, `$2`, etc) or positional placeholders (`?`); the default is `false` (positional placeholders); this option was added in 2.4.962,
* `:params` -- a hash map providing values for named parameters, identified by names (keywords or symbols) that start with `?` in the DSL; the default is that any such named parameters will have `nil` values, * `:params` -- a hash map providing values for named parameters, identified by names (keywords or symbols) that start with `?` in the DSL; the default is that any such named parameters will have `nil` values,
* `:quoted` -- a Boolean indicating whether or not to quote (strop) SQL entity names (table and column names); the default is `nil` -- alphanumeric SQL entity names are not quoted but (as of 2.3.928) "unusual" SQL entity names are quoted; a `false` value turns off all quoting, * `:quoted` -- a Boolean indicating whether or not to quote (strop) SQL entity names (table and column names); the default is `nil` -- alphanumeric SQL entity names are not quoted but (as of 2.3.next) "unusual" SQL entity names are quoted; a `false` value turns off all quoting,
* `:quoted-always` -- an optional regex that matches SQL entity names that should always be quoted (stropped) regardless of the value of `:quoted`; the default is `nil` -- no SQL entity names are always quoted,
* `:quoted-snake` -- a Boolean indicating whether or not quoted and string SQL entity names should have `-` replaced by `_`; the default is `false` -- quoted and string SQL entity names are left exactly as-is, * `:quoted-snake` -- a Boolean indicating whether or not quoted and string SQL entity names should have `-` replaced by `_`; the default is `false` -- quoted and string SQL entity names are left exactly as-is,
* `:values-default-columns` -- a sequence of column names that should have `DEFAULT` values instead of `NULL` values if used in a `VALUES` clause with no associated matching value in the hash maps passed in; the default behavior is for such missing columns to be given `NULL` values. * `:values-default-columns` -- a sequence of column names that should have `DEFAULT` values instead of `NULL` values if used in a `VALUES` clause with no associated matching value in the hash maps passed in; the default behavior is for such missing columns to be given `NULL` values.
As of 2.4.947, you can call `set-options!` with an options hash map to change the
global defaults of certain options:
* `:checking` -- can be `:basic` or `:strict`; specify `:none` to reset to the default,
* `:inline` -- can be `true` but consider the security issues this causes by not using parameterized SQL statements; specify `false` (or `nil`) to reset to the default,
* `:numbered` -- can be `true` or `false`; specify `false` to reset to the default,
* `:quoted` -- can be `true` or `false`; specify `nil` to reset to the default; calling `set-dialect!` or providing a `:dialect` option to `format` will override the global default,
* `:quoted-snake` -- can be `true`; specify `false` (or `nil`) to reset to the default.
Other options may only be specified directly in calls to `format` as they are considered
per-statement, rather than global.
See below for the interaction between `:dialect` and `:quoted`. See below for the interaction between `:dialect` and `:quoted`.
## `:cache` ## `:cache`
@ -100,13 +85,6 @@ was wrapped in `[:inline `..`]`:
> Note: you can provide additional inline formatting by extending the `InlineValue` protocol from `honey.sql.protocols` to new types. > Note: you can provide additional inline formatting by extending the `InlineValue` protocol from `honey.sql.protocols` to new types.
## `:numbered`
By default, HoneySQL generates SQL using positional placeholders (`?`).
Specifying `:numbered true` tells HoneySQL to generate SQL using
numbered placeholders instead (`$1`, `$2`, etc). This can be set
globally using `set-options!`.
## `:params` ## `:params`
The `:params` option provides a mapping from named parameters The `:params` option provides a mapping from named parameters
@ -132,10 +110,7 @@ selected dialect.
If `:quoted false`, SQL entity names that represent tables and columns If `:quoted false`, SQL entity names that represent tables and columns
will not be quoted. If those SQL entity names are reserved words in will not be quoted. If those SQL entity names are reserved words in
SQL, the generated SQL will be invalid. You can use the `:quoted-always` SQL, the generated SQL will be invalid.
option to specify a regex, to identify SQL entity names that should
always be quoted (stropped) regardless of the value of `:quoted`, e.g.,
reserved words that you happen to use as table or column names.
The quoting (stropping) is dialect-dependent: The quoting (stropping) is dialect-dependent:
* `:ansi` -- uses double quotes * `:ansi` -- uses double quotes
@ -143,7 +118,7 @@ The quoting (stropping) is dialect-dependent:
* `:oracle` -- uses double quotes * `:oracle` -- uses double quotes
* `:sqlserver` -- user square brackets * `:sqlserver` -- user square brackets
As of 2.3.928, if `:quoted` and `:dialect` are not provided, and no As of 2.3.next, if `:quoted` and `:dialect` are not provided, and no
default quoting strategy has been specified (via `set-dialect!`) then default quoting strategy has been specified (via `set-dialect!`) then
alphanumeric SQL entity names will not be quoted but "unusual" SQL entity names will alphanumeric SQL entity names will not be quoted but "unusual" SQL entity names will

View file

@ -10,12 +10,6 @@ Everything that the nilenso library provided (in 0.4.112) is implemented
directly in HoneySQL 2.x although a few things have a directly in HoneySQL 2.x although a few things have a
slightly different syntax. slightly different syntax.
If you are using HoneySQL with the Node.js PostgreSQL driver, it
only accepts numbered placeholders, not positional placeholders,
so you will need to specify the `:numbered true` option that was
added in 2.4.962. You may find it convenient to set this option
globally, via `set-options!`.
## Code Examples ## Code Examples
The code examples herein assume: The code examples herein assume:
@ -37,27 +31,14 @@ Clojure users can opt for the shorter `(require '[honey.sql :as sql] '[honey.sql
## Working with Arrays ## Working with Arrays
HoneySQL supports `:array` as special syntax to produce `ARRAY[..]` expressions: HoneySQL supports `:array` as special syntax to produce `ARRAY[..]` expressions
but PostgreSQL also has an "array constructor" for creating arrays from subquery results.
```clojure
user=> (sql/format {:select [[[:array [1 2 3]] :a]]})
["SELECT ARRAY[?, ?, ?] AS a" 1 2 3]
```
PostgreSQL also has an "array constructor" for creating arrays from subquery results.
```sql ```sql
SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%'); SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%');
``` ```
As of 2.5.1091, HoneySQL supports this syntax directly: In order to produce that SQL, you can use HoneySQL's "as-is" function syntax to circumvent
```clojure
user=> (sql/format {:select [[[:array {:select :oid :from :pg_proc :where [:like :proname [:inline "bytea%"]]}]]]})
["SELECT ARRAY(SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%')"]
```
Prior to 2.5.1091, you had to use HoneySQL's "as-is" function syntax to circumvent
the special syntax: the special syntax:
```clojure ```clojure
@ -65,6 +46,13 @@ user=> (sql/format {:select [[[:'ARRAY {:select :oid :from :pg_proc :where [:lik
["SELECT ARRAY (SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%')"] ["SELECT ARRAY (SELECT oid FROM pg_proc WHERE proname LIKE 'bytea%')"]
``` ```
Compare this with the `ARRAY[..]` syntax:
```clojure
user=> (sql/format {:select [[[:array [1 2 3]] :a]]})
["SELECT ARRAY[?, ?, ?] AS a" 1 2 3]
```
## Operators with @, #, and ~ ## Operators with @, #, and ~
A number of PostgreSQL operators contain `@`, `#`, or `~` which are not legal in a Clojure keyword or symbol (as literal syntax). The namespace `honey.sql.pg-ops` provides convenient symbolic names for these JSON and regex operators, substituting `at` for `@`, `hash` for `#`, and `tilde` for `~`. A number of PostgreSQL operators contain `@`, `#`, or `~` which are not legal in a Clojure keyword or symbol (as literal syntax). The namespace `honey.sql.pg-ops` provides convenient symbolic names for these JSON and regex operators, substituting `at` for `@`, `hash` for `#`, and `tilde` for `~`.
@ -108,8 +96,8 @@ user=> (-> (insert-into :distributors)
(returning :*) (returning :*)
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?), (?, ?) (did, dname) VALUES (?, ?), (?, ?)
ON CONFLICT (did) ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname DO UPDATE SET dname = EXCLUDED.dname
RETURNING * RETURNING *
@ -130,8 +118,8 @@ user=> (-> (insert-into :distributors)
(returning :*) (returning :*)
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?), (?, ?) (did, dname) VALUES (?, ?), (?, ?)
ON CONFLICT (did) ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname DO UPDATE SET dname = EXCLUDED.dname
RETURNING * RETURNING *
@ -150,8 +138,8 @@ user=> (-> (insert-into :distributors)
do-nothing)) do-nothing))
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?) (did, dname) VALUES (?, ?)
ON CONFLICT (did) ON CONFLICT (did)
DO NOTHING DO NOTHING
" "
@ -167,8 +155,8 @@ user=> (-> (insert-into :distributors)
do-nothing do-nothing
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?) (did, dname) VALUES (?, ?)
ON CONFLICT (did) ON CONFLICT (did)
DO NOTHING DO NOTHING
" "
@ -186,8 +174,8 @@ user=> (-> (insert-into :distributors)
do-nothing do-nothing
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?) (did, dname) VALUES (?, ?)
ON CONFLICT ON CONSTRAINT distributors_pkey ON CONFLICT ON CONSTRAINT distributors_pkey
DO NOTHING DO NOTHING
" "
@ -200,8 +188,8 @@ user=> (-> (insert-into :distributors)
do-nothing do-nothing
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO distributors (did, dname) INSERT INTO distributors
VALUES (?, ?) (did, dname) VALUES (?, ?)
ON CONFLICT ON CONFLICT
ON CONSTRAINT distributors_pkey ON CONSTRAINT distributors_pkey
DO NOTHING DO NOTHING
@ -221,8 +209,8 @@ user=> (-> (insert-into :user)
(do-update-set :phone :name (where [:= :user.active false])) (do-update-set :phone :name (where [:= :user.active false]))
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
INSERT INTO user (phone, name) INSERT INTO user
VALUES (?, ?) (phone, name) VALUES (?, ?)
ON CONFLICT (phone) WHERE phone IS NOT NULL ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
" "
@ -237,8 +225,8 @@ user=> (sql/format
:where [:= :user.active false]}} :where [:= :user.active false]}}
{:pretty true}) {:pretty true})
[" ["
INSERT INTO user (phone, name) INSERT INTO user
VALUES (?, ?) (phone, name) VALUES (?, ?)
ON CONFLICT (phone) WHERE phone IS NOT NULL ON CONFLICT (phone) WHERE phone IS NOT NULL
DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE
" "
@ -261,89 +249,6 @@ By comparison, this is the DSL structure that nilenso would have required:
:where [:= :user.active false]}}} :where [:= :user.active false]}}}
``` ```
All of the examples for `:do-update-set` so far provide one or
more columns and generated `SET` clauses using `EXCLUDED` columns.
You can also perform regular `SET` operations, where the right-hand
side is a full SQL expression by specifying a hash map of column /
expression pairs, like you would for a regular `:set` clause:
```clojure
user=> (-> (insert-into :table)
(values [{:id "id" :counter 1}])
(on-conflict :id)
(do-update-set {:counter [:+ :table.counter 1]})
(sql/format {:pretty true}))
["
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ?
" "id" 1 1]
;; using the DSL directly:
user=> (-> {:insert-into :table
:values [{:id "id" :counter 1}]
:on-conflict :id
:do-update-set {:counter [:+ :table.counter 1]}}
(sql/format {:pretty true}))
["
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ?
" "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
with a `WHERE` clause, you need to explicitly use the `:fields` /
`:where` format explained above. Here's how those two examples
look with a `WHERE` clause added:
```clojure
user=> (-> (insert-into :table)
(values [{:id "id" :counter 1}])
(on-conflict :id)
(do-update-set {:fields {:counter [:+ :table.counter 1]}
:where [:> :table.counter 1]})
(sql/format {:pretty true}))
["
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
" "id" 1 1 1]
;; using the DSL directly:
user=> (-> {:insert-into :table
:values [{:id "id" :counter 1}]
:on-conflict :id
:do-update-set {:fields {:counter [:+ :table.counter 1]}
:where [:> :table.counter 1]}}
(sql/format {:pretty true}))
["
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
" "id" 1 1 1]
```
## INSERT INTO AS ## INSERT INTO AS
HoneySQL supports aliases directly in `:insert-into` so no special HoneySQL supports aliases directly in `:insert-into` so no special
@ -411,14 +316,14 @@ user=> (-> (create-table :cities)
;; default values for columns: ;; default values for columns:
user=> (-> (create-table :distributors) user=> (-> (create-table :distributors)
(with-columns [[:did :integer [:primary-key] (with-columns [[:did :integer [:primary-key]
;; "serial" is inlined as 'serial': ;; "serial" is inlined as 'SERIAL':
[:default [:nextval "serial"]]] [:default [:nextval "serial"]]]
[:name [:varchar 40] [:not nil]]]) [:name [:varchar 40] [:not nil]]])
(sql/format {:pretty true})) (sql/format {:pretty true}))
;; newlines inserted for readability: ;; newlines inserted for readability:
[" ["
CREATE TABLE distributors CREATE TABLE distributors
(did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL) (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)
"] "]
;; PostgreSQL CHECK constraint is supported: ;; PostgreSQL CHECK constraint is supported:
user=> (-> (create-table :products) user=> (-> (create-table :products)
@ -430,7 +335,7 @@ user=> (-> (create-table :products)
(sql/format {:pretty true})) (sql/format {:pretty true}))
[" ["
CREATE TABLE products CREATE TABLE products
(product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price))) (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))
"] "]
;; conditional creation: ;; conditional creation:
user=> (-> (create-table :products :if-not-exists) user=> (-> (create-table :products :if-not-exists)
@ -456,6 +361,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 +378,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

@ -6,57 +6,17 @@ as special syntactic forms.
The first group are used for SQL expressions. The second (last group) are used primarily in column definitions (as part of `:with-columns` and `:add-column` / `:alter-column`). The first group are used for SQL expressions. The second (last group) are used primarily in column definitions (as part of `:with-columns` and `:add-column` / `:alter-column`).
The examples in this section assume the following: ## array
Accepts a single argument, which is expected to evaluate to
a sequence, and produces `ARRAY[?, ?, ..]` for the elements
of that sequence (as SQL parameters):
```clojure ```clojure
(require '[honey.sql :as sql]) (require '[honey.sql :as sql])
```
## alias
Accepts a single argument which should be an alias name (from an `AS` clause
elsewhere in the overall SQL statement) and uses alias formatting rules rather
than table/column formatting rules (different handling of dots and hyphens).
This allows you to override HoneySQL's default assumption about entity names
and strings.
```clojure
(sql/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias "some-alias"]]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sql/format {:select [[:column-name :'some-alias]]
:from :b
:order-by [[[:alias :'some-alias]]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sql/format {:select [[:column-name "some-alias"]]
:from :b
:group-by [[:alias "some-alias"]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
(sql/format {:select [[:column-name "some-alias"]]
:from :b
:group-by [[:alias :'some-alias]]})
;;=> ["SELECT column_name AS \"some-alias\" FROM b GROUP BY \"some-alias\""]
```
## array
Accepts either an expression (that evaluates to a sequence) or a subquery
(hash map). In the expression case, also accepts an optional second argument
that specifies the type of the array.
Produces either an `ARRAY[..]` or an `ARRAY(subquery)` expression.
In the expression case, produces `ARRAY[?, ?, ..]` for the elements of that
sequence (as SQL parameters):
```clojure
(sql/format-expr [:array (range 5)]) (sql/format-expr [:array (range 5)])
;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4] ;;=> ["ARRAY[?, ?, ?, ?, ?]" 0 1 2 3 4]
(sql/format-expr [:array (range 3) :text])
;;=> ["ARRAY[?, ?, ?]::TEXT[]" 0 1 2]
(sql/format-expr [:array [] :integer])
;;=> ["ARRAY[]::INTEGER[]"]
``` ```
> Note: you cannot provide a named parameter as the argument for `:array` because the generated SQL depends on the number of elements in the sequence, so the following throws an exception: > Note: you cannot provide a named parameter as the argument for `:array` because the generated SQL depends on the number of elements in the sequence, so the following throws an exception:
@ -81,50 +41,7 @@ In addition, the argument to `:array` is treated as a literal sequence of Clojur
;;=> ["SELECT ARRAY[inline, (?, ?, ?)] AS arr" 1 2 3] ;;=> ["SELECT ARRAY[inline, (?, ?, ?)] AS arr" 1 2 3]
``` ```
In the subquery case, produces `ARRAY(subquery)`: ## between
```clojure
(sql/format {:select [[[:array {:select :* :from :table}] :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
Accepts two arguments: an expression (assumed to be a date/time of some sort)
and a time zone name or identifier (can be a string, a symbol, or a keyword):
```clojure
(sql/format-expr [:at-time-zone [:now] :UTC])
;;=> ["NOW() AT TIME ZONE 'UTC'"]
```
The time zone name or identifier will be inlined (as a string) and therefore
cannot be an expression.
## between and not-between
Accepts three arguments: an expression, a lower bound, and Accepts three arguments: an expression, a lower bound, and
an upper bound: an upper bound:
@ -132,9 +49,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
@ -160,44 +74,14 @@ this using `:case-expr`:
## cast ## cast
A SQL `CAST` expression. Expects an expression and something A SQL CAST expression. Expects an expression and something
that produces a SQL type: that produces a SQL type:
```clojure ```clojure
(sql/format [:cast :a :int]) (sql/format-expr [:cast :a :int])
;;=> ["CAST(a AS INT)"] ;;=> ["CAST(a AS int)"]
``` ```
Quoting does not affect the type in a `CAST`, only the expression:
```clojure
(sql/format [:cast :a :int] {:quoted true})
;;=> ["CAST(\"a\" AS INT)"]
```
A hyphen (`-`) in the type name becomes a space:
```clojure
(sql/format [:cast :a :double-precision])
;;=> ["CAST(a AS DOUBLE PRECISION)"]
```
If you want an underscore in the type name, you have two choices:
```clojure
(sql/format [:cast :a :some_type])
;;=> ["CAST(a AS SOME_TYPE)"]
```
or:
```clojure
(sql/format [:cast :a :'some-type])
;;=> ["CAST(a AS some_type)"]
```
> Note: In HoneySQL 2.4.947 and earlier, the type name was incorrectly affected by the quoting feature, and a hyphen in a type name was incorrectly changed to underscore. This was corrected in 2.4.962.
## composite ## composite
Accepts any number of expressions and produces a composite Accepts any number of expressions and produces a composite
@ -208,23 +92,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,33 +101,6 @@ 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 . .:.
Accepts an expression and one or more fields (or columns). Plain dot produces
plain dotted selection:
```clojure
(sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]})
;;=> ["SELECT t.c, s.t.c"]
```
Dot colon dot produces Snowflake-style dotted selection:
```clojure
(sql/format {:select [ [[:.:. :t :c]] [[:.:. :s :t :c]] ]})
;;=> ["SELECT t:c, s:t.c"]
```
Can be used with `:nest` for field selection from composites:
```clojure
(sql/format {:select [ [[:. [:nest :v] :*]] [[:. [:nest [: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
@ -332,22 +172,10 @@ FROM aa
100] 100]
``` ```
## ignore/respect nulls
Both of these accept a single argument -- an expression -- and
renders that expression followed by `IGNORE NULLS` or `RESPECT NULLS`:
```clojure
(sql/format-expr [:array_agg [:ignore-nulls :a]])
;;=> ["ARRAY_AGG(a IGNORE NULLS)"]
(sql/format-expr [:array_agg [:respect-nulls :a]])
;;=> ["ARRAY_AGG(a RESPECT NULLS)"]
```
## inline ## inline
Accepts one or more arguments and tries to render them as a Accepts a single argument and tries to render it as a
SQL values directly in the formatted SQL string rather SQL value directly in the formatted SQL string rather
than turning it into a positional parameter: than turning it into a positional parameter:
* `nil` becomes `NULL` * `nil` becomes `NULL`
* keywords and symbols become upper case entities (with `-` replaced by space) * keywords and symbols become upper case entities (with `-` replaced by space)
@ -360,73 +188,17 @@ than turning it into a positional parameter:
;;=> ["WHERE x = 'foo'"] ;;=> ["WHERE x = 'foo'"]
``` ```
If multiple arguments are provided, they are individually formatted as above
and joined into a single SQL string with spaces:
```clojure
(sql/format {:where [:= :x [:inline :DATE "2019-01-01"]]})
;;=> ["WHERE x = DATE '2019-01-01'"]
```
This is convenient for rendering DATE/TIME/TIMESTAMP literals in SQL.
If an argument is an expression, it is formatted as a regular SQL expression
except that any parameters are inlined:
```clojure
(sql/format {:where [:= :x [:inline [:date_add [:now] [:interval 30 :days]]]]})
;;=> ["WHERE x = DATE_ADD(NOW(), INTERVAL 30 DAYS)"]
```
In particular, that means that you can use `:inline` to inline a parameter
value:
```clojure
(sql/format {:where [:= :x [:inline :?foo]]} {:params {:foo "bar"}})
;;=> ["WHERE x = 'bar'"]
(sql/format {:where [:= :x [:inline [:param :foo]]]} {:params {:foo "bar"}})
;;=> ["WHERE x = 'bar'"]
```
## interval ## interval
Accepts one or two arguments: either a string or an expression and Accepts two arguments: an expression and a keyword (or a symbol)
a keyword (or a symbol) that represents a time unit. that represents a time unit. Produces an `INTERVAL` expression:
Produces an `INTERVAL` expression:
```clojure ```clojure
(sql/format-expr [:date_add [:now] [:interval 30 :days]]) (sql/format-expr [:date_add [:now] [:interval 30 :days]])
;;=> ["DATE_ADD(NOW(), INTERVAL ? DAYS)" 30] ;;=> ["DATE_ADD(NOW(), INTERVAL ? DAYS)" 30]
(sql/format-expr [:date_add [:now] [:interval "24 Hours"]])
;;=> ["DATE_ADD(NOW(), INTERVAL '24 Hours')"]
``` ```
> Note: PostgreSQL also has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector). > Note: PostgreSQL has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector).
## join
Accepts a table name (or expression) followed by one or more join clauses.
Produces a nested `JOIN` expression, typically used as the table expression of
a `JOIN` clause.
```clojure
(sql/format {:join [[[: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
@ -560,12 +332,6 @@ parameters from them:
;;=> ["SELECT a, @var := 'foo'"] ;;=> ["SELECT a, @var := 'foo'"]
(sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]}) (sql/format {:select [:a [[:raw ["@var := " ["foo"]]]]]})
;;=> ["SELECT a, @var := (?)" "foo"] ;;=> ["SELECT a, @var := (?)" "foo"]
;; when multiple expressions are provided, the enclosing
;; vector can be omitted:
(sql/format {:select [:a [[:raw "@var := " [:inline "foo"]]]]})
;;=> ["SELECT a, @var := 'foo'"]
(sql/format {:select [:a [[:raw "@var := " ["foo"]]]]})
;;=> ["SELECT a, @var := (?)" "foo"]
``` ```
`:raw` is also supported as a SQL clause for the same reason. `:raw` is also supported as a SQL clause for the same reason.

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,14 +1,10 @@
;; copyright (c) 2020-2025 sean corfield, all rights reserved ;; copyright (c) 2020-2022 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.
All helper functions are inherently variadic. All helper functions are inherently variadic. Typical
usage is threaded, like this:
In general, `(helper :foo expr)` will produce `{:helper [:foo expr]}`,
with a few exceptions: see the docstring of the helper function for details.
Typical usage is threaded, like this:
``` ```
(-> (select :a :b :c) (-> (select :a :b :c)
@ -17,16 +13,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,26 +44,29 @@
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 [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]))
#?(:clj (set! *warn-on-reflection* true))
;; implementation helpers: ;; implementation helpers:
(defn- default-merge [current args] (defn- default-merge [current args]
(let [mdata (meta current) (c/into (vec current) args))
current (cond
(nil? current) [] (defn- sym->kw
(sequential? current) (vec current) "Given a symbol, produce a keyword, retaining the namespace
:else [current])] qualifier, if any."
(c/into (with-meta current mdata) args))) [s]
(if (symbol? s)
(if-let [n (namespace s)]
(keyword n (name s))
(keyword (name s)))
s))
(defn- conjunction? (defn- conjunction?
[e] [e]
(and (ident? e) (and (ident? e)
(contains? #{:and :or} (#'h/sym->kw e)))) (contains? #{:and :or} (sym->kw e))))
(defn- simplify-logic (defn- simplify-logic
"For Boolean expressions, simplify the logic to make "For Boolean expressions, simplify the logic to make
@ -89,11 +78,11 @@
[e] [e]
(if (= 1 (count (rest e))) (if (= 1 (count (rest e)))
(fnext e) (fnext e)
(let [conjunction (#'h/sym->kw (first e))] (let [conjunction (sym->kw (first e))]
(reduce (fn [acc e] (reduce (fn [acc e]
(if (and (sequential? e) (if (and (sequential? e)
(conjunction? (first e)) (conjunction? (first e))
(= conjunction (#'h/sym->kw (first e)))) (= conjunction (sym->kw (first e))))
(c/into acc (rest e)) (c/into acc (rest e))
(conj acc e))) (conj acc e)))
[conjunction] [conjunction]
@ -137,26 +126,11 @@
:having #'conjunction-merge}) :having #'conjunction-merge})
(defn- helper-merge [data k args] (defn- helper-merge [data k args]
(let [k' (#'h/sym->kw k) (if-let [merge-fn (special-merges k)]
k (#'h/kw->sym k) (if-let [clause (merge-fn (get data k) args)]
d (get data k)
d' (get data k')
mf (special-merges k')
mf' (or mf default-merge)]
(cond (some? d)
(if-some [clause (mf' d args)]
(assoc data k clause) (assoc data k clause)
data) data)
(some? d') (clojure.core/update data k default-merge args)))
(if-some [clause (mf' d' args)]
(assoc data k' clause)
data)
mf
(if-some [clause (mf nil args)]
(assoc data k' clause)
data)
:else
(c/update data k' default-merge args))))
(defn- generic [k args] (defn- generic [k args]
(if (map? (first args)) (if (map? (first args))
@ -164,12 +138,6 @@
(helper-merge data k args)) (helper-merge data k args))
(helper-merge {} k args))) (helper-merge {} k args)))
(defn- generic-grouped [k args]
(if (map? (first args))
(let [[data & args] args]
(helper-merge data k [args]))
(helper-merge {} k [args])))
(defn- generic-1 [k [data arg]] (defn- generic-1 [k [data arg]]
(if (map? data) (if (map? data)
(assoc data k arg) (assoc data k arg)
@ -201,18 +169,15 @@
(add-column :name [:varchar 32] [:not nil])" (add-column :name [:varchar 32] [:not nil])"
[& col-elems] [& col-elems]
(generic-grouped :add-column col-elems)) (generic :add-column col-elems))
(defn drop-column (defn drop-column
"Takes one or more column names (use with `alter-table`). "Takes a single column name (use with `alter-table`).
Accepts an `IF EXISTS` flag (keyword or symbol) before (alter-table :foo (drop-column :bar))"
any column names.
(alter-table :foo (drop-column :bar :if-exists :quux))"
{:arglists '([col])} {:arglists '([col])}
[& col-elems] [& args]
(generic :drop-column col-elems)) (generic-1 :drop-column args))
(defn alter-column (defn alter-column
"Like add-column, accepts any number of SQL elements "Like add-column, accepts any number of SQL elements
@ -220,7 +185,7 @@
(alter-column :name [:varchar 64] [:not nil])" (alter-column :name [:varchar 64] [:not nil])"
[& col-elems] [& col-elems]
(generic-grouped :alter-column col-elems)) (generic :alter-column col-elems))
(defn modify-column (defn modify-column
"Like add-column, accepts any number of SQL elements "Like add-column, accepts any number of SQL elements
@ -231,7 +196,7 @@
MySQL-specific, deprecated. Use `alter-column` and MySQL-specific, deprecated. Use `alter-column` and
specify the MySQL dialect to get `MODIFY COLUMN`." specify the MySQL dialect to get `MODIFY COLUMN`."
[& col-elems] [& col-elems]
(generic-grouped :modify-column col-elems)) (generic :modify-column col-elems))
(defn rename-column (defn rename-column
"Accepts two column names: the original name and the "Accepts two column names: the original name and the
@ -340,15 +305,6 @@
[& args] [& args]
(generic :create-view args)) (generic :create-view args))
(defn create-or-replace-view
"Accepts a single view name to create.
(-> (create-or-replace-view :cities)
(select :*) (from :city))"
{:arglists '([view])}
[& args]
(generic :create-or-replace-view args))
(defn create-materialized-view (defn create-materialized-view
"Accepts a single view name to create. "Accepts a single view name to create.
@ -387,25 +343,6 @@
[& views] [& views]
(generic :refresh-materialized-view views)) (generic :refresh-materialized-view views))
(defn create-index
"Accepts an index spexification and a column specification. The column
specification consists of table name and one or more columns.
(create-index :name-of-idx [:table :col])
(create-index :name-of-idx [:table :col1 :col2])
(create-index [:unique :name-of-idx] [:table :col])
PostgreSQL also supports :if-not-exists and expressions instead of columns.
(create-index [:name-of-idx :if-not-exists] [:table :%lower.col])"
[& 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 +389,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,32 +442,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
"Like `select-distinct` but produces DISTINCT..."
[& args]
(generic-1 :distinct args))
(defn expr
"Like `distinct` but produces ... (i.e., just the expression that follows)."
[& 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 +455,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,33 +470,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
"Accepts a table name or a table/alias pair. That
can optionally be followed by a collection of
column names. That can optionally be followed by
a (select) statement clause.
The arguments are identical to insert-into.
The REPLACE INTO statement is only supported by
MySQL and SQLite."
{:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args]
(stuff-into :replace-into args))
(defn update (defn update
"Accepts either a table name or a table/alias pair. "Accepts either a table name or a table/alias pair.
@ -629,15 +503,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])}
@ -978,8 +843,7 @@
(defn on-conflict (defn on-conflict
"Accepts zero or more SQL entities (keywords or symbols), "Accepts zero or more SQL entities (keywords or symbols),
optionally followed by a single SQL clause (`{:where <condition>}`). optionally followed by a single SQL clause (hash map)."
Ex.: `(on-conflict :mom :dad {:where [:= :race \"human\"]}`"
{:arglists '([column* where-clause])} {:arglists '([column* where-clause])}
[& args] [& args]
(generic :on-conflict args)) (generic :on-conflict args))
@ -1099,59 +963,6 @@
[& args] [& args]
(c/into [:within-group] args)) (c/into [:within-group] args))
;; nrql-specific helpers:
(defn facet
"Accepts any number of column names, or column/alias
pairs, or SQL expressions (optionally aliased):
(facet :id [:foo :bar] [[:max :quux]])
Produces: FACET id, foo AS bar, MAX(quux)"
[& args]
(generic :facet args))
(defn since
"Accepts a time interval such as:
(since 2 :days :ago)
Produces: SINCE 2 DAYS AGO"
[& args]
(generic :since args))
(defn until
"Accepts a time interval such as:
(until 1 :month :ago)
Produces: UNTIL 1 MONTH AGO"
[& args]
(generic :until args))
(defn compare-with
"Accepts a time interval such as:
(compare-with 1 :week :ago)
Produces: COMPARE WITH 1 WEEK AGO"
[& args]
(generic :compare-with args))
(defn timeseries
"Accepts a time interval such as:
(timeseries 1 :day)
or:
(timeseries :auto)
Produces: TIMESERIES 1 DAY
Or: TIMESERIES AUTO"
[& args]
(generic :timeseries args))
;; this helper is intended to ease the migration from nilenso: ;; this helper is intended to ease the migration from nilenso:
(defn upsert (defn upsert
"Provided purely to ease migration from nilenso/honeysql-postgres "Provided purely to ease migration from nilenso/honeysql-postgres
@ -1199,16 +1010,7 @@
(assoc :do-nothing do-nothing) (assoc :do-nothing do-nothing)
do-update-set do-update-set
(assoc :do-update-set (if where (assoc :do-update-set (if where
{:fields {:fields do-update-set
(cond (and (= 1 (count do-update-set))
(map? (first do-update-set)))
(first do-update-set)
(every? #(and (vector? %)
(= 2 (count %)))
do-update-set)
(into {} do-update-set)
:else
do-update-set)
:where where} :where where}
do-update-set)))))) do-update-set))))))
@ -1232,11 +1034,16 @@
[k args] [k args]
(generic-1 k args)) (generic-1 k args))
(comment #?(:clj
(-> (delete-from :table) (do
(where [:in (composite :first :second) ;; #409 this assert is only valid when :doc metadata is not elided:
[(composite 1 2) (composite 2 1)]]) (when (-> #'generic-helper-unary meta :doc)
(h/format)) ;; ensure #295 stays true (all public functions have docstring):
(-> (select [:%count.* :total]) (from :foo) h/format) (assert (empty? (->> (ns-publics *ns*) (vals) (c/filter (comp not :doc meta))))))
(-> (select [[:count :*] :total]) (from :foo) h/format) ;; ensure all public functions match clauses:
) (assert (= (clojure.core/set (conj @#'honey.sql/default-clause-order
:composite :filter :lateral :over :within-group
:upsert
:generic-helper-variadic :generic-helper-unary))
(clojure.core/set (conj (map keyword (keys (ns-publics *ns*)))
:nest :raw))))))

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,43 +25,22 @@
(: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 -> "The -> operator." :->)
"The -> operator for accessing nested JSON(B) values as JSON(B). (def ->> "The ->> operator." :->>)
Ex.: (def hash> "The #> operator." :#>)
```clojure (def hash>> "The #>> operator." :#>>)
(sql/format {:select [[[:->> [:-> :my_column \"kids\" [:inline 0]] \"name\"]]]}) (def at> "The @> operator." (keyword "@>"))
; => [\"SELECT (my_column -> ? -> 0) ->> ?\" \"kids\" \"name\"] (def <at "The <@ operator." (keyword "<@"))
``` (def ? "The ? operator." :?)
(def ?| "The ?| operator." :?|)
Notice we need to wrap the keys/indices with :inline if we don't want them to become parameters." (def ?& "The ?& operator." :?&)
:->) (def || "The || operator." :||)
(def ->> "The ->> operator - like -> but returns the value as text instead of a JSON object." :->>) (def - "The - operator." :-)
(def hash> "The #> operator extracts JSON sub-object at the specified path." :#>) (def hash- "The #- operator." :#-)
(def hash>> "The #>> operator - like hash> but returns the value as text instead of JSON object." :#>>) (def at? "The @? operator." (keyword "@?"))
(def at> "The @> operator - does the first JSON value contain the second?" (keyword "@>")) (def atat "The @@ operator." (keyword "@@"))
(def <at "The <@ operator - is the first JSON value contained in the second?" (keyword "<@"))
(def ? "The ? operator - does the text string exist as a top-level key or array element within the JSON value?" :?)
(def ?| "The ?| operator - do any of the strings in the text array exist as top-level keys or array elements?" :?|)
(def ?& "The ?& operator - do all of the strings in the text array exist as top-level keys or array elements?" :?&)
(def || "The || operator - concatenates two jsonb values (arrays or objects; anything else treated as 1-element array)." :||)
(def -
"The - operator:
- 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)"
:-)
(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 atat
"The @@ operator:
- 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.
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 "@@"))
(def tilde "The case-sensitive regex match operator." (keyword "~")) (def tilde "The case-sensitive regex match operator." (keyword "~"))
(def tilde* "The case-insensitive regex match operator." (keyword "~*")) (def tilde* "The case-insensitive regex match operator." (keyword "~*"))
@ -73,7 +52,7 @@
(def !regex !tilde) (def !regex !tilde)
(def !iregex !tilde*) (def !iregex !tilde*)
(sql/register-op! :->) (sql/register-op! :-> :variadic true)
(sql/register-op! :->>) (sql/register-op! :->>)
(sql/register-op! :#>) (sql/register-op! :#>)
(sql/register-op! :#>>) (sql/register-op! :#>>)

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

26
template/pom.xml Normal file
View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>honeysql</name>
<description>SQL as Clojure data structures.</description>
<url>https://github.com/seancorfield/honeysql</url>
<licenses>
<license>
<name>Eclipse Public License</name>
<url>http://www.eclipse.org/legal/epl-v10.html</url>
</license>
</licenses>
<developers>
<developer>
<name>Sean Corfield</name>
</developer>
<developer>
<name>Justin Kramer</name>
</developer>
</developers>
<scm>
<url>https://github.com/seancorfield/honeysql</url>
<connection>scm:git:git://github.com/seancorfield/honeysql.git</connection>
<developerConnection>scm:git:ssh://git@github.com/seancorfield/honeysql.git</developerConnection>
</scm>
</project>

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,19 +0,0 @@
;; copyright (c) 2023-2025 sean corfield, all rights reserved
(ns honey.ops-test
(:refer-clojure :exclude [format])
(:require [clojure.test :refer [deftest is]]
[honey.sql :as sut]))
(deftest issue-454
(is (= ["SELECT a - b - c AS x"]
(-> {:select [[[:- :a :b :c] :x]]}
(sut/format)))))
(deftest issue-566
(is (= ["SELECT * FROM table WHERE a IS DISTINCT FROM b"]
(-> {:select :* :from :table :where [:is-distinct-from :a :b]}
(sut/format))))
(is (= ["SELECT * FROM table WHERE a IS NOT DISTINCT FROM b"]
(-> {:select :* :from :table :where [:is-not-distinct-from :a :b]}
(sut/format)))))

View file

@ -1,44 +1,24 @@
;; copyright (c) 2020-2024 sean corfield, all rights reserved ;; copyright (c) 2020-2021 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
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 :*)
@ -164,14 +144,14 @@
(deftest select-top-tests (deftest select-top-tests
(testing "Basic TOP syntax" (testing "Basic TOP syntax"
(is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10] (is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10]
(sql/format {:select-top [10 :foo] :from :bar :order-by :quux}))) (sql/format {:select-top [10 :foo] :from :bar :order-by [:quux]})))
(is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10] (is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10]
(sql/format (-> (select-top 10 :foo) (sql/format (-> (select-top 10 :foo)
(from :bar) (from :bar)
(order-by :quux)))))) (order-by :quux))))))
(testing "Expanded TOP syntax" (testing "Expanded TOP syntax"
(is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10] (is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10]
(sql/format {:select-top [[10 :percent :with-ties] :foo :baz] :from :bar :order-by :quux}))) (sql/format {:select-top [[10 :percent :with-ties] :foo :baz] :from :bar :order-by [:quux]})))
(is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10] (is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10]
(sql/format (-> (select-top [10 :percent :with-ties] :foo :baz) (sql/format (-> (select-top [10 :percent :with-ties] :foo :baz)
(from :bar) (from :bar)
@ -269,19 +249,10 @@
(sql/format)))))) (sql/format))))))
(deftest test-cast (deftest test-cast
(is (= ["SELECT foo, CAST(bar AS INTEGER)"] (is (= ["SELECT foo, CAST(bar AS integer)"]
(sql/format {:select [:foo [[:cast :bar :integer]]]}))) (sql/format {:select [:foo [[:cast :bar :integer]]]})))
(is (= ["SELECT foo, CAST(bar AS INTEGER)"] (is (= ["SELECT foo, CAST(bar AS integer)"]
(sql/format {:select [:foo [[:cast :bar 'integer]]]}))) (sql/format {:select [:foo [[:cast :bar 'integer]]]}))))
(is (= ["SELECT foo, CAST(bar AS DOUBLE PRECISION)"] ;; Postgres example
(sql/format {:select [:foo [[:cast :bar :double-precision]]]})))
(is (= ["SELECT \"foo\", CAST(\"bar\" AS INTEGER)"]
(sql/format {:select [:foo [[:cast :bar :integer]]]} {:quoted true})))
(is (= ["SELECT `foo`, CAST(`bar` AS INTEGER)"]
(sql/format {:select [:foo [[:cast :bar :integer]]]} {:dialect :mysql})))
(is (= ["SELECT `foo`, CAST(`bar` AS CHAR(10))"]
(sql/format {:select [:foo [[:cast :bar [:char 10]]]]} {:dialect :mysql
:inline true}))))
(deftest test-value (deftest test-value
(is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}] (is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}]
@ -342,28 +313,10 @@
(sql/format {:select [:*] (sql/format {:select [:*]
:from [:customers] :from [:customers]
:where [:in :id :?ids]} :where [:in :id :?ids]}
{:params {:ids values}}))) {:params {:ids values}})))))))
(is (= ["SELECT * FROM customers WHERE id IN ($1, $2)" "1" "2"]
(sql/format {:select [:*]
:from [:customers]
:where [:in :id values]}
{:numbered true})))
(is (= ["SELECT * FROM customers WHERE id IN ($2, $3)" nil "1" "2"]
(sql/format {:select [:*]
:from [:customers]
:where [:in :id :?ids]}
{: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 +505,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]
@ -608,24 +534,6 @@
(where [:= :metroflag "y"]) (where [:= :metroflag "y"])
(with-data false))) (with-data false)))
["CREATE TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"])) ["CREATE TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
(is (= (sql/format (-> (create-table-as :metro :or-replace)
(select :*)
(from :cities)
(where [:= :metroflag "y"])
(with-data false)))
["CREATE OR REPLACE TABLE metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
(is (= (sql/format (-> (create-table-as :temp :metro :if-not-exists)
(select :*)
(from :cities)
(where [:= :metroflag "y"])
(with-data false)))
["CREATE TEMP TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
(is (= (sql/format (-> (create-table-as :temp :metro :or-replace)
(select :*)
(from :cities)
(where [:= :metroflag "y"])
(with-data false)))
["CREATE OR REPLACE TEMP TABLE metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"]))
(is (= (sql/format (-> (create-materialized-view :metro :if-not-exists) (is (= (sql/format (-> (create-materialized-view :metro :if-not-exists)
(select :*) (select :*)
(from :cities) (from :cities)
@ -642,16 +550,6 @@
[(str "CREATE TABLE IF NOT EXISTS metro" [(str "CREATE TABLE IF NOT EXISTS metro"
" (foo, bar, baz) TABLESPACE quux" " (foo, bar, baz) TABLESPACE quux"
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"])) " AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
(is (= (sql/format (-> (create-table-as :metro :or-replace
(columns :foo :bar :baz)
[:tablespace [:entity :quux]])
(select :*)
(from :cities)
(where [:= :metroflag "y"])
(with-data false)))
[(str "CREATE OR REPLACE TABLE metro"
" (foo, bar, baz) TABLESPACE quux"
" AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"]))
(is (= (sql/format (-> (create-materialized-view :metro :if-not-exists (is (= (sql/format (-> (create-materialized-view :metro :if-not-exists
(columns :foo :bar :baz) (columns :foo :bar :baz)
[:tablespace [:entity :quux]]) [:tablespace [:entity :quux]])
@ -733,20 +631,10 @@
(is (= (sql/format (-> (alter-table :fruit) (is (= (sql/format (-> (alter-table :fruit)
(add-column :id :int [:not nil]))) (add-column :id :int [:not nil])))
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"])) ["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"]))
(is (= (sql/format (-> (alter-table :fruit)
(add-column :id :int [:not nil])
(add-column :a1 :int nil)
(add-column :be :text [:not nil])))
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, ADD COLUMN a1 INT NULL, ADD COLUMN be TEXT NOT NULL"]))
(is (= (sql/format (alter-table :fruit (is (= (sql/format (alter-table :fruit
(add-column :id :int [:not nil]) (add-column :id :int [:not nil])
(drop-column :ident) (drop-column :ident)))
(drop-column :if-exists :another))) ["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident"])))
["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident, DROP COLUMN IF EXISTS another"]))
(is (= (sql/format (alter-table :fruit
(drop-column :a :b :if-exists :c :d)
(drop-column :if-exists :e)))
["ALTER TABLE fruit DROP COLUMN a, DROP COLUMN b, DROP COLUMN IF EXISTS c, DROP COLUMN d, DROP COLUMN IF EXISTS e"])))
(deftest issue-293-insert-into-data (deftest issue-293-insert-into-data
;; insert into as (and other tests) based on :insert-into ;; insert into as (and other tests) based on :insert-into
@ -836,10 +724,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
@ -951,7 +836,7 @@
[[:filter ; two pairs -- alias is on last pair [[:filter ; two pairs -- alias is on last pair
[:avg :x [:order-by :y [:a :desc]]] {:where [:< :i 10]} [:avg :x [:order-by :y [:a :desc]]] {:where [:< :i 10]}
[:sum :q] {:where [:= :x nil]}] :b] [:sum :q] {:where [:= :x nil]}] :b]
[[:within-group [:foo :y] {:order-by :x}]]]}) [[:within-group [:foo :y] {:order-by [:x]}]]]})
[(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a," [(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a,"
" AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?)," " AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?),"
" SUM(q) FILTER (WHERE x IS NULL) AS b," " SUM(q) FILTER (WHERE x IS NULL) AS b,"
@ -992,69 +877,3 @@
{:with [[:a]], {:with [[:a]],
:insert-into [[:quux [:x :y]] :insert-into [[:quux [:x :y]]
{:select [:id], :from [:table]}]})))) {:select [:id], :from [:table]}]}))))
(deftest issue-431
(testing "where false should not be ignored"
(is (= {:where false}
(where false)))
(is (= ["SELECT * FROM table WHERE FALSE"]
(sql/format {:select [:*] :from [:table] :where false})))))
(deftest issue-505
(testing "where should merge symbols/keywords correctly"
(is (= '{where [:and (= a 1) [:= :b 2]]}
(-> '{where (= a 1)}
(where [:= :b 2]))))
(is (= '{where (= a 1)}
(-> '{where (= a 1)}
(where))))
(is (= '{:where [:and (= a 1) [:= :b 2]]}
(-> '{:where (= a 1)}
(where [:= :b 2]))))
(is (= '{:where (= a 1)}
(-> '{:where (= a 1)}
(where))))
(is (= '{:where [:= :b 2]}
(-> '{}
(where [:= :b 2]))))
(is (= '{}
(-> '{}
(where))))))
(deftest test-create-index
(testing "create index, commonly supported features"
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column)"]
(sql/format {:create-index [:my-column-idx [:my-table :my-column]]})))
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column)"]
(sql/format (create-index :my-column-idx [:my-table :my-column]))))
(is (= ["CREATE UNIQUE INDEX my_column_idx ON my_table (my_column)"]
(sql/format (create-index [:unique :my-column-idx] [:my-table :my-column]))))
(is (= ["CREATE INDEX my_column_idx ON my_table (my_column, my_other_column)"]
(sql/format (create-index :my-column-idx [:my-table :my-column :my-other-column])))))
(testing "PostgreSQL extensions (IF NOT EXISTS and expressions)"
(is (= ["CREATE INDEX IF NOT EXISTS my_column_idx ON my_table (my_column)"]
(sql/format (create-index [:my-column-idx :if-not-exists] [: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]))))
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column])))))
(testing "PostgreSQL extensions (USING GIN/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

@ -93,20 +93,6 @@
(upsert (-> (on-conflict (on-constraint :distributors_pkey)) (upsert (-> (on-conflict (on-constraint :distributors_pkey))
do-nothing)) do-nothing))
sql/format))) sql/format)))
(is (= ["INSERT INTO foo (id, data) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET into = ((STATE(?), MODIFIED(NOW()))) WHERE state = ?" 1 42 "enabled" "disabled"]
(sql/format (-> (insert-into :foo)
(values [{:id 1 :data 42}])
(upsert (-> (on-conflict :id)
(do-update-set [:state "enabled"]
[:modified [:now]])
(where [:= :state "disabled"])))))))
(is (= ["INSERT INTO foo (id, data) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET state = ?, modified = NOW() WHERE state = ?" 1 42 "enabled" "disabled"]
(sql/format (-> (insert-into :foo)
(values [{:id 1 :data 42}])
(upsert (-> (on-conflict :id)
(do-update-set {:state "enabled"
:modified [:now]})
(where [:= :state "disabled"])))))))
(is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" 10 "Pinp Design" 11 "Foo Bar Works"] (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" 10 "Pinp Design" 11 "Foo Bar Works"]
(sql/format {:insert-into :distributors (sql/format {:insert-into :distributors
:values [{:did 10 :dname "Pinp Design"} :values [{:did 10 :dname "Pinp Design"}
@ -199,7 +185,7 @@
[:location :point]]) [:location :point]])
sql/format)))) sql/format))))
(testing "create table with foreign key reference" (testing "create table with foreign key reference"
(is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES cities(city), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"] (is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES CITIES(CITY), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"]
(-> (create-table :weather) (-> (create-table :weather)
(with-columns [[:city [:varchar :80] [:references :cities :city]] (with-columns [[:city [:varchar :80] [:references :cities :city]]
[:temp_lo :int] [:temp_lo :int]
@ -208,7 +194,7 @@
[:date :date]]) [:date :date]])
sql/format)))) sql/format))))
(testing "creating table with table level constraint" (testing "creating table with table level constraint"
(is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(code, title))"] (is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(CODE, TITLE))"]
(-> (create-table :films) (-> (create-table :films)
(with-columns [[:code [:char 5]] (with-columns [[:code [:char 5]]
[:title [:varchar 40]] [:title [:varchar 40]]
@ -218,7 +204,7 @@
[[:constraint :code_title] [:primary-key :code :title]]]) [[:constraint :code_title] [:primary-key :code :title]]])
sql/format)))) sql/format))))
(testing "creating table with column level constraint" (testing "creating table with column level constraint"
(is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT firstkey PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"] (is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT FIRSTKEY PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"]
(-> (create-table :films) (-> (create-table :films)
(with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]] (with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]]
[:title [:varchar 40] [:not nil]] [:title [:varchar 40] [:not nil]]
@ -227,13 +213,13 @@
[:kind [:varchar 10]]]) [:kind [:varchar 10]]])
sql/format)))) sql/format))))
(testing "creating table with columns with default values" (testing "creating table with columns with default values"
(is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL)"] (is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)"]
(-> (create-table :distributors) (-> (create-table :distributors)
(with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]] (with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]]
[:name [:varchar 40] [:not nil]]]) [:name [:varchar 40] [:not nil]]])
sql/format)))) sql/format))))
(testing "creating table with column checks" (testing "creating table with column checks"
(is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"] (is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"]
(-> (create-table :products) (-> (create-table :products)
(with-columns [[:product_no :integer] (with-columns [[:product_no :integer]
[:name :text] [:name :text]
@ -242,39 +228,7 @@
[[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]]) [[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]])
sql/format))))) sql/format)))))
(deftest references-issue-386
(is (= ["CREATE TABLE IF NOT EXISTS user (id VARCHAR(255) NOT NULL PRIMARY KEY, company_id INT NOT NULL, name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY(company_id) REFERENCES company(id))"]
(-> {:create-table [:user :if-not-exists]
:with-columns
[[:id [:varchar 255] [:not nil] [:primary-key]]
[:company-id :int [:not nil]]
[:name [:varchar 255] [:not nil]]
[:password [:varchar 255] [:not nil]]
[:created-time :datetime [:default :CURRENT_TIMESTAMP]]
[:updated-time :datetime [:default :CURRENT_TIMESTAMP]
:on :update :CURRENT_TIMESTAMP]
[[:foreign-key :company-id] [:references :company :id]]]}
(sql/format)))))
(deftest create-table-issue-437
(is (= ["CREATE TABLE bar (did UUID DEFAULT GEN_RANDOM_UUID(), foo_id VARCHAR NOT NULL, PRIMARY KEY(did, foo_id), FOREIGN KEY(foo_id) REFERENCES foo(id) ON DELETE CASCADE)"]
(-> (create-table :bar)
(with-columns
[[:did :uuid [:default [:gen_random_uuid]]]
[:foo-id :varchar [:not nil]]
[[:primary-key :did :foo-id]]
[[:foreign-key :foo-id]
[:references :foo :id]
:on-delete :cascade]])
(sql/format)))))
(deftest over-test (deftest over-test
(testing "simple window statement"
(is (= ["SELECT AVG(salary) OVER w FROM employee WINDOW w AS (PARTITION BY department ORDER BY salary ASC)"]
(sql/format {:select [[[:over [[:avg :salary] :w]]]]
:from :employee
:window [:w {:partition-by :department
:order-by :salary}]}))))
(testing "window function over on select statemt" (testing "window function over on select statemt"
(is (= ["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)"] (is (= ["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)"]
;; honeysql treats over as a function: ;; honeysql treats over as a function:
@ -408,18 +362,6 @@
(modifiers :distinct-on :a :b) (modifiers :distinct-on :a :b)
(sql/format :quoting :ansi)))))) (sql/format :quoting :ansi))))))
(deftest select-agg-order-by-test
(testing "single expression in order by"
(is (= ["SELECT ARRAY_AGG(a ORDER BY x ASC) FROM products"]
(sql/format
{:select [[[:array_agg [:order-by :a :x]]]]
:from :products}))))
(testing "multiple expressions in order by"
(is (= ["SELECT ARRAY_AGG(a ORDER BY x ASC, y DESC, z ASC) FROM products"]
(sql/format
{:select [[[:array_agg [:order-by :a [:x :asc] [:y :desc] :z]]]]
:from :products})))))
(deftest create-extension-test (deftest create-extension-test
;; previously, honeysql required :allow-dashed-names? true ;; previously, honeysql required :allow-dashed-names? true
(testing "create extension" (testing "create extension"
@ -437,21 +379,3 @@
(is (= ["DROP EXTENSION \"uuid-ossp\""] (is (= ["DROP EXTENSION \"uuid-ossp\""]
(-> (drop-extension :uuid-ossp) (-> (drop-extension :uuid-ossp)
(sql/format {:quoted true})))))) (sql/format {:quoted true}))))))
(deftest issue-453-constraint
(testing "standalone constraint"
(is (= ["CREATE TABLE bar (a INTEGER, b INTEGER, CONSTRAINT foo_natural_key UNIQUE (a, b))"]
(-> {:create-table [:bar]
:with-columns
[[:a :integer]
[:b :integer]
[[:constraint :foo_natural_key] :unique [:composite :a :b]]]}
(sql/format)))))
(testing "inline constraint"
(is (= ["CREATE TABLE foo (a INTEGER CONSTRAINT a_pos CHECK(a > 0), b INTEGER, CONSTRAINT a_bigger CHECK(b < a))"]
(-> '{create-table foo
with-columns
((a integer (constraint a_pos) (check (> a 0)))
(b integer)
((constraint a_bigger) (check (< b a))))}
(sql/format))))))

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-2022 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"]
@ -71,14 +68,6 @@
(is (= ["INTERVAL ? DAYS" 30] (is (= ["INTERVAL ? DAYS" 30]
(sut/format-expr [:interval 30 :days])))) (sut/format-expr [:interval 30 :days]))))
(deftest issue-486-interval
(is (= ["INTERVAL '30 Days'"]
(sut/format-expr [:interval "30 Days"]))))
(deftest issue-455-null
(is (= ["WHERE (abc + ?) IS NULL" "abc"]
(sut/format {:where [:= [:+ :abc "abc"] nil]}))))
(deftest where-test (deftest where-test
(is (= ["WHERE id = ?" 1] (is (= ["WHERE id = ?" 1]
(#'sut/format-on-expr :where [:= :id 1])))) (#'sut/format-on-expr :where [:= :id 1]))))
@ -90,62 +79,21 @@
(sut/format {:select [:*] :from [:table] :where (sut/map= {:id 1})} {:quoted true}))) (sut/format {:select [:*] :from [:table] :where (sut/map= {:id 1})} {:quoted true})))
(is (= ["SELECT \"t\".* FROM \"table\" AS \"t\" WHERE \"id\" = ?" 1] (is (= ["SELECT \"t\".* FROM \"table\" AS \"t\" WHERE \"id\" = ?" 1]
(sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {:quoted true}))) (sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" GROUP BY \"foo\""]
(sut/format {:select [:*] :from [:table] :group-by :foo} {:quoted true})))
(is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""] (is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""]
(sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} {:quoted true}))) (sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"] (is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"]
(sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} {:quoted true}))) (sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" ASC"]
(sut/format {:select [:*] :from [:table] :order-by :foo} {:quoted true})))
(is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"] (is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"]
(sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {:quoted true}))) (sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"] (is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"]
(sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true}))) (sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL ? DAYS) < NOW()" 30] (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL ? DAYS) < NOW()" 30]
(sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true}))) (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true})))
(is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL '30 Days') < NOW()"]
(sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval "30 Days"]] [:now]]} {:quoted true})))
(is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1]
(sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}))) (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql})))
(is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4] (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4]
(sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true})))) (sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true}))))
(deftest general-numbered-tests
(is (= ["SELECT * FROM \"table\" WHERE \"id\" = $1" 1]
(sut/format {:select [:*] :from [:table] :where [:= :id 1]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" WHERE \"id\" = $1" 1]
(sut/format {:select [:*] :from [:table] :where (sut/map= {:id 1})}
{:quoted true :numbered true})))
(is (= ["SELECT \"t\".* FROM \"table\" AS \"t\" WHERE \"id\" = $1" 1]
(sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""]
(sut/format {:select [:*] :from [:table] :group-by [:foo :bar]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"]
(sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"]
(sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"]
(sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL $1 DAYS) < NOW()" 30]
(sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL '30 Days') < NOW()"]
(sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval "30 Days"]] [:now]]}
{:quoted true :numbered true})))
(is (= ["SELECT * FROM `table` WHERE `id` = $1" 1]
(sut/format {:select [:*] :from [:table] :where [:= :id 1]}
{:dialect :mysql :numbered true})))
(is (= ["SELECT * FROM \"table\" WHERE \"id\" IN ($1, $2, $3, $4)" 1 2 3 4]
(sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]}
{:quoted true :numbered true}))))
;; issue-based tests ;; issue-based tests
(deftest subquery-alias-263 (deftest subquery-alias-263
@ -180,10 +128,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]
@ -198,49 +144,7 @@
{:values [[1 2] [4 5 6]]}]] {:values [[1 2] [4 5 6]]}]]
:select [:*] :select [:*]
:from [:static]}) :from [:static]})
["WITH static (a, b, c) AS (VALUES (?, ?, NULL), (?, ?, ?)) SELECT * FROM static" 1 2 4 5 6])) ["WITH static (a, b, c) AS (VALUES (?, ?, NULL), (?, ?, ?)) SELECT * FROM static" 1 2 4 5 6])))
(testing "When the expression passed to WITH clause is a string or `ident?` the syntax of WITH clause is `with expr AS ident`"
(is (= (format
{:with [[:ts_upper_bound "2019-08-01 15:23:00"]]
:select [:*]
:from [:hits]
:where [:= :EventDate :ts_upper_bound]})
["WITH ? AS ts_upper_bound SELECT * FROM hits WHERE EventDate = ts_upper_bound" "2019-08-01 15:23:00"]))
(is (= (format
{:with [[:ts_upper_bound :2019-08-01]]
:select [:*]
:from [:hits]
:where [:= :EventDate :ts_upper_bound]})
["WITH 2019_08_01 AS ts_upper_bound SELECT * FROM hits WHERE EventDate = ts_upper_bound"])))
(testing "Mixing the syntax of WITH in the resulting clause"
(is (= (format
{:with [[:ts_upper_bound "2019-08-01 15:23:00"]
[:stuff {: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"])))
(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})
@ -315,20 +219,7 @@
:columns [:baz] :columns [:baz]
:values [[[:array :?vals]]]} :values [[[:array :?vals]]]}
{:params {:vals [1 2 3 4]}}) {:params {:vals [1 2 3 4]}})
["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4])) ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4])))
(testing "typed array"
(is (= (format {:select [[[:array [] :integer]]]})
["SELECT ARRAY[]::INTEGER[]"]))
(is (= (format {:select [[[:array [1 2] :text]]]})
["SELECT ARRAY[?, ?]::TEXT[]" 1 2])))
(testing "array subquery"
(is (= (format {:select [[[:array {:select [:foo] :from [:bar]}]]]})
["SELECT ARRAY(SELECT foo FROM bar)"]))
(is (= (format {:select [[[:array {:select ^{:as :struct} [:foo :bar] :from [:bar]}]]]})
["SELECT ARRAY(SELECT AS STRUCT foo, bar FROM bar)"]))
;; documented subquery workaround:
(is (= (format {:select [[[:'ARRAY {:select [:foo] :from [:bar]}]]]})
["SELECT ARRAY (SELECT foo FROM bar)"]))))
(deftest union-test (deftest union-test
;; UNION and INTERSECT subexpressions should not be parenthesized. ;; UNION and INTERSECT subexpressions should not be parenthesized.
@ -380,11 +271,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 +462,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? #?(:clj (clojure-version)
:default (clojure-version))] :cljs *clojurescript-version*) "1.11")
(when (or (str/starts-with? version "1.12")
(str/starts-with? version "1.11"))
(testing "format can be called with mixed arguments" (testing "format can be called with mixed arguments"
(is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42] (is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42]
(-> {:delete-from :foo (-> {:delete-from :foo
:where [:= :foo.id 42]} :where [:= :foo.id 42]}
(format :dialect :mysql {:pretty true})))))))) (format :dialect :mysql {:pretty true})))))))
(deftest delete-from-test (deftest delete-from-test
(is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42] (is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
@ -609,17 +494,8 @@
(format))))) (format)))))
(deftest truncate-test (deftest truncate-test
(is (= ["TRUNCATE TABLE `foo`"] (is (= ["TRUNCATE `foo`"]
(-> {:truncate :foo} (-> {:truncate :foo}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"]
(-> {:truncate [:foo :continue :identity]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2`"]
(-> {:truncate [[:t1 :t2]]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2` CONTINUE IDENTITY"]
(-> {:truncate [[:t1 :t2] :continue :identity]}
(format {:dialect :mysql}))))) (format {:dialect :mysql})))))
(deftest inlined-values-are-stringified-correctly (deftest inlined-values-are-stringified-correctly
@ -731,7 +607,8 @@ VALUES (?, ?, ?, ?, ?, ?)
:values [["UA502" "Bananas" 105 "1971-07-13" "Comedy" "82 minutes"]]} :values [["UA502" "Bananas" 105 "1971-07-13" "Comedy" "82 minutes"]]}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO films (code, title, did, date_prod, kind) INSERT INTO films
(code, title, did, date_prod, kind)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
" "T_601", "Yojimo", 106, "1961-06-16", "Drama"] " "T_601", "Yojimo", 106, "1961-06-16", "Drama"]
(format {:insert-into :films (format {:insert-into :films
@ -746,7 +623,8 @@ VALUES (?, ?, ?, DEFAULT, ?, ?)
:values [["UA502" "Bananas" 105 [:default] "Comedy" "82 minutes"]]} :values [["UA502" "Bananas" 105 [:default] "Comedy" "82 minutes"]]}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO films (code, title, did, date_prod, kind) INSERT INTO films
(code, title, did, date_prod, kind)
VALUES (?, ?, ?, DEFAULT, ?) VALUES (?, ?, ?, DEFAULT, ?)
" "T_601", "Yojimo", 106, "Drama"] " "T_601", "Yojimo", 106, "Drama"]
(format {:insert-into :films (format {:insert-into :films
@ -757,7 +635,8 @@ VALUES (?, ?, ?, DEFAULT, ?)
(deftest on-conflict-tests (deftest on-conflict-tests
;; these examples are taken from https://www.postgresqltutorial.com/postgresql-upsert/ ;; these examples are taken from https://www.postgresqltutorial.com/postgresql-upsert/
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ON CONSTRAINT customers_name_key ON CONFLICT ON CONSTRAINT customers_name_key
DO NOTHING DO NOTHING
@ -769,7 +648,8 @@ DO NOTHING
:do-nothing true} :do-nothing true}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ON CONFLICT
ON CONSTRAINT customers_name_key ON CONSTRAINT customers_name_key
@ -783,7 +663,8 @@ DO NOTHING
:do-nothing true} :do-nothing true}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name) ON CONFLICT (name)
DO NOTHING DO NOTHING
@ -795,7 +676,8 @@ DO NOTHING
:do-nothing true} :do-nothing true}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name) ON CONFLICT (name)
DO NOTHING DO NOTHING
@ -807,19 +689,21 @@ DO NOTHING
:do-nothing true} :do-nothing true}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ((foo + ?), name, (TRIM(email))) ON CONFLICT (name, email)
DO NOTHING DO NOTHING
" 1] "]
(format {:insert-into :customers (format {:insert-into :customers
:columns [:name :email] :columns [:name :email]
:values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]]
:on-conflict [[:+ :foo 1] :name [:trim :email]] :on-conflict [:name :email]
:do-nothing true} :do-nothing true}
{:pretty true}))) {:pretty true})))
(is (= [" (is (= ["
INSERT INTO customers (name, email) INSERT INTO customers
(name, email)
VALUES ('Microsoft', 'hotline@microsoft.com') VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name) ON CONFLICT (name)
DO UPDATE SET email = EXCLUDED.email || ';' || customers.email DO UPDATE SET email = EXCLUDED.email || ';' || customers.email
@ -867,16 +751,6 @@ ORDER BY id = ? DESC
(format {:insert-into [:table [:a :b :c]] (format {:insert-into [:table [:a :b :c]]
:values [[1 [:default] 3] :default]} :values [[1 [:default] 3] :default]}
{:inline true})))) {:inline true}))))
(testing "map values with default row, no columns"
(is (= ["INSERT INTO table (a, b, c) VALUES (1, 2, 3), DEFAULT, (4, 5, 6)"]
(format {:insert-into :table
:values [{:a 1 :b 2 :c 3} :default {:a 4 :b 5 :c 6}]}
{:inline true}))))
(testing "map values with default column, no columns"
(is (= ["INSERT INTO table (a, b, c) VALUES (1, DEFAULT, 3), DEFAULT"]
(format {:insert-into :table
:values [{:a 1 :b [:default] :c 3} :default]}
{:inline true}))))
(testing "empty values" (testing "empty values"
(is (= ["INSERT INTO table (a, b, c) VALUES ()"] (is (= ["INSERT INTO table (a, b, c) VALUES ()"]
(format {:insert-into [:table [:a :b :c]] (format {:insert-into [:table [:a :b :c]]
@ -893,7 +767,7 @@ ORDER BY id = ? DESC
:order-by [(keyword sort-column)]} :order-by [(keyword sort-column)]}
(format)) (format))
(is false "; not detected in entity!") (is false "; not detected in entity!")
(catch #?(:cljs :default :default Exception) e (catch #?(:clj Throwable :cljs :default) e
(is (:disallowed (ex-data e)))))))) (is (:disallowed (ex-data e))))))))
;; should not produce: ["SELECT foo, bar FROM mytable ORDER BY foo; select * from users"] ;; should not produce: ["SELECT foo, bar FROM mytable ORDER BY foo; select * from users"]
@ -942,13 +816,7 @@ ORDER BY id = ? DESC
{:params {:y [nil]}}))) {:params {:y [nil]}})))
(is (= ["WHERE x IN (?)" nil] (is (= ["WHERE x IN (?)" nil]
(format {:where [:in :x :?y]} (format {:where [:in :x :?y]}
{:params {:y [nil]} :checking :basic}))) {:params {:y [nil]} :checking :basic}))))
(is (= ["WHERE x IN ($2)" nil nil]
(format {:where [:in :x :?y]}
{:params {:y [nil]} :numbered true})))
(is (= ["WHERE x IN ($2)" nil nil]
(format {:where [:in :x :?y]}
{:params {:y [nil]} :checking :basic :numbered true}))))
(testing "IN NULL is flagged in strict mode" (testing "IN NULL is flagged in strict mode"
(is (thrown-with-msg? ExceptionInfo #"does not match" (is (thrown-with-msg? ExceptionInfo #"does not match"
(format {:where [:in :x [nil]]} (format {:where [:in :x [nil]]}
@ -1090,19 +958,6 @@ ORDER BY id = ? DESC
(is (= ["SELECT `A``B`"] (sut/format {:select (keyword "A`B")} {:dialect :mysql}))) (is (= ["SELECT `A``B`"] (sut/format {:select (keyword "A`B")} {:dialect :mysql})))
(is (= ["SELECT \"A\"\"B\""] (sut/format {:select (keyword "A\"B")} {:dialect :oracle})))) (is (= ["SELECT \"A\"\"B\""] (sut/format {:select (keyword "A\"B")} {:dialect :oracle}))))
(deftest issue-407-temporal
(is (= ["SELECT f.* FROM foo FOR SYSTEM_TIME ALL AS f WHERE f.id = ?" 1]
(sut/format {:select :f.* :from [[:foo :f :for :system-time :all]] :where [:= :f.id 1]})))
(is (= ["SELECT * FROM foo FOR SYSTEM_TIME ALL WHERE id = ?" 1]
(sut/format {:select :* :from [[:foo :for :system-time :all]] :where [:= :id 1]}))))
(deftest issue-421-mysql-replace-into
(is (= ["INSERT INTO table VALUES (?, ?, ?)" 1 2 3]
(sut/format {:insert-into :table :values [[1 2 3]]})))
(is (= ["REPLACE INTO table VALUES (?, ?, ?)" 1 2 3]
(sut/format {:replace-into :table :values [[1 2 3]]}
{:dialect :mysql :quoted false}))))
(deftest issue-422-quoting (deftest issue-422-quoting
;; default quote if strange entity: ;; default quote if strange entity:
(is (= ["SELECT A, \"B C\""] (sut/format {:select [:A (keyword "B C")]}))) (is (= ["SELECT A, \"B C\""] (sut/format {:select [:A (keyword "B C")]})))
@ -1114,417 +969,3 @@ ORDER BY id = ? DESC
;; don't quote if quoting disabled (illegal SQL): ;; don't quote if quoting disabled (illegal SQL):
(is (= ["SELECT A, B C"] (sut/format {:select [:A (keyword "B C")]} (is (= ["SELECT A, B C"] (sut/format {:select [:A (keyword "B C")]}
{:quoted false})))) {:quoted false}))))
(deftest issue-434-case-quoting
(is (= ["SELECT ARRAY (SELECT \"oid\" FROM \"pg_proc\" WHERE \"proname\" LIKE 'bytea%')"]
(sut/format {:select [[[:'ARRAY {:select :oid :from :pg_proc :where [:like :proname [:inline "bytea%"]]}]]]} :quoted true))))
(deftest issue-456-format-expr
(is (= ["`x` + ?" 1]
(sut/format [:+ :x 1] {:dialect :mysql}))))
(deftest issue-459-variadic-ops
(sut/register-op! :op)
(is (= ["SELECT a"] ; not unary!
(sut/format {:select [[[:op :a]]]})))
(is (= ["SELECT a OP b"]
(sut/format {:select [[[:op :a :b]]]})))
(is (= ["SELECT a OP b OP c"]
(sut/format {:select [[[:op :a :b :c]]]}))))
(deftest issue-461-unary-ops
(is (= ["SELECT TRUE"]
(sut/format {:select [[[:and true]]]})))
(is (= ["SELECT TRUE"]
(sut/format {:select [[[:or true]]]})))
(is (= ["SELECT ?" 1]
(sut/format {:select [[[:* 1]]]})))
(is (= ["SELECT TRUE AND a AND b"]
(sut/format {:select [[[:and true :a :b]]]})))
(is (= ["SELECT TRUE OR a OR b"]
(sut/format {:select [[[:or true :a :b]]]})))
(is (= ["SELECT ? * ? * ?" 1 2 3]
(sut/format {:select [[[:* 1 2 3]]]})))
;; but these three genuinely are unary:
(is (= ["SELECT + ?" 1]
(sut/format {:select [[[:+ 1]]]})))
(is (= ["SELECT - ?" 1]
(sut/format {:select [[[:- 1]]]})))
(is (= ["SELECT ~ ?" 1] ; bitwise negation
(sut/format {:select [[[(keyword "~") 1]]]})))
;; and can still be used as variadic:
(is (= ["SELECT ? + ?" 1 2]
(sut/format {:select [[[:+ 1 2]]]})))
(is (= ["SELECT ? - ?" 1 2]
(sut/format {:select [[[:- 1 2]]]})))
(is (= ["SELECT ? ~ ?" "a" "b"] ; regex op
(sut/format {:select [[[(keyword "~") "a" "b"]]]}))))
(deftest issue-471-interspersed-kws
(testing "overlay"
(is (= ["SELECT OVERLAY(foo PLACING ? FROM ? FOR ?)"
"bar" 3 4]
(sut/format {:select [[[:overlay :foo :!placing "bar" :!from 3 :!for 4]]]}))))
(testing "position"
(is (= ["SELECT POSITION(? IN bar)" "foo"]
(sut/format {:select [[[:position "foo" :!in :bar]]]}))))
(testing "trim"
(is (= ["SELECT TRIM(LEADING FROM bar)"]
(sut/format {:select [[[:trim :!leading :!from :bar]]]})))
(is (= ["SELECT TRIM(LEADING FROM bar)"]
(sut/format {:select [[[:trim :!leading-from :bar]]]}))))
(testing "extract"
(is (= ["SELECT EXTRACT(CENTURY FROM TIMESTAMP '2000-12-16 12:21:13')"]
(sut/format {:select [[[:extract :!century :!from
:!timestamp [:inline "2000-12-16 12:21:13"]]]]}))))
(testing "xmlelement"
(is (= ["SELECT XMLELEMENT(NAME \"foo$bar\", XMLATTRIBUTES('xyz' AS \"a&b\"))"]
(sut/format {:select [[[:xmlelement :!name :foo$bar
[:xmlattributes [:inline "xyz"] :!as :a&b]]]]})))))
(deftest issue-474-dot-selection
(testing "basic dot 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})))
(is (= ["SELECT (v).*, (w).x, (Y(z)).*"]
(sut/format '{select (((get-in v *))
((get-in w x))
((get-in (y z) *)))})))
(is (= ["SELECT (`v`).*, (`w`).`x`, (Y(`z`)).*"]
(sut/format '{select (((get-in v *))
((get-in w x))
((get-in (y z) *)))}
{:dialect :mysql})))))
(deftest issue-570-snowflake-dot-selection
(testing "basic colon selection"
(is (= ["SELECT a:b, c:d, a:d.x, a:d.x.y"]
(let [t :a c :d]
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]]
[[:.:. t c :x]] [[:.:. t c :x :y]]]}))))
(is (= ["SELECT [a]:[b], [c]:[d], [a]:[d].[x]"]
(let [t :a c :d]
(sut/format {:select [[[:.:. t :b]] [[:.:. :c c]] [[:.:. t c :x]]]}
{:dialect :sqlserver})))))
(testing "basic field selection from composite"
(is (= ["SELECT (v):*, (w):x, (Y(z)):*"]
(sut/format '{select (((.:. (nest v) *))
((.:. (nest w) x))
((.:. (nest (y z)) *)))})))
(is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"]
(sut/format '{select (((.:. (nest v) *))
((.:. (nest w) x))
((.:. (nest (y z)) *)))}
{:dialect :mysql}))))
(testing "bracket selection"
(is (= ["SELECT a['b'], c['b'], a['d'].x, a:e[0].name"]
(sut/format {:select [[[:at :a [:inline "b"]]]
[[:at :c "b"]]
[[:at :a [:inline "d"] :x]]
[[:.:. :a [:at :e [:inline 0]] :name]]]})))
(is (= ["SELECT a[?].name" 0]
(sut/format '{select (((at a (lift 0) name)))})))
;; sanity check, compare with get-in:
(is (= ["SELECT (a)[?].name" 0]
(sut/format '{select (((get-in a (lift 0) name)))})))
(is (= ["SELECT (a)['b'], (c)['b'], (a)['d'].x, a:(e)[0].name"]
(sut/format {:select [[[:get-in :a [:inline "b"]]]
[[:get-in :c "b"]]
[[:get-in :a [:inline "d"] :x]]
[[:.:. :a [:get-in :e [:inline 0]] :name]]]})))))
(deftest issue-476-raw
(testing "single argument :raw"
(is (= ["@foo := 42"]
(sut/format [:raw "@foo := 42"])))
(is (= ["@foo := 42"]
(sut/format [:raw ["@foo := 42"]])))
(is (= ["@foo := 42"]
(sut/format [:raw ["@foo := " 42]])))
(is (= ["@foo := (?)" 42]
(sut/format [:raw ["@foo := " [42]]])))
(is (= ["@foo := MYFUNC(?)" 42]
(sut/format [:raw ["@foo := " [:myfunc 42]]]))))
(testing "multi-argument :raw"
(is (= ["@foo := 42"]
(sut/format [:raw "@foo := " 42])))
(is (= ["@foo := (?)" 42]
(sut/format [:raw "@foo := " [42]])))
(is (= ["@foo := MYFUNC(?)" 42]
(sut/format [:raw "@foo := " [:myfunc 42]])))))
(deftest issue-483-join
(testing "single nested join"
(is (= ["SELECT * FROM tbl1 LEFT JOIN (tbl2 INNER JOIN tbl3 USING (common_column)) ON (tbl2.col2 = tbl1.col2) AND (tbl3.col3 = tbl1.col3)"]
(-> {:select :*
:from :tbl1
:left-join [[[:join :tbl2 {:join [:tbl3 [:using [:common_column]]]}]]
[:and
[:= :tbl2.col2 :tbl1.col2]
[:= :tbl3.col3 :tbl1.col3]]]}
(sut/format)))))
(testing "multiple nested join"
(is (= ["SELECT * FROM tbl1 LEFT JOIN (tbl2 INNER JOIN tbl3 USING (common_column) RIGHT JOIN tbl4 USING (id)) ON (tbl2.col2 = tbl1.col2) AND (tbl3.col3 = tbl1.col3)"]
(-> {:select :*
:from :tbl1
:left-join [[[:join :tbl2
{:join [:tbl3 [:using [:common_column]]]}
{:right-join [:tbl4 [:using :id]]}]]
[:and
[:= :tbl2.col2 :tbl1.col2]
[:= :tbl3.col3 :tbl1.col3]]]}
(sut/format)))))
(testing "special syntax example"
(is (= ["INNER JOIN (tbl1 LEFT JOIN tbl2 USING (id))"]
(sut/format {:join [[[:join :tbl1 {:left-join [:tbl2 [:using :id]]}]]]})))))
#?(:clj
(deftest issue-495-formatv
(is (= ["SELECT * FROM foo WHERE x = ?" 13]
(let [v 13 x 42]
(assert x) ; just to mark it as used
(sut/formatv [v] '{select * from foo where (= x v)}))))))
(deftest issue-496-overriding
(is (= ["INSERT INTO table (a, b) OVERRIDING SYSTEM VALUE VALUES (?, ?)" 1 2]
(sut/format {:insert-into [{:overriding-value :system} :table]
:columns [:a :b]
:values [[1 2]]})))
(is (= ["INSERT INTO table (a, b) OVERRIDING USER VALUE VALUES (?, ?)" 1 2]
(sut/format {:insert-into [{:overriding-value :user} :table [:a :b]]
:values [[1 2]]})))
(is (= ["INSERT INTO table (a, b) OVERRIDING SYSTEM VALUE VALUES (?, ?)" 1 2]
(sut/format {:insert-into [{:overriding-value :system} :table]
:values [{:a 1 :b 2}]}))))
(deftest issue-497-alias
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:raw "\"some-alias\""]]]})))
;; likely illegal SQL, but shows quoted keyword escaping the -/_ replace:
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY some-alias ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[:'some-alias]]})))
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias "some-alias"]]]})))
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY some_alias ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias :some-alias]]]})))
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias :'some-alias]]]})))
(is (= ["SELECT column_name AS \"some-alias\" FROM b ORDER BY \"some-alias\" ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias "some-alias"]]]})))
(is (= ["SELECT \"column-name\" AS \"some-alias\" FROM \"b\" ORDER BY ? ASC"
"some-alias"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by ["some-alias"]}
{:quoted true})))
(is (= ["SELECT `column-name` AS `some-alias` FROM `b` ORDER BY `some-alias` ASC"]
(sut/format {:select [[:column-name "some-alias"]]
:from :b
:order-by [[[:alias "some-alias"]]]}
{:dialect :mysql}))))
(deftest output-clause-post-501
(sut/register-clause! :output :select :values)
(is (= ["INSERT INTO foo (bar) OUTPUT inserted.* VALUES (?)" 1]
(sut/format {:insert-into :foo :output [:inserted.*] :values [{:bar 1}]})))
(is (= ["INSERT INTO foo (bar) OUTPUT inserted.* VALUES (?)" 1]
(sut/format {:insert-into :foo :columns [:bar] :output [:inserted.*] :values [[1]]}))))
(deftest at-time-zone-503
(is (= ["SELECT foo AT TIME ZONE 'UTC'"]
(sut/format {:select [[[:at-time-zone :foo "UTC"]]]})))
(is (= ["SELECT foo AT TIME ZONE 'UTC'"]
(sut/format {:select [[[:at-time-zone :foo :UTC]]]})))
(is (= ["SELECT FOO(bar) AT TIME ZONE 'UTC'"]
(sut/format {:select [[[:at-time-zone [:foo :bar] :UTC]]]}))))
(deftest issue-512
(testing "select with metadata"
(is (= ["SELECT DISTINCT * FROM table"]
(sut/format {:select-distinct [:*] :from [:table]})))
(is (= ["SELECT DISTINCT * FROM table"]
(sut/format {:select ^{:distinct true} [:*] :from [:table]})))
(is (= ["SELECT DISTINCT * FROM table"]
(sut/format {:select ^:distinct [:*] :from [:table]})))))
(deftest issue-515
(testing ":always-quoting option"
(is (= ["SELECT foo FROM table"]
(sut/format '{select foo from table})))
(is (= ["SELECT \"foo\" FROM \"table\""]
(sut/format '{select foo from table}
{:quoted-always #"^(foo|table)$"})))
(is (= ["SELECT \"foo\" FROM \"table\""]
(sut/format '{select foo from table}
{:quoted-always #"^(foo|table)$"
:quoted false})))
(is (= ["SELECT \"foo\" FROM table"]
(sut/format '{select foo from table}
{:quoted-always #"^(foo)$"
:quoted false})))))
(deftest issue-520
(testing ":inline with a single argument"
(is (= ["SELECT 42 AS x"]
(sut/format '{select [[[inline 42] x]]}))))
(testing ":inline with multiple arguments"
(is (= ["SELECT DATE '2024-01-06' AS x"]
(sut/format '{select [[[inline DATE "2024-01-06"] x]]}))))
(testing ":inline with a parameter"
(is (= ["SELECT 42 AS x"]
(sut/format '{select [[[inline [param foo]] x]]}
{:params {'foo 42}}))))
(testing ":inline with a sequence"
(is (= ["SELECT ('a', 'b', 'c') AS x"]
(sut/format '{select [[[inline ["a" "b" "c"]] x]]}))))
(testing ":inline with a lifted sequence"
(is (= ["SELECT ['a', 'b', 'c'] AS x"]
(sut/format '{select [[[inline [lift ["a" "b" "c"]]] x]]})))))
(deftest issue-522
(testing "from with metadata"
(is (= ["SELECT * FROM table WITH (HINT)"]
(sut/format {:select [:*] :from [^:hint [:table]]})))
;; hash map (metadata) is unordered:
(is (or (= ["SELECT * FROM table WITH (ABC, DEF)"]
(sut/format {:select [:*] :from [^:abc ^:def [:table]]}))
(= ["SELECT * FROM table WITH (DEF, ABC)"]
(sut/format {:select [:*] :from [^:abc ^:def [:table]]}))))
(is (or (= ["SELECT * FROM table WITH (ABC, DEF)"]
(sut/format {:select [:*] :from [^{:abc true :def true} [:table]]}))
(= ["SELECT * FROM table WITH (DEF, ABC)"]
(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
;; partial (incorrect!) workaround for #407:
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})
;; correct version:
(sut/format {:select :f.* :from [[:foo :f :for :system-time]] :where [:= :f.id 1]})
(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,10 +0,0 @@
(ns honey.unhashable-test
(:require [clojure.test :refer [deftest is]]
[honey.sql :as sut]))
(deftest unhashable-value-509
(let [unhashable (reify Object
(toString [_] "unhashable")
(hashCode [_] (throw (ex-info "Unsupported" {}))))]
(is (= ["INSERT INTO table VALUES (?)" unhashable]
(sut/format {:insert-into :table :values [[unhashable]]})))))

View file

@ -1,51 +0,0 @@
;; copyright (c) 2023-2024 sean corfield, all rights reserved
(ns honey.union-test
(:refer-clojure :exclude [format])
(:require [clojure.test :refer [deftest is]]
[honey.sql :as sut]))
(deftest issue-451
(is (= [(str "SELECT ids.id AS id"
" FROM ((SELECT dimension.human_readable_field_id AS id"
" FROM dimension AS dimension"
" WHERE (dimension.field_id = ?) AND (dimension.human_readable_field_id IS NOT NULL)"
" LIMIT ?)"
" UNION"
" (SELECT dest.id AS id"
" FROM field AS source"
" LEFT JOIN table AS table ON source.table_id = table.id"
" LEFT JOIN field AS dest ON dest.table_id = table.id"
" WHERE (source.id = ?) AND (source.semantic_type IN (?)) AND (dest.semantic_type IN (?))"
" LIMIT ?)) AS ids"
" LIMIT ?")
1
1
1
"type/PK"
"type/Name"
1
1]
(-> {:select [[:ids.id :id]]
:from [[{:union
[{:nest
{:select [[:dimension.human_readable_field_id :id]]
:from [[:dimension :dimension]]
:where [:and
[:= :dimension.field_id 1]
[:not= :dimension.human_readable_field_id nil]]
:limit 1}}
{:nest
{:select [[:dest.id :id]]
:from [[:field :source]]
:left-join [[:table :table] [:= :source.table_id :table.id] [:field :dest] [:= :dest.table_id :table.id]]
:where [:and
[:= :source.id 1]
[:in :source.semantic_type #{"type/PK"}]
[:in :dest.semantic_type #{"type/Name"}]]
:limit 1}}]}
:ids]]
:limit 1}
(sut/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]))))