squirreling 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/execute/aggregates.js +10 -4
- package/src/execute/execute.js +16 -10
- package/src/execute/expression.js +202 -38
- package/src/execute/having.js +7 -2
- package/src/execute/join.js +5 -4
- package/src/execute/math.js +165 -0
- package/src/executionErrors.js +62 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -8
- package/src/parse/expression.js +53 -13
- package/src/parse/state.js +13 -3
- package/src/parse/tokenize.js +34 -21
- package/src/parseErrors.js +117 -0
- package/src/types.d.ts +37 -13
- package/src/validation.js +12 -1
- package/src/validationErrors.js +127 -0
- package/src/errors.js +0 -230
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { SqlPrimitive } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
import { argCountError } from '../validationErrors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate a math function
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {string} options.funcName - Uppercase function name
|
|
11
|
+
* @param {SqlPrimitive[]} options.args - Function arguments
|
|
12
|
+
* @param {number} options.positionStart - Start position in query
|
|
13
|
+
* @param {number} options.positionEnd - End position in query
|
|
14
|
+
* @param {number} [options.rowNumber] - 1-based row number for error reporting
|
|
15
|
+
* @returns {SqlPrimitive} Result
|
|
16
|
+
*/
|
|
17
|
+
export function evaluateMathFunc({ funcName, args, positionStart, positionEnd, rowNumber }) {
|
|
18
|
+
if (funcName === 'FLOOR') {
|
|
19
|
+
if (args.length !== 1) {
|
|
20
|
+
throw argCountError({
|
|
21
|
+
funcName: 'FLOOR',
|
|
22
|
+
expected: 1,
|
|
23
|
+
received: args.length,
|
|
24
|
+
positionStart,
|
|
25
|
+
positionEnd,
|
|
26
|
+
rowNumber,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
const val = args[0]
|
|
30
|
+
if (val == null) return null
|
|
31
|
+
return Math.floor(Number(val))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (funcName === 'CEIL' || funcName === 'CEILING') {
|
|
35
|
+
if (args.length !== 1) {
|
|
36
|
+
throw argCountError({
|
|
37
|
+
funcName,
|
|
38
|
+
expected: 1,
|
|
39
|
+
received: args.length,
|
|
40
|
+
positionStart,
|
|
41
|
+
positionEnd,
|
|
42
|
+
rowNumber,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
const val = args[0]
|
|
46
|
+
if (val == null) return null
|
|
47
|
+
return Math.ceil(Number(val))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (funcName === 'ABS') {
|
|
51
|
+
if (args.length !== 1) {
|
|
52
|
+
throw argCountError({
|
|
53
|
+
funcName: 'ABS',
|
|
54
|
+
expected: 1,
|
|
55
|
+
received: args.length,
|
|
56
|
+
positionStart,
|
|
57
|
+
positionEnd,
|
|
58
|
+
rowNumber,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
const val = args[0]
|
|
62
|
+
if (val == null) return null
|
|
63
|
+
return Math.abs(Number(val))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (funcName === 'MOD') {
|
|
67
|
+
if (args.length !== 2) {
|
|
68
|
+
throw argCountError({
|
|
69
|
+
funcName: 'MOD',
|
|
70
|
+
expected: 2,
|
|
71
|
+
received: args.length,
|
|
72
|
+
positionStart,
|
|
73
|
+
positionEnd,
|
|
74
|
+
rowNumber,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
const dividend = args[0]
|
|
78
|
+
const divisor = args[1]
|
|
79
|
+
if (dividend == null || divisor == null) return null
|
|
80
|
+
return Number(dividend) % Number(divisor)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (funcName === 'EXP') {
|
|
84
|
+
if (args.length !== 1) {
|
|
85
|
+
throw argCountError({
|
|
86
|
+
funcName: 'EXP',
|
|
87
|
+
expected: 1,
|
|
88
|
+
received: args.length,
|
|
89
|
+
positionStart,
|
|
90
|
+
positionEnd,
|
|
91
|
+
rowNumber,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
const val = args[0]
|
|
95
|
+
if (val == null) return null
|
|
96
|
+
return Math.exp(Number(val))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (funcName === 'LN') {
|
|
100
|
+
if (args.length !== 1) {
|
|
101
|
+
throw argCountError({
|
|
102
|
+
funcName: 'LN',
|
|
103
|
+
expected: 1,
|
|
104
|
+
received: args.length,
|
|
105
|
+
positionStart,
|
|
106
|
+
positionEnd,
|
|
107
|
+
rowNumber,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
const val = args[0]
|
|
111
|
+
if (val == null) return null
|
|
112
|
+
return Math.log(Number(val))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (funcName === 'LOG10') {
|
|
116
|
+
if (args.length !== 1) {
|
|
117
|
+
throw argCountError({
|
|
118
|
+
funcName: 'LOG10',
|
|
119
|
+
expected: 1,
|
|
120
|
+
received: args.length,
|
|
121
|
+
positionStart,
|
|
122
|
+
positionEnd,
|
|
123
|
+
rowNumber,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
const val = args[0]
|
|
127
|
+
if (val == null) return null
|
|
128
|
+
return Math.log10(Number(val))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (funcName === 'POWER') {
|
|
132
|
+
if (args.length !== 2) {
|
|
133
|
+
throw argCountError({
|
|
134
|
+
funcName: 'POWER',
|
|
135
|
+
expected: 2,
|
|
136
|
+
received: args.length,
|
|
137
|
+
positionStart,
|
|
138
|
+
positionEnd,
|
|
139
|
+
rowNumber,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
const base = args[0]
|
|
143
|
+
const exponent = args[1]
|
|
144
|
+
if (base == null || exponent == null) return null
|
|
145
|
+
return Number(base) ** Number(exponent)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (funcName === 'SQRT') {
|
|
149
|
+
if (args.length !== 1) {
|
|
150
|
+
throw argCountError({
|
|
151
|
+
funcName: 'SQRT',
|
|
152
|
+
expected: 1,
|
|
153
|
+
received: args.length,
|
|
154
|
+
positionStart,
|
|
155
|
+
positionEnd,
|
|
156
|
+
rowNumber,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
const val = args[0]
|
|
160
|
+
if (val == null) return null
|
|
161
|
+
return Math.sqrt(Number(val))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// EXECUTION ERRORS - Issues during query execution
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structured execution error with position range and optional row number.
|
|
7
|
+
*/
|
|
8
|
+
export class ExecutionError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} message - Human-readable error message
|
|
11
|
+
* @param {number} positionStart - Start position (0-based character offset)
|
|
12
|
+
* @param {number} positionEnd - End position (exclusive, 0-based character offset)
|
|
13
|
+
* @param {number} [rowNumber] - 1-based row number where error occurred
|
|
14
|
+
*/
|
|
15
|
+
constructor(message, positionStart, positionEnd, rowNumber) {
|
|
16
|
+
const rowSuffix = rowNumber != null ? ` (row ${rowNumber})` : ''
|
|
17
|
+
super(message + rowSuffix)
|
|
18
|
+
this.name = 'ExecutionError'
|
|
19
|
+
this.positionStart = positionStart
|
|
20
|
+
this.positionEnd = positionEnd
|
|
21
|
+
this.rowNumber = rowNumber
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error for missing table.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {string} options.tableName - The missing table name
|
|
30
|
+
* @returns {Error}
|
|
31
|
+
*/
|
|
32
|
+
export function tableNotFoundError({ tableName }) {
|
|
33
|
+
return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Error for invalid context (e.g., INTERVAL without date arithmetic).
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
* @param {string} options.item - What was used incorrectly
|
|
41
|
+
* @param {string} options.validContext - Where it can be used
|
|
42
|
+
* @param {number} options.positionStart - Start position in query
|
|
43
|
+
* @param {number} options.positionEnd - End position in query
|
|
44
|
+
* @param {number} [options.rowNumber] - 1-based row number where error occurred
|
|
45
|
+
* @returns {ExecutionError}
|
|
46
|
+
*/
|
|
47
|
+
export function invalidContextError({ item, validContext, positionStart, positionEnd, rowNumber }) {
|
|
48
|
+
return new ExecutionError(`${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error for unsupported operation combinations.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {string} options.operation - The unsupported operation
|
|
56
|
+
* @param {string} [options.hint] - How to fix it
|
|
57
|
+
* @returns {Error}
|
|
58
|
+
*/
|
|
59
|
+
export function unsupportedOperationError({ operation, hint }) {
|
|
60
|
+
const suffix = hint ? `. ${hint}` : ''
|
|
61
|
+
return new Error(`${operation}${suffix}`)
|
|
62
|
+
}
|
package/src/index.js
CHANGED
package/src/parse/comparison.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { syntaxError } from '../
|
|
1
|
+
import { syntaxError } from '../parseErrors.js'
|
|
2
2
|
import { isBinaryOp } from '../validation.js'
|
|
3
3
|
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
4
|
-
import { consume, current, expect, match, peekToken } from './state.js'
|
|
4
|
+
import { consume, current, expect, lastPosition, match, peekToken } from './state.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @import { ExprNode, ParserState } from '../types.js'
|
|
@@ -26,6 +26,8 @@ export function parseComparison(state) {
|
|
|
26
26
|
type: 'unary',
|
|
27
27
|
op: 'IS NOT NULL',
|
|
28
28
|
argument: left,
|
|
29
|
+
positionStart: left.positionStart,
|
|
30
|
+
positionEnd: lastPosition(state),
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
expect(state, 'keyword', 'NULL')
|
|
@@ -33,6 +35,8 @@ export function parseComparison(state) {
|
|
|
33
35
|
type: 'unary',
|
|
34
36
|
op: 'IS NULL',
|
|
35
37
|
argument: left,
|
|
38
|
+
positionStart: left.positionStart,
|
|
39
|
+
positionEnd: lastPosition(state),
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -40,6 +44,7 @@ export function parseComparison(state) {
|
|
|
40
44
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
41
45
|
const nextTok = peekToken(state, 1)
|
|
42
46
|
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
47
|
+
const notPositionStart = tok.positionStart
|
|
43
48
|
consume(state) // NOT
|
|
44
49
|
consume(state) // LIKE
|
|
45
50
|
const right = parseAdditive(state)
|
|
@@ -51,7 +56,11 @@ export function parseComparison(state) {
|
|
|
51
56
|
op: 'LIKE',
|
|
52
57
|
left,
|
|
53
58
|
right,
|
|
59
|
+
positionStart: left.positionStart,
|
|
60
|
+
positionEnd: right.positionEnd,
|
|
54
61
|
},
|
|
62
|
+
positionStart: notPositionStart,
|
|
63
|
+
positionEnd: right.positionEnd,
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
}
|
|
@@ -64,6 +73,8 @@ export function parseComparison(state) {
|
|
|
64
73
|
op: 'LIKE',
|
|
65
74
|
left,
|
|
66
75
|
right,
|
|
76
|
+
positionStart: left.positionStart,
|
|
77
|
+
positionEnd: right.positionEnd,
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -71,6 +82,7 @@ export function parseComparison(state) {
|
|
|
71
82
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
72
83
|
const nextTok = peekToken(state, 1)
|
|
73
84
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
85
|
+
const notPositionStart = tok.positionStart
|
|
74
86
|
consume(state) // NOT
|
|
75
87
|
consume(state) // BETWEEN
|
|
76
88
|
const lower = parseAdditive(state)
|
|
@@ -80,8 +92,10 @@ export function parseComparison(state) {
|
|
|
80
92
|
return {
|
|
81
93
|
type: 'binary',
|
|
82
94
|
op: 'OR',
|
|
83
|
-
left: { type: 'binary', op: '<', left, right: lower },
|
|
84
|
-
right: { type: 'binary', op: '>', left, right: upper },
|
|
95
|
+
left: { type: 'binary', op: '<', left, right: lower, positionStart: left.positionStart, positionEnd: lower.positionEnd },
|
|
96
|
+
right: { type: 'binary', op: '>', left, right: upper, positionStart: left.positionStart, positionEnd: upper.positionEnd },
|
|
97
|
+
positionStart: notPositionStart,
|
|
98
|
+
positionEnd: upper.positionEnd,
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
}
|
|
@@ -95,8 +109,10 @@ export function parseComparison(state) {
|
|
|
95
109
|
return {
|
|
96
110
|
type: 'binary',
|
|
97
111
|
op: 'AND',
|
|
98
|
-
left: { type: 'binary', op: '>=', left, right: lower },
|
|
99
|
-
right: { type: 'binary', op: '<=', left, right: upper },
|
|
112
|
+
left: { type: 'binary', op: '>=', left, right: lower, positionStart: left.positionStart, positionEnd: lower.positionEnd },
|
|
113
|
+
right: { type: 'binary', op: '<=', left, right: upper, positionStart: left.positionStart, positionEnd: upper.positionEnd },
|
|
114
|
+
positionStart: left.positionStart,
|
|
115
|
+
positionEnd: upper.positionEnd,
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
|
|
@@ -104,6 +120,7 @@ export function parseComparison(state) {
|
|
|
104
120
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
105
121
|
const nextTok = peekToken(state, 1)
|
|
106
122
|
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
123
|
+
const notPositionStart = tok.positionStart
|
|
107
124
|
consume(state) // NOT
|
|
108
125
|
consume(state) // IN
|
|
109
126
|
|
|
@@ -111,12 +128,13 @@ export function parseComparison(state) {
|
|
|
111
128
|
// parseSubquery expects to consume the opening paren itself
|
|
112
129
|
const parenTok = current(state)
|
|
113
130
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
114
|
-
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`,
|
|
131
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
|
|
115
132
|
}
|
|
116
133
|
const peekTok = peekToken(state, 1)
|
|
117
134
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
118
135
|
// Subquery - let parseSubquery handle the parens
|
|
119
136
|
const subquery = parseSubquery(state)
|
|
137
|
+
const positionEnd = lastPosition(state)
|
|
120
138
|
return {
|
|
121
139
|
type: 'unary',
|
|
122
140
|
op: 'NOT',
|
|
@@ -124,7 +142,11 @@ export function parseComparison(state) {
|
|
|
124
142
|
type: 'in',
|
|
125
143
|
expr: left,
|
|
126
144
|
subquery,
|
|
145
|
+
positionStart: left.positionStart,
|
|
146
|
+
positionEnd,
|
|
127
147
|
},
|
|
148
|
+
positionStart: notPositionStart,
|
|
149
|
+
positionEnd,
|
|
128
150
|
}
|
|
129
151
|
} else {
|
|
130
152
|
// Parse list of values - we handle the parens
|
|
@@ -136,6 +158,7 @@ export function parseComparison(state) {
|
|
|
136
158
|
if (!match(state, 'comma')) break
|
|
137
159
|
}
|
|
138
160
|
expect(state, 'paren', ')')
|
|
161
|
+
const positionEnd = lastPosition(state)
|
|
139
162
|
return {
|
|
140
163
|
type: 'unary',
|
|
141
164
|
op: 'NOT',
|
|
@@ -143,7 +166,11 @@ export function parseComparison(state) {
|
|
|
143
166
|
type: 'in valuelist',
|
|
144
167
|
expr: left,
|
|
145
168
|
values,
|
|
169
|
+
positionStart: left.positionStart,
|
|
170
|
+
positionEnd,
|
|
146
171
|
},
|
|
172
|
+
positionStart: notPositionStart,
|
|
173
|
+
positionEnd,
|
|
147
174
|
}
|
|
148
175
|
}
|
|
149
176
|
}
|
|
@@ -156,7 +183,7 @@ export function parseComparison(state) {
|
|
|
156
183
|
// parseSubquery expects to consume the opening paren itself
|
|
157
184
|
const parenTok = current(state)
|
|
158
185
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
159
|
-
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`,
|
|
186
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
|
|
160
187
|
}
|
|
161
188
|
const peekTok = peekToken(state, 1)
|
|
162
189
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
@@ -166,6 +193,8 @@ export function parseComparison(state) {
|
|
|
166
193
|
type: 'in',
|
|
167
194
|
expr: left,
|
|
168
195
|
subquery,
|
|
196
|
+
positionStart: left.positionStart,
|
|
197
|
+
positionEnd: lastPosition(state),
|
|
169
198
|
}
|
|
170
199
|
} else {
|
|
171
200
|
// Parse list of values - we handle the parens
|
|
@@ -181,6 +210,8 @@ export function parseComparison(state) {
|
|
|
181
210
|
type: 'in valuelist',
|
|
182
211
|
expr: left,
|
|
183
212
|
values,
|
|
213
|
+
positionStart: left.positionStart,
|
|
214
|
+
positionEnd: lastPosition(state),
|
|
184
215
|
}
|
|
185
216
|
}
|
|
186
217
|
}
|
|
@@ -193,6 +224,8 @@ export function parseComparison(state) {
|
|
|
193
224
|
op: tok.value,
|
|
194
225
|
left,
|
|
195
226
|
right,
|
|
227
|
+
positionStart: left.positionStart,
|
|
228
|
+
positionEnd: right.positionEnd,
|
|
196
229
|
}
|
|
197
230
|
}
|
|
198
231
|
|
package/src/parse/expression.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
missingClauseError,
|
|
4
4
|
syntaxError,
|
|
5
5
|
unknownFunctionError,
|
|
6
|
-
} from '../
|
|
7
|
-
import { isAggregateFunc, isIntervalUnit, isStringFunc } from '../validation.js'
|
|
6
|
+
} from '../parseErrors.js'
|
|
7
|
+
import { isAggregateFunc, isIntervalUnit, isMathFunc, isStringFunc } from '../validation.js'
|
|
8
8
|
import { parseComparison } from './comparison.js'
|
|
9
9
|
import { parseSelectInternal } from './parse.js'
|
|
10
|
-
import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
|
|
10
|
+
import { consume, current, expect, expectIdentifier, lastPosition, match, peekToken } from './state.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @import { ExprNode, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
|
|
@@ -18,6 +18,7 @@ import { consume, current, expect, expectIdentifier, match, peekToken } from './
|
|
|
18
18
|
* @returns {IntervalNode}
|
|
19
19
|
*/
|
|
20
20
|
function parseInterval(state) {
|
|
21
|
+
const { positionStart } = current(state)
|
|
21
22
|
consume(state) // INTERVAL
|
|
22
23
|
|
|
23
24
|
// Handle optional negative sign
|
|
@@ -39,11 +40,11 @@ function parseInterval(state) {
|
|
|
39
40
|
consume(state)
|
|
40
41
|
const parsed = parseFloat(valueTok.value)
|
|
41
42
|
if (isNaN(parsed)) {
|
|
42
|
-
throw invalidLiteralError({ type: 'interval value', value: valueTok.value,
|
|
43
|
+
throw invalidLiteralError({ type: 'interval value', value: valueTok.value, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
43
44
|
}
|
|
44
45
|
value = sign * parsed
|
|
45
46
|
} else {
|
|
46
|
-
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`,
|
|
47
|
+
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// Get unit keyword
|
|
@@ -52,13 +53,14 @@ function parseInterval(state) {
|
|
|
52
53
|
throw invalidLiteralError({
|
|
53
54
|
type: 'interval unit',
|
|
54
55
|
value: unitTok.value,
|
|
55
|
-
|
|
56
|
+
positionStart: unitTok.positionStart,
|
|
57
|
+
positionEnd: unitTok.positionEnd,
|
|
56
58
|
validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
|
|
57
59
|
})
|
|
58
60
|
}
|
|
59
61
|
consume(state)
|
|
60
62
|
|
|
61
|
-
return { type: 'interval', value, unit: unitTok.value }
|
|
63
|
+
return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: lastPosition(state) }
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -75,6 +77,7 @@ export function parseExpression(state) {
|
|
|
75
77
|
*/
|
|
76
78
|
export function parsePrimary(state) {
|
|
77
79
|
const tok = current(state)
|
|
80
|
+
const { positionStart } = tok
|
|
78
81
|
|
|
79
82
|
if (tok.type === 'paren' && tok.value === '(') {
|
|
80
83
|
// Peek ahead to see if this is a scalar subquery
|
|
@@ -85,6 +88,8 @@ export function parsePrimary(state) {
|
|
|
85
88
|
return {
|
|
86
89
|
type: 'subquery',
|
|
87
90
|
subquery,
|
|
91
|
+
positionStart,
|
|
92
|
+
positionEnd: lastPosition(state),
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
// Regular grouped expression
|
|
@@ -109,6 +114,8 @@ export function parsePrimary(state) {
|
|
|
109
114
|
type: 'cast',
|
|
110
115
|
expr,
|
|
111
116
|
toType: typeTok.value,
|
|
117
|
+
positionStart,
|
|
118
|
+
positionEnd: lastPosition(state),
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -117,8 +124,8 @@ export function parsePrimary(state) {
|
|
|
117
124
|
const funcName = tok.value
|
|
118
125
|
|
|
119
126
|
// validate function names
|
|
120
|
-
if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
|
|
121
|
-
throw unknownFunctionError(funcName, tok.
|
|
127
|
+
if (!isStringFunc(funcName) && !isAggregateFunc(funcName) && !isMathFunc(funcName)) {
|
|
128
|
+
throw unknownFunctionError({ funcName, positionStart: tok.positionStart, positionEnd: tok.positionEnd })
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
consume(state) // function name
|
|
@@ -131,10 +138,13 @@ export function parsePrimary(state) {
|
|
|
131
138
|
while (true) {
|
|
132
139
|
// Handle COUNT(*) - treat * as a special identifier
|
|
133
140
|
if (current(state).type === 'operator' && current(state).value === '*') {
|
|
141
|
+
const starTok = current(state)
|
|
134
142
|
consume(state)
|
|
135
143
|
args.push({
|
|
136
144
|
type: 'identifier',
|
|
137
145
|
name: '*',
|
|
146
|
+
positionStart: starTok.positionStart,
|
|
147
|
+
positionEnd: lastPosition(state),
|
|
138
148
|
})
|
|
139
149
|
} else {
|
|
140
150
|
const arg = parseExpression(state)
|
|
@@ -150,6 +160,8 @@ export function parsePrimary(state) {
|
|
|
150
160
|
type: 'function',
|
|
151
161
|
name: funcName,
|
|
152
162
|
args,
|
|
163
|
+
positionStart,
|
|
164
|
+
positionEnd: lastPosition(state),
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -161,6 +173,8 @@ export function parsePrimary(state) {
|
|
|
161
173
|
type: 'function',
|
|
162
174
|
name: tok.value,
|
|
163
175
|
args: [],
|
|
176
|
+
positionStart,
|
|
177
|
+
positionEnd: lastPosition(state),
|
|
164
178
|
}
|
|
165
179
|
}
|
|
166
180
|
|
|
@@ -177,6 +191,8 @@ export function parsePrimary(state) {
|
|
|
177
191
|
return {
|
|
178
192
|
type: 'identifier',
|
|
179
193
|
name,
|
|
194
|
+
positionStart,
|
|
195
|
+
positionEnd: lastPosition(state),
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
|
|
@@ -185,6 +201,8 @@ export function parsePrimary(state) {
|
|
|
185
201
|
return {
|
|
186
202
|
type: 'literal',
|
|
187
203
|
value: tok.numericValue ?? null,
|
|
204
|
+
positionStart,
|
|
205
|
+
positionEnd: lastPosition(state),
|
|
188
206
|
}
|
|
189
207
|
}
|
|
190
208
|
|
|
@@ -193,21 +211,23 @@ export function parsePrimary(state) {
|
|
|
193
211
|
return {
|
|
194
212
|
type: 'literal',
|
|
195
213
|
value: tok.value,
|
|
214
|
+
positionStart,
|
|
215
|
+
positionEnd: lastPosition(state),
|
|
196
216
|
}
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
if (tok.type === 'keyword') {
|
|
200
220
|
if (tok.value === 'TRUE') {
|
|
201
221
|
consume(state)
|
|
202
|
-
return { type: 'literal', value: true }
|
|
222
|
+
return { type: 'literal', value: true, positionStart, positionEnd: lastPosition(state) }
|
|
203
223
|
}
|
|
204
224
|
if (tok.value === 'FALSE') {
|
|
205
225
|
consume(state)
|
|
206
|
-
return { type: 'literal', value: false }
|
|
226
|
+
return { type: 'literal', value: false, positionStart, positionEnd: lastPosition(state) }
|
|
207
227
|
}
|
|
208
228
|
if (tok.value === 'NULL') {
|
|
209
229
|
consume(state)
|
|
210
|
-
return { type: 'literal', value: null }
|
|
230
|
+
return { type: 'literal', value: null, positionStart, positionEnd: lastPosition(state) }
|
|
211
231
|
}
|
|
212
232
|
if (tok.value === 'EXISTS') {
|
|
213
233
|
consume(state) // EXISTS
|
|
@@ -215,6 +235,8 @@ export function parsePrimary(state) {
|
|
|
215
235
|
return {
|
|
216
236
|
type: 'exists',
|
|
217
237
|
subquery,
|
|
238
|
+
positionStart,
|
|
239
|
+
positionEnd: lastPosition(state),
|
|
218
240
|
}
|
|
219
241
|
}
|
|
220
242
|
if (tok.value === 'CASE') {
|
|
@@ -260,6 +282,8 @@ export function parsePrimary(state) {
|
|
|
260
282
|
caseExpr,
|
|
261
283
|
whenClauses,
|
|
262
284
|
elseResult,
|
|
285
|
+
positionStart,
|
|
286
|
+
positionEnd: lastPosition(state),
|
|
263
287
|
}
|
|
264
288
|
}
|
|
265
289
|
if (tok.value === 'INTERVAL') {
|
|
@@ -274,11 +298,13 @@ export function parsePrimary(state) {
|
|
|
274
298
|
type: 'unary',
|
|
275
299
|
op: '-',
|
|
276
300
|
argument,
|
|
301
|
+
positionStart,
|
|
302
|
+
positionEnd: argument.positionEnd,
|
|
277
303
|
}
|
|
278
304
|
}
|
|
279
305
|
|
|
280
306
|
const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
281
|
-
throw syntaxError({ expected: 'expression', received: found,
|
|
307
|
+
throw syntaxError({ expected: 'expression', received: found, positionStart: tok.positionStart, positionEnd: tok.positionEnd })
|
|
282
308
|
}
|
|
283
309
|
|
|
284
310
|
/**
|
|
@@ -294,6 +320,8 @@ function parseOr(state) {
|
|
|
294
320
|
op: 'OR',
|
|
295
321
|
left: node,
|
|
296
322
|
right,
|
|
323
|
+
positionStart: node.positionStart,
|
|
324
|
+
positionEnd: right.positionEnd,
|
|
297
325
|
}
|
|
298
326
|
}
|
|
299
327
|
return node
|
|
@@ -312,6 +340,8 @@ function parseAnd(state) {
|
|
|
312
340
|
op: 'AND',
|
|
313
341
|
left: node,
|
|
314
342
|
right,
|
|
343
|
+
positionStart: node.positionStart,
|
|
344
|
+
positionEnd: right.positionEnd,
|
|
315
345
|
}
|
|
316
346
|
}
|
|
317
347
|
return node
|
|
@@ -322,7 +352,9 @@ function parseAnd(state) {
|
|
|
322
352
|
* @returns {ExprNode}
|
|
323
353
|
*/
|
|
324
354
|
function parseNot(state) {
|
|
355
|
+
const tok = current(state)
|
|
325
356
|
if (match(state, 'keyword', 'NOT')) {
|
|
357
|
+
const { positionStart } = tok
|
|
326
358
|
// Check for NOT EXISTS
|
|
327
359
|
const nextTok = current(state)
|
|
328
360
|
if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
|
|
@@ -331,6 +363,8 @@ function parseNot(state) {
|
|
|
331
363
|
return {
|
|
332
364
|
type: 'not exists',
|
|
333
365
|
subquery,
|
|
366
|
+
positionStart,
|
|
367
|
+
positionEnd: lastPosition(state),
|
|
334
368
|
}
|
|
335
369
|
}
|
|
336
370
|
const argument = parseNot(state)
|
|
@@ -338,6 +372,8 @@ function parseNot(state) {
|
|
|
338
372
|
type: 'unary',
|
|
339
373
|
op: 'NOT',
|
|
340
374
|
argument,
|
|
375
|
+
positionStart,
|
|
376
|
+
positionEnd: argument.positionEnd,
|
|
341
377
|
}
|
|
342
378
|
}
|
|
343
379
|
return parseComparison(state)
|
|
@@ -359,6 +395,8 @@ export function parseAdditive(state) {
|
|
|
359
395
|
op: tok.value,
|
|
360
396
|
left: node,
|
|
361
397
|
right,
|
|
398
|
+
positionStart: node.positionStart,
|
|
399
|
+
positionEnd: right.positionEnd,
|
|
362
400
|
}
|
|
363
401
|
} else {
|
|
364
402
|
break
|
|
@@ -383,6 +421,8 @@ function parseMultiplicative(state) {
|
|
|
383
421
|
op: tok.value,
|
|
384
422
|
left: node,
|
|
385
423
|
right,
|
|
424
|
+
positionStart: node.positionStart,
|
|
425
|
+
positionEnd: right.positionEnd,
|
|
386
426
|
}
|
|
387
427
|
} else {
|
|
388
428
|
break
|