fixes #558 by implementing patch-into

also fix records helper and document a some more xtdb support

Signed-off-by: Sean Corfield <sean@corfield.org>
This commit is contained in:
Sean Corfield 2024-12-11 11:50:47 -08:00
parent e0356bc9c5
commit e4762a1a70
No known key found for this signature in database
6 changed files with 66 additions and 15 deletions

View file

@ -1,6 +1,7 @@
# Changes # Changes
* 2.6.next in progress * 2.6.next in progress
* Address [#558](https://github.com/seancorfield/honeysql/issues/558) by adding `:patch-into` (and `patch-into` helper) for XTDB (but in core).
* Address [#555](https://github.com/seancorfield/honeysql/issues/555) by supporting `SETTING` clause for XTDB. * Address [#555](https://github.com/seancorfield/honeysql/issues/555) by supporting `SETTING` clause for XTDB.
* Replace `assert` calls with proper validation, throwing `ex-info` on failure (like other existing validation in HoneySQL). * Replace `assert` calls with proper validation, throwing `ex-info` on failure (like other existing validation in HoneySQL).
* Experimental `:xtdb` dialect removed (since XTDB no longer supports qualified column names). * Experimental `:xtdb` dialect removed (since XTDB no longer supports qualified column names).

View file

@ -670,9 +670,9 @@ user=> (sql/format '{select * bulk-collect-into [arrv 100] from mytable})
["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100] ["SELECT * BULK COLLECT INTO arrv LIMIT ? FROM mytable" 100]
``` ```
## insert-into, replace-into ## insert-into, replace-into, patch-into
There are three use cases with `:insert-into`. There are three use cases with `:insert-into` etc.
The first case takes just a table specifier (either a The first case takes just a table specifier (either a
table name or a table/alias pair), table name or a table/alias pair),
@ -690,6 +690,10 @@ For the first and second cases, you'll use the `:values` clause
to specify rows of values to insert. See [**values**](#values) below to specify rows of values to insert. See [**values**](#values) below
for more detail on the `:values` clause. for more detail on the `:values` clause.
`:patch-into` is only supported by XTDB but is
part of HoneySQL's "core" dialect anyway. It produces a `PATCH INTO`
statement but otherwise has identical syntax to `:insert-into`.
`:replace-into` is only supported by MySQL and SQLite but is `:replace-into` is only supported by MySQL and SQLite but is
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO` part of HoneySQL's "core" dialect anyway. It produces a `REPLACE INTO`
statement but otherwise has identical syntax to `:insert-into`. statement but otherwise has identical syntax to `:insert-into`.
@ -794,7 +798,7 @@ You can also `UPDATE .. FROM (VALUES ..) ..` where you might also need `:composi
["UPDATE table SET a = v.a FROM (VALUES (?, ?, ?), (?, ?, ?)) AS v (a, b, c) WHERE (x = v.b) AND (y > v.c)" 1 2 3 4 5 6] ["UPDATE table SET a = v.a FROM (VALUES (?, ?, ?), (?, ?, ?)) AS v (a, b, c) WHERE (x = v.b) AND (y > v.c)" 1 2 3 4 5 6]
``` ```
## delete, delete-from ## delete, delete-from, erase-from
`:delete-from` is the simple use case here, accepting just a `:delete-from` is the simple use case here, accepting just a
SQL entity (table name). `:delete` allows for deleting from SQL entity (table name). `:delete` allows for deleting from
@ -811,6 +815,10 @@ user=> (sql/format {:delete [:order :item]
["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42] ["DELETE order, item FROM order INNER JOIN item ON order.item_id = item.id WHERE item.id = ?" 42]
``` ```
`:erase-from` is only supported by XTDB and produces an `ERASE FROM`
statement but otherwise has identical syntax to `:delete-from`. It
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)

View file

@ -62,7 +62,7 @@
:records :records
:distinct :expr :exclude :rename :distinct :expr :exclude :rename
:into :bulk-collect-into :into :bulk-collect-into
:insert-into :replace-into :update :delete :delete-from :erase-from :truncate :insert-into :patch-into :replace-into :update :delete :delete-from :erase-from :truncate
:columns :set :from :using :columns :set :from :using
:join-by :join-by
:join :left-join :right-join :inner-join :outer-join :full-join :join :left-join :right-join :inner-join :outer-join :full-join
@ -792,6 +792,7 @@
(defn- format-columns [k xs] (defn- format-columns [k xs]
(if (and (= :columns k) (if (and (= :columns k)
(or (contains-clause? :insert-into) (or (contains-clause? :insert-into)
(contains-clause? :patch-into)
(contains-clause? :replace-into))) (contains-clause? :replace-into)))
[] []
(let [[sqls params] (format-expr-list xs {:drop-ns true})] (let [[sqls params] (format-expr-list xs {:drop-ns true})]
@ -1213,6 +1214,7 @@
;; [{:a 1 :b 2 :c 3}] ;; [{:a 1 :b 2 :c 3}]
(let [[cols cols-sql] (let [[cols cols-sql]
(columns-from-values xs (or (contains-clause? :insert-into) (columns-from-values xs (or (contains-clause? :insert-into)
(contains-clause? :patch-into)
(contains-clause? :replace-into) (contains-clause? :replace-into)
(contains-clause? :columns))) (contains-clause? :columns)))
[sqls params] [sqls params]
@ -1684,6 +1686,7 @@
:into #'format-select-into :into #'format-select-into
:bulk-collect-into #'format-select-into :bulk-collect-into #'format-select-into
:insert-into #'format-insert :insert-into #'format-insert
:patch-into #'format-insert
:replace-into #'format-insert :replace-into #'format-insert
:update (check-where #'format-selector) :update (check-where #'format-selector)
:delete (check-where #'format-selects) :delete (check-where #'format-selects)

View file

@ -504,9 +504,10 @@
(generic :select-distinct-top args)) (generic :select-distinct-top args))
(defn records (defn records
"Produces RECORDS {...}, {...}, ..." "Produces RECORDS {...}, {...}, ...
Like `values` so it accepts a collection of maps."
[& args] [& args]
(generic :records args)) (generic-1 :records args))
(defn distinct (defn distinct
"Like `select-distinct` but produces DISTINCT..." "Like `select-distinct` but produces DISTINCT..."
@ -541,6 +542,14 @@
[& args] [& args]
(generic :bulk-collect-into args)) (generic :bulk-collect-into args))
(defn- stuff-into [k args]
(let [[data & args :as args']
(if (map? (first args)) args (cons {} args))
[table cols statement] args]
(if (and (sequential? cols) (map? statement))
(generic k [data [table cols] statement])
(generic k args'))))
(defn insert-into (defn insert-into
"Accepts a table name or a table/alias pair. That "Accepts a table name or a table/alias pair. That
can optionally be followed by a collection of can optionally be followed by a collection of
@ -556,12 +565,20 @@
(-> (select :*) (from :other)))" (-> (select :*) (from :other)))"
{:arglists '([table] [table cols] [table statement] [table cols statement])} {:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args] [& args]
(let [[data & args :as args'] (stuff-into :insert-into args))
(if (map? (first args)) args (cons {} args))
[table cols statement] args] (defn patch-into
(if (and (sequential? cols) (map? statement)) "Accepts a table name or a table/alias pair. That
(generic :insert-into [data [table cols] statement]) can optionally be followed by a collection of
(generic :insert-into args')))) column names. That can optionally be followed by
a (select) statement clause.
The arguments are identical to insert-into.
The PATCH INTO statement is only supported by
XTDB."
{:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args]
(stuff-into :patch-into args))
(defn replace-into (defn replace-into
"Accepts a table name or a table/alias pair. That "Accepts a table name or a table/alias pair. That
@ -574,7 +591,7 @@
MySQL and SQLite." MySQL and SQLite."
{:arglists '([table] [table cols] [table statement] [table cols statement])} {:arglists '([table] [table cols] [table statement] [table cols statement])}
[& args] [& args]
(apply insert-into args)) (stuff-into :replace-into args))
(defn update (defn update
"Accepts either a table name or a table/alias pair. "Accepts either a table name or a table/alias pair.

View file

@ -12,7 +12,7 @@
bulk-collect-into bulk-collect-into
cross-join do-update-set drop-column drop-index drop-table cross-join do-update-set drop-column drop-index drop-table
filter from full-join filter from full-join
group-by having insert-into group-by having insert-into replace-into
join-by join lateral left-join limit offset on-conflict join-by join lateral left-join limit offset on-conflict
on-duplicate-key-update on-duplicate-key-update
order-by over partition-by refresh-materialized-view order-by over partition-by refresh-materialized-view
@ -835,7 +835,10 @@
["INSERT INTO transport (id, name) SELECT * FROM cars"])) ["INSERT INTO transport (id, name) SELECT * FROM cars"]))
;; three arguments with an alias and columns: ;; three arguments with an alias and columns:
(is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)})) (is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)}))
["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))) ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))
;; and again with replace-into:
(is (= (sql/format (replace-into '(transport t) '(id, name) '{select (*) from (cars)}))
["REPLACE INTO transport AS t (id, name) SELECT * FROM cars"])))
;; these tests are adapted from Cam Saul's PR #283 ;; these tests are adapted from Cam Saul's PR #283

View file

@ -53,6 +53,10 @@
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42] (is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
(-> {:erase-from :foo (-> {:erase-from :foo
:where [:= :foo.id 42]} :where [:= :foo.id 42]}
(sql/format))))
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
(-> (h/erase-from :foo)
(h/where [:= :foo.id 42])
(sql/format))))) (sql/format)))))
(deftest inline-record-body (deftest inline-record-body
@ -80,6 +84,21 @@
{:records [{:_id 1 :name "cat"} {:records [{:_id 1 :name "cat"}
{:_id 2 :name "dog"}]}]}))))) {:_id 2 :name "dog"}]}]})))))
(deftest patch-statement
(testing "patch with records"
(is (= ["PATCH INTO foo RECORDS {_id: 1, name: 'cat'}, {_id: 2, name: 'dog'}"]
(sql/format {:patch-into [:foo
{:records [[:inline {:_id 1 :name "cat"}]
[:inline {:_id 2 :name "dog"}]]}]})))
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
(sql/format {:patch-into [:foo
{:records [{:_id 1 :name "cat"}
{:_id 2 :name "dog"}]}]})))
(is (= ["PATCH INTO foo RECORDS ?, ?" {:_id 1 :name "cat"} {:_id 2 :name "dog"}]
(sql/format (h/patch-into :foo
(h/records [{:_id 1 :name "cat"}
{:_id 2 :name "dog"}])))))))
(deftest object-record-expr (deftest object-record-expr
(testing "object literal" (testing "object literal"
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"] (is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]