squirreling 0.4.6 → 0.4.8
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 +12 -0
- package/package.json +2 -2
- package/src/errors.js +230 -0
- package/src/execute/aggregates.js +33 -7
- package/src/execute/date.js +57 -0
- package/src/execute/execute.js +15 -8
- package/src/execute/expression.js +151 -21
- package/src/execute/having.js +3 -2
- package/src/execute/join.js +11 -7
- package/src/execute/utils.js +34 -2
- package/src/parse/comparison.js +12 -11
- package/src/parse/expression.js +124 -5
- package/src/parse/parse.js +6 -1
- package/src/parse/state.js +5 -3
- package/src/parse/tokenize.js +89 -37
- package/src/types.d.ts +39 -6
- package/src/validation.js +28 -3
package/src/parse/parse.js
CHANGED
|
@@ -63,6 +63,11 @@ function parseSelectList(state) {
|
|
|
63
63
|
return cols
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Keywords that can start a valid expression in SELECT
|
|
67
|
+
const EXPRESSION_START_KEYWORDS = new Set([
|
|
68
|
+
'CASE', 'TRUE', 'FALSE', 'NULL', 'EXISTS', 'NOT', 'INTERVAL',
|
|
69
|
+
])
|
|
70
|
+
|
|
66
71
|
/**
|
|
67
72
|
* @param {ParserState} state
|
|
68
73
|
* @returns {SelectColumn}
|
|
@@ -70,7 +75,7 @@ function parseSelectList(state) {
|
|
|
70
75
|
function parseSelectItem(state) {
|
|
71
76
|
const tok = current(state)
|
|
72
77
|
|
|
73
|
-
if (tok.type === 'keyword' && tok.value
|
|
78
|
+
if (tok.type === 'keyword' && !EXPRESSION_START_KEYWORDS.has(tok.value) || tok.type === 'eof') {
|
|
74
79
|
throw parseError(state, 'column name or expression')
|
|
75
80
|
}
|
|
76
81
|
|
package/src/parse/state.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { syntaxError } from '../errors.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @import { ParserState, Token, TokenType } from '../types.js'
|
|
3
5
|
*/
|
|
@@ -86,7 +88,7 @@ export function expectIdentifier(state) {
|
|
|
86
88
|
export function parseError(state, expected) {
|
|
87
89
|
const tok = current(state)
|
|
88
90
|
const prevToken = state.tokens[state.pos - 1]
|
|
89
|
-
const after = prevToken ?
|
|
90
|
-
const
|
|
91
|
-
return
|
|
91
|
+
const after = prevToken ? prevToken.originalValue ?? prevToken.value : undefined
|
|
92
|
+
const received = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
|
|
93
|
+
return syntaxError({ expected, received, position: tok.position, after })
|
|
92
94
|
}
|
package/src/parse/tokenize.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
invalidLiteralError,
|
|
3
|
+
unexpectedCharError,
|
|
4
|
+
unterminatedError,
|
|
5
|
+
} from '../errors.js'
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* @import { Token } from '../types.d.ts'
|
|
3
9
|
*/
|
|
@@ -41,6 +47,13 @@ const KEYWORDS = new Set([
|
|
|
41
47
|
'FULL',
|
|
42
48
|
'OUTER',
|
|
43
49
|
'ON',
|
|
50
|
+
'INTERVAL',
|
|
51
|
+
'DAY',
|
|
52
|
+
'MONTH',
|
|
53
|
+
'YEAR',
|
|
54
|
+
'HOUR',
|
|
55
|
+
'MINUTE',
|
|
56
|
+
'SECOND',
|
|
44
57
|
])
|
|
45
58
|
|
|
46
59
|
/**
|
|
@@ -71,6 +84,61 @@ export function tokenize(sql) {
|
|
|
71
84
|
return ch
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {number} startPos
|
|
89
|
+
* @param {string} prefix
|
|
90
|
+
* @returns {Token}
|
|
91
|
+
*/
|
|
92
|
+
function parseNumber(startPos, prefix = '') {
|
|
93
|
+
let text = prefix
|
|
94
|
+
while (isDigit(peek())) {
|
|
95
|
+
text += nextChar()
|
|
96
|
+
}
|
|
97
|
+
if (peek() === '.') {
|
|
98
|
+
text += nextChar()
|
|
99
|
+
while (isDigit(peek())) {
|
|
100
|
+
text += nextChar()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// exponent
|
|
104
|
+
if (peek() === 'e' || peek() === 'E') {
|
|
105
|
+
text += nextChar()
|
|
106
|
+
if (peek() === '+' || peek() === '-') {
|
|
107
|
+
text += nextChar()
|
|
108
|
+
}
|
|
109
|
+
while (isDigit(peek())) {
|
|
110
|
+
text += nextChar()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// bigint suffix
|
|
114
|
+
if (peek() === 'n') {
|
|
115
|
+
text += nextChar()
|
|
116
|
+
try {
|
|
117
|
+
return {
|
|
118
|
+
type: 'number',
|
|
119
|
+
value: text,
|
|
120
|
+
position: startPos,
|
|
121
|
+
numericValue: BigInt(text.slice(0, -1)),
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
throw invalidLiteralError({ type: 'bigint', value: text.slice(0, -1), position: startPos })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (isAlpha(peek())) {
|
|
128
|
+
throw invalidLiteralError({ type: 'number', value: text + peek(), position: startPos })
|
|
129
|
+
}
|
|
130
|
+
const num = parseFloat(text)
|
|
131
|
+
if (isNaN(num)) {
|
|
132
|
+
throw invalidLiteralError({ type: 'number', value: text, position: startPos })
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
type: 'number',
|
|
136
|
+
value: text,
|
|
137
|
+
position: startPos,
|
|
138
|
+
numericValue: num,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
74
142
|
while (i < length) {
|
|
75
143
|
const ch = peek()
|
|
76
144
|
|
|
@@ -102,41 +170,25 @@ export function tokenize(sql) {
|
|
|
102
170
|
|
|
103
171
|
const pos = i
|
|
104
172
|
|
|
173
|
+
// negative numbers (when not subtraction)
|
|
174
|
+
if (ch === '-' && i + 1 < length && isDigit(sql[i + 1])) {
|
|
175
|
+
const lastToken = tokens[tokens.length - 1]
|
|
176
|
+
const isValueBefore = lastToken && (
|
|
177
|
+
lastToken.type === 'identifier' ||
|
|
178
|
+
lastToken.type === 'number' ||
|
|
179
|
+
lastToken.type === 'string' ||
|
|
180
|
+
lastToken.type === 'paren' && lastToken.value === ')'
|
|
181
|
+
)
|
|
182
|
+
if (!isValueBefore) {
|
|
183
|
+
nextChar() // consume '-'
|
|
184
|
+
tokens.push(parseNumber(pos, '-'))
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
105
189
|
// numbers
|
|
106
190
|
if (isDigit(ch)) {
|
|
107
|
-
|
|
108
|
-
while (isDigit(peek())) {
|
|
109
|
-
text += nextChar()
|
|
110
|
-
}
|
|
111
|
-
if (peek() === '.') {
|
|
112
|
-
text += nextChar()
|
|
113
|
-
while (isDigit(peek())) {
|
|
114
|
-
text += nextChar()
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// exponent
|
|
118
|
-
if (peek() === 'e' || peek() === 'E') {
|
|
119
|
-
text += nextChar()
|
|
120
|
-
if (peek() === '+' || peek() === '-') {
|
|
121
|
-
text += nextChar()
|
|
122
|
-
}
|
|
123
|
-
while (isDigit(peek())) {
|
|
124
|
-
text += nextChar()
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (isAlpha(peek())) {
|
|
128
|
-
throw new Error(`Invalid number at position ${pos}: ${text}${peek()}`)
|
|
129
|
-
}
|
|
130
|
-
const num = parseFloat(text)
|
|
131
|
-
if (isNaN(num)) {
|
|
132
|
-
throw new Error(`Invalid number at position ${pos}: ${text}`)
|
|
133
|
-
}
|
|
134
|
-
tokens.push({
|
|
135
|
-
type: 'number',
|
|
136
|
-
value: text,
|
|
137
|
-
position: pos,
|
|
138
|
-
numericValue: num,
|
|
139
|
-
})
|
|
191
|
+
tokens.push(parseNumber(pos))
|
|
140
192
|
continue
|
|
141
193
|
}
|
|
142
194
|
|
|
@@ -170,7 +222,7 @@ export function tokenize(sql) {
|
|
|
170
222
|
let text = ''
|
|
171
223
|
while (i <= length) {
|
|
172
224
|
if (i === length) {
|
|
173
|
-
throw
|
|
225
|
+
throw unterminatedError('string', pos)
|
|
174
226
|
}
|
|
175
227
|
const c = nextChar()
|
|
176
228
|
if (c === quote) {
|
|
@@ -198,7 +250,7 @@ export function tokenize(sql) {
|
|
|
198
250
|
let text = ''
|
|
199
251
|
while (i <= length) {
|
|
200
252
|
if (i === length) {
|
|
201
|
-
throw
|
|
253
|
+
throw unterminatedError('identifier', pos)
|
|
202
254
|
}
|
|
203
255
|
const c = nextChar()
|
|
204
256
|
if (c === quote) {
|
|
@@ -288,9 +340,9 @@ export function tokenize(sql) {
|
|
|
288
340
|
}
|
|
289
341
|
|
|
290
342
|
if (tokens.length === 0) {
|
|
291
|
-
throw
|
|
343
|
+
throw unexpectedCharError(ch, pos, true)
|
|
292
344
|
}
|
|
293
|
-
throw
|
|
345
|
+
throw unexpectedCharError(ch, pos)
|
|
294
346
|
}
|
|
295
347
|
|
|
296
348
|
tokens.push({
|
package/src/types.d.ts
CHANGED
|
@@ -27,7 +27,15 @@ export interface ExecuteSqlOptions {
|
|
|
27
27
|
query: string
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export type SqlPrimitive =
|
|
30
|
+
export type SqlPrimitive =
|
|
31
|
+
| string
|
|
32
|
+
| number
|
|
33
|
+
| bigint
|
|
34
|
+
| boolean
|
|
35
|
+
| Date
|
|
36
|
+
| null
|
|
37
|
+
| SqlPrimitive[]
|
|
38
|
+
| Record<string, any>
|
|
31
39
|
|
|
32
40
|
export interface SelectStatement {
|
|
33
41
|
distinct: boolean
|
|
@@ -54,7 +62,9 @@ export interface FromSubquery {
|
|
|
54
62
|
alias: string
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
export type
|
|
65
|
+
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
66
|
+
|
|
67
|
+
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
58
68
|
|
|
59
69
|
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
60
70
|
|
|
@@ -127,6 +137,14 @@ export interface SubqueryNode {
|
|
|
127
137
|
subquery: SelectStatement
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
|
|
141
|
+
|
|
142
|
+
export interface IntervalNode {
|
|
143
|
+
type: 'interval'
|
|
144
|
+
value: number
|
|
145
|
+
unit: IntervalUnit
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
export type ExprNode =
|
|
131
149
|
| LiteralNode
|
|
132
150
|
| IdentifierNode
|
|
@@ -139,6 +157,7 @@ export type ExprNode =
|
|
|
139
157
|
| ExistsNode
|
|
140
158
|
| CaseNode
|
|
141
159
|
| SubqueryNode
|
|
160
|
+
| IntervalNode
|
|
142
161
|
|
|
143
162
|
export interface StarColumn {
|
|
144
163
|
kind: 'star'
|
|
@@ -146,9 +165,23 @@ export interface StarColumn {
|
|
|
146
165
|
alias?: string
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
|
|
150
|
-
|
|
151
|
-
export type StringFunc =
|
|
168
|
+
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
|
|
169
|
+
|
|
170
|
+
export type StringFunc =
|
|
171
|
+
| 'UPPER'
|
|
172
|
+
| 'LOWER'
|
|
173
|
+
| 'CONCAT'
|
|
174
|
+
| 'LENGTH'
|
|
175
|
+
| 'SUBSTRING'
|
|
176
|
+
| 'SUBSTR'
|
|
177
|
+
| 'TRIM'
|
|
178
|
+
| 'REPLACE'
|
|
179
|
+
| 'JSON_VALUE'
|
|
180
|
+
| 'JSON_QUERY'
|
|
181
|
+
| 'JSON_OBJECT'
|
|
182
|
+
| 'CURRENT_DATE'
|
|
183
|
+
| 'CURRENT_TIME'
|
|
184
|
+
| 'CURRENT_TIMESTAMP'
|
|
152
185
|
|
|
153
186
|
export interface AggregateArgStar {
|
|
154
187
|
kind: 'star'
|
|
@@ -214,6 +247,6 @@ export interface Token {
|
|
|
214
247
|
type: TokenType
|
|
215
248
|
value: string
|
|
216
249
|
position: number
|
|
217
|
-
numericValue?: number
|
|
250
|
+
numericValue?: number | bigint
|
|
218
251
|
originalValue?: string
|
|
219
252
|
}
|
package/src/validation.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, BinaryOp, ComparisonOp, StringFunc} from './types.js'
|
|
3
|
+
* @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, StringFunc} from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|
|
7
7
|
export function isAggregateFunc(name) {
|
|
8
|
-
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(name)
|
|
8
|
+
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG'].includes(name)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} name
|
|
13
|
+
* @returns {name is IntervalUnit}
|
|
14
|
+
*/
|
|
15
|
+
export function isIntervalUnit(name) {
|
|
16
|
+
return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
/**
|
|
@@ -13,7 +21,24 @@ export function isAggregateFunc(name) {
|
|
|
13
21
|
* @returns {name is StringFunc}
|
|
14
22
|
*/
|
|
15
23
|
export function isStringFunc(name) {
|
|
16
|
-
return [
|
|
24
|
+
return [
|
|
25
|
+
'UPPER',
|
|
26
|
+
'LOWER',
|
|
27
|
+
'CONCAT',
|
|
28
|
+
'LENGTH',
|
|
29
|
+
'SUBSTRING',
|
|
30
|
+
'SUBSTR',
|
|
31
|
+
'TRIM',
|
|
32
|
+
'REPLACE',
|
|
33
|
+
'RANDOM',
|
|
34
|
+
'RAND',
|
|
35
|
+
'JSON_VALUE',
|
|
36
|
+
'JSON_QUERY',
|
|
37
|
+
'JSON_OBJECT',
|
|
38
|
+
'CURRENT_DATE',
|
|
39
|
+
'CURRENT_TIME',
|
|
40
|
+
'CURRENT_TIMESTAMP',
|
|
41
|
+
].includes(name)
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
/**
|