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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -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, ExprNode, AsyncRow } from '../types.js'
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<number | null>} aggregated result
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
 
@@ -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, ExprNode, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
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 + ':' + JSON.stringify(v))
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(JSON.stringify(v))
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 -Number(val)
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
- const leftVal = await evaluateExpr({ node: node.left, row, tables })
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
- const leftVal = await evaluateExpr({ node: node.left, row, tables })
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
- for (let i = 0; i < args.length; i += 1) {
136
- if (args[i] == null) return null
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
  }
@@ -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
- return Boolean(left && right)
37
+ if (!left) return false
37
38
  }
38
39
  if (expr.op === 'OR') {
39
- return Boolean(left || right)
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
- if (expr.op === '=') return left === right
50
- if (expr.op === '!=' || expr.op === '<>') return left !== right
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') {
@@ -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 = JSON.stringify(keyValue)
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 = JSON.stringify(keyValue)
286
+ const keyStr = stringify(keyValue)
287
287
 
288
288
  const matchingRightRows = hashMap.get(keyStr)
289
289
 
@@ -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} comparison result
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 === b) return 0
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { isBinaryOp } from '../validation.js'
2
- import { parseExpression, parsePrimary, parseSubquery } from './expression.js'
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 = parsePrimary(state)
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 = parsePrimary(state)
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 = parsePrimary(state)
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 = parsePrimary(state)
75
+ const lower = parseAdditive(state)
76
76
  expect(state, 'keyword', 'AND')
77
- const upper = parsePrimary(state)
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 = parsePrimary(state)
90
+ const lower = parseAdditive(state)
91
91
  expect(state, 'keyword', 'AND')
92
- const upper = parsePrimary(state)
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 = parsePrimary(state)
189
+ const right = parseAdditive(state)
190
190
  return {
191
191
  type: 'binary',
192
192
  op: tok.value,
@@ -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 BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp
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 = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM' | 'REPLACE'
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 ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE', 'RANDOM', 'RAND'].includes(name)
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