honeysql 2025-04-14

Building a MERGE query Since MERGE is not yet supported by HoneySQL, and we need that in our project, therefore I am trying to build that using the available structures. I almost succeeded, with simple values my implementation works, but it fails with JSON values. One part of the query gets built using {:insert-into …} but I cannot get keyword parameters in. Does anybody know why this isn’t working?

(-> {:insert-into [:table :t], :using [[{:values [:?value :?key]} [:s [:composite :value :key]]]], :where [:and [:= :t/key :s/key]]}
      (hsql/format {:params {:foo "FOO"}}))

✅ 1

@ikaraszi Have you written a merge formatter that you register?

No, I didn’t do that, I used the existing functions to create a MERGE statement for my use-case. I could have dig deeper, but I needed this fast

So it is a hack of using the existing formatters and then replace some parts of the statement

I don’t think that is usable for you in its current form 😞

Ah, interesting. If you get a chance to write up what you ended doing in a GH issue for HoneySQL, I would still be interested, since I'm not familiar with the MERGE clause and it seems to have "interesting" syntax which is partly why I haven't implemented it yet...

This is the top-level build function that I came up with:

(defn build [table set-params where-params]
  {:pre [(keyword? table) (valid-params? set-params) (valid-params? where-params)]}
  (let [all-params (merge set-params where-params)
        [insert-into-stmnt & params] (-> {:insert-into [table :t]
                                          :using [[(build-source table) :s]]
                                          :where (table+source [:and] [:=] (keys where-params))}
                                         (hsql/format {:params {:all all-params}}))
        when-stmnt (build-when (keys all-params) (keys set-params))]
    (-> insert-into-stmnt
        (str/replace #"^INSERT INTO" "MERGE INTO")
        (str/replace " WHERE " " ON ")
        (str " " when-stmnt)
        vector
        (into params))))

(There's an open GH issue about it)

And build-when is a version of build-where?

(defn build-when [insert-columns update-columns]
  (let [diff-conditions (table+source [:or] [:is-distinct-from] update-columns)
        insert-stmnt (build-insert insert-columns)
        update-stmnt (build-update update-columns)]
    (-> [:case
         [:not :MATCHED] [:raw insert-stmnt]
         [:and :MATCHED diff-conditions] [:raw update-stmnt]]
        hsql/format
        first
        (str/replace #"^CASE " "")
        (str/replace #" END$" ""))))

(defn build-insert [columns]
  (-> {:insert-into :table
       :columns columns
       :values (->> columns
                    (mapv (fn [column] (qualified-column :s column)))
                    vector)}
      hsql/format
      first
      (str/replace #"^INSERT INTO table" "INSERT")))

(defn build-update [columns]
  (-> {:update :table
       :set (table+source {} columns)}
      hsql/format
      first
      (str/replace #"^UPDATE table" "UPDATE")))

As you can see it’s quite hacky

Still helpful tho', thank you.

I am glad if you find it useful

See https://github.com/seancorfield/honeysql/issues/499 if you have any more thoughts...

👍 1

I would change the API of my fn to simply pass a set-params and a unique-fields instead of expecting to maps

I believe a vector is missing

What does the desired result look like?

(-> {:insert-into [:table :t], :using [[{:values [[:?value :?key]]} [:s [:composite :value :key]]]], :where [:and [:= :t/key :s/key]]}
      (hsql/format {:params {:value "VALUE" :key "KEY"}}))

this works

It does work after I added the missing vector wrapping