squirreling 0.4.1 → 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/package.json +3 -3
- package/src/execute/execute.js +69 -130
- package/src/execute/expression.js +6 -37
- package/src/execute/having.js +1 -15
- package/src/execute/utils.js +38 -1
- package/src/index.d.ts +1 -0
- package/src/parse/expression.js +45 -16
- package/src/types.d.ts +2 -10
- package/src/validation.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "24.10.1",
|
|
41
|
-
"@vitest/coverage-v8": "4.0.
|
|
41
|
+
"@vitest/coverage-v8": "4.0.15",
|
|
42
42
|
"eslint": "9.39.1",
|
|
43
43
|
"eslint-plugin-jsdoc": "61.4.1",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
|
-
"vitest": "4.0.
|
|
45
|
+
"vitest": "4.0.15"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -4,7 +4,7 @@ import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
|
4
4
|
import { evaluateExpr } from './expression.js'
|
|
5
5
|
import { evaluateHavingExpr } from './having.js'
|
|
6
6
|
import { executeJoins } from './join.js'
|
|
7
|
-
import { defaultDerivedAlias } from './utils.js'
|
|
7
|
+
import { compareForTerm, defaultDerivedAlias } from './utils.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
|
|
@@ -89,31 +89,6 @@ async function stableRowKey(row) {
|
|
|
89
89
|
return parts.join('|')
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
* Compares two SQL values for sorting
|
|
94
|
-
*
|
|
95
|
-
* @param {SqlPrimitive} a
|
|
96
|
-
* @param {SqlPrimitive} b
|
|
97
|
-
* @returns {number} negative if a < b, positive if a > b, 0 if equal
|
|
98
|
-
*/
|
|
99
|
-
function compareValues(a, b) {
|
|
100
|
-
if (a === b) return 0
|
|
101
|
-
if (a == null) return -1
|
|
102
|
-
if (b == null) return 1
|
|
103
|
-
|
|
104
|
-
if (typeof a === 'number' && typeof b === 'number') {
|
|
105
|
-
if (a < b) return -1
|
|
106
|
-
if (a > b) return 1
|
|
107
|
-
return 0
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const aa = String(a)
|
|
111
|
-
const bb = String(b)
|
|
112
|
-
if (aa < bb) return -1
|
|
113
|
-
if (aa > bb) return 1
|
|
114
|
-
return 0
|
|
115
|
-
}
|
|
116
|
-
|
|
117
92
|
/**
|
|
118
93
|
* Applies DISTINCT filtering to remove duplicate rows
|
|
119
94
|
*
|
|
@@ -135,127 +110,89 @@ async function applyDistinct(rows, distinct) {
|
|
|
135
110
|
}
|
|
136
111
|
return result
|
|
137
112
|
}
|
|
138
|
-
|
|
139
113
|
/**
|
|
140
|
-
* Applies ORDER BY sorting to
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
144
|
-
* @param {Record<string, AsyncDataSource>} tables
|
|
145
|
-
* @returns {Promise<AsyncRow[]>} the sorted row sources
|
|
146
|
-
*/
|
|
147
|
-
async function sortRowSources(rows, orderBy, tables) {
|
|
148
|
-
if (!orderBy.length) return rows
|
|
149
|
-
|
|
150
|
-
// Pre-evaluate ORDER BY expressions for all rows
|
|
151
|
-
/** @type {SqlPrimitive[][]} */
|
|
152
|
-
const evaluatedValues = []
|
|
153
|
-
for (const row of rows) {
|
|
154
|
-
/** @type {SqlPrimitive[]} */
|
|
155
|
-
const rowValues = []
|
|
156
|
-
for (const term of orderBy) {
|
|
157
|
-
const value = await evaluateExpr({ node: term.expr, row, tables })
|
|
158
|
-
rowValues.push(value)
|
|
159
|
-
}
|
|
160
|
-
evaluatedValues.push(rowValues)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Create index array and sort it
|
|
164
|
-
const indices = rows.map((_, i) => i)
|
|
165
|
-
indices.sort((aIdx, bIdx) => {
|
|
166
|
-
for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
|
|
167
|
-
const term = orderBy[termIdx]
|
|
168
|
-
const dir = term.direction
|
|
169
|
-
const av = evaluatedValues[aIdx][termIdx]
|
|
170
|
-
const bv = evaluatedValues[bIdx][termIdx]
|
|
171
|
-
|
|
172
|
-
// Handle NULLS FIRST / NULLS LAST
|
|
173
|
-
const aIsNull = av == null
|
|
174
|
-
const bIsNull = bv == null
|
|
175
|
-
|
|
176
|
-
if (aIsNull || bIsNull) {
|
|
177
|
-
if (aIsNull && bIsNull) continue
|
|
178
|
-
|
|
179
|
-
const nullsFirst = term.nulls === 'LAST' ? false : true
|
|
180
|
-
|
|
181
|
-
if (aIsNull) {
|
|
182
|
-
return nullsFirst ? -1 : 1
|
|
183
|
-
} else {
|
|
184
|
-
return nullsFirst ? 1 : -1
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const cmp = compareValues(av, bv)
|
|
189
|
-
if (cmp !== 0) {
|
|
190
|
-
return dir === 'DESC' ? -cmp : cmp
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return 0
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Return sorted rows
|
|
197
|
-
return indices.map(i => rows[i])
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Applies ORDER BY sorting to rows
|
|
114
|
+
* Applies ORDER BY sorting to rows using multi-pass lazy evaluation.
|
|
115
|
+
* Secondary ORDER BY columns are only evaluated for rows that tie on
|
|
116
|
+
* previous columns, reducing expensive cell evaluations.
|
|
202
117
|
*
|
|
203
118
|
* @param {AsyncRow[]} rows - the input rows
|
|
204
119
|
* @param {OrderByItem[]} orderBy - the sort specifications
|
|
205
120
|
* @param {Record<string, AsyncDataSource>} tables
|
|
206
121
|
* @returns {Promise<AsyncRow[]>} the sorted rows
|
|
207
122
|
*/
|
|
208
|
-
async function
|
|
123
|
+
async function sortRows(rows, orderBy, tables) {
|
|
209
124
|
if (!orderBy.length) return rows
|
|
210
125
|
|
|
211
|
-
//
|
|
212
|
-
/** @type {SqlPrimitive[][]} */
|
|
213
|
-
const evaluatedValues =
|
|
214
|
-
for (const row of rows) {
|
|
215
|
-
/** @type {SqlPrimitive[]} */
|
|
216
|
-
const rowValues = []
|
|
217
|
-
for (const term of orderBy) {
|
|
218
|
-
const value = await evaluateExpr({ node: term.expr, row, tables })
|
|
219
|
-
rowValues.push(value)
|
|
220
|
-
}
|
|
221
|
-
evaluatedValues.push(rowValues)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Create index array and sort it
|
|
225
|
-
const indices = rows.map((_, i) => i)
|
|
226
|
-
indices.sort((aIdx, bIdx) => {
|
|
227
|
-
for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
|
|
228
|
-
const term = orderBy[termIdx]
|
|
229
|
-
const dir = term.direction
|
|
230
|
-
const av = evaluatedValues[aIdx][termIdx]
|
|
231
|
-
const bv = evaluatedValues[bIdx][termIdx]
|
|
126
|
+
// Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
|
|
127
|
+
/** @type {(SqlPrimitive | undefined)[][]} */
|
|
128
|
+
const evaluatedValues = rows.map(() => Array(orderBy.length))
|
|
232
129
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
130
|
+
// Start with all indices in one group
|
|
131
|
+
/** @type {number[][]} */
|
|
132
|
+
let groups = [rows.map((_, i) => i)]
|
|
236
133
|
|
|
237
|
-
|
|
238
|
-
|
|
134
|
+
// Process each ORDER BY column incrementally
|
|
135
|
+
for (let orderByIdx = 0; orderByIdx < orderBy.length; orderByIdx++) {
|
|
136
|
+
const term = orderBy[orderByIdx]
|
|
137
|
+
/** @type {number[][]} */
|
|
138
|
+
const nextGroups = []
|
|
239
139
|
|
|
240
|
-
|
|
140
|
+
for (const group of groups) {
|
|
141
|
+
// Single-element groups don't need sorting or evaluation
|
|
142
|
+
if (group.length <= 1) {
|
|
143
|
+
nextGroups.push(group)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
241
146
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
147
|
+
// Evaluate this column for all rows in the group
|
|
148
|
+
for (const idx of group) {
|
|
149
|
+
if (evaluatedValues[idx][orderByIdx] === undefined) {
|
|
150
|
+
evaluatedValues[idx][orderByIdx] = await evaluateExpr({
|
|
151
|
+
node: term.expr,
|
|
152
|
+
row: rows[idx],
|
|
153
|
+
tables,
|
|
154
|
+
})
|
|
246
155
|
}
|
|
247
156
|
}
|
|
248
157
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
158
|
+
// Sort the group by this column
|
|
159
|
+
group.sort((aIdx, bIdx) => {
|
|
160
|
+
const av = evaluatedValues[aIdx][orderByIdx]
|
|
161
|
+
const bv = evaluatedValues[bIdx][orderByIdx]
|
|
162
|
+
return compareForTerm(av, bv, term)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Split into sub-groups based on ties (for next column)
|
|
166
|
+
if (orderByIdx < orderBy.length - 1) {
|
|
167
|
+
/** @type {number[]} */
|
|
168
|
+
let currentSubGroup = [group[0]]
|
|
169
|
+
for (let i = 1; i < group.length; i++) {
|
|
170
|
+
const prevIdx = group[i - 1]
|
|
171
|
+
const currIdx = group[i]
|
|
172
|
+
const prevVal = evaluatedValues[prevIdx][orderByIdx]
|
|
173
|
+
const currVal = evaluatedValues[currIdx][orderByIdx]
|
|
174
|
+
|
|
175
|
+
if (compareForTerm(prevVal, currVal, term) === 0) {
|
|
176
|
+
// Same value, extend current sub-group
|
|
177
|
+
currentSubGroup.push(currIdx)
|
|
178
|
+
} else {
|
|
179
|
+
// Different value, start new sub-group
|
|
180
|
+
nextGroups.push(currentSubGroup)
|
|
181
|
+
currentSubGroup = [currIdx]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
nextGroups.push(currentSubGroup)
|
|
185
|
+
} else {
|
|
186
|
+
// Last column, no need to split
|
|
187
|
+
nextGroups.push(group)
|
|
252
188
|
}
|
|
253
189
|
}
|
|
254
|
-
return 0
|
|
255
|
-
})
|
|
256
190
|
|
|
257
|
-
|
|
258
|
-
|
|
191
|
+
groups = nextGroups
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Flatten groups to get final sorted indices
|
|
195
|
+
return groups.flat().map(i => rows[i])
|
|
259
196
|
}
|
|
260
197
|
|
|
261
198
|
/**
|
|
@@ -466,7 +403,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
466
403
|
} else {
|
|
467
404
|
// No grouping, simple projection
|
|
468
405
|
// Sort before projection so ORDER BY can access columns not in SELECT
|
|
469
|
-
const sorted = await
|
|
406
|
+
const sorted = await sortRows(filtered, select.orderBy, tables)
|
|
470
407
|
|
|
471
408
|
// OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
|
|
472
409
|
// to avoid reading expensive cells for rows that won't be in the final result
|
|
@@ -498,7 +435,9 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
498
435
|
projected = await applyDistinct(projected, select.distinct)
|
|
499
436
|
|
|
500
437
|
// Step 5: ORDER BY (final sort for grouped queries)
|
|
501
|
-
|
|
438
|
+
if (useGrouping) {
|
|
439
|
+
projected = await sortRows(projected, select.orderBy, tables)
|
|
440
|
+
}
|
|
502
441
|
|
|
503
442
|
// Step 6: OFFSET and LIMIT
|
|
504
443
|
// For non-DISTINCT, non-grouping queries, OFFSET/LIMIT was already applied before projection
|
|
@@ -110,21 +110,6 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// BETWEEN and NOT BETWEEN
|
|
114
|
-
if (node.type === 'between' || node.type === 'not between') {
|
|
115
|
-
const expr = await evaluateExpr({ node: node.expr, row, tables })
|
|
116
|
-
const lower = await evaluateExpr({ node: node.lower, row, tables })
|
|
117
|
-
const upper = await evaluateExpr({ node: node.upper, row, tables })
|
|
118
|
-
|
|
119
|
-
// If any value is NULL, return false (SQL behavior)
|
|
120
|
-
if (expr == null || lower == null || upper == null) {
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const isBetween = expr >= lower && expr <= upper
|
|
125
|
-
return node.type === 'between' ? isBetween : !isBetween
|
|
126
|
-
}
|
|
127
|
-
|
|
128
113
|
// Function calls
|
|
129
114
|
if (node.type === 'function') {
|
|
130
115
|
const funcName = node.name.toUpperCase()
|
|
@@ -200,6 +185,11 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
200
185
|
return String(str).replaceAll(String(searchStr), String(replaceStr))
|
|
201
186
|
}
|
|
202
187
|
|
|
188
|
+
if (funcName === 'RANDOM' || funcName === 'RAND') {
|
|
189
|
+
if (args.length !== 0) throw new Error(`${funcName} takes no arguments`)
|
|
190
|
+
return Math.random()
|
|
191
|
+
}
|
|
192
|
+
|
|
203
193
|
throw new Error('Unsupported function ' + funcName)
|
|
204
194
|
}
|
|
205
195
|
|
|
@@ -238,16 +228,7 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
238
228
|
}
|
|
239
229
|
return false
|
|
240
230
|
}
|
|
241
|
-
|
|
242
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
243
|
-
for (const valueNode of node.values) {
|
|
244
|
-
const val = await evaluateExpr({ node: valueNode, row, tables })
|
|
245
|
-
if (exprVal === val) return false
|
|
246
|
-
}
|
|
247
|
-
return true
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// IN and NOT IN with subqueries
|
|
231
|
+
// IN with subqueries
|
|
251
232
|
if (node.type === 'in') {
|
|
252
233
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
253
234
|
const results = executeSelect(node.subquery, tables)
|
|
@@ -260,18 +241,6 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
260
241
|
}
|
|
261
242
|
return values.includes(exprVal)
|
|
262
243
|
}
|
|
263
|
-
if (node.type === 'not in') {
|
|
264
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables })
|
|
265
|
-
const results = executeSelect(node.subquery, tables)
|
|
266
|
-
/** @type {SqlPrimitive[]} */
|
|
267
|
-
const values = []
|
|
268
|
-
for await (const resRow of results) {
|
|
269
|
-
const firstKey = Object.keys(resRow)[0]
|
|
270
|
-
const val = await resRow[firstKey]()
|
|
271
|
-
values.push(val)
|
|
272
|
-
}
|
|
273
|
-
return !values.includes(exprVal)
|
|
274
|
-
}
|
|
275
244
|
|
|
276
245
|
// EXISTS and NOT EXISTS with subqueries
|
|
277
246
|
if (node.type === 'exists') {
|
package/src/execute/having.js
CHANGED
|
@@ -76,20 +76,6 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
if (expr.type === 'between' || expr.type === 'not between') {
|
|
80
|
-
const exprVal = await evaluateHavingValue(expr.expr, context, group, tables)
|
|
81
|
-
const lower = await evaluateHavingValue(expr.lower, context, group, tables)
|
|
82
|
-
const upper = await evaluateHavingValue(expr.upper, context, group, tables)
|
|
83
|
-
|
|
84
|
-
// If any value is NULL, return false (SQL behavior)
|
|
85
|
-
if (exprVal == null || lower == null || upper == null) {
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const isBetween = exprVal >= lower && exprVal <= upper
|
|
90
|
-
return expr.type === 'between' ? isBetween : !isBetween
|
|
91
|
-
}
|
|
92
|
-
|
|
93
79
|
// For other expression types, use the context row
|
|
94
80
|
return Boolean(await evaluateExpr({ node: expr, row: context, tables }))
|
|
95
81
|
}
|
|
@@ -112,7 +98,7 @@ function evaluateHavingValue(expr, context, group, tables) {
|
|
|
112
98
|
}
|
|
113
99
|
|
|
114
100
|
// For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
|
|
115
|
-
if (expr.type === 'binary' || expr.type === 'unary'
|
|
101
|
+
if (expr.type === 'binary' || expr.type === 'unary') {
|
|
116
102
|
return evaluateHavingExpr(expr, context, group, tables)
|
|
117
103
|
}
|
|
118
104
|
|
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, ExprNode, SqlPrimitive} from '../types.js'
|
|
5
42
|
* @param {AsyncGenerator<AsyncRow>} asyncRows
|
|
6
43
|
* @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
|
|
7
44
|
*/
|
package/src/index.d.ts
CHANGED
package/src/parse/expression.js
CHANGED
|
@@ -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
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -90,21 +90,14 @@ export interface CastNode {
|
|
|
90
90
|
toType: string
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export interface BetweenNode {
|
|
94
|
-
type: 'between' | 'not between'
|
|
95
|
-
expr: ExprNode
|
|
96
|
-
lower: ExprNode
|
|
97
|
-
upper: ExprNode
|
|
98
|
-
}
|
|
99
|
-
|
|
100
93
|
export interface InSubqueryNode {
|
|
101
|
-
type: 'in'
|
|
94
|
+
type: 'in'
|
|
102
95
|
expr: ExprNode
|
|
103
96
|
subquery: SelectStatement
|
|
104
97
|
}
|
|
105
98
|
|
|
106
99
|
export interface InValuesNode {
|
|
107
|
-
type: 'in valuelist'
|
|
100
|
+
type: 'in valuelist'
|
|
108
101
|
expr: ExprNode
|
|
109
102
|
values: ExprNode[]
|
|
110
103
|
}
|
|
@@ -138,7 +131,6 @@ export type ExprNode =
|
|
|
138
131
|
| BinaryNode
|
|
139
132
|
| FunctionNode
|
|
140
133
|
| CastNode
|
|
141
|
-
| BetweenNode
|
|
142
134
|
| InSubqueryNode
|
|
143
135
|
| InValuesNode
|
|
144
136
|
| ExistsNode
|
package/src/validation.js
CHANGED
|
@@ -13,5 +13,5 @@ export function isAggregateFunc(name) {
|
|
|
13
13
|
* @returns {name is StringFunc}
|
|
14
14
|
*/
|
|
15
15
|
export function isStringFunc(name) {
|
|
16
|
-
return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE'].includes(name)
|
|
16
|
+
return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE', 'RANDOM', 'RAND'].includes(name)
|
|
17
17
|
}
|