[Git][clojure-team/honeysql-clojure][upstream] New upstream version 1.0.461
Jérôme Charaoui (@lavamind)
gitlab at salsa.debian.org
Mon Jul 4 18:37:44 BST 2022
Jérôme Charaoui pushed to branch upstream at Debian Clojure Maintainers / honeysql-clojure
Commits:
f635aa25 by Jérôme Charaoui at 2022-07-04T11:54:38-04:00
New upstream version 1.0.461
- - - - -
21 changed files:
- + .circleci/config.yml
- + .clj-kondo/config.edn
- + .github/FUNDING.yml
- + .github/workflows/test.yml
- .gitignore
- − .travis.yml
- + CHANGELOG.md
- − CHANGES.md
- README.md
- + deps.edn
- + pom.xml
- − project.clj
- resources/data_readers.clj
- + run-tests.sh
- src/honeysql/core.cljc
- src/honeysql/format.cljc
- src/honeysql/helpers.cljc
- src/honeysql/types.cljc
- test/honeysql/core_test.cljc
- test/honeysql/format_test.cljc
- − test/honeysql/test.cljs
Changes:
=====================================
.circleci/config.yml
=====================================
@@ -0,0 +1,28 @@
+version: 2
+jobs:
+ build:
+ working_directory: ~/honeysql
+ docker:
+ - image: circleci/clojure:openjdk-11-tools-deps-1.10.0.442
+ steps:
+ - checkout
+ - restore_cache:
+ key: honeysql-{{ checksum "deps.edn" }}
+ - run:
+ name: Install Node
+ command: sudo apt-get update && sudo apt-get install -y nodejs
+ - run:
+ name: Add Node symlink
+ command: sudo ln -s /usr/bin/js /usr/bin/node
+ - run:
+ name: Download Dependencies
+ command: clojure -Spath -R:test:runner:cljs-runner:eastwood:readme && clojure -Spath -A:1.7 && clojure -Spath -A:1.8 && clojure -Spath -A:1.9
+ - save_cache:
+ paths:
+ - ~/.m2
+ - ~/.gitlibs
+ - ~/node_modules
+ key: honeysql-{{ checksum "deps.edn" }}
+ - run:
+ name: Run all the tests
+ command: sh run-tests.sh all
=====================================
.clj-kondo/config.edn
=====================================
@@ -0,0 +1,2 @@
+{:lint-as
+ {honeysql.helpers/defhelper clojure.core/defn}}
=====================================
.github/FUNDING.yml
=====================================
@@ -0,0 +1 @@
+github: seancorfield
=====================================
.github/workflows/test.yml
=====================================
@@ -0,0 +1,22 @@
+name: Clojure CI
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java: [ '8', '11', '14' ]
+ steps:
+ - uses: actions/checkout at v2
+ - name: Setup Java
+ uses: actions/setup-java at v1
+ with:
+ java-version: ${{ matrix.java }}
+ - name: Setup Clojure
+ uses: DeLaGuardo/setup-clojure at master
+ with:
+ tools-deps: '1.10.1.754'
+ - name: Run Tests
+ run: sh run-tests.sh all
=====================================
.gitignore
=====================================
@@ -5,15 +5,17 @@
/lib
/classes
/checkouts
-pom.xml*
+/dist
*.jar
*.class
-.lein-deps-sum
-.lein-failures
-.lein-plugins
-.lein-repl-history
+.cpcache
+.clj-kondo/.cache
+.eastwood
+.lsp
.nrepl-port
+.socket-repl-port
.classpath
.project
.nrepl-port
bin
+/cljs-test-runner-out
=====================================
.travis.yml deleted
=====================================
@@ -1,3 +0,0 @@
-language: clojure
-lein: lein2
-script: lein do check, test
=====================================
CHANGELOG.md
=====================================
@@ -0,0 +1,158 @@
+# Changes
+
+* 1.0.461 -- 2021-02-22
+ * **Fix #299 potential SQL injection vulnerability.**
+ * Fix/Improve `merge-where` (and `merge-having`) behavior. #282 via #283 (@camsaul)
+
+* 1.0.444 -- 2020-05-29
+ * Fix #259 so column names are always unqualified in inserts. (@jrdoane)
+ * Fix #257 by adding support for `cross-join` / `merge-cross-join` / `:cross-join`. (@dcj)
+ * Switch dev/test pipeline to use CLI/`deps.edn` instead of Leiningen. Also add CI vi both CircleCI and GitHub Actions.
+ * Switch to MAJOR.MINOR.COMMITS versioning.
+ * Remove macrovich dependency as this is no longer needed with modern ClojureScript.
+ * Add mention of `next.jdbc` everywhere `clojure.java.jdbc` was mentioned.
+
+* 0.9.10 -- 2020-03-06
+ * Fix #254 #255 by adding support for `except`. (@ted-coakley-otm)
+ * Fix #253 properly by supporting `false` as well. (@ted-coakley-otm)
+ * Add cljs testing to `deps.edn`, also multi-version clj testing and new `readme` testing.
+
+* 0.9.9 -- 2020-03-02
+ * Fix #253 by supporting non-sequential join expressions.
+
+* 0.9.8 -- 2019-09-07
+ * Fix #249 by adding `honeysql.format/*namespace-as-table?*` and `:namespace-as-table?` option to `format`. (@seancorfield)
+
+* 0.9.7 -- 2019-09-07
+ * Fix #248 by treating alias as "not a subquery" when generating SQL for it. (@seancorfield)
+ * Fix #247 by reverting #132 / #131 so the default behavior is friendlier for namespace-qualified keywords used for table and column names, but adds `honeysql.format/*allow-namespaced-names?*` to restore the previous behavior. A `:allow-namespaced-names?` option has been adding to `format` to set this more easily. (@seancorfield)
+ * Fix #235 by adding `set0` (`:set0`) and `set1` (`:set1`) variants of `sset` (`:set`) to support different placements of `SET` (before `FROM` or after `JOIN` respectively) that different databases require. See also #200. (@seancorfield)
+ * Fix #162 by adding `composite`/`:composite` constructor for values. (@seancorfield)
+ * Fix #139 by checking arguments to `columns`/`merge-columns` and throwing an exception if a single collection is supplied (instead of varargs). (@seancorfield)
+ * Fix #128 by adding `truncate` support. (@seancorfield)
+ * Fix #99 by adding a note to the first use of `select` in the README that column names can be keywords or symbols but not strings. (@seancorfield)
+
+* 0.9.6 -- 2019-09-24
+ * Filter `nil` conditions out of `where`/`merge-where`. Fix #246. (@seancorfield)
+ * Fix reflection warning introduced in 0.9.5 (via PR #237).
+
+* 0.9.5 -- 2019-09-07
+ * Support JDK11 (update Midje). PR #238. (@camsaul)
+ * Support Turkish user.language. PR #237. (@camsaul)
+ * `format-predicate` now accepts `parameterizer` as a named argument (default `:jdbc`) to match `format`. PR #234. (@glittershark)
+
+* 0.9.4 -- 2018-10-01
+ * `#sql/inline nil` should produce `NULL`. Fix #221. (@seancorfield)
+ * `#sql/inline :kw` should produce `"kw"`. Fix #224 via PR #225. (@vincent-dm) Note: this introduces a new protocol, `Inlinable`, which controls inline value rendering, and changes the behavior of `#sql/inline :foo/bar` to produce just `"bar"` (where it was probably invalid SQL before).
+ * Alias expressions `[:col :alias]` are now checked to have exactly two elements. Fix #226. (@seancorfield)
+ * Allow `where` and `merge-where` to be given no predicates. Fix #228 and PR #230. (@seancorfield, @arichiardi)
+ * `as` alias is no longer split during quoting. Fix #221 and PR #231. (@gws)
+
+## Earlier releases
+
+Not all of these releases were tagged on GitHub and none of them have release notes on GitHub. Releases prior to 0.5.0 were not documented (although some were tagged on GitHub).
+
+* 0.9.3
+ * Support parameters in `#sql/raw`. Fix #219. (@seancorfield)
+ * Add examples of table/column aliases to the README. Fix #215. (@seancorfield)
+ * Refactor parameterizer to use multimethods. PR #214. (@xlevus)
+ * Add examples of `raw` and `inline` to the README. Fix #213. (@seancorfield)
+ * Add `register-parameterizer` for custom parameterizers. PR #209. (@juvenn)
+ * Change `set` priority to after `join`. Fix #200. (@michaelblume)
+ * Switch dependency version checker to deps.co. PR #197. (@danielcompton)
+ * Support `join ... using( ... )`. Fix #188, PR #201. (@vincent-dm)
+ * Add multi-version testing for Clojure 1.7, 1.8, 1.9, 1.10 (master) (@seancorfield)
+ * Bring all dependencies up-to-date. (@seancorfield)
+ * Add `run-tests.sh` to make it easier to run the same tests manually that run on Travis-CI. (@seancorfield)
+ * Add `deps.edn` to support `clj`/`tools.deps.alpha`. (@seancorfield)
+ * Expose `#sql/inline` data reader. (@seancorfield)
+
+* 0.9.2
+ * Remove `nil` `:and` arguments for where. Fix #203. (@michaelblume)
+ * Fix nested `select` formatting. Fix #198. (@visibletrap)
+ * Limit value context to sequences in value positions. (@xiongtx)
+ * Avoid wrapping QUERY with parens while formatting `INSERT INTO ... QUERY`. (@emidln)
+ * Allow for custom name-transform-fn. Fix #193. (@madvas)
+ * Add :intersect to default-clause-priorities. (@kenfehling)
+ * Add `:parameterizer` `:none` for skipping `next.jdbc` or `clojure.java.jdbc` parameter generation. (@arichiardi)
+ * Add ClojureScript self-host support. (@arichiardi)
+
+* 0.9.1
+ * Add helper to inline values/prevent parameterization (@michaelblume)
+
+* 0.9.0 --
+ * BREAKING CHANGE: Some tuples used as values no longer work. See #168..
+ * Reprioritize WITH wrt UNION and UNION ALL (@emidln)
+ * Helpers for `:with` and `:with-recursive` clauses (@enaeher)
+ * Ensure sequences act as function invocations when in value position (@joodie)
+ * Correct generated arglist for helpers defined with defhelper (@michaelblume)
+ * Don't depend on map iteration order, fix bug with multiple map types (@tomconnors)
+ * Don't throw away namespace portion of keywords (@jrdoane)
+ * Update CLJS dependencies (@michaelblume)
+ * Add helpers for :with and :with-recursive clauses (@enaher)
+
+* 0.8.2
+ * Don't parenthesize the subclauses of a UNION, UNION ALL, or INTERSECT clause. (@rnewman)
+
+* 0.8.1
+ * Add priority for union/union-all (@seancorfield)
+
+* 0.8.0
+ * Get arglists right for generated helpers (@camsaul, @michaelblume)
+ * Allow HoneySQL to be used from Clojurescript (@rnewman, @michaelblume)
+ * BREAKING CHANGE: HoneySQL now requires Clojure 1.7.0 or above.
+
+* 0.7.0
+ * Parameterize numbers, properly handle NaN, Infinity, -Infinity (@akhudek)
+ * Fix lock example in README (@michaelblume)
+ * Allow joins without a predicate (@stuarth)
+ * Escape quotes in quoted identifiers (@csummers)
+ * Add support for INTERSECT (@jakemcc)
+ * Upgrade Clojure dependency (@michaelblume)
+
+* 0.6.3
+ * Fix bug when SqlCall/SqlRaw object is first argument to another helper (@MichaelBlume)
+ * Add support for :intersect clause (@jakemcc)
+
+* 0.6.2
+ * Support column names in :with clauses (@emidln)
+ * Support preserving dashes in quoted names (@jrdoane)
+ * Document correct use of the :union clause (@dball)
+ * Tests for :union and :union-all (@dball)
+ * Add fn-handler for CASE statement (@loganlinn)
+ * Build/test with Clojure 1.7 (@michaelblume)
+ * Refactors for clarity (@michaelblume)
+
+* 0.6.1
+ * Define parameterizable protocol on nil (@dball)
+
+* 0.6.0
+ * Convert seq param values to literal sql param lists (@dball)
+ * Apply seq to sets when converting to sql (@dball)
+
+* 0.5.3
+ * Support exists syntax (@cddr)
+ * Support locking selects (@dball)
+ * Add sql array type and reader literal (@loganmhb)
+ * Allow user to specify where in sort order NULLs fall (@mishok13)
+
+* 0.5.2
+ * Add value type to inhibit interpreting clojure sequences as subqueries (@MichaelParam)
+ * Improve documentation (@hlship)
+ * Add type hints to avoid reflection (@MichaelBlume)
+ * Allow database-specific query parameterization (@icambron, @MichaelBlume)
+
+* 0.5.1
+ * Add :url to project.clj (@MichaelBlume)
+
+* 0.5.0
+ * Support basic common table expressions (:with, :with-recursive) (@akhudek)
+ * Make clause order extensible (@MichaelBlume)
+ * Support extended INSERT INTO...SELECT syntax (@ddellacosta)
+ * Update clojure version to 1.6.0 (@MichaelBlume)
+ * Implement ToSql on Object, vastly improving performance (@MichaelBlume)
+ * Support CAST(foo AS type) (@senior)
+ * Support postgres-native parameters (@icambron)
+ * Support :full-join (@justindell)
+ * Expose :arglist metadata in defhelper (@hlship)
+ * Improvements to the documentation, especially showing some recently added features, such as inserts and updates.
=====================================
CHANGES.md deleted
=====================================
@@ -1,79 +0,0 @@
-## 0.8.2
-
-* Don't parenthesize the subclauses of a UNION, UNION ALL, or INTERSECT clause. (@rnewman)
-
-## 0.8.1
-
-* Add priority for union/union-all (@seancorfield)
-
-## 0.8.0
-
-* Get arglists right for generated helpers (@camsaul, @michaelblume)
-* Allow HoneySQL to be used from Clojurescript (@rnewman, @michaelblume)
-* BREAKING CHANGE: HoneySQL now requires Clojure 1.7.0 or above.
-
-## 0.7.0
-
-* Parameterize numbers, properly handle NaN, Infinity, -Infinity (@akhudek)
-* Fix lock example in README (@michaelblume)
-* Allow joins without a predicate (@stuarth)
-* Escape quotes in quoted identifiers (@csummers)
-* Add support for INTERSECT (@jakemcc)
-* Upgrade Clojure dependency (@michaelblume)
-
-## 0.6.3
-
-Fix bug when SqlCall/SqlRaw object is first argument to another helper (@MichaelBlume)
-
-* Add support for :intersect clause (@jakemcc)
-
-## 0.6.2
-
-Support column names in :with clauses (@emidln)
-Support preserving dashes in quoted names (@jrdoane)
-Document correct use of the :union clause (@dball)
-Tests for :union and :union-all (@dball)
-Add fn-handler for CASE statement (@loganlinn)
-Build/test with Clojure 1.7 (@michaelblume)
-Refactors for clarity (@michaelblume)
-
-## 0.6.1
-
-* Define parameterizable protocol on nil (@dball)
-
-## 0.6.0
-
-* Convert seq param values to literal sql param lists (@dball)
-* Apply seq to sets when converting to sql (@dball)
-
-## 0.5.3
-
-* Support exists syntax (@cddr)
-* Support locking selects (@dball)
-* Add sql array type and reader literal (@loganmhb)
-* Allow user to specify where in sort order NULLs fall (@mishok13)
-
-## 0.5.2
-
-* Add value type to inhibit interpreting clojure sequences as subqueries (@MichaelParam)
-* Improve documentation (@hlship)
-* Add type hints to avoid reflection (@MichaelBlume)
-* Allow database-specific query parameterization (@icambron, @MichaelBlume)
-
-## 0.5.1
-
-* Add :url to project.clj (@MichaelBlume)
-
-## 0.5.0
-
-* Support basic common table expressions (:with, :with-recursive) (@akhudek)
-* Make clause order extensible (@MichaelBlume)
-* Support extended INSERT INTO...SELECT syntax (@ddellacosta)
-* Update clojure version to 1.6.0 (@MichaelBlume)
-* Implement ToSql on Object, vastly improving performance (@MichaelBlume)
-* Support CAST(foo AS type) (@senior)
-* Support postgres-native parameters (@icambron)
-* Support :full-join (@justindell)
-* Expose :arglist metadata in defhelper (@hlship)
-* Improvements to the documentation, especially showing some recently added features, such as inserts
- and updates.
=====================================
README.md
=====================================
@@ -1,41 +1,85 @@
-# Honey SQL
+# Honey SQL [](https://circleci.com/gh/seancorfield/honeysql/tree/develop)
SQL as Clojure data structures. Build queries programmatically -- even at runtime -- without having to bash strings together.
## Build
-[](https://travis-ci.org/jkk/honeysql)
-[](http://jarkeeper.com/jkk/honeysql)
+The latest versions on Clojars and on cljdoc:
-## Leiningen Coordinates
+[](https://clojars.org/honeysql) [](https://cljdoc.org/d/honeysql/honeysql/CURRENT)
-[](http://clojars.org/honeysql)
+This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
+
+## Note on code samples
+
+All sample code in this README is automatically run as a unit test using
+[seancorfield/readme](https://github.com/seancorfield/readme).
+
+Note that while some of these samples show pretty-printed SQL, this is just for
+README readability; honeysql does not generate pretty-printed SQL.
+The `#sql/regularize` directive tells the test-runner to ignore the extraneous
+whitespace.
## Usage
-```clj
+```clojure
(require '[honeysql.core :as sql]
- '[honeysql.helpers :refer :all])
+ '[honeysql.helpers :refer :all :as helpers])
```
Everything is built on top of maps representing SQL queries:
-```clj
+```clojure
(def sqlmap {:select [:a :b :c]
- :from [:foo]
- :where [:= :f.a "baz"]})
+ :from [:foo]
+ :where [:= :f.a "baz"]})
```
-`format` turns maps into `clojure.java.jdbc`-compatible, parameterized SQL:
+Column names can be provided as keywords or symbols (but not strings -- HoneySQL treats strings as values that should be lifted out of the SQL as parameters).
-```clj
+### `format`
+
+`format` turns maps into `next.jdbc`-compatible (and `clojure.java.jdbc`-compatible), parameterized SQL:
+
+```clojure
(sql/format sqlmap)
-=> ["SELECT a, b, c FROM foo WHERE (f.a = ?)" "baz"]
+=> ["SELECT a, b, c FROM foo WHERE f.a = ?" "baz"]
```
-You can build up SQL maps yourself or use helper functions. `build` is the Swiss Army Knife helper. It lets you leave out brackets here and there:
+By default, namespace-qualified keywords are treated as simple keywords: their namespace portion is ignored. This was the behavior in HoneySQL prior to the 0.9.0 release and has been restored since the 0.9.7 release as this is considered the least surprising behavior.
+As of version 0.9.7, `format` accepts `:allow-namespaced-names? true` to provide the somewhat unusual behavior of 0.9.0-0.9.6, namely that namespace-qualified keywords were passed through into the SQL "as-is", i.e., with the `/` in them (which generally required a quoting strategy as well).
+As of version 0.9.8, `format` accepts `:namespace-as-table? true` to treat namespace-qualified keywords as if the `/` were `.`, allowing `:table/column` as an alternative to `:table.column`. 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
+(def q-sqlmap {:select [:foo/a :foo/b :foo/c]
+ :from [:foo]
+ :where [:= :foo/a "baz"]})
+(sql/format q-sqlmap :namespace-as-table? true)
+=> ["SELECT foo.a, foo.b, foo.c FROM foo WHERE foo.a = ?" "baz"]
+```
+
+Honeysql is a relatively "pure" library, it does not manage your sql connection
+or run queries for you, it simply generates SQL strings. You can then pass them
+to jdbc:
+
+```clj
+(jdbc/query conn (sql/format sqlmap))
+```
+
+If you want to format the query as a string with no parameters (e.g. to use the SQL statement in a SQL console), pass `:parameterizer :none` to the `sql/format`:
```clj
+(sql/format sqlmap :parameterizer :none)
+=> ["SELECT a, b, c FROM foo WHERE f.a = baz"]
+```
+
+Note that HoneySQL 1.0 does not correctly turn string parameters into inline SQL strings -- it will only work with numbers (and Booleans). HoneySQL 2.0 will do this correctly (via the new `:inline` option to `sql/format`
+
+### `build`
+
+You can build up SQL maps yourself or use helper functions. `build` is the Swiss Army Knife helper. It lets you leave out brackets here and there:
+
+```clojure
(sql/build :select :*
:from :foo
:where [:= :f.a "baz"])
@@ -44,14 +88,20 @@ You can build up SQL maps yourself or use helper functions. `build` is the Swiss
You can provide a "base" map as the first argument to build:
-```clj
+```clojure
(sql/build sqlmap :offset 10 :limit 10)
-=> {:limit 10, :offset 10, :select [:a :b :c], :where [:= :f.a "baz"], :from [:foo]}
+=> {:limit 10
+ :offset 10
+ :select [:a :b :c]
+ :where [:= :f.a "baz"]
+ :from [:foo]}
```
+### Vanilla SQL clause helpers
+
There are also functions for each clause type in the `honeysql.helpers` namespace:
-```clj
+```clojure
(-> (select :a :b :c)
(from :foo)
(where [:= :f.a "baz"]))
@@ -59,7 +109,7 @@ There are also functions for each clause type in the `honeysql.helpers` namespac
Order doesn't matter:
-```clj
+```clojure
(= (-> (select :*) (from :foo))
(-> (from :foo) (select :*)))
=> true
@@ -67,36 +117,52 @@ Order doesn't matter:
When using the vanilla helper functions, new clauses will replace old clauses:
-```clj
+```clojure
(-> sqlmap (select :*))
-=> {:from [:foo], :where [:= :f.a "baz"], :select (:*)}
+=> '{:from [:foo], :where [:= :f.a "baz"], :select (:*)}
```
To add to clauses instead of replacing them, use `merge-select`, `merge-where`, etc.:
-```clj
+```clojure
(-> sqlmap
(merge-select :d :e)
(merge-where [:> :b 10])
sql/format)
-=> ["SELECT a, b, c, d, e FROM foo WHERE (f.a = ? AND b > 10)" "baz"]
+=> ["SELECT a, b, c, d, e FROM foo WHERE (f.a = ? AND b > ?)" "baz" 10]
```
-`where` will combine multiple clauses together using and:
+`where` will combine multiple clauses together using SQL's `AND`:
-```clj
+```clojure
(-> (select :*)
(from :foo)
(where [:= :a 1] [:< :b 100])
sql/format)
-=> ["SELECT * FROM foo WHERE (a = 1 AND b < 100)"]
+=> ["SELECT * FROM foo WHERE (a = ? AND b < ?)" 1 100]
+```
+
+Column and table names may be aliased by using a vector pair of the original
+name and the desired alias:
+
+```clojure
+(-> (select :a [:b :bar] :c [:d :x])
+ (from [:foo :quux])
+ (where [:= :quux.a 1] [:< :bar 100])
+ sql/format)
+=> ["SELECT a, b AS bar, c, d AS x FROM foo quux WHERE (quux.a = ? AND bar < ?)" 1 100]
```
-Inserts are supported in two patterns.
+In particular, note that `(select [:a :b])` means `SELECT a AS b` rather than
+`SELECT a, b` -- `select` is variadic and does not take a collection of column names.
+
+### Inserts
+
+Inserts are supported in two patterns.
In the first pattern, you must explicitly specify the columns to insert,
then provide a collection of rows, each a collection of column values:
-```clj
+```clojure
(-> (insert-into :properties)
(columns :name :surname :age)
(values
@@ -104,31 +170,35 @@ then provide a collection of rows, each a collection of column values:
["Andrew" "Cooper" 12]
["Jane" "Daniels" 56]])
sql/format)
-=> ["INSERT INTO properties (name, surname, age)
- VALUES (?, ?, 34), (?, ?, 12), (?, ?, 56)"
- "Jon" "Smith" "Andrew" "Cooper" "Jane" "Daniels"]
+=> [#sql/regularize
+ "INSERT INTO properties (name, surname, age)
+ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)"
+ "Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
```
Alternately, you can simply specify the values as maps; the first map defines the columns to insert,
and the remaining maps *must* have the same set of keys and values:
-```clj
+```clojure
(-> (insert-into :properties)
(values [{:name "John" :surname "Smith" :age 34}
{:name "Andrew" :surname "Cooper" :age 12}
{:name "Jane" :surname "Daniels" :age 56}])
sql/format)
-=> ["INSERT INTO properties (age, name, surname)
- VALUES (34, ?, ?), (12, ?, ?), (56, ?, ?)"
- "John" "Smith"
- "Andrew" "Cooper"
- "Jane" "Daniels"]
+=> [#sql/regularize
+ "INSERT INTO properties (name, surname, age)
+ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)"
+ "John" "Smith" 34
+ "Andrew" "Cooper" 12
+ "Jane" "Daniels" 56]
```
+### Nested subqueries
+
The column values do not have to be literals, they can be nested queries:
-```clj
+```clojure
(let [user-id 12345
role-name "user"]
(-> (insert-into :user_profile_to_role)
@@ -138,62 +208,128 @@ The column values do not have to be literals, they can be nested queries:
(where [:= :name role-name]))}])
sql/format))
-=> ["INSERT INTO user_profile_to_role (user_profile_id, role_id)
- VALUES (12345, (SELECT id FROM role WHERE name = ?))"
+=> [#sql/regularize
+ "INSERT INTO user_profile_to_role (user_profile_id, role_id)
+ VALUES (?, (SELECT id FROM role WHERE name = ?))"
+ 12345
"user"]
```
+```clojure
+(-> (select :*)
+ (from :foo)
+ (where [:in :foo.a (-> (select :a) (from :bar))])
+ sql/format)
+=> ["SELECT * FROM foo WHERE (foo.a in (SELECT a FROM bar))"]
+```
+
+### Composite types
+
+Composite types are supported:
+
+```clojure
+(-> (insert-into :comp_table)
+ (columns :name :comp_column)
+ (values
+ [["small" (composite 1 "inch")]
+ ["large" (composite 10 "feet")]])
+ sql/format)
+=> [#sql/regularize
+ "INSERT INTO comp_table (name, comp_column)
+ VALUES (?, (?, ?)), (?, (?, ?))"
+ "small" 1 "inch" "large" 10 "feet"]
+```
+
+### Updates
+
Updates are possible too (note the double S in `sset` to avoid clashing
with `clojure.core/set`):
-```clj
-(-> (update :films)
+```clojure
+(-> (helpers/update :films)
(sset {:kind "dramatic"
- :watched true})
+ :watched (sql/call :+ :watched 1)})
(where [:= :kind "drama"])
sql/format)
-=> ["UPDATE films SET watched = TRUE, kind = ? WHERE kind = ?" "dramatic" "drama"]
+=> [#sql/regularize
+ "UPDATE films SET kind = ?, watched = (watched + ?)
+ WHERE kind = ?"
+ "dramatic"
+ 1
+ "drama"]
```
+If you are trying to build a compound update statement (with `from` or `join`),
+be aware that different databases have slightly different syntax in terms of
+where `SET` should appear. The default above is to put `SET` after any `JOIN`.
+There are two variants of `sset` (and the underlying `:set` in the SQL map):
+
+* `set0` (and `:set0`) -- this puts the `SET` before `FROM`,
+* `set1` (and `:set1`) -- a synonym for `sset` (and `:set`) that puts the `SET` after `JOIN`.
+
+### Deletes
+
Deletes look as you would expect:
-```clj
+```clojure
(-> (delete-from :films)
(where [:<> :kind "musical"])
sql/format)
=> ["DELETE FROM films WHERE kind <> ?" "musical"]
```
-Queries can be nested:
+If your database supports it, you can also delete from multiple tables:
-```clj
-(-> (select :*)
- (from :foo)
- (where [:in :foo.a (-> (select :a) (from :bar))])
+```clojure
+(-> (delete [:films :directors])
+ (from :films)
+ (join :directors [:= :films.director_id :directors.id])
+ (where [:<> :kind "musical"])
sql/format)
-=> ["SELECT * FROM foo WHERE (foo.a IN (SELECT a FROM bar))"]
+=> [#sql/regularize
+ "DELETE films, directors
+ FROM films
+ INNER JOIN directors ON films.director_id = directors.id
+ WHERE kind <> ?"
+ "musical"]
```
-Queries may be united within a :union or :union-all keyword:
+If you want to delete everything from a table, you can use `truncate`:
-```clj
+```clojure
+(-> (truncate :films)
+ sql/format)
+=> ["TRUNCATE films"]
+```
+
+### Set operations
+
+Queries may be combined within a :union, :union-all, :intersect or :except keyword:
+
+```clojure
(sql/format {:union [(-> (select :*) (from :foo))
(-> (select :*) (from :bar))]})
=> ["SELECT * FROM foo UNION SELECT * FROM bar"]
```
+### Functions
+
Keywords that begin with `%` are interpreted as SQL function calls:
-```clj
+```clojure
(-> (select :%count.*) (from :foo) sql/format)
=> ["SELECT count(*) FROM foo"]
+```
+```clojure
(-> (select :%max.id) (from :foo) sql/format)
=> ["SELECT max(id) FROM foo"]
```
+### Bindable parameters
+
Keywords that begin with `?` are interpreted as bindable parameters:
-```clj
+```clojure
(-> (select :id)
(from :foo)
(where [:= :a :?baz])
@@ -201,21 +337,77 @@ Keywords that begin with `?` are interpreted as bindable parameters:
=> ["SELECT id FROM foo WHERE a = ?" "BAZ"]
```
-There are helper functions and data literals for SQL function calls, field qualifiers, raw SQL fragments, and named input parameters:
+### Miscellaneous
-```clj
-(-> (select (sql/call :foo :bar) (sql/qualify :foo :a) (sql/raw "@var := foo.bar"))
+There are helper functions and data literals for SQL function calls, field
+qualifiers, raw SQL fragments, inline values, and named input parameters:
+
+```clojure
+(def call-qualify-map
+ (-> (select (sql/call :foo :bar) (sql/qualify :foo :a) (sql/raw "@var := foo.bar"))
+ (from :foo)
+ (where [:= :a (sql/param :baz)] [:= :b (sql/inline 42)])))
+```
+```clojure
+call-qualify-map
+=> '{:where [:and [:= :a #sql/param :baz] [:= :b #sql/inline 42]]
+ :from (:foo)
+ :select (#sql/call [:foo :bar] :foo.a #sql/raw "@var := foo.bar")}
+```
+```clojure
+(sql/format call-qualify-map :params {:baz "BAZ"})
+=> ["SELECT foo(bar), foo.a, @var := foo.bar FROM foo WHERE (a = ? AND b = 42)" "BAZ"]
+```
+
+#### PostGIS
+
+A common example in the wild is the PostGIS extension to PostgreSQL where you
+have a lot of function calls needed in code:
+
+```clojure
+(-> (insert-into :sample)
+ (values [{:location (sql/call :ST_SetSRID
+ (sql/call :ST_MakePoint 0.291 32.621)
+ (sql/call :cast 4326 :integer))}])
+ (sql/format))
+=> [#sql/regularize
+ "INSERT INTO sample (location)
+ VALUES (ST_SetSRID(ST_MakePoint(?, ?), CAST(? AS integer)))"
+ 0.291 32.621 4326]
+```
+
+#### Raw SQL fragments
+
+Raw SQL fragments that are strings are treated exactly as-is when rendered into
+the formatted SQL string (with no parsing or parameterization). Inline values
+will not be lifted out as parameters, so they end up in the SQL string as-is.
+
+Raw SQL can also be supplied as a vector of strings and values. Strings are
+rendered as-is into the formatted SQL string. Non-strings are lifted as
+parameters. If you need a string parameter lifted, you must use `#sql/param`
+or the `param` helper.
+
+```clojure
+(-> (select :*)
(from :foo)
- (where [:= :a (sql/param :baz)]))
-=> {:where [:= :a #sql/param :baz], :from (:foo), :select (#sql/call [:foo :bar] :foo.a #sql/raw "@var := foo.bar")}
+ (where [:< :expired_at (sql/raw ["now() - '" 5 " seconds'"])])
+ (sql/format {:foo 5}))
+=> ["SELECT * FROM foo WHERE expired_at < now() - '? seconds'" 5]
+```
-(sql/format *1 :params {:baz "BAZ"})
-=> ["SELECT FOO(bar), foo.a, @var := foo.bar FROM foo WHERE a = ?" "BAZ"]
+```clojure
+(-> (select :*)
+ (from :foo)
+ (where [:< :expired_at (sql/raw ["now() - '" #sql/param :t " seconds'"])])
+ (sql/format {:t 5}))
+=> ["SELECT * FROM foo WHERE expired_at < now() - '? seconds'" 5]
```
+#### Identifiers
+
To quote identifiers, pass the `:quoting` keyword option to `format`. Valid options are `:ansi` (PostgreSQL), `:mysql`, or `:sqlserver`:
-```clj
+```clojure
(-> (select :foo.a)
(from :foo)
(where [:= :foo.a "baz"])
@@ -223,14 +415,16 @@ To quote identifiers, pass the `:quoting` keyword option to `format`. Valid opti
=> ["SELECT `foo`.`a` FROM `foo` WHERE `foo`.`a` = ?" "baz"]
```
-To issue a locking select, add a :lock to the query or use the lock helper. The lock value must be a map with a :mode value. The built-in
-modes are the standard :update (FOR UPDATE) or the vendor-specific :mysql-share (LOCK IN SHARE MODE) or :postresql-share (FOR SHARE). The
-lock map may also provide a :wait value, which if false will append the NOWAIT parameter, supported by PostgreSQL.
+#### Locking
-```clj
+To issue a locking select, add a `:lock` to the query or use the lock helper. The lock value must be a map with a `:mode` value. The built-in
+modes are the standard `:update` (FOR UPDATE) or the vendor-specific `:mysql-share` (LOCK IN SHARE MODE) or `:postresql-share` (FOR SHARE). The
+lock map may also provide a `:wait` value, which if false will append the NOWAIT parameter, supported by PostgreSQL.
+
+```clojure
(-> (select :foo.a)
(from :foo)
- (where [:= foo.a "baz"])
+ (where [:= :foo.a "baz"])
(lock :mode :update)
(sql/format))
=> ["SELECT foo.a FROM foo WHERE foo.a = ? FOR UPDATE" "baz"]
@@ -239,36 +433,42 @@ lock map may also provide a :wait value, which if false will append the NOWAIT p
To support novel lock modes, implement the `format-lock-clause` multimethod.
To be able to use dashes in quoted names, you can pass ```:allow-dashed-names true``` as an argument to the ```format``` function.
-```clj
-(format
+```clojure
+(sql/format
{:select [:f.foo-id :f.foo-name]
:from [[:foo-bar :f]]
:where [:= :f.foo-id 12345]}
:allow-dashed-names? true
:quoting :ansi)
-=> ["SELECT \"f\".\"foo-id\", \"f\".\"foo-name\" FROM \"foo-bar\" \"f\" WHERE \"f\".\"foo-id\" = 12345"]
+=> ["SELECT \"f\".\"foo-id\", \"f\".\"foo-name\" FROM \"foo-bar\" \"f\" WHERE \"f\".\"foo-id\" = ?" 12345]
```
+### Big, complicated example
+
Here's a big, complicated query. Note that Honey SQL makes no attempt to verify that your queries make any sense. It merely renders surface syntax.
-```clj
-(-> (select :f.* :b.baz :c.quux [:b.bla "bla-bla"]
- (sql/call :now) (sql/raw "@x := 10"))
- (modifiers :distinct)
- (from [:foo :f] [:baz :b])
- (join :draq [:= :f.b :draq.x])
- (left-join [:clod :c] [:= :f.a :c.d])
- (right-join :bock [:= :bock.z :c.e])
- (where [:or
- [:and [:= :f.a "bort"] [:not= :b.baz (sql/param :param1)]]
- [:< 1 2 3]
- [:in :f.e [1 (sql/param :param2) 3]]
- [:between :f.e 10 20]])
- (group :f.a)
- (having [:< 0 :f.e])
- (order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
- (limit 50)
- (offset 10))
+```clojure
+(def big-complicated-map
+ (-> (select :f.* :b.baz :c.quux [:b.bla "bla-bla"]
+ (sql/call :now) (sql/raw "@x := 10"))
+ (modifiers :distinct)
+ (from [:foo :f] [:baz :b])
+ (join :draq [:= :f.b :draq.x])
+ (left-join [:clod :c] [:= :f.a :c.d])
+ (right-join :bock [:= :bock.z :c.e])
+ (where [:or
+ [:and [:= :f.a "bort"] [:not= :b.baz (sql/param :param1)]]
+ [:< 1 2 3]
+ [:in :f.e [1 (sql/param :param2) 3]]
+ [:between :f.e 10 20]])
+ (group :f.a :c.e)
+ (having [:< 0 :f.e])
+ (order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
+ (limit 50)
+ (offset 10)))
+```
+```clojure
+big-complicated-map
=> {:select [:f.* :b.baz :c.quux [:b.bla "bla-bla"]
(sql/call :now) (sql/raw "@x := 10")]
:modifiers [:distinct]
@@ -281,31 +481,34 @@ Here's a big, complicated query. Note that Honey SQL makes no attempt to verify
[:< 1 2 3]
[:in :f.e [1 (sql/param :param2) 3]]
[:between :f.e 10 20]]
- :group-by [:f.a]
+ :group-by [:f.a :c.e]
:having [:< 0 :f.e]
- :order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]
+ :order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]]
:limit 50
:offset 10}
-
-(sql/format *1 {:param1 "gabba" :param2 2})
-=> ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10
- FROM foo AS f, baz AS b
+```
+```clojure
+(sql/format big-complicated-map {:param1 "gabba" :param2 2})
+=> [#sql/regularize
+ "SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10
+ FROM foo f, baz b
INNER JOIN draq ON f.b = draq.x
- LEFT JOIN clod AS c ON f.a = c.d
+ LEFT JOIN clod c ON f.a = c.d
RIGHT JOIN bock ON bock.z = c.e
WHERE ((f.a = ? AND b.baz <> ?)
- OR (1 < 2 AND 2 < 3)
- OR (f.e IN (1, ?, 3))
- OR f.e BETWEEN 10 AND 20)
- GROUP BY f.a
- HAVING 0 < f.e
+ OR (? < ? AND ? < ?)
+ OR (f.e in (?, ?, ?))
+ OR f.e BETWEEN ? AND ?)
+ GROUP BY f.a, c.e
+ HAVING ? < f.e
ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST
- LIMIT 50
- OFFSET 10 "
- "bort" "gabba" 2]
-
+ LIMIT ?
+ OFFSET ? "
+ "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
+```
+```clojure
;; Printable and readable
-(= *2 (read-string (pr-str *2)))
+(= big-complicated-map (read-string (pr-str big-complicated-map)))
=> true
```
@@ -313,45 +516,79 @@ Here's a big, complicated query. Note that Honey SQL makes no attempt to verify
You can define your own function handlers for use in `where`:
-```clj
+```clojure
(require '[honeysql.format :as fmt])
-
+```
+```clojure
(defmethod fmt/fn-handler "betwixt" [_ field lower upper]
(str (fmt/to-sql field) " BETWIXT "
(fmt/to-sql lower) " AND " (fmt/to-sql upper)))
(-> (select :a) (where [:betwixt :a 1 10]) sql/format)
-=> ["SELECT a WHERE a BETWIXT 1 AND 10"]
-
+=> ["SELECT a WHERE a BETWIXT ? AND ?" 1 10]
```
You can also define your own clauses:
-```clj
-
+```clojure
;; Takes a MapEntry of the operator & clause data, plus the entire SQL map
(defmethod fmt/format-clause :foobar [[op v] sqlmap]
(str "FOOBAR " (fmt/to-sql v)))
-
+```
+```clojure
(sql/format {:select [:a :b] :foobar :baz})
=> ["SELECT a, b FOOBAR baz"]
-
+```
+```clojure
(require '[honeysql.helpers :refer [defhelper]])
;; Defines a helper function, and allows 'build' to recognize your clause
(defhelper foobar [m args]
(assoc m :foobar (first args)))
-
+```
+```clojure
(-> (select :a :b) (foobar :baz) sql/format)
=> ["SELECT a, b FOOBAR baz"]
```
-If you do implement a clause or function handler, consider submitting a pull request so others can use it, too.
+When adding a new clause, you may also need to register it with a specific priority so that it formats correctly, for example:
+
+```clojure
+(fmt/register-clause! :foobar 110)
+```
+
+If you do implement a clause or function handler for an ANSI SQL, consider submitting a pull request so others can use it, too. For non-standard clauses and/or functions, look for a library that extends `honeysql` for that specific database or create one, if no such library exists.
+
+## Why does my parameter get emitted as `()`?
+
+If you want to use your own datatype as a parameter then the idiomatic approach of implementing
+`next.jdbc`'s [`SettableParameter`](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.prepare#SettableParameter)
+or `clojure.java.jdbc`'s [`ISQLValue`](https://clojure.github.io/java.jdbc/#clojure.java.jdbc/ISQLValue) protocol isn't enough as `honeysql` won't correct pass through your datatype, rather it will interpret it incorrectly.
+To teach `honeysql` how to handle your datatype you need to implement [`honeysql.format/ToSql`](https://github.com/seancorfield/honeysql/blob/a9dffec632be62c961be7d9e695d0b2b85732c53/src/honeysql/format.cljc#L94). For example:
+``` clojure
+;; given:
+(defrecord MyDateWrapper [...]
+ (to-sql-timestamp [this]...)
+)
+
+;; executing:
+(hsql/format {:where [:> :some_column (MyDateWrapper. ...)]})
+;; results in => "where :some_column > ()"
+
+;; we can teach honeysql about it:
+(extend-protocol honeysql.format/ToSql
+ MyDateWrapper
+ (to-sql [v] (to-sql (date/to-sql-timestamp v))))
+
+;; allowing us to now:
+(hsql/format {:where [:> :some_column (MyDateWrapper. ...)]})
+;; which correctly results in => "where :some_column>?" and the parameter correctly set
+```
## TODO
-* Create table, etc.
+- [ ] Create table, etc.
## Extensions
@@ -359,6 +596,6 @@ If you do implement a clause or function handler, consider submitting a pull req
## License
-Copyright © 2012-2016 Justin Kramer
+Copyright © 2012-2017 Justin Kramer
Distributed under the Eclipse Public License, the same as Clojure.
=====================================
deps.edn
=====================================
@@ -0,0 +1,28 @@
+{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
+ :paths ["src" "resources"]
+ :deps {org.clojure/clojure {:mvn/version "1.10.1"}}
+ :aliases
+ {:1.7 {:override-deps {org.clojure/clojure {:mvn/version "1.7.0"}}}
+ :1.8 {:override-deps {org.clojure/clojure {:mvn/version "1.8.0"}}}
+ :1.9 {:override-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
+ :1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.1"}}}
+ :master {:override-deps {org.clojure/clojure {:mvn/version "1.11.0-master-SNAPSHOT"}}}
+ :test {:extra-paths ["test"]}
+ :runner
+ {:extra-deps {com.cognitect/test-runner
+ {:git/url "https://github.com/cognitect-labs/test-runner"
+ :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}}
+ :main-opts ["-m" "cognitect.test-runner"
+ "-d" "test"]}
+ :cljs-runner {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}}
+ :main-opts ["-m" "cljs-test-runner.main"]}
+ :readme {:extra-deps {seancorfield/readme {:mvn/version "1.0.13"}}
+ :main-opts ["-m" "seancorfield.readme"]}
+ :eastwood {:extra-deps {jonase/eastwood {:mvn/version "RELEASE"}}
+ :main-opts ["-m" "eastwood.lint" "{:source-paths,[\"src\"]}"]}
+ :jar {:extra-deps {seancorfield/depstar {:mvn/version "1.1.117"}}
+ :main-opts ["-m" "hf.depstar.jar" "honeysql.jar"]}
+ :install {:extra-deps {deps-deploy/deps-deploy {:mvn/version "0.0.9"}}
+ :main-opts ["-m" "deps-deploy.deps-deploy" "install" "honeysql.jar"]}
+ :deploy {:extra-deps {deps-deploy/deps-deploy {:mvn/version "0.0.9"}}
+ :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "honeysql.jar"]}}}
=====================================
pom.xml
=====================================
@@ -0,0 +1,57 @@
+<?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>
+ <groupId>honeysql</groupId>
+ <artifactId>honeysql</artifactId>
+ <version>1.0.461</version>
+ <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>
+ <tag>v1.0.461</tag>
+ </scm>
+ <dependencies>
+ <dependency>
+ <groupId>org.clojure</groupId>
+ <artifactId>clojure</artifactId>
+ <version>1.10.1</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src</sourceDirectory>
+ </build>
+ <repositories>
+ <repository>
+ <id>clojars</id>
+ <url>https://repo.clojars.org/</url>
+ </repository>
+ <repository>
+ <id>sonatype</id>
+ <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+ </repository>
+ </repositories>
+ <distributionManagement>
+ <repository>
+ <id>clojars</id>
+ <name>Clojars repository</name>
+ <url>https://clojars.org/repo</url>
+ </repository>
+ </distributionManagement>
+</project>
=====================================
project.clj deleted
=====================================
@@ -1,27 +0,0 @@
-(defproject honeysql "0.8.2"
- :description "SQL as Clojure data structures"
- :license {:name "Eclipse Public License"
- :url "http://www.eclipse.org/legal/epl-v10.html"}
- :url "https://github.com/jkk/honeysql"
- :scm {:name "git"
- :url "https://github.com/jkk/honeysql"}
- :dependencies [[org.clojure/clojure "1.8.0"]]
- :cljsbuild {:builds {:release {:source-paths ["src"]
- :compiler {:output-to "dist/honeysql.js"
- :optimizations :advanced
- :output-wrapper false
- :parallel-build true
- :pretty-print false}}
- :test {:source-paths ["src" "test"]
- :compiler {:output-to "target/test/honeysql.js"
- :output-dir "target/test"
- :source-map true
- :main honeysql.test
- :parallel-build true
- :target :nodejs}}}}
- :doo {:build "test"}
- :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"]
- [org.clojure/clojurescript "1.9.89"]
- [cljsbuild "1.1.3"]]
- :plugins [[lein-cljsbuild "1.1.3"]
- [lein-doo "0.1.6"]]}})
=====================================
resources/data_readers.clj
=====================================
@@ -1,4 +1,6 @@
{sql/call honeysql.types/read-sql-call
+ sql/inline honeysql.types/read-sql-inline
sql/raw honeysql.types/read-sql-raw
sql/param honeysql.types/read-sql-param
- sql/array honeysql.types/read-sql-array}
+ sql/array honeysql.types/read-sql-array
+ sql/regularize honeysql.format/regularize}
=====================================
run-tests.sh
=====================================
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+echo ==== Test README.md ==== && clojure -A:readme && \
+ echo ==== Lint Source ==== && clojure -A:eastwood && \
+ echo ==== Test ClojureScript ==== && clojure -A:test:cljs-runner
+
+if test $? -eq 0
+then
+ if test "$1" = "all"
+ then
+ for v in 1.7 1.8 1.9 1.10 master
+ do
+ echo ==== Test Clojure $v ====
+ clojure -A:test:runner:$v
+ if test $? -ne 0
+ then
+ exit 1
+ fi
+ done
+ else
+ echo ==== Test Clojure ====
+ clojure -A:test:runner
+ fi
+else
+ exit 1
+fi
=====================================
src/honeysql/core.cljc
=====================================
@@ -9,6 +9,7 @@
(#?(:clj defalias :cljs def) call types/call)
(#?(:clj defalias :cljs def) raw types/raw)
(#?(:clj defalias :cljs def) param types/param)
+(#?(:clj defalias :cljs def) inline types/inline)
(#?(:clj defalias :cljs def) format format/format)
(#?(:clj defalias :cljs def) format-predicate format/format-predicate)
(#?(:clj defalias :cljs def) quote-identifier format/quote-identifier)
@@ -42,6 +43,7 @@
:left-join, :merge-left-join
:right-join, :merge-right-join
:full-join, :merge-full-join
+ :cross-join, :merge-cross-join
:where, :merge-where
:group-by, :merge-group-by
:having, :merge-having
=====================================
src/honeysql/format.cljc
=====================================
@@ -1,9 +1,10 @@
(ns honeysql.format
(:refer-clojure :exclude [format])
- (:require [honeysql.types :refer [call raw param param-name
- #?@(:cljs [SqlCall SqlRaw SqlParam SqlArray])]]
+ (:require [honeysql.types :as types
+ :refer [call raw param param-name inline-str
+ #?@(:cljs [SqlCall SqlRaw SqlParam SqlArray SqlInline])]]
[clojure.string :as string])
- #?(:clj (:import [honeysql.types SqlCall SqlRaw SqlParam SqlArray])))
+ #?(:clj (:import [honeysql.types SqlCall SqlRaw SqlParam SqlArray SqlInline])))
;;(set! *warn-on-reflection* true)
@@ -37,19 +38,36 @@
(def ^:dynamic *fn-context?* false)
+(def ^:dynamic *value-context?* false)
+
(def ^:dynamic *subquery?* false)
(def ^:dynamic *allow-dashed-names?* false)
+(def ^:dynamic *allow-namespaced-names?* false)
+
+(def ^:dynamic *namespace-as-table?* false)
+
+(def ^:dynamic *name-transform-fn* nil)
+
(def ^:private quote-fns
{:ansi #(str \" (string/replace % "\"" "\"\"") \")
:mysql #(str \` (string/replace % "`" "``") \`)
:sqlserver #(str \[ (string/replace % "]" "]]") \])
:oracle #(str \" (string/replace % "\"" "\"\"") \")})
-(def ^:private parameterizers
- {:postgresql #(str "$" (swap! *all-param-counter* inc))
- :jdbc (constantly "?")})
+
+(defmulti parameterize (fn [parameterizer & args] parameterizer))
+
+(defmethod parameterize :postgresql [_ value pname]
+ (str "$" (swap! *all-param-counter* inc)))
+
+(defmethod parameterize :jdbc [_ value pname]
+ "?")
+
+(defmethod parameterize :none [_ value pname]
+ (str (last @*params*)))
+
(def ^:dynamic *quote-identifier-fn* nil)
(def ^:dynamic *parameterizer* nil)
@@ -57,13 +75,38 @@
(defn- undasherize [s]
(string/replace s "-" "_"))
+;; String.toUpperCase() or `string/upper-case` for that matter converts the string to uppercase for the DEFAULT
+;; LOCALE. Normally this does what you'd expect but things like `inner join` get converted to `İNNER JOIN` (dot over
+;; the I) when user locale is Turkish. This predictably has bad consequences for people who like their SQL queries to
+;; work. The fix here is to use String.toUpperCase(Locale/US) instead which always converts things the way we'd expect.
+;;
+;; Use this function instead of `string/upper-case` as it will always use Locale/US.
+(def ^:private ^{:arglists '([s])} upper-case
+ ;; TODO - not sure if there's a JavaScript equivalent here we should be using as well
+ #?(:clj (fn [^String s] (.. s toString (toUpperCase (java.util.Locale/US))))
+ :cljs string/upper-case))
+
(defn quote-identifier [x & {:keys [style split] :or {split true}}]
- (let [name-transform-fn (if *allow-dashed-names?* identity undasherize)
+ (let [name-transform-fn (cond
+ *name-transform-fn* *name-transform-fn*
+ *allow-dashed-names?* identity
+ :else undasherize)
qf (if style
(quote-fns style)
*quote-identifier-fn*)
s (cond
- (or (keyword? x) (symbol? x)) (name-transform-fn (name x))
+ (or (keyword? x) (symbol? x))
+ (name-transform-fn
+ (cond *namespace-as-table?*
+ (str (when-let [n (namespace x)]
+ (str n "."))
+ (name x))
+ *allow-namespaced-names?*
+ (str (when-let [n (namespace x)]
+ (str n "/"))
+ (name x))
+ :else
+ (name x)))
(string? x) (if qf x (name-transform-fn x))
:else (str x))]
(if-not qf
@@ -91,6 +134,10 @@
(defprotocol ToSql
(to-sql [x]))
+(defn to-sql-value [x]
+ (binding [*value-context?* (sequential? x)]
+ (to-sql x)))
+
(defmulti fn-handler (fn [op & args] op))
(defn expand-binary-ops [op & args]
@@ -121,40 +168,40 @@
(if (seq more)
(apply expand-binary-ops "=" a b more)
(cond
- (nil? a) (str (to-sql b) " IS NULL")
- (nil? b) (str (to-sql a) " IS NULL")
- :else (str (to-sql a) " = " (to-sql b)))))
+ (nil? a) (str (to-sql-value b) " IS NULL")
+ (nil? b) (str (to-sql-value a) " IS NULL")
+ :else (str (to-sql-value a) " = " (to-sql-value b)))))
(defmethod fn-handler "<>" [_ a b & more]
(if (seq more)
(apply expand-binary-ops "<>" a b more)
(cond
- (nil? a) (str (to-sql b) " IS NOT NULL")
- (nil? b) (str (to-sql a) " IS NOT NULL")
- :else (str (to-sql a) " <> " (to-sql b)))))
+ (nil? a) (str (to-sql-value b) " IS NOT NULL")
+ (nil? b) (str (to-sql-value a) " IS NOT NULL")
+ :else (str (to-sql-value a) " <> " (to-sql-value b)))))
(defmethod fn-handler "<" [_ a b & more]
(if (seq more)
(apply expand-binary-ops "<" a b more)
- (str (to-sql a) " < " (to-sql b))))
+ (str (to-sql-value a) " < " (to-sql-value b))))
(defmethod fn-handler "<=" [_ a b & more]
(if (seq more)
(apply expand-binary-ops "<=" a b more)
- (str (to-sql a) " <= " (to-sql b))))
+ (str (to-sql-value a) " <= " (to-sql-value b))))
(defmethod fn-handler ">" [_ a b & more]
(if (seq more)
(apply expand-binary-ops ">" a b more)
- (str (to-sql a) " > " (to-sql b))))
+ (str (to-sql-value a) " > " (to-sql-value b))))
(defmethod fn-handler ">=" [_ a b & more]
(if (seq more)
(apply expand-binary-ops ">=" a b more)
- (str (to-sql a) " >= " (to-sql b))))
+ (str (to-sql-value a) " >= " (to-sql-value b))))
(defmethod fn-handler "between" [_ field lower upper]
- (str (to-sql field) " BETWEEN " (to-sql lower) " AND " (to-sql upper)))
+ (str (to-sql-value field) " BETWEEN " (to-sql-value lower) " AND " (to-sql-value upper)))
;; Handles MySql's MATCH (field) AGAINST (pattern). The third argument
;; can be a set containing one or more of :boolean, :natural, or :expand..
@@ -163,7 +210,7 @@
(comma-join
(map to-sql (if (coll? fields) fields [fields])))
") AGAINST ("
- (to-sql pattern)
+ (to-sql-value pattern)
(when (seq opts)
(str " " (space-join (for [opt opts]
(case opt
@@ -174,21 +221,29 @@
(def default-clause-priorities
"Determines the order that clauses will be placed within generated SQL"
- {:union 20
- :union-all 25
- :with 30
- :with-recursive 40
+ {:with 20
+ :with-recursive 30
+ :intersect 35
+ :union 40
+ :union-all 45
+ :except 47
:select 50
:insert-into 60
:update 70
+ :delete 75
:delete-from 80
+ :truncate 85
:columns 90
- :set 100
+ :composite 95
+ :set0 100 ; low-priority set clause
:from 110
:join 120
:left-join 130
:right-join 140
:full-join 150
+ :cross-join 152
+ :set 155
+ :set1 156 ; high-priority set clause (synonym for :set)
:where 160
:group-by 170
:having 180
@@ -213,7 +268,8 @@
(defn format
"Takes a SQL map and optional input parameters and returns a vector
- of a SQL string and parameters, as expected by clojure.java.jdbc.
+ of a SQL string and parameters, as expected by `next.jbc` and
+ `clojure.java.jdbc`.
Input parameters will be filled into designated spots according to
name (if a map is provided) or by position (if a sequence is provided)..
@@ -222,7 +278,8 @@
:params - input parameters
:quoting - quote style to use for identifiers; one of :ansi (PostgreSQL),
:mysql, :sqlserver, or :oracle. Defaults to no quoting.
- :parameterizer - style of parameter naming, one of :postgresql or :jdbc, defaults to :jdbc
+ :parameterizer - style of parameter naming, :postgresql,
+ :jdbc or :none. Defaults to :jdbc.
:return-param-names - when true, returns a vector of
[sql-str param-values param-names]"
[sql-map & params-or-opts]
@@ -237,10 +294,12 @@
*param-names* (atom [])
*input-params* (atom params)
*quote-identifier-fn* (quote-fns (:quoting opts))
- *parameterizer* (parameterizers (or (:parameterizer opts) :jdbc))
- *allow-dashed-names?* (:allow-dashed-names? opts)]
+ *parameterizer* (or (:parameterizer opts) :jdbc)
+ *allow-dashed-names?* (:allow-dashed-names? opts)
+ *allow-namespaced-names?* (:allow-namespaced-names? opts)
+ *namespace-as-table?* (:namespace-as-table? opts)]
(let [sql-str (to-sql sql-map)]
- (if (seq @*params*)
+ (if (and (seq @*params*) (not= :none (:parameterizer opts)))
(if (:return-param-names opts)
[sql-str @*params* @*param-names*]
(into [sql-str] @*params*))
@@ -255,7 +314,7 @@
(defn to-params-default [value pname]
(swap! *params* conj value)
(swap! *param-names* conj pname)
- (*parameterizer*))
+ (parameterize *parameterizer* value pname))
(extend-protocol Parameterizable
#?@(:clj
@@ -271,7 +330,7 @@
(to-params [value pname]
(swap! *params* conj value)
(swap! *param-names* conj pname)
- (*parameterizer*))
+ (parameterize *parameterizer* value pname))
#?(:clj Object :cljs default)
(to-params [value pname]
#?(:clj
@@ -310,19 +369,40 @@
(paren-wrap sql-str)
sql-str)))
+(declare format-predicate*)
+
(defn seq->sql [x]
- (if *fn-context?*
+ (cond
+ *value-context?*
+ ;; sequences are operators/functions
+ (format-predicate* x)
+ *fn-context?*
;; list argument in fn call
(paren-wrap (comma-join (map to-sql x)))
+ :else
;; alias
- (str (to-sql (first x))
- ; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it
- (if (= :select *clause*)
- " AS "
- " ")
- (if (string? (second x))
- (quote-identifier (second x))
- (to-sql (second x))))))
+ (do
+ (assert (= 2 (count x)) (str "Alias should have two parts" x))
+ (let [[target alias] x]
+ (str (to-sql target)
+ ; Omit AS in FROM, JOIN, etc. - Oracle doesn't allow it
+ (if (= :select *clause*) " AS " " ")
+ (if (or (string? alias) (keyword? alias) (symbol? alias))
+ (quote-identifier alias :split false)
+ (binding [*subquery?* false]
+ (to-sql alias))))))))
+
+(extend-protocol types/Inlinable
+ #?(:clj clojure.lang.Keyword
+ :cljs cljs.core/Keyword)
+ (inline-str [x]
+ (name x))
+ nil
+ (inline-str [_]
+ "NULL")
+ #?(:clj Object :cljs default)
+ (inline-str [x]
+ (str x)))
(extend-protocol ToSql
#?(:clj clojure.lang.Keyword
@@ -350,7 +430,11 @@
fn-name (fn-aliases fn-name fn-name)]
(apply fn-handler fn-name (.-args x)))))
SqlRaw
- (to-sql [x] (.-s x))
+ (to-sql [x]
+ (let [s (.-s x)]
+ (if (vector? s)
+ (string/join "" (map (fn [x] (if (string? x) x (to-sql x))) s))
+ s)))
#?(:clj clojure.lang.IPersistentMap
:cljs cljs.core/PersistentArrayMap)
(to-sql [x]
@@ -372,6 +456,9 @@
SqlArray
(to-sql [x]
(str "ARRAY[" (comma-join (map to-sql (.-values x))) "]"))
+ SqlInline
+ (to-sql [x]
+ (inline-str (.-value x)))
#?(:clj Object :cljs default)
(to-sql [x]
#?(:clj (add-anon-param x)
@@ -396,9 +483,11 @@
"not" (str "NOT " (format-predicate* (first args)))
("and" "or" "xor")
- (paren-wrap
- (string/join (str " " (string/upper-case op-name) " ")
- (map format-predicate* args)))
+ (->> args
+ (remove nil?)
+ (map format-predicate*)
+ (string/join (str " " (upper-case op-name) " "))
+ (paren-wrap))
"exists"
(str "EXISTS " (to-sql (first args)))
@@ -407,12 +496,14 @@
(defn format-predicate
"Formats a predicate (e.g., for WHERE, JOIN, or HAVING) as a string."
- [pred & {:keys [quoting]}]
+ [pred & {:keys [quoting parameterizer]
+ :or {parameterizer :jdbc}}]
(binding [*params* (atom [])
*param-counter* (atom 0)
*param-names* (atom [])
*quote-identifier-fn* (or (quote-fns quoting)
- *quote-identifier-fn*)]
+ *quote-identifier-fn*)
+ *parameterizer* parameterizer]
(let [sql-str (format-predicate* pred)]
(if (seq @*params*)
(into [sql-str] @*params*)
@@ -433,12 +524,17 @@
(defmethod format-clause :exists [[_ table-expr] _]
(str "EXISTS " (to-sql table-expr)))
+(defmulti format-modifiers (fn [[op & _]] op))
+
+(defmethod format-modifiers :distinct [_] "DISTINCT")
+
+(defmethod format-modifiers :default [coll]
+ (space-join (map (comp upper-case name) coll)))
+
(defmethod format-clause :select [[_ fields] sql-map]
(str "SELECT "
(when (:modifiers sql-map)
- (str (space-join (map (comp string/upper-case name)
- (:modifiers sql-map)))
- " "))
+ (str (format-modifiers (:modifiers sql-map)) " "))
(comma-join (map to-sql fields))))
(defmethod format-clause :from [[_ tables] _]
@@ -448,10 +544,13 @@
(str "WHERE " (format-predicate* pred)))
(defn format-join [type table pred]
- (cond-> (str (when type
- (str (string/upper-case (name type)) " "))
- "JOIN " (to-sql table))
- pred (str " ON " (format-predicate* pred))))
+ (str (when type
+ (str (upper-case (name type)) " "))
+ "JOIN " (to-sql table)
+ (when (some? pred)
+ (if (and (sequential? pred) (= :using (first pred)))
+ (str " USING (" (->> pred rest (map quote-identifier) comma-join) ")")
+ (str " ON " (format-predicate* pred))))))
(defmethod format-clause :join [[_ join-groups] _]
(space-join (map #(apply format-join :inner %)
@@ -469,6 +568,9 @@
(space-join (map #(apply format-join :full %)
(partition 2 join-groups))))
+(defmethod format-clause :cross-join [[_ join-groups] _]
+ (space-join (map #(format-join :cross % nil) join-groups)))
+
(defmethod format-clause :group-by [[_ fields] _]
(str "GROUP BY " (comma-join (map to-sql fields))))
@@ -517,21 +619,32 @@
(if (and (sequential? table) (sequential? (first table)))
(str "INSERT INTO "
(to-sql (ffirst table))
- " (" (comma-join (map to-sql (second (first table)))) ") "
- (to-sql (second table)))
+ (binding [*namespace-as-table?* false]
+ (str " (" (comma-join (map to-sql (second (first table)))) ") "))
+ (binding [*subquery?* false]
+ (to-sql (second table))))
(str "INSERT INTO " (to-sql table))))
(defmethod format-clause :columns [[_ fields] _]
- (str "(" (comma-join (map to-sql fields)) ")"))
+ (binding [*namespace-as-table?* false]
+ (str "(" (comma-join (map to-sql fields)) ")")))
+
+(defmethod format-clause :composite [[_ fields] _]
+ (comma-join (map to-sql fields)))
(defmethod format-clause :values [[_ values] _]
(if (sequential? (first values))
(str "VALUES " (comma-join (for [x values]
- (str "(" (comma-join (map to-sql x)) ")"))))
- (str
- "(" (comma-join (map to-sql (keys (first values)))) ") VALUES "
- (comma-join (for [x values]
- (str "(" (comma-join (map to-sql (vals x))) ")"))))))
+ (binding [*fn-context?* true]
+ (str "(" (comma-join (map to-sql x)) ")")))))
+ (let [cols (keys (first values))]
+ (str
+ (binding [*namespace-as-table?* false]
+ (str "(" (comma-join (map to-sql cols)) ")"))
+ " VALUES "
+ (comma-join (for [x values]
+ (binding [*fn-context?* true]
+ (str "(" (comma-join (map #(to-sql (get x %)) cols)) ")"))))))))
(defmethod format-clause :query-values [[_ query-values] _]
(to-sql query-values))
@@ -543,9 +656,23 @@
(str "SET " (comma-join (for [[k v] values]
(str (to-sql k) " = " (to-sql v))))))
+(defmethod format-clause :set0 [[_ values] _]
+ (str "SET " (comma-join (for [[k v] values]
+ (str (to-sql k) " = " (to-sql v))))))
+
+(defmethod format-clause :set1 [[_ values] _]
+ (str "SET " (comma-join (for [[k v] values]
+ (str (to-sql k) " = " (to-sql v))))))
+
(defmethod format-clause :delete-from [[_ table] _]
(str "DELETE FROM " (to-sql table)))
+(defmethod format-clause :delete [[_ tables] _]
+ (str "DELETE " (comma-join (map to-sql tables))))
+
+(defmethod format-clause :truncate [[_ table] _]
+ (str "TRUNCATE " (to-sql table)))
+
(defn cte->sql
[[cte-name query]]
(str (binding [*subquery?* false]
@@ -571,6 +698,10 @@
(binding [*subquery?* false]
(string/join " INTERSECT " (map to-sql maps))))
+(defmethod format-clause :except [[_ maps] _]
+ (binding [*subquery?* false]
+ (string/join " EXCEPT " (map to-sql maps))))
+
(defmethod fn-handler "case" [_ & clauses]
(str "CASE "
(space-join
@@ -580,3 +711,6 @@
(let [pred (format-predicate* condition)]
(str "WHEN " pred " THEN " (to-sql result))))))
" END"))
+
+(defn regularize [sql-string]
+ (string/replace sql-string #"\s+" " "))
=====================================
src/honeysql/helpers.cljc
=====================================
@@ -15,9 +15,17 @@
#?(:clj
(defmacro defhelper [helper arglist & more]
- (let [kw (keyword (name helper))]
+ (when-not (vector? arglist)
+ (throw #?(:clj (IllegalArgumentException. "arglist must be a vector")
+ :cljs (js/Error. "arglist must be a vector"))))
+ (when-not (= (count arglist) 2)
+ (throw #?(:clj (IllegalArgumentException. "arglist must have two entries, map and varargs")
+ :cljs (js/Error. "arglist must have two entries, map and varargs"))))
+
+ (let [kw (keyword (name helper))
+ [m-arg varargs] arglist]
`(do
- (defmethod build-clause ~kw ~(into ['_] arglist) ~@more)
+ (defmethod build-clause ~kw ~['_ m-arg varargs] ~@more)
(defn ~helper [& args#]
(let [[m# args#] (if (plain-map? (first args#))
[(first args#) (rest args#)]
@@ -27,11 +35,11 @@
;; maintain the original arglist instead of getting
;; ([& args__6880__auto__])
(alter-meta!
- (var ~helper)
- assoc
- :arglists
- '(~(into [] (rest arglist))
- ~(into [(first arglist)] (rest arglist))))))))
+ (var ~helper)
+ assoc
+ :arglists
+ '(~['& varargs]
+ ~[m-arg '& varargs]))))))
(defn collify [x]
(if (coll? x) x [x]))
@@ -56,38 +64,87 @@
m
(assoc m :where pred)))
-(defn- prep-where [args]
- (let [[m preds] (if (map? (first args))
- [(first args) (rest args)]
- [{} args])
- [logic-op preds] (if (keyword? (first preds))
- [(first preds) (rest preds)]
- [:and preds])
- pred (if (= 1 (count preds))
- (first preds)
- (into [logic-op] preds))]
- [m pred logic-op]))
-
-(defn where [& args]
- (let [[m pred] (prep-where args)]
+(defn- merge-where-args
+ "Handle optional args passed to `merge-where` or similar functions. Returns tuple of
+
+ [m where-clauses conjunction-operator]"
+ [args]
+ (let [[m & args] (if (map? (first args))
+ args
+ (cons {} args))
+ [conjunction & clauses] (if (keyword? (first args))
+ args
+ (cons :and args))]
+ [m (filter some? clauses) conjunction]))
+
+(defn- where-args
+ "Handle optional args passed to `where` or similar functions. Merges clauses together. Returns tuple of
+
+ [m merged-clause]"
+ [args]
+ (let [[m clauses conjunction] (merge-where-args args)]
+ [m (if (<= (count clauses) 1)
+ (first clauses)
+ (into [conjunction] clauses))]))
+
+(defn- where-like
+ "Create a WHERE-style clause with key `k` (e.g. `:where` or `:having`)"
+ [k args]
+ (let [[m pred] (where-args args)]
(if (nil? pred)
m
- (assoc m :where pred))))
-
-(defmethod build-clause :merge-where [_ m pred]
- (if (nil? pred)
- m
- (assoc m :where (if (not (nil? (:where m)))
- [:and (:where m) pred]
- pred))))
+ (assoc m k pred))))
-(defn merge-where [& args]
- (let [[m pred logic-op] (prep-where args)]
- (if (nil? pred)
- m
- (assoc m :where (if (not (nil? (:where m)))
- [logic-op (:where m) pred]
- pred)))))
+(defn where [& args]
+ (where-like :where args))
+
+(defn- is-clause? [clause x]
+ (and (sequential? x) (= (first x) clause)))
+
+(defn- merge-where-like
+ "Merge a WHERE-style clause with key `k` (e.g. `:where` or `:having`)"
+ [k args]
+ (let [[m new-clauses conjunction] (merge-where-args args)]
+ (reduce
+ (fn [m new-clause]
+ ;; combine existing clause and new clause if they're both of the specified conjunction type, e.g.
+ ;; [:and a b] + [:and c d] -> [:and a b c d]
+ (update-in m [k] (fn [existing-clause]
+ (let [existing-subclauses (when (some? existing-clause)
+ (if (is-clause? conjunction existing-clause)
+ (rest existing-clause)
+ [existing-clause]))
+ new-subclauses (if (is-clause? conjunction new-clause)
+ (rest new-clause)
+ [new-clause])
+ subclauses (concat existing-subclauses new-subclauses)]
+ (if (> (count subclauses) 1)
+ (into [conjunction] subclauses)
+ (first subclauses))))))
+ m
+ new-clauses)))
+
+(defn merge-where
+ "Merge a series of `where-clauses` together. Supports two optional args: a map to merge the results into, and a
+ `conjunction` to use to combine clauses (defaults to `:and`).
+
+ (merge-where [:= :x 1] [:= :y 2])
+ {:where [:and [:= :x 1] [:= :y 2]]}
+
+ (merge-where {:where [:= :x 1]} [:= :y 2])
+ ;; -> {:where [:and [:= :x 1] [:= :y 2]]}
+
+ (merge-where :or [:= :x 1] [:= :y 2])
+ ;; -> {:where [:or [:= :x 1] [:= :y 2]]}"
+ {:arglists '([& where-clauses]
+ [m-or-conjunction & where-clauses]
+ [m conjunction & where-clauses])}
+ [& args]
+ (merge-where-like :where args))
+
+(defmethod build-clause :merge-where
+ [_ m where-clause]
+ (merge-where m where-clause))
(defhelper join [m clauses]
(assoc m :join clauses))
@@ -113,6 +170,12 @@
(defhelper merge-full-join [m clauses]
(update-in m [:full-join] concat clauses))
+(defhelper cross-join [m clauses]
+ (assoc m :cross-join clauses))
+
+(defhelper merge-cross-join [m clauses]
+ (update-in m [:cross-join] concat clauses))
+
(defmethod build-clause :group-by [_ m fields]
(assoc m :group-by (collify fields)))
@@ -131,25 +194,29 @@
(assoc m :having pred)))
(defn having [& args]
- (let [[m pred] (prep-where args)]
- (if (nil? pred)
- m
- (assoc m :having pred))))
+ (where-like :having args))
-(defmethod build-clause :merge-having [_ m pred]
- (if (nil? pred)
- m
- (assoc m :having (if (not (nil? (:having m)))
- [:and (:having m) pred]
- pred))))
+(defn merge-having
+ "Merge a series of `having-clauses` together. Supports two optional args: a map to merge the results into, and a
+ `conjunction` to use to combine clauses (defaults to `:and`).
-(defn merge-having [& args]
- (let [[m pred logic-op] (prep-where args)]
- (if (nil? pred)
- m
- (assoc m :having (if (not (nil? (:having m)))
- [logic-op (:having m) pred]
- pred)))))
+ (merge-having [:= :x 1] [:= :y 2])
+ {:having [:and [:= :x 1] [:= :y 2]]}
+
+ (merge-having {:having [:= :x 1]} [:= :y 2])
+ ;; -> {:having [:and [:= :x 1] [:= :y 2]]}
+
+ (merge-having :or [:= :x 1] [:= :y 2])
+ ;; -> {:having [:or [:= :x 1] [:= :y 2]]}"
+ {:arglists '([& having-clauses]
+ [m-or-conjunction & having-clauses]
+ [m conjunction & having-clauses])}
+ [& args]
+ (merge-where-like :having args))
+
+(defmethod build-clause :merge-having
+ [_ m where-clause]
+ (merge-having m where-clause))
(defhelper order-by [m fields]
(assoc m :order-by (collify fields)))
@@ -169,8 +236,8 @@
(defhelper lock [m lock]
(cond-> m
- lock
- (assoc :lock lock)))
+ lock
+ (assoc :lock lock)))
(defhelper modifiers [m ms]
(if (nil? ms)
@@ -189,12 +256,40 @@
([table] (insert-into nil table))
([m table] (build-clause :insert-into m table)))
-(defhelper columns [m fields]
+(defn- check-varargs
+ "Called for helpers that require unrolled arguments to catch the mistake
+ of passing a collection as a single argument."
+ [helper args]
+ (when (and (coll? args) (= 1 (count args)) (coll? (first args)))
+ (let [msg (str (name helper) " takes varargs, not a single collection")]
+ (throw #?(:clj (IllegalArgumentException. msg)
+ :cljs (js/Error. msg))))))
+
+(defmethod build-clause :columns [_ m fields]
(assoc m :columns (collify fields)))
-(defhelper merge-columns [m fields]
+(defn columns [& args]
+ (let [[m fields] (if (map? (first args))
+ [(first args) (rest args)]
+ [{} args])]
+ (check-varargs :columns fields)
+ (build-clause :columns m fields)))
+
+(defmethod build-clause :merge-columns [_ m fields]
(update-in m [:columns] concat (collify fields)))
+(defn merge-columns [& args]
+ (let [[m fields] (if (map? (first args))
+ [(first args) (rest args)]
+ [{} args])]
+ (check-varargs :merge-columns fields)
+ (build-clause :merge-columns m fields)))
+
+(defhelper composite [m vs]
+ (if (nil? vs)
+ m
+ (assoc m :composite (collify vs))))
+
(defmethod build-clause :values [_ m vs]
(assoc m :values vs))
@@ -228,9 +323,25 @@
;; short for sql set, to avoid name collision with clojure.core/set
(defn sset
- ([vs] (values nil vs))
+ ([vs] (sset nil vs))
([m vs] (build-clause :set m vs)))
+(defmethod build-clause :set0 [_ m values]
+ (assoc m :set0 values))
+
+;; set with lower priority (before from)
+(defn set0
+ ([vs] (set0 nil vs))
+ ([m vs] (build-clause :set0 m vs)))
+
+(defmethod build-clause :set [_ m values]
+ (assoc m :set values))
+
+;; set with higher priority (after join)
+(defn set1
+ ([vs] (set1 nil vs))
+ ([m vs] (build-clause :set1 m vs)))
+
(defmethod build-clause :delete-from [_ m table]
(assoc m :delete-from table))
@@ -238,10 +349,24 @@
([table] (delete-from nil table))
([m table] (build-clause :delete-from m table)))
-(defmethod build-clause :with [_ m ctes]
+(defmethod build-clause :delete [_ m tables]
+ (assoc m :delete tables))
+
+(defn delete
+ ([tables] (delete nil tables))
+ ([m tables] (build-clause :delete m tables)))
+
+(defmethod build-clause :truncate [_ m table]
+ (assoc m :truncate table))
+
+(defn truncate
+ ([table] (truncate nil table))
+ ([m table] (build-clause :truncate m table)))
+
+(defhelper with [m ctes]
(assoc m :with ctes))
-(defmethod build-clause :with-recursive [_ m ctes]
+(defhelper with-recursive [m ctes]
(assoc m :with-recursive ctes))
(defmethod build-clause :union [_ m maps]
@@ -252,3 +377,6 @@
(defmethod build-clause :intersect [_ m maps]
(assoc m :intersect maps))
+
+(defmethod build-clause :except [_ m maps]
+ (assoc m :except maps))
=====================================
src/honeysql/types.cljc
=====================================
@@ -19,7 +19,7 @@
(defn raw
"Represents a raw SQL string"
[s]
- (SqlRaw. (str s)))
+ (SqlRaw. (if (vector? s) s (str s))))
(defn read-sql-raw [form]
;; late bind, as above
@@ -57,6 +57,21 @@
;; late bind, as above
(#?(:clj (resolve `array) :cljs array) form))
+;;;;
+
+(defrecord SqlInline [value])
+
+(defprotocol Inlinable
+ (inline-str [x]))
+
+(defn inline
+ "Prevents parameterization"
+ [value]
+ (SqlInline. value))
+
+(defn read-sql-inline [form]
+ (#?(:clj (resolve `inline) :cljs inline) form))
+
#?(:clj
(do
(defmethod print-method SqlCall [^SqlCall o ^java.io.Writer w]
@@ -81,4 +96,10 @@
(.write w (str "#sql/array " (pr-str (.values a)))))
(defmethod print-dup SqlArray [a w]
+ (print-method a w))
+
+ (defmethod print-method SqlInline [^SqlInline a ^java.io.Writer w]
+ (.write w (str "#sql/inline " (pr-str (.value a)))))
+
+ (defmethod print-dup SqlInline [a w]
(print-method a w))))
=====================================
test/honeysql/core_test.cljc
=====================================
@@ -4,15 +4,19 @@
:cljs [cljs.test :refer-macros]) [deftest testing is]]
[honeysql.core :as sql]
[honeysql.helpers :refer [select modifiers from join left-join
- right-join full-join where group having
+ right-join full-join cross-join
+ where group having
order-by limit offset values columns
- insert-into]]
+ insert-into with merge-where merge-having]]
honeysql.format-test))
;; TODO: more tests
(deftest test-select
- (let [m1 (-> (select :f.* :b.baz :c.quux [:b.bla :bla-bla]
+ (let [m1 (-> (with [:cte (-> (select :*)
+ (from :example)
+ (where [:= :example-column 0]))])
+ (select :f.* :b.baz :c.quux [:b.bla :bla-bla]
:%now (sql/raw "@x := 10"))
;;(un-select :c.quux)
(modifiers :distinct)
@@ -32,7 +36,10 @@
(order-by [:b.baz :desc] :c.quux [:f.a :nulls-first])
(limit 50)
(offset 10))
- m2 {:select [:f.* :b.baz :c.quux [:b.bla :bla-bla]
+ m2 {:with [[:cte {:select [:*]
+ :from [:example]
+ :where [:= :example-column 0]}]]
+ :select [:f.* :b.baz :c.quux [:b.bla :bla-bla]
:%now (sql/raw "@x := 10")]
;;:un-select :c.quux
:modifiers :distinct
@@ -57,18 +64,18 @@
(testing "Various construction methods are consistent"
(is (= m1 m3 m4)))
(testing "SQL data formats correctly"
- (is (= ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? "
- "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
+ (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? "
+ 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
(sql/format m1 {:param1 "gabba" :param2 2}))))
#?(:clj (testing "SQL data prints and reads correctly"
(is (= m1 (read-string (pr-str m1))))))
(testing "SQL data formats correctly with alternate param naming"
(is (= (sql/format m1 :params {:param1 "gabba" :param2 2} :parameterizer :postgresql)
- ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y 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 HAVING $12 < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT $13 OFFSET $14 "
- "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
+ ["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2 AND b.baz <> $3) OR ($4 < $5 AND $6 < $7) OR (f.e in ($8, $9, $10)) OR f.e BETWEEN $11 AND $12) GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT $14 OFFSET $15 "
+ 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10])))
(testing "Locking"
- (is (= ["SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? FOR UPDATE "
- "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
+ (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS bla_bla, now(), @x := 10 FROM foo f, baz b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ? AND b.baz <> ?) OR (? < ? AND ? < ?) OR (f.e in (?, ?, ?)) OR f.e BETWEEN ? AND ?) GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux, f.a NULLS FIRST LIMIT ? OFFSET ? FOR UPDATE "
+ 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]
(sql/format (assoc m1 :lock {:mode :update})
{:param1 "gabba" :param2 2}))))))
@@ -84,7 +91,13 @@
(insert-into :foo)
(columns :bar)
(values [[(honeysql.format/value {:baz "my-val"})]])
- sql/format))))
+ sql/format)))
+ (is (= ["INSERT INTO foo (a, b, c) VALUES (?, ?, ?), (?, ?, ?)"
+ "a" "b" "c" "a" "b" "c"]
+ (-> (insert-into :foo)
+ (values [(array-map :a "a" :b "b" :c "c")
+ (hash-map :a "a" :b "b" :c "c")])
+ sql/format))))
(deftest test-operators
(testing "="
@@ -180,4 +193,148 @@
(join :x [:= :foo.id :x.id] :y nil)
sql/format)))))
+(deftest join-using-test
+ (testing "nil join"
+ (is (= ["SELECT * FROM foo INNER JOIN x USING (id) INNER JOIN y USING (foo, bar)"]
+ (-> (select :*)
+ (from :foo)
+ (join :x [:using :id] :y [:using :foo :bar])
+ sql/format)))))
+
+(deftest inline-test
+ (is (= ["SELECT * FROM foo WHERE id = 5"]
+ (-> (select :*)
+ (from :foo)
+ (where [:= :id (sql/inline 5)])
+ sql/format)))
+ ;; testing for = NULL always fails in SQL -- this test is just to show
+ ;; that an #inline nil should render as NULL (so make sure you only use
+ ;; it in contexts where a literal NULL is acceptable!)
+ (is (= ["SELECT * FROM foo WHERE id = NULL"]
+ (-> (select :*)
+ (from :foo)
+ (where [:= :id (sql/inline nil)])
+ sql/format))))
+
+(deftest merge-where-no-params-test
+ (doseq [[k [f merge-f]] {"WHERE" [where merge-where]
+ "HAVING" [having merge-having]}]
+ (testing "merge-where called with just the map as parameter - see #228"
+ (let [sqlmap (-> (select :*)
+ (from :table)
+ (f [:= :foo :bar]))]
+ (is (= [(str "SELECT * FROM table " k " foo = bar")]
+ (sql/format (apply merge-f sqlmap []))))))))
+
+(deftest merge-where-test
+ (doseq [[k sql-keyword f merge-f] [[:where "WHERE" where merge-where]
+ [:having "HAVING" having merge-having]]]
+ (is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar AND quuz = xyzzy)")]
+ (-> (select :*)
+ (from :table)
+ (f [:= :foo :bar] [:= :quuz :xyzzy])
+ sql/format)))
+ (is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar AND quuz = xyzzy)")]
+ (-> (select :*)
+ (from :table)
+ (f [:= :foo :bar])
+ (merge-f [:= :quuz :xyzzy])
+ sql/format)))
+ (testing "Should work when first arg isn't a map"
+ (is (= {k [:and [:x] [:y]]}
+ (merge-f [:x] [:y]))))
+ (testing "Shouldn't use conjunction if there is only one clause in the result"
+ (is (= {k [:x]}
+ (merge-f {} [:x]))))
+ (testing "Should be able to specify the conjunction type"
+ (is (= {k [:or [:x] [:y]]}
+ (merge-f {}
+ :or
+ [:x] [:y]))))
+ (testing "Should ignore nil clauses"
+ (is (= {k [:or [:x] [:y]]}
+ (merge-f {}
+ :or
+ [:x] nil [:y]))))))
+
+(deftest merge-where-build-clause-test
+ (doseq [k [:where :having]]
+ (testing (str "Should be able to build a " k " clause with sql/build")
+ (is (= {k [:and [:a] [:x] [:y]]}
+ (sql/build
+ k [:a]
+ (keyword (str "merge-" (name k))) [:and [:x] [:y]]))))))
+
+(deftest merge-where-combine-clauses-test
+ (doseq [[k f] {:where merge-where
+ :having merge-having}]
+ (testing (str "Combine new " k " clauses into the existing clause when appropriate. (#282)")
+ (testing "No existing clause"
+ (is (= {k [:and [:x] [:y]]}
+ (f {}
+ [:x] [:y]))))
+ (testing "Existing clause is not a conjunction."
+ (is (= {k [:and [:a] [:x] [:y]]}
+ (f {k [:a]}
+ [:x] [:y]))))
+ (testing "Existing clause IS a conjunction."
+ (testing "New clause(s) are not conjunctions"
+ (is (= {k [:and [:a] [:b] [:x] [:y]]}
+ (f {k [:and [:a] [:b]]}
+ [:x] [:y]))))
+ (testing "New clauses(s) ARE conjunction(s)"
+ (is (= {k [:and [:a] [:b] [:x] [:y]]}
+ (f {k [:and [:a] [:b]]}
+ [:and [:x] [:y]])))
+ (is (= {k [:and [:a] [:b] [:x] [:y]]}
+ (f {k [:and [:a] [:b]]}
+ [:and [:x]]
+ [:y])))
+ (is (= {k [:and [:a] [:b] [:x] [:y]]}
+ (f {k [:and [:a] [:b]]}
+ [:and [:x]]
+ [:and [:y]])))))
+ (testing "if existing clause isn't the same conjunction, don't merge into it"
+ (testing "existing conjunction is `:or`"
+ (is (= {k [:and [:or [:a] [:b]] [:x] [:y]]}
+ (f {k [:or [:a] [:b]]}
+ [:x] [:y]))))
+ (testing "pass conjunction type as a param (override default of :and)"
+ (is (= {k [:or [:and [:a] [:b]] [:x] [:y]]}
+ (f {k [:and [:a] [:b]]}
+ :or
+ [:x] [:y]))))))))
+
+(deftest where-nil-params-test
+ (doseq [[k sql-keyword f] [[:where "WHERE" where]
+ [:having "HAVING" having]]]
+ (testing (str sql-keyword " called with nil parameters - see #246")
+ (is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar AND quuz = xyzzy)")]
+ (-> (select :*)
+ (from :table)
+ (f nil [:= :foo :bar] nil [:= :quuz :xyzzy] nil)
+ sql/format)))
+ (is (= ["SELECT * FROM table"]
+ (-> (select :*)
+ (from :table)
+ (f)
+ sql/format)))
+ (is (= ["SELECT * FROM table"]
+ (-> (select :*)
+ (from :table)
+ (f nil nil nil nil)
+ sql/format))))))
+
+(deftest cross-join-test
+ (is (= ["SELECT * FROM foo CROSS JOIN bar"]
+ (-> (select :*)
+ (from :foo)
+ (cross-join :bar)
+ sql/format)))
+ (is (= ["SELECT * FROM foo f CROSS JOIN bar b"]
+ (-> (select :*)
+ (from [:foo :f])
+ (cross-join [:bar :b])
+ sql/format))))
+
#?(:cljs (cljs.test/run-all-tests))
=====================================
test/honeysql/format_test.cljc
=====================================
@@ -2,9 +2,13 @@
(:refer-clojure :exclude [format])
(:require [#?@(:clj [clojure.test :refer]
:cljs [cljs.test :refer-macros]) [deftest testing is are]]
+ honeysql.core
[honeysql.types :as sql]
[honeysql.format :refer
- [*allow-dashed-names?* quote-identifier format-clause format]]))
+ [*allow-dashed-names?* *allow-namespaced-names?*
+ *namespace-as-table?*
+ quote-identifier format-clause format
+ parameterize]]))
(deftest test-quote
(are
@@ -32,6 +36,29 @@
(is (= (quote-identifier :foo-bar.moo-bar :style :ansi)
"\"foo-bar\".\"moo-bar\""))))
+(deftest test-namespaced-identifier
+ (is (= (quote-identifier :foo/bar) "bar"))
+ (is (= (quote-identifier :foo/bar :style :ansi) "\"bar\""))
+ (binding [*namespace-as-table?* true]
+ (is (= (quote-identifier :foo/bar) "foo.bar"))
+ (is (= (quote-identifier :foo/bar :style :ansi) "\"foo\".\"bar\""))
+ (is (= (quote-identifier :foo/bar :style :ansi :split false) "\"foo.bar\"")))
+ (binding [*allow-namespaced-names?* true]
+ (is (= (quote-identifier :foo/bar) "foo/bar"))
+ (is (= (quote-identifier :foo/bar :style :ansi) "\"foo/bar\""))))
+
+(deftest alias-splitting
+ (is (= ["SELECT `aa`.`c` AS `a.c`, `bb`.`c` AS `b.c`, `cc`.`c` AS `c.c`"]
+ (format {:select [[:aa.c "a.c"]
+ [:bb.c :b.c]
+ [:cc.c 'c.c]]}
+ :quoting :mysql))
+ "aliases containing \".\" are quoted as necessary but not split"))
+
+(deftest values-alias
+ (is (= ["SELECT vals.a FROM (VALUES (?, ?, ?)) vals (a, b, c)" 1 2 3]
+ (format {:select [:vals.a]
+ :from [[{:values [[1 2 3]]} [:vals {:columns [:a :b :c]}]]]}))))
(deftest test-cte
(is (= (format-clause
(first {:with [[:query {:select [:foo] :from [:bar]}]]}) nil)
@@ -54,7 +81,22 @@
(is (= (format-clause (first {:insert-into [:foo {:select [:bar] :from [:baz]}]}) nil)
"INSERT INTO foo SELECT bar FROM baz"))
(is (= (format-clause (first {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) nil)
- "INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz")))
+ "INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"))
+ (is (= (format {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]})
+ ["INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"])))
+
+(deftest insert-into-namespaced
+ ;; un-namespaced: works as expected:
+ (is (= (format {:insert-into :foo :values [{:foo/id 1}]})
+ ["INSERT INTO foo (id) VALUES (?)" 1]))
+ (is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]})
+ ["INSERT INTO foo (id) VALUES (?)" 2]))
+ (is (= (format {:insert-into :foo :values [{:foo/id 1}]}
+ :namespace-as-table? true)
+ ["INSERT INTO foo (id) VALUES (?)" 1]))
+ (is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]}
+ :namespace-as-table? true)
+ ["INSERT INTO foo (id) VALUES (?)" 2])))
(deftest exists-test
(is (= (format {:exists {:select [:a] :from [:foo]}})
@@ -97,6 +139,11 @@
{:select [:foo] :from [:bar2]}]})
["SELECT foo FROM bar1 INTERSECT SELECT foo FROM bar2"])))
+(deftest except-test
+ (is (= (format {:except [{:select [:foo] :from [:bar1]}
+ {:select [:foo] :from [:bar2]}]})
+ ["SELECT foo FROM bar1 EXCEPT SELECT foo FROM bar2"])))
+
(deftest inner-parts-test
(testing "The correct way to apply ORDER BY to various parts of a UNION"
(is (= (format
@@ -110,3 +157,180 @@
:limit 5}]}]
:order-by [[:amount :asc]]})
["SELECT amount, id, created_on FROM transactions UNION SELECT amount, id, created_on FROM (SELECT amount, id, created_on FROM other_transactions ORDER BY amount DESC LIMIT ?) ORDER BY amount ASC" 5]))))
+
+(deftest compare-expressions-test
+ (testing "Sequences should be fns when in value/comparison spots"
+ (is (= ["SELECT foo FROM bar WHERE (col1 mod ?) = (col2 + ?)" 4 4]
+ (format {:select [:foo]
+ :from [:bar]
+ :where [:= [:mod :col1 4] [:+ :col2 4]]}))))
+
+ (testing "Value context only applies to sequences in value/comparison spots"
+ (let [sub {:select [:%sum.amount]
+ :from [:bar]
+ :where [:in :id ["id-1" "id-2"]]}]
+ (is (= ["SELECT total FROM foo WHERE (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) = total" "id-1" "id-2"]
+ (format {:select [:total]
+ :from [:foo]
+ :where [:= sub :total]})))
+ (is (= ["WITH t AS (SELECT sum(amount) FROM bar WHERE (id in (?, ?))) SELECT total FROM foo WHERE total = t" "id-1" "id-2"]
+ (format {:with [[:t sub]]
+ :select [:total]
+ :from [:foo]
+ :where [:= :total :t]}))))))
+
+(deftest union-with-cte
+ (is (= (format {:union [{:select [:foo] :from [:bar1]}
+ {:select [:foo] :from [:bar2]}]
+ :with [[[:bar {:columns [:spam :eggs]}]
+ {:values [[1 2] [3 4] [5 6]]}]]})
+ ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) SELECT foo FROM bar1 UNION SELECT foo FROM bar2" 1 2 3 4 5 6])))
+
+
+(deftest union-all-with-cte
+ (is (= (format {:union-all [{:select [:foo] :from [:bar1]}
+ {:select [:foo] :from [:bar2]}]
+ :with [[[:bar {:columns [:spam :eggs]}]
+ {:values [[1 2] [3 4] [5 6]]}]]})
+ ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) SELECT foo FROM bar1 UNION ALL SELECT foo FROM bar2" 1 2 3 4 5 6])))
+
+(deftest parameterizer-none
+ (testing "array parameter"
+ (is (= (format {:insert-into :foo
+ :columns [:baz]
+ :values [[(sql/array [1 2 3 4])]]}
+ :parameterizer :none)
+ ["INSERT INTO foo (baz) VALUES (ARRAY[1, 2, 3, 4])"])))
+
+ (testing "union complex values"
+ (is (= (format {:union [{:select [:foo] :from [:bar1]}
+ {:select [:foo] :from [:bar2]}]
+ :with [[[:bar {:columns [:spam :eggs]}]
+ {:values [[1 2] [3 4] [5 6]]}]]}
+ :parameterizer :none)
+ ["WITH bar (spam, eggs) AS (VALUES (1, 2), (3, 4), (5, 6)) SELECT foo FROM bar1 UNION SELECT foo FROM bar2"]))))
+
+(deftest where-and
+ (testing "should ignore a nil predicate"
+ (is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
+ :parameterizer :postgresql)
+ ["WHERE (foo = $1 AND bar = $2)" "foo" "bar"]))))
+
+
+(defmethod parameterize :single-quote [_ value pname] (str \' value \'))
+(defmethod parameterize :mysql-fill [_ value pname] "?")
+
+(deftest customized-parameterizer
+ (testing "should fill param with single quote"
+ (is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
+ :parameterizer :single-quote)
+ ["WHERE (foo = 'foo' AND bar = 'bar')" "foo" "bar"])))
+ (testing "should fill param with ?"
+ (is (= (format {:where [:and [:= :foo "foo"] [:= :bar "bar"] nil]}
+ :parameterizer :mysql-fill)
+ ["WHERE (foo = ? AND bar = ?)" "foo" "bar"]))))
+
+
+(deftest set-before-from ; issue 235
+ (is (=
+ ["UPDATE `films` `f` SET `kind` = `c`.`test` FROM (SELECT `b`.`test` FROM `bar` `b` WHERE `b`.`id` = ?) `c` WHERE `f`.`kind` = ?" 1 "drama"]
+ (->
+ {:update [:films :f]
+ :set0 {:kind :c.test}
+ :from [[{:select [:b.test]
+ :from [[:bar :b]]
+ :where [:= :b.id 1]} :c]]
+ :where [:= :f.kind "drama"]}
+ (format :quoting :mysql)))))
+
+(deftest set-after-join
+ (is (=
+ ["UPDATE `foo` INNER JOIN `bar` ON `bar`.`id` = `foo`.`bar_id` SET `a` = ? WHERE `bar`.`b` = ?" 1 42]
+ (->
+ {:update :foo
+ :join [:bar [:= :bar.id :foo.bar_id]]
+ :set {:a 1}
+ :where [:= :bar.b 42]}
+ (format :quoting :mysql))))
+ (is (=
+ ["UPDATE `foo` INNER JOIN `bar` ON `bar`.`id` = `foo`.`bar_id` SET `a` = ? WHERE `bar`.`b` = ?" 1 42]
+ (->
+ {:update :foo
+ :join [:bar [:= :bar.id :foo.bar_id]]
+ :set1 {:a 1}
+ :where [:= :bar.b 42]}
+ (format :quoting :mysql)))))
+
+(deftest delete-from-test
+ (is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42]
+ (-> {:delete-from :foo
+ :where [:= :foo.id 42]}
+ (format :quoting :mysql)))))
+
+(deftest delete-test
+ (is (= ["DELETE `t1`, `t2` FROM `table1` `t1` INNER JOIN `table2` `t2` ON `t1`.`fk` = `t2`.`id` WHERE `t1`.`bar` = ?" 42]
+ (-> {:delete [:t1 :t2]
+ :from [[:table1 :t1]]
+ :join [[:table2 :t2] [:= :t1.fk :t2.id]]
+ :where [:= :t1.bar 42]}
+ (format :quoting :mysql)))))
+
+(deftest truncate-test
+ (is (= ["TRUNCATE `foo`"]
+ (-> {:truncate :foo}
+ (format :quoting :mysql)))))
+
+(deftest inlined-values-are-stringified-correctly
+ (is (= ["SELECT foo, bar, NULL"]
+ (format {:select [(honeysql.core/inline "foo")
+ (honeysql.core/inline :bar)
+ (honeysql.core/inline nil)]}))))
+
+;; Make sure if Locale is Turkish we're not generating queries like İNNER JOIN (dot over the I) because
+;; `string/upper-case` is converting things to upper-case using the default Locale. Generated query should be the same
+;; regardless of system Locale. See #236
+#?(:clj
+ (deftest statements-generated-correctly-with-turkish-locale
+ (let [format-with-locale (fn [^String language-tag]
+ (let [original-locale (java.util.Locale/getDefault)]
+ (try
+ (java.util.Locale/setDefault (java.util.Locale/forLanguageTag language-tag))
+ (format {:select [:t2.name]
+ :from [[:table1 :t1]]
+ :join [[:table2 :t2] [:= :t1.fk :t2.id]]
+ :where [:= :t1.id 1]})
+ (finally
+ (java.util.Locale/setDefault original-locale)))))]
+ (is (= (format-with-locale "en")
+ (format-with-locale "tr"))))))
+
+(deftest join-on-true-253
+ ;; used to work on honeysql 0.9.2; broke in 0.9.3
+ (is (= ["SELECT foo FROM bar INNER JOIN table t ON TRUE"]
+ (format {:select [:foo]
+ :from [:bar]
+ :join [[:table :t] true]}))))
+
+(deftest cross-join-test
+ (is (= ["SELECT * FROM foo CROSS JOIN bar"]
+ (format {:select [:*]
+ :from [:foo]
+ :cross-join [:bar]})))
+ (is (= ["SELECT * FROM foo f CROSS JOIN bar b"]
+ (format {:select [:*]
+ :from [[:foo :f]]
+ :cross-join [[:bar :b]]}))))
+
+(deftest issue-299-test
+ (let [name "test field"
+ ;; this was being rendered inline into the SQL
+ ;; creating an injection vulnerability (v1 only)
+ ;; the context for seq->sql here seems to be the
+ ;; 'regular' one so it tries to treat this as an
+ ;; alias: 'value alias' -- the fix was to make it
+ ;; a function context so it becomes (TRUE, ?):
+ enabled [true, "); SELECT case when (SELECT current_setting('is_superuser'))='off' then pg_sleep(0.2) end; -- "]]
+ (is (= ["INSERT INTO table (name, enabled) VALUES (?, (TRUE, ?))" name (second enabled)]
+ (format {:insert-into :table
+ :values [{:name name
+ :enabled enabled}]})))))
\ No newline at end of file
=====================================
test/honeysql/test.cljs deleted
=====================================
@@ -1,9 +0,0 @@
-(ns honeysql.test
- (:require
- [doo.runner :refer-macros [doo-tests]]
- [cljs.test :as t :refer-macros [is are deftest testing]]
- honeysql.core-test
- honeysql.format-test))
-
-(doo-tests 'honeysql.core-test
- 'honeysql.format-test)
View it on GitLab: https://salsa.debian.org/clojure-team/honeysql-clojure/-/commit/f635aa2522498ca6714b106080373469dba365a6
--
View it on GitLab: https://salsa.debian.org/clojure-team/honeysql-clojure/-/commit/f635aa2522498ca6714b106080373469dba365a6
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20220704/7276e88b/attachment.htm>
More information about the pkg-java-commits
mailing list