From c53ec6c78dca243767020cbeea3be8e5fdb10e70 Mon Sep 17 00:00:00 2001 From: Unnikrishnan Date: Thu, 7 Apr 2016 13:23:36 +0530 Subject: [PATCH] Add postgres extension - upsert & returning --- src/honeysql/postgres/format.clj | 61 +++++++++++++++++++++++++++++++ src/honeysql/postgres/helpers.clj | 26 +++++++++++++ test/honeysql/postgres_test.clj | 49 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 src/honeysql/postgres/format.clj create mode 100644 src/honeysql/postgres/helpers.clj create mode 100644 test/honeysql/postgres_test.clj diff --git a/src/honeysql/postgres/format.clj b/src/honeysql/postgres/format.clj new file mode 100644 index 0000000..de94356 --- /dev/null +++ b/src/honeysql/postgres/format.clj @@ -0,0 +1,61 @@ +(ns honeysql.postgres.format + (:refer-clojure :exclude [format]) + (:require [honeysql.format :refer :all])) + +;; Move the whole default priorities here ? +(def postgres-clause-priorities + {:upsert 225 + :on-conflict 230 + :on-conflict-constraint 230 + :do-update-set 235 + :do-update-set! 235 + :do-nothing 235 + :returning 240 + :query-values 250}) + +;; FIXME : Not sure if this is the best way to implement this, but since the `clause-store` is being used +;; by format to decide the order of clauses, not really sure what would be a better implementation. +(doseq [[k v] postgres-clause-priorities] + (register-clause! k v)) + +(defn get-first [x] + (if (coll? x) + (first x) + x)) + +(defmethod format-clause :on-conflict-constraint [[_ k] _] + (str "ON CONFLICT ON CONSTRAINT " (-> k + get-first + to-sql))) + +(defmethod format-clause :on-conflict [[_ id] _] + (str "ON CONFLICT (" (-> id + get-first + to-sql) ")")) + +(defmethod format-clause :do-nothing [_ _] + "DO NOTHING") + +;; Used when there is a need to update the columns with modified values if the +;; row(id) already exits - accepts a map of column and value +(defmethod format-clause :do-update-set! [[_ values] _] + (str "DO UPDATE SET " (comma-join (for [[k v] values] + (str (to-sql k) " = " (to-sql v)))))) + +;; Used when it is a simple upsert - accepts a vector of columns +(defmethod format-clause :do-update-set [[_ values] _] + (str "DO UPDATE SET " + (comma-join (map #(str (to-sql %) " = EXCLUDED." (to-sql %)) + values)))) + +(defn format-upsert-clause [upsert] + (let [ks (keys upsert)] + (map #(format-clause % (find upsert %)) upsert))) + +;; Accepts a map with the following possible keys +;; :on-conflict, :do-update-set or :do-update-set! or :do-nothing +(defmethod format-clause :upsert [[_ upsert] _] + (space-join (format-upsert-clause upsert))) + +(defmethod format-clause :returning [[_ fields] _] + (str "RETURNING " (comma-join (map to-sql fields)))) diff --git a/src/honeysql/postgres/helpers.clj b/src/honeysql/postgres/helpers.clj new file mode 100644 index 0000000..bbfac13 --- /dev/null +++ b/src/honeysql/postgres/helpers.clj @@ -0,0 +1,26 @@ +(ns honeysql.postgres.helpers + (:refer-clojure :exclude [update]) + (:require [honeysql.helpers :refer :all])) + +(defn do-nothing [m] + (assoc m :do-nothing [])) + +(defhelper do-update-set [m args] + (assoc m :do-update-set (collify args))) + +(defhelper db-update-set! [m args] + (assoc m :do-update-set! args)) + +(defhelper on-conflict [m args] + (assoc m :on-conflict args)) + +(defhelper on-conflict-constraint [m args] + (assoc m :on-conflict-constraint args)) + +(defhelper upsert [m args] + (if (plain-map? args) + (assoc m :upsert args) + (assoc m :upsert (first args)))) + +(defhelper returning [m fields] + (assoc m :returning (collify fields))) diff --git a/test/honeysql/postgres_test.clj b/test/honeysql/postgres_test.clj new file mode 100644 index 0000000..6e5abea --- /dev/null +++ b/test/honeysql/postgres_test.clj @@ -0,0 +1,49 @@ +(ns honeysql.postgres-test + (:refer-clojure :exclude [update]) + (:require [honeysql.postgres.format :refer :all] + [honeysql.postgres.helpers :refer :all] + [honeysql.helpers :refer :all] + [honeysql.format :as sql] + [clojure.test :refer :all])) + +(deftest upsert-test + (testing "upsert sql generation for postgresql" + (is (= ["INSERT INTO distributors d (did, dname) VALUES (5, ?), (6, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING d.*" "Gizmo Transglobal" "Associated Computing, Inc"] + (-> (insert-into [:distributors :d]) + (values [{:did 5 :dname "Gizmo Transglobal"} + {:did 6 :dname "Associated Computing, Inc"}]) + (upsert (-> (on-conflict :did) + (do-update-set :dname))) + (returning :d.*) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (7, ?) ON CONFLICT (did) DO NOTHING" "Redline GmbH"] + (-> (insert-into :distributors) + (values [{:did 7 :dname "Redline GmbH"}]) + (upsert (-> (on-conflict :did) + do-nothing)) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (9, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" "Antwerp Design"] + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (upsert (-> (on-conflict-constraint :distributors_pkey) + do-nothing)) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (10, ?), (11, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" "Pinp Design" "Foo Bar Works"] + (sql/format {:insert-into :distributors + :values [{:did 10 :dname "Pinp Design"} + {:did 11 :dname "Foo Bar Works"}] + :upsert {:on-conflict :did + :do-update-set [:dname]}}))))) + +(deftest returning-test + (testing "returning clause in sql generation for postgresql" + (is (= ["DELETE FROM distributors WHERE did > 10 RETURNING *"] + (sql/format {:delete-from :distributors + :where [:> :did :10] + :returning [:*] }))) + (is (= ["UPDATE distributors SET dname = ? WHERE did = 2 RETURNING did dname" "Foo Bar Designs"] + (-> (update :distributors) + (sset {:dname "Foo Bar Designs"}) + (where [:= :did :2]) + (returning [:did :dname]) + sql/format)))))