Fork me on GitHub
#honeysql
<
2022-08-02
>
quan xing04:08:42

I want to generate dynamic sql where conditions with honeysql . Here is my code that has achieved the result I want. Is there a better or easy way to write it?

(defn select-audit-list
  [conn {revitId :revitId
         type :type 
         pro :pro
         username :username}]
  (let [con (data.common/with-logging conn)]
    (let [
          wheresql (cond-> [:and [:= revitId :RevitID]]
                     (not (empty? type)) (conj [:in :Type type])
                     (not (empty? pro)) (conj [:in :Pro pro])
                     (not (empty? username)) (conj [:in :username username]))
          sqlmap {:select :*
                  :from :audit
                  :where wheresql}]
      sqlmap
      (sql/format sqlmap {:inline true}))))
  
(def args {
             :revitId "123"
             :type [1 2 3]
             :pro ["p1" "p2" "p3"]
             :username ["user1"]
  })
  
(select-audit-list nil args)
==> ["SELECT * FROM audit WHERE ('123' = RevitID) AND (Type IN (1, 2, 3)) AND (Pro IN ('p1', 'p2', 'p3')) AND (username IN ('user1'))"]

(select-audit-list2 nil (assoc args :username []))
==> ["SELECT * FROM audit WHERE ('123' = RevitID) AND (Type IN (1, 2, 3)) AND (Pro IN ('p1', 'p2', 'p3'))"]

seancorfield04:08:41

Using the where helper would make this easier since it takes care of the connectives:

(require [honey.sql.helpers :refer [where]])
...
(cond-> (where := :revitId :RevitID)
  (seq type) (where :in :Type type)
  (seq pro) (where :in :Pro pro)
  (seq username) (where :in :username username))
Assuming you're using HoneySQL 2.x

seancorfield04:08:11

As an example:

user=> (let [username "test" type [1 2 3] revitId 42]                                                                                                          #_=>   (cond-> (where := revitId :RevitID)
  #_=>     (seq type) (where :in :Type type)
  #_=>     (seq username) (where := username :username)
  #_=>     :and (select :*)                                                                                                                                    #_=>     :then (from :table)
  #_=>     :format (sql/format)))
["SELECT * FROM table WHERE (? = RevitID) AND (Type IN (?, ?, ?)) AND (? = username)" 42 1 2 3 "test"]
user=>
(I know that's not quite the data you have but it should give you an idea of what I'm suggesting)

seancorfield04:08:09

Note, in particular, that (seq foo) is more idiomatic than (not (empty? foo)) per the empty? docstring:

clojure.core/empty?
([coll])
  Returns true if coll has no items - same as (not (seq coll)).
  Please use the idiom (seq x) rather than (not (empty? x))

quan xing06:08:04

Thank you very much @U04V70XH6

slipset06:08:44

FYI. We’ve built some fns on top of honey to make stuff a bit more fluent and saves us from runtime errors:

(def none [:= true false])

(def all [:= true true])

(defn in
  [column-name values]
  (if (seq values)
    [:in column-name (set values)]
    none))

(defn and [& clauses]
  (into [:and] (seq clauses)))

(defn or [& clauses]
  (into [:or] (seq clauses)))

(defn lift [x]
  [:lift x])

Cora (she/her)06:08:49

now that's interesting

Cora (she/her)06:08:46

the in bit especially

Cora (she/her)06:08:24

filtering of nil clauses would be useful, so you could go (and (when (seq type) (in :Type type)) (when (seq pro) (in :Pro pro)))

Cora (she/her)06:08:22

(defmacro in-when
  [condition column-name values]
  `(when ~condition
     [:in ~column-name ~values]))

Cora (she/her)06:08:05

(defn and [& clauses]
  (let [c (remove nil? clauses)]
    (when (seq c)
      (if (second c)
        (into [:and] (seq c))
        (first c)))))

Cora (she/her)06:08:43

you could do the same with or

slipset06:08:48

The and and or are nice in so far that you can write (pq/and foo bar baz) but, it’s a bit sucky in this situation (apply pq/and (map lol xs))

slipset06:08:25

The reason for lift is so that you can (update foo :myjsonprop lift)

ts150313:08:30

Hello guys. How can I use :drop-table clause with table names such as user (reserved names) I’m trying these things (sql/format {:drop-table "foo"}) => ["DROP TABLE FOO"] (sql/format {:drop-table :foo}) => ["DROP TABLE foo"] and it doesn’t work

seancorfield15:08:24

Check the docs for the :quoted option. You can also select the :dialect to get the quoting store you want for your database.

ts150320:08:58

that helps. thanks a lot

ts150313:08:37

(sql/format {:create-table "foo"}) does work indeed => ["CREATE TABLE \"foo\""]

Cora (she/her)15:08:50

wanted to run some functions by you to see if you're interested in something like this in honeysql. putting it in this thread

Cora (she/her)16:08:40

they're rather a lot but if you have multiple queries and need to merge their concerns this gets you there. I'm not a huge fan but it ended up being something we needed. it perhaps could be written better

seancorfield16:08:07

Looks interesting but I have a couple of concerns: a) it assumes there is a helper for every clause -- which isn't true out of the box b) it won't work for any clauses registered by users c) does ns-publics and resolve work in ClojureScript?

Cora (she/her)16:08:26

ahhh ok good call. never mind!

Cora (she/her)16:08:03

I'm sure there would be a way to handle this well, but maybe it's not a good call overall

seancorfield16:08:05

I recommend always using the helpers to builder queries and just keep threading stuff together instead of building separate pieces and trying to merge them after the fact.

Cora (she/her)16:08:45

that ship has sailed a bit, for a number of reasons, but mostly my colleagues don't like to use the helpers ¯\(ツ)

seancorfield16:08:49

Hmm, maybe there's a lesson to be had in that then? 😉

Cora (she/her)16:08:06

well, sure, I agree