squirreling 0.10.0 → 0.10.2
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/package.json +8 -6
- package/src/ast.d.ts +184 -0
- package/src/execute/execute.js +60 -12
- package/src/execute/utils.js +14 -14
- package/src/expression/alias.js +2 -2
- package/src/expression/evaluate.js +35 -68
- package/src/expression/regexp.js +13 -25
- package/src/expression/strings.js +14 -22
- package/src/index.d.ts +1 -0
- package/src/parse/comparison.js +2 -2
- package/src/parse/expression.js +21 -10
- package/src/parse/functions.js +14 -23
- package/src/parse/joins.js +5 -2
- package/src/parse/parse.js +5 -4
- package/src/parse/state.js +2 -2
- package/src/parse/tokenize.js +2 -9
- package/src/parse/types.d.ts +1 -1
- package/src/plan/plan.js +45 -12
- package/src/spatial/bbox.js +1 -1
- package/src/types.d.ts +12 -191
- package/src/validation/aggregates.js +67 -0
- package/src/validation/executionErrors.js +35 -0
- package/src/validation/expressionErrors.js +57 -0
- package/src/validation/functions.js +281 -0
- package/src/{parseErrors.js → validation/parseErrors.js} +21 -57
- package/src/validation/planErrors.js +42 -0
- package/src/executionErrors.js +0 -80
- package/src/validation.js +0 -343
- package/src/validationErrors.js +0 -141
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"dataset",
|
|
11
11
|
"hyperparam",
|
|
12
12
|
"hyparquet",
|
|
13
|
-
"parquet"
|
|
13
|
+
"parquet",
|
|
14
|
+
"query",
|
|
15
|
+
"relational"
|
|
14
16
|
],
|
|
15
17
|
"license": "MIT",
|
|
16
18
|
"repository": {
|
|
@@ -37,11 +39,11 @@
|
|
|
37
39
|
"test": "vitest run"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.
|
|
41
|
-
"@vitest/coverage-v8": "4.0
|
|
42
|
+
"@types/node": "25.5.0",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.0",
|
|
42
44
|
"eslint": "9.39.2",
|
|
43
|
-
"eslint-plugin-jsdoc": "62.
|
|
45
|
+
"eslint-plugin-jsdoc": "62.8.0",
|
|
44
46
|
"typescript": "5.9.3",
|
|
45
|
-
"vitest": "4.0
|
|
47
|
+
"vitest": "4.1.0"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/src/ast.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export type SqlPrimitive =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| bigint
|
|
5
|
+
| boolean
|
|
6
|
+
| Date
|
|
7
|
+
| null
|
|
8
|
+
| SqlPrimitive[]
|
|
9
|
+
| Record<string, any>
|
|
10
|
+
|
|
11
|
+
export interface SelectStatement {
|
|
12
|
+
with?: WithClause
|
|
13
|
+
distinct: boolean
|
|
14
|
+
columns: SelectColumn[]
|
|
15
|
+
from: FromTable | FromSubquery
|
|
16
|
+
joins: JoinClause[]
|
|
17
|
+
where?: ExprNode
|
|
18
|
+
groupBy: ExprNode[]
|
|
19
|
+
having?: ExprNode
|
|
20
|
+
orderBy: OrderByItem[]
|
|
21
|
+
limit?: number
|
|
22
|
+
offset?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WithClause {
|
|
26
|
+
ctes: CTEDefinition[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CTEDefinition {
|
|
30
|
+
name: string
|
|
31
|
+
query: SelectStatement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FromTable extends AstBase {
|
|
35
|
+
kind: 'table'
|
|
36
|
+
table: string
|
|
37
|
+
alias?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FromSubquery {
|
|
41
|
+
kind: 'subquery'
|
|
42
|
+
query: SelectStatement
|
|
43
|
+
alias: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
47
|
+
|
|
48
|
+
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
49
|
+
|
|
50
|
+
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
51
|
+
|
|
52
|
+
export interface LiteralNode extends AstBase {
|
|
53
|
+
type: 'literal'
|
|
54
|
+
value: SqlPrimitive
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IdentifierNode extends AstBase {
|
|
58
|
+
type: 'identifier'
|
|
59
|
+
name: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UnaryNode extends AstBase {
|
|
63
|
+
type: 'unary'
|
|
64
|
+
op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
|
|
65
|
+
argument: ExprNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BinaryNode extends AstBase {
|
|
69
|
+
type: 'binary'
|
|
70
|
+
op: BinaryOp
|
|
71
|
+
left: ExprNode
|
|
72
|
+
right: ExprNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FunctionNode extends AstBase {
|
|
76
|
+
type: 'function'
|
|
77
|
+
funcName: string
|
|
78
|
+
args: ExprNode[]
|
|
79
|
+
distinct?: boolean
|
|
80
|
+
filter?: ExprNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL'
|
|
84
|
+
|
|
85
|
+
export interface CastNode extends AstBase {
|
|
86
|
+
type: 'cast'
|
|
87
|
+
expr: ExprNode
|
|
88
|
+
toType: CastType
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface InSubqueryNode extends AstBase {
|
|
92
|
+
type: 'in'
|
|
93
|
+
expr: ExprNode
|
|
94
|
+
subquery: SelectStatement
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface InValuesNode extends AstBase {
|
|
98
|
+
type: 'in valuelist'
|
|
99
|
+
expr: ExprNode
|
|
100
|
+
values: ExprNode[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ExistsNode extends AstBase {
|
|
104
|
+
type: 'exists' | 'not exists'
|
|
105
|
+
subquery: SelectStatement
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WhenClause extends AstBase {
|
|
109
|
+
condition: ExprNode
|
|
110
|
+
result: ExprNode
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CaseNode extends AstBase {
|
|
114
|
+
type: 'case'
|
|
115
|
+
caseExpr?: ExprNode
|
|
116
|
+
whenClauses: WhenClause[]
|
|
117
|
+
elseResult?: ExprNode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SubqueryNode extends AstBase {
|
|
121
|
+
type: 'subquery'
|
|
122
|
+
subquery: SelectStatement
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
|
|
126
|
+
|
|
127
|
+
export interface IntervalNode extends AstBase {
|
|
128
|
+
type: 'interval'
|
|
129
|
+
value: number
|
|
130
|
+
unit: IntervalUnit
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface StarNode extends AstBase {
|
|
134
|
+
type: 'star'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type ExprNode =
|
|
138
|
+
| LiteralNode
|
|
139
|
+
| IdentifierNode
|
|
140
|
+
| UnaryNode
|
|
141
|
+
| BinaryNode
|
|
142
|
+
| FunctionNode
|
|
143
|
+
| CastNode
|
|
144
|
+
| InSubqueryNode
|
|
145
|
+
| InValuesNode
|
|
146
|
+
| ExistsNode
|
|
147
|
+
| CaseNode
|
|
148
|
+
| SubqueryNode
|
|
149
|
+
| IntervalNode
|
|
150
|
+
| StarNode
|
|
151
|
+
|
|
152
|
+
export interface StarColumn {
|
|
153
|
+
kind: 'star'
|
|
154
|
+
table?: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface DerivedColumn {
|
|
158
|
+
kind: 'derived'
|
|
159
|
+
expr: ExprNode
|
|
160
|
+
alias?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type SelectColumn = StarColumn | DerivedColumn
|
|
164
|
+
|
|
165
|
+
export interface OrderByItem {
|
|
166
|
+
expr: ExprNode
|
|
167
|
+
direction: 'ASC' | 'DESC'
|
|
168
|
+
nulls?: 'FIRST' | 'LAST'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'POSITIONAL'
|
|
172
|
+
|
|
173
|
+
export interface JoinClause extends AstBase {
|
|
174
|
+
joinType: JoinType
|
|
175
|
+
table: string
|
|
176
|
+
alias?: string
|
|
177
|
+
on?: ExprNode
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// All AST node derive from this base, which includes position info for error reporting and other purposes
|
|
181
|
+
interface AstBase {
|
|
182
|
+
positionStart: number // start position in query (0-based, inclusive)
|
|
183
|
+
positionEnd: number // end position in query (0-based, exclusive)
|
|
184
|
+
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { memorySource } from '../backend/dataSource.js'
|
|
2
|
-
import { tableNotFoundError } from '../executionErrors.js'
|
|
3
2
|
import { derivedAlias } from '../expression/alias.js'
|
|
4
3
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
5
4
|
import { parseSql } from '../parse/parse.js'
|
|
6
5
|
import { planSql } from '../plan/plan.js'
|
|
6
|
+
import { tableNotFoundError } from '../validation/planErrors.js'
|
|
7
7
|
import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
8
8
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
9
9
|
import { executeSort } from './sort.js'
|
|
@@ -46,7 +46,7 @@ export async function* executeSql({ tables, query, functions, signal }) {
|
|
|
46
46
|
* @yields {AsyncRow}
|
|
47
47
|
*/
|
|
48
48
|
export async function* executeSelect({ select, context }) {
|
|
49
|
-
const plan = planSql({ query: select, functions: context.functions })
|
|
49
|
+
const plan = planSql({ query: select, functions: context.functions, tables: context.tables })
|
|
50
50
|
yield* executePlan({ plan, context })
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -98,7 +98,7 @@ async function* executeScan(plan, context) {
|
|
|
98
98
|
// check table
|
|
99
99
|
const table = tables[plan.table]
|
|
100
100
|
if (!table) {
|
|
101
|
-
throw tableNotFoundError({
|
|
101
|
+
throw tableNotFoundError({ table: plan.table, tables })
|
|
102
102
|
}
|
|
103
103
|
// check columns
|
|
104
104
|
const missingColumn = plan.hints.columns?.find(col => !table.columns.includes(col))
|
|
@@ -119,7 +119,7 @@ async function* executeScan(plan, context) {
|
|
|
119
119
|
|
|
120
120
|
// Apply WHERE if data source did not
|
|
121
121
|
if (!appliedWhere && plan.hints.where) {
|
|
122
|
-
result = filterRows(result, plan.hints.where, context)
|
|
122
|
+
result = filterRows(result, plan.hints.where, context, plan.hints.limit)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Apply LIMIT/OFFSET if data source did not
|
|
@@ -140,7 +140,7 @@ async function* executeScan(plan, context) {
|
|
|
140
140
|
async function* executeCount(plan, { tables, signal }) {
|
|
141
141
|
const table = tables[plan.table]
|
|
142
142
|
if (!table) {
|
|
143
|
-
throw tableNotFoundError({
|
|
143
|
+
throw tableNotFoundError({ table: plan.table, tables })
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Use source numRows if available
|
|
@@ -174,15 +174,42 @@ async function* executeCount(plan, { tables, signal }) {
|
|
|
174
174
|
* @param {AsyncIterable<AsyncRow>} rows
|
|
175
175
|
* @param {ExprNode} condition
|
|
176
176
|
* @param {ExecuteContext} context
|
|
177
|
+
* @param {number} [limit] - downstream LIMIT hint for chunk sizing
|
|
177
178
|
* @yields {AsyncRow}
|
|
178
179
|
*/
|
|
179
|
-
async function* filterRows(rows, condition, context) {
|
|
180
|
+
async function* filterRows(rows, condition, context, limit) {
|
|
181
|
+
const MAX_CHUNK = 256
|
|
182
|
+
let chunkSize = limit ?? Infinity
|
|
180
183
|
let rowIndex = 0
|
|
184
|
+
|
|
185
|
+
/** @type {{ row: AsyncRow, rowIndex: number }[]} */
|
|
186
|
+
let buffer = []
|
|
187
|
+
|
|
181
188
|
for await (const row of rows) {
|
|
182
189
|
if (context.signal?.aborted) return
|
|
183
190
|
rowIndex++
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
buffer.push({ row, rowIndex })
|
|
192
|
+
|
|
193
|
+
if (buffer.length >= chunkSize) {
|
|
194
|
+
const results = await Promise.all(buffer.map(b =>
|
|
195
|
+
evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
|
|
196
|
+
))
|
|
197
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
198
|
+
if (results[i]) yield buffer[i].row
|
|
199
|
+
}
|
|
200
|
+
buffer = []
|
|
201
|
+
chunkSize = Math.min(chunkSize * 2, MAX_CHUNK)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Flush remaining rows
|
|
206
|
+
if (buffer.length > 0) {
|
|
207
|
+
const results = await Promise.all(buffer.map(b =>
|
|
208
|
+
evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
|
|
209
|
+
))
|
|
210
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
211
|
+
if (results[i]) yield buffer[i].row
|
|
212
|
+
}
|
|
186
213
|
}
|
|
187
214
|
}
|
|
188
215
|
|
|
@@ -275,17 +302,38 @@ async function* executeProject(plan, context) {
|
|
|
275
302
|
*/
|
|
276
303
|
async function* executeDistinct(plan, context) {
|
|
277
304
|
const { signal } = context
|
|
305
|
+
const MAX_CHUNK = 256
|
|
278
306
|
|
|
279
307
|
/** @type {Set<string>} */
|
|
280
308
|
const seen = new Set()
|
|
281
309
|
|
|
310
|
+
/** @type {AsyncRow[]} */
|
|
311
|
+
let buffer = []
|
|
312
|
+
|
|
282
313
|
for await (const row of executePlan({ plan: plan.child, context })) {
|
|
283
314
|
if (signal?.aborted) return
|
|
315
|
+
buffer.push(row)
|
|
284
316
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
if (buffer.length >= MAX_CHUNK) {
|
|
318
|
+
const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
|
|
319
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
320
|
+
if (!seen.has(keys[i])) {
|
|
321
|
+
seen.add(keys[i])
|
|
322
|
+
yield buffer[i]
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
buffer = []
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Flush remaining
|
|
330
|
+
if (buffer.length > 0) {
|
|
331
|
+
const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
|
|
332
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
333
|
+
if (!seen.has(keys[i])) {
|
|
334
|
+
seen.add(keys[i])
|
|
335
|
+
yield buffer[i]
|
|
336
|
+
}
|
|
289
337
|
}
|
|
290
338
|
}
|
|
291
339
|
}
|
package/src/execute/utils.js
CHANGED
|
@@ -44,17 +44,22 @@ export function compareForTerm(a, b, term) {
|
|
|
44
44
|
* @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
|
|
45
45
|
*/
|
|
46
46
|
export async function collect(asyncRows) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Collect all rows first, then materialize cells concurrently
|
|
48
|
+
// This enables dataloader-style batching of cell accessors
|
|
49
|
+
/** @type {AsyncRow[]} */
|
|
50
|
+
const rows = []
|
|
49
51
|
for await (const asyncRow of asyncRows) {
|
|
52
|
+
rows.push(asyncRow)
|
|
53
|
+
}
|
|
54
|
+
return Promise.all(rows.map(async asyncRow => {
|
|
55
|
+
const values = await Promise.all(asyncRow.columns.map(k => asyncRow.cells[k]()))
|
|
50
56
|
/** @type {Record<string, SqlPrimitive>} */
|
|
51
57
|
const item = {}
|
|
52
|
-
for (
|
|
53
|
-
item[
|
|
58
|
+
for (let i = 0; i < asyncRow.columns.length; i++) {
|
|
59
|
+
item[asyncRow.columns[i]] = values[i]
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return results
|
|
61
|
+
return item
|
|
62
|
+
}))
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
@@ -79,11 +84,6 @@ export function stringify(value) {
|
|
|
79
84
|
*/
|
|
80
85
|
export async function stableRowKey(cells) {
|
|
81
86
|
const keys = Object.keys(cells).sort()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const k of keys) {
|
|
85
|
-
const v = await cells[k]()
|
|
86
|
-
parts.push(k + ':' + stringify(v))
|
|
87
|
-
}
|
|
88
|
-
return parts.join('|')
|
|
87
|
+
const values = await Promise.all(keys.map(k => cells[k]()))
|
|
88
|
+
return keys.map((k, i) => k + ':' + stringify(values[i])).join('|')
|
|
89
89
|
}
|
package/src/expression/alias.js
CHANGED
|
@@ -31,9 +31,9 @@ export function derivedAlias(expr) {
|
|
|
31
31
|
if (expr.type === 'function') {
|
|
32
32
|
// Handle aggregate functions with star (COUNT(*) -> count_all)
|
|
33
33
|
if (expr.args.length === 1 && expr.args[0].type === 'star') {
|
|
34
|
-
return expr.
|
|
34
|
+
return expr.funcName.toLowerCase() + '_all'
|
|
35
35
|
}
|
|
36
|
-
return expr.
|
|
36
|
+
return expr.funcName.toLowerCase() + '_' + expr.args.map(derivedAlias).join('_')
|
|
37
37
|
}
|
|
38
38
|
if (expr.type === 'interval') {
|
|
39
39
|
return `interval_${expr.value}_${expr.unit.toLowerCase()}`
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { executeSelect } from '../execute/execute.js'
|
|
2
2
|
import { stringify } from '../execute/utils.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation.js'
|
|
6
|
-
import {
|
|
3
|
+
import { invalidContextError } from '../validation/executionErrors.js'
|
|
4
|
+
import { aggregateError, argValueError, castError } from '../validation/expressionErrors.js'
|
|
5
|
+
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
6
|
+
import { unknownFunctionError } from '../validation/parseErrors.js'
|
|
7
|
+
import { columnNotFoundError } from '../validation/planErrors.js'
|
|
7
8
|
import { derivedAlias } from './alias.js'
|
|
8
9
|
import { applyBinaryOp } from './binary.js'
|
|
9
10
|
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
|
@@ -99,7 +100,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
99
100
|
|
|
100
101
|
// Function calls
|
|
101
102
|
if (node.type === 'function') {
|
|
102
|
-
const funcName = node.
|
|
103
|
+
const funcName = node.funcName.toUpperCase()
|
|
103
104
|
|
|
104
105
|
// Handle aggregate functions
|
|
105
106
|
if (isAggregateFunc(funcName)) {
|
|
@@ -110,22 +111,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
110
111
|
if (row.columns.includes(alias)) {
|
|
111
112
|
return row.cells[alias]()
|
|
112
113
|
} else {
|
|
113
|
-
throw aggregateError(
|
|
114
|
-
funcName,
|
|
115
|
-
positionStart: node.positionStart,
|
|
116
|
-
positionEnd: node.positionEnd,
|
|
117
|
-
})
|
|
114
|
+
throw aggregateError(node)
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
// Apply FILTER clause if present
|
|
122
119
|
let filteredRows = rows
|
|
123
120
|
if (node.filter) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
121
|
+
const passes = await Promise.all(rows.map(row =>
|
|
122
|
+
evaluateExpr({ node: node.filter, row, context })
|
|
123
|
+
))
|
|
124
|
+
filteredRows = rows.filter((_, i) => passes[i])
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
const argNode = node.args[0]
|
|
@@ -135,23 +131,27 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
135
131
|
return filteredRows.length
|
|
136
132
|
}
|
|
137
133
|
|
|
134
|
+
const values = await Promise.all(filteredRows.map(row =>
|
|
135
|
+
evaluateExpr({ node: argNode, row, context })
|
|
136
|
+
))
|
|
138
137
|
if (node.distinct) {
|
|
139
138
|
const seen = new Set()
|
|
140
|
-
for (const
|
|
141
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
139
|
+
for (const v of values) {
|
|
142
140
|
if (v != null) seen.add(v)
|
|
143
141
|
}
|
|
144
142
|
return seen.size
|
|
145
143
|
}
|
|
146
144
|
let count = 0
|
|
147
|
-
for (const
|
|
148
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
145
|
+
for (const v of values) {
|
|
149
146
|
if (v != null) count++
|
|
150
147
|
}
|
|
151
148
|
return count
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
|
|
152
|
+
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
153
|
+
evaluateExpr({ node: argNode, row, context })
|
|
154
|
+
))
|
|
155
155
|
let sum = 0
|
|
156
156
|
let count = 0
|
|
157
157
|
/** @type {number | null} */
|
|
@@ -159,8 +159,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
159
159
|
/** @type {number | null} */
|
|
160
160
|
let max = null
|
|
161
161
|
|
|
162
|
-
for (const
|
|
163
|
-
const raw = await evaluateExpr({ node: argNode, row, context })
|
|
162
|
+
for (const raw of rawValues) {
|
|
164
163
|
if (raw == null) continue
|
|
165
164
|
const num = Number(raw)
|
|
166
165
|
if (!Number.isFinite(num)) continue
|
|
@@ -183,9 +182,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
185
|
+
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
186
|
+
evaluateExpr({ node: argNode, row, context })
|
|
187
|
+
))
|
|
188
|
+
/** @type {number[]} */
|
|
186
189
|
const values = []
|
|
187
|
-
for (const
|
|
188
|
-
const raw = await evaluateExpr({ node: argNode, row, context })
|
|
190
|
+
for (const raw of rawValues) {
|
|
189
191
|
if (raw == null) continue
|
|
190
192
|
const num = Number(raw)
|
|
191
193
|
if (!Number.isFinite(num)) continue
|
|
@@ -230,23 +232,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
230
232
|
: await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
|
|
231
233
|
|
|
232
234
|
if (isStringFunc(funcName)) {
|
|
233
|
-
return evaluateStringFunc({
|
|
234
|
-
funcName,
|
|
235
|
-
args,
|
|
236
|
-
positionStart: node.positionStart,
|
|
237
|
-
positionEnd: node.positionEnd,
|
|
238
|
-
rowIndex,
|
|
239
|
-
})
|
|
235
|
+
return evaluateStringFunc({ funcName, node, args, rowIndex })
|
|
240
236
|
}
|
|
241
237
|
|
|
242
238
|
if (isRegexpFunc(funcName)) {
|
|
243
|
-
return evaluateRegexpFunc({
|
|
244
|
-
funcName,
|
|
245
|
-
args,
|
|
246
|
-
positionStart: node.positionStart,
|
|
247
|
-
positionEnd: node.positionEnd,
|
|
248
|
-
rowIndex,
|
|
249
|
-
})
|
|
239
|
+
return evaluateRegexpFunc({ funcName, node, args, rowIndex })
|
|
250
240
|
}
|
|
251
241
|
|
|
252
242
|
if (isMathFunc(funcName)) {
|
|
@@ -296,10 +286,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
296
286
|
if (funcName === 'JSON_OBJECT') {
|
|
297
287
|
if (args.length % 2 !== 0) {
|
|
298
288
|
throw argValueError({
|
|
299
|
-
|
|
289
|
+
...node,
|
|
300
290
|
message: 'requires an even number of arguments (key-value pairs)',
|
|
301
|
-
positionStart: node.positionStart,
|
|
302
|
-
positionEnd: node.positionEnd,
|
|
303
291
|
rowIndex,
|
|
304
292
|
})
|
|
305
293
|
}
|
|
@@ -310,10 +298,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
310
298
|
const value = args[i + 1]
|
|
311
299
|
if (key == null) {
|
|
312
300
|
throw argValueError({
|
|
313
|
-
|
|
301
|
+
...node,
|
|
314
302
|
message: 'key cannot be null',
|
|
315
|
-
positionStart: node.positionStart,
|
|
316
|
-
positionEnd: node.positionEnd,
|
|
317
303
|
hint: 'All keys must be non-null values.',
|
|
318
304
|
rowIndex,
|
|
319
305
|
})
|
|
@@ -360,10 +346,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
360
346
|
jsonArg = JSON.parse(jsonArg)
|
|
361
347
|
} catch {
|
|
362
348
|
throw argValueError({
|
|
363
|
-
|
|
349
|
+
...node,
|
|
364
350
|
message: 'invalid JSON string',
|
|
365
|
-
positionStart: node.positionStart,
|
|
366
|
-
positionEnd: node.positionEnd,
|
|
367
351
|
hint: 'First argument must be valid JSON.',
|
|
368
352
|
rowIndex,
|
|
369
353
|
})
|
|
@@ -371,10 +355,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
371
355
|
}
|
|
372
356
|
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
373
357
|
throw argValueError({
|
|
374
|
-
|
|
358
|
+
...node,
|
|
375
359
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
376
|
-
positionStart: node.positionStart,
|
|
377
|
-
positionEnd: node.positionEnd,
|
|
378
360
|
rowIndex,
|
|
379
361
|
})
|
|
380
362
|
}
|
|
@@ -414,30 +396,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
414
396
|
}
|
|
415
397
|
}
|
|
416
398
|
|
|
417
|
-
throw unknownFunctionError(
|
|
418
|
-
funcName,
|
|
419
|
-
positionStart: node.positionStart,
|
|
420
|
-
positionEnd: node.positionEnd,
|
|
421
|
-
})
|
|
399
|
+
throw unknownFunctionError(node)
|
|
422
400
|
}
|
|
423
401
|
|
|
424
402
|
if (node.type === 'cast') {
|
|
425
403
|
const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
426
404
|
if (val == null) return null
|
|
427
|
-
const toType = node
|
|
405
|
+
const { toType } = node
|
|
428
406
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
429
407
|
if (typeof val === 'object') return stringify(val)
|
|
430
408
|
return String(val)
|
|
431
409
|
}
|
|
432
410
|
// Can only cast primitives to other primitive types
|
|
433
411
|
if (typeof val === 'object') {
|
|
434
|
-
throw castError({
|
|
435
|
-
toType: node.toType,
|
|
436
|
-
positionStart: node.positionStart,
|
|
437
|
-
positionEnd: node.positionEnd,
|
|
438
|
-
fromType: 'object',
|
|
439
|
-
rowIndex,
|
|
440
|
-
})
|
|
412
|
+
throw castError({ ...node, fromType: 'object', rowIndex })
|
|
441
413
|
}
|
|
442
414
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
443
415
|
const num = Number(val)
|
|
@@ -455,12 +427,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
455
427
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
456
428
|
return Boolean(val)
|
|
457
429
|
}
|
|
458
|
-
throw castError({
|
|
459
|
-
toType: node.toType,
|
|
460
|
-
positionStart: node.positionStart,
|
|
461
|
-
positionEnd: node.positionEnd,
|
|
462
|
-
rowIndex,
|
|
463
|
-
})
|
|
430
|
+
throw castError({ ...node, rowIndex })
|
|
464
431
|
}
|
|
465
432
|
|
|
466
433
|
// IN and NOT IN with value lists
|