Building WHEN clause with a clojure expression in hugsql - clojure

I have a database with a status entity that I'd like to be able to fetch in many different ways. As a result, I'd like to build the WHEN clause of my query based on the content of a map.
For instance, like this:
(get-status *db* {:message_id 2 :user_id 1 :status "sent"})
;; or
(get-status *db* {:message_id 2})
;; or
(get-status *db* {:user_id 1})
;; etc.
I'm struggling using hubsql's clojure expressions. I am doing the following:
-- :name get-status
-- :doc fetch the status of a specific message
-- :command :query
-- :return :many
/* :require [clojure.string :as s] */
SELECT
*
FROM
message_status
/*~
(let [params (filter (comp some? val) params)]
(when (not (empty? params))
(str "WHERE "
(s/join ","
(for [[field value] params]
(str field " = " (keyword field)))))))
~*/
However, here is how the request is prepared by hugsql:
=> (get-status-sqlvec {:message_id 2 :user_id 1})
["SELECT\n *\nFROM\n message_status\nWHERE ? = ?,? = ?" 2 2 1 1]
Whereas I want something like:
=> (get-status-sqlvec {:message_id 2 :user_id 1})
["SELECT\n *\nFROM\n message_status\nWHERE message_id = 2, user_id = 1"]

I finally managed to get this working. The above code had two issues.
First, we have
(s/join ","
(for [[field value] params]
(str field " = " (keyword field)))
Since field is a keyword, this actually generates this kind of string: :foo = :foo, :bar = :bar. The keywords are then replaced by ? by hugsql. What we want instead is build this kind of string foo = :foo, bar = :bar, which we can do with this code:
(s/join ", "
(for [[field _] params]
(str (name field) " = " field))))))
The second problem is that the WHEN clause is not even valid SQL. The above code ends up generating requests such as:
SELECT * FROM message_status WHERE foo = 2, bar = 1
The commas in the WHERE clause should be AND, so the final (working) code is:
-- :name get-status
-- :doc fetch the status of a specific message
-- :command :query
-- :return :many
/* :require [clojure.string :as s] */
SELECT
*
FROM
message_status
/*~
(let [params (filter (comp some? val) params)]
(when (not (empty? params))
(str "WHERE "
(s/join " AND "
(for [[field _] params]
(str (name field) " = " field))))))
~*/

Related

clojure string null check failed with string/blank

(defn get-coll-id [^String coll_id]
(log/info "coll_id: " coll_id)
(if (string/blank? coll_id)
(let [collVal (get-coll-val)]
(log/info "collVal: " collSeqVal)
(format "C%011.0f" collVal))
coll_id))
The log shows "coll_id: null". However, string/blank did not detect the null, and thus log of collVal is skipped. What is the method to check null string?
Something (perhaps a DB?) is giving you the string "null" for coll_id, or perhaps (log/info ...) converts a Clojure nil into the string "null".
Consider this code:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[clojure.string :as str]
))
(defn get-coll-id [^String coll_id]
(println "coll_id: " coll_id)
(if (str/blank? coll_id)
(println :blank)
coll_id))
(dotest
(newline)
(println :v1)
(spyx (get-coll-id nil))
(newline)
(println :v2)
(spyx (get-coll-id (pr-str nil)))
)
with output:
:v1
coll_id: nil
:blank
(get-coll-id nil) => nil
:v2
coll_id: nil
(get-coll-id (pr-str nil)) => "nil"
No matter what you do, you get either the value nil or the string "nil" printed.
Since I haven't used Java for a while, I tried to force it to generate the string "null", but calling o.toString() for a null value creates a
NullPointerException, so that is not the answer.
Update
As amalloy points out, String.valueOf() will convert a Java null into the string "null":
package demo;
public class Demo {
public static String go() {
Object o = null;
return String.valueOf( o );
}
}
when run:
(newline)
(spyx :v3 (demo.Demo/go))
with result
:v3 (demo.Demo/go) => "null"
As to your original question, the nil? function could be used:
(defn blank-or-nil?
[s]
(or (nil? s)
(str/blank? s)))
(defn get-coll-id [^String coll_id]
(println "coll_id: " coll_id)
(if (blank-or-nil? coll_id)
(println "found blank-or-nil coll_id")
coll_id))
which then prints found blank-or-nil coll_id when passed a nil value. However, this could confuse things if your are passed the string "null" or the string "nil".
You need to clarify which value is the input, then track down the source.
The above code is based on my favorite template project.

