squirreling 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @import { ExprCursor, ExprNode, BinaryOp } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * @param {ExprCursor} c
7
+ * @returns {ExprNode}
8
+ */
9
+ export function parseExpression(c) {
10
+ return parseOr(c)
11
+ }
12
+
13
+ /**
14
+ * Exposed so SELECT list parsing can reuse the same notion of "primary"
15
+ * for function arguments, etc.
16
+ *
17
+ * @param {ExprCursor} c
18
+ * @returns {ExprNode}
19
+ */
20
+ export function parsePrimary(c) {
21
+ const tok = c.current()
22
+
23
+ if (tok.type === 'paren' && tok.value === '(') {
24
+ c.consume()
25
+ const expr = parseExpression(c)
26
+ c.expect('paren', ')')
27
+ return expr
28
+ }
29
+
30
+ if (tok.type === 'identifier') {
31
+ const next = c.peek(1)
32
+
33
+ // function call
34
+ if (next.type === 'paren' && next.value === '(') {
35
+ const funcName = tok.value
36
+ // TODO: validate function name
37
+ c.consume() // function name
38
+ c.consume() // '('
39
+
40
+ /** @type {ExprNode[]} */
41
+ const args = []
42
+
43
+ if (c.current().type !== 'paren' || c.current().value !== ')') {
44
+ while (true) {
45
+ const arg = parseExpression(c)
46
+ args.push(arg)
47
+ if (!c.match('comma')) break
48
+ }
49
+ }
50
+
51
+ c.expect('paren', ')')
52
+
53
+ return {
54
+ type: 'function',
55
+ name: funcName,
56
+ args,
57
+ }
58
+ }
59
+
60
+ c.consume()
61
+ let name = tok.value
62
+
63
+ // table.column
64
+ if (c.current().type === 'dot') {
65
+ c.consume()
66
+ const columnTok = c.expectIdentifier()
67
+ name = name + '.' + columnTok.value
68
+ }
69
+
70
+ return {
71
+ type: 'identifier',
72
+ name,
73
+ }
74
+ }
75
+
76
+ if (tok.type === 'number') {
77
+ c.consume()
78
+ return {
79
+ type: 'literal',
80
+ value: tok.numericValue ?? null,
81
+ }
82
+ }
83
+
84
+ if (tok.type === 'string') {
85
+ c.consume()
86
+ return {
87
+ type: 'literal',
88
+ value: tok.value,
89
+ }
90
+ }
91
+
92
+ if (tok.type === 'keyword') {
93
+ if (tok.value === 'TRUE') {
94
+ c.consume()
95
+ return { type: 'literal', value: true }
96
+ }
97
+ if (tok.value === 'FALSE') {
98
+ c.consume()
99
+ return { type: 'literal', value: false }
100
+ }
101
+ if (tok.value === 'NULL') {
102
+ c.consume()
103
+ return { type: 'literal', value: null }
104
+ }
105
+ }
106
+
107
+ if (tok.type === 'operator' && tok.value === '-') {
108
+ c.consume()
109
+ const argument = parsePrimary(c)
110
+ return {
111
+ type: 'unary',
112
+ op: '-',
113
+ argument,
114
+ }
115
+ }
116
+
117
+ throw new Error(
118
+ 'Unexpected token in expression at position ' +
119
+ tok.position +
120
+ ': ' +
121
+ tok.type +
122
+ ' ' +
123
+ tok.value
124
+ )
125
+ }
126
+
127
+ /**
128
+ * @param {ExprCursor} c
129
+ * @returns {ExprNode}
130
+ */
131
+ function parseOr(c) {
132
+ let node = parseAnd(c)
133
+ while (c.match('keyword', 'OR')) {
134
+ const right = parseAnd(c)
135
+ node = {
136
+ type: 'binary',
137
+ op: 'OR',
138
+ left: node,
139
+ right,
140
+ }
141
+ }
142
+ return node
143
+ }
144
+
145
+ /**
146
+ * @param {ExprCursor} c
147
+ * @returns {ExprNode}
148
+ */
149
+ function parseAnd(c) {
150
+ let node = parseNot(c)
151
+ while (c.match('keyword', 'AND')) {
152
+ const right = parseNot(c)
153
+ node = {
154
+ type: 'binary',
155
+ op: 'AND',
156
+ left: node,
157
+ right,
158
+ }
159
+ }
160
+ return node
161
+ }
162
+
163
+ /**
164
+ * @param {ExprCursor} c
165
+ * @returns {ExprNode}
166
+ */
167
+ function parseNot(c) {
168
+ if (c.match('keyword', 'NOT')) {
169
+ const argument = parseNot(c)
170
+ return {
171
+ type: 'unary',
172
+ op: 'NOT',
173
+ argument,
174
+ }
175
+ }
176
+ return parseComparison(c)
177
+ }
178
+
179
+ /**
180
+ * @param {ExprCursor} c
181
+ * @returns {ExprNode}
182
+ */
183
+ function parseComparison(c) {
184
+ const left = parsePrimary(c)
185
+ const tok = c.current()
186
+
187
+ // IS [NOT] NULL
188
+ if (tok.type === 'keyword' && tok.value === 'IS') {
189
+ c.consume()
190
+ const notToken = c.current()
191
+ if (notToken.type === 'keyword' && notToken.value === 'NOT') {
192
+ c.consume()
193
+ c.expect('keyword', 'NULL')
194
+ return {
195
+ type: 'unary',
196
+ op: 'IS NOT NULL',
197
+ argument: left,
198
+ }
199
+ }
200
+ c.expect('keyword', 'NULL')
201
+ return {
202
+ type: 'unary',
203
+ op: 'IS NULL',
204
+ argument: left,
205
+ }
206
+ }
207
+
208
+ // LIKE
209
+ if (tok.type === 'keyword' && tok.value === 'LIKE') {
210
+ c.consume()
211
+ const right = parsePrimary(c)
212
+ return {
213
+ type: 'binary',
214
+ op: 'LIKE',
215
+ left,
216
+ right,
217
+ }
218
+ }
219
+
220
+ if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
221
+ c.consume()
222
+ const right = parsePrimary(c)
223
+ return {
224
+ type: 'binary',
225
+ op: tok.value,
226
+ left,
227
+ right,
228
+ }
229
+ }
230
+
231
+ return left
232
+ }
233
+
234
+ /**
235
+ * @param {string} op
236
+ * @returns {op is BinaryOp}
237
+ */
238
+ function isComparisonOperator(op) {
239
+ return (
240
+ op === '=' ||
241
+ op === '!=' ||
242
+ op === '<>' ||
243
+ op === '<' ||
244
+ op === '>' ||
245
+ op === '<=' ||
246
+ op === '>='
247
+ )
248
+ }