From e4762a1a70759db9606dceb8a7bb3511aafa9c45 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Wed, 11 Dec 2024 11:50:47 -0800 Subject: [PATCH] fixes #558 by implementing patch-into also fix records helper and document a some more xtdb support Signed-off-by: Sean Corfield --- CHANGELOG.md | 1 + doc/clause-reference.md | 14 ++++++++++--- src/honey/sql.cljc | 5 ++++- src/honey/sql/helpers.cljc | 35 ++++++++++++++++++++++++-------- test/honey/sql/helpers_test.cljc | 7 +++++-- test/honey/sql/xtdb_test.cljc | 19 +++++++++++++++++ 6 files changed, 66 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd17f8..14cb211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/doc/clause-reference.md b/doc/clause-reference.md index 69ed736..7422f80 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -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) diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index b7142a0..239c6fa 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -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) diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index bddd974..23114c0 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -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. diff --git a/test/honey/sql/helpers_test.cljc b/test/honey/sql/helpers_test.cljc index 62bf953..1819417 100644 --- a/test/honey/sql/helpers_test.cljc +++ b/test/honey/sql/helpers_test.cljc @@ -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 diff --git a/test/honey/sql/xtdb_test.cljc b/test/honey/sql/xtdb_test.cljc index 6fc908c..27914cc 100644 --- a/test/honey/sql/xtdb_test.cljc +++ b/test/honey/sql/xtdb_test.cljc @@ -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')"]