-
Notifications
You must be signed in to change notification settings - Fork 54
/
parser.clj
344 lines (300 loc) · 10.4 KB
/
parser.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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
(ns hugsql.parser
(:require [clojure.string :as string]
[clojure.tools.reader.reader-types :as r]))
(defn- parse-error
([rdr msg]
(parse-error rdr msg {}))
([rdr msg data]
(if (r/indexing-reader? rdr)
(throw
(ex-info
(str msg " line: " (r/get-line-number rdr)
", column: " (r/get-column-number rdr))
(merge data
{:line (r/get-line-number rdr)
:column (r/get-column-number rdr)})))
(throw (ex-info msg (merge data {:error :parse-error}))))))
(defn- sb-append
[^StringBuilder sb ^Character c]
(doto sb (.append c)))
(defn- whitespace? [^Character c]
(when c
(Character/isWhitespace ^Character c)))
(defn- symbol-char?
[c]
(boolean (re-matches #"[\pL\pM\pS\d\_\-\.\+\*\?\:\/%]" (str c))))
(defn- skip-ws-to-next-line
"Read from reader until a non-whitespace or newline char is encountered."
[rdr]
(loop [c (r/peek-char rdr)]
(when (and (whitespace? c)
(not (= \newline c)))
(r/read-char rdr)
(recur (r/peek-char rdr)))))
(defn- skip-to-next-line
"Read from reader until a new line is encountered.
Reads (eats) the encountered new line."
[rdr]
(loop [c (r/read-char rdr)]
(when (and c (not (= \newline c)))
(recur (r/read-char rdr)))))
(defn- skip-to-chars
"Read from reader until the two chars `c1 and `c2` are encountered.
Read (eat) the encountered chars."
[rdr c1 c2]
(loop [rc (r/read-char rdr)
pc (r/peek-char rdr)]
(if (or (nil? rc) (nil? pc)
(and (= rc c1) (= pc c2)))
(do (r/read-char rdr) nil) ; read last peek char off, return nil
(recur (r/read-char rdr) (r/peek-char rdr)))))
(defn- read-to-char
"Read and return a string up to the encountered char `c`.
Does not read the encountered character."
[rdr c]
(loop [s (StringBuilder.)
pc (r/peek-char rdr)]
(if (or (nil? pc) (= c pc))
(str s)
(recur (sb-append s (r/read-char rdr))
(r/peek-char rdr)))))
(defn- read-to-chars
"Read and return a string up to the encountered chars `c1` and `c2`.
Does not read the encountered characters"
[rdr c1 c2]
(loop [s (StringBuilder.)
rc (r/read-char rdr)
pc (r/peek-char rdr)]
(if (or (nil? rc) (nil? pc)
(and (= c1 rc) (= c2 pc)))
(do (r/unread rdr rc) (str s))
(recur (sb-append s rc)
(r/read-char rdr)
(r/peek-char rdr)))))
(defn- read-keyword
[rdr]
(loop [result {}
s (StringBuilder.)
rc (r/read-char rdr)
pc (r/peek-char rdr)]
(let [pgcast? (and (= \: rc) (= \: pc))]
(cond
(or (nil? rc) (nil? pc) pgcast? (not (symbol-char? pc)))
;; We're done.
(do
(when pgcast? (r/unread rdr rc))
(let [s (str (if pgcast? s (sb-append s rc)))]
(if (> (count s) 0)
(assoc result :name s)
(parse-error rdr (str "Incomplete keyword :" s)))))
(= \: rc)
;; This is the end of the type specification.
(recur (assoc result :type (str s))
(StringBuilder.)
(r/read-char rdr)
(r/peek-char rdr))
(= \/ rc)
;; This is the end of the namespace.
(recur (assoc result :namespace (str s))
(StringBuilder.)
(r/read-char rdr)
(r/peek-char rdr))
:else
(recur result
(sb-append s rc)
(r/read-char rdr)
(r/peek-char rdr))))))
(defn- sing-line-comment-start?
[c rdr]
(and c (= \- c) (= \- (r/peek-char rdr))))
(defn- mult-line-comment-start?
[c rdr]
(and c (= \/ c) (= \* (r/peek-char rdr))))
(defn- sql-quoted-start?
[c]
(contains? #{\' \"} c))
(defn- sql-unmatched-quoted?
[c]
(contains? #{\' \"} c))
(defn- pg-type-cast-start?
[rdr c]
(and (= \: c) (= \: (r/peek-char rdr))))
(defn- escape-start?
[rdr c]
(let [p (r/peek-char rdr)]
(and (= \\ c) (or (= \: p) (= \\ p)))))
(defn- hugsql-param-start?
[c]
(= \: c))
(defn- values-vector
[s]
(vec (remove string/blank?
(string/split s #"\s+"))))
(defn- read-sing-line-header
[rdr]
(let [_ (r/read-char rdr) ; eat colon (:)
key (-> rdr read-keyword :name keyword)
line (read-to-char rdr \newline)
values (if (= key :doc)
[(string/trim line)]
(values-vector line))]
(skip-to-next-line rdr)
{key values}))
(defn- read-mult-line-header
[rdr]
(let [_ (r/read-char rdr) ; eat colon (:)
key (-> rdr read-keyword :name keyword)
lines (read-to-chars rdr \* \/)
_ (skip-to-chars rdr \* \/)
values (if (= key :doc)
[(string/trim lines)]
(values-vector lines))]
(skip-to-next-line rdr)
{key values}))
(defn- read-sing-line-expr
[rdr]
(let [_ (r/read-char rdr) ; eat ~
expr (string/trim (read-to-char rdr \newline))]
[expr :end]))
(defn- read-mult-line-expr
[rdr]
(let [_ (r/read-char rdr) ; eat ~
expr (string/trim (read-to-chars rdr \* \/))
_ (skip-to-chars rdr \* \/)
end? (= \~ (last expr))
expr (if end? (string/trim (string/join "" (butlast expr))) expr)
sign (if end? :end :cont)]
(if (string/blank? expr) [sign] [expr sign])))
(defn- read-mult-line-hint
[rdr]
(let [_ (r/read-char rdr) ; eat +
hint (read-to-chars rdr \* \/)
_ (skip-to-chars rdr \* \/)]
(str "/*+" hint "*/")))
(defn- read-sing-line-comment
[rdr]
(r/read-char rdr) ; eat second dash (-) of comment start
(skip-ws-to-next-line rdr)
(condp = (r/peek-char rdr)
\: (read-sing-line-header rdr)
\~ (read-sing-line-expr rdr)
(skip-to-next-line rdr)))
(defn- read-mult-line-comment
[rdr]
(r/read-char rdr) ; eat second comment char (*)
(skip-ws-to-next-line rdr)
(condp = (r/peek-char rdr)
\: (read-mult-line-header rdr)
\~ (read-mult-line-expr rdr)
\+ (read-mult-line-hint rdr)
(skip-to-chars rdr \* \/)))
(defn- read-sql-quoted
[rdr c]
(let [quot c]
(loop [s (sb-append (StringBuilder.) c)
c (r/read-char rdr)]
(condp = c
nil (parse-error rdr "SQL String terminated unexpectedly with EOF")
quot (let [pc (r/peek-char rdr)]
(if (and pc (= pc quot) (not (= c pc)))
(recur (sb-append s c) (r/read-char rdr))
(str (sb-append s c))))
;; else
(recur (sb-append s c) (r/read-char rdr))))))
(defn- read-hugsql-param
[rdr]
(let [{:keys [name namespace type]} (read-keyword rdr)]
{:type (keyword (or type "v"))
:name (if namespace
(keyword namespace name)
(keyword name))}))
(defn parse
"Parse hugsql SQL string `sql` and return
sequence of statement definitions
of the form:
```
{:hdr {:name [\"my-query\"]
:doc [\"my doc string\"]
:command [\":?\"]
:result [\":1\"]
:file \"sql/queries.sql\"
:line 12}
:sql [\"select * from emp where id = \"
{:type :v :name :id}]}
```
Throws `clojure.lang.ExceptionInfo` on error."
([sql] (parse sql {}))
([sql {:keys [no-header file]}]
(if (string/blank? sql)
(throw (ex-info "SQL is empty" {}))
(let [sql (string/replace sql "\r\n" "\n")
rdr (r/source-logging-push-back-reader sql)
nsb #(StringBuilder.)]
(loop [hdr {}
sql []
sb (nsb)
all []]
(let [c (r/read-char rdr)]
(cond
;; end of string, so return all, filtering out empty
(nil? c)
(vec
(remove #(and (empty? (:hdr %))
(or (empty? (:sql %))
(and
(every? string? (:sql %))
(string/blank? (string/join (:sql %))))))
(conj all
{:hdr hdr
:sql (filterv seq (conj sql (string/trimr sb)))})))
;; SQL comments and hugsql header comments
(or
(sing-line-comment-start? c rdr)
(mult-line-comment-start? c rdr))
(if-let [x (if (sing-line-comment-start? c rdr)
(read-sing-line-comment rdr)
(read-mult-line-comment rdr))]
;; hdr was read from comment
(cond
(map? x)
;; if sql is active, then new hdr section
(if (or (> (.length ^StringBuilder sb) 0) (empty? hdr))
(recur (merge x {:file file :line (max 1 (dec (r/get-line-number rdr)))})
[]
(nsb)
(conj all
{:hdr hdr
:sql (filterv seq (conj sql (str sb)))}))
(recur (merge hdr x) sql sb all))
;; hint
(string? x)
(recur hdr sql (sb-append sb x) all)
:else
;; clj expr was read from comment
(recur hdr (conj sql (str sb) x) (nsb) all))
(recur hdr sql sb all))
;; quoted SQL (which cannot contain hugsql params,
;; so we consider them separately here before
(sql-quoted-start? c)
(recur hdr sql (sb-append sb (read-sql-quoted rdr c)) all)
;; missing an SQL quote
(sql-unmatched-quoted? c)
(parse-error rdr (str "Unmatched SQL quote: " c))
;; postgresql :: type cast is not hugsql param, so skip double-colon
(pg-type-cast-start? rdr c)
(recur hdr sql (sb-append (sb-append sb c) (r/read-char rdr)) all)
;; escaped colon
(escape-start? rdr c)
(recur hdr sql (sb-append sb (r/read-char rdr)) all)
;; hugsql params
(hugsql-param-start? c)
(recur hdr
(vec (filter seq
(conj sql (str sb) (read-hugsql-param rdr))))
(nsb)
all)
;; all else is SQL
:else
(if (and (not (string/blank? sb)) (empty? hdr) (not no-header))
(parse-error rdr "Encountered SQL with no hugsql header")
(recur hdr sql (sb-append sb c) all)))))))))