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 cbaea22..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 @@ -140,6 +141,7 @@ (def ^:private ^:dynamic *dsl* nil) ;; caching data to detect expressions that cannot be cached: (def ^:private ^:dynamic *caching* nil) +(def ^:private ^:dynamic *numbered* nil) ;; clause helpers @@ -321,6 +323,18 @@ {::wrapper (fn [fk _] (param-value (fk)))})) +(defn ->numbered [v] + (let [n (count (swap! *numbered* conj v))] + [(str "$" n) (with-meta (constantly (dec n)) + {::wrapper + (fn [fk _] (get @*numbered* (fk)))})])) + +(defn ->numbered-param [k] + (let [n (count (swap! *numbered* conj k))] + [(str "$" n) (with-meta (constantly (dec n)) + {::wrapper + (fn [fk _] (param-value (get @*numbered* (fk))))})])) + (def ^:private ^:dynamic *formatted-column* (atom false)) (defn- format-var [x & [opts]] @@ -335,9 +349,12 @@ "(" (str/join ", " quoted-args) ")")]) (= \? (first c)) (let [k (keyword (subs c 1))] - (if *inline* - [(sqlize-value (param-value k))] - ["?" (->param k)])) + (cond *inline* + [(sqlize-value (param-value k))] + *numbered* + (->numbered-param k) + :else + ["?" (->param k)])) (= \' (first c)) (do (reset! *formatted-column* true) @@ -1279,30 +1296,44 @@ (defn- format-in [in [x y]] (let [[sql-x & params-x] (format-expr x {:nested true}) [sql-y & params-y] (format-expr y {:nested true}) - values (unwrap (first params-y) {})] + [v1 :as values] (map #(unwrap % {}) params-y)] ;; #396: prevent caching IN () when named parameter is used: (when (and (meta (first params-y)) (::wrapper (meta (first params-y))) *caching*) (throw (ex-info "SQL that includes IN () expressions cannot be cached" {}))) (when-not (= :none *checking*) - (when (or (and (sequential? y) (empty? y)) - (and (sequential? values) (empty? values))) + (when (or (and (sequential? y) (empty? y)) + (and (sequential? v1) (empty? v1))) (throw (ex-info "IN () empty collection is illegal" {:clause [in x y]}))) (when (and (= :strict *checking*) - (or (and (sequential? y) (some nil? y)) - (and (sequential? values) (some nil? values)))) + (or (and (sequential? y) (some nil? y)) + (and (sequential? v1) (some nil? v1)))) (throw (ex-info "IN (NULL) does not match" {:clause [in x y]})))) - (if (and (= "?" sql-y) (= 1 (count params-y)) (coll? values)) - (let [sql (str "(" (str/join ", " (repeat (count values) "?")) ")")] - (-> [(str sql-x " " (sql-kw in) " " sql)] - (into params-x) - (into values))) - (-> [(str sql-x " " (sql-kw in) " " sql-y)] - (into params-x) - (into params-y))))) + (cond (and (not *numbered*) + (= "?" sql-y) + (= 1 (count params-y)) + (coll? v1)) + (let [sql (str "(" (str/join ", " (repeat (count v1) "?")) ")")] + (-> [(str sql-x " " (sql-kw in) " " sql)] + (into params-x) + (into v1))) + (and *numbered* + (= (str "$" (count @*numbered*)) sql-y) + (= 1 (count params-y)) + (coll? v1)) + (let [vs (for [v v1] (->numbered v)) + sql (str "(" (str/join ", " (map first vs)) ")")] + (-> [(str sql-x " " (sql-kw in) " " sql)] + (into params-x) + (conj nil) + (into (map second vs)))) + :else + (-> [(str sql-x " " (sql-kw in) " " sql-y)] + (into params-x) + (into (if *numbered* values params-y)))))) (defn- function-0 [k xs] [(str (sql-kw k) @@ -1465,13 +1496,16 @@ (into [(str "LATERAL " sql)] params)))) :lift (fn [_ [x]] - (if *inline* - ;; this is pretty much always going to be wrong, - ;; but it could produce a valid result so we just - ;; assume that the user knows what they are doing: - [(sqlize-value x)] - ["?" (with-meta (constantly x) - {::wrapper (fn [fx _] (fx))})])) + (cond *inline* + ;; this is pretty much always going to be wrong, + ;; but it could produce a valid result so we just + ;; assume that the user knows what they are doing: + [(sqlize-value x)] + *numbered* + (->numbered x) + :else + ["?" (with-meta (constantly x) + {::wrapper (fn [fx _] (fx))})])) :nest (fn [_ [x]] (let [[sql & params] (format-expr x)] @@ -1503,9 +1537,12 @@ (into [(str/join ", " sqls)] params))) :param (fn [_ [k]] - (if *inline* - [(sqlize-value (param-value k))] - ["?" (->param k)])) + (cond *inline* + [(sqlize-value (param-value k))] + *numbered* + (->numbered-param k) + :else + ["?" (->param k)])) :raw (fn [_ [xs]] (raw-render xs)) @@ -1586,9 +1623,12 @@ ["NULL"] :else - (if *inline* - [(sqlize-value expr)] - ["?" expr]))) + (cond *inline* + [(sqlize-value expr)] + *numbered* + (->numbered expr) + :else + ["?" expr]))) (defn- check-dialect [dialect] (when-not (contains? @dialects dialect) @@ -1628,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) @@ -1642,6 +1685,8 @@ *inline* (if (contains? opts :inline) (:inline opts) @default-inline) + *numbered* (when numbered + (atom [])) *quoted* (cond (contains? opts :quoted) (:quoted opts) dialect? @@ -1681,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.") @@ -1693,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 ec2af97..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]) @@ -322,7 +322,17 @@ (sql/format {:select [:*] :from [:customers] :where [:in :id :?ids]} - {:params {:ids values}}))))))) + {:params {:ids values}}))) + (is (= ["SELECT * FROM customers WHERE id IN ($1, $2)" "1" "2"] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id values]} + {:numbered true}))) + (is (= ["SELECT * FROM customers WHERE id IN ($2, $3)" nil "1" "2"] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id :?ids]} + {:params {:ids values} :numbered true}))))))) (deftest test-case (is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo MOD ?) = ?) THEN foo / ? ELSE ? END FROM bar" diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 1b54476..be2d003 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -94,6 +94,38 @@ (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4] (sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true})))) +(deftest general-numbered-tests + (is (= ["SELECT * FROM \"table\" WHERE \"id\" = $1" 1] + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" WHERE \"id\" = $1" 1] + (sut/format {:select [:*] :from [:table] :where (sut/map= {:id 1})} + {:quoted true :numbered true}))) + (is (= ["SELECT \"t\".* FROM \"table\" AS \"t\" WHERE \"id\" = $1" 1] + (sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""] + (sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"] + (sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"] + (sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"] + (sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL $1 DAYS) < NOW()" 30] + (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} + {:quoted true :numbered true}))) + (is (= ["SELECT * FROM `table` WHERE `id` = $1" 1] + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} + {:dialect :mysql :numbered true}))) + (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN ($1, $2, $3, $4)" 1 2 3 4] + (sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} + {:quoted true :numbered true})))) + ;; issue-based tests (deftest subquery-alias-263 @@ -852,7 +884,13 @@ ORDER BY id = ? DESC {:params {:y [nil]}}))) (is (= ["WHERE x IN (?)" nil] (format {:where [:in :x :?y]} - {:params {:y [nil]} :checking :basic})))) + {:params {:y [nil]} :checking :basic}))) + (is (= ["WHERE x IN ($2)" nil nil] + (format {:where [:in :x :?y]} + {:params {:y [nil]} :numbered true}))) + (is (= ["WHERE x IN ($2)" nil nil] + (format {:where [:in :x :?y]} + {:params {:y [nil]} :checking :basic :numbered true})))) (testing "IN NULL is flagged in strict mode" (is (thrown-with-msg? ExceptionInfo #"does not match" (format {:where [:in :x [nil]]}