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
* 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.
* 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).

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]
```
## 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
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
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
part of HoneySQL's "core" dialect anyway. It produces a `REPLACE 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]
```
## delete, delete-from
## delete, delete-from, erase-from
`:delete-from` is the simple use case here, accepting just a
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]
```
`: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` accepts a simple SQL entity (table name)

View file

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

View file

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

View file

@ -12,7 +12,7 @@
bulk-collect-into
cross-join do-update-set drop-column drop-index drop-table
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
on-duplicate-key-update
order-by over partition-by refresh-materialized-view
@ -835,7 +835,10 @@
["INSERT INTO transport (id, name) SELECT * FROM cars"]))
;; three arguments with an alias and columns:
(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

View file

@ -53,6 +53,10 @@
(is (= ["ERASE FROM foo WHERE foo.id = ?" 42]
(-> {:erase-from :foo
: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)))))
(deftest inline-record-body
@ -80,6 +84,21 @@
{:records [{:_id 1 :name "cat"}
{:_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
(testing "object literal"
(is (= ["SELECT OBJECT (_id: 1, name: 'foo')"]