squirreling 0.4.0 → 0.4.2
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.
- package/README.md +2 -2
- package/package.json +3 -3
- package/src/backend/dataSource.js +1 -1
- package/src/execute/aggregates.js +2 -23
- package/src/execute/execute.js +92 -174
- package/src/execute/expression.js +21 -41
- package/src/execute/having.js +4 -34
- package/src/execute/join.js +357 -0
- package/src/execute/utils.js +70 -1
- package/src/index.d.ts +1 -0
- package/src/parse/expression.js +49 -20
- package/src/parse/parse.js +59 -16
- package/src/types.d.ts +11 -12
- package/src/validation.js +1 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { evaluateExpr } from './expression.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Executes JOIN operations against a base data source
|
|
9
|
+
*
|
|
10
|
+
* @param {AsyncDataSource} leftSource - the left side of the join (FROM table)
|
|
11
|
+
* @param {JoinClause[]} joins - array of join clauses to execute
|
|
12
|
+
* @param {string} leftTableName - name of the left table (for column prefixing)
|
|
13
|
+
* @param {Record<string, AsyncDataSource>} tables - all available tables
|
|
14
|
+
* @returns {Promise<AsyncDataSource>} data source yielding joined rows
|
|
15
|
+
*/
|
|
16
|
+
export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
17
|
+
let currentLeftTable = leftTableName
|
|
18
|
+
|
|
19
|
+
// Single join optimization: stream left rows without buffering
|
|
20
|
+
if (joins.length === 1) {
|
|
21
|
+
const join = joins[0]
|
|
22
|
+
const rightSource = tables[join.table]
|
|
23
|
+
if (rightSource === undefined) {
|
|
24
|
+
throw new Error(`Table "${join.table}" not found`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Buffer right rows for hash index (required for hash join)
|
|
28
|
+
/** @type {AsyncRow[]} */
|
|
29
|
+
const rightRows = []
|
|
30
|
+
for await (const row of rightSource.getRows()) {
|
|
31
|
+
rightRows.push(row)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use alias for column prefixing if present
|
|
35
|
+
const rightTableName = join.alias ?? join.table
|
|
36
|
+
|
|
37
|
+
// Return streaming data source - left rows stream through without buffering
|
|
38
|
+
return {
|
|
39
|
+
async *getRows() {
|
|
40
|
+
yield* hashJoin({
|
|
41
|
+
leftRows: leftSource.getRows(), // Stream directly, not buffered
|
|
42
|
+
rightRows,
|
|
43
|
+
join,
|
|
44
|
+
leftTable: currentLeftTable,
|
|
45
|
+
rightTable: rightTableName,
|
|
46
|
+
tables,
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Multiple joins: buffer intermediate results, stream final join
|
|
53
|
+
/** @type {AsyncRow[]} */
|
|
54
|
+
let leftRows = []
|
|
55
|
+
for await (const row of leftSource.getRows()) {
|
|
56
|
+
leftRows.push(row)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Process all but the last join, buffering intermediate results
|
|
60
|
+
for (let i = 0; i < joins.length - 1; i++) {
|
|
61
|
+
const join = joins[i]
|
|
62
|
+
const rightSource = tables[join.table]
|
|
63
|
+
if (rightSource === undefined) {
|
|
64
|
+
throw new Error(`Table "${join.table}" not found`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {AsyncRow[]} */
|
|
68
|
+
const rightRows = []
|
|
69
|
+
for await (const row of rightSource.getRows()) {
|
|
70
|
+
rightRows.push(row)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Use alias for column prefixing if present
|
|
74
|
+
const rightTableName = join.alias ?? join.table
|
|
75
|
+
|
|
76
|
+
// Collect intermediate results into array for next join
|
|
77
|
+
/** @type {AsyncRow[]} */
|
|
78
|
+
const newLeftRows = []
|
|
79
|
+
const joined = hashJoin({
|
|
80
|
+
leftRows,
|
|
81
|
+
rightRows,
|
|
82
|
+
join,
|
|
83
|
+
leftTable: currentLeftTable,
|
|
84
|
+
rightTable: rightTableName,
|
|
85
|
+
tables,
|
|
86
|
+
})
|
|
87
|
+
for await (const row of joined) {
|
|
88
|
+
newLeftRows.push(row)
|
|
89
|
+
}
|
|
90
|
+
leftRows = newLeftRows
|
|
91
|
+
|
|
92
|
+
// After join, the "left" table for the next join includes all joined tables
|
|
93
|
+
currentLeftTable = `${currentLeftTable}_${rightTableName}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Final join: stream the results
|
|
97
|
+
const lastJoin = joins[joins.length - 1]
|
|
98
|
+
const rightSource = tables[lastJoin.table]
|
|
99
|
+
if (rightSource === undefined) {
|
|
100
|
+
throw new Error(`Table "${lastJoin.table}" not found`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @type {AsyncRow[]} */
|
|
104
|
+
const rightRows = []
|
|
105
|
+
for await (const row of rightSource.getRows()) {
|
|
106
|
+
rightRows.push(row)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Use alias for column prefixing if present
|
|
110
|
+
const lastRightTableName = lastJoin.alias ?? lastJoin.table
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
async *getRows() {
|
|
114
|
+
yield* hashJoin({
|
|
115
|
+
leftRows,
|
|
116
|
+
rightRows,
|
|
117
|
+
join: lastJoin,
|
|
118
|
+
leftTable: currentLeftTable,
|
|
119
|
+
rightTable: lastRightTableName,
|
|
120
|
+
tables,
|
|
121
|
+
})
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks if an expression references a specific table.
|
|
128
|
+
* Returns true if the expression is an identifier prefixed with the table name.
|
|
129
|
+
*
|
|
130
|
+
* @param {ExprNode} expr
|
|
131
|
+
* @param {string} tableName
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function exprReferencesTable(expr, tableName) {
|
|
135
|
+
return expr.type === 'identifier' && expr.name.startsWith(`${tableName}.`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extracts the join key expressions from an ON condition.
|
|
140
|
+
* Handles both `left.col = right.col` and `right.col = left.col` orderings.
|
|
141
|
+
*
|
|
142
|
+
* @param {ExprNode} onCondition
|
|
143
|
+
* @param {string} leftTable
|
|
144
|
+
* @param {string} rightTable
|
|
145
|
+
* @returns {{ leftKey: ExprNode, rightKey: ExprNode } | undefined}
|
|
146
|
+
*/
|
|
147
|
+
function extractJoinKeys(onCondition, leftTable, rightTable) {
|
|
148
|
+
if (onCondition.type === 'binary' && onCondition.op === '=') {
|
|
149
|
+
const { left, right } = onCondition
|
|
150
|
+
|
|
151
|
+
// Check if keys are swapped (right table referenced in left position)
|
|
152
|
+
const leftRefsRight = exprReferencesTable(left, rightTable)
|
|
153
|
+
const rightRefsLeft = exprReferencesTable(right, leftTable)
|
|
154
|
+
|
|
155
|
+
if (leftRefsRight && rightRefsLeft) {
|
|
156
|
+
// Keys are swapped, return them in correct order
|
|
157
|
+
return { leftKey: right, rightKey: left }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Default: assume left operand is for left table
|
|
161
|
+
return { leftKey: left, rightKey: right }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates a NULL-filled row with the given column names
|
|
167
|
+
*
|
|
168
|
+
* @param {string[]} columnNames
|
|
169
|
+
* @returns {AsyncRow}
|
|
170
|
+
*/
|
|
171
|
+
function createNullRow(columnNames) {
|
|
172
|
+
/** @type {AsyncRow} */
|
|
173
|
+
const row = {}
|
|
174
|
+
for (const col of columnNames) {
|
|
175
|
+
row[col] = () => Promise.resolve(null)
|
|
176
|
+
}
|
|
177
|
+
return row
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Merges two rows into one, prefixing columns with table names
|
|
182
|
+
*
|
|
183
|
+
* @param {AsyncRow} leftRow
|
|
184
|
+
* @param {AsyncRow} rightRow
|
|
185
|
+
* @param {string} leftTable
|
|
186
|
+
* @param {string} rightTable
|
|
187
|
+
* @returns {AsyncRow}
|
|
188
|
+
*/
|
|
189
|
+
function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
190
|
+
/** @type {AsyncRow} */
|
|
191
|
+
const merged = {}
|
|
192
|
+
|
|
193
|
+
// Add left table columns with prefix
|
|
194
|
+
for (const [key, cell] of Object.entries(leftRow)) {
|
|
195
|
+
// Skip already-prefixed keys (from previous joins)
|
|
196
|
+
if (!key.includes('.')) {
|
|
197
|
+
merged[`${leftTable}.${key}`] = cell
|
|
198
|
+
} else {
|
|
199
|
+
merged[key] = cell
|
|
200
|
+
}
|
|
201
|
+
// Also keep unqualified name for convenience (may be overwritten if ambiguous)
|
|
202
|
+
merged[key] = cell
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add right table columns with prefix
|
|
206
|
+
for (const [key, cell] of Object.entries(rightRow)) {
|
|
207
|
+
if (!key.includes('.')) {
|
|
208
|
+
merged[`${rightTable}.${key}`] = cell
|
|
209
|
+
} else {
|
|
210
|
+
merged[key] = cell
|
|
211
|
+
}
|
|
212
|
+
// Unqualified name (overwrites if same name exists in left table)
|
|
213
|
+
merged[key] = cell
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return merged
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Performs a hash join between left and right row sets (streaming).
|
|
221
|
+
* Yields rows as they are found instead of buffering all results.
|
|
222
|
+
*
|
|
223
|
+
* @param {Object} params
|
|
224
|
+
* @param {AsyncIterable<AsyncRow>|AsyncRow[]} params.leftRows - rows from left table (can stream)
|
|
225
|
+
* @param {AsyncRow[]} params.rightRows - rows from right table (must be buffered for hash index)
|
|
226
|
+
* @param {JoinClause} params.join - join specification
|
|
227
|
+
* @param {string} params.leftTable - name of left table (for column prefixing)
|
|
228
|
+
* @param {string} params.rightTable - name of right table (for column prefixing, may be alias)
|
|
229
|
+
* @param {Record<string, AsyncDataSource>} params.tables - all tables for expression evaluation
|
|
230
|
+
* @yields {AsyncRow} joined rows
|
|
231
|
+
*/
|
|
232
|
+
async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tables }) {
|
|
233
|
+
const { joinType, on: onCondition } = join
|
|
234
|
+
|
|
235
|
+
if (!onCondition) {
|
|
236
|
+
throw new Error('JOIN requires ON condition')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const keys = extractJoinKeys(onCondition, leftTable, rightTable)
|
|
240
|
+
|
|
241
|
+
// Get column names for NULL row generation (right side is always buffered)
|
|
242
|
+
const rightCols = rightRows.length ? Object.keys(rightRows[0]) : []
|
|
243
|
+
const rightPrefixedCols = rightCols.flatMap(col =>
|
|
244
|
+
col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// Track left column info - captured from first row during iteration
|
|
248
|
+
/** @type {string[]|null} */
|
|
249
|
+
let leftPrefixedCols = null
|
|
250
|
+
|
|
251
|
+
if (keys) {
|
|
252
|
+
// Hash join: build hash map on right table
|
|
253
|
+
/** @type {Map<string, AsyncRow[]>} */
|
|
254
|
+
const hashMap = new Map()
|
|
255
|
+
|
|
256
|
+
// BUILD PHASE: Index right rows by join key
|
|
257
|
+
// Skip null keys - SQL semantics: NULL != NULL
|
|
258
|
+
for (const rightRow of rightRows) {
|
|
259
|
+
const keyValue = await evaluateExpr({ node: keys.rightKey, row: rightRow, tables })
|
|
260
|
+
if (keyValue == null) continue // NULL keys never match
|
|
261
|
+
const keyStr = JSON.stringify(keyValue)
|
|
262
|
+
|
|
263
|
+
let bucket = hashMap.get(keyStr)
|
|
264
|
+
if (!bucket) {
|
|
265
|
+
bucket = []
|
|
266
|
+
hashMap.set(keyStr, bucket)
|
|
267
|
+
}
|
|
268
|
+
bucket.push(rightRow)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Track which right rows matched (only needed for RIGHT/FULL joins)
|
|
272
|
+
/** @type {Set<AsyncRow>|null} */
|
|
273
|
+
const matchedRightRows = joinType === 'RIGHT' || joinType === 'FULL' ? new Set() : null
|
|
274
|
+
|
|
275
|
+
// PROBE PHASE: Stream through left rows, yield matches immediately
|
|
276
|
+
for await (const leftRow of leftRows) {
|
|
277
|
+
// Capture left column info from first row (for NULL row generation)
|
|
278
|
+
if (!leftPrefixedCols) {
|
|
279
|
+
const leftCols = Object.keys(leftRow)
|
|
280
|
+
leftPrefixedCols = leftCols.flatMap(col =>
|
|
281
|
+
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const keyValue = await evaluateExpr({ node: keys.leftKey, row: leftRow, tables })
|
|
286
|
+
const keyStr = JSON.stringify(keyValue)
|
|
287
|
+
|
|
288
|
+
const matchingRightRows = hashMap.get(keyStr)
|
|
289
|
+
|
|
290
|
+
if (matchingRightRows && matchingRightRows.length > 0) {
|
|
291
|
+
for (const rightRow of matchingRightRows) {
|
|
292
|
+
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
293
|
+
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
294
|
+
}
|
|
295
|
+
} else if (joinType === 'LEFT' || joinType === 'FULL') {
|
|
296
|
+
const nullRight = createNullRow(rightPrefixedCols)
|
|
297
|
+
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
298
|
+
}
|
|
299
|
+
// INNER join with no match: don't yield anything
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// UNMATCHED PHASE: Handle unmatched right rows for RIGHT/FULL joins
|
|
303
|
+
if (matchedRightRows) {
|
|
304
|
+
for (const rightRow of rightRows) {
|
|
305
|
+
if (!matchedRightRows.has(rightRow)) {
|
|
306
|
+
// Use empty array if left table was empty (no rows to derive columns from)
|
|
307
|
+
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
308
|
+
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
// Fallback to nested loop for complex ON conditions
|
|
314
|
+
// Left rows stream through, right rows are iterated for each left row
|
|
315
|
+
/** @type {Set<AsyncRow>|null} */
|
|
316
|
+
const matchedRightRows = joinType === 'RIGHT' || joinType === 'FULL' ? new Set() : null
|
|
317
|
+
|
|
318
|
+
for await (const leftRow of leftRows) {
|
|
319
|
+
// Capture left column info from first row (for NULL row generation)
|
|
320
|
+
if (!leftPrefixedCols) {
|
|
321
|
+
const leftCols = Object.keys(leftRow)
|
|
322
|
+
leftPrefixedCols = leftCols.flatMap(col =>
|
|
323
|
+
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let hasMatch = false
|
|
328
|
+
|
|
329
|
+
for (const rightRow of rightRows) {
|
|
330
|
+
const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
331
|
+
const matches = await evaluateExpr({ node: onCondition, row: tempMerged, tables })
|
|
332
|
+
|
|
333
|
+
if (matches) {
|
|
334
|
+
hasMatch = true
|
|
335
|
+
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
336
|
+
yield tempMerged
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!hasMatch && (joinType === 'LEFT' || joinType === 'FULL')) {
|
|
341
|
+
const nullRight = createNullRow(rightPrefixedCols)
|
|
342
|
+
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Handle unmatched right rows for RIGHT/FULL joins
|
|
347
|
+
if (matchedRightRows) {
|
|
348
|
+
for (const rightRow of rightRows) {
|
|
349
|
+
if (!matchedRightRows.has(rightRow)) {
|
|
350
|
+
// Use empty array if left table was empty (no rows to derive columns from)
|
|
351
|
+
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
352
|
+
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
package/src/execute/utils.js
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {AsyncRow, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compares two values for a single ORDER BY term, handling nulls and direction
|
|
7
|
+
*
|
|
8
|
+
* @param {SqlPrimitive} a
|
|
9
|
+
* @param {SqlPrimitive} b
|
|
10
|
+
* @param {OrderByItem} term
|
|
11
|
+
* @returns {number} comparison result
|
|
12
|
+
*/
|
|
13
|
+
export function compareForTerm(a, b, term) {
|
|
14
|
+
const aIsNull = a == null
|
|
15
|
+
const bIsNull = b == null
|
|
16
|
+
|
|
17
|
+
if (aIsNull || bIsNull) {
|
|
18
|
+
if (aIsNull && bIsNull) return 0
|
|
19
|
+
const nullsFirst = term.nulls !== 'LAST'
|
|
20
|
+
if (aIsNull) return nullsFirst ? -1 : 1
|
|
21
|
+
return nullsFirst ? 1 : -1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Compare non-null values
|
|
25
|
+
if (a === b) return 0
|
|
26
|
+
|
|
27
|
+
let cmp
|
|
28
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
29
|
+
cmp = a < b ? -1 : a > b ? 1 : 0
|
|
30
|
+
} else {
|
|
31
|
+
const aa = String(a)
|
|
32
|
+
const bb = String(b)
|
|
33
|
+
cmp = aa < bb ? -1 : aa > bb ? 1 : 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return term.direction === 'DESC' ? -cmp : cmp
|
|
37
|
+
}
|
|
38
|
+
|
|
1
39
|
/**
|
|
2
40
|
* Collects and materialize all results from an async row generator into an array
|
|
3
41
|
*
|
|
4
|
-
* @import {AsyncRow, SqlPrimitive} from '../types.js'
|
|
5
42
|
* @param {AsyncGenerator<AsyncRow>} asyncRows
|
|
6
43
|
* @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
|
|
7
44
|
*/
|
|
@@ -18,3 +55,35 @@ export async function collect(asyncRows) {
|
|
|
18
55
|
}
|
|
19
56
|
return results
|
|
20
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generates a default alias for a derived column expression
|
|
61
|
+
*
|
|
62
|
+
* @param {ExprNode} expr - the expression node
|
|
63
|
+
* @returns {string} the generated alias
|
|
64
|
+
*/
|
|
65
|
+
export function defaultDerivedAlias(expr) {
|
|
66
|
+
if (expr.type === 'identifier') {
|
|
67
|
+
// For qualified names like 'users.name', use just the column part as alias
|
|
68
|
+
if (expr.name.includes('.')) {
|
|
69
|
+
return expr.name.split('.').pop()
|
|
70
|
+
}
|
|
71
|
+
return expr.name
|
|
72
|
+
}
|
|
73
|
+
if (expr.type === 'literal') {
|
|
74
|
+
return String(expr.value)
|
|
75
|
+
}
|
|
76
|
+
if (expr.type === 'cast') {
|
|
77
|
+
return defaultDerivedAlias(expr.expr) + '_as_' + expr.toType
|
|
78
|
+
}
|
|
79
|
+
if (expr.type === 'unary') {
|
|
80
|
+
return expr.op + '_' + defaultDerivedAlias(expr.argument)
|
|
81
|
+
}
|
|
82
|
+
if (expr.type === 'binary') {
|
|
83
|
+
return defaultDerivedAlias(expr.left) + '_' + expr.op + '_' + defaultDerivedAlias(expr.right)
|
|
84
|
+
}
|
|
85
|
+
if (expr.type === 'function') {
|
|
86
|
+
return expr.name.toLowerCase() + '_' + expr.args.map(defaultDerivedAlias).join('_')
|
|
87
|
+
}
|
|
88
|
+
return 'expr'
|
|
89
|
+
}
|
package/src/index.d.ts
CHANGED
package/src/parse/expression.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isAggregateFunc, isStringFunc } from '../validation.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @import { ExprCursor, ExprNode,
|
|
4
|
+
* @import { BinaryOp, ExprCursor, ExprNode, WhenClause } from '../types.js'
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -153,7 +153,7 @@ function parsePrimary(c) {
|
|
|
153
153
|
c.consume() // CASE
|
|
154
154
|
|
|
155
155
|
// Check if it's simple CASE (CASE expr WHEN ...) or searched CASE (CASE WHEN ...)
|
|
156
|
-
/** @type {
|
|
156
|
+
/** @type {ExprNode | undefined} */
|
|
157
157
|
let caseExpr
|
|
158
158
|
const nextTok = c.current()
|
|
159
159
|
if (nextTok.type !== 'keyword' || nextTok.value !== 'WHEN') {
|
|
@@ -162,7 +162,7 @@ function parsePrimary(c) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// Parse WHEN clauses
|
|
165
|
-
/** @type {
|
|
165
|
+
/** @type {WhenClause[]} */
|
|
166
166
|
const whenClauses = []
|
|
167
167
|
while (c.match('keyword', 'WHEN')) {
|
|
168
168
|
const condition = parseExpression(c)
|
|
@@ -176,7 +176,7 @@ function parsePrimary(c) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Parse optional ELSE clause
|
|
179
|
-
/** @type {
|
|
179
|
+
/** @type {ExprNode | undefined} */
|
|
180
180
|
let elseResult
|
|
181
181
|
if (c.match('keyword', 'ELSE')) {
|
|
182
182
|
elseResult = parseExpression(c)
|
|
@@ -301,7 +301,26 @@ function parseComparison(c) {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
// LIKE
|
|
304
|
+
// [NOT] LIKE
|
|
305
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
306
|
+
const nextTok = c.peek(1)
|
|
307
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
308
|
+
c.consume() // NOT
|
|
309
|
+
c.consume() // LIKE
|
|
310
|
+
const right = parsePrimary(c)
|
|
311
|
+
return {
|
|
312
|
+
type: 'unary',
|
|
313
|
+
op: 'NOT',
|
|
314
|
+
argument: {
|
|
315
|
+
type: 'binary',
|
|
316
|
+
op: 'LIKE',
|
|
317
|
+
left,
|
|
318
|
+
right,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
305
324
|
if (tok.type === 'keyword' && tok.value === 'LIKE') {
|
|
306
325
|
c.consume()
|
|
307
326
|
const right = parsePrimary(c)
|
|
@@ -313,7 +332,7 @@ function parseComparison(c) {
|
|
|
313
332
|
}
|
|
314
333
|
}
|
|
315
334
|
|
|
316
|
-
// [NOT] BETWEEN
|
|
335
|
+
// [NOT] BETWEEN - convert to range comparison
|
|
317
336
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
318
337
|
const nextTok = c.peek(1)
|
|
319
338
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
@@ -322,11 +341,12 @@ function parseComparison(c) {
|
|
|
322
341
|
const lower = parsePrimary(c)
|
|
323
342
|
c.expect('keyword', 'AND')
|
|
324
343
|
const upper = parsePrimary(c)
|
|
344
|
+
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
325
345
|
return {
|
|
326
|
-
type: '
|
|
327
|
-
|
|
328
|
-
lower,
|
|
329
|
-
upper,
|
|
346
|
+
type: 'binary',
|
|
347
|
+
op: 'OR',
|
|
348
|
+
left: { type: 'binary', op: '<', left, right: lower },
|
|
349
|
+
right: { type: 'binary', op: '>', left, right: upper },
|
|
330
350
|
}
|
|
331
351
|
}
|
|
332
352
|
}
|
|
@@ -336,11 +356,12 @@ function parseComparison(c) {
|
|
|
336
356
|
const lower = parsePrimary(c)
|
|
337
357
|
c.expect('keyword', 'AND')
|
|
338
358
|
const upper = parsePrimary(c)
|
|
359
|
+
// BETWEEN -> expr >= lower AND expr <= upper
|
|
339
360
|
return {
|
|
340
|
-
type: '
|
|
341
|
-
|
|
342
|
-
lower,
|
|
343
|
-
upper,
|
|
361
|
+
type: 'binary',
|
|
362
|
+
op: 'AND',
|
|
363
|
+
left: { type: 'binary', op: '>=', left, right: lower },
|
|
364
|
+
right: { type: 'binary', op: '<=', left, right: upper },
|
|
344
365
|
}
|
|
345
366
|
}
|
|
346
367
|
|
|
@@ -365,9 +386,13 @@ function parseComparison(c) {
|
|
|
365
386
|
}
|
|
366
387
|
const subquery = c.parseSubquery()
|
|
367
388
|
return {
|
|
368
|
-
type: '
|
|
369
|
-
|
|
370
|
-
|
|
389
|
+
type: 'unary',
|
|
390
|
+
op: 'NOT',
|
|
391
|
+
argument: {
|
|
392
|
+
type: 'in',
|
|
393
|
+
expr: left,
|
|
394
|
+
subquery,
|
|
395
|
+
},
|
|
371
396
|
}
|
|
372
397
|
} else {
|
|
373
398
|
// Parse list of values - we handle the parens
|
|
@@ -380,9 +405,13 @@ function parseComparison(c) {
|
|
|
380
405
|
}
|
|
381
406
|
c.expect('paren', ')')
|
|
382
407
|
return {
|
|
383
|
-
type: '
|
|
384
|
-
|
|
385
|
-
|
|
408
|
+
type: 'unary',
|
|
409
|
+
op: 'NOT',
|
|
410
|
+
argument: {
|
|
411
|
+
type: 'in valuelist',
|
|
412
|
+
expr: left,
|
|
413
|
+
values,
|
|
414
|
+
},
|
|
386
415
|
}
|
|
387
416
|
}
|
|
388
417
|
}
|