Compare commits

...

5 commits

Author SHA1 Message Date
Sean Corfield
f5dbc274be
fixes #440 by supporting multiple tables in truncate
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-04-23 17:45:59 -04:00
Sean Corfield
df753e8635
update tools.build
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-27 23:06:49 -07:00
Sean Corfield
b4b2ca7d79
add general using-* index support
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-26 10:28:57 -07:00
Sean Corfield
78f7d5282f
update dev/build deps
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-25 17:19:42 -07:00
Sean Corfield
024d17b11e
fixes #571 by supporting empty order by clause
Signed-off-by: Sean Corfield <sean@corfield.org>
2025-03-22 10:34:57 -07:00
10 changed files with 110 additions and 46 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1488' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:

View file

@ -17,7 +17,7 @@ jobs:
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1488' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -49,7 +49,7 @@ jobs:
- name: Clojure CLI - name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1488' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:

View file

@ -18,7 +18,7 @@ jobs:
- name: Clojure CLI - name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1488' cli: '1.12.0.1530'
bb: latest bb: latest
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4

View file

@ -17,7 +17,7 @@ jobs:
- name: Clojure CLI - name: Clojure CLI
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1488' cli: '1.12.0.1530'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:

View file

@ -1,5 +1,11 @@
# Changes # Changes
* 2.7.next in progress
* Address [#440](https://github.com/seancorfield/honeysql/issues/440) by supporting multiple tables in `:truncate`.
* Support `USING HASH` as well as `USING GIN`.
* Fix [#571](https://github.com/seancorfield/honeysql/issues/571) by allowing `:order-by` to take an empty sequence of columns (and be omitted).
* Update dev/build deps.
* 2.7.1295 -- 2025-03-12 * 2.7.1295 -- 2025-03-12
* Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax. * Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax.
* Drop support for Clojure 1.9 [#561](https://github.com/seancorfield/honeysql/issues/561). * Drop support for Clojure 1.9 [#561](https://github.com/seancorfield/honeysql/issues/561).

View file

@ -3,7 +3,7 @@
:deps {org.clojure/clojure {:mvn/version "1.10.3"}} :deps {org.clojure/clojure {:mvn/version "1.10.3"}}
:aliases :aliases
{;; for help: clojure -A:deps -T:build help/doc {;; for help: clojure -A:deps -T:build help/doc
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.6"} :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.8"}
slipset/deps-deploy {:mvn/version "0.2.2"}} slipset/deps-deploy {:mvn/version "0.2.2"}}
:ns-default build} :ns-default build}
@ -30,7 +30,7 @@
:main-opts ["-m" "cljs-test-runner.main"]} :main-opts ["-m" "cljs-test-runner.main"]}
:gen-doc-tests {:replace-paths ["build"] :gen-doc-tests {:replace-paths ["build"]
:extra-deps {babashka/fs {:mvn/version "0.5.22"} :extra-deps {babashka/fs {:mvn/version "0.5.24"}
com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}} com.github.lread/test-doc-blocks {:mvn/version "1.1.20"}}
:main-opts ["-m" "honey.gen-doc-tests"]} :main-opts ["-m" "honey.gen-doc-tests"]}

View file

