squirreling 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +5 -5
- package/src/ast.d.ts +32 -15
- package/src/backend/dataSource.js +4 -3
- package/src/execute/aggregates.js +160 -19
- package/src/execute/execute.js +129 -23
- package/src/execute/join.js +20 -21
- package/src/execute/utils.js +19 -7
- package/src/expression/alias.js +3 -2
- package/src/expression/evaluate.js +87 -61
- package/src/expression/math.js +2 -0
- package/src/expression/regexp.js +11 -9
- package/src/expression/strings.js +11 -9
- package/src/index.d.ts +10 -5
- package/src/index.js +1 -1
- package/src/parse/expression.js +187 -351
- package/src/parse/functions.js +63 -51
- package/src/parse/joins.js +24 -38
- package/src/parse/parse.js +244 -200
- package/src/parse/primary.js +281 -0
- package/src/parse/state.js +11 -25
- package/src/parse/tokenize.js +77 -196
- package/src/plan/columns.js +115 -17
- package/src/plan/plan.js +121 -44
- package/src/plan/types.d.ts +11 -1
- package/src/spatial/bbox.js +3 -3
- package/src/spatial/geometry.d.ts +1 -1
- package/src/spatial/index.d.ts +6 -0
- package/src/spatial/index.js +3 -0
- package/src/spatial/spatial.js +19 -53
- package/src/types.d.ts +17 -5
- package/src/validation/executionErrors.js +20 -12
- package/src/validation/functions.js +28 -53
- package/src/validation/keywords.js +35 -0
- package/src/validation/parseErrors.js +101 -82
- package/src/validation/planErrors.js +41 -33
- package/src/parse/comparison.js +0 -233
- package/src/validation/expressionErrors.js +0 -57
package/src/execute/join.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
|
-
import {
|
|
2
|
+
import { keyify } from './utils.js'
|
|
3
3
|
import { executePlan } from './execute.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -26,13 +26,12 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
26
26
|
rightRows.push(row)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
29
|
+
const rightPrefixedCols = rightRows.length ? prefixColumns(rightRows[0].columns, rightTable) : []
|
|
31
30
|
|
|
32
|
-
/** @type {string[] |
|
|
33
|
-
let leftPrefixedCols =
|
|
34
|
-
/** @type {Set<AsyncRow> |
|
|
35
|
-
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() :
|
|
31
|
+
/** @type {string[] | undefined} */
|
|
32
|
+
let leftPrefixedCols = undefined
|
|
33
|
+
/** @type {Set<AsyncRow> | undefined} */
|
|
34
|
+
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
36
35
|
|
|
37
36
|
for await (const leftRow of executePlan({ plan: plan.left, context })) {
|
|
38
37
|
if (context.signal?.aborted) break
|
|
@@ -53,7 +52,7 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
53
52
|
|
|
54
53
|
if (matches) {
|
|
55
54
|
hasMatch = true
|
|
56
|
-
|
|
55
|
+
matchedRightRows?.add(rightRow)
|
|
57
56
|
yield tempMerged
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -68,7 +67,7 @@ export async function* executeNestedLoopJoin(plan, context) {
|
|
|
68
67
|
if (matchedRightRows) {
|
|
69
68
|
for (const rightRow of rightRows) {
|
|
70
69
|
if (!matchedRightRows.has(rightRow)) {
|
|
71
|
-
const nullLeft = createNullRow(leftPrefixedCols
|
|
70
|
+
const nullLeft = createNullRow(leftPrefixedCols ?? [])
|
|
72
71
|
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
73
72
|
}
|
|
74
73
|
}
|
|
@@ -135,7 +134,7 @@ export async function* executeHashJoin(plan, context) {
|
|
|
135
134
|
rightRows.push(row)
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
/** @type {Map<
|
|
137
|
+
/** @type {Map<any, AsyncRow[]>} */
|
|
139
138
|
const hashMap = new Map()
|
|
140
139
|
for (const rightRow of rightRows) {
|
|
141
140
|
const keyValue = await evaluateExpr({
|
|
@@ -144,11 +143,11 @@ export async function* executeHashJoin(plan, context) {
|
|
|
144
143
|
context,
|
|
145
144
|
})
|
|
146
145
|
if (keyValue == null) continue
|
|
147
|
-
const
|
|
148
|
-
let bucket = hashMap.get(
|
|
146
|
+
const key = keyify(keyValue)
|
|
147
|
+
let bucket = hashMap.get(key)
|
|
149
148
|
if (!bucket) {
|
|
150
149
|
bucket = []
|
|
151
|
-
hashMap.set(
|
|
150
|
+
hashMap.set(key, bucket)
|
|
152
151
|
}
|
|
153
152
|
bucket.push(rightRow)
|
|
154
153
|
}
|
|
@@ -157,10 +156,10 @@ export async function* executeHashJoin(plan, context) {
|
|
|
157
156
|
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
158
157
|
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
159
158
|
|
|
160
|
-
/** @type {string[] |
|
|
161
|
-
let leftPrefixedCols
|
|
162
|
-
/** @type {Set<AsyncRow> |
|
|
163
|
-
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() :
|
|
159
|
+
/** @type {string[] | undefined} */
|
|
160
|
+
let leftPrefixedCols
|
|
161
|
+
/** @type {Set<AsyncRow> | undefined} */
|
|
162
|
+
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : undefined
|
|
164
163
|
|
|
165
164
|
// Probe phase: stream left rows
|
|
166
165
|
for await (const leftRow of executePlan({ plan: plan.left, context })) {
|
|
@@ -175,12 +174,12 @@ export async function* executeHashJoin(plan, context) {
|
|
|
175
174
|
row: leftRow,
|
|
176
175
|
context,
|
|
177
176
|
})
|
|
178
|
-
const
|
|
179
|
-
const matchingRightRows = hashMap.get(
|
|
177
|
+
const key = keyify(keyValue)
|
|
178
|
+
const matchingRightRows = hashMap.get(key)
|
|
180
179
|
|
|
181
180
|
if (matchingRightRows?.length) {
|
|
182
181
|
for (const rightRow of matchingRightRows) {
|
|
183
|
-
|
|
182
|
+
matchedRightRows?.add(rightRow)
|
|
184
183
|
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
185
184
|
}
|
|
186
185
|
} else if (plan.joinType === 'LEFT' || plan.joinType === 'FULL') {
|
|
@@ -193,7 +192,7 @@ export async function* executeHashJoin(plan, context) {
|
|
|
193
192
|
if (matchedRightRows) {
|
|
194
193
|
for (const rightRow of rightRows) {
|
|
195
194
|
if (!matchedRightRows.has(rightRow)) {
|
|
196
|
-
const nullLeft = createNullRow(leftPrefixedCols
|
|
195
|
+
const nullLeft = createNullRow(leftPrefixedCols ?? [])
|
|
197
196
|
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
198
197
|
}
|
|
199
198
|
}
|
package/src/execute/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import {
|
|
2
|
+
* @import { AsyncRow, OrderByItem, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -76,14 +76,26 @@ export function stringify(value) {
|
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Returns a value suitable for use as a Set/Map key.
|
|
81
|
+
* Primitives are returned as-is (fast path), objects are stringified.
|
|
82
|
+
*
|
|
83
|
+
* @param {SqlPrimitive[]} values
|
|
84
|
+
* @returns {string | number | bigint | boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function keyify(...values) {
|
|
87
|
+
if (values.length === 1 && typeof values[0] !== 'object') return values[0]
|
|
88
|
+
// Strings must be stringified to avoid collisions when joined
|
|
89
|
+
return values.map(v => typeof v === 'object' ? stringify(v) : v).join('|')
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
/**
|
|
80
93
|
* Creates a stable string key for a row to enable deduplication
|
|
81
94
|
*
|
|
82
|
-
* @param {
|
|
83
|
-
* @returns {Promise<string>}
|
|
95
|
+
* @param {AsyncRow} row
|
|
96
|
+
* @returns {Promise<string | number | bigint | boolean>}
|
|
84
97
|
*/
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return keys.map((k, i) => k + ':' + stringify(values[i])).join('|')
|
|
98
|
+
export function stableRowKey(row) {
|
|
99
|
+
return Promise.all(row.columns.map(k => row.cells[k]()))
|
|
100
|
+
.then(values => keyify(...values))
|
|
89
101
|
}
|
package/src/expression/alias.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
export function derivedAlias(expr) {
|
|
12
12
|
if (expr.type === 'identifier') {
|
|
13
13
|
// For qualified names like 'users.name', use just the column part as alias
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const dotIndex = expr.name.indexOf('.')
|
|
15
|
+
if (dotIndex >= 0) {
|
|
16
|
+
return expr.name.substring(dotIndex + 1)
|
|
16
17
|
}
|
|
17
18
|
return expr.name
|
|
18
19
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { stringify } from '../execute/utils.js'
|
|
3
|
-
import {
|
|
4
|
-
import { aggregateError, argValueError, castError } from '../validation/expressionErrors.js'
|
|
1
|
+
import { executeStatement } from '../execute/execute.js'
|
|
2
|
+
import { keyify, stringify } from '../execute/utils.js'
|
|
3
|
+
import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
|
|
5
4
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
6
|
+
import { ColumnNotFoundError } from '../validation/planErrors.js'
|
|
8
7
|
import { derivedAlias } from './alias.js'
|
|
9
8
|
import { applyBinaryOp } from './binary.js'
|
|
10
9
|
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
|
@@ -46,18 +45,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
// Unknown identifier
|
|
49
|
-
throw
|
|
48
|
+
throw new ColumnNotFoundError({
|
|
50
49
|
columnName: node.name,
|
|
51
50
|
availableColumns: row.columns,
|
|
52
|
-
positionStart: node.positionStart,
|
|
53
|
-
positionEnd: node.positionEnd,
|
|
54
51
|
rowIndex,
|
|
52
|
+
...node,
|
|
55
53
|
})
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
// Scalar subquery - returns a single value
|
|
59
57
|
if (node.type === 'subquery') {
|
|
60
|
-
const gen =
|
|
58
|
+
const gen = executeStatement({ query: node.subquery, context })
|
|
61
59
|
const { value } = await gen.next() // Start the generator
|
|
62
60
|
gen.return(undefined) // Stop further execution
|
|
63
61
|
if (!value) return null
|
|
@@ -111,15 +109,19 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
111
109
|
if (row.columns.includes(alias)) {
|
|
112
110
|
return row.cells[alias]()
|
|
113
111
|
} else {
|
|
114
|
-
throw
|
|
112
|
+
throw new ExecutionError({
|
|
113
|
+
message: `Aggregate function ${funcName} is not available in this context`,
|
|
114
|
+
...node,
|
|
115
|
+
})
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
// Apply FILTER clause if present
|
|
119
120
|
let filteredRows = rows
|
|
120
121
|
if (node.filter) {
|
|
122
|
+
const filterNode = node.filter
|
|
121
123
|
const passes = await Promise.all(rows.map(row =>
|
|
122
|
-
evaluateExpr({ node:
|
|
124
|
+
evaluateExpr({ node: filterNode, row, context })
|
|
123
125
|
))
|
|
124
126
|
filteredRows = rows.filter((_, i) => passes[i])
|
|
125
127
|
}
|
|
@@ -137,7 +139,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
137
139
|
if (node.distinct) {
|
|
138
140
|
const seen = new Set()
|
|
139
141
|
for (const v of values) {
|
|
140
|
-
if (v != null) seen.add(v)
|
|
142
|
+
if (v != null) seen.add(keyify(v))
|
|
141
143
|
}
|
|
142
144
|
return seen.size
|
|
143
145
|
}
|
|
@@ -154,23 +156,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
154
156
|
))
|
|
155
157
|
let sum = 0
|
|
156
158
|
let count = 0
|
|
157
|
-
/** @type {
|
|
159
|
+
/** @type {SqlPrimitive} */
|
|
158
160
|
let min = null
|
|
159
|
-
/** @type {
|
|
161
|
+
/** @type {SqlPrimitive} */
|
|
160
162
|
let max = null
|
|
161
163
|
|
|
162
164
|
for (const raw of rawValues) {
|
|
163
165
|
if (raw == null) continue
|
|
166
|
+
if (min === null || raw < min) min = raw
|
|
167
|
+
if (max === null || raw > max) max = raw
|
|
164
168
|
const num = Number(raw)
|
|
165
169
|
if (!Number.isFinite(num)) continue
|
|
166
|
-
|
|
167
|
-
if (count === 0) {
|
|
168
|
-
min = num
|
|
169
|
-
max = num
|
|
170
|
-
} else {
|
|
171
|
-
if (min == null || num < min) min = num
|
|
172
|
-
if (max == null || num > max) max = num
|
|
173
|
-
}
|
|
174
170
|
sum += num
|
|
175
171
|
count++
|
|
176
172
|
}
|
|
@@ -185,6 +181,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
185
181
|
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
186
182
|
evaluateExpr({ node: argNode, row, context })
|
|
187
183
|
))
|
|
184
|
+
let sum = 0
|
|
188
185
|
/** @type {number[]} */
|
|
189
186
|
const values = []
|
|
190
187
|
for (const raw of rawValues) {
|
|
@@ -192,37 +189,77 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
192
189
|
const num = Number(raw)
|
|
193
190
|
if (!Number.isFinite(num)) continue
|
|
194
191
|
values.push(num)
|
|
192
|
+
sum += num
|
|
195
193
|
}
|
|
196
194
|
const n = values.length
|
|
197
195
|
if (n === 0) return null
|
|
198
196
|
if (funcName === 'STDDEV_SAMP' && n === 1) return null
|
|
199
197
|
|
|
200
|
-
const mean =
|
|
198
|
+
const mean = sum / n
|
|
201
199
|
const squaredDiffs = values.reduce((acc, val) => acc + (val - mean) ** 2, 0)
|
|
202
200
|
const divisor = funcName === 'STDDEV_SAMP' ? n - 1 : n
|
|
203
201
|
return Math.sqrt(squaredDiffs / divisor)
|
|
204
202
|
}
|
|
205
203
|
|
|
206
|
-
if (funcName === '
|
|
207
|
-
|
|
204
|
+
if (funcName === 'MEDIAN' || funcName === 'PERCENTILE_CONT' || funcName === 'APPROX_QUANTILE') {
|
|
205
|
+
let fraction
|
|
206
|
+
let valueNode
|
|
207
|
+
if (funcName === 'MEDIAN') {
|
|
208
|
+
fraction = 0.5
|
|
209
|
+
valueNode = argNode
|
|
210
|
+
} else if (funcName === 'PERCENTILE_CONT') {
|
|
211
|
+
fraction = Number(await evaluateExpr({ node: node.args[0], row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
|
|
212
|
+
valueNode = node.args[1]
|
|
213
|
+
} else {
|
|
214
|
+
// APPROX_QUANTILE: (expression, fraction)
|
|
215
|
+
fraction = Number(await evaluateExpr({ node: node.args[1], row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
|
|
216
|
+
valueNode = argNode
|
|
217
|
+
}
|
|
218
|
+
if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1) {
|
|
219
|
+
throw new ExecutionError({
|
|
220
|
+
message: `${funcName}: fraction must be between 0 and 1, got ${fraction}`,
|
|
221
|
+
...node,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
225
|
+
evaluateExpr({ node: valueNode, row, context })
|
|
226
|
+
))
|
|
227
|
+
/** @type {number[]} */
|
|
208
228
|
const values = []
|
|
229
|
+
for (const raw of rawValues) {
|
|
230
|
+
if (raw == null) continue
|
|
231
|
+
const num = Number(raw)
|
|
232
|
+
if (!Number.isFinite(num)) continue
|
|
233
|
+
values.push(num)
|
|
234
|
+
}
|
|
235
|
+
if (values.length === 0) return null
|
|
236
|
+
values.sort((a, b) => a - b)
|
|
237
|
+
const pos = fraction * (values.length - 1)
|
|
238
|
+
const lower = Math.floor(pos)
|
|
239
|
+
const upper = Math.ceil(pos)
|
|
240
|
+
if (lower === upper) return values[lower]
|
|
241
|
+
return values[lower] + (values[upper] - values[lower]) * (pos - lower)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (funcName === 'JSON_ARRAYAGG') {
|
|
209
245
|
if (node.distinct) {
|
|
246
|
+
/** @type {SqlPrimitive[]} */
|
|
247
|
+
const values = []
|
|
210
248
|
const seen = new Set()
|
|
211
249
|
for (const row of filteredRows) {
|
|
212
250
|
const v = await evaluateExpr({ node: argNode, row, context })
|
|
213
|
-
const key =
|
|
251
|
+
const key = keyify(v)
|
|
214
252
|
if (!seen.has(key)) {
|
|
215
253
|
seen.add(key)
|
|
216
254
|
values.push(v)
|
|
217
255
|
}
|
|
218
256
|
}
|
|
257
|
+
return values
|
|
219
258
|
} else {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
259
|
+
return await Promise.all(filteredRows.map(row =>
|
|
260
|
+
evaluateExpr({ node: argNode, row, context })
|
|
261
|
+
))
|
|
224
262
|
}
|
|
225
|
-
return values
|
|
226
263
|
}
|
|
227
264
|
}
|
|
228
265
|
|
|
@@ -258,9 +295,9 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
258
295
|
|
|
259
296
|
if (funcName === 'NULLIF') {
|
|
260
297
|
// NULLIF(a, b) returns null if a = b, otherwise returns a
|
|
298
|
+
const val2 = evaluateExpr({ node: node.args[1], row, rowIndex, rows, context })
|
|
261
299
|
const val1 = await evaluateExpr({ node: node.args[0], row, rowIndex, rows, context })
|
|
262
|
-
|
|
263
|
-
return val1 == val2 ? null : val1
|
|
300
|
+
return val1 == await val2 ? null : val1
|
|
264
301
|
}
|
|
265
302
|
|
|
266
303
|
if (funcName === 'DATE_TRUNC') {
|
|
@@ -285,7 +322,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
285
322
|
|
|
286
323
|
if (funcName === 'JSON_OBJECT') {
|
|
287
324
|
if (args.length % 2 !== 0) {
|
|
288
|
-
throw
|
|
325
|
+
throw new ArgValueError({
|
|
289
326
|
...node,
|
|
290
327
|
message: 'requires an even number of arguments (key-value pairs)',
|
|
291
328
|
rowIndex,
|
|
@@ -297,7 +334,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
297
334
|
const key = args[i]
|
|
298
335
|
const value = args[i + 1]
|
|
299
336
|
if (key == null) {
|
|
300
|
-
throw
|
|
337
|
+
throw new ArgValueError({
|
|
301
338
|
...node,
|
|
302
339
|
message: 'key cannot be null',
|
|
303
340
|
hint: 'All keys must be non-null values.',
|
|
@@ -335,7 +372,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
335
372
|
})
|
|
336
373
|
}
|
|
337
374
|
|
|
338
|
-
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
|
|
375
|
+
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT') {
|
|
339
376
|
let jsonArg = args[0]
|
|
340
377
|
const pathArg = args[1]
|
|
341
378
|
if (jsonArg == null || pathArg == null) return null
|
|
@@ -345,7 +382,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
345
382
|
try {
|
|
346
383
|
jsonArg = JSON.parse(jsonArg)
|
|
347
384
|
} catch {
|
|
348
|
-
throw
|
|
385
|
+
throw new ArgValueError({
|
|
349
386
|
...node,
|
|
350
387
|
message: 'invalid JSON string',
|
|
351
388
|
hint: 'First argument must be valid JSON.',
|
|
@@ -354,7 +391,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
354
391
|
}
|
|
355
392
|
}
|
|
356
393
|
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
357
|
-
throw
|
|
394
|
+
throw new ArgValueError({
|
|
358
395
|
...node,
|
|
359
396
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
360
397
|
rowIndex,
|
|
@@ -396,7 +433,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
396
433
|
}
|
|
397
434
|
}
|
|
398
435
|
|
|
399
|
-
throw
|
|
436
|
+
throw new UnknownFunctionError(node)
|
|
400
437
|
}
|
|
401
438
|
|
|
402
439
|
if (node.type === 'cast') {
|
|
@@ -409,7 +446,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
409
446
|
}
|
|
410
447
|
// Can only cast primitives to other primitive types
|
|
411
448
|
if (typeof val === 'object') {
|
|
412
|
-
throw
|
|
449
|
+
throw new ExecutionError({ message: `Cannot CAST object to ${toType}`, rowIndex, ...node })
|
|
413
450
|
}
|
|
414
451
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
415
452
|
const num = Number(val)
|
|
@@ -427,7 +464,6 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
427
464
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
428
465
|
return Boolean(val)
|
|
429
466
|
}
|
|
430
|
-
throw castError({ ...node, rowIndex })
|
|
431
467
|
}
|
|
432
468
|
|
|
433
469
|
// IN and NOT IN with value lists
|
|
@@ -442,7 +478,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
442
478
|
// IN with subqueries
|
|
443
479
|
if (node.type === 'in') {
|
|
444
480
|
const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
445
|
-
const results =
|
|
481
|
+
const results = executeStatement({ query: node.subquery, context })
|
|
446
482
|
for await (const resRow of results) {
|
|
447
483
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
448
484
|
if (exprVal == value) return true
|
|
@@ -452,11 +488,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
452
488
|
|
|
453
489
|
// EXISTS and NOT EXISTS with subqueries
|
|
454
490
|
if (node.type === 'exists') {
|
|
455
|
-
const results = await
|
|
491
|
+
const results = await executeStatement({ query: node.subquery, context }).next()
|
|
456
492
|
return results.done === false
|
|
457
493
|
}
|
|
458
494
|
if (node.type === 'not exists') {
|
|
459
|
-
const results = await
|
|
495
|
+
const results = await executeStatement({ query: node.subquery, context }).next()
|
|
460
496
|
return results.done === true
|
|
461
497
|
}
|
|
462
498
|
|
|
@@ -467,17 +503,9 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
467
503
|
|
|
468
504
|
// Iterate through WHEN clauses
|
|
469
505
|
for (const whenClause of node.whenClauses) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
|
|
474
|
-
conditionResult = caseValue == whenValue
|
|
475
|
-
} else {
|
|
476
|
-
// Searched CASE: evaluate condition as boolean
|
|
477
|
-
conditionResult = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (conditionResult) {
|
|
506
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
|
|
507
|
+
// compare caseValue with condition or evaluate as boolean
|
|
508
|
+
if (caseValue !== undefined ? caseValue == whenValue : whenValue) {
|
|
481
509
|
return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
|
|
482
510
|
}
|
|
483
511
|
}
|
|
@@ -492,12 +520,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
492
520
|
// INTERVAL expressions should only appear as part of binary +/- operations
|
|
493
521
|
// which are handled above. A standalone interval is an error.
|
|
494
522
|
if (node.type === 'interval') {
|
|
495
|
-
throw
|
|
496
|
-
|
|
497
|
-
validContext: 'date arithmetic (+ or -)',
|
|
498
|
-
positionStart: node.positionStart,
|
|
499
|
-
positionEnd: node.positionEnd,
|
|
523
|
+
throw new ExecutionError({
|
|
524
|
+
message: 'INTERVAL can only be used with date arithmetic (+ or -)',
|
|
500
525
|
rowIndex,
|
|
526
|
+
...node,
|
|
501
527
|
})
|
|
502
528
|
}
|
|
503
529
|
|
package/src/expression/math.js
CHANGED
package/src/expression/regexp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArgValueError } from '../validation/executionErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { FunctionNode, RegExpFunction, SqlPrimitive } from '../types.js'
|
|
@@ -11,11 +11,11 @@ import { argValueError } from '../validation/expressionErrors.js'
|
|
|
11
11
|
* @param {RegExpFunction} options.funcName
|
|
12
12
|
* @param {FunctionNode} options.node
|
|
13
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
14
|
-
* @param {number} options.rowIndex - Row index for error reporting
|
|
14
|
+
* @param {number} [options.rowIndex] - Row index for error reporting
|
|
15
15
|
* @returns {SqlPrimitive}
|
|
16
16
|
*/
|
|
17
17
|
export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
18
|
-
if (funcName === 'REGEXP_SUBSTR') {
|
|
18
|
+
if (funcName === 'REGEXP_SUBSTR' || funcName === 'REGEXP_EXTRACT') {
|
|
19
19
|
const str = args[0]
|
|
20
20
|
const pattern = args[1]
|
|
21
21
|
if (str == null || pattern == null) return null
|
|
@@ -27,7 +27,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
27
27
|
if (args.length >= 3 && args[2] != null) {
|
|
28
28
|
position = Number(args[2])
|
|
29
29
|
if (!Number.isInteger(position) || position < 1) {
|
|
30
|
-
throw
|
|
30
|
+
throw new ArgValueError({
|
|
31
31
|
...node,
|
|
32
32
|
message: `position must be a positive integer, got ${args[2]}`,
|
|
33
33
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -41,7 +41,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
41
41
|
if (args.length >= 4 && args[3] != null) {
|
|
42
42
|
occurrence = Number(args[3])
|
|
43
43
|
if (!Number.isInteger(occurrence) || occurrence < 1) {
|
|
44
|
-
throw
|
|
44
|
+
throw new ArgValueError({
|
|
45
45
|
...node,
|
|
46
46
|
message: `occurrence must be a positive integer, got ${args[3]}`,
|
|
47
47
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -55,7 +55,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
55
55
|
try {
|
|
56
56
|
regex = new RegExp(patternStr, 'g')
|
|
57
57
|
} catch (/** @type {any} */ error) {
|
|
58
|
-
throw
|
|
58
|
+
throw new ArgValueError({
|
|
59
59
|
...node,
|
|
60
60
|
message: `invalid regex pattern: ${error.message}`,
|
|
61
61
|
rowIndex,
|
|
@@ -92,7 +92,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
92
92
|
if (args.length >= 4 && args[3] != null) {
|
|
93
93
|
position = Number(args[3])
|
|
94
94
|
if (!Number.isInteger(position) || position < 1) {
|
|
95
|
-
throw
|
|
95
|
+
throw new ArgValueError({
|
|
96
96
|
...node,
|
|
97
97
|
message: `position must be a positive integer, got ${args[3]}`,
|
|
98
98
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -106,7 +106,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
106
106
|
if (args.length >= 5 && args[4] != null) {
|
|
107
107
|
occurrence = Number(args[4])
|
|
108
108
|
if (!Number.isInteger(occurrence) || occurrence < 0) {
|
|
109
|
-
throw
|
|
109
|
+
throw new ArgValueError({
|
|
110
110
|
...node,
|
|
111
111
|
message: `occurrence must be a non-negative integer, got ${args[4]}`,
|
|
112
112
|
hint: 'Use 0 to replace all occurrences.',
|
|
@@ -120,7 +120,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
120
120
|
try {
|
|
121
121
|
regex = new RegExp(patternStr, 'g')
|
|
122
122
|
} catch (/** @type {any} */ error) {
|
|
123
|
-
throw
|
|
123
|
+
throw new ArgValueError({
|
|
124
124
|
...node,
|
|
125
125
|
message: `invalid regex pattern: ${error.message}`,
|
|
126
126
|
rowIndex,
|
|
@@ -144,4 +144,6 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
144
144
|
})
|
|
145
145
|
return prefix + result
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`Unsupported regexp function: ${funcName}`)
|
|
147
149
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ArgValueError } from '../validation/executionErrors.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @import { FunctionNode, SqlPrimitive, StringFunc } from '../types.js'
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
import { argValueError } from '../validation/expressionErrors.js'
|
|
6
|
-
|
|
7
7
|
/**
|
|
8
8
|
* Evaluate a string function
|
|
9
9
|
*
|
|
@@ -11,7 +11,7 @@ import { argValueError } from '../validation/expressionErrors.js'
|
|
|
11
11
|
* @param {StringFunc} options.funcName
|
|
12
12
|
* @param {FunctionNode} options.node
|
|
13
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
14
|
-
* @param {number} options.rowIndex - Row index for error reporting
|
|
14
|
+
* @param {number} [options.rowIndex] - Row index for error reporting
|
|
15
15
|
* @returns {SqlPrimitive}
|
|
16
16
|
*/
|
|
17
17
|
export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
@@ -19,7 +19,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
19
19
|
// Returns NULL if any argument is NULL
|
|
20
20
|
if (args.some(a => a == null)) return null
|
|
21
21
|
if (args.some(a => typeof a === 'object')) {
|
|
22
|
-
throw
|
|
22
|
+
throw new ArgValueError({
|
|
23
23
|
...node,
|
|
24
24
|
message: 'does not support object arguments',
|
|
25
25
|
hint: 'Use CAST to convert objects to strings first.',
|
|
@@ -49,7 +49,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
49
49
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
50
50
|
const start = Number(args[1])
|
|
51
51
|
if (!Number.isInteger(start) || start < 1) {
|
|
52
|
-
throw
|
|
52
|
+
throw new ArgValueError({
|
|
53
53
|
...node,
|
|
54
54
|
message: `start position must be a positive integer, got ${args[1]}`,
|
|
55
55
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -61,7 +61,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
61
61
|
if (args.length === 3) {
|
|
62
62
|
const len = Number(args[2])
|
|
63
63
|
if (!Number.isInteger(len) || len < 0) {
|
|
64
|
-
throw
|
|
64
|
+
throw new ArgValueError({
|
|
65
65
|
...node,
|
|
66
66
|
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
67
67
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -90,7 +90,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
90
90
|
if (n == null) return null
|
|
91
91
|
const len = Number(n)
|
|
92
92
|
if (!Number.isInteger(len) || len < 0) {
|
|
93
|
-
throw
|
|
93
|
+
throw new ArgValueError({
|
|
94
94
|
...node,
|
|
95
95
|
message: `length must be a non-negative integer, got ${n}`,
|
|
96
96
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -105,7 +105,7 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
105
105
|
if (n == null) return null
|
|
106
106
|
const len = Number(n)
|
|
107
107
|
if (!Number.isInteger(len) || len < 0) {
|
|
108
|
-
throw
|
|
108
|
+
throw new ArgValueError({
|
|
109
109
|
...node,
|
|
110
110
|
message: `length must be a non-negative integer, got ${n}`,
|
|
111
111
|
hint: 'SQL uses 1-based indexing.',
|
|
@@ -116,10 +116,12 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
116
116
|
return str.substring(str.length - len)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (funcName === 'INSTR') {
|
|
119
|
+
if (funcName === 'INSTR' || funcName === 'POSITION' || funcName === 'STRPOS') {
|
|
120
120
|
const search = args[1]
|
|
121
121
|
if (search == null) return null
|
|
122
122
|
// INSTR returns 1-based position, 0 if not found
|
|
123
123
|
return str.indexOf(String(search)) + 1
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
throw new Error(`Unsupported string function: ${funcName}`)
|
|
125
127
|
}
|