squirreling 0.11.4 → 0.11.5
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 +2 -2
- package/src/ast.d.ts +4 -4
- package/src/expression/binary.js +1 -1
- package/src/expression/evaluate.js +1 -1
- package/src/expression/math.js +1 -1
- package/src/parse/parse.js +9 -6
- package/src/parse/primary.js +12 -9
- package/src/parse/state.js +8 -1
- package/src/parse/tokenize.js +29 -27
- package/src/parse/types.d.ts +2 -2
- package/src/plan/columns.js +2 -2
- package/src/plan/plan.js +17 -3
- package/src/types.d.ts +1 -0
- package/src/validation/functions.js +3 -2
- package/src/validation/parseErrors.js +19 -2
- package/src/validation/tables.js +62 -10
- package/src/validation/planErrors.js +0 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@types/node": "25.5.0",
|
|
43
43
|
"@vitest/coverage-v8": "4.1.2",
|
|
44
44
|
"eslint": "9.39.2",
|
|
45
|
-
"eslint-plugin-jsdoc": "62.
|
|
45
|
+
"eslint-plugin-jsdoc": "62.9.0",
|
|
46
46
|
"typescript": "6.0.2",
|
|
47
47
|
"vitest": "4.1.2"
|
|
48
48
|
}
|
package/src/ast.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
|
64
64
|
|
|
65
65
|
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
66
66
|
|
|
67
|
-
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
67
|
+
export type ComparisonOp = '=' | '==' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
68
68
|
|
|
69
69
|
export interface LiteralNode extends AstBase {
|
|
70
70
|
type: 'literal'
|
|
@@ -167,12 +167,12 @@ export type ExprNode =
|
|
|
167
167
|
| IntervalNode
|
|
168
168
|
| StarNode
|
|
169
169
|
|
|
170
|
-
export interface StarColumn {
|
|
170
|
+
export interface StarColumn extends AstBase {
|
|
171
171
|
type: 'star'
|
|
172
172
|
table?: string
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
export interface DerivedColumn {
|
|
175
|
+
export interface DerivedColumn extends AstBase {
|
|
176
176
|
type: 'derived'
|
|
177
177
|
expr: ExprNode
|
|
178
178
|
alias?: string
|
|
@@ -180,7 +180,7 @@ export interface DerivedColumn {
|
|
|
180
180
|
|
|
181
181
|
export type SelectColumn = StarColumn | DerivedColumn
|
|
182
182
|
|
|
183
|
-
export interface OrderByItem {
|
|
183
|
+
export interface OrderByItem extends AstBase {
|
|
184
184
|
expr: ExprNode
|
|
185
185
|
direction: 'ASC' | 'DESC'
|
|
186
186
|
nulls?: 'FIRST' | 'LAST'
|
package/src/expression/binary.js
CHANGED
|
@@ -30,7 +30,7 @@ export function applyBinaryOp(op, a, b) {
|
|
|
30
30
|
if (op === 'AND') return Boolean(a) && Boolean(b)
|
|
31
31
|
if (op === 'OR') return Boolean(a) || Boolean(b)
|
|
32
32
|
if (op === '!=' || op === '<>') return a != b
|
|
33
|
-
if (op === '=') return a == b
|
|
33
|
+
if (op === '=' || op === '==') return a == b
|
|
34
34
|
if (op === '<') return a < b
|
|
35
35
|
if (op === '<=') return a <= b
|
|
36
36
|
if (op === '>') return a > b
|
|
@@ -3,7 +3,7 @@ import { keyify, stringify } from '../execute/utils.js'
|
|
|
3
3
|
import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
|
|
4
4
|
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
5
5
|
import { UnknownFunctionError } from '../validation/parseErrors.js'
|
|
6
|
-
import { ColumnNotFoundError } from '../validation/
|
|
6
|
+
import { ColumnNotFoundError } from '../validation/tables.js'
|
|
7
7
|
import { derivedAlias } from './alias.js'
|
|
8
8
|
import { applyBinaryOp } from './binary.js'
|
|
9
9
|
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
package/src/expression/math.js
CHANGED
|
@@ -27,7 +27,7 @@ export function evaluateMathFunc({ funcName, args }) {
|
|
|
27
27
|
return Number(dividend) % Number(divisor)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
if (funcName === 'POWER') {
|
|
30
|
+
if (funcName === 'POWER' || funcName === 'POW') {
|
|
31
31
|
const [base, exponent] = args
|
|
32
32
|
if (base == null || exponent == null) return null
|
|
33
33
|
return Number(base) ** Number(exponent)
|
package/src/parse/parse.js
CHANGED
|
@@ -175,7 +175,7 @@ function parseSelect(state) {
|
|
|
175
175
|
|
|
176
176
|
// Support duckdb-style shorthand "FROM table"
|
|
177
177
|
if (match(state, 'keyword', 'FROM')) {
|
|
178
|
-
columns = [{ type: 'star' }]
|
|
178
|
+
columns = [{ type: 'star', positionStart, positionEnd: positionStart }]
|
|
179
179
|
} else {
|
|
180
180
|
expect(state, 'keyword', 'SELECT')
|
|
181
181
|
distinct = match(state, 'keyword', 'DISTINCT')
|
|
@@ -283,6 +283,9 @@ function parseSelect(state) {
|
|
|
283
283
|
expr,
|
|
284
284
|
direction,
|
|
285
285
|
nulls,
|
|
286
|
+
positionStart,
|
|
287
|
+
positionEnd: state.lastPos,
|
|
288
|
+
|
|
286
289
|
})
|
|
287
290
|
if (!match(state, 'comma')) break
|
|
288
291
|
}
|
|
@@ -339,17 +342,17 @@ function parseSelectList(state) {
|
|
|
339
342
|
const cols = []
|
|
340
343
|
|
|
341
344
|
while (true) {
|
|
342
|
-
const
|
|
345
|
+
const { positionStart, type } = current(state)
|
|
343
346
|
|
|
344
347
|
// Check for qualified asterisk (table.*)
|
|
345
|
-
if (
|
|
348
|
+
if (type === 'identifier') {
|
|
346
349
|
const next = peekToken(state, 1)
|
|
347
350
|
const nextNext = peekToken(state, 2)
|
|
348
351
|
if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
|
|
349
352
|
const table = consume(state).value
|
|
350
353
|
consume(state) // consume dot
|
|
351
354
|
consume(state) // consume asterisk
|
|
352
|
-
cols.push({ type: 'star', table })
|
|
355
|
+
cols.push({ type: 'star', table, positionStart, positionEnd: state.lastPos })
|
|
353
356
|
if (!match(state, 'comma')) break
|
|
354
357
|
continue
|
|
355
358
|
}
|
|
@@ -357,7 +360,7 @@ function parseSelectList(state) {
|
|
|
357
360
|
|
|
358
361
|
// Check for unqualified asterisk (*)
|
|
359
362
|
if (match(state, 'operator', '*')) {
|
|
360
|
-
cols.push({ type: 'star' })
|
|
363
|
+
cols.push({ type: 'star', positionStart, positionEnd: state.lastPos })
|
|
361
364
|
if (!match(state, 'comma')) break
|
|
362
365
|
continue
|
|
363
366
|
}
|
|
@@ -365,7 +368,7 @@ function parseSelectList(state) {
|
|
|
365
368
|
// Parse derived column with optional alias
|
|
366
369
|
const expr = parseExpression(state)
|
|
367
370
|
const alias = parseAs(state)
|
|
368
|
-
cols.push({ type: 'derived', expr, alias })
|
|
371
|
+
cols.push({ type: 'derived', expr, alias, positionStart, positionEnd: state.lastPos })
|
|
369
372
|
|
|
370
373
|
if (!match(state, 'comma')) break
|
|
371
374
|
}
|
package/src/parse/primary.js
CHANGED
|
@@ -4,13 +4,15 @@ import { RESERVED_KEYWORDS } from '../validation/keywords.js'
|
|
|
4
4
|
import { parseExpression } from './expression.js'
|
|
5
5
|
import { parseFunctionCall } from './functions.js'
|
|
6
6
|
import { parseStatement } from './parse.js'
|
|
7
|
-
import { consume, current, expect, match, peekToken } from './state.js'
|
|
7
|
+
import { consume, current, expect, match, parseError, peekToken } from './state.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @import { ExprNode, IntervalNode, ParserState, WhenClause } from '../types.js'
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
+
* Parse a primary expression, which is the innermost order of operations.
|
|
15
|
+
*
|
|
14
16
|
* @param {ParserState} state
|
|
15
17
|
* @returns {ExprNode}
|
|
16
18
|
*/
|
|
@@ -246,7 +248,7 @@ export function parsePrimary(state) {
|
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
throw
|
|
251
|
+
throw parseError(state, 'expression')
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
/**
|
|
@@ -257,19 +259,20 @@ function parseInterval(state) {
|
|
|
257
259
|
const { positionStart } = expect(state, 'keyword', 'INTERVAL')
|
|
258
260
|
|
|
259
261
|
// Get value (number or quoted string)
|
|
260
|
-
const valueTok =
|
|
262
|
+
const valueTok = current(state)
|
|
261
263
|
/** @type {number} */
|
|
262
264
|
let value
|
|
263
265
|
if (valueTok.type === 'number') {
|
|
264
266
|
value = Number(valueTok.numericValue)
|
|
265
|
-
} else if (valueTok.type === 'string') {
|
|
266
|
-
value =
|
|
267
|
-
if (isNaN(value)) {
|
|
268
|
-
throw new InvalidLiteralError({ expected: 'interval value', ...valueTok })
|
|
269
|
-
}
|
|
267
|
+
} else if (valueTok.type === 'string' && valueTok.value.trim() !== '') {
|
|
268
|
+
value = Number(valueTok.value)
|
|
270
269
|
} else {
|
|
271
|
-
throw
|
|
270
|
+
throw parseError(state, 'interval value (number)')
|
|
271
|
+
}
|
|
272
|
+
if (isNaN(value)) {
|
|
273
|
+
throw new InvalidLiteralError({ expected: 'interval value', ...valueTok })
|
|
272
274
|
}
|
|
275
|
+
consume(state)
|
|
273
276
|
|
|
274
277
|
// Get unit keyword
|
|
275
278
|
const unitTok = consume(state)
|
package/src/parse/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SyntaxError } from '../validation/parseErrors.js'
|
|
1
|
+
import { SyntaxError, UnexpectedDotError } from '../validation/parseErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { ParserState, Token, TokenType } from '../types.js'
|
|
@@ -78,5 +78,12 @@ export function parseError(state, expected) {
|
|
|
78
78
|
const tok = current(state)
|
|
79
79
|
const prevToken = state.tokens[state.pos - 1]
|
|
80
80
|
const after = prevToken?.originalValue ?? prevToken?.value
|
|
81
|
+
if (tok.type === 'dot' && prevToken?.type === 'identifier') {
|
|
82
|
+
const nextToken = state.tokens[state.pos + 1]
|
|
83
|
+
if (nextToken && (nextToken.type === 'identifier' || nextToken.type === 'keyword')) {
|
|
84
|
+
const dottedName = after + '.' + (nextToken.originalValue ?? nextToken.value)
|
|
85
|
+
return new UnexpectedDotError({ dottedName, positionStart: prevToken.positionStart, positionEnd: nextToken.positionEnd })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
81
88
|
return new SyntaxError({ expected, after, ...tok })
|
|
82
89
|
}
|
package/src/parse/tokenize.js
CHANGED
|
@@ -5,7 +5,7 @@ import { InvalidLiteralError, ParseError, UnexpectedCharError } from '../validat
|
|
|
5
5
|
* @import { Token } from '../types.d.ts'
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const NUMBER_REGEX = /^-?(?:\d+n
|
|
8
|
+
const NUMBER_REGEX = /^-?(?:\d+n|(?:\d+\.?\d*|\d*\.\d+)(?:[eE][+-]?\d+)?)/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @param {string} query
|
|
@@ -32,40 +32,42 @@ export function tokenizeSql(query) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @param {number} positionStart
|
|
36
35
|
* @returns {Token}
|
|
37
36
|
*/
|
|
38
|
-
function parseNumber(
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
throw new InvalidLiteralError({ expected: 'number', value: query[i] || 'eof', positionStart, positionEnd: i + 1 })
|
|
42
|
-
}
|
|
37
|
+
function parseNumber() {
|
|
38
|
+
const positionStart = i
|
|
39
|
+
let value = query.slice(i).match(NUMBER_REGEX)?.[0] ?? ''
|
|
43
40
|
i += value.length
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
// check for invalid characters immediately following the number
|
|
42
|
+
const ch = peek()
|
|
43
|
+
if (!value || isAlphaNumeric(ch) || ch === '.') {
|
|
44
|
+
const after = query.slice(i).match(/^-?(?:[0-9a-zA-Z_$.]*[0-9][eE][+-]?[0-9])?[0-9a-zA-Z_$.]*/)?.[0] ?? ch
|
|
45
|
+
value += after
|
|
46
|
+
i += after.length
|
|
47
|
+
throw new InvalidLiteralError({ expected: 'number', value, positionStart, positionEnd: i })
|
|
47
48
|
}
|
|
48
49
|
if (value.endsWith('n')) {
|
|
49
50
|
return {
|
|
50
51
|
type: 'number',
|
|
51
52
|
value,
|
|
53
|
+
numericValue: BigInt(value.slice(0, -1)),
|
|
52
54
|
positionStart,
|
|
53
55
|
positionEnd: i,
|
|
54
|
-
numericValue: BigInt(value.slice(0, -1)),
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
return {
|
|
58
59
|
type: 'number',
|
|
59
60
|
value,
|
|
61
|
+
numericValue: Number(value),
|
|
60
62
|
positionStart,
|
|
61
63
|
positionEnd: i,
|
|
62
|
-
numericValue: Number(value),
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
while (i < len) {
|
|
67
68
|
const positionStart = i
|
|
68
|
-
const ch =
|
|
69
|
+
const ch = query[i]
|
|
70
|
+
const next = query[i + 1]
|
|
69
71
|
|
|
70
72
|
if (isWhitespace(ch)) {
|
|
71
73
|
i++
|
|
@@ -73,7 +75,7 @@ export function tokenizeSql(query) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// line comment --
|
|
76
|
-
if (ch === '-' &&
|
|
78
|
+
if (ch === '-' && next === '-') {
|
|
77
79
|
while (i < len && query[i] !== '\n') {
|
|
78
80
|
i++
|
|
79
81
|
}
|
|
@@ -81,11 +83,11 @@ export function tokenizeSql(query) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// block comment /* ... */
|
|
84
|
-
if (ch === '/' &&
|
|
85
|
-
i +=
|
|
86
|
+
if (ch === '/' && next === '*') {
|
|
87
|
+
i += 3
|
|
86
88
|
while (i < len) {
|
|
87
|
-
if (query[i] === '*' && query[i
|
|
88
|
-
i
|
|
89
|
+
if (query[i - 1] === '*' && query[i] === '/') {
|
|
90
|
+
i++
|
|
89
91
|
break
|
|
90
92
|
}
|
|
91
93
|
i++
|
|
@@ -94,7 +96,7 @@ export function tokenizeSql(query) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// negative numbers (when not subtraction)
|
|
97
|
-
if (ch === '-' && isDigit(query[i +
|
|
99
|
+
if (ch === '-' && (isDigit(next) || next === '.' && isDigit(query[i + 2]))) {
|
|
98
100
|
const lastToken = tokens[tokens.length - 1]
|
|
99
101
|
const isValueBefore = lastToken && (
|
|
100
102
|
lastToken.type === 'identifier' ||
|
|
@@ -103,23 +105,23 @@ export function tokenizeSql(query) {
|
|
|
103
105
|
lastToken.type === 'paren' && lastToken.value === ')'
|
|
104
106
|
)
|
|
105
107
|
if (!isValueBefore) {
|
|
106
|
-
tokens.push(parseNumber(
|
|
108
|
+
tokens.push(parseNumber())
|
|
107
109
|
continue
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// numbers
|
|
112
|
-
if (isDigit(ch)) {
|
|
113
|
-
tokens.push(parseNumber(
|
|
114
|
+
if (isDigit(ch) || ch === '.' && isDigit(next)) {
|
|
115
|
+
tokens.push(parseNumber())
|
|
114
116
|
continue
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
// identifiers / keywords
|
|
118
120
|
if (isAlpha(ch)) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
do {
|
|
122
|
+
i++
|
|
123
|
+
} while (isAlphaNumeric(query[i]))
|
|
124
|
+
const value = query.slice(positionStart, i)
|
|
123
125
|
const upper = value.toUpperCase()
|
|
124
126
|
if (KEYWORDS.has(upper)) {
|
|
125
127
|
tokens.push({
|
|
@@ -173,7 +175,7 @@ export function tokenizeSql(query) {
|
|
|
173
175
|
// operators
|
|
174
176
|
if ('<>!=+-*/%'.includes(ch)) {
|
|
175
177
|
let op = nextChar()
|
|
176
|
-
if ((op === '<' || op === '>' || op === '!') && peek() === '=') {
|
|
178
|
+
if ((op === '<' || op === '>' || op === '!' || op === '=') && peek() === '=') {
|
|
177
179
|
op += nextChar()
|
|
178
180
|
} else if (op === '<' && peek() === '>') {
|
|
179
181
|
op += nextChar()
|
package/src/parse/types.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export type TokenType =
|
|
|
23
23
|
export interface Token {
|
|
24
24
|
type: TokenType
|
|
25
25
|
value: string
|
|
26
|
+
numericValue?: number | bigint // only for type number
|
|
27
|
+
originalValue?: string // keywords are uppercased, this keeps the original case
|
|
26
28
|
positionStart: number
|
|
27
29
|
positionEnd: number
|
|
28
|
-
numericValue?: number | bigint
|
|
29
|
-
originalValue?: string
|
|
30
30
|
}
|
package/src/plan/columns.js
CHANGED
|
@@ -225,9 +225,9 @@ function inferSelectSourceColumns({ select, cteColumns, tables }) {
|
|
|
225
225
|
result.push(`${fromAlias}.${col}`)
|
|
226
226
|
}
|
|
227
227
|
for (const join of select.joins) {
|
|
228
|
-
const
|
|
228
|
+
const joinAlias = join.alias ?? join.table
|
|
229
229
|
for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
|
|
230
|
-
result.push(`${
|
|
230
|
+
result.push(`${joinAlias}.${col}`)
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
return result
|
package/src/plan/plan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { derivedAlias } from '../expression/alias.js'
|
|
2
2
|
import { parseSql } from '../parse/parse.js'
|
|
3
3
|
import { findAggregate } from '../validation/aggregates.js'
|
|
4
|
-
import { ColumnNotFoundError, TableNotFoundError } from '../validation/
|
|
4
|
+
import { ColumnNotFoundError, TableNotFoundError } from '../validation/tables.js'
|
|
5
5
|
import { validateScan, validateTableRefs } from '../validation/tables.js'
|
|
6
6
|
import { extractColumns, fromAlias, inferStatementColumns } from './columns.js'
|
|
7
7
|
|
|
@@ -113,7 +113,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
113
113
|
// Source alias for FROM clause
|
|
114
114
|
const sourceAlias = fromAlias(select.from)
|
|
115
115
|
|
|
116
|
-
//
|
|
116
|
+
// Resolve aliases (and validate qualified references)
|
|
117
117
|
const scopeTables = Object.fromEntries([sourceAlias, ...select.joins.map(j => j.alias ?? j.table)].map(a => [a, true]))
|
|
118
118
|
/** @type {Map<string, ExprNode>} */
|
|
119
119
|
const aliases = new Map()
|
|
@@ -128,11 +128,25 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns }) {
|
|
|
128
128
|
}
|
|
129
129
|
// Validate qualified references
|
|
130
130
|
if (col.table && !(col.table in scopeTables)) {
|
|
131
|
-
|
|
131
|
+
const qualified = col.table + '.*'
|
|
132
|
+
throw new TableNotFoundError({ table: col.table, qualified, tables: scopeTables, ...col })
|
|
132
133
|
}
|
|
133
134
|
return col
|
|
134
135
|
})
|
|
135
136
|
|
|
137
|
+
// Validate qualified references in other clauses
|
|
138
|
+
validateTableRefs(select.where, scopeTables)
|
|
139
|
+
validateTableRefs(select.having, scopeTables)
|
|
140
|
+
for (const expr of select.groupBy) {
|
|
141
|
+
validateTableRefs(expr, scopeTables)
|
|
142
|
+
}
|
|
143
|
+
for (const term of select.orderBy) {
|
|
144
|
+
validateTableRefs(term.expr, scopeTables)
|
|
145
|
+
}
|
|
146
|
+
for (const join of select.joins) {
|
|
147
|
+
validateTableRefs(join.on, scopeTables)
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
// Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
|
|
137
151
|
// included so they are only applied to fresh scans, not CTE/subquery plans)
|
|
138
152
|
/** @type {ScanOptions} */
|
package/src/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function isAggregateFunc(name) {
|
|
|
20
20
|
*/
|
|
21
21
|
export function isMathFunc(name) {
|
|
22
22
|
return [
|
|
23
|
-
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
23
|
+
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POW', 'POWER', 'SQRT',
|
|
24
24
|
'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2', 'DEGREES', 'RADIANS', 'PI',
|
|
25
25
|
'RAND', 'RANDOM',
|
|
26
26
|
].includes(name)
|
|
@@ -87,7 +87,7 @@ export function isStringFunc(name) {
|
|
|
87
87
|
* @returns {op is BinaryOp}
|
|
88
88
|
*/
|
|
89
89
|
export function isBinaryOp(op) {
|
|
90
|
-
return ['AND', 'OR', 'LIKE', '=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
90
|
+
return ['AND', 'OR', 'LIKE', '=', '==', '!=', '<>', '<', '>', '<=', '>='].includes(op)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
@@ -135,6 +135,7 @@ export const FUNCTION_SIGNATURES = {
|
|
|
135
135
|
LN: { min: 1, max: 1, signature: 'number' },
|
|
136
136
|
LOG10: { min: 1, max: 1, signature: 'number' },
|
|
137
137
|
POWER: { min: 2, max: 2, signature: 'base, exponent' },
|
|
138
|
+
POW: { min: 2, max: 2, signature: 'base, exponent' },
|
|
138
139
|
SQRT: { min: 1, max: 1, signature: 'number' },
|
|
139
140
|
SIN: { min: 1, max: 1, signature: 'radians' },
|
|
140
141
|
COS: { min: 1, max: 1, signature: 'radians' },
|
|
@@ -37,6 +37,21 @@ export class SyntaxError extends ParseError {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Error when a dot appears after an identifier, suggesting the user meant a dotted name.
|
|
42
|
+
*/
|
|
43
|
+
export class UnexpectedDotError extends ParseError {
|
|
44
|
+
/**
|
|
45
|
+
* @param {Object} options
|
|
46
|
+
* @param {string} options.dottedName - The combined dotted name (e.g., "dataset.parquet")
|
|
47
|
+
* @param {number} options.positionStart
|
|
48
|
+
* @param {number} options.positionEnd
|
|
49
|
+
*/
|
|
50
|
+
constructor({ dottedName, positionStart, positionEnd }) {
|
|
51
|
+
super({ message: `Unexpected "." in "${dottedName}". If this is an identifier, use double quotes: "${dottedName}"`, positionStart, positionEnd })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
/**
|
|
41
56
|
* Error for invalid literals (numbers, intervals, etc).
|
|
42
57
|
*/
|
|
@@ -48,10 +63,12 @@ export class InvalidLiteralError extends ParseError {
|
|
|
48
63
|
* @param {number} options.positionStart
|
|
49
64
|
* @param {number} options.positionEnd
|
|
50
65
|
* @param {string} [options.validValues] - List of valid values (for enums like interval units)
|
|
66
|
+
* @param {string} [options.after] - What token came before (for context)
|
|
51
67
|
*/
|
|
52
|
-
constructor({ expected, value, positionStart, positionEnd, validValues }) {
|
|
68
|
+
constructor({ expected, value, positionStart, positionEnd, validValues, after }) {
|
|
53
69
|
const suffix = validValues ? `. Valid values: ${validValues}` : ''
|
|
54
|
-
|
|
70
|
+
const afterStr = after ? ` after "${after}"` : ''
|
|
71
|
+
super({ message: `Invalid ${expected} ${value}${afterStr} at position ${positionStart}${suffix}`, positionStart, positionEnd })
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
package/src/validation/tables.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ExecutionError } from './executionErrors.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { AsyncDataSource, ExprNode, ScanOptions } from '../types.js'
|
|
@@ -7,15 +7,16 @@ import { ColumnNotFoundError, TableNotFoundError } from './planErrors.js'
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {Object} options
|
|
9
9
|
* @param {string} options.table - The name of the table to validate
|
|
10
|
+
* @param {string} [options.qualified] - The qualified identifier used in the query (for error messages)
|
|
10
11
|
* @param {Record<string, AsyncDataSource>} options.tables - Object mapping table names to data sources
|
|
11
12
|
* @param {number} [options.positionStart] - Optional start position for error reporting
|
|
12
13
|
* @param {number} [options.positionEnd] - Optional end position for error reporting
|
|
13
14
|
* @returns {AsyncDataSource}
|
|
14
15
|
*/
|
|
15
|
-
export function validateTable({ table, tables, positionStart, positionEnd } ) {
|
|
16
|
+
export function validateTable({ table, qualified, tables, positionStart, positionEnd } ) {
|
|
16
17
|
const resolved = tables[table]
|
|
17
18
|
if (!resolved) {
|
|
18
|
-
throw new TableNotFoundError({ table, tables, positionStart, positionEnd })
|
|
19
|
+
throw new TableNotFoundError({ table, qualified, tables, positionStart, positionEnd })
|
|
19
20
|
}
|
|
20
21
|
return resolved
|
|
21
22
|
}
|
|
@@ -52,13 +53,14 @@ export function validateScan({ table, hints, tables, positionStart, positionEnd
|
|
|
52
53
|
*/
|
|
53
54
|
export function validateTableRefs(expr, tables) {
|
|
54
55
|
if (!expr) return
|
|
55
|
-
if (expr.type === 'identifier') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
if (expr.type === 'identifier' && expr.prefix && !(expr.prefix in tables)) {
|
|
57
|
+
throw new TableNotFoundError({
|
|
58
|
+
table: expr.prefix,
|
|
59
|
+
qualified: expr.prefix + '.' + expr.name,
|
|
60
|
+
tables,
|
|
61
|
+
positionStart: expr.positionStart,
|
|
62
|
+
positionEnd: expr.positionStart + expr.prefix.length,
|
|
63
|
+
})
|
|
62
64
|
}
|
|
63
65
|
if (expr.type === 'binary') {
|
|
64
66
|
validateTableRefs(expr.left, tables)
|
|
@@ -85,3 +87,53 @@ export function validateTableRefs(expr, tables) {
|
|
|
85
87
|
validateTableRefs(expr.elseResult, tables)
|
|
86
88
|
}
|
|
87
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Error for missing table references.
|
|
93
|
+
*/
|
|
94
|
+
export class TableNotFoundError extends ExecutionError {
|
|
95
|
+
/**
|
|
96
|
+
* @param {Object} options
|
|
97
|
+
* @param {string} options.table - The missing table name
|
|
98
|
+
* @param {string} [options.qualified] - The identifier used in the query
|
|
99
|
+
* @param {Record<string, any>} options.tables - Available tables object
|
|
100
|
+
* @param {number} [options.positionStart]
|
|
101
|
+
* @param {number} [options.positionEnd]
|
|
102
|
+
*/
|
|
103
|
+
constructor({ table, qualified, tables, positionStart, positionEnd }) {
|
|
104
|
+
const usage = qualified ? ` in "${qualified}"` : ''
|
|
105
|
+
const available = tables
|
|
106
|
+
? `. Available tables: ${Object.keys(tables).join(', ')}`
|
|
107
|
+
: ''
|
|
108
|
+
super({
|
|
109
|
+
message: `Table "${table}" not found${usage}${available}`,
|
|
110
|
+
positionStart,
|
|
111
|
+
positionEnd,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Error for missing column references.
|
|
118
|
+
*/
|
|
119
|
+
export class ColumnNotFoundError extends ExecutionError {
|
|
120
|
+
/**
|
|
121
|
+
* @param {Object} options
|
|
122
|
+
* @param {string} options.missingColumn - The missing column name
|
|
123
|
+
* @param {string[]} options.availableColumns - List of available column names
|
|
124
|
+
* @param {number} options.positionStart
|
|
125
|
+
* @param {number} options.positionEnd
|
|
126
|
+
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
127
|
+
*/
|
|
128
|
+
constructor({ missingColumn, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
129
|
+
const available = availableColumns.length > 0
|
|
130
|
+
? `. Available columns: ${availableColumns.join(', ')}`
|
|
131
|
+
: ''
|
|
132
|
+
super({
|
|
133
|
+
message: `Column "${missingColumn}" not found${available}`,
|
|
134
|
+
positionStart,
|
|
135
|
+
positionEnd,
|
|
136
|
+
rowIndex,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { ExecutionError } from './executionErrors.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Error for missing table references.
|
|
5
|
-
*/
|
|
6
|
-
export class TableNotFoundError extends ExecutionError {
|
|
7
|
-
/**
|
|
8
|
-
* @param {Object} options
|
|
9
|
-
* @param {string} options.table - The missing table name
|
|
10
|
-
* @param {Record<string, any>} options.tables - Available tables object
|
|
11
|
-
* @param {number} [options.positionStart]
|
|
12
|
-
* @param {number} [options.positionEnd]
|
|
13
|
-
*/
|
|
14
|
-
constructor({ table, tables, positionStart, positionEnd }) {
|
|
15
|
-
const names = tables ? Object.keys(tables) : []
|
|
16
|
-
const available = names.length
|
|
17
|
-
? `. Available tables: ${names.join(', ')}`
|
|
18
|
-
: ''
|
|
19
|
-
super({
|
|
20
|
-
message: `Table "${table}" not found${available}`,
|
|
21
|
-
positionStart,
|
|
22
|
-
positionEnd,
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Error for missing column references.
|
|
29
|
-
*/
|
|
30
|
-
export class ColumnNotFoundError extends ExecutionError {
|
|
31
|
-
/**
|
|
32
|
-
* @param {Object} options
|
|
33
|
-
* @param {string} options.missingColumn - The missing column name
|
|
34
|
-
* @param {string[]} options.availableColumns - List of available column names
|
|
35
|
-
* @param {number} options.positionStart
|
|
36
|
-
* @param {number} options.positionEnd
|
|
37
|
-
* @param {number} [options.rowIndex] - 1-based row number where error occurred
|
|
38
|
-
*/
|
|
39
|
-
constructor({ missingColumn, availableColumns, positionStart, positionEnd, rowIndex }) {
|
|
40
|
-
const available = availableColumns.length > 0
|
|
41
|
-
? `. Available columns: ${availableColumns.join(', ')}`
|
|
42
|
-
: ''
|
|
43
|
-
super({
|
|
44
|
-
message: `Column "${missingColumn}" not found${available}`,
|
|
45
|
-
positionStart,
|
|
46
|
-
positionEnd,
|
|
47
|
-
rowIndex,
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
}
|