squirreling 0.10.0 → 0.10.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/package.json +5 -3
- package/src/ast.d.ts +184 -0
- package/src/execute/execute.js +4 -4
- package/src/expression/alias.js +2 -2
- package/src/expression/evaluate.js +17 -55
- 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.1",
|
|
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,7 +39,7 @@
|
|
|
37
39
|
"test": "vitest run"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.
|
|
42
|
+
"@types/node": "25.4.0",
|
|
41
43
|
"@vitest/coverage-v8": "4.0.18",
|
|
42
44
|
"eslint": "9.39.2",
|
|
43
45
|
"eslint-plugin-jsdoc": "62.7.1",
|
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))
|
|
@@ -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
|
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,11 +111,7 @@ 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
|
|
|
@@ -230,23 +227,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
230
227
|
: await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
|
|
231
228
|
|
|
232
229
|
if (isStringFunc(funcName)) {
|
|
233
|
-
return evaluateStringFunc({
|
|
234
|
-
funcName,
|
|
235
|
-
args,
|
|
236
|
-
positionStart: node.positionStart,
|
|
237
|
-
positionEnd: node.positionEnd,
|
|
238
|
-
rowIndex,
|
|
239
|
-
})
|
|
230
|
+
return evaluateStringFunc({ funcName, node, args, rowIndex })
|
|
240
231
|
}
|
|
241
232
|
|
|
242
233
|
if (isRegexpFunc(funcName)) {
|
|
243
|
-
return evaluateRegexpFunc({
|
|
244
|
-
funcName,
|
|
245
|
-
args,
|
|
246
|
-
positionStart: node.positionStart,
|
|
247
|
-
positionEnd: node.positionEnd,
|
|
248
|
-
rowIndex,
|
|
249
|
-
})
|
|
234
|
+
return evaluateRegexpFunc({ funcName, node, args, rowIndex })
|
|
250
235
|
}
|
|
251
236
|
|
|
252
237
|
if (isMathFunc(funcName)) {
|
|
@@ -296,10 +281,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
296
281
|
if (funcName === 'JSON_OBJECT') {
|
|
297
282
|
if (args.length % 2 !== 0) {
|
|
298
283
|
throw argValueError({
|
|
299
|
-
|
|
284
|
+
...node,
|
|
300
285
|
message: 'requires an even number of arguments (key-value pairs)',
|
|
301
|
-
positionStart: node.positionStart,
|
|
302
|
-
positionEnd: node.positionEnd,
|
|
303
286
|
rowIndex,
|
|
304
287
|
})
|
|
305
288
|
}
|
|
@@ -310,10 +293,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
310
293
|
const value = args[i + 1]
|
|
311
294
|
if (key == null) {
|
|
312
295
|
throw argValueError({
|
|
313
|
-
|
|
296
|
+
...node,
|
|
314
297
|
message: 'key cannot be null',
|
|
315
|
-
positionStart: node.positionStart,
|
|
316
|
-
positionEnd: node.positionEnd,
|
|
317
298
|
hint: 'All keys must be non-null values.',
|
|
318
299
|
rowIndex,
|
|
319
300
|
})
|
|
@@ -360,10 +341,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
360
341
|
jsonArg = JSON.parse(jsonArg)
|
|
361
342
|
} catch {
|
|
362
343
|
throw argValueError({
|
|
363
|
-
|
|
344
|
+
...node,
|
|
364
345
|
message: 'invalid JSON string',
|
|
365
|
-
positionStart: node.positionStart,
|
|
366
|
-
positionEnd: node.positionEnd,
|
|
367
346
|
hint: 'First argument must be valid JSON.',
|
|
368
347
|
rowIndex,
|
|
369
348
|
})
|
|
@@ -371,10 +350,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
371
350
|
}
|
|
372
351
|
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
373
352
|
throw argValueError({
|
|
374
|
-
|
|
353
|
+
...node,
|
|
375
354
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
376
|
-
positionStart: node.positionStart,
|
|
377
|
-
positionEnd: node.positionEnd,
|
|
378
355
|
rowIndex,
|
|
379
356
|
})
|
|
380
357
|
}
|
|
@@ -414,30 +391,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
414
391
|
}
|
|
415
392
|
}
|
|
416
393
|
|
|
417
|
-
throw unknownFunctionError(
|
|
418
|
-
funcName,
|
|
419
|
-
positionStart: node.positionStart,
|
|
420
|
-
positionEnd: node.positionEnd,
|
|
421
|
-
})
|
|
394
|
+
throw unknownFunctionError(node)
|
|
422
395
|
}
|
|
423
396
|
|
|
424
397
|
if (node.type === 'cast') {
|
|
425
398
|
const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
426
399
|
if (val == null) return null
|
|
427
|
-
const toType = node
|
|
400
|
+
const { toType } = node
|
|
428
401
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
429
402
|
if (typeof val === 'object') return stringify(val)
|
|
430
403
|
return String(val)
|
|
431
404
|
}
|
|
432
405
|
// Can only cast primitives to other primitive types
|
|
433
406
|
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
|
-
})
|
|
407
|
+
throw castError({ ...node, fromType: 'object', rowIndex })
|
|
441
408
|
}
|
|
442
409
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
443
410
|
const num = Number(val)
|
|
@@ -455,12 +422,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
455
422
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
456
423
|
return Boolean(val)
|
|
457
424
|
}
|
|
458
|
-
throw castError({
|
|
459
|
-
toType: node.toType,
|
|
460
|
-
positionStart: node.positionStart,
|
|
461
|
-
positionEnd: node.positionEnd,
|
|
462
|
-
rowIndex,
|
|
463
|
-
})
|
|
425
|
+
throw castError({ ...node, rowIndex })
|
|
464
426
|
}
|
|
465
427
|
|
|
466
428
|
// IN and NOT IN with value lists
|
package/src/expression/regexp.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { argValueError } from '../
|
|
1
|
+
import { argValueError } from '../validation/expressionErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @import { SqlPrimitive } from '../types.js'
|
|
4
|
+
* @import { FunctionNode, RegExpFunction, SqlPrimitive } from '../types.js'
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Evaluate a regexp function
|
|
9
9
|
*
|
|
10
10
|
* @param {Object} options
|
|
11
|
-
* @param {
|
|
11
|
+
* @param {RegExpFunction} options.funcName
|
|
12
|
+
* @param {FunctionNode} options.node
|
|
12
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
13
|
-
* @param {number} options.
|
|
14
|
-
* @param {number} options.positionEnd - End position in SQL string for error reporting
|
|
15
|
-
* @param {number} [options.rowIndex] - Row number for error reporting
|
|
14
|
+
* @param {number} options.rowIndex - Row index for error reporting
|
|
16
15
|
* @returns {SqlPrimitive}
|
|
17
16
|
*/
|
|
18
|
-
export function evaluateRegexpFunc({ funcName,
|
|
17
|
+
export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
19
18
|
if (funcName === 'REGEXP_SUBSTR') {
|
|
20
19
|
const str = args[0]
|
|
21
20
|
const pattern = args[1]
|
|
@@ -29,10 +28,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
29
28
|
position = Number(args[2])
|
|
30
29
|
if (!Number.isInteger(position) || position < 1) {
|
|
31
30
|
throw argValueError({
|
|
32
|
-
|
|
31
|
+
...node,
|
|
33
32
|
message: `position must be a positive integer, got ${args[2]}`,
|
|
34
|
-
positionStart,
|
|
35
|
-
positionEnd,
|
|
36
33
|
hint: 'SQL uses 1-based indexing.',
|
|
37
34
|
rowIndex,
|
|
38
35
|
})
|
|
@@ -45,10 +42,9 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
45
42
|
occurrence = Number(args[3])
|
|
46
43
|
if (!Number.isInteger(occurrence) || occurrence < 1) {
|
|
47
44
|
throw argValueError({
|
|
48
|
-
|
|
45
|
+
...node,
|
|
49
46
|
message: `occurrence must be a positive integer, got ${args[3]}`,
|
|
50
|
-
|
|
51
|
-
positionEnd,
|
|
47
|
+
hint: 'SQL uses 1-based indexing.',
|
|
52
48
|
rowIndex,
|
|
53
49
|
})
|
|
54
50
|
}
|
|
@@ -60,10 +56,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
60
56
|
regex = new RegExp(patternStr, 'g')
|
|
61
57
|
} catch (/** @type {any} */ error) {
|
|
62
58
|
throw argValueError({
|
|
63
|
-
|
|
59
|
+
...node,
|
|
64
60
|
message: `invalid regex pattern: ${error.message}`,
|
|
65
|
-
positionStart,
|
|
66
|
-
positionEnd,
|
|
67
61
|
rowIndex,
|
|
68
62
|
})
|
|
69
63
|
}
|
|
@@ -99,10 +93,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
99
93
|
position = Number(args[3])
|
|
100
94
|
if (!Number.isInteger(position) || position < 1) {
|
|
101
95
|
throw argValueError({
|
|
102
|
-
|
|
96
|
+
...node,
|
|
103
97
|
message: `position must be a positive integer, got ${args[3]}`,
|
|
104
|
-
positionStart,
|
|
105
|
-
positionEnd,
|
|
106
98
|
hint: 'SQL uses 1-based indexing.',
|
|
107
99
|
rowIndex,
|
|
108
100
|
})
|
|
@@ -115,10 +107,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
115
107
|
occurrence = Number(args[4])
|
|
116
108
|
if (!Number.isInteger(occurrence) || occurrence < 0) {
|
|
117
109
|
throw argValueError({
|
|
118
|
-
|
|
110
|
+
...node,
|
|
119
111
|
message: `occurrence must be a non-negative integer, got ${args[4]}`,
|
|
120
|
-
positionStart,
|
|
121
|
-
positionEnd,
|
|
122
112
|
hint: 'Use 0 to replace all occurrences.',
|
|
123
113
|
rowIndex,
|
|
124
114
|
})
|
|
@@ -131,10 +121,8 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
131
121
|
regex = new RegExp(patternStr, 'g')
|
|
132
122
|
} catch (/** @type {any} */ error) {
|
|
133
123
|
throw argValueError({
|
|
134
|
-
|
|
124
|
+
...node,
|
|
135
125
|
message: `invalid regex pattern: ${error.message}`,
|
|
136
|
-
positionStart,
|
|
137
|
-
positionEnd,
|
|
138
126
|
rowIndex,
|
|
139
127
|
})
|
|
140
128
|
}
|
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { SqlPrimitive, StringFunc } from '../types.js'
|
|
2
|
+
* @import { FunctionNode, SqlPrimitive, StringFunc } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { argValueError } from '../
|
|
5
|
+
import { argValueError } from '../validation/expressionErrors.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Evaluate a string function
|
|
9
9
|
*
|
|
10
10
|
* @param {Object} options
|
|
11
|
-
* @param {StringFunc} options.funcName
|
|
11
|
+
* @param {StringFunc} options.funcName
|
|
12
|
+
* @param {FunctionNode} options.node
|
|
12
13
|
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
13
|
-
* @param {number} options.
|
|
14
|
-
* @param {number} options.positionEnd - End position for error reporting
|
|
15
|
-
* @param {number} [options.rowIndex] - Row index for error reporting
|
|
14
|
+
* @param {number} options.rowIndex - Row index for error reporting
|
|
16
15
|
* @returns {SqlPrimitive}
|
|
17
16
|
*/
|
|
18
|
-
export function evaluateStringFunc({ funcName,
|
|
17
|
+
export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
19
18
|
if (funcName === 'CONCAT') {
|
|
20
19
|
// Returns NULL if any argument is NULL
|
|
21
20
|
if (args.some(a => a == null)) return null
|
|
22
21
|
if (args.some(a => typeof a === 'object')) {
|
|
23
22
|
throw argValueError({
|
|
24
|
-
|
|
23
|
+
...node,
|
|
25
24
|
message: 'does not support object arguments',
|
|
26
|
-
positionStart,
|
|
27
|
-
positionEnd,
|
|
28
25
|
hint: 'Use CAST to convert objects to strings first.',
|
|
29
26
|
rowIndex,
|
|
30
27
|
})
|
|
@@ -53,10 +50,8 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
53
50
|
const start = Number(args[1])
|
|
54
51
|
if (!Number.isInteger(start) || start < 1) {
|
|
55
52
|
throw argValueError({
|
|
56
|
-
|
|
53
|
+
...node,
|
|
57
54
|
message: `start position must be a positive integer, got ${args[1]}`,
|
|
58
|
-
positionStart,
|
|
59
|
-
positionEnd,
|
|
60
55
|
hint: 'SQL uses 1-based indexing.',
|
|
61
56
|
rowIndex,
|
|
62
57
|
})
|
|
@@ -67,10 +62,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
67
62
|
const len = Number(args[2])
|
|
68
63
|
if (!Number.isInteger(len) || len < 0) {
|
|
69
64
|
throw argValueError({
|
|
70
|
-
|
|
65
|
+
...node,
|
|
71
66
|
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
72
|
-
|
|
73
|
-
positionEnd,
|
|
67
|
+
hint: 'SQL uses 1-based indexing.',
|
|
74
68
|
rowIndex,
|
|
75
69
|
})
|
|
76
70
|
}
|
|
@@ -97,10 +91,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
97
91
|
const len = Number(n)
|
|
98
92
|
if (!Number.isInteger(len) || len < 0) {
|
|
99
93
|
throw argValueError({
|
|
100
|
-
|
|
94
|
+
...node,
|
|
101
95
|
message: `length must be a non-negative integer, got ${n}`,
|
|
102
|
-
|
|
103
|
-
positionEnd,
|
|
96
|
+
hint: 'SQL uses 1-based indexing.',
|
|
104
97
|
rowIndex,
|
|
105
98
|
})
|
|
106
99
|
}
|
|
@@ -113,10 +106,9 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
113
106
|
const len = Number(n)
|
|
114
107
|
if (!Number.isInteger(len) || len < 0) {
|
|
115
108
|
throw argValueError({
|
|
116
|
-
|
|
109
|
+
...node,
|
|
117
110
|
message: `length must be a non-negative integer, got ${n}`,
|
|
118
|
-
|
|
119
|
-
positionEnd,
|
|
111
|
+
hint: 'SQL uses 1-based indexing.',
|
|
120
112
|
rowIndex,
|
|
121
113
|
})
|
|
122
114
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function parseSql(options: ParseSqlOptions): SelectStatement
|
|
|
55
55
|
* @param options
|
|
56
56
|
* @param options.query - SQL query string or parsed SelectStatement
|
|
57
57
|
* @param options.functions - user-defined functions available in the SQL context
|
|
58
|
+
* @param options.tables - optional table metadata for planning
|
|
58
59
|
* @returns the root of the query plan tree
|
|
59
60
|
*/
|
|
60
61
|
export function planSql(options: PlanSqlOptions): QueryPlan
|
package/src/parse/comparison.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isBinaryOp } from '../validation/functions.js'
|
|
2
|
+
import { syntaxError } from '../validation/parseErrors.js'
|
|
3
3
|
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
4
4
|
import { consume, current, expect, match, peekToken } from './state.js'
|
|
5
5
|
|