From 1710e072318e90d58ee434b4a6766f32d0218d25 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Thu, 13 Apr 2023 22:46:37 -0700 Subject: [PATCH] fix #486 by support ansi/postgresl interval --- CHANGELOG.md | 1 + doc/differences-from-1-x.md | 2 +- doc/special-syntax.md | 9 ++++++--- src/honey/sql.cljc | 11 +++++++++-- test/honey/sql_test.cljc | 9 +++++++++ 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d1689..33fb135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changes * 2.4.next in progress + * Fix [#486](https://github.com/seancorfield/honeysql/issues/486) by supporting ANSI-style `INTERVAL` syntax. * Fix [#484](https://github.com/seancorfield/honeysql/issues/484) by adding `TABLE` to `TRUNCATE`. * Fix [#483](https://github.com/seancorfield/honeysql/issues/483) by adding a function-like `:join` syntax to produce nested `JOIN` expressions. * 2.4.1011 -- 2023-03-23 diff --git a/doc/differences-from-1-x.md b/doc/differences-from-1-x.md index b59f950..eb1b84d 100644 --- a/doc/differences-from-1-x.md +++ b/doc/differences-from-1-x.md @@ -133,7 +133,7 @@ The following new syntax has been added: * `:default` -- for `DEFAULT` values (in inserts) and for declaring column defaults in table definitions, * `:escape` -- used to wrap a regular expression so that non-standard escape characters can be provided, * `:inline` -- used as a function to replace the `sql/inline` / `#sql/inline` machinery, -* `:interval` -- used as a function to support `INTERVAL `, e.g., `[:interval 30 :days]` for databases that support it (e.g., MySQL), +* `:interval` -- used as a function to support `INTERVAL `, e.g., `[:interval 30 :days]` for databases that support it (e.g., MySQL) and, as of 2.4.next, for `INTERVAL 'n units'`, e.g., `[:interval "24 hours"]` for ANSI/PostgreSQL. * `:lateral` -- used to wrap a statement or expression, to provide a `LATERAL` join, * `:lift` -- used as a function to prevent interpretation of a Clojure data structure as DSL syntax (e.g., when passing a vector or hash map as a parameter value) -- this should mostly be a replacement for `honeysql.format/value`, * `:nest` -- used as a function to add an extra level of nesting (parentheses) around an expression, diff --git a/doc/special-syntax.md b/doc/special-syntax.md index a24f1f9..4f1deb4 100644 --- a/doc/special-syntax.md +++ b/doc/special-syntax.md @@ -210,15 +210,18 @@ than turning it into a positional parameter: ## interval -Accepts two arguments: an expression and a keyword (or a symbol) -that represents a time unit. Produces an `INTERVAL` expression: +Accepts one or two arguments: either a string or an expression and +a keyword (or a symbol) that represents a time unit. +Produces an `INTERVAL` expression: ```clojure (sql/format-expr [:date_add [:now] [:interval 30 :days]]) ;;=> ["DATE_ADD(NOW(), INTERVAL ? DAYS)" 30] +(sql/format-expr [:date_add [:now] [:interval "24 Hours"]]) +;;=> ["DATE_ADD(NOW(), INTERVAL '24 Hours')"] ``` -> Note: PostgreSQL has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector). +> Note: PostgreSQL also has an `INTERVAL` data type which is unrelated to this syntax. In PostgreSQL, the closet equivalent would be `[:cast "30 days" :interval]` which will lift `"30 days"` out as a parameter. In DDL, for PostgreSQL, you can use `:interval` to produce the `INTERVAL` data type (without wrapping it in a vector). ## join diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 68cda52..5b564a1 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -1543,8 +1543,12 @@ (format-expr x))) :interval (fn [_ [n units]] - (let [[sql & params] (format-expr n)] - (into [(str "INTERVAL " sql " " (sql-kw units))] params))) + (if units + (let [[sql & params] (format-expr n)] + (into [(str "INTERVAL " sql " " (sql-kw units))] params)) + (binding [*inline* true] + (let [[sql & params] (format-expr n)] + (into [(str "INTERVAL " sql)] params))))) :join (fn [_ [e & js]] (let [[sqls params] (reduce-sql (cons (format-expr e) @@ -2010,6 +2014,9 @@ (format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {}) (format-expr [:interval 30 :days]) + (format {:select [:*] :from [:table] + :where [:< [:date_add :expiry [:interval "30 Days"]] [:now]]} {}) + (format-expr [:interval "30 Days"]) (format {:select [:*] :from [:table] :where [:= :id (int 1)]} {:dialect :mysql}) (map fn? (format {:select [:*] :from [:table] diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 2e8fa0d..80fa6bb 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -68,6 +68,10 @@ (is (= ["INTERVAL ? DAYS" 30] (sut/format-expr [:interval 30 :days])))) +(deftest issue-486-interval + (is (= ["INTERVAL '30 Days'"] + (sut/format-expr [:interval "30 Days"])))) + (deftest issue-455-null (is (= ["WHERE (abc + ?) IS NULL" "abc"] (sut/format {:where [:= [:+ :abc "abc"] nil]})))) @@ -97,6 +101,8 @@ (sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true}))) (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL ? DAYS) < NOW()" 30] (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL '30 Days') < NOW()"] + (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval "30 Days"]] [:now]]} {:quoted true}))) (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}))) (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4] @@ -127,6 +133,9 @@ (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL $1 DAYS) < NOW()" 30] (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true :numbered true}))) + (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL '30 Days') < NOW()"] + (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval "30 Days"]] [:now]]} + {:quoted true :numbered true}))) (is (= ["SELECT * FROM `table` WHERE `id` = $1" 1] (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql :numbered true})))