diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c5bba..8a7cb0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Changes * 2.4.next in progress + * 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). * 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!** diff --git a/README.md b/README.md index edb715d..02f73c1 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,13 @@ 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'"] ``` +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.next, 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. ```clojure @@ -388,6 +395,19 @@ INSERT INTO comp_table VALUES (?, (?, ?)), (?, (?, ?)) " "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: (-> {:insert-into [:comp_table], :columns [:name :comp_column], @@ -608,6 +628,12 @@ Keywords that begin with `?` are interpreted as bindable parameters: (where [:= :a :?baz]) (sql/format {:params {:baz "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: (-> {:select [:id], :from [:foo], :where [:= :a :?baz]} (sql/format {:params {:baz "BAZ"}})) @@ -832,6 +858,24 @@ LIMIT ? OFFSET ? " "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 ;; Printable and readable @@ -882,8 +926,13 @@ Or perhaps your database supports syntax like `a BETWIXT b AND c`, in which case ;; example usage: (-> (select :a) (where [:betwixt :a 1 10]) sql/format) => ["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: ```clojure @@ -909,6 +958,6 @@ If you find yourself registering an operator, a function (syntax), or a new clau ## License -Copyright (c) 2020-2021 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield. +Copyright (c) 2020-2022 Sean Corfield. HoneySQL 1.x was copyright (c) 2012-2020 Justin Kramer and Sean Corfield. Distributed under the Eclipse Public License, the same as Clojure. diff --git a/doc/differences-from-1-x.md b/doc/differences-from-1-x.md index de4044b..5b47979 100644 --- a/doc/differences-from-1-x.md +++ b/doc/differences-from-1-x.md @@ -104,7 +104,7 @@ You can now select a non-ANSI dialect of SQL using the new `honey.sql/set-dialec ## Option Changes -The `:quoting ` option has superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `:dialect ` instead or set a default dialect (via `set-dialect!`) and then use `:quoted true` in `format` calls where you want quoting. +The `:quoting ` option has been superseded by the new dialect machinery and a new `:quoted` option that turns quoting on or off. You either use `: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. SQL entity names are automatically quoted if you specify a `:dialect` option to `format`, unless you also specify `:quoted false`. @@ -112,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-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`. -* `: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). +* `: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.next, the ability to generated SQL with numbered parameters, i.e., `$1` instead of positional parameters, `?`, has been added via the `:numbered true` option]_ * `: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. diff --git a/doc/getting-started.md b/doc/getting-started.md index 1b40eab..3bd928e 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -110,6 +110,8 @@ Some "functions" are considered to be operators. In general, `42` and `"c"` lifted out into the overall vector result (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). Special syntax can have zero or more arguments and each form is described in the [Special Syntax](special-syntax.md) section. @@ -179,7 +181,7 @@ expression requires an extra level of nesting: 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. -They are replaced by `?` in the generated SQL string and added to the +By default, they are replaced by `?` in the generated SQL string and added to the parameter list in order: @@ -187,6 +189,14 @@ parameter list in order: [: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 (`?`). + + +```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 of identifying a named parameter: * a keyword or symbol that begins with `?` @@ -206,6 +216,18 @@ call as the `:params` key of the options hash map. ;;=> ["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 In addition to the hash map (and sequences) approach of building diff --git a/doc/options.md b/doc/options.md index e6cb061..739fb2e 100644 --- a/doc/options.md +++ b/doc/options.md @@ -19,6 +19,7 @@ All options may be omitted. The default behavior of each option is described in * `: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, * `: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.next, * `: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-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, @@ -29,6 +30,7 @@ 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. @@ -96,6 +98,13 @@ was wrapped in `[:inline `..`]`: > 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` The `:params` option provides a mapping from named parameters diff --git a/doc/postgresql.md b/doc/postgresql.md index 4721f30..6b9dc71 100644 --- a/doc/postgresql.md +++ b/doc/postgresql.md @@ -10,6 +10,12 @@ Everything that the nilenso library provided (in 0.4.112) is implemented directly in HoneySQL 2.x although a few things have a 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.next. You may find it convenient to set this option +globally, via `set-options!`. + ## Code Examples The code examples herein assume: diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 7a93c7b..134ef7e 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -120,6 +120,7 @@ (def ^:private default-quoted-snake (atom nil)) (def ^:private default-inline (atom nil)) (def ^:private default-checking (atom :none)) +(def ^:private default-numbered (atom false)) (def ^:private ^:dynamic *dialect* nil) ;; nil would be a better default but that makes testing individual @@ -1667,7 +1668,10 @@ ([data opts] (let [cache (:cache opts) dialect? (contains? opts :dialect) - dialect (when dialect? (get @dialects (check-dialect (:dialect opts))))] + dialect (when dialect? (get @dialects (check-dialect (:dialect opts)))) + numbered (if (contains? opts :numbered) + (:numbered opts) + @default-numbered)] (binding [*dialect* (if dialect? dialect @default-dialect) *caching* cache *checking* (if (contains? opts :checking) @@ -1681,7 +1685,7 @@ *inline* (if (contains? opts :inline) (:inline opts) @default-inline) - *numbered* (when (:numbered opts) + *numbered* (when numbered (atom [])) *quoted* (cond (contains? opts :quoted) (:quoted opts) @@ -1722,11 +1726,12 @@ "Set default values for any or all of the following options: * :checking * :inline + * :numbered * :quoted * :quoted-snake Note that calling `set-dialect!` can override the default for `:quoted`." [opts] - (let [unknowns (dissoc opts :checking :inline :quoted :quoted-snake)] + (let [unknowns (dissoc opts :checking :inline :numbered :quoted :quoted-snake)] (when (seq unknowns) (throw (ex-info (str (str/join ", " (keys unknowns)) " are not options that can be set globally.") @@ -1734,11 +1739,13 @@ (when (contains? opts :checking) (reset! default-checking (:checking opts))) (when (contains? opts :inline) - (reset! default-checking (:inline opts))) + (reset! default-inline (:inline opts))) + (when (contains? opts :numbered) + (reset! default-numbered (:numbered opts))) (when (contains? opts :quoted) - (reset! default-checking (:quoted opts))) + (reset! default-quoted (:quoted opts))) (when (contains? opts :quoted-snake) - (reset! default-checking (:quoted-snake opts))))) + (reset! default-quoted-snake (:quoted-snake opts))))) (defn clause-order "Return the current order that known clauses will be applied when diff --git a/test/honey/sql/helpers_test.cljc b/test/honey/sql/helpers_test.cljc index 33ee9bf..be159ba 100644 --- a/test/honey/sql/helpers_test.cljc +++ b/test/honey/sql/helpers_test.cljc @@ -1,4 +1,4 @@ -;; copyright (c) 2020-2021 sean corfield, all rights reserved +;; copyright (c) 2020-2022 sean corfield, all rights reserved (ns honey.sql.helpers-test (:refer-clojure :exclude [filter for group-by partition-by set update])