squirreling 0.4.5 → 0.4.7
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 +11 -0
- package/package.json +1 -1
- package/src/execute/aggregates.js +28 -3
- package/src/execute/execute.js +5 -5
- package/src/execute/expression.js +79 -43
- package/src/execute/having.js +6 -26
- package/src/execute/join.js +3 -3
- package/src/execute/utils.js +65 -3
- package/src/parse/comparison.js +9 -9
- package/src/parse/expression.js +48 -0
- package/src/types.d.ts +18 -5
- package/src/validation.js +18 -4
package/README.md
CHANGED
|
@@ -67,3 +67,14 @@ const allUsers: Record<string, SqlPrimitive>[] = await collect(executeSql({
|
|
|
67
67
|
}))
|
|
68
68
|
console.log(allUsers)
|
|
69
69
|
```
|
|
70
|
+
|
|
71
|
+
## Supported SQL Features
|
|
72
|
+
|
|
73
|
+
- `SELECT` statements with `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`
|
|
74
|
+
- Subqueries in `SELECT`, `FROM`, and `WHERE` clauses
|
|
75
|
+
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`
|
|
76
|
+
- `GROUP BY` and `HAVING` clauses
|
|
77
|
+
- Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
78
|
+
- String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
|
|
79
|
+
- Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
80
|
+
- Basic expressions and arithmetic operations
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { evaluateExpr } from './expression.js'
|
|
2
|
-
import { defaultDerivedAlias } from './utils.js'
|
|
2
|
+
import { defaultDerivedAlias, stringify } from './utils.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Evaluates an aggregate function over a set of rows
|
|
6
6
|
*
|
|
7
|
-
* @import { AggregateColumn, AsyncDataSource,
|
|
7
|
+
* @import { AggregateColumn, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
8
8
|
* @param {Object} options
|
|
9
9
|
* @param {AggregateColumn} options.col - aggregate column definition
|
|
10
10
|
* @param {AsyncRow[]} options.rows - rows to aggregate
|
|
11
11
|
* @param {Record<string, AsyncDataSource>} options.tables
|
|
12
|
-
* @returns {Promise<
|
|
12
|
+
* @returns {Promise<SqlPrimitive>} aggregated result
|
|
13
13
|
*/
|
|
14
14
|
export async function evaluateAggregate({ col, rows, tables }) {
|
|
15
15
|
const { arg, func } = col
|
|
@@ -70,6 +70,31 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
70
70
|
if (func === 'MAX') return max
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
if (func === 'JSON_ARRAYAGG') {
|
|
74
|
+
if (arg.kind === 'star') {
|
|
75
|
+
throw new Error('JSON_ARRAYAGG(*) is not supported, use a column name or expression')
|
|
76
|
+
}
|
|
77
|
+
/** @type {SqlPrimitive[]} */
|
|
78
|
+
const values = []
|
|
79
|
+
if (arg.quantifier === 'distinct') {
|
|
80
|
+
const seen = new Set()
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
83
|
+
const key = stringify(v)
|
|
84
|
+
if (!seen.has(key)) {
|
|
85
|
+
seen.add(key)
|
|
86
|
+
values.push(v)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
92
|
+
values.push(v)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return values
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
throw new Error('Unsupported aggregate function ' + func)
|
|
74
99
|
}
|
|
75
100
|
|
package/src/execute/execute.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
2
2
|
import { parseSql } from '../parse/parse.js'
|
|
3
3
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
4
|
+
import { extractColumns } from './columns.js'
|
|
4
5
|
import { evaluateExpr } from './expression.js'
|
|
5
6
|
import { evaluateHavingExpr } from './having.js'
|
|
6
7
|
import { executeJoins } from './join.js'
|
|
7
|
-
import { compareForTerm, defaultDerivedAlias } from './utils.js'
|
|
8
|
-
import { extractColumns } from './columns.js'
|
|
8
|
+
import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions,
|
|
11
|
+
* @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -85,7 +85,7 @@ async function stableRowKey(row) {
|
|
|
85
85
|
const parts = []
|
|
86
86
|
for (const k of keys) {
|
|
87
87
|
const v = await row[k]()
|
|
88
|
-
parts.push(k + ':' +
|
|
88
|
+
parts.push(k + ':' + stringify(v))
|
|
89
89
|
}
|
|
90
90
|
return parts.join('|')
|
|
91
91
|
}
|
|
@@ -358,7 +358,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
358
358
|
const keyParts = []
|
|
359
359
|
for (const expr of select.groupBy) {
|
|
360
360
|
const v = await evaluateExpr({ node: expr, row, tables })
|
|
361
|
-
keyParts.push(
|
|
361
|
+
keyParts.push(stringify(v))
|
|
362
362
|
}
|
|
363
363
|
const key = keyParts.join('|')
|
|
364
364
|
let group = map.get(key)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeSelect } from './execute.js'
|
|
2
|
+
import { applyBinaryOp, stringify } from './utils.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
|
|
@@ -59,60 +60,30 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
59
60
|
if (node.op === '-') {
|
|
60
61
|
const val = await evaluateExpr({ node: node.argument, row, tables })
|
|
61
62
|
if (val == null) return null
|
|
62
|
-
return -
|
|
63
|
+
return -val
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// Binary operators
|
|
67
68
|
if (node.type === 'binary') {
|
|
69
|
+
const left = await evaluateExpr({ node: node.left, row, tables })
|
|
70
|
+
|
|
71
|
+
// Short-circuit evaluation for AND and OR
|
|
68
72
|
if (node.op === 'AND') {
|
|
69
|
-
|
|
70
|
-
if (!leftVal) return false
|
|
71
|
-
return Boolean(await evaluateExpr({ node: node.right, row, tables }))
|
|
73
|
+
if (!left) return false
|
|
72
74
|
}
|
|
73
|
-
|
|
74
75
|
if (node.op === 'OR') {
|
|
75
|
-
|
|
76
|
-
if (leftVal) return true
|
|
77
|
-
return Boolean(await evaluateExpr({ node: node.right, row, tables }))
|
|
76
|
+
if (left) return true
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
const left = await evaluateExpr({ node: node.left, row, tables })
|
|
81
79
|
const right = await evaluateExpr({ node: node.right, row, tables })
|
|
82
|
-
|
|
83
|
-
// In SQL, NULL comparisons with =, !=, <> always return false (unknown)
|
|
84
|
-
// You must use IS NULL or IS NOT NULL to check for NULL
|
|
85
|
-
if (left == null || right == null) {
|
|
86
|
-
if (node.op === '=' || node.op === '!=' || node.op === '<>') {
|
|
87
|
-
return false
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (node.op === '=') return left === right
|
|
92
|
-
if (node.op === '!=' || node.op === '<>') return left !== right
|
|
93
|
-
if (node.op === '<') return left < right
|
|
94
|
-
if (node.op === '>') return left > right
|
|
95
|
-
if (node.op === '<=') return left <= right
|
|
96
|
-
if (node.op === '>=') return left >= right
|
|
97
|
-
|
|
98
|
-
if (node.op === 'LIKE') {
|
|
99
|
-
const str = String(left)
|
|
100
|
-
const pattern = String(right)
|
|
101
|
-
// Convert SQL LIKE pattern to regex
|
|
102
|
-
// % matches zero or more characters
|
|
103
|
-
// _ matches exactly one character
|
|
104
|
-
const regexPattern = pattern
|
|
105
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
106
|
-
.replace(/%/g, '.*') // Replace % with .*
|
|
107
|
-
.replace(/_/g, '.') // Replace _ with .
|
|
108
|
-
const regex = new RegExp('^' + regexPattern + '$', 'i')
|
|
109
|
-
return regex.test(str)
|
|
110
|
-
}
|
|
80
|
+
return applyBinaryOp(node.op, left, right)
|
|
111
81
|
}
|
|
112
82
|
|
|
113
83
|
// Function calls
|
|
114
84
|
if (node.type === 'function') {
|
|
115
85
|
const funcName = node.name.toUpperCase()
|
|
86
|
+
/** @type {SqlPrimitive[]} */
|
|
116
87
|
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
|
|
117
88
|
|
|
118
89
|
if (funcName === 'UPPER') {
|
|
@@ -132,8 +103,9 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
132
103
|
if (funcName === 'CONCAT') {
|
|
133
104
|
if (args.length < 1) throw new Error('CONCAT requires at least 1 argument')
|
|
134
105
|
// SQL CONCAT returns NULL if any argument is NULL
|
|
135
|
-
|
|
136
|
-
|
|
106
|
+
if (args.some(a => a == null)) return null
|
|
107
|
+
if (args.some(a => typeof a === 'object')) {
|
|
108
|
+
throw new Error('CONCAT does not support object arguments')
|
|
137
109
|
}
|
|
138
110
|
return args.map(a => String(a)).join('')
|
|
139
111
|
}
|
|
@@ -190,6 +162,67 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
190
162
|
return Math.random()
|
|
191
163
|
}
|
|
192
164
|
|
|
165
|
+
if (funcName === 'JSON_OBJECT') {
|
|
166
|
+
if (args.length % 2 !== 0) {
|
|
167
|
+
throw new Error('JSON_OBJECT requires an even number of arguments (key-value pairs)')
|
|
168
|
+
}
|
|
169
|
+
/** @type {Record<string, SqlPrimitive>} */
|
|
170
|
+
const result = {}
|
|
171
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
172
|
+
const key = args[i]
|
|
173
|
+
const value = args[i + 1]
|
|
174
|
+
if (key == null) {
|
|
175
|
+
throw new Error('JSON_OBJECT: key cannot be null')
|
|
176
|
+
}
|
|
177
|
+
result[String(key)] = value
|
|
178
|
+
}
|
|
179
|
+
return result
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
|
|
183
|
+
if (args.length !== 2) throw new Error(`${funcName} requires exactly 2 arguments`)
|
|
184
|
+
let jsonArg = args[0]
|
|
185
|
+
const pathArg = args[1]
|
|
186
|
+
if (jsonArg == null || pathArg == null) return null
|
|
187
|
+
|
|
188
|
+
// Parse JSON if string, otherwise use directly
|
|
189
|
+
if (typeof jsonArg === 'string') {
|
|
190
|
+
try {
|
|
191
|
+
jsonArg = JSON.parse(jsonArg)
|
|
192
|
+
} catch {
|
|
193
|
+
throw new Error(`${funcName}: invalid JSON string`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof jsonArg !== 'object') {
|
|
197
|
+
throw new Error(`${funcName}: first argument must be JSON string or object`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Parse path ("$.foo.bar[0].baz" or "foo.bar[0]")
|
|
201
|
+
const path = String(pathArg)
|
|
202
|
+
const normalizedPath = path.startsWith('$') ? path.slice(1) : path
|
|
203
|
+
|
|
204
|
+
// Navigate the path
|
|
205
|
+
let current = jsonArg
|
|
206
|
+
const segments = normalizedPath.match(/\.?([^.[]+)|\[(\d+)\]/g) || []
|
|
207
|
+
for (const segment of segments) {
|
|
208
|
+
if (current == null) return null
|
|
209
|
+
if (segment.startsWith('[')) {
|
|
210
|
+
// Array index access
|
|
211
|
+
const index = parseInt(segment.slice(1, -1), 10)
|
|
212
|
+
if (!Array.isArray(current)) return null
|
|
213
|
+
current = current[index]
|
|
214
|
+
} else {
|
|
215
|
+
// Property access
|
|
216
|
+
const key = segment.startsWith('.') ? segment.slice(1) : segment
|
|
217
|
+
if (typeof current !== 'object' || Array.isArray(current)) return null
|
|
218
|
+
current = current[key]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (current == null) return null
|
|
223
|
+
return current
|
|
224
|
+
}
|
|
225
|
+
|
|
193
226
|
throw new Error('Unsupported function ' + funcName)
|
|
194
227
|
}
|
|
195
228
|
|
|
@@ -197,6 +230,12 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
197
230
|
const val = await evaluateExpr({ node: node.expr, row, tables })
|
|
198
231
|
if (val == null) return null
|
|
199
232
|
const toType = node.toType.toUpperCase()
|
|
233
|
+
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
234
|
+
if (typeof val === 'object') return stringify(val)
|
|
235
|
+
return String(val)
|
|
236
|
+
}
|
|
237
|
+
// Can only cast primitives to other primitive types
|
|
238
|
+
if (typeof val === 'object') throw new Error(`Cannot CAST object to type ${node.toType}`)
|
|
200
239
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
201
240
|
const num = Number(val)
|
|
202
241
|
if (isNaN(num)) return null
|
|
@@ -210,9 +249,6 @@ export async function evaluateExpr({ node, row, tables }) {
|
|
|
210
249
|
if (isNaN(num)) return null
|
|
211
250
|
return num
|
|
212
251
|
}
|
|
213
|
-
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
214
|
-
return String(val)
|
|
215
|
-
}
|
|
216
252
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
217
253
|
return Boolean(val)
|
|
218
254
|
}
|
package/src/execute/having.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAggregateFunc } from '../validation.js'
|
|
2
2
|
import { evaluateExpr } from './expression.js'
|
|
3
|
+
import { applyBinaryOp } from './utils.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
|
|
@@ -30,38 +31,17 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
|
|
|
30
31
|
|
|
31
32
|
if (expr.type === 'binary') {
|
|
32
33
|
const left = await evaluateHavingValue(expr.left, context, group, tables)
|
|
33
|
-
const right = await evaluateHavingValue(expr.right, context, group, tables)
|
|
34
34
|
|
|
35
|
+
// Short-circuit evaluation for AND and OR
|
|
35
36
|
if (expr.op === 'AND') {
|
|
36
|
-
|
|
37
|
+
if (!left) return false
|
|
37
38
|
}
|
|
38
39
|
if (expr.op === 'OR') {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Handle NULL comparisons
|
|
43
|
-
if (left == null || right == null) {
|
|
44
|
-
if (expr.op === '=' || expr.op === '!=' || expr.op === '<>') {
|
|
45
|
-
return false
|
|
46
|
-
}
|
|
40
|
+
if (left) return true
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (expr.op === '<') return left < right
|
|
52
|
-
if (expr.op === '>') return left > right
|
|
53
|
-
if (expr.op === '<=') return left <= right
|
|
54
|
-
if (expr.op === '>=') return left >= right
|
|
55
|
-
if (expr.op === 'LIKE') {
|
|
56
|
-
const str = String(left)
|
|
57
|
-
const pattern = String(right)
|
|
58
|
-
const regexPattern = pattern
|
|
59
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
60
|
-
.replace(/%/g, '.*')
|
|
61
|
-
.replace(/_/g, '.')
|
|
62
|
-
const regex = new RegExp('^' + regexPattern + '$', 'i')
|
|
63
|
-
return regex.test(str)
|
|
64
|
-
}
|
|
43
|
+
const right = await evaluateHavingValue(expr.right, context, group, tables)
|
|
44
|
+
return Boolean(applyBinaryOp(expr.op, left, right))
|
|
65
45
|
}
|
|
66
46
|
|
|
67
47
|
if (expr.type === 'unary') {
|
package/src/execute/join.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { evaluateExpr } from './expression.js'
|
|
2
|
+
import { stringify } from './utils.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
|
|
@@ -258,8 +259,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
258
259
|
for (const rightRow of rightRows) {
|
|
259
260
|
const keyValue = await evaluateExpr({ node: keys.rightKey, row: rightRow, tables })
|
|
260
261
|
if (keyValue == null) continue // NULL keys never match
|
|
261
|
-
const keyStr =
|
|
262
|
-
|
|
262
|
+
const keyStr = stringify(keyValue)
|
|
263
263
|
let bucket = hashMap.get(keyStr)
|
|
264
264
|
if (!bucket) {
|
|
265
265
|
bucket = []
|
|
@@ -283,7 +283,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
const keyValue = await evaluateExpr({ node: keys.leftKey, row: leftRow, tables })
|
|
286
|
-
const keyStr =
|
|
286
|
+
const keyStr = stringify(keyValue)
|
|
287
287
|
|
|
288
288
|
const matchingRightRows = hashMap.get(keyStr)
|
|
289
289
|
|
package/src/execute/utils.js
CHANGED
|
@@ -1,14 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import {AsyncRow, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
|
|
2
|
+
* @import {AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Applies a binary operator to two values, handling nulls according to SQL semantics
|
|
7
|
+
*
|
|
8
|
+
* @param {BinaryOp} op
|
|
9
|
+
* @param {SqlPrimitive} a
|
|
10
|
+
* @param {SqlPrimitive} b
|
|
11
|
+
* @returns {SqlPrimitive}
|
|
12
|
+
*/
|
|
13
|
+
export function applyBinaryOp(op, a, b) {
|
|
14
|
+
// Arithmetic operators return null if either operand is null
|
|
15
|
+
if (op === '+' || op === '-' || op === '*' || op === '/' || op === '%') {
|
|
16
|
+
if (a == null || b == null) return null
|
|
17
|
+
const numA = Number(a)
|
|
18
|
+
const numB = Number(b)
|
|
19
|
+
if (op === '+') return numA + numB
|
|
20
|
+
if (op === '-') return numA - numB
|
|
21
|
+
if (op === '*') return numA * numB
|
|
22
|
+
if (op === '/') return numB === 0 ? null : numA / numB
|
|
23
|
+
if (op === '%') return numB === 0 ? null : numA % numB
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Comparison and logical operators
|
|
27
|
+
if (a == null || b == null) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
if (op === 'AND') return Boolean(a) && Boolean(b)
|
|
31
|
+
if (op === 'OR') return Boolean(a) || Boolean(b)
|
|
32
|
+
if (op === '!=' || op === '<>') return a != b
|
|
33
|
+
if (op === '=') return a == b
|
|
34
|
+
if (op === '<') return a < b
|
|
35
|
+
if (op === '<=') return a <= b
|
|
36
|
+
if (op === '>') return a > b
|
|
37
|
+
if (op === '>=') return a >= b
|
|
38
|
+
|
|
39
|
+
if (op === 'LIKE') {
|
|
40
|
+
const str = String(a)
|
|
41
|
+
const pattern = String(b)
|
|
42
|
+
const regexPattern = pattern
|
|
43
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
44
|
+
.replace(/%/g, '.*')
|
|
45
|
+
.replace(/_/g, '.')
|
|
46
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i')
|
|
47
|
+
return regex.test(str)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
5
53
|
/**
|
|
6
54
|
* Compares two values for a single ORDER BY term, handling nulls and direction
|
|
7
55
|
*
|
|
8
56
|
* @param {SqlPrimitive} a
|
|
9
57
|
* @param {SqlPrimitive} b
|
|
10
58
|
* @param {OrderByItem} term
|
|
11
|
-
* @returns {number}
|
|
59
|
+
* @returns {number}
|
|
12
60
|
*/
|
|
13
61
|
export function compareForTerm(a, b, term) {
|
|
14
62
|
const aIsNull = a == null
|
|
@@ -22,7 +70,7 @@ export function compareForTerm(a, b, term) {
|
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
// Compare non-null values
|
|
25
|
-
if (a
|
|
73
|
+
if (a == b) return 0
|
|
26
74
|
|
|
27
75
|
const primitives = ['number', 'bigint', 'boolean', 'string']
|
|
28
76
|
let cmp
|
|
@@ -88,3 +136,17 @@ export function defaultDerivedAlias(expr) {
|
|
|
88
136
|
}
|
|
89
137
|
return 'expr'
|
|
90
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {SqlPrimitive} value
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function stringify(value) {
|
|
145
|
+
if (value == null) return 'NULL'
|
|
146
|
+
return JSON.stringify(value, (_, val) => {
|
|
147
|
+
if (typeof val === 'bigint') {
|
|
148
|
+
return val <= Number.MAX_SAFE_INTEGER ? Number(val) : val.toString()
|
|
149
|
+
}
|
|
150
|
+
return val
|
|
151
|
+
})
|
|
152
|
+
}
|
package/src/parse/comparison.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isBinaryOp } from '../validation.js'
|
|
2
|
-
import {
|
|
2
|
+
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
3
3
|
import { consume, current, expect, match, peekToken } from './state.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -11,7 +11,7 @@ import { consume, current, expect, match, peekToken } from './state.js'
|
|
|
11
11
|
* @returns {ExprNode}
|
|
12
12
|
*/
|
|
13
13
|
export function parseComparison(state) {
|
|
14
|
-
const left =
|
|
14
|
+
const left = parseAdditive(state)
|
|
15
15
|
const tok = current(state)
|
|
16
16
|
|
|
17
17
|
// IS [NOT] NULL
|
|
@@ -41,7 +41,7 @@ export function parseComparison(state) {
|
|
|
41
41
|
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
42
42
|
consume(state) // NOT
|
|
43
43
|
consume(state) // LIKE
|
|
44
|
-
const right =
|
|
44
|
+
const right = parseAdditive(state)
|
|
45
45
|
return {
|
|
46
46
|
type: 'unary',
|
|
47
47
|
op: 'NOT',
|
|
@@ -57,7 +57,7 @@ export function parseComparison(state) {
|
|
|
57
57
|
|
|
58
58
|
if (tok.type === 'keyword' && tok.value === 'LIKE') {
|
|
59
59
|
consume(state)
|
|
60
|
-
const right =
|
|
60
|
+
const right = parseAdditive(state)
|
|
61
61
|
return {
|
|
62
62
|
type: 'binary',
|
|
63
63
|
op: 'LIKE',
|
|
@@ -72,9 +72,9 @@ export function parseComparison(state) {
|
|
|
72
72
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
73
73
|
consume(state) // NOT
|
|
74
74
|
consume(state) // BETWEEN
|
|
75
|
-
const lower =
|
|
75
|
+
const lower = parseAdditive(state)
|
|
76
76
|
expect(state, 'keyword', 'AND')
|
|
77
|
-
const upper =
|
|
77
|
+
const upper = parseAdditive(state)
|
|
78
78
|
// NOT BETWEEN -> expr < lower OR expr > upper
|
|
79
79
|
return {
|
|
80
80
|
type: 'binary',
|
|
@@ -87,9 +87,9 @@ export function parseComparison(state) {
|
|
|
87
87
|
|
|
88
88
|
if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
|
|
89
89
|
consume(state)
|
|
90
|
-
const lower =
|
|
90
|
+
const lower = parseAdditive(state)
|
|
91
91
|
expect(state, 'keyword', 'AND')
|
|
92
|
-
const upper =
|
|
92
|
+
const upper = parseAdditive(state)
|
|
93
93
|
// BETWEEN -> expr >= lower AND expr <= upper
|
|
94
94
|
return {
|
|
95
95
|
type: 'binary',
|
|
@@ -186,7 +186,7 @@ export function parseComparison(state) {
|
|
|
186
186
|
|
|
187
187
|
if (tok.type === 'operator' && isBinaryOp(tok.value)) {
|
|
188
188
|
consume(state)
|
|
189
|
-
const right =
|
|
189
|
+
const right = parseAdditive(state)
|
|
190
190
|
return {
|
|
191
191
|
type: 'binary',
|
|
192
192
|
op: tok.value,
|
package/src/parse/expression.js
CHANGED
|
@@ -272,6 +272,54 @@ function parseNot(state) {
|
|
|
272
272
|
return parseComparison(state)
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @param {ParserState} state
|
|
277
|
+
* @returns {ExprNode}
|
|
278
|
+
*/
|
|
279
|
+
export function parseAdditive(state) {
|
|
280
|
+
let node = parseMultiplicative(state)
|
|
281
|
+
while (true) {
|
|
282
|
+
const tok = current(state)
|
|
283
|
+
if (tok.type === 'operator' && (tok.value === '+' || tok.value === '-')) {
|
|
284
|
+
consume(state)
|
|
285
|
+
const right = parseMultiplicative(state)
|
|
286
|
+
node = {
|
|
287
|
+
type: 'binary',
|
|
288
|
+
op: tok.value,
|
|
289
|
+
left: node,
|
|
290
|
+
right,
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return node
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {ParserState} state
|
|
301
|
+
* @returns {ExprNode}
|
|
302
|
+
*/
|
|
303
|
+
function parseMultiplicative(state) {
|
|
304
|
+
let node = parsePrimary(state)
|
|
305
|
+
while (true) {
|
|
306
|
+
const tok = current(state)
|
|
307
|
+
if (tok.type === 'operator' && (tok.value === '*' || tok.value === '/' || tok.value === '%')) {
|
|
308
|
+
consume(state)
|
|
309
|
+
const right = parsePrimary(state)
|
|
310
|
+
node = {
|
|
311
|
+
type: 'binary',
|
|
312
|
+
op: tok.value,
|
|
313
|
+
left: node,
|
|
314
|
+
right,
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return node
|
|
321
|
+
}
|
|
322
|
+
|
|
275
323
|
/**
|
|
276
324
|
* Creates an ExprCursor adapter for the ParserState.
|
|
277
325
|
*
|
package/src/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface ExecuteSqlOptions {
|
|
|
27
27
|
query: string
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export type SqlPrimitive = string | number | bigint | boolean | null
|
|
30
|
+
export type SqlPrimitive = string | number | bigint | boolean | SqlPrimitive[] | Record<string, any> | null
|
|
31
31
|
|
|
32
32
|
export interface SelectStatement {
|
|
33
33
|
distinct: boolean
|
|
@@ -54,7 +54,9 @@ export interface FromSubquery {
|
|
|
54
54
|
alias: string
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export type
|
|
57
|
+
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
58
|
+
|
|
59
|
+
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
58
60
|
|
|
59
61
|
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
60
62
|
|
|
@@ -146,9 +148,20 @@ export interface StarColumn {
|
|
|
146
148
|
alias?: string
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
|
|
150
|
-
|
|
151
|
-
export type StringFunc =
|
|
151
|
+
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
|
|
152
|
+
|
|
153
|
+
export type StringFunc =
|
|
154
|
+
| 'UPPER'
|
|
155
|
+
| 'LOWER'
|
|
156
|
+
| 'CONCAT'
|
|
157
|
+
| 'LENGTH'
|
|
158
|
+
| 'SUBSTRING'
|
|
159
|
+
| 'SUBSTR'
|
|
160
|
+
| 'TRIM'
|
|
161
|
+
| 'REPLACE'
|
|
162
|
+
| 'JSON_VALUE'
|
|
163
|
+
| 'JSON_QUERY'
|
|
164
|
+
| 'JSON_OBJECT'
|
|
152
165
|
|
|
153
166
|
export interface AggregateArgStar {
|
|
154
167
|
kind: 'star'
|
package/src/validation.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, BinaryOp, StringFunc} from './types.js'
|
|
3
|
+
* @import {AggregateFunc, BinaryOp, ComparisonOp, StringFunc} from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|
|
7
7
|
export function isAggregateFunc(name) {
|
|
8
|
-
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(name)
|
|
8
|
+
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG'].includes(name)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -13,7 +13,21 @@ export function isAggregateFunc(name) {
|
|
|
13
13
|
* @returns {name is StringFunc}
|
|
14
14
|
*/
|
|
15
15
|
export function isStringFunc(name) {
|
|
16
|
-
return [
|
|
16
|
+
return [
|
|
17
|
+
'UPPER',
|
|
18
|
+
'LOWER',
|
|
19
|
+
'CONCAT',
|
|
20
|
+
'LENGTH',
|
|
21
|
+
'SUBSTRING',
|
|
22
|
+
'SUBSTR',
|
|
23
|
+
'TRIM',
|
|
24
|
+
'REPLACE',
|
|
25
|
+
'RANDOM',
|
|
26
|
+
'RAND',
|
|
27
|
+
'JSON_VALUE',
|
|
28
|
+
'JSON_QUERY',
|
|
29
|
+
'JSON_OBJECT',
|
|
30
|
+
].includes(name)
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
/**
|
|
@@ -21,7 +35,7 @@ export function isStringFunc(name) {
|
|
|
21
35
|
* @returns {op is BinaryOp}
|
|
22
36
|
*/
|
|
23
37
|
export function isBinaryOp(op) {
|
|
24
|
-
return ['=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
38
|
+
return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
// Keywords that cannot be used as implicit aliases after a column
|