squirreling 0.5.0 → 0.6.1
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 -1
- package/package.json +5 -5
- package/src/backend/dataSource.js +23 -16
- package/src/execute/columns.js +39 -5
- package/src/execute/execute.js +91 -69
- package/src/execute/expression.js +141 -40
- package/src/execute/join.js +38 -31
- package/src/execute/math.js +178 -3
- package/src/execute/utils.js +6 -2
- package/src/executionErrors.js +7 -6
- package/src/parse/expression.js +20 -0
- package/src/parse/parse.js +3 -52
- package/src/parseErrors.js +15 -14
- package/src/types.d.ts +40 -37
- package/src/validation.js +2 -0
- package/src/validationErrors.js +14 -3
- package/src/execute/aggregates.js +0 -119
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
2
|
import { invalidContextError } from '../executionErrors.js'
|
|
3
3
|
import {
|
|
4
|
+
aggregateError,
|
|
4
5
|
argCountError,
|
|
5
6
|
argValueError,
|
|
6
7
|
castError,
|
|
7
8
|
} from '../validationErrors.js'
|
|
8
|
-
import { isMathFunc } from '../validation.js'
|
|
9
|
+
import { isAggregateFunc, isMathFunc } from '../validation.js'
|
|
9
10
|
import { applyIntervalToDate } from './date.js'
|
|
10
11
|
import { executeSelect } from './execute.js'
|
|
11
12
|
import { evaluateMathFunc } from './math.js'
|
|
@@ -23,23 +24,24 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
23
24
|
* @param {AsyncRow} params.row - The data row to evaluate against
|
|
24
25
|
* @param {Record<string, AsyncDataSource>} params.tables
|
|
25
26
|
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
27
|
+
* @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
|
|
26
28
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
27
29
|
*/
|
|
28
|
-
export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
30
|
+
export async function evaluateExpr({ node, row, tables, rowIndex, rows }) {
|
|
29
31
|
if (node.type === 'literal') {
|
|
30
32
|
return node.value
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
if (node.type === 'identifier') {
|
|
34
36
|
// Try exact match first (handles both qualified and unqualified names)
|
|
35
|
-
if (row[node.name]) {
|
|
36
|
-
return row[node.name]()
|
|
37
|
+
if (row.cells[node.name]) {
|
|
38
|
+
return row.cells[node.name]()
|
|
37
39
|
}
|
|
38
40
|
// For qualified names like 'users.id', also try just the column part
|
|
39
41
|
if (node.name.includes('.')) {
|
|
40
42
|
const colName = node.name.split('.').pop()
|
|
41
|
-
if (colName && row[colName]) {
|
|
42
|
-
return row[colName]()
|
|
43
|
+
if (colName && row.cells[colName]) {
|
|
44
|
+
return row.cells[colName]()
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
return null
|
|
@@ -47,29 +49,26 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
|
47
49
|
|
|
48
50
|
// Scalar subquery - returns a single value
|
|
49
51
|
if (node.type === 'subquery') {
|
|
50
|
-
const gen = executeSelect(node.subquery, tables)
|
|
51
|
-
const
|
|
52
|
+
const gen = executeSelect({ select: node.subquery, tables })
|
|
53
|
+
const { value } = await gen.next() // Start the generator
|
|
52
54
|
gen.return(undefined) // Stop further execution
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
const firstRow = first.value
|
|
56
|
-
const firstKey = Object.keys(firstRow)[0]
|
|
57
|
-
return firstRow[firstKey]()
|
|
55
|
+
if (!value) return null
|
|
56
|
+
return value.cells[value.columns[0]]()
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
// Unary operators
|
|
61
60
|
if (node.type === 'unary') {
|
|
62
61
|
if (node.op === 'NOT') {
|
|
63
|
-
return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
62
|
+
return !await evaluateExpr({ node: node.argument, row, tables, rowIndex, rows })
|
|
64
63
|
}
|
|
65
64
|
if (node.op === 'IS NULL') {
|
|
66
|
-
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
|
|
65
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex, rows }) == null
|
|
67
66
|
}
|
|
68
67
|
if (node.op === 'IS NOT NULL') {
|
|
69
|
-
return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
|
|
68
|
+
return await evaluateExpr({ node: node.argument, row, tables, rowIndex, rows }) != null
|
|
70
69
|
}
|
|
71
70
|
if (node.op === '-') {
|
|
72
|
-
const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
|
|
71
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex, rows })
|
|
73
72
|
if (val == null) return null
|
|
74
73
|
return -val
|
|
75
74
|
}
|
|
@@ -79,15 +78,15 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
|
79
78
|
if (node.type === 'binary') {
|
|
80
79
|
// Handle date +/- interval at AST level
|
|
81
80
|
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
82
|
-
const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
81
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex, rows })
|
|
83
82
|
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
84
83
|
}
|
|
85
84
|
if (node.op === '+' && node.left.type === 'interval') {
|
|
86
|
-
const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
85
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex, rows })
|
|
87
86
|
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
|
|
89
|
+
const left = await evaluateExpr({ node: node.left, row, tables, rowIndex, rows })
|
|
91
90
|
|
|
92
91
|
// Short-circuit evaluation for AND and OR
|
|
93
92
|
if (node.op === 'AND') {
|
|
@@ -97,15 +96,120 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
|
97
96
|
if (left) return true
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
|
|
99
|
+
const right = await evaluateExpr({ node: node.right, row, tables, rowIndex, rows })
|
|
101
100
|
return applyBinaryOp(node.op, left, right)
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
// Function calls
|
|
105
104
|
if (node.type === 'function') {
|
|
106
105
|
const funcName = node.name.toUpperCase()
|
|
106
|
+
|
|
107
|
+
// Handle aggregate functions
|
|
108
|
+
if (isAggregateFunc(funcName)) {
|
|
109
|
+
if (!rows) {
|
|
110
|
+
throw aggregateError({
|
|
111
|
+
funcName,
|
|
112
|
+
issue: 'requires GROUP BY or will act on the whole dataset',
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for star argument (COUNT(*))
|
|
117
|
+
if (node.args.length === 1 && node.args[0].type === 'identifier' && node.args[0].name === '*') {
|
|
118
|
+
if (funcName === 'COUNT') {
|
|
119
|
+
return rows.length
|
|
120
|
+
}
|
|
121
|
+
throw aggregateError({
|
|
122
|
+
funcName,
|
|
123
|
+
issue: '(*) is not supported, use a column name',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (node.args.length !== 1) {
|
|
128
|
+
throw argCountError({
|
|
129
|
+
funcName,
|
|
130
|
+
expected: 1,
|
|
131
|
+
received: node.args.length,
|
|
132
|
+
positionStart: node.positionStart,
|
|
133
|
+
positionEnd: node.positionEnd,
|
|
134
|
+
rowNumber: rowIndex,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const argNode = node.args[0]
|
|
139
|
+
|
|
140
|
+
if (funcName === 'COUNT') {
|
|
141
|
+
if (node.distinct) {
|
|
142
|
+
const seen = new Set()
|
|
143
|
+
for (const r of rows) {
|
|
144
|
+
const v = await evaluateExpr({ node: argNode, row: r, tables })
|
|
145
|
+
if (v != null) seen.add(v)
|
|
146
|
+
}
|
|
147
|
+
return seen.size
|
|
148
|
+
}
|
|
149
|
+
let count = 0
|
|
150
|
+
for (const r of rows) {
|
|
151
|
+
const v = await evaluateExpr({ node: argNode, row: r, tables })
|
|
152
|
+
if (v != null) count++
|
|
153
|
+
}
|
|
154
|
+
return count
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
|
|
158
|
+
let sum = 0
|
|
159
|
+
let count = 0
|
|
160
|
+
/** @type {number | null} */
|
|
161
|
+
let min = null
|
|
162
|
+
/** @type {number | null} */
|
|
163
|
+
let max = null
|
|
164
|
+
|
|
165
|
+
for (const r of rows) {
|
|
166
|
+
const raw = await evaluateExpr({ node: argNode, row: r, tables })
|
|
167
|
+
if (raw == null) continue
|
|
168
|
+
const num = Number(raw)
|
|
169
|
+
if (!Number.isFinite(num)) continue
|
|
170
|
+
|
|
171
|
+
if (count === 0) {
|
|
172
|
+
min = num
|
|
173
|
+
max = num
|
|
174
|
+
} else {
|
|
175
|
+
if (min == null || num < min) min = num
|
|
176
|
+
if (max == null || num > max) max = num
|
|
177
|
+
}
|
|
178
|
+
sum += num
|
|
179
|
+
count++
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (funcName === 'SUM') return sum
|
|
183
|
+
if (funcName === 'AVG') return count === 0 ? null : sum / count
|
|
184
|
+
if (funcName === 'MIN') return min
|
|
185
|
+
if (funcName === 'MAX') return max
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (funcName === 'JSON_ARRAYAGG') {
|
|
189
|
+
/** @type {SqlPrimitive[]} */
|
|
190
|
+
const values = []
|
|
191
|
+
if (node.distinct) {
|
|
192
|
+
const seen = new Set()
|
|
193
|
+
for (const r of rows) {
|
|
194
|
+
const v = await evaluateExpr({ node: argNode, row: r, tables })
|
|
195
|
+
const key = stringify(v)
|
|
196
|
+
if (!seen.has(key)) {
|
|
197
|
+
seen.add(key)
|
|
198
|
+
values.push(v)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
for (const r of rows) {
|
|
203
|
+
const v = await evaluateExpr({ node: argNode, row: r, tables })
|
|
204
|
+
values.push(v)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return values
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
107
211
|
/** @type {SqlPrimitive[]} */
|
|
108
|
-
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex })))
|
|
212
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex, rows })))
|
|
109
213
|
|
|
110
214
|
if (funcName === 'UPPER') {
|
|
111
215
|
if (args.length !== 1) {
|
|
@@ -430,7 +534,7 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
|
430
534
|
}
|
|
431
535
|
|
|
432
536
|
if (node.type === 'cast') {
|
|
433
|
-
const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
537
|
+
const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex, rows })
|
|
434
538
|
if (val == null) return null
|
|
435
539
|
const toType = node.toType.toUpperCase()
|
|
436
540
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
@@ -473,62 +577,59 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
|
|
|
473
577
|
|
|
474
578
|
// IN and NOT IN with value lists
|
|
475
579
|
if (node.type === 'in valuelist') {
|
|
476
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
580
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex, rows })
|
|
477
581
|
for (const valueNode of node.values) {
|
|
478
|
-
const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
|
|
582
|
+
const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex, rows })
|
|
479
583
|
if (exprVal === val) return true
|
|
480
584
|
}
|
|
481
585
|
return false
|
|
482
586
|
}
|
|
483
587
|
// IN with subqueries
|
|
484
588
|
if (node.type === 'in') {
|
|
485
|
-
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
|
|
486
|
-
const results = executeSelect(node.subquery, tables)
|
|
487
|
-
/** @type {SqlPrimitive[]} */
|
|
488
|
-
const values = []
|
|
589
|
+
const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex, rows })
|
|
590
|
+
const results = executeSelect({ select: node.subquery, tables })
|
|
489
591
|
for await (const resRow of results) {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
values.push(val)
|
|
592
|
+
const value = await resRow.cells[resRow.columns[0]]()
|
|
593
|
+
if (exprVal === value) return true
|
|
493
594
|
}
|
|
494
|
-
return
|
|
595
|
+
return false
|
|
495
596
|
}
|
|
496
597
|
|
|
497
598
|
// EXISTS and NOT EXISTS with subqueries
|
|
498
599
|
if (node.type === 'exists') {
|
|
499
|
-
const results = await executeSelect(node.subquery, tables).next()
|
|
600
|
+
const results = await executeSelect({ select: node.subquery, tables }).next()
|
|
500
601
|
return results.done === false
|
|
501
602
|
}
|
|
502
603
|
if (node.type === 'not exists') {
|
|
503
|
-
const results = await executeSelect(node.subquery, tables).next()
|
|
604
|
+
const results = await executeSelect({ select: node.subquery, tables }).next()
|
|
504
605
|
return results.done === true
|
|
505
606
|
}
|
|
506
607
|
|
|
507
608
|
// CASE expressions
|
|
508
609
|
if (node.type === 'case') {
|
|
509
610
|
// For simple CASE: evaluate the case expression once
|
|
510
|
-
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
|
|
611
|
+
const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex, rows })
|
|
511
612
|
|
|
512
613
|
// Iterate through WHEN clauses
|
|
513
614
|
for (const whenClause of node.whenClauses) {
|
|
514
615
|
let conditionResult
|
|
515
616
|
if (caseValue !== undefined) {
|
|
516
617
|
// Simple CASE: compare caseValue with condition
|
|
517
|
-
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
618
|
+
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex, rows })
|
|
518
619
|
conditionResult = caseValue === whenValue
|
|
519
620
|
} else {
|
|
520
621
|
// Searched CASE: evaluate condition as boolean
|
|
521
|
-
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
|
|
622
|
+
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex, rows })
|
|
522
623
|
}
|
|
523
624
|
|
|
524
625
|
if (conditionResult) {
|
|
525
|
-
return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
|
|
626
|
+
return evaluateExpr({ node: whenClause.result, row, tables, rowIndex, rows })
|
|
526
627
|
}
|
|
527
628
|
}
|
|
528
629
|
|
|
529
630
|
// No WHEN clause matched, return ELSE result or NULL
|
|
530
631
|
if (node.elseResult) {
|
|
531
|
-
return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
|
|
632
|
+
return evaluateExpr({ node: node.elseResult, row, tables, rowIndex, rows })
|
|
532
633
|
}
|
|
533
634
|
return null
|
|
534
635
|
}
|
package/src/execute/join.js
CHANGED
|
@@ -4,7 +4,7 @@ import { evaluateExpr } from './expression.js'
|
|
|
4
4
|
import { stringify } from './utils.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
|
|
7
|
+
* @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode, AsyncCells } from '../types.js'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -30,7 +30,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
30
30
|
// Buffer right rows for hash index (required for hash join)
|
|
31
31
|
/** @type {AsyncRow[]} */
|
|
32
32
|
const rightRows = []
|
|
33
|
-
for await (const row of rightSource.
|
|
33
|
+
for await (const row of rightSource.scan({})) {
|
|
34
34
|
rightRows.push(row)
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -39,14 +39,16 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
39
39
|
|
|
40
40
|
// Return streaming data source - left rows stream through without buffering
|
|
41
41
|
return {
|
|
42
|
-
async *
|
|
42
|
+
async *scan(options) {
|
|
43
|
+
const { signal } = options
|
|
43
44
|
yield* hashJoin({
|
|
44
|
-
leftRows: leftSource.
|
|
45
|
+
leftRows: leftSource.scan(options), // Stream directly, not buffered
|
|
45
46
|
rightRows,
|
|
46
47
|
join,
|
|
47
48
|
leftTable: currentLeftTable,
|
|
48
49
|
rightTable: rightTableName,
|
|
49
50
|
tables,
|
|
51
|
+
signal,
|
|
50
52
|
})
|
|
51
53
|
},
|
|
52
54
|
}
|
|
@@ -55,7 +57,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
55
57
|
// Multiple joins: buffer intermediate results, stream final join
|
|
56
58
|
/** @type {AsyncRow[]} */
|
|
57
59
|
let leftRows = []
|
|
58
|
-
for await (const row of leftSource.
|
|
60
|
+
for await (const row of leftSource.scan({})) {
|
|
59
61
|
leftRows.push(row)
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -69,7 +71,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
69
71
|
|
|
70
72
|
/** @type {AsyncRow[]} */
|
|
71
73
|
const rightRows = []
|
|
72
|
-
for await (const row of rightSource.
|
|
74
|
+
for await (const row of rightSource.scan({})) {
|
|
73
75
|
rightRows.push(row)
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -105,7 +107,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
105
107
|
|
|
106
108
|
/** @type {AsyncRow[]} */
|
|
107
109
|
const rightRows = []
|
|
108
|
-
for await (const row of rightSource.
|
|
110
|
+
for await (const row of rightSource.scan({})) {
|
|
109
111
|
rightRows.push(row)
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -113,7 +115,8 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
113
115
|
const lastRightTableName = lastJoin.alias ?? lastJoin.table
|
|
114
116
|
|
|
115
117
|
return {
|
|
116
|
-
async *
|
|
118
|
+
async *scan(options) {
|
|
119
|
+
const { signal } = options
|
|
117
120
|
yield* hashJoin({
|
|
118
121
|
leftRows,
|
|
119
122
|
rightRows,
|
|
@@ -121,6 +124,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
|
|
|
121
124
|
leftTable: currentLeftTable,
|
|
122
125
|
rightTable: lastRightTableName,
|
|
123
126
|
tables,
|
|
127
|
+
signal,
|
|
124
128
|
})
|
|
125
129
|
},
|
|
126
130
|
}
|
|
@@ -172,12 +176,12 @@ function extractJoinKeys(onCondition, leftTable, rightTable) {
|
|
|
172
176
|
* @returns {AsyncRow}
|
|
173
177
|
*/
|
|
174
178
|
function createNullRow(columnNames) {
|
|
175
|
-
/** @type {
|
|
176
|
-
const
|
|
179
|
+
/** @type {AsyncCells} */
|
|
180
|
+
const cells = {}
|
|
177
181
|
for (const col of columnNames) {
|
|
178
|
-
|
|
182
|
+
cells[col] = () => Promise.resolve(null)
|
|
179
183
|
}
|
|
180
|
-
return
|
|
184
|
+
return { columns: columnNames, cells }
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
/**
|
|
@@ -190,33 +194,35 @@ function createNullRow(columnNames) {
|
|
|
190
194
|
* @returns {AsyncRow}
|
|
191
195
|
*/
|
|
192
196
|
function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
const columns = []
|
|
198
|
+
/** @type {AsyncCells} */
|
|
199
|
+
const cells = {}
|
|
195
200
|
|
|
196
201
|
// Add left table columns with prefix
|
|
197
|
-
for (const [key, cell] of Object.entries(leftRow)) {
|
|
202
|
+
for (const [key, cell] of Object.entries(leftRow.cells)) {
|
|
198
203
|
// Skip already-prefixed keys (from previous joins)
|
|
199
204
|
if (!key.includes('.')) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
merged[key] = cell
|
|
205
|
+
const alias = `${leftTable}.${key}`
|
|
206
|
+
cells[alias] = cell
|
|
203
207
|
}
|
|
204
|
-
// Also keep unqualified name for convenience
|
|
205
|
-
|
|
208
|
+
// Also keep unqualified name for convenience
|
|
209
|
+
columns.push(key)
|
|
210
|
+
cells[key] = cell
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
// Add right table columns with prefix
|
|
209
|
-
for (const [key, cell] of Object.entries(rightRow)) {
|
|
214
|
+
for (const [key, cell] of Object.entries(rightRow.cells)) {
|
|
210
215
|
if (!key.includes('.')) {
|
|
211
|
-
|
|
216
|
+
cells[`${rightTable}.${key}`] = cell
|
|
212
217
|
} else {
|
|
213
|
-
|
|
218
|
+
cells[key] = cell
|
|
214
219
|
}
|
|
215
220
|
// Unqualified name (overwrites if same name exists in left table)
|
|
216
|
-
|
|
221
|
+
columns.push(key)
|
|
222
|
+
cells[key] = cell
|
|
217
223
|
}
|
|
218
224
|
|
|
219
|
-
return
|
|
225
|
+
return { columns, cells }
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
/**
|
|
@@ -230,9 +236,10 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
230
236
|
* @param {string} params.leftTable - name of left table (for column prefixing)
|
|
231
237
|
* @param {string} params.rightTable - name of right table (for column prefixing, may be alias)
|
|
232
238
|
* @param {Record<string, AsyncDataSource>} params.tables - all tables for expression evaluation
|
|
239
|
+
* @param {AbortSignal} [params.signal] - abort signal for cancellation
|
|
233
240
|
* @yields {AsyncRow} joined rows
|
|
234
241
|
*/
|
|
235
|
-
async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tables }) {
|
|
242
|
+
async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tables, signal }) {
|
|
236
243
|
const { joinType, on: onCondition } = join
|
|
237
244
|
|
|
238
245
|
if (!onCondition) {
|
|
@@ -245,7 +252,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
245
252
|
const keys = extractJoinKeys(onCondition, leftTable, rightTable)
|
|
246
253
|
|
|
247
254
|
// Get column names for NULL row generation (right side is always buffered)
|
|
248
|
-
const rightCols = rightRows.length ?
|
|
255
|
+
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
249
256
|
const rightPrefixedCols = rightCols.flatMap(col =>
|
|
250
257
|
col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
|
|
251
258
|
)
|
|
@@ -279,10 +286,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
279
286
|
|
|
280
287
|
// PROBE PHASE: Stream through left rows, yield matches immediately
|
|
281
288
|
for await (const leftRow of leftRows) {
|
|
289
|
+
if (signal?.aborted) break
|
|
282
290
|
// Capture left column info from first row (for NULL row generation)
|
|
283
291
|
if (!leftPrefixedCols) {
|
|
284
|
-
|
|
285
|
-
leftPrefixedCols = leftCols.flatMap(col =>
|
|
292
|
+
leftPrefixedCols = leftRow.columns.flatMap(col =>
|
|
286
293
|
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
287
294
|
)
|
|
288
295
|
}
|
|
@@ -321,10 +328,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
|
|
|
321
328
|
const matchedRightRows = joinType === 'RIGHT' || joinType === 'FULL' ? new Set() : null
|
|
322
329
|
|
|
323
330
|
for await (const leftRow of leftRows) {
|
|
331
|
+
if (signal?.aborted) break
|
|
324
332
|
// Capture left column info from first row (for NULL row generation)
|
|
325
333
|
if (!leftPrefixedCols) {
|
|
326
|
-
|
|
327
|
-
leftPrefixedCols = leftCols.flatMap(col =>
|
|
334
|
+
leftPrefixedCols = leftRow.columns.flatMap(col =>
|
|
328
335
|
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
329
336
|
)
|
|
330
337
|
}
|
package/src/execute/math.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { SqlPrimitive } from '../types.js'
|
|
2
|
+
* @import { MathFunc, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
import { argCountError } from '../validationErrors.js'
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ import { argCountError } from '../validationErrors.js'
|
|
|
7
7
|
* Evaluate a math function
|
|
8
8
|
*
|
|
9
9
|
* @param {Object} options
|
|
10
|
-
* @param {
|
|
10
|
+
* @param {MathFunc} options.funcName - Uppercase function name
|
|
11
11
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
12
12
|
* @param {number} options.positionStart - Start position in query
|
|
13
13
|
* @param {number} options.positionEnd - End position in query
|
|
@@ -161,5 +161,180 @@ export function evaluateMathFunc({ funcName, args, positionStart, positionEnd, r
|
|
|
161
161
|
return Math.sqrt(Number(val))
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
if (funcName === 'SIN') {
|
|
165
|
+
if (args.length !== 1) {
|
|
166
|
+
throw argCountError({
|
|
167
|
+
funcName: 'SIN',
|
|
168
|
+
expected: 1,
|
|
169
|
+
received: args.length,
|
|
170
|
+
positionStart,
|
|
171
|
+
positionEnd,
|
|
172
|
+
rowNumber,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
const val = args[0]
|
|
176
|
+
if (val == null) return null
|
|
177
|
+
return Math.sin(Number(val))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (funcName === 'COS') {
|
|
181
|
+
if (args.length !== 1) {
|
|
182
|
+
throw argCountError({
|
|
183
|
+
funcName: 'COS',
|
|
184
|
+
expected: 1,
|
|
185
|
+
received: args.length,
|
|
186
|
+
positionStart,
|
|
187
|
+
positionEnd,
|
|
188
|
+
rowNumber,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
const val = args[0]
|
|
192
|
+
if (val == null) return null
|
|
193
|
+
return Math.cos(Number(val))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (funcName === 'TAN') {
|
|
197
|
+
if (args.length !== 1) {
|
|
198
|
+
throw argCountError({
|
|
199
|
+
funcName: 'TAN',
|
|
200
|
+
expected: 1,
|
|
201
|
+
received: args.length,
|
|
202
|
+
positionStart,
|
|
203
|
+
positionEnd,
|
|
204
|
+
rowNumber,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
const val = args[0]
|
|
208
|
+
if (val == null) return null
|
|
209
|
+
return Math.tan(Number(val))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (funcName === 'COT') {
|
|
213
|
+
if (args.length !== 1) {
|
|
214
|
+
throw argCountError({
|
|
215
|
+
funcName: 'COT',
|
|
216
|
+
expected: 1,
|
|
217
|
+
received: args.length,
|
|
218
|
+
positionStart,
|
|
219
|
+
positionEnd,
|
|
220
|
+
rowNumber,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
const val = args[0]
|
|
224
|
+
if (val == null) return null
|
|
225
|
+
return 1 / Math.tan(Number(val))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (funcName === 'ASIN') {
|
|
229
|
+
if (args.length !== 1) {
|
|
230
|
+
throw argCountError({
|
|
231
|
+
funcName: 'ASIN',
|
|
232
|
+
expected: 1,
|
|
233
|
+
received: args.length,
|
|
234
|
+
positionStart,
|
|
235
|
+
positionEnd,
|
|
236
|
+
rowNumber,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
const val = args[0]
|
|
240
|
+
if (val == null) return null
|
|
241
|
+
return Math.asin(Number(val))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (funcName === 'ACOS') {
|
|
245
|
+
if (args.length !== 1) {
|
|
246
|
+
throw argCountError({
|
|
247
|
+
funcName: 'ACOS',
|
|
248
|
+
expected: 1,
|
|
249
|
+
received: args.length,
|
|
250
|
+
positionStart,
|
|
251
|
+
positionEnd,
|
|
252
|
+
rowNumber,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
const val = args[0]
|
|
256
|
+
if (val == null) return null
|
|
257
|
+
return Math.acos(Number(val))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (funcName === 'ATAN') {
|
|
261
|
+
if (args.length !== 1) {
|
|
262
|
+
throw argCountError({
|
|
263
|
+
funcName: 'ATAN',
|
|
264
|
+
expected: 1,
|
|
265
|
+
received: args.length,
|
|
266
|
+
positionStart,
|
|
267
|
+
positionEnd,
|
|
268
|
+
rowNumber,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
const val = args[0]
|
|
272
|
+
if (val == null) return null
|
|
273
|
+
return Math.atan(Number(val))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (funcName === 'ATAN2') {
|
|
277
|
+
if (args.length !== 2) {
|
|
278
|
+
throw argCountError({
|
|
279
|
+
funcName: 'ATAN2',
|
|
280
|
+
expected: 2,
|
|
281
|
+
received: args.length,
|
|
282
|
+
positionStart,
|
|
283
|
+
positionEnd,
|
|
284
|
+
rowNumber,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
const y = args[0]
|
|
288
|
+
const x = args[1]
|
|
289
|
+
if (y == null || x == null) return null
|
|
290
|
+
return Math.atan2(Number(y), Number(x))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (funcName === 'DEGREES') {
|
|
294
|
+
if (args.length !== 1) {
|
|
295
|
+
throw argCountError({
|
|
296
|
+
funcName: 'DEGREES',
|
|
297
|
+
expected: 1,
|
|
298
|
+
received: args.length,
|
|
299
|
+
positionStart,
|
|
300
|
+
positionEnd,
|
|
301
|
+
rowNumber,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
const val = args[0]
|
|
305
|
+
if (val == null) return null
|
|
306
|
+
return Number(val) * 180 / Math.PI
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (funcName === 'RADIANS') {
|
|
310
|
+
if (args.length !== 1) {
|
|
311
|
+
throw argCountError({
|
|
312
|
+
funcName: 'RADIANS',
|
|
313
|
+
expected: 1,
|
|
314
|
+
received: args.length,
|
|
315
|
+
positionStart,
|
|
316
|
+
positionEnd,
|
|
317
|
+
rowNumber,
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
const val = args[0]
|
|
321
|
+
if (val == null) return null
|
|
322
|
+
return Number(val) * Math.PI / 180
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (funcName === 'PI') {
|
|
326
|
+
if (args.length !== 0) {
|
|
327
|
+
throw argCountError({
|
|
328
|
+
funcName: 'PI',
|
|
329
|
+
expected: 0,
|
|
330
|
+
received: args.length,
|
|
331
|
+
positionStart,
|
|
332
|
+
positionEnd,
|
|
333
|
+
rowNumber,
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
return Math.PI
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null
|
|
165
340
|
}
|