-
Notifications
You must be signed in to change notification settings - Fork 5
/
statement.lisp
215 lines (184 loc) · 5.61 KB
/
statement.lisp
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
(defpackage :cl-yesql/statement
(:documentation "Parser for statements.")
(:use :cl :alexandria :serapeum :esrap)
(:shadow :comment :whitespace :string)
(:shadowing-import-from :cl-yesql/defrule
:defrule)
(:import-from :trivia :match)
(:export
:placeholder
:statement
:lispify-sql-id
:parameter
:parameter-var
:parameter-whitelist
:positional?
:too-many-placeholders))
(in-package :cl-yesql/statement)
(defclass parameter ()
((whitelist
:type list
:initarg :whitelist
:reader parameter-whitelist))
(:default-initargs :whitelist nil))
(defclass named-parameter (parameter)
((var
:type symbol
:initarg :var
:initform (required-argument :var)
:reader parameter-var)))
(defmethod print-object ((self named-parameter) stream)
(print-unreadable-object (self stream :type t)
(format stream "~a" (parameter-var self))))
(defclass placeholder-parameter (parameter)
())
(defclass anonymous-placeholder (placeholder-parameter)
())
(defclass named-placeholder (placeholder-parameter named-parameter)
())
(defclass keyword-parameter (named-parameter)
())
(defgeneric positional? (param)
(:method ((param parameter))
nil)
(:method ((param placeholder-parameter))
t))
(defconst positional-args
(loop for i from 0 to 50
collect (intern (fmt "?~a" i) #.*package*)))
(defun handle-placeholders (orig-statement)
(nlet rec ((statement orig-statement)
(positionals positional-args)
(acc '()))
(cond ((endp statement)
(nreverse acc))
((endp positionals)
(error 'too-many-placeholders
:statement orig-statement))
((and (typep (first statement) 'anonymous-placeholder))
(rec (rest statement)
(rest positionals)
(cons (make 'named-placeholder
:var (first positionals)
:whitelist (parameter-whitelist
(first statement)))
acc)))
(t
(rec (rest statement)
positionals
(cons (first statement) acc))))))
(defun lispify-sql-id (id &key (package *package*))
(~> id
(substitute #\- #\_ _)
string-upcase
(intern package)))
(defcondition too-many-placeholders (error)
((statement :initarg :statement))
(:report (lambda (c s)
(with-slots (statement) c
(format s "Too many (>~a) positional arguments in ~a."
(length positional-args)
statement)))))
(defrule statement
(and substatement (* (and parameter substatement)))
(:lambda (tree)
(~>> tree
flatten
handle-placeholders
(remove-if (conjoin #'stringp #'emptyp)))))
(defrule substatement
(* (or (or (+ (not (or #\? #\: #\' comment-start)))
"::")
comment
string))
(:text t))
(defrule whitelist
(and (and #\{ (* whitespace))
(* whitelist-item)
(and (* whitespace) #\}))
(:lambda (args)
(let* ((whitelist (second args))
(uniqs (remove-duplicates whitelist :test #'string=)))
(if (length= whitelist uniqs) whitelist
(progn
(cerror "Drop duplicated items"
"Duplicate items in whitelist: ~a" whitelist)
uniqs)))))
(defrule whitelist-item
(and (* whitespace)
(+ (not (or whitespace #\, #\})))
(and (* whitespace) (? #\,)))
(:lambda (m)
(text (second m))))
(defrule string
(and string-delimiter
(* (or string-normal string-special))
string-delimiter)
(:lambda (s)
(fmt "'~a'" (text s))))
(defrule string-delimiter "'"
(:constant nil))
(defrule string-normal
(not (or #\' #\\)))
(defrule string-special
(and #\\ character)
(:function second))
(defrule parameter
(and simple-parameter (? whitelist))
(:lambda (args)
(apply #'make
(append (first args)
(list :whitelist (second args))))))
(defrule simple-parameter
(or named-placeholder
placeholder-parameter
keyword-parameter))
(defrule placeholder-parameter "?"
(:lambda (args)
(declare (ignore args))
(list 'anonymous-placeholder)))
(defrule named-placeholder
(and "?" parameter-name)
(:lambda (args)
(list 'named-placeholder
:var (second args))))
(defrule keyword-parameter
(and ":" parameter-name)
(:lambda (args)
(list 'keyword-parameter
:var (second args))))
(defrule parameter-name
(+ (not
#.`(or whitespace newline
,@(coerce "{},\"':&;()|=+\-*%/\\<>^" 'list))))
(:lambda (args)
(~> args
text
string-upcase
lispify-sql-id)))
(defrule single-line-comment-start "--")
(defrule single-line-comment
(and single-line-comment-start
(* (not newline))
(? newline))
(:lambda (args)
;; When possible, preserve single-line comments by rewriting them
;; as multi-line comments.
(let ((comment-text (second args)))
;; Perversity.
(if (search "/*" comment-text)
nil
(list "/*" (text (second args)) "*/")))))
(defrule multi-line-comment-start "/*")
(defrule multi-line-comment
(and multi-line-comment-start
(* (not "*/"))
"*/")
(:lambda (args)
(list "/*" (text (second args)) "*/")))
(defrule comment-start (or single-line-comment-start multi-line-comment-start))
(defrule comment (or single-line-comment multi-line-comment))
(defrule whitespace
(+ (or #\Space #\Tab)))
(defrule newline (or #\Newline (and #\Return #\Newline))
(:text t))