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.
@@ -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
@@ -2,3 +2,4 @@ export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
3
  export { collect } from './execute/utils.js'
4
4
  export { cachedDataSource } from './backend/dataSource.js'
5
+ export { ParseError } from './parseErrors.js'
@@ -1,7 +1,7 @@
1
- import { syntaxError } from '../errors.js'
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}"`, position: parenTok.position, after: 'IN' })
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}"`, position: parenTok.position, after: 'IN' })
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
 
@@ -3,11 +3,11 @@ import {
3
3
  missingClauseError,
4
4
  syntaxError,
5
5
  unknownFunctionError,
6
- } from '../errors.js'
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, position: valueTok.position })
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}"`, position: valueTok.position })
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
- position: unitTok.position,
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.position)
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, position: tok.position })
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