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.
@@ -11,150 +11,115 @@
11
11
  * @returns {SqlPrimitive}
12
12
  */
13
13
  export function evaluateMathFunc({ funcName, args }) {
14
- if (funcName === 'FLOOR') {
15
- const val = args[0]
16
- if (val == null) return null
17
- return Math.floor(Number(val))
14
+ // No args
15
+ if (funcName === 'PI') {
16
+ return Math.PI
18
17
  }
19
18
 
20
- if (funcName === 'CEIL' || funcName === 'CEILING') {
21
- const val = args[0]
22
- if (val == null) return null
23
- return Math.ceil(Number(val))
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} Result
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
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} Result
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
- // SQL CONCAT returns NULL if any argument is NULL
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
- rowNumber: rowIndex,
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
- const val = args[0]
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
- rowNumber: rowIndex,
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
- rowNumber: rowIndex,
74
+ rowIndex,
79
75
  })
80
76
  }
81
- return strVal.substring(startIdx, startIdx + len)
77
+ return str.substring(startIdx, startIdx + len)
82
78
  }
83
- return strVal.substring(startIdx)
79
+ return str.substring(startIdx)
84
80
  }
85
81
 
86
82
  if (funcName === 'TRIM') {
87
- const val = args[0]
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 (str == null || searchStr == null || replaceStr == null) return null
98
- return String(str).replaceAll(String(searchStr), String(replaceStr))
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 (str == null || n == null) return null
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
- rowNumber: rowIndex,
104
+ rowIndex,
113
105
  })
114
106
  }
115
- return String(str).substring(0, len)
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 (str == null || n == null) return null
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
- rowNumber: rowIndex,
120
+ rowIndex,
130
121
  })
131
122
  }
132
- const strVal = String(str)
133
- if (len >= strVal.length) return strVal
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 (str == null || search == null) return null
129
+ if (search == null) return null
141
130
  // INSTR returns 1-based position, 0 if not found
142
- const pos = String(str).indexOf(String(search))
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 { AsyncCells, AsyncDataSource, AsyncRow, ExprNode, ParseSqlOptions, SelectStatement, SqlPrimitive, Token, UserDefinedFunction } from './types.js'
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
@@ -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
+ }
@@ -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
- if (match(state, 'keyword', 'OUTER')) {
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
- if (match(state, 'keyword', 'OUTER')) {
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
- if (match(state, 'keyword', 'OUTER')) {
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 {import('../types.js').ExprNode | undefined} */
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({
@@ -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
 
@@ -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 {import('../parseErrors.js').ParseError}
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
+ }