Function for applying to a user with a various number of data fields

The question was born when I was practicing an Observer topic in a tutorial
I am trying to apply the function to the user but cannot use user's data fields like name, surname.
Let's say that the user may have various number of data fields so we must use & args argument. My code that does not work:
(ns observer.core)
(def user {:name "Alan" :surname "Smith" :alias "Mike"})
(def user2 {:name "Jane" :surname "Smith"})
(apply
(fn [& args] (println (str "I am " (:name args) " " (:surname args) ".")))
user)
(apply
(fn [& args] (println (str "My sister is " (:name args) " " (:surname args) ".")))
user2)
The output:
I am .
My sister is .
observer.core>
How to fix it regarding that the apply function must be used?
apply converts a map to a seq, i.e.
{:name "Alan" :surname "Smith" :alias "Mike"} becomes ([:name "Alan"] [:surname "Smith"] [:alias "Mike"])
You could put it back into a map, if that is what you need:
(let [user {:name "Alan" :surname "Smith" :alias "Mike"}]
(apply
(fn [& args]
(let [args (into {} args)]
(println (str "I am " (:name args) " " (:surname args) "."))))
user))
but this looks a bit of a stretch to me. I believe the solution could have been better if I knew how this function is supposed to be used.
Usually there are two types of functions: (fn :text "some" :row 25) and (fn {:text "some" :row 25}).
In the spirit of learning:
Check out Clojure - Cheatsheet.
10 years with Clojure, and I still use it daily.
(apply some-func (list x y z)) becomes (some-func x y z), because apply assumes that the second argument is a list (which it then unpacks).
And what you are currently doing is collecting all the arguments back into a list called args
(def user {:name "Alan" :surname "Smith" :alias "Mike"})
(apply
(fn [& args]
(prn 'ARGS args) ;; lets see what is actually in args
(println (str "I am " (:name args) " " (:surname args) ".")))
user)
;; -> ARGS ([:name "Alan"] [:surname "Smith"] [:alias "Mike"])
;; -> I am .
And the outut is as #akond says.
You could, of course, put 'user' in a vector (or list), but then don't use '&' to collect everything back into a list, (which you would then have to pick stuff out of again):
(def user {:name "Alan" :surname "Smith" :alias "Mike"})
(apply
(fn [args]
(prn 'ARGS args)
(println (str "I am " (:name args) " " (:surname args) ".")))
[user])
That would give you the output you expected. But this is a bit strange, perhaps, but certainly viable if you must use apply and you can control the "list" part of the argument.
So, #akond's solution is simple and clean.
And augmenting it with Clojure "destructing":
(def user {:name "Alan" :surname "Smith" :alias "Mike"})
(apply
(fn [& args]
(let [{:keys [name surname alias]} (into {} args)]
(println (str "I am " name " " surname "." (when alias (str " But call me " alias "!"))))))
user)
I believe you intended to do something like this:
(def user {:name "Alan" :surname "Smith" :alias "Mike"})
(def user2 {:name "Jane" :surname "Smith"})
(defn fn-1
[item]
(println (str "I am " (:name item) " " (:surname item) ".")) )
(defn fn-2
[item]
(println (str "My sister is " (:name item) " " (:surname item) ".")))
(fn-1 user)
(fn-2 user2)
with result:
I am Alan Smith.
My sister is Jane Smith.
One has to wrap a user object or the map by a list.
(ns observer.core)
(defrecord Person [name balance])
(def user (Person. "Alan" 150.34))
(def user2 {:name "Adam" :balance 629.74})
(def observers (atom #{}))
(swap! observers conj (fn [l] (println (str "2. " (:name l)))))
(swap! observers conj (fn [l] (println (str "1. " (:balance l)))))
(println "user")
(vec (map #(apply % (list user)) #observers))
(println "\nuser2")
(vec (map #(apply % (list user2)) #observers))
Output
user
1. 150.34
2. Alan
user2
1. 629.74
2. Adam
observer.core>

Is it possible to destructure in case for use in a threading macro?

(def val { :type "bar" })
(-> val
(case ???
"bar" "bar type"
"baz" "baz type"
"other type"))
I'd like to include a case in a threading macro so I can branch based on one of the keys of val, a hash map. Is this possible?
EDIT: I need to thread val not a key from val as further functions will need the whole of val. I essentially want to branch to a function within the threading macro based on a key of val. But still pass val onward.
Consider using a multimethod dispatch here:
(defmulti some-operation :type)
(defmethod some-operation "bar"
[val]
(println "Bar type!")
(assoc val :x 42))
(defmethod some-operation "baz"
[val]
(println "Baz type!")
(assoc val :x 100))
(-> {:type "bar"}
some-operation
some-other-operation)
This is something which seems to work and serves as a better example of what I want to do:
(def val { :type "bar" })
(-> val
(do-something-to-val-based-on-type)
(do-something-else-to-val))
(defn do-something-to-val-based-on-type [val]
(let [:type (:type val)]
(case type
"bar" (do-something-to-bar-type-val val)
"baz" (do-something-to-baz-type-val val)
val))) ;; default, no-op
(defn do-something-to-bar-type-val [val]
;; something
val)
(defn do-something-to-baz-type-val [val]
;; something
val)
also, since a threading macro simply adds an item to a seq for every "action", you can easily use anonymous function for that:
user> (def val { :type "bar" })
#'user/val
user> (-> val
((fn [{type :type}]
(case type
"bar" "bar type"
"baz" "baz type"
"other type"))))
;;=> "bar type"
if you wish, you can also make up special macro, rearranging let for usage in ->:
user> (defmacro let-inv [x binding & body]
`(let [~binding ~x] ~#body))
#'user/let-inv
user> (-> val
(let-inv {type :type}
(case type
"bar" "bar type"
"baz" "baz type"
"other type")))
;;=> "bar type"
This is easily accomplished using the it-> threading macro from the Tupelo library:
(ns tst.clj.core
(:use clj.core tupelo.test)
(:require [tupelo.core :as t] ))
(t/refer-tupelo)
(def val { :type "bar" })
(println "result => "
(it-> val
(case (grab :type it)
"bar" "bar-type"
"baz" "baz-type"
"other-type")))
to yield the desired result:
result => bar-type
The it-> macro assigns the intermediate value to the symbol it at each stage of the pipeline. In this case we use the grab function to extract the :type value. It works like (:type it) but will throw if the key is not found.
Another example:
(it-> 1
(inc it) ; thread-first or thread-last
(+ it 3) ; thread-first
(/ 10 it) ; thread-last
(str "We need to order " it " items." ) ; middle of 3 arguments
;=> "We need to order 2 items." )

Clojure say-hi with varargs

Input: "Michael" "Julia" "Joe" "Sam"
Output: Hi, Michael, Julia, Joe, and Sam. (pay attention to the commas and the word "and")
Input: nil
Output: Hi, world.
Here is my first attempt:
(defn say-hi [& name]
(print "Hi," name))
user> (say-hi "Michael")
Hi, (Michael)
nil
user> (say-hi "Michael" "Julia")
Hi, (Michael Julia)
nil
Question:
How to implement default: (no input, say "Hi World!")
How to get rid of the parents around names in output?
How to implement the commas separation and add the conjunction word "and"?
First off, Clojure supports multi-arity functions, so you could do something like this to achieve default behaviour:
(defn say-hi
([] (say-hi "World"))
([& names] ...))
Then, what you want is to take a seq and join all the strings it contains together, using ", " in between. The clojure.string namespaces contains lots of string manipulation functions, one of them being clojure.string/join:
(require '[clojure.string :as string])
(string/join ", " ["Michael", "Julia"])
;; => "Michael, Julia"
But the last element of the seq should be concatenated using " and " as a separator, so you'll end up with something like this:
(require '[clojure.string :as string])
(defn say-hi
([] (say-hi "World"))
([& names]
(if (next names)
(format "Hi, %s, and %s!"
(string/join ", " (butlast names))
(last names))
(format "Hi, %s!" (first names)))))
Note that you have to differentiate between the single- and multi-name cases and (next names) basically checks whether the seq contains more than one element. (You could achieve the same by adding another arity to the function.)
(say-hi)
;; => "Hi, World!"
(say-hi "Michael")
;; => "Hi, Michael!"
(say-hi "Michael" "Julia" "Joe" "Sam")
;; => "Hi, Michael, Julia, Joe, and Sam!"
You can use clojure.string/join:
(use '[clojure.string :only [join]])
(defn sentencify [& elems]
(->>
[(join ", " (butlast elems)) (last elems)]
(remove empty?)
(join " and ")))
(defn say-hi [& name]
(print "Hi," (if name
(sentencify name)
"World!")))
A concise solution:
(defn say-hi [& names]
(let [names (case (count names)
0 ["world"]
1 names
(concat (butlast names) (list (str "and " (last names)))))]
(->> names, (cons "Hi"), (interpose ", "), (apply str))))
(say-hi)
;"Hi, world"
(say-hi "Michael")
;"Hi, Michael"
(say-hi "Michael" "Julia" "Joe" "Sam")
;"Hi, Michael, Julia, Joe, and Sam"
For long lists of names, you would want to eschew count, last, and butlast, maybe by pouring names into a vector first.
To print (as the question does) rather than return the formatted string, append print to the final form:
(->> names, (cons "Hi"), (interpose ", "), (apply str), print)

Error Thrown While Processing DB Query Resultset

Can anyone help me understand why my code (see below) is resulting in the following error:
Exception in thread "main" java.lang.UnsupportedOperationException:
nth not supported on this type: PersistentStructMap
(defn search [query]
(with-connection db
(with-query-results rs [query]
(doseq [[k v] rs]
(println v)))))
(search (nth *command-line-args* 0))
rs is a sequence (list), representing all the records in your resultset. Each element of rs is a hashmap representing a single record, with key/value pairs in the map representing field names and values for that record. You're trying to do the equivalent of this:
user> (let [rs [{:id 1 :val "foo"} {:id 2 :val "bar"}]]
(doseq [[k v] rs]
(println v)))
; Evaluation aborted.
; nth not supported on this type: PersistentArrayMap
This is trying to destructure each map into [k v], doing the rough equivalent of this:
user> (let [k (nth {:id 1 :val "foo"} 0)
v (nth {:id 1 :val "foo"} 1)])
; Evaluation aborted.
; nth not supported on this type: PersistentArrayMap
If you're trying to print the value for every field in every record, you need to do this:
user> (let [rs [{:id 1 :val "foo"} {:id 2 :val "bar"}]]
(doseq [record rs
[k v] record]
(println v)))
foo
1
bar
2
"For each record in the resultset, for each key/value in that record, print the value."
If your resultset contains only a single record (or you only care about one of them) and you're trying to iterate over the fields of that single record, then pass doseq only the first:
user> (let [rs [{:id 1 :val "foo"}]]
(doseq [[k v] (first rs)]
(println v)))
foo
1
"For every key/value in the first record of the resultset, print the value."