squirreling 0.7.9 → 0.8.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 +46 -0
- package/package.json +6 -6
- package/src/backend/dataSource.js +52 -47
- package/src/execute/aggregates.js +150 -0
- package/src/execute/columns.js +0 -39
- package/src/execute/execute.js +158 -415
- package/src/execute/join.js +179 -333
- package/src/execute/sort.js +99 -0
- package/src/execute/utils.js +18 -49
- package/src/executionErrors.js +10 -10
- package/src/expression/binary.js +51 -0
- package/src/{execute → expression}/date.js +18 -18
- package/src/{execute/expression.js → expression/evaluate.js} +61 -62
- package/src/{execute → expression}/math.js +46 -81
- package/src/{execute → expression}/regexp.js +7 -7
- package/src/{execute → expression}/strings.js +33 -45
- package/src/index.d.ts +15 -1
- package/src/parse/expression.js +42 -50
- package/src/parse/joins.js +10 -11
- package/src/parse/parse.js +14 -3
- package/src/parse/state.js +2 -1
- package/src/parse/types.d.ts +30 -0
- package/src/plan/plan.js +234 -0
- package/src/plan/types.d.ts +101 -0
- package/src/types.d.ts +19 -39
- package/src/validation.js +66 -2
- package/src/validationErrors.js +9 -7
- package/src/execute/having.js +0 -202
- package/src/execute/tableSource.js +0 -63
|
@@ -11,150 +11,115 @@
|
|
|
11
11
|
* @returns {SqlPrimitive}
|
|
12
12
|
*/
|
|
13
13
|
export function evaluateMathFunc({ funcName, args }) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return Math.floor(Number(val))
|
|
14
|
+
// No args
|
|
15
|
+
if (funcName === 'PI') {
|
|
16
|
+
return Math.PI
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
if (funcName === '
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
if (funcName === 'RAND' || funcName === 'RANDOM') {
|
|
20
|
+
return Math.random()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Two args
|
|
24
|
+
if (funcName === 'MOD') {
|
|
25
|
+
const [dividend, divisor] = args
|
|
26
|
+
if (dividend == null || divisor == null) return null
|
|
27
|
+
return Number(dividend) % Number(divisor)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (funcName === 'POWER') {
|
|
31
|
+
const [base, exponent] = args
|
|
32
|
+
if (base == null || exponent == null) return null
|
|
33
|
+
return Number(base) ** Number(exponent)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (funcName === 'ATAN2') {
|
|
37
|
+
const [y, x] = args
|
|
38
|
+
if (y == null || x == null) return null
|
|
39
|
+
return Math.atan2(Number(y), Number(x))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// One arg
|
|
43
|
+
const [val] = args
|
|
44
|
+
if (val == null) return null
|
|
45
|
+
|
|
46
|
+
if (funcName === 'ATAN') {
|
|
47
|
+
if (args.length === 1) {
|
|
48
|
+
return Math.atan(Number(val))
|
|
49
|
+
} else {
|
|
50
|
+
const [y, x] = args
|
|
51
|
+
if (y == null || x == null) return null
|
|
52
|
+
return Math.atan2(Number(y), Number(x))
|
|
53
|
+
}
|
|
24
54
|
}
|
|
25
55
|
|
|
26
56
|
if (funcName === 'ROUND') {
|
|
27
|
-
const val = args[0]
|
|
28
|
-
if (val == null) return null
|
|
29
57
|
const decimals = args[1] ?? 0
|
|
30
|
-
if (decimals == null) return null
|
|
31
58
|
const multiplier = 10 ** Number(decimals)
|
|
32
59
|
return Math.round(Number(val) * multiplier) / multiplier
|
|
33
60
|
}
|
|
34
61
|
|
|
62
|
+
if (funcName === 'FLOOR') {
|
|
63
|
+
return Math.floor(Number(val))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (funcName === 'CEIL' || funcName === 'CEILING') {
|
|
67
|
+
return Math.ceil(Number(val))
|
|
68
|
+
}
|
|
69
|
+
|
|
35
70
|
if (funcName === 'ABS') {
|
|
36
|
-
const val = args[0]
|
|
37
|
-
if (val == null) return null
|
|
38
71
|
return Math.abs(Number(val))
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
if (funcName === 'SIGN') {
|
|
42
|
-
const val = args[0]
|
|
43
|
-
if (val == null) return null
|
|
44
75
|
return Math.sign(Number(val))
|
|
45
76
|
}
|
|
46
77
|
|
|
47
|
-
if (funcName === 'MOD') {
|
|
48
|
-
const dividend = args[0]
|
|
49
|
-
const divisor = args[1]
|
|
50
|
-
if (dividend == null || divisor == null) return null
|
|
51
|
-
return Number(dividend) % Number(divisor)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
78
|
if (funcName === 'EXP') {
|
|
55
|
-
const val = args[0]
|
|
56
|
-
if (val == null) return null
|
|
57
79
|
return Math.exp(Number(val))
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
if (funcName === 'LN') {
|
|
61
|
-
const val = args[0]
|
|
62
|
-
if (val == null) return null
|
|
63
83
|
return Math.log(Number(val))
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
if (funcName === 'LOG10') {
|
|
67
|
-
const val = args[0]
|
|
68
|
-
if (val == null) return null
|
|
69
87
|
return Math.log10(Number(val))
|
|
70
88
|
}
|
|
71
89
|
|
|
72
|
-
if (funcName === 'POWER') {
|
|
73
|
-
const base = args[0]
|
|
74
|
-
const exponent = args[1]
|
|
75
|
-
if (base == null || exponent == null) return null
|
|
76
|
-
return Number(base) ** Number(exponent)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
90
|
if (funcName === 'SQRT') {
|
|
80
|
-
const val = args[0]
|
|
81
|
-
if (val == null) return null
|
|
82
91
|
return Math.sqrt(Number(val))
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
if (funcName === 'SIN') {
|
|
86
|
-
const val = args[0]
|
|
87
|
-
if (val == null) return null
|
|
88
95
|
return Math.sin(Number(val))
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
if (funcName === 'COS') {
|
|
92
|
-
const val = args[0]
|
|
93
|
-
if (val == null) return null
|
|
94
99
|
return Math.cos(Number(val))
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
if (funcName === 'TAN') {
|
|
98
|
-
const val = args[0]
|
|
99
|
-
if (val == null) return null
|
|
100
103
|
return Math.tan(Number(val))
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
if (funcName === 'COT') {
|
|
104
|
-
const val = args[0]
|
|
105
|
-
if (val == null) return null
|
|
106
107
|
return 1 / Math.tan(Number(val))
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
if (funcName === 'ASIN') {
|
|
110
|
-
const val = args[0]
|
|
111
|
-
if (val == null) return null
|
|
112
111
|
return Math.asin(Number(val))
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
if (funcName === 'ACOS') {
|
|
116
|
-
const val = args[0]
|
|
117
|
-
if (val == null) return null
|
|
118
115
|
return Math.acos(Number(val))
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
if (funcName === 'ATAN') {
|
|
122
|
-
if (args.length === 1) {
|
|
123
|
-
const val = args[0]
|
|
124
|
-
if (val == null) return null
|
|
125
|
-
return Math.atan(Number(val))
|
|
126
|
-
} else {
|
|
127
|
-
const y = args[0]
|
|
128
|
-
const x = args[1]
|
|
129
|
-
if (y == null || x == null) return null
|
|
130
|
-
return Math.atan2(Number(y), Number(x))
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (funcName === 'ATAN2') {
|
|
135
|
-
const y = args[0]
|
|
136
|
-
const x = args[1]
|
|
137
|
-
if (y == null || x == null) return null
|
|
138
|
-
return Math.atan2(Number(y), Number(x))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
118
|
if (funcName === 'DEGREES') {
|
|
142
|
-
const val = args[0]
|
|
143
|
-
if (val == null) return null
|
|
144
119
|
return Number(val) * 180 / Math.PI
|
|
145
120
|
}
|
|
146
121
|
|
|
147
122
|
if (funcName === 'RADIANS') {
|
|
148
|
-
const val = args[0]
|
|
149
|
-
if (val == null) return null
|
|
150
123
|
return Number(val) * Math.PI / 180
|
|
151
124
|
}
|
|
152
|
-
|
|
153
|
-
if (funcName === 'PI') {
|
|
154
|
-
return Math.PI
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (funcName === 'RAND' || funcName === 'RANDOM') {
|
|
158
|
-
return Math.random()
|
|
159
|
-
}
|
|
160
125
|
}
|
|
@@ -13,7 +13,7 @@ import { argValueError } from '../validationErrors.js'
|
|
|
13
13
|
* @param {number} [options.positionStart] - Start position in SQL string for error reporting
|
|
14
14
|
* @param {number} [options.positionEnd] - End position in SQL string for error reporting
|
|
15
15
|
* @param {number} [options.rowIndex] - Row number for error reporting
|
|
16
|
-
* @returns {SqlPrimitive}
|
|
16
|
+
* @returns {SqlPrimitive}
|
|
17
17
|
*/
|
|
18
18
|
export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd, rowIndex }) {
|
|
19
19
|
if (funcName === 'REGEXP_SUBSTR') {
|
|
@@ -34,7 +34,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
34
34
|
positionStart,
|
|
35
35
|
positionEnd,
|
|
36
36
|
hint: 'SQL uses 1-based indexing.',
|
|
37
|
-
|
|
37
|
+
rowIndex,
|
|
38
38
|
})
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -49,7 +49,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
49
49
|
message: `occurrence must be a positive integer, got ${args[3]}`,
|
|
50
50
|
positionStart,
|
|
51
51
|
positionEnd,
|
|
52
|
-
|
|
52
|
+
rowIndex,
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -64,7 +64,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
64
64
|
message: `invalid regex pattern: ${error.message}`,
|
|
65
65
|
positionStart,
|
|
66
66
|
positionEnd,
|
|
67
|
-
|
|
67
|
+
rowIndex,
|
|
68
68
|
})
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -104,7 +104,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
104
104
|
positionStart,
|
|
105
105
|
positionEnd,
|
|
106
106
|
hint: 'SQL uses 1-based indexing.',
|
|
107
|
-
|
|
107
|
+
rowIndex,
|
|
108
108
|
})
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -120,7 +120,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
120
120
|
positionStart,
|
|
121
121
|
positionEnd,
|
|
122
122
|
hint: 'Use 0 to replace all occurrences.',
|
|
123
|
-
|
|
123
|
+
rowIndex,
|
|
124
124
|
})
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -135,7 +135,7 @@ export function evaluateRegexpFunc({ funcName, args, positionStart, positionEnd,
|
|
|
135
135
|
message: `invalid regex pattern: ${error.message}`,
|
|
136
136
|
positionStart,
|
|
137
137
|
positionEnd,
|
|
138
|
-
|
|
138
|
+
rowIndex,
|
|
139
139
|
})
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -13,23 +13,11 @@ import { argValueError } from '../validationErrors.js'
|
|
|
13
13
|
* @param {number} [options.positionStart] - Start position for error reporting
|
|
14
14
|
* @param {number} [options.positionEnd] - End position for error reporting
|
|
15
15
|
* @param {number} [options.rowIndex] - Row index for error reporting
|
|
16
|
-
* @returns {SqlPrimitive}
|
|
16
|
+
* @returns {SqlPrimitive}
|
|
17
17
|
*/
|
|
18
18
|
export function evaluateStringFunc({ funcName, args, positionStart, positionEnd, rowIndex }) {
|
|
19
|
-
if (funcName === 'UPPER') {
|
|
20
|
-
const val = args[0]
|
|
21
|
-
if (val == null) return null
|
|
22
|
-
return String(val).toUpperCase()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (funcName === 'LOWER') {
|
|
26
|
-
const val = args[0]
|
|
27
|
-
if (val == null) return null
|
|
28
|
-
return String(val).toLowerCase()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
19
|
if (funcName === 'CONCAT') {
|
|
32
|
-
//
|
|
20
|
+
// Returns NULL if any argument is NULL
|
|
33
21
|
if (args.some(a => a == null)) return null
|
|
34
22
|
if (args.some(a => typeof a === 'object')) {
|
|
35
23
|
throw argValueError({
|
|
@@ -38,22 +26,30 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
38
26
|
positionStart,
|
|
39
27
|
positionEnd,
|
|
40
28
|
hint: 'Use CAST to convert objects to strings first.',
|
|
41
|
-
|
|
29
|
+
rowIndex,
|
|
42
30
|
})
|
|
43
31
|
}
|
|
44
32
|
return args.map(a => String(a)).join('')
|
|
45
33
|
}
|
|
46
34
|
|
|
35
|
+
// String first arg
|
|
36
|
+
const [val] = args
|
|
37
|
+
if (val == null) return null
|
|
38
|
+
const str = String(val)
|
|
39
|
+
|
|
40
|
+
if (funcName === 'UPPER') {
|
|
41
|
+
return str.toUpperCase()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (funcName === 'LOWER') {
|
|
45
|
+
return str.toLowerCase()
|
|
46
|
+
}
|
|
47
|
+
|
|
47
48
|
if (funcName === 'LENGTH') {
|
|
48
|
-
|
|
49
|
-
if (val == null) return null
|
|
50
|
-
return String(val).length
|
|
49
|
+
return str.length
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
54
|
-
const str = args[0]
|
|
55
|
-
if (str == null) return null
|
|
56
|
-
const strVal = String(str)
|
|
57
53
|
const start = Number(args[1])
|
|
58
54
|
if (!Number.isInteger(start) || start < 1) {
|
|
59
55
|
throw argValueError({
|
|
@@ -62,7 +58,7 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
62
58
|
positionStart,
|
|
63
59
|
positionEnd,
|
|
64
60
|
hint: 'SQL uses 1-based indexing.',
|
|
65
|
-
|
|
61
|
+
rowIndex,
|
|
66
62
|
})
|
|
67
63
|
}
|
|
68
64
|
// SQL uses 1-based indexing
|
|
@@ -75,33 +71,29 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
75
71
|
message: `length must be a non-negative integer, got ${args[2]}`,
|
|
76
72
|
positionStart,
|
|
77
73
|
positionEnd,
|
|
78
|
-
|
|
74
|
+
rowIndex,
|
|
79
75
|
})
|
|
80
76
|
}
|
|
81
|
-
return
|
|
77
|
+
return str.substring(startIdx, startIdx + len)
|
|
82
78
|
}
|
|
83
|
-
return
|
|
79
|
+
return str.substring(startIdx)
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
if (funcName === 'TRIM') {
|
|
87
|
-
|
|
88
|
-
if (val == null) return null
|
|
89
|
-
return String(val).trim()
|
|
83
|
+
return str.trim()
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
if (funcName === 'REPLACE') {
|
|
93
|
-
const str = args[0]
|
|
94
87
|
const searchStr = args[1]
|
|
95
88
|
const replaceStr = args[2]
|
|
96
89
|
// SQL REPLACE returns NULL if any argument is NULL
|
|
97
|
-
if (
|
|
98
|
-
return
|
|
90
|
+
if (searchStr == null || replaceStr == null) return null
|
|
91
|
+
return str.replaceAll(String(searchStr), String(replaceStr))
|
|
99
92
|
}
|
|
100
93
|
|
|
101
94
|
if (funcName === 'LEFT') {
|
|
102
|
-
const str = args[0]
|
|
103
95
|
const n = args[1]
|
|
104
|
-
if (
|
|
96
|
+
if (n == null) return null
|
|
105
97
|
const len = Number(n)
|
|
106
98
|
if (!Number.isInteger(len) || len < 0) {
|
|
107
99
|
throw argValueError({
|
|
@@ -109,16 +101,15 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
109
101
|
message: `length must be a non-negative integer, got ${n}`,
|
|
110
102
|
positionStart,
|
|
111
103
|
positionEnd,
|
|
112
|
-
|
|
104
|
+
rowIndex,
|
|
113
105
|
})
|
|
114
106
|
}
|
|
115
|
-
return
|
|
107
|
+
return str.substring(0, len)
|
|
116
108
|
}
|
|
117
109
|
|
|
118
110
|
if (funcName === 'RIGHT') {
|
|
119
|
-
const str = args[0]
|
|
120
111
|
const n = args[1]
|
|
121
|
-
if (
|
|
112
|
+
if (n == null) return null
|
|
122
113
|
const len = Number(n)
|
|
123
114
|
if (!Number.isInteger(len) || len < 0) {
|
|
124
115
|
throw argValueError({
|
|
@@ -126,20 +117,17 @@ export function evaluateStringFunc({ funcName, args, positionStart, positionEnd,
|
|
|
126
117
|
message: `length must be a non-negative integer, got ${n}`,
|
|
127
118
|
positionStart,
|
|
128
119
|
positionEnd,
|
|
129
|
-
|
|
120
|
+
rowIndex,
|
|
130
121
|
})
|
|
131
122
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return strVal.substring(strVal.length - len)
|
|
123
|
+
if (len >= str.length) return str
|
|
124
|
+
return str.substring(str.length - len)
|
|
135
125
|
}
|
|
136
126
|
|
|
137
127
|
if (funcName === 'INSTR') {
|
|
138
|
-
const str = args[0]
|
|
139
128
|
const search = args[1]
|
|
140
|
-
if (
|
|
129
|
+
if (search == null) return null
|
|
141
130
|
// INSTR returns 1-based position, 0 if not found
|
|
142
|
-
|
|
143
|
-
return pos === -1 ? 0 : pos + 1
|
|
131
|
+
return str.indexOf(String(search)) + 1
|
|
144
132
|
}
|
|
145
133
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ParseSqlOptions, SelectStatement, SqlPrimitive, Token } from './types.js'
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
AsyncCells,
|
|
4
|
+
AsyncDataSource,
|
|
5
|
+
AsyncRow,
|
|
6
|
+
ExecuteSqlOptions,
|
|
7
|
+
ExprNode,
|
|
8
|
+
ParseSqlOptions,
|
|
9
|
+
QueryPlan,
|
|
10
|
+
ScanOptions,
|
|
11
|
+
ScanResults,
|
|
12
|
+
SelectStatement,
|
|
13
|
+
SqlPrimitive,
|
|
14
|
+
Token,
|
|
15
|
+
UserDefinedFunction,
|
|
16
|
+
} from './types.js'
|
|
3
17
|
|
|
4
18
|
/**
|
|
5
19
|
* Executes a SQL SELECT query against an array of data rows
|
package/src/parse/expression.js
CHANGED
|
@@ -14,56 +14,6 @@ import { consume, current, expect, expectIdentifier, lastPosition, match, peekTo
|
|
|
14
14
|
* @import { ExprNode, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* @param {ParserState} state
|
|
19
|
-
* @returns {IntervalNode}
|
|
20
|
-
*/
|
|
21
|
-
function parseInterval(state) {
|
|
22
|
-
const { positionStart } = current(state)
|
|
23
|
-
consume(state) // INTERVAL
|
|
24
|
-
|
|
25
|
-
// Handle optional negative sign
|
|
26
|
-
let sign = 1
|
|
27
|
-
const signTok = current(state)
|
|
28
|
-
if (signTok.type === 'operator' && signTok.value === '-') {
|
|
29
|
-
consume(state)
|
|
30
|
-
sign = -1
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Get value (number or quoted string)
|
|
34
|
-
const valueTok = current(state)
|
|
35
|
-
/** @type {number} */
|
|
36
|
-
let value
|
|
37
|
-
if (valueTok.type === 'number') {
|
|
38
|
-
consume(state)
|
|
39
|
-
value = sign * Number(valueTok.numericValue)
|
|
40
|
-
} else if (valueTok.type === 'string') {
|
|
41
|
-
consume(state)
|
|
42
|
-
const parsed = parseFloat(valueTok.value)
|
|
43
|
-
if (isNaN(parsed)) {
|
|
44
|
-
throw invalidLiteralError({ type: 'interval value', value: valueTok.value, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
45
|
-
}
|
|
46
|
-
value = sign * parsed
|
|
47
|
-
} else {
|
|
48
|
-
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Get unit keyword
|
|
52
|
-
const unitTok = current(state)
|
|
53
|
-
if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
|
|
54
|
-
throw invalidLiteralError({
|
|
55
|
-
type: 'interval unit',
|
|
56
|
-
value: unitTok.value,
|
|
57
|
-
positionStart: unitTok.positionStart,
|
|
58
|
-
positionEnd: unitTok.positionEnd,
|
|
59
|
-
validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
consume(state)
|
|
63
|
-
|
|
64
|
-
return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: lastPosition(state) }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
17
|
/**
|
|
68
18
|
* @param {ParserState} state
|
|
69
19
|
* @returns {ExprNode}
|
|
@@ -422,3 +372,45 @@ export function parseSubquery(state) {
|
|
|
422
372
|
expect(state, 'paren', ')')
|
|
423
373
|
return query
|
|
424
374
|
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {ParserState} state
|
|
378
|
+
* @returns {IntervalNode}
|
|
379
|
+
*/
|
|
380
|
+
function parseInterval(state) {
|
|
381
|
+
const { positionStart } = current(state)
|
|
382
|
+
consume(state) // INTERVAL
|
|
383
|
+
|
|
384
|
+
// Get value (number or quoted string)
|
|
385
|
+
const valueTok = current(state)
|
|
386
|
+
/** @type {number} */
|
|
387
|
+
let value
|
|
388
|
+
if (valueTok.type === 'number') {
|
|
389
|
+
consume(state)
|
|
390
|
+
value = Number(valueTok.numericValue)
|
|
391
|
+
} else if (valueTok.type === 'string') {
|
|
392
|
+
consume(state)
|
|
393
|
+
const parsed = parseFloat(valueTok.value)
|
|
394
|
+
if (isNaN(parsed)) {
|
|
395
|
+
throw invalidLiteralError({ type: 'interval value', value: valueTok.value, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
396
|
+
}
|
|
397
|
+
value = parsed
|
|
398
|
+
} else {
|
|
399
|
+
throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, positionStart: valueTok.positionStart, positionEnd: valueTok.positionEnd })
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Get unit keyword
|
|
403
|
+
const unitTok = current(state)
|
|
404
|
+
if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
|
|
405
|
+
throw invalidLiteralError({
|
|
406
|
+
type: 'interval unit',
|
|
407
|
+
value: unitTok.value,
|
|
408
|
+
positionStart: unitTok.positionStart,
|
|
409
|
+
positionEnd: unitTok.positionEnd,
|
|
410
|
+
validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
consume(state)
|
|
414
|
+
|
|
415
|
+
return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: lastPosition(state) }
|
|
416
|
+
}
|
package/src/parse/joins.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { parseExpression } from './expression.js'
|
|
2
2
|
import { parseTableAlias } from './parse.js'
|
|
3
3
|
import { consume, current, expect, expectIdentifier, match } from './state.js'
|
|
4
|
+
import { expectNoAggregate } from '../validation.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import { ExprNode, JoinClause, JoinType, ParserState } from '../types.js'
|
|
8
|
+
*/
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
|
-
* @import { JoinClause, JoinType, ParserState } from '../types.js'
|
|
7
11
|
* @param {ParserState} state
|
|
8
12
|
* @returns {JoinClause[]}
|
|
9
13
|
*/
|
|
@@ -24,21 +28,15 @@ export function parseJoins(state) {
|
|
|
24
28
|
joinType = 'INNER'
|
|
25
29
|
} else if (tok.value === 'LEFT') {
|
|
26
30
|
consume(state)
|
|
27
|
-
|
|
28
|
-
// LEFT OUTER JOIN
|
|
29
|
-
}
|
|
31
|
+
match(state, 'keyword', 'OUTER') // LEFT OUTER JOIN
|
|
30
32
|
joinType = 'LEFT'
|
|
31
33
|
} else if (tok.value === 'RIGHT') {
|
|
32
34
|
consume(state)
|
|
33
|
-
|
|
34
|
-
// RIGHT OUTER JOIN
|
|
35
|
-
}
|
|
35
|
+
match(state, 'keyword', 'OUTER') // RIGHT OUTER JOIN
|
|
36
36
|
joinType = 'RIGHT'
|
|
37
37
|
} else if (tok.value === 'FULL') {
|
|
38
38
|
consume(state)
|
|
39
|
-
|
|
40
|
-
// FULL OUTER JOIN
|
|
41
|
-
}
|
|
39
|
+
match(state, 'keyword', 'OUTER') // FULL OUTER JOIN
|
|
42
40
|
joinType = 'FULL'
|
|
43
41
|
} else if (tok.value === 'POSITIONAL') {
|
|
44
42
|
consume(state)
|
|
@@ -65,11 +63,12 @@ export function parseJoins(state) {
|
|
|
65
63
|
const tableAlias = parseTableAlias(state)
|
|
66
64
|
|
|
67
65
|
// Parse ON condition (not for POSITIONAL joins)
|
|
68
|
-
/** @type {
|
|
66
|
+
/** @type {ExprNode | undefined} */
|
|
69
67
|
let condition
|
|
70
68
|
if (joinType !== 'POSITIONAL') {
|
|
71
69
|
expect(state, 'keyword', 'ON')
|
|
72
70
|
condition = parseExpression(state)
|
|
71
|
+
expectNoAggregate(condition, 'JOIN ON')
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
joins.push({
|
package/src/parse/parse.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseExpression } from './expression.js'
|
|
2
|
-
import { tokenizeSql } from './tokenize.js'
|
|
3
|
-
import { consume, current, expect, expectIdentifier, match, parseError, peekToken } from './state.js'
|
|
4
2
|
import { parseJoins } from './joins.js'
|
|
3
|
+
import { consume, current, expect, expectIdentifier, match, parseError, peekToken } from './state.js'
|
|
4
|
+
import { tokenizeSql } from './tokenize.js'
|
|
5
5
|
import { duplicateCTEError } from '../parseErrors.js'
|
|
6
|
-
import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE, isKnownFunction } from '../validation.js'
|
|
6
|
+
import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE, expectNoAggregate, isKnownFunction } from '../validation.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @import { CTEDefinition, ExprNode, FromSubquery, FromTable, OrderByItem, ParseSqlOptions, ParserState, SelectStatement, SelectColumn, WithClause } from '../types.js'
|
|
@@ -266,12 +266,14 @@ export function parseSelectInternal(state) {
|
|
|
266
266
|
|
|
267
267
|
if (match(state, 'keyword', 'WHERE')) {
|
|
268
268
|
where = parseExpression(state)
|
|
269
|
+
expectNoAggregate(where, 'WHERE')
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
if (match(state, 'keyword', 'GROUP')) {
|
|
272
273
|
expect(state, 'keyword', 'BY')
|
|
273
274
|
while (true) {
|
|
274
275
|
const expr = parseExpression(state)
|
|
276
|
+
expectNoAggregate(expr, 'GROUP BY')
|
|
275
277
|
groupBy.push(expr)
|
|
276
278
|
if (!match(state, 'comma')) break
|
|
277
279
|
}
|
|
@@ -325,6 +327,9 @@ export function parseSelectInternal(state) {
|
|
|
325
327
|
if (!Number.isFinite(n)) {
|
|
326
328
|
throw parseError(state, 'valid LIMIT value')
|
|
327
329
|
}
|
|
330
|
+
if (n < 0) {
|
|
331
|
+
throw parseError(state, 'non-negative LIMIT value')
|
|
332
|
+
}
|
|
328
333
|
limit = n
|
|
329
334
|
|
|
330
335
|
if (match(state, 'keyword', 'OFFSET')) {
|
|
@@ -337,6 +342,9 @@ export function parseSelectInternal(state) {
|
|
|
337
342
|
if (!Number.isFinite(off)) {
|
|
338
343
|
throw parseError(state, 'valid OFFSET value')
|
|
339
344
|
}
|
|
345
|
+
if (off < 0) {
|
|
346
|
+
throw parseError(state, 'non-negative OFFSET value')
|
|
347
|
+
}
|
|
340
348
|
offset = off
|
|
341
349
|
}
|
|
342
350
|
} else if (match(state, 'keyword', 'OFFSET')) {
|
|
@@ -349,6 +357,9 @@ export function parseSelectInternal(state) {
|
|
|
349
357
|
if (!Number.isFinite(off)) {
|
|
350
358
|
throw parseError(state, 'valid OFFSET value')
|
|
351
359
|
}
|
|
360
|
+
if (off < 0) {
|
|
361
|
+
throw parseError(state, 'non-negative OFFSET value')
|
|
362
|
+
}
|
|
352
363
|
offset = off
|
|
353
364
|
}
|
|
354
365
|
|
package/src/parse/state.js
CHANGED
|
@@ -2,6 +2,7 @@ import { syntaxError } from '../parseErrors.js'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @import { ParserState, Token, TokenType } from '../types.js'
|
|
5
|
+
* @import { ParseError } from '../parseErrors.js'
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -93,7 +94,7 @@ export function expectIdentifier(state) {
|
|
|
93
94
|
* Helper function to create consistent parser error messages.
|
|
94
95
|
* @param {ParserState} state
|
|
95
96
|
* @param {string} expected - Description of what was expected
|
|
96
|
-
* @returns {
|
|
97
|
+
* @returns {ParseError}
|
|
97
98
|
*/
|
|
98
99
|
export function parseError(state, expected) {
|
|
99
100
|
const tok = current(state)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UserDefinedFunction } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export interface ParserState {
|
|
4
|
+
tokens: Token[]
|
|
5
|
+
pos: number
|
|
6
|
+
lastPos?: number
|
|
7
|
+
functions?: Record<string, UserDefinedFunction>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Tokenizer types
|
|
11
|
+
export type TokenType =
|
|
12
|
+
| 'keyword'
|
|
13
|
+
| 'identifier'
|
|
14
|
+
| 'number'
|
|
15
|
+
| 'string'
|
|
16
|
+
| 'operator'
|
|
17
|
+
| 'comma'
|
|
18
|
+
| 'dot'
|
|
19
|
+
| 'paren'
|
|
20
|
+
| 'semicolon'
|
|
21
|
+
| 'eof'
|
|
22
|
+
|
|
23
|
+
export interface Token {
|
|
24
|
+
type: TokenType
|
|
25
|
+
value: string
|
|
26
|
+
positionStart: number
|
|
27
|
+
positionEnd: number
|
|
28
|
+
numericValue?: number | bigint
|
|
29
|
+
originalValue?: string
|
|
30
|
+
}
|