@ -149,6 +149,14 @@ user=> (sql/format {:create-index [:my-idx [:fruit :using-gin :appearance]]})
["CREATE INDEX my_idx ON fruit USING GIN (appearance)"] ["CREATE INDEX my_idx ON fruit USING GIN (appearance)"]
``` ```
As of 2.7.next, `USING HASH` index creation is also possible using the keyword
`:using-hash` after the table name (or the symbol `using-hash`):
```clojure
user=> (sql/format {:create-index [:my-idx [:fruit :using-hash :appearance]]})
["CREATE INDEX my_idx ON fruit USING HASH (appearance)"]
```
### rename-table ### rename-table
Used with `:alter-table`, Used with `:alter-table`,
@ -822,13 +830,23 @@ is a "hard" delete as opposed to a temporal delete.
## truncate ## truncate
`:truncate` accepts a simple SQL entity (table name) `:truncate` accepts a simple SQL entity (table name)
or a table name followed by various options: or a table name followed by various options, or a
sequence that starts with a sequence of one or more table names,
optionally followed by various options:
```clojure ```clojure
user=> (sql/format '{truncate transport}) user=> (sql/format '{truncate transport})
["TRUNCATE TABLE transport"] ["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport)})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate (transport restart identity)}) user=> (sql/format '{truncate (transport restart identity)})
["TRUNCATE TABLE transport RESTART IDENTITY"] ["TRUNCATE TABLE transport RESTART IDENTITY"]
user=> (sql/format '{truncate ((transport))})
["TRUNCATE TABLE transport"]
user=> (sql/format '{truncate ((transport other))})
["TRUNCATE TABLE transport, other"]
user=> (sql/format '{truncate ((transport other) restart identity)})
["TRUNCATE TABLE transport, other RESTART IDENTITY"]
``` ```
## columns ## columns
@ -1070,6 +1088,9 @@ The `:where` clause can have a single SQL expression, or
a sequence of SQL expressions prefixed by either `:and` a sequence of SQL expressions prefixed by either `:and`
or `:or`. See examples of `:where` in various clauses above. or `:or`. See examples of `:where` in various clauses above.
If `:where` is given an empty sequence, the `WHERE` clause will
be omitted from the generated SQL.
Sometimes it is convenient to construct a `WHERE` clause that Sometimes it is convenient to construct a `WHERE` clause that
tests several columns for equality, and you might have a Clojure tests several columns for equality, and you might have a Clojure
hash map containing those values. `honey.sql/map=` exists to hash map containing those values. `honey.sql/map=` exists to
@ -1210,12 +1231,15 @@ user=> (sql/format {:select [[[:over
## order-by ## order-by
`:order-by` accepts a sequence of one or more ordering `:order-by` accepts a sequence of zero or more ordering
expressions. Each ordering expression is either a simple expressions. Each ordering expression is either a simple
SQL entity or a pair of a SQL expression and a direction SQL entity or a pair of a SQL expression and a direction
(which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`, (which can be `:asc`, `:desc`, `:nulls-first`, `:desc-null-last`,
etc -- or the symbol equivalent). etc -- or the symbol equivalent).
If `:order-by` is given an empty sequence, the `ORDER BY` clause will
be omitted from the generated SQL.
If you want to order by an expression, you should wrap it If you want to order by an expression, you should wrap it
as a pair with a direction: as a pair with a direction:

View file

@ -370,6 +370,20 @@
(keyword (name s))) (keyword (name s)))
s)) s))
(defn- kw->sym
"Given a keyword, produce a symbol, retaining the namespace
qualifier, if any."
[k]
(if (keyword? k)
#?(:bb (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k)))
:clj (.sym ^clojure.lang.Keyword k)
:default (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k))))
k))
(defn- inline-map [x & [open close]] (defn- inline-map [x & [open close]]
(str (or open "{") (str (or open "{")
(join ", " (map (fn [[k v]] (join ", " (map (fn [[k v]]
@ -445,8 +459,8 @@
(if (str/starts-with? c "'") (if (str/starts-with? c "'")
(do (do
(reset! *formatted-column* true) (reset! *formatted-column* true)
[(subs c 1)]) (subs c 1))
[(format-entity x opts)]))) (format-entity x opts))))
(defn- format-var (defn- format-var
([x] (format-var x {})) ([x] (format-var x {}))
@ -473,7 +487,7 @@
:else :else
["?" (->param k)])) ["?" (->param k)]))
:else :else
(format-simple-var x c opts))))) [(format-simple-var x c opts)]))))
(defn- format-entity-alias [x] (defn- format-entity-alias [x]
(cond (sequential? x) (cond (sequential? x)
@ -1119,11 +1133,13 @@
dirs (map #(when (sequential? %) (second %)) xs) dirs (map #(when (sequential? %) (second %)) xs)
[sqls params] [sqls params]
(format-expr-list (map #(if (sequential? %) (first %) %) xs))] (format-expr-list (map #(if (sequential? %) (first %) %) xs))]
(if (seq sqls)
(into [(str (sql-kw k) " " (into [(str (sql-kw k) " "
(join ", " (map (fn [sql dir] (join ", " (map (fn [sql dir]
(str sql " " (sql-kw (or dir :asc)))) (str sql " " (sql-kw (or dir :asc))))
sqls sqls
dirs)))] params))) dirs)))] params)
[])))
(defn- format-lock-strength [k xs] (defn- format-lock-strength [k xs]
(let [[strength tables nowait] (ensure-sequential xs)] (let [[strength tables nowait] (ensure-sequential xs)]
@ -1372,20 +1388,17 @@
[(butlast coll) (last coll) nil]))] [(butlast coll) (last coll) nil]))]
(into [(join " " (map sql-kw) prequel) (into [(join " " (map sql-kw) prequel)
(when table (when table
(let [[v & more] (format-simple-var table)] (format-simple-var table))
(when (seq more)
(throw (ex-info (str "DDL syntax error at: "
(pr-str table)
" - expected table name")
{:unexpected more})))
v))
(when ine (sql-kw ine))] (when ine (sql-kw ine))]
(when opts (when opts
(format-ddl-options opts context))))) (format-ddl-options opts context)))))
(defn- format-truncate [_ xs] (defn- format-truncate [_ xs]
(let [[table & options] (ensure-sequential xs) (let [[table & options] (ensure-sequential xs)
[pre table ine options] (destructure-ddl-item [table options] "truncate")] table (if (or (ident? table) (string? table))
(format-simple-var table)
(join ", " (map format-simple-var table)))
[pre _ ine options] (destructure-ddl-item [nil options] "truncate")]
(when (seq pre) (throw (ex-info "TRUNCATE syntax error" {:unexpected pre}))) (when (seq pre) (throw (ex-info "TRUNCATE syntax error" {:unexpected pre})))
(when (seq ine) (throw (ex-info "TRUNCATE syntax error" {:unexpected ine}))) (when (seq ine) (throw (ex-info "TRUNCATE syntax error" {:unexpected ine})))
[(join " " (cond-> ["TRUNCATE TABLE" table] [(join " " (cond-> ["TRUNCATE TABLE" table]
@ -1398,6 +1411,11 @@
(destructure-ddl-item [:id [:int :unsigned :auto-increment]] "test") (destructure-ddl-item [:id [:int :unsigned :auto-increment]] "test")
(destructure-ddl-item [[[:foreign-key :bar]] :quux [[:wibble :wobble]]] "test") (destructure-ddl-item [[[:foreign-key :bar]] :quux [[:wibble :wobble]]] "test")
(format-truncate :truncate [:foo]) (format-truncate :truncate [:foo])
(format-truncate :truncate ["foo, bar"])
(format-truncate :truncate "foo, bar")
(format-truncate :truncate [[:foo :bar]])
(format-truncate :truncate :foo)
(format {:truncate [[:foo] :x]})
) )
(defn- format-create [q k item as] (defn- format-create [q k item as]
@ -1416,10 +1434,12 @@
(defn- format-create-index [k clauses] (defn- format-create-index [k clauses]
(let [[index-spec [table & exprs]] clauses (let [[index-spec [table & exprs]] clauses
[pre entity ine & more] (destructure-ddl-item index-spec (str (sql-kw k) " options")) [pre entity ine & more] (destructure-ddl-item index-spec (str (sql-kw k) " options"))
[using & exprs] (if (contains? #{:using-gin 'using-gin} [using & exprs]
(first exprs)) (let [item (first exprs)]
(if (and (ident? item)
(str/starts-with? (str (kw->sym item)) "using-"))
exprs exprs
(cons nil exprs)) (cons nil exprs)))
[sqls params] (format-expr-list exprs)] [sqls params] (format-expr-list exprs)]
(into [(join " " (remove empty?) (into [(join " " (remove empty?)
(-> ["CREATE" pre "INDEX" ine entity (-> ["CREATE" pre "INDEX" ine entity
@ -1730,20 +1750,6 @@
(set @current-clause-order) (set @current-clause-order)
(set (keys @clause-format)))) (set (keys @clause-format))))
(defn- kw->sym
"Given a keyword, produce a symbol, retaining the namespace
qualifier, if any."
[k]
(if (keyword? k)
#?(:bb (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k)))
:clj (.sym ^clojure.lang.Keyword k)
:default (if-let [n (namespace k)]
(symbol n (name k))
(symbol (name k))))
k))
(defn format-dsl (defn format-dsl
"Given a hash map representing a SQL statement and a hash map "Given a hash map representing a SQL statement and a hash map
of options, return a vector containing a string -- the formatted of options, return a vector containing a string -- the formatted

View file

@ -1038,11 +1038,15 @@
(sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column])))) (sql/format (create-index [:unique :my-column-idx :if-not-exists] [:my-table :my-column]))))
(is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"] (is (= ["CREATE INDEX my_column_idx ON my_table (LOWER(my_column))"]
(sql/format (create-index :my-column-idx [:my-table :%lower.my-column]))))) (sql/format (create-index :my-column-idx [:my-table :%lower.my-column])))))
(testing "PostgreSQL extensions (USING GIN)" (testing "PostgreSQL extensions (USING GIN/HASH)"
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"] (is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
(sql/format {:create-index [:my-column-idx [:my-table :using-gin :my-column]]}))) (sql/format {:create-index [:my-column-idx [:my-table :using-gin :my-column]]})))
(is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"] (is (= ["CREATE INDEX my_column_idx ON my_table USING GIN (my_column)"]
(sql/format (create-index :my-column-idx [:my-table :using-gin :my-column])))))) (sql/format (create-index :my-column-idx [:my-table :using-gin :my-column]))))
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
(sql/format {:create-index [:my-column-idx [:my-table :using-hash :my-column]]})))
(is (= ["CREATE INDEX my_column_idx ON my_table USING HASH (my_column)"]
(sql/format (create-index :my-column-idx [:my-table :using-hash :my-column]))))))
(deftest join-with-alias (deftest join-with-alias
(is (= ["SELECT * FROM foo LEFT JOIN (populatons AS pm INNER JOIN customers AS pc ON (pm.id = pc.id) AND (pm.other_id = pc.other_id)) ON foo.fk_id = pm.id"] (is (= ["SELECT * FROM foo LEFT JOIN (populatons AS pm INNER JOIN customers AS pc ON (pm.id = pc.id) AND (pm.other_id = pc.other_id)) ON foo.fk_id = pm.id"]

View file

@ -614,6 +614,12 @@
(format {:dialect :mysql})))) (format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"] (is (= ["TRUNCATE TABLE `foo` CONTINUE IDENTITY"]
(-> {:truncate [:foo :continue :identity]} (-> {:truncate [:foo :continue :identity]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2`"]
(-> {:truncate [[:t1 :t2]]}
(format {:dialect :mysql}))))
(is (= ["TRUNCATE TABLE `t1`, `t2` CONTINUE IDENTITY"]
(-> {:truncate [[:t1 :t2] :continue :identity]}
(format {:dialect :mysql}))))) (format {:dialect :mysql})))))
(deftest inlined-values-are-stringified-correctly (deftest inlined-values-are-stringified-correctly
@ -1486,6 +1492,24 @@ ORDER BY id = ? DESC
(h/select :*) (h/select :*)
(h/from :table))))))) (h/from :table)))))))
(deftest issue-571
(testing "an empty where clause is omitted"
(is (= ["SELECT * FROM foo"]
(sut/format {:select :* :from :foo :where []})))
#?(:clj
(is (thrown? clojure.lang.ExceptionInfo
(sut/format {:select :* :from :foo :where nil}))))
(is (= ["SELECT * FROM foo WHERE 1 = 1"]
(sut/format {:select :* :from :foo :where [:= 1 1]} {:inline true}))))
(testing "an empty order by clause is omitted"
(is (= ["SELECT * FROM foo"]
(sut/format {:select :* :from :foo :order-by []})))
#?(:clj
(is (thrown? clojure.lang.ExceptionInfo
(sut/format {:select :* :from :foo :order-by nil}))))
(is (= ["SELECT * FROM foo ORDER BY bar ASC"]
(sut/format {:select :* :from :foo :order-by [:bar]})))))
(comment (comment
;; partial (incorrect!) workaround for #407: ;; partial (incorrect!) workaround for #407:
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]}) (sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})