/
predicates.clj
140 lines (122 loc) · 4.56 KB
/
predicates.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
(ns clojureql.predicates
(:use clojureql.internal
[clojure.string :only [join] :rename {join join-str}]))
(defn sanitize [expression]
(reduce #(conj %1 %2) [] (remove keyword? expression)))
(defn parameterize [expression]
(map #(if (keyword? %) (str (to-tablename %)) "?") expression))
(declare predicate)
(defprotocol Predicate
(sql-or [this exprs] "Compiles to (expr OR expr)")
(sql-and [this exprs] "Compiles to (expr AND expr)")
(sql-not [this exprs] "Compiles to NOT(exprs)")
(spec-op [this expr] "Compiles a special, ie. non infix operation")
(infix [this op exprs] "Compiles an infix operation")
(prefix [this op field exprs] "Compiles a prefix operation"))
(defrecord APredicate [stmt env]
Object
(toString [this] (apply str stmt))
Predicate
(sql-or [this exprs]
(if (empty? (-> exprs first :stmt))
(assoc this
:stmt (map str exprs)
:env (mapcat :env exprs))
(assoc this
:stmt (conj stmt (str "(" (join-str " OR " exprs) ")"))
:env (into env (mapcat :env exprs)))))
(sql-and [this exprs]
(if (empty? (-> exprs first :stmt))
(assoc this
:stmt (map str exprs)
:env (mapcat :env exprs))
(assoc this
:stmt (conj stmt (str "(" (join-str " AND " exprs) ")"))
:env (into env (mapcat :env exprs)))))
(sql-not [this expr]
(if (empty? (-> expr first :stmt))
(assoc this
:stmt (map str expr)
:env (mapcat :env expr))
(assoc this
:stmt (conj stmt (str "NOT(" (join-str expr) ")"))
:env (into env (mapcat :env expr)))))
(spec-op [this expr]
(let [[op p1 p2] expr]
(cond
(every? nil? (rest expr))
(assoc this
:stmt (conj stmt "(NULL " op " NULL)")
:env env)
(nil? p1)
(.spec-op this [op p2 p1])
(nil? p2)
(assoc this
:stmt (conj stmt (str "(" (name p1) " " op " NULL)"))
:env [])
:else
(infix this "=" (rest expr)))))
(infix [this op expr]
(assoc this
:stmt (conj stmt (format "(%s)"
(join-str (format " %s " (upper-name op))
(parameterize expr))))
:env (into env (sanitize expr))))
(prefix [this op field expr]
(assoc this
:stmt (conj stmt (format "%s %s (%s)"
(nskeyword field)
(upper-name op)
(->> (if (vector? (first expr))
(first expr)
expr)
parameterize
(join-str ","))))
:env (into env (sanitize expr)))))
(defn predicate
([] (predicate [] []))
([stmt] (predicate stmt []))
([stmt env] (APredicate. stmt env)))
(defn fuse-predicates
"Combines two predicates into one using AND"
[p1 p2]
(if (and (nil? (:env p1)) (nil? (:stmt p1)))
p2
(predicate (join-str " AND " [p1 p2])
(mapcat :env [p1 p2]))))
(defn or* [& args] (sql-or (predicate) args))
(defn and* [& args] (sql-and (predicate) args))
(defn not* [& args] (sql-not (predicate) args))
(defn =* [& args]
(if (some #(nil? %) args)
(spec-op (predicate) (into ["IS"] args))
(infix (predicate) "=" args)))
(defn !=* [& args]
(if (some #(nil? %) args)
(spec-op (predicate) (into ["IS NOT"] args))
(infix (predicate) "!=" args)))
(defmacro definfixoperator [name op doc]
`(defn ~name ~doc [& args#]
(infix (predicate) (name ~op) args#)))
(definfixoperator like :like "LIKE operator: (like :x \"%y%\"")
(definfixoperator >* :> "> operator: (> :x 5)")
(definfixoperator <* :< "< operator: (< :x 5)")
(definfixoperator <=* :<= "<= operator: (<= :x 5)")
(definfixoperator >=* :>= ">= operator: (>= :x 5)")
(defmacro defprefixoperator [name op doc]
`(defn ~name ~doc [field# & args#]
(prefix (predicate) (name ~op) field# args#)))
(defprefixoperator in :in
"IN operator: (in :name \"Jack\" \"John\"). Accepts both
a vector of items or an arbitrary amount of values as seen
above.")
(defn restrict
"Returns a query string.
Takes a raw string with params as %1 %2 %n.
(restrict 'id=%1 OR id < %2' 15 10) => 'id=15 OR id < 10'"
[pred & args]
(apply sql-clause pred args))
(defn restrict-not
"The inverse of the restrict fn"
([ast] (into [(str "NOT(" ast ")")] (:env ast)))
([pred & args] (str "NOT(" (apply sql-clause pred args) ")")))