forked from LauJensen/clojureql
-
Notifications
You must be signed in to change notification settings - Fork 0
/
predicates.clj
176 lines (156 loc) · 5.87 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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
(ns clojureql.predicates
(:use clojureql.internal
[clojure.string :only [join] :rename {join join-str}]))
(defn sanitize [expression]
"Returns all values from an expression"
(reduce #(if (coll? %2)
(into %1 %2)
(conj %1 %2)) [] (remove keyword? expression)))
(defn parameterize [expression]
"Replace all values with questionmarks in an 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 cols]
Object
(toString [this] (apply str stmt))
Predicate
(sql-or [this exprs]
(if (empty? (-> exprs first :stmt))
(assoc this
:stmt (map str exprs)
:cols (into (or cols []) (mapcat :cols exprs))
:env (mapcat :env exprs))
(assoc this
:stmt (conj stmt (str "(" (join-str " OR " exprs) ")"))
:cols (into (or cols []) (mapcat :cols exprs))
:env (into env (mapcat :env exprs)))))
(sql-and [this exprs]
(if (empty? (-> exprs first :stmt))
(assoc this
:stmt (map str exprs)
:cols (into (or cols []) (mapcat :cols exprs))
:env (mapcat :env exprs))
(assoc this
:stmt (conj stmt (str "(" (join-str " AND " exprs) ")"))
:cols (into (or cols []) (mapcat :cols exprs))
:env (into env (mapcat :env exprs)))))
(sql-not [this expr]
(if (empty? (-> expr first :stmt))
(assoc this
:stmt (map str expr)
:cols (into (or cols []) (mapcat :cols expr))
:env (mapcat :env expr))
(assoc this
:stmt (conj stmt (str "NOT(" (join-str expr) ")"))
:cols (into (or cols []) (mapcat :cols 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)"))
:cols (into (or cols []) (filter keyword? [p1 p2]))
: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))))
:cols (filter keyword? 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 ","))))
:cols [field]
:env (into env (sanitize expr)))))
(defn predicate
([] (predicate [] []))
([stmt] (predicate stmt []))
([stmt env] (predicate stmt env nil))
([stmt env col]
(APredicate. stmt env col)))
(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])
(mapcat :cols [p1 p2]))))
(defn qualify-predicate
[this pred]
(let [tname (to-tablename (:tname this))
{:keys [stmt env cols]} pred
aggregates (find-aggregates this)]
(predicate
(reduce #(let [colname (nskeyword %2)]
(.replaceAll %1 colname
(if (some (fn [i] (= colname (nskeyword i))) aggregates)
colname
(if (.contains colname ".")
colname
(str (to-tablealias (:tname this)) \. colname)))))
(str pred) (set cols))
env
cols)))
(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)))
(defn nil?* [field]
(=* nil field))
(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) ")")))