squirreling 0.7.9 → 0.8.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 +46 -0
- package/package.json +6 -6
- package/src/backend/dataSource.js +52 -47
- package/src/execute/aggregates.js +150 -0
- package/src/execute/columns.js +0 -39
- package/src/execute/execute.js +158 -415
- package/src/execute/join.js +179 -333
- package/src/execute/sort.js +99 -0
- package/src/execute/utils.js +18 -49
- package/src/executionErrors.js +10 -10
- package/src/expression/binary.js +51 -0
- package/src/{execute → expression}/date.js +18 -18
- package/src/{execute/expression.js → expression/evaluate.js} +61 -62
- package/src/{execute → expression}/math.js +46 -81
- package/src/{execute → expression}/regexp.js +7 -7
- package/src/{execute → expression}/strings.js +33 -45
- package/src/index.d.ts +15 -1
- package/src/parse/expression.js +42 -50
- package/src/parse/joins.js +10 -11
- package/src/parse/parse.js +14 -3
- package/src/parse/state.js +2 -1
- package/src/parse/types.d.ts +30 -0
- package/src/plan/plan.js +234 -0
- package/src/plan/types.d.ts +101 -0
- package/src/types.d.ts +19 -39
- package/src/validation.js +66 -2
- package/src/validationErrors.js +9 -7
- package/src/execute/having.js +0 -202
- package/src/execute/tableSource.js +0 -63
package/src/execute/utils.js
CHANGED
|
@@ -1,55 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import {
|
|
2
|
+
* @import { AsyncCells, AsyncRow, 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
|
-
|
|
53
5
|
/**
|
|
54
6
|
* Compares two values for a single ORDER BY term, handling nulls and direction
|
|
55
7
|
*
|
|
@@ -157,3 +109,20 @@ export function stringify(value) {
|
|
|
157
109
|
return val
|
|
158
110
|
})
|
|
159
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a stable string key for a row to enable deduplication
|
|
115
|
+
*
|
|
116
|
+
* @param {AsyncCells} cells
|
|
117
|
+
* @returns {Promise<string>}
|
|
118
|
+
*/
|
|
119
|
+
export async function stableRowKey(cells) {
|
|
120
|
+
const keys = Object.keys(cells).sort()
|
|
121
|
+
/** @type {string[]} */
|
|
122
|
+
const parts = []
|
|
123
|
+
for (const k of keys) {
|
|
124
|
+
const v = await cells[k]()
|
|
125
|
+
parts.push(k + ':' + stringify(v))
|
|
126
|
+
}
|
|
127
|
+
return parts.join('|')
|
|
128
|
+
}
|
package/src/executionErrors.js
CHANGED
|
@@ -11,15 +11,15 @@ export class ExecutionError extends Error {
|
|
|
11
11
|
* @param {string} options.message - Human-readable error message
|
|
12
12
|
* @param {number} options.positionStart - Start position (0-based character offset)
|
|
13
13
|
* @param {number} options.positionEnd - End position (exclusive, 0-based character offset)
|
|
14
|
-
* @param {number} [options.
|
|
14
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
15
15
|
*/
|
|
16
|
-
constructor({ message, positionStart, positionEnd,
|
|
17
|
-
const rowSuffix =
|
|
16
|
+
constructor({ message, positionStart, positionEnd, rowIndex }) {
|
|
17
|
+
const rowSuffix = rowIndex != null ? ` (row ${rowIndex})` : ''
|
|
18
18
|
super(message + rowSuffix)
|
|
19
19
|
this.name = 'ExecutionError'
|
|
20
20
|
this.positionStart = positionStart
|
|
21
21
|
this.positionEnd = positionEnd
|
|
22
|
-
this.
|
|
22
|
+
this.rowIndex = rowIndex
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -40,11 +40,11 @@ export function tableNotFoundError({ tableName }) {
|
|
|
40
40
|
* @param {string} options.validContext - Where it can be used
|
|
41
41
|
* @param {number} options.positionStart - Start position in query
|
|
42
42
|
* @param {number} options.positionEnd - End position in query
|
|
43
|
-
* @param {number} [options.
|
|
43
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
44
44
|
* @returns {ExecutionError}
|
|
45
45
|
*/
|
|
46
|
-
export function invalidContextError({ item, validContext, positionStart, positionEnd,
|
|
47
|
-
return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd,
|
|
46
|
+
export function invalidContextError({ item, validContext, positionStart, positionEnd, rowIndex }) {
|
|
47
|
+
return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowIndex })
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -64,10 +64,10 @@ export function unsupportedOperationError({ operation, hint }) {
|
|
|
64
64
|
* @param {string[]} options.availableColumns - List of available column names
|
|
65
65
|
* @param {number} options.positionStart - Start position in query
|
|
66
66
|
* @param {number} options.positionEnd - End position in query
|
|
67
|
-
* @param {number} [options.
|
|
67
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
68
68
|
* @returns {ExecutionError}
|
|
69
69
|
*/
|
|
70
|
-
export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd,
|
|
70
|
+
export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
71
71
|
const available = availableColumns.length > 0
|
|
72
72
|
? `. Available columns: ${availableColumns.join(', ')}`
|
|
73
73
|
: ''
|
|
@@ -75,6 +75,6 @@ export function columnNotFoundError({ columnName, availableColumns, positionStar
|
|
|
75
75
|
message: `Column "${columnName}" not found${available}`,
|
|
76
76
|
positionStart,
|
|
77
77
|
positionEnd,
|
|
78
|
-
|
|
78
|
+
rowIndex,
|
|
79
79
|
})
|
|
80
80
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BinaryOp, SqlPrimitive } from '../types.js'
|
|
3
|
+
*/
|
|
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
|
+
}
|
|
@@ -2,29 +2,13 @@
|
|
|
2
2
|
* @import { SqlPrimitive, IntervalUnit } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @param {SqlPrimitive} val
|
|
7
|
-
* @returns {Date | null}
|
|
8
|
-
*/
|
|
9
|
-
function toDate(val) {
|
|
10
|
-
if (val instanceof Date) return val
|
|
11
|
-
const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
|
|
12
|
-
if (typeof val === 'string' && dateOrTime.test(val)) {
|
|
13
|
-
const date = new Date(val)
|
|
14
|
-
if (!isNaN(date.getTime())) {
|
|
15
|
-
return date
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
|
|
21
5
|
/**
|
|
22
6
|
* Apply an interval to a date
|
|
23
7
|
* @param {SqlPrimitive} dateVal
|
|
24
8
|
* @param {number} value
|
|
25
9
|
* @param {IntervalUnit} unit
|
|
26
10
|
* @param {'+' | '-'} op
|
|
27
|
-
* @returns {string | null}
|
|
11
|
+
* @returns {Date | string | null}
|
|
28
12
|
*/
|
|
29
13
|
export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
30
14
|
const date = toDate(dateVal)
|
|
@@ -48,10 +32,26 @@ export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
|
48
32
|
}
|
|
49
33
|
|
|
50
34
|
// Return in same format as input
|
|
51
|
-
if (dateVal instanceof Date) return date
|
|
35
|
+
if (dateVal instanceof Date) return date
|
|
52
36
|
if (String(dateVal).includes('T')) {
|
|
53
37
|
return date.toISOString()
|
|
54
38
|
} else {
|
|
55
39
|
return date.toISOString().split('T')[0]
|
|
56
40
|
}
|
|
57
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {SqlPrimitive} val
|
|
45
|
+
* @returns {Date | null}
|
|
46
|
+
*/
|
|
47
|
+
function toDate(val) {
|
|
48
|
+
if (val instanceof Date) return val
|
|
49
|
+
const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
|
|
50
|
+
if (typeof val === 'string' && dateOrTime.test(val)) {
|
|
51
|
+
const date = new Date(val)
|
|
52
|
+
if (!isNaN(date.getTime())) {
|
|
53
|
+
return date
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { executeSelect } from '../execute/execute.js'
|
|
2
|
+
import { stringify } from '../execute/utils.js'
|
|
2
3
|
import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
|
|
4
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
3
5
|
import { aggregateError, argValueError, castError } from '../validationErrors.js'
|
|
4
6
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
|
|
7
|
+
import { applyBinaryOp } from './binary.js'
|
|
5
8
|
import { applyIntervalToDate } from './date.js'
|
|
6
|
-
import { executeSelect } from './execute.js'
|
|
7
9
|
import { evaluateMathFunc } from './math.js'
|
|
8
10
|
import { evaluateRegexpFunc } from './regexp.js'
|
|
9
11
|
import { evaluateStringFunc } from './strings.js'
|
|
10
|
-
import { applyBinaryOp, stringify } from './utils.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, UserDefinedFunction } from '../types.js'
|
|
@@ -24,9 +25,10 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
24
25
|
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
25
26
|
* @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
|
|
26
27
|
* @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
|
|
28
|
+
* @param {AbortSignal} [params.signal] - abort signal for cancellation
|
|
27
29
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
28
30
|
*/
|
|
29
|
-
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases }) {
|
|
31
|
+
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases, signal }) {
|
|
30
32
|
if (node.type === 'literal') {
|
|
31
33
|
return node.value
|
|
32
34
|
}
|
|
@@ -45,7 +47,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
45
47
|
}
|
|
46
48
|
// Check if it's a SELECT alias (for ORDER BY)
|
|
47
49
|
if (aliases?.has(node.name)) {
|
|
48
|
-
return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases })
|
|
50
|
+
return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases, signal })
|
|
49
51
|
}
|
|
50
52
|
// Unknown identifier
|
|
51
53
|
throw columnNotFoundError({
|
|
@@ -53,13 +55,13 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
53
55
|
availableColumns: Object.keys(row.cells),
|
|
54
56
|
positionStart: node.positionStart,
|
|
55
57
|
positionEnd: node.positionEnd,
|
|
56
|
-
|
|
58
|
+
rowIndex,
|
|
57
59
|
})
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// Scalar subquery - returns a single value
|
|
61
63
|
if (node.type === 'subquery') {
|
|
62
|
-
const gen = executeSelect({ select: node.subquery, tables })
|
|
64
|
+
const gen = executeSelect({ select: node.subquery, tables, functions, signal })
|
|
63
65
|
const { value } = await gen.next() // Start the generator
|
|
64
66
|
gen.return(undefined) // Stop further execution
|
|
65
67
|
if (!value) return null
|
|
@@ -68,45 +70,35 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
68
70
|
|
|
69
71
|
// Unary operators
|
|
70
72
|
if (node.type === 'unary') {
|
|
71
|
-
|
|
72
|
-
return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
73
|
-
}
|
|
74
|
-
if (node.op === 'IS NULL') {
|
|
75
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) == null
|
|
76
|
-
}
|
|
77
|
-
if (node.op === 'IS NOT NULL') {
|
|
78
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) != null
|
|
79
|
-
}
|
|
73
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases, signal })
|
|
80
74
|
if (node.op === '-') {
|
|
81
|
-
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
82
75
|
if (val == null) return null
|
|
83
76
|
return -val
|
|
84
77
|
}
|
|
78
|
+
if (node.op === 'NOT') return !val
|
|
79
|
+
if (node.op === 'IS NULL') return val == null
|
|
80
|
+
if (node.op === 'IS NOT NULL') return val != null
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
// Binary operators
|
|
88
84
|
if (node.type === 'binary') {
|
|
89
|
-
// Handle date +/- interval
|
|
85
|
+
// Handle date +/- interval
|
|
90
86
|
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
91
|
-
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
87
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
|
|
92
88
|
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
93
89
|
}
|
|
94
90
|
if (node.op === '+' && node.left.type === 'interval') {
|
|
95
|
-
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
91
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
|
|
96
92
|
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
97
93
|
}
|
|
98
94
|
|
|
99
|
-
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
95
|
+
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
|
|
100
96
|
|
|
101
97
|
// Short-circuit evaluation for AND and OR
|
|
102
|
-
if (node.op === 'AND')
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
if (node.op === 'OR') {
|
|
106
|
-
if (left) return true
|
|
107
|
-
}
|
|
98
|
+
if (node.op === 'AND' && !left) return false
|
|
99
|
+
if (node.op === 'OR' && left) return true
|
|
108
100
|
|
|
109
|
-
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
101
|
+
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
|
|
110
102
|
return applyBinaryOp(node.op, left, right)
|
|
111
103
|
}
|
|
112
104
|
|
|
@@ -119,7 +111,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
119
111
|
if (!rows) {
|
|
120
112
|
throw aggregateError({
|
|
121
113
|
funcName,
|
|
122
|
-
issue: '
|
|
114
|
+
issue: ' is not allowed outside of aggregate context',
|
|
123
115
|
})
|
|
124
116
|
}
|
|
125
117
|
|
|
@@ -128,7 +120,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
128
120
|
if (node.filter) {
|
|
129
121
|
filteredRows = []
|
|
130
122
|
for (const row of rows) {
|
|
131
|
-
const passes = await evaluateExpr({ node: node.filter, row, tables, functions })
|
|
123
|
+
const passes = await evaluateExpr({ node: node.filter, row, tables, functions, signal })
|
|
132
124
|
if (passes) filteredRows.push(row)
|
|
133
125
|
}
|
|
134
126
|
}
|
|
@@ -140,7 +132,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
140
132
|
}
|
|
141
133
|
throw aggregateError({
|
|
142
134
|
funcName,
|
|
143
|
-
issue: '(*) is not supported
|
|
135
|
+
issue: '(*) is not supported. Only COUNT supports *.',
|
|
144
136
|
})
|
|
145
137
|
}
|
|
146
138
|
|
|
@@ -150,14 +142,14 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
150
142
|
if (node.distinct) {
|
|
151
143
|
const seen = new Set()
|
|
152
144
|
for (const row of filteredRows) {
|
|
153
|
-
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
145
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
154
146
|
if (v != null) seen.add(v)
|
|
155
147
|
}
|
|
156
148
|
return seen.size
|
|
157
149
|
}
|
|
158
150
|
let count = 0
|
|
159
151
|
for (const row of filteredRows) {
|
|
160
|
-
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
152
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
161
153
|
if (v != null) count++
|
|
162
154
|
}
|
|
163
155
|
return count
|
|
@@ -172,7 +164,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
172
164
|
let max = null
|
|
173
165
|
|
|
174
166
|
for (const row of filteredRows) {
|
|
175
|
-
const raw = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
167
|
+
const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
176
168
|
if (raw == null) continue
|
|
177
169
|
const num = Number(raw)
|
|
178
170
|
if (!Number.isFinite(num)) continue
|
|
@@ -197,7 +189,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
197
189
|
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
198
190
|
const values = []
|
|
199
191
|
for (const row of filteredRows) {
|
|
200
|
-
const raw = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
192
|
+
const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
201
193
|
if (raw == null) continue
|
|
202
194
|
const num = Number(raw)
|
|
203
195
|
if (!Number.isFinite(num)) continue
|
|
@@ -219,7 +211,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
219
211
|
if (node.distinct) {
|
|
220
212
|
const seen = new Set()
|
|
221
213
|
for (const row of filteredRows) {
|
|
222
|
-
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
214
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
223
215
|
const key = stringify(v)
|
|
224
216
|
if (!seen.has(key)) {
|
|
225
217
|
seen.add(key)
|
|
@@ -228,7 +220,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
228
220
|
}
|
|
229
221
|
} else {
|
|
230
222
|
for (const row of filteredRows) {
|
|
231
|
-
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
223
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
|
|
232
224
|
values.push(v)
|
|
233
225
|
}
|
|
234
226
|
}
|
|
@@ -237,7 +229,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
237
229
|
}
|
|
238
230
|
|
|
239
231
|
/** @type {SqlPrimitive[]} */
|
|
240
|
-
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases })))
|
|
232
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases, signal })))
|
|
241
233
|
|
|
242
234
|
if (isStringFunc(funcName)) {
|
|
243
235
|
return evaluateStringFunc({
|
|
@@ -259,15 +251,26 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
259
251
|
})
|
|
260
252
|
}
|
|
261
253
|
|
|
254
|
+
if (isMathFunc(funcName)) {
|
|
255
|
+
return evaluateMathFunc({ funcName, args })
|
|
256
|
+
}
|
|
257
|
+
|
|
262
258
|
if (funcName === 'COALESCE') {
|
|
263
259
|
// Short-circuit: evaluate args one at a time, return first non-null
|
|
264
260
|
for (const arg of node.args) {
|
|
265
|
-
const val = await evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })
|
|
261
|
+
const val = await evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, signal })
|
|
266
262
|
if (val != null) return val
|
|
267
263
|
}
|
|
268
264
|
return null
|
|
269
265
|
}
|
|
270
266
|
|
|
267
|
+
if (funcName === 'NULLIF') {
|
|
268
|
+
// NULLIF(a, b) returns null if a = b, otherwise returns a
|
|
269
|
+
const val1 = await evaluateExpr({ node: node.args[0], row, tables, functions, rowIndex, rows, signal })
|
|
270
|
+
const val2 = await evaluateExpr({ node: node.args[1], row, tables, functions, rowIndex, rows, signal })
|
|
271
|
+
return val1 == val2 ? null : val1
|
|
272
|
+
}
|
|
273
|
+
|
|
271
274
|
if (funcName === 'CURRENT_DATE') {
|
|
272
275
|
return new Date().toISOString().split('T')[0]
|
|
273
276
|
}
|
|
@@ -287,7 +290,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
287
290
|
message: 'requires an even number of arguments (key-value pairs)',
|
|
288
291
|
positionStart: node.positionStart,
|
|
289
292
|
positionEnd: node.positionEnd,
|
|
290
|
-
|
|
293
|
+
rowIndex,
|
|
291
294
|
})
|
|
292
295
|
}
|
|
293
296
|
/** @type {Record<string, SqlPrimitive>} */
|
|
@@ -302,7 +305,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
302
305
|
positionStart: node.positionStart,
|
|
303
306
|
positionEnd: node.positionEnd,
|
|
304
307
|
hint: 'All keys must be non-null values.',
|
|
305
|
-
|
|
308
|
+
rowIndex,
|
|
306
309
|
})
|
|
307
310
|
}
|
|
308
311
|
result[String(key)] = value
|
|
@@ -326,7 +329,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
326
329
|
positionStart: node.positionStart,
|
|
327
330
|
positionEnd: node.positionEnd,
|
|
328
331
|
hint: 'First argument must be valid JSON.',
|
|
329
|
-
|
|
332
|
+
rowIndex,
|
|
330
333
|
})
|
|
331
334
|
}
|
|
332
335
|
}
|
|
@@ -336,7 +339,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
336
339
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
337
340
|
positionStart: node.positionStart,
|
|
338
341
|
positionEnd: node.positionEnd,
|
|
339
|
-
|
|
342
|
+
rowIndex,
|
|
340
343
|
})
|
|
341
344
|
}
|
|
342
345
|
|
|
@@ -366,10 +369,6 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
366
369
|
return current
|
|
367
370
|
}
|
|
368
371
|
|
|
369
|
-
if (isMathFunc(funcName)) {
|
|
370
|
-
return evaluateMathFunc({ funcName, args })
|
|
371
|
-
}
|
|
372
|
-
|
|
373
372
|
// Check user-defined functions (case-insensitive lookup)
|
|
374
373
|
if (functions) {
|
|
375
374
|
const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
|
|
@@ -386,7 +385,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
386
385
|
}
|
|
387
386
|
|
|
388
387
|
if (node.type === 'cast') {
|
|
389
|
-
const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
|
|
388
|
+
const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
|
|
390
389
|
if (val == null) return null
|
|
391
390
|
const toType = node.toType.toUpperCase()
|
|
392
391
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
@@ -400,7 +399,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
400
399
|
positionStart: node.positionStart,
|
|
401
400
|
positionEnd: node.positionEnd,
|
|
402
401
|
fromType: 'object',
|
|
403
|
-
|
|
402
|
+
rowIndex,
|
|
404
403
|
})
|
|
405
404
|
}
|
|
406
405
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
@@ -423,23 +422,23 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
423
422
|
toType: node.toType,
|
|
424
423
|
positionStart: node.positionStart,
|
|
425
424
|
positionEnd: node.positionEnd,
|
|
426
|
-
|
|
425
|
+
rowIndex,
|
|
427
426
|
})
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
// IN and NOT IN with value lists
|
|
431
430
|
if (node.type === 'in valuelist') {
|
|
432
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
|
|
431
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
|
|
433
432
|
for (const valueNode of node.values) {
|
|
434
|
-
const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows })
|
|
433
|
+
const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows, signal })
|
|
435
434
|
if (exprVal == val) return true
|
|
436
435
|
}
|
|
437
436
|
return false
|
|
438
437
|
}
|
|
439
438
|
// IN with subqueries
|
|
440
439
|
if (node.type === 'in') {
|
|
441
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
|
|
442
|
-
const results = executeSelect({ select: node.subquery, tables })
|
|
440
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
|
|
441
|
+
const results = executeSelect({ select: node.subquery, tables, functions, signal })
|
|
443
442
|
for await (const resRow of results) {
|
|
444
443
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
445
444
|
if (exprVal == value) return true
|
|
@@ -449,39 +448,39 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
449
448
|
|
|
450
449
|
// EXISTS and NOT EXISTS with subqueries
|
|
451
450
|
if (node.type === 'exists') {
|
|
452
|
-
const results = await executeSelect({ select: node.subquery, tables }).next()
|
|
451
|
+
const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
|
|
453
452
|
return results.done === false
|
|
454
453
|
}
|
|
455
454
|
if (node.type === 'not exists') {
|
|
456
|
-
const results = await executeSelect({ select: node.subquery, tables }).next()
|
|
455
|
+
const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
|
|
457
456
|
return results.done === true
|
|
458
457
|
}
|
|
459
458
|
|
|
460
459
|
// CASE expressions
|
|
461
460
|
if (node.type === 'case') {
|
|
462
461
|
// For simple CASE: evaluate the case expression once
|
|
463
|
-
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows })
|
|
462
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows, signal })
|
|
464
463
|
|
|
465
464
|
// Iterate through WHEN clauses
|
|
466
465
|
for (const whenClause of node.whenClauses) {
|
|
467
466
|
let conditionResult
|
|
468
467
|
if (caseValue !== undefined) {
|
|
469
468
|
// Simple CASE: compare caseValue with condition
|
|
470
|
-
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
|
469
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
|
|
471
470
|
conditionResult = caseValue == whenValue
|
|
472
471
|
} else {
|
|
473
472
|
// Searched CASE: evaluate condition as boolean
|
|
474
|
-
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
|
473
|
+
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
|
|
475
474
|
}
|
|
476
475
|
|
|
477
476
|
if (conditionResult) {
|
|
478
|
-
return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows })
|
|
477
|
+
return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows, signal })
|
|
479
478
|
}
|
|
480
479
|
}
|
|
481
480
|
|
|
482
481
|
// No WHEN clause matched, return ELSE result or NULL
|
|
483
482
|
if (node.elseResult) {
|
|
484
|
-
return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows })
|
|
483
|
+
return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows, signal })
|
|
485
484
|
}
|
|
486
485
|
return null
|
|
487
486
|
}
|
|
@@ -494,7 +493,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
494
493
|
validContext: 'date arithmetic (+ or -)',
|
|
495
494
|
positionStart: node.positionStart,
|
|
496
495
|
positionEnd: node.positionEnd,
|
|
497
|
-
|
|
496
|
+
rowIndex,
|
|
498
497
|
})
|
|
499
498
|
}
|
|
500
499
|
|