diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afe36d..4dd79b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changes +* 2.0.next in progress + * Fix #323 by supporting more than one SQL entity in `:on-conflict`. + * 2.0.0-beta2 (for testing; 2021-04-13) * The documentation continues to be expanded and clarified in response to feedback! * Fix #322 by rewriting/simplifying `WHERE`/`HAVING` merge logic. **Important bug fix!** diff --git a/doc/clause-reference.md b/doc/clause-reference.md index 631c2cf..df93457 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -815,10 +815,11 @@ These are grouped together because they are handled as if they are separate clauses but they will appear in pairs: `ON ... DO ...`. -`:on-conflict` accepts either a single SQL entity -(a keyword or symbol), or a SQL clause, or a pair -of a SQL entity and a SQL clause. The SQL entity is -a column name and the SQL clause can be an +`:on-conflict` accepts a sequence of zero or more +SQL entities (keywords or symbols), optionally +followed by a single SQL clause (hash map). It can also +accept either a single SQL entity or a single SQL clause. +The SQL entities are column names and the SQL clause can be an `:on-constraint` clause or a`:where` clause. _[For convenience of use with the `on-conflict` helper, this clause can also accept any of those arguments, wrapped in a sequence; it can also accept an empty sequence, and just produce `ON CONFLICT`, so that it can be combined with other clauses directly]_ diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 337d02c..1c70232 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -589,26 +589,25 @@ (into [(str (sql-kw k) " " (str/join ", " sqls))] params))) (defn- format-on-conflict [k x] - (cond (ident? x) - [(str (sql-kw k) " (" (format-entity x) ")")] - (map? x) - (let [[sql & params] (format-dsl x)] - (into [(str (sql-kw k) " " sql)] params)) - (and (sequential? x) - (ident? (first x)) - (map? (second x))) - (let [[sql & params] (format-dsl (second x))] - (into [(str (sql-kw k) - " (" (format-entity (first x)) ") " - sql)] - params)) - (and (sequential? x) (= 1 (count x))) - (format-on-conflict k (first x)) - (and (sequential? x) (= 0 (count x))) - [(sql-kw k)] - :else - (throw (ex-info "unsupported :on-conflict format" - {:clause x})))) + (if (sequential? x) + (let [entities (take-while ident? x) + n (count entities) + [clause & more] (drop n x) + _ (when (or (seq more) + (and clause (not (map? clause)))) + (throw (ex-info "unsupported :on-conflict format" + {:clause x}))) + [sql & params] (when clause + (format-dsl clause))] + (into [(str (sql-kw k) + (when (pos? n) + (str " (" + (str/join ", " (map #'format-entity entities)) + ")")) + (when sql + (str " " sql)))] + params)) + (format-on-conflict k [x]))) (defn- format-do-update-set [k x] (cond (map? x) diff --git a/src/honey/sql/helpers.cljc b/src/honey/sql/helpers.cljc index 27f2ca7..95dde1a 100644 --- a/src/honey/sql/helpers.cljc +++ b/src/honey/sql/helpers.cljc @@ -768,10 +768,9 @@ (generic-1 :values args)) (defn on-conflict - "Accepts a single column name to detect conflicts - during an upsert, optionally followed by a `WHERE` - clause." - {:arglists '([column] [column where-clause])} + "Accepts zero or more SQL entities (keywords or symbols), + optionally followed by a single SQL clause (hash map)." + {:arglists '([column* where-clause])} [& args] (generic :on-conflict args)) diff --git a/test/honey/sql/postgres_test.cljc b/test/honey/sql/postgres_test.cljc index 86d9852..4a5685b 100644 --- a/test/honey/sql/postgres_test.cljc +++ b/test/honey/sql/postgres_test.cljc @@ -72,6 +72,20 @@ (on-conflict (on-constraint :distributors_pkey)) do-nothing sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; with both name and clause: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (on-conflict :did (on-constraint :distributors_pkey)) + do-nothing + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did, dname) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; with multiple names and a clause: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (on-conflict :did :dname (on-constraint :distributors_pkey)) + do-nothing + sql/format))) (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] ;; almost identical to nilenso version: (-> (insert-into :distributors) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 4dd43dd..0a73eb9 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -618,6 +618,32 @@ INSERT INTO customers (name, email) VALUES ('Microsoft', 'hotline@microsoft.com') ON CONFLICT (name) +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict [:name] + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name, email) +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict [:name :email] + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name) DO UPDATE SET email = EXCLUDED.email || ';' || customers.email "] (format {:insert-into :customers