squirreling 0.4.7 → 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/README.md +1 -0
- package/package.json +3 -3
- package/src/execute/aggregates.js +12 -5
- package/src/execute/date.js +57 -0
- package/src/execute/execute.js +20 -7
- package/src/execute/expression.js +268 -40
- package/src/execute/having.js +7 -1
- package/src/execute/join.js +9 -4
- package/src/execute/math.js +165 -0
- package/src/execute/utils.js +3 -0
- package/src/executionErrors.js +62 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -7
- package/src/parse/expression.js +121 -10
- package/src/parse/parse.js +6 -1
- package/src/parse/state.js +16 -4
- package/src/parse/tokenize.js +113 -48
- package/src/parseErrors.js +117 -0
- package/src/types.d.ts +58 -14
- package/src/validation.js +23 -1
- package/src/validationErrors.js +127 -0
|
@@ -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
|
+
}
|
package/src/execute/utils.js
CHANGED
|
@@ -134,6 +134,9 @@ export function defaultDerivedAlias(expr) {
|
|
|
134
134
|
if (expr.type === 'function') {
|
|
135
135
|
return expr.name.toLowerCase() + '_' + expr.args.map(defaultDerivedAlias).join('_')
|
|
136
136
|
}
|
|
137
|
+
if (expr.type === 'interval') {
|
|
138
|
+
return `interval_${expr.value}_${expr.unit.toLowerCase()}`
|
|
139
|
+
}
|
|
137
140
|
return 'expr'
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -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,6 +1,7 @@
|
|
|
1
|
+
import { syntaxError } from '../parseErrors.js'
|
|
1
2
|
import { isBinaryOp } from '../validation.js'
|
|
2
3
|
import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
|
|
3
|
-
import { consume, current, expect, match, peekToken } from './state.js'
|
|
4
|
+
import { consume, current, expect, lastPosition, match, peekToken } from './state.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @import { ExprNode, ParserState } from '../types.js'
|
|
@@ -25,6 +26,8 @@ export function parseComparison(state) {
|
|
|
25
26
|
type: 'unary',
|
|
26
27
|
op: 'IS NOT NULL',
|
|
27
28
|
argument: left,
|
|
29
|
+
positionStart: left.positionStart,
|
|
30
|
+
positionEnd: lastPosition(state),
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
expect(state, 'keyword', 'NULL')
|
|
@@ -32,6 +35,8 @@ export function parseComparison(state) {
|
|
|
32
35
|
type: 'unary',
|
|
33
36
|
op: 'IS NULL',
|
|
34
37
|
argument: left,
|
|
38
|
+
positionStart: left.positionStart,
|
|
39
|
+
positionEnd: lastPosition(state),
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -39,6 +44,7 @@ export function parseComparison(state) {
|
|
|
39
44
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
40
45
|
const nextTok = peekToken(state, 1)
|
|
41
46
|
if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
|
|
47
|
+
const notPositionStart = tok.positionStart
|
|
42
48
|
consume(state) // NOT
|
|
43
49
|
consume(state) // LIKE
|
|
44
50
|
const right = parseAdditive(state)
|
|
@@ -50,7 +56,11 @@ export function parseComparison(state) {
|
|
|
50
56
|
op: 'LIKE',
|
|
51
57
|
left,
|
|
52
58
|
right,
|
|
59
|
+
positionStart: left.positionStart,
|
|
60
|
+
positionEnd: right.positionEnd,
|
|
53
61
|
},
|
|
62
|
+
positionStart: notPositionStart,
|
|
63
|
+
positionEnd: right.positionEnd,
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
}
|
|
@@ -63,6 +73,8 @@ export function parseComparison(state) {
|
|
|
63
73
|
op: 'LIKE',
|
|
64
74
|
left,
|
|
65
75
|
right,
|
|
76
|
+
positionStart: left.positionStart,
|
|
77
|
+
positionEnd: right.positionEnd,
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -70,6 +82,7 @@ export function parseComparison(state) {
|
|
|
70
82
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
71
83
|
const nextTok = peekToken(state, 1)
|
|
72
84
|
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
85
|
+
const notPositionStart = tok.positionStart
|
|
73
86
|
consume(state) // NOT
|
|
74
87
|
consume(state) // BETWEEN
|
|
75
88
|
const lower = parseAdditive(state)
|
|
@@ -79,8 +92,10 @@ export function parseComparison(state) {
|
|
|
79
92
|
return {
|
|
80
93
|
type: 'binary',
|
|
81
94
|
op: 'OR',
|
|
82
|
-
left: { type: 'binary', op: '<', left, right: lower },
|
|
83
|
-
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,
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
}
|
|
@@ -94,8 +109,10 @@ export function parseComparison(state) {
|
|
|
94
109
|
return {
|
|
95
110
|
type: 'binary',
|
|
96
111
|
op: 'AND',
|
|
97
|
-
left: { type: 'binary', op: '>=', left, right: lower },
|
|
98
|
-
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,
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
118
|
|
|
@@ -103,6 +120,7 @@ export function parseComparison(state) {
|
|
|
103
120
|
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
104
121
|
const nextTok = peekToken(state, 1)
|
|
105
122
|
if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
|
|
123
|
+
const notPositionStart = tok.positionStart
|
|
106
124
|
consume(state) // NOT
|
|
107
125
|
consume(state) // IN
|
|
108
126
|
|
|
@@ -110,12 +128,13 @@ export function parseComparison(state) {
|
|
|
110
128
|
// parseSubquery expects to consume the opening paren itself
|
|
111
129
|
const parenTok = current(state)
|
|
112
130
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
113
|
-
throw
|
|
131
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
|
|
114
132
|
}
|
|
115
133
|
const peekTok = peekToken(state, 1)
|
|
116
134
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
117
135
|
// Subquery - let parseSubquery handle the parens
|
|
118
136
|
const subquery = parseSubquery(state)
|
|
137
|
+
const positionEnd = lastPosition(state)
|
|
119
138
|
return {
|
|
120
139
|
type: 'unary',
|
|
121
140
|
op: 'NOT',
|
|
@@ -123,7 +142,11 @@ export function parseComparison(state) {
|
|
|
123
142
|
type: 'in',
|
|
124
143
|
expr: left,
|
|
125
144
|
subquery,
|
|
145
|
+
positionStart: left.positionStart,
|
|
146
|
+
positionEnd,
|
|
126
147
|
},
|
|
148
|
+
positionStart: notPositionStart,
|
|
149
|
+
positionEnd,
|
|
127
150
|
}
|
|
128
151
|
} else {
|
|
129
152
|
// Parse list of values - we handle the parens
|
|
@@ -135,6 +158,7 @@ export function parseComparison(state) {
|
|
|
135
158
|
if (!match(state, 'comma')) break
|
|
136
159
|
}
|
|
137
160
|
expect(state, 'paren', ')')
|
|
161
|
+
const positionEnd = lastPosition(state)
|
|
138
162
|
return {
|
|
139
163
|
type: 'unary',
|
|
140
164
|
op: 'NOT',
|
|
@@ -142,7 +166,11 @@ export function parseComparison(state) {
|
|
|
142
166
|
type: 'in valuelist',
|
|
143
167
|
expr: left,
|
|
144
168
|
values,
|
|
169
|
+
positionStart: left.positionStart,
|
|
170
|
+
positionEnd,
|
|
145
171
|
},
|
|
172
|
+
positionStart: notPositionStart,
|
|
173
|
+
positionEnd,
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
}
|
|
@@ -155,7 +183,7 @@ export function parseComparison(state) {
|
|
|
155
183
|
// parseSubquery expects to consume the opening paren itself
|
|
156
184
|
const parenTok = current(state)
|
|
157
185
|
if (parenTok.type !== 'paren' || parenTok.value !== '(') {
|
|
158
|
-
throw
|
|
186
|
+
throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, positionStart: parenTok.positionStart, positionEnd: parenTok.positionEnd, after: 'IN' })
|
|
159
187
|
}
|
|
160
188
|
const peekTok = peekToken(state, 1)
|
|
161
189
|
if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
|
|
@@ -165,6 +193,8 @@ export function parseComparison(state) {
|
|
|
165
193
|
type: 'in',
|
|
166
194
|
expr: left,
|
|
167
195
|
subquery,
|
|
196
|
+
positionStart: left.positionStart,
|
|
197
|
+
positionEnd: lastPosition(state),
|
|
168
198
|
}
|
|
169
199
|
} else {
|
|
170
200
|
// Parse list of values - we handle the parens
|
|
@@ -180,6 +210,8 @@ export function parseComparison(state) {
|
|
|
180
210
|
type: 'in valuelist',
|
|
181
211
|
expr: left,
|
|
182
212
|
values,
|
|
213
|
+
positionStart: left.positionStart,
|
|
214
|
+
positionEnd: lastPosition(state),
|
|
183
215
|
}
|
|
184
216
|
}
|
|
185
217
|
}
|
|
@@ -192,6 +224,8 @@ export function parseComparison(state) {
|
|
|
192
224
|
op: tok.value,
|
|
193
225
|
left,
|
|
194
226
|
right,
|
|
227
|
+
positionStart: left.positionStart,
|
|
228
|
+
positionEnd: right.positionEnd,
|
|
195
229
|
}
|
|
196
230
|
}
|
|
197
231
|
|