squirreling 0.8.0 → 0.9.1

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.
@@ -2,8 +2,9 @@ import { executeSelect } from '../execute/execute.js'
2
2
  import { stringify } from '../execute/utils.js'
3
3
  import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
4
4
  import { unknownFunctionError } from '../parseErrors.js'
5
- import { aggregateError, argValueError, castError } from '../validationErrors.js'
6
5
  import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
6
+ import { aggregateError, argValueError, castError } from '../validationErrors.js'
7
+ import { derivedAlias } from './alias.js'
7
8
  import { applyBinaryOp } from './binary.js'
8
9
  import { applyIntervalToDate } from './date.js'
9
10
  import { evaluateMathFunc } from './math.js'
@@ -11,24 +12,21 @@ import { evaluateRegexpFunc } from './regexp.js'
11
12
  import { evaluateStringFunc } from './strings.js'
12
13
 
13
14
  /**
14
- * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, UserDefinedFunction } from '../types.js'
15
+ * @import { ExprNode, AsyncRow, ExecuteContext, SqlPrimitive } from '../types.js'
15
16
  */
16
17
 
17
18
  /**
18
- * Evaluates an expression node against a row of data (async version)
19
+ * Evaluates an expression node against a row of data
19
20
  *
20
- * @param {Object} params
21
- * @param {ExprNode} params.node - The expression node to evaluate
22
- * @param {AsyncRow} params.row - The data row to evaluate against
23
- * @param {Record<string, AsyncDataSource>} params.tables
24
- * @param {Record<string, UserDefinedFunction>} [params.functions] - User-defined functions
25
- * @param {number} [params.rowIndex] - 1-based row index for error reporting
26
- * @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
27
- * @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
28
- * @param {AbortSignal} [params.signal] - abort signal for cancellation
21
+ * @param {Object} options
22
+ * @param {ExprNode} options.node - The expression node to evaluate
23
+ * @param {AsyncRow} options.row - The data row to evaluate against
24
+ * @param {number} [options.rowIndex] - 1-based row index for error reporting
25
+ * @param {AsyncRow[]} [options.rows] - Group of rows for aggregate functions (undefined if not in aggregate context)
26
+ * @param {ExecuteContext} options.context - execution context (tables, functions, signal)
29
27
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
30
28
  */
31
- export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases, signal }) {
29
+ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
32
30
  if (node.type === 'literal') {
33
31
  return node.value
34
32
  }
@@ -45,10 +43,6 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
45
43
  return row.cells[colName]()
46
44
  }
47
45
  }
48
- // Check if it's a SELECT alias (for ORDER BY)
49
- if (aliases?.has(node.name)) {
50
- return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases, signal })
51
- }
52
46
  // Unknown identifier
53
47
  throw columnNotFoundError({
54
48
  columnName: node.name,
@@ -61,7 +55,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
61
55
 
62
56
  // Scalar subquery - returns a single value
63
57
  if (node.type === 'subquery') {
64
- const gen = executeSelect({ select: node.subquery, tables, functions, signal })
58
+ const gen = executeSelect({ select: node.subquery, context })
65
59
  const { value } = await gen.next() // Start the generator
66
60
  gen.return(undefined) // Stop further execution
67
61
  if (!value) return null
@@ -70,7 +64,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
70
64
 
71
65
  // Unary operators
72
66
  if (node.type === 'unary') {
73
- const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases, signal })
67
+ const val = await evaluateExpr({ node: node.argument, row, rowIndex, rows, context })
74
68
  if (node.op === '-') {
75
69
  if (val == null) return null
76
70
  return -val
@@ -84,21 +78,21 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
84
78
  if (node.type === 'binary') {
85
79
  // Handle date +/- interval
86
80
  if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
87
- const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
81
+ const dateVal = await evaluateExpr({ node: node.left, row, rowIndex, rows, context })
88
82
  return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
89
83
  }
90
84
  if (node.op === '+' && node.left.type === 'interval') {
91
- const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
85
+ const dateVal = await evaluateExpr({ node: node.right, row, rowIndex, rows, context })
92
86
  return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
93
87
  }
94
88
 
95
- const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases, signal })
89
+ const left = await evaluateExpr({ node: node.left, row, rowIndex, rows, context })
96
90
 
97
91
  // Short-circuit evaluation for AND and OR
98
92
  if (node.op === 'AND' && !left) return false
99
93
  if (node.op === 'OR' && left) return true
100
94
 
101
- const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases, signal })
95
+ const right = await evaluateExpr({ node: node.right, row, rowIndex, rows, context })
102
96
  return applyBinaryOp(node.op, left, right)
103
97
  }
104
98
 
@@ -109,10 +103,18 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
109
103
  // Handle aggregate functions
110
104
  if (isAggregateFunc(funcName)) {
111
105
  if (!rows) {
112
- throw aggregateError({
113
- funcName,
114
- issue: ' is not allowed outside of aggregate context',
115
- })
106
+ // Aggregate function used outside of aggregate context
107
+ // This is only allowed if same aggregate was in the SELECT list
108
+ const alias = derivedAlias(node)
109
+ if (row.columns.includes(alias)) {
110
+ return row.cells[alias]()
111
+ } else {
112
+ throw aggregateError({
113
+ funcName,
114
+ positionStart: node.positionStart,
115
+ positionEnd: node.positionEnd,
116
+ })
117
+ }
116
118
  }
117
119
 
118
120
  // Apply FILTER clause if present
@@ -120,20 +122,14 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
120
122
  if (node.filter) {
121
123
  filteredRows = []
122
124
  for (const row of rows) {
123
- const passes = await evaluateExpr({ node: node.filter, row, tables, functions, signal })
125
+ const passes = await evaluateExpr({ node: node.filter, row, context })
124
126
  if (passes) filteredRows.push(row)
125
127
  }
126
128
  }
127
129
 
128
- // Check for star argument (COUNT(*))
129
- if (node.args.length === 1 && node.args[0].type === 'identifier' && node.args[0].name === '*') {
130
- if (funcName === 'COUNT') {
131
- return filteredRows.length
132
- }
133
- throw aggregateError({
134
- funcName,
135
- issue: '(*) is not supported. Only COUNT supports *.',
136
- })
130
+ // Handle COUNT(*) special case
131
+ if (node.args.length === 1 && node.args[0].type === 'identifier' && funcName === 'COUNT' && node.args[0].name === '*') {
132
+ return filteredRows.length
137
133
  }
138
134
 
139
135
  const argNode = node.args[0]
@@ -142,14 +138,14 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
142
138
  if (node.distinct) {
143
139
  const seen = new Set()
144
140
  for (const row of filteredRows) {
145
- const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
141
+ const v = await evaluateExpr({ node: argNode, row, context })
146
142
  if (v != null) seen.add(v)
147
143
  }
148
144
  return seen.size
149
145
  }
150
146
  let count = 0
151
147
  for (const row of filteredRows) {
152
- const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
148
+ const v = await evaluateExpr({ node: argNode, row, context })
153
149
  if (v != null) count++
154
150
  }
155
151
  return count
@@ -164,7 +160,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
164
160
  let max = null
165
161
 
166
162
  for (const row of filteredRows) {
167
- const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
163
+ const raw = await evaluateExpr({ node: argNode, row, context })
168
164
  if (raw == null) continue
169
165
  const num = Number(raw)
170
166
  if (!Number.isFinite(num)) continue
@@ -189,7 +185,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
189
185
  if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
190
186
  const values = []
191
187
  for (const row of filteredRows) {
192
- const raw = await evaluateExpr({ node: argNode, row, tables, functions, signal })
188
+ const raw = await evaluateExpr({ node: argNode, row, context })
193
189
  if (raw == null) continue
194
190
  const num = Number(raw)
195
191
  if (!Number.isFinite(num)) continue
@@ -211,7 +207,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
211
207
  if (node.distinct) {
212
208
  const seen = new Set()
213
209
  for (const row of filteredRows) {
214
- const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
210
+ const v = await evaluateExpr({ node: argNode, row, context })
215
211
  const key = stringify(v)
216
212
  if (!seen.has(key)) {
217
213
  seen.add(key)
@@ -220,7 +216,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
220
216
  }
221
217
  } else {
222
218
  for (const row of filteredRows) {
223
- const v = await evaluateExpr({ node: argNode, row, tables, functions, signal })
219
+ const v = await evaluateExpr({ node: argNode, row, context })
224
220
  values.push(v)
225
221
  }
226
222
  }
@@ -229,7 +225,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
229
225
  }
230
226
 
231
227
  /** @type {SqlPrimitive[]} */
232
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases, signal })))
228
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
233
229
 
234
230
  if (isStringFunc(funcName)) {
235
231
  return evaluateStringFunc({
@@ -258,7 +254,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
258
254
  if (funcName === 'COALESCE') {
259
255
  // Short-circuit: evaluate args one at a time, return first non-null
260
256
  for (const arg of node.args) {
261
- const val = await evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, signal })
257
+ const val = await evaluateExpr({ node: arg, row, rowIndex, rows, context })
262
258
  if (val != null) return val
263
259
  }
264
260
  return null
@@ -266,8 +262,8 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
266
262
 
267
263
  if (funcName === 'NULLIF') {
268
264
  // NULLIF(a, b) returns null if a = b, otherwise returns a
269
- const val1 = await evaluateExpr({ node: node.args[0], row, tables, functions, rowIndex, rows, signal })
270
- const val2 = await evaluateExpr({ node: node.args[1], row, tables, functions, rowIndex, rows, signal })
265
+ const val1 = await evaluateExpr({ node: node.args[0], row, rowIndex, rows, context })
266
+ const val2 = await evaluateExpr({ node: node.args[1], row, rowIndex, rows, context })
271
267
  return val1 == val2 ? null : val1
272
268
  }
273
269
 
@@ -370,6 +366,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
370
366
  }
371
367
 
372
368
  // Check user-defined functions (case-insensitive lookup)
369
+ const { functions } = context
373
370
  if (functions) {
374
371
  const udfName = Object.keys(functions).find(k => k.toUpperCase() === funcName)
375
372
  if (udfName) {
@@ -385,7 +382,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
385
382
  }
386
383
 
387
384
  if (node.type === 'cast') {
388
- const val = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
385
+ const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
389
386
  if (val == null) return null
390
387
  const toType = node.toType.toUpperCase()
391
388
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
@@ -428,17 +425,17 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
428
425
 
429
426
  // IN and NOT IN with value lists
430
427
  if (node.type === 'in valuelist') {
431
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
428
+ const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
432
429
  for (const valueNode of node.values) {
433
- const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows, signal })
430
+ const val = await evaluateExpr({ node: valueNode, row, rowIndex, rows, context })
434
431
  if (exprVal == val) return true
435
432
  }
436
433
  return false
437
434
  }
438
435
  // IN with subqueries
439
436
  if (node.type === 'in') {
440
- const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows, signal })
441
- const results = executeSelect({ select: node.subquery, tables, functions, signal })
437
+ const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
438
+ const results = executeSelect({ select: node.subquery, context })
442
439
  for await (const resRow of results) {
443
440
  const value = await resRow.cells[resRow.columns[0]]()
444
441
  if (exprVal == value) return true
@@ -448,39 +445,39 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
448
445
 
449
446
  // EXISTS and NOT EXISTS with subqueries
450
447
  if (node.type === 'exists') {
451
- const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
448
+ const results = await executeSelect({ select: node.subquery, context }).next()
452
449
  return results.done === false
453
450
  }
454
451
  if (node.type === 'not exists') {
455
- const results = await executeSelect({ select: node.subquery, tables, functions, signal }).next()
452
+ const results = await executeSelect({ select: node.subquery, context }).next()
456
453
  return results.done === true
457
454
  }
458
455
 
459
456
  // CASE expressions
460
457
  if (node.type === 'case') {
461
458
  // For simple CASE: evaluate the case expression once
462
- const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, functions, rowIndex, rows, signal })
459
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, rowIndex, rows, context })
463
460
 
464
461
  // Iterate through WHEN clauses
465
462
  for (const whenClause of node.whenClauses) {
466
463
  let conditionResult
467
464
  if (caseValue !== undefined) {
468
465
  // Simple CASE: compare caseValue with condition
469
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
466
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
470
467
  conditionResult = caseValue == whenValue
471
468
  } else {
472
469
  // Searched CASE: evaluate condition as boolean
473
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows, signal })
470
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
474
471
  }
475
472
 
476
473
  if (conditionResult) {
477
- return evaluateExpr({ node: whenClause.result, row, tables, functions, rowIndex, rows, signal })
474
+ return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
478
475
  }
479
476
  }
480
477
 
481
478
  // No WHEN clause matched, return ELSE result or NULL
482
479
  if (node.elseResult) {
483
- return evaluateExpr({ node: node.elseResult, row, tables, functions, rowIndex, rows, signal })
480
+ return evaluateExpr({ node: node.elseResult, row, rowIndex, rows, context })
484
481
  }
485
482
  return null
486
483
  }
package/src/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ParseSqlOptions, SelectStatement, SqlPrimitive, Token } from './types.js'
1
+ import type { AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, ParseSqlOptions, PlanSqlOptions, QueryPlan, SelectStatement, SqlPrimitive, Token } from './types.js'
2
2
  export type {
3
3
  AsyncCells,
4
4
  AsyncDataSource,
5
5
  AsyncRow,
6
+ ExecuteContext,
6
7
  ExecuteSqlOptions,
7
8
  ExprNode,
8
9
  ParseSqlOptions,
10
+ PlanSqlOptions,
9
11
  QueryPlan,
10
12
  ScanOptions,
11
13
  ScanResults,
@@ -16,7 +18,7 @@ export type {
16
18
  } from './types.js'
17
19
 
18
20
  /**
19
- * Executes a SQL SELECT query against an array of data rows
21
+ * Executes a SQL SELECT query against tables
20
22
  *
21
23
  * @param options
22
24
  * @param options.tables - source data as a list of objects or an AsyncDataSource
@@ -27,6 +29,16 @@ export type {
27
29
  */
28
30
  export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<AsyncRow>
29
31
 
32
+ /**
33
+ * Executes a query plan and yields result rows
34
+ *
35
+ * @param options
36
+ * @param options.plan - the query plan to execute
37
+ * @param options.context - execution context with tables, functions, and signal
38
+ * @returns async generator yielding result rows
39
+ */
40
+ export function executePlan(options: { plan: QueryPlan, context: ExecuteContext }): AsyncGenerator<AsyncRow>
41
+
30
42
  /**
31
43
  * Parses a SQL query string into an abstract syntax tree
32
44
  *
@@ -37,6 +49,16 @@ export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<AsyncRow>
37
49
  */
38
50
  export function parseSql(options: ParseSqlOptions): SelectStatement
39
51
 
52
+ /**
53
+ * Builds a query plan from a SQL query string or AST
54
+ *
55
+ * @param options
56
+ * @param options.query - SQL query string or parsed SelectStatement
57
+ * @param options.functions - user-defined functions available in the SQL context
58
+ * @returns the root of the query plan tree
59
+ */
60
+ export function planSql(options: PlanSqlOptions): QueryPlan
61
+
40
62
  /**
41
63
  * Tokenizes a SQL query string into an array of tokens
42
64
  *
@@ -54,3 +76,12 @@ export function tokenizeSql(sql: string): Token[]
54
76
  export function collect<T>(asyncGen: AsyncGenerator<AsyncRow>): Promise<Record<string, SqlPrimitive>[]>
55
77
 
56
78
  export function cachedDataSource(source: AsyncDataSource): AsyncDataSource
79
+
80
+ /**
81
+ * Generates a default alias for a derived column expression.
82
+ * Useful for generating column names pre-execution.
83
+ *
84
+ * @param expr - the expression node
85
+ * @returns the generated alias
86
+ */
87
+ export function derivedAlias(expr: ExprNode): string
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
- export { executeSql } from './execute/execute.js'
1
+ export { executePlan, executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
+ export { planSql } from './plan/plan.js'
3
4
  export { tokenizeSql } from './parse/tokenize.js'
4
5
  export { collect } from './execute/utils.js'
5
6
  export { cachedDataSource } from './backend/dataSource.js'
6
- export { ParseError } from './parseErrors.js'
7
+ export { derivedAlias } from './expression/alias.js'
@@ -1,7 +1,7 @@
1
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, lastPosition, match, peekToken } from './state.js'
4
+ import { consume, current, expect, match, peekToken } from './state.js'
5
5
 
6
6
  /**
7
7
  * @import { ExprNode, ParserState } from '../types.js'
@@ -27,7 +27,7 @@ export function parseComparison(state) {
27
27
  op: 'IS NOT NULL',
28
28
  argument: left,
29
29
  positionStart: left.positionStart,
30
- positionEnd: lastPosition(state),
30
+ positionEnd: state.lastPos,
31
31
  }
32
32
  }
33
33
  expect(state, 'keyword', 'NULL')
@@ -36,7 +36,7 @@ export function parseComparison(state) {
36
36
  op: 'IS NULL',
37
37
  argument: left,
38
38
  positionStart: left.positionStart,
39
- positionEnd: lastPosition(state),
39
+ positionEnd: state.lastPos,
40
40
  }
41
41
  }
42
42
 
@@ -134,7 +134,7 @@ export function parseComparison(state) {
134
134
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
135
135
  // Subquery - let parseSubquery handle the parens
136
136
  const subquery = parseSubquery(state)
137
- const positionEnd = lastPosition(state)
137
+ const positionEnd = state.lastPos
138
138
  return {
139
139
  type: 'unary',
140
140
  op: 'NOT',
@@ -158,7 +158,7 @@ export function parseComparison(state) {
158
158
  if (!match(state, 'comma')) break
159
159
  }
160
160
  expect(state, 'paren', ')')
161
- const positionEnd = lastPosition(state)
161
+ const positionEnd = state.lastPos
162
162
  return {
163
163
  type: 'unary',
164
164
  op: 'NOT',
@@ -194,7 +194,7 @@ export function parseComparison(state) {
194
194
  expr: left,
195
195
  subquery,
196
196
  positionStart: left.positionStart,
197
- positionEnd: lastPosition(state),
197
+ positionEnd: state.lastPos,
198
198
  }
199
199
  } else {
200
200
  // Parse list of values - we handle the parens
@@ -211,7 +211,7 @@ export function parseComparison(state) {
211
211
  expr: left,
212
212
  values,
213
213
  positionStart: left.positionStart,
214
- positionEnd: lastPosition(state),
214
+ positionEnd: state.lastPos,
215
215
  }
216
216
  }
217
217
  }
@@ -8,7 +8,7 @@ import { isIntervalUnit, isKnownFunction } from '../validation.js'
8
8
  import { parseComparison } from './comparison.js'
9
9
  import { parseFunctionCall } from './functions.js'
10
10
  import { parseSelectInternal } from './parse.js'
11
- import { consume, current, expect, expectIdentifier, lastPosition, match, peekToken } from './state.js'
11
+ import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
12
12
 
13
13
  /**
14
14
  * @import { ExprNode, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
@@ -40,7 +40,7 @@ export function parsePrimary(state) {
40
40
  type: 'subquery',
41
41
  subquery,
42
42
  positionStart,
43
- positionEnd: lastPosition(state),
43
+ positionEnd: state.lastPos,
44
44
  }
45
45
  }
46
46
  // Regular grouped expression
@@ -66,7 +66,7 @@ export function parsePrimary(state) {
66
66
  expr,
67
67
  toType: typeTok.value,
68
68
  positionStart,
69
- positionEnd: lastPosition(state),
69
+ positionEnd: state.lastPos,
70
70
  }
71
71
  }
72
72
 
@@ -96,7 +96,7 @@ export function parsePrimary(state) {
96
96
  name: tok.value,
97
97
  args: [],
98
98
  positionStart,
99
- positionEnd: lastPosition(state),
99
+ positionEnd: state.lastPos,
100
100
  }
101
101
  }
102
102
 
@@ -114,7 +114,7 @@ export function parsePrimary(state) {
114
114
  type: 'identifier',
115
115
  name,
116
116
  positionStart,
117
- positionEnd: lastPosition(state),
117
+ positionEnd: state.lastPos,
118
118
  }
119
119
  }
120
120
 
@@ -124,7 +124,7 @@ export function parsePrimary(state) {
124
124
  type: 'literal',
125
125
  value: tok.numericValue ?? null,
126
126
  positionStart,
127
- positionEnd: lastPosition(state),
127
+ positionEnd: state.lastPos,
128
128
  }
129
129
  }
130
130
 
@@ -134,7 +134,7 @@ export function parsePrimary(state) {
134
134
  type: 'literal',
135
135
  value: tok.value,
136
136
  positionStart,
137
- positionEnd: lastPosition(state),
137
+ positionEnd: state.lastPos,
138
138
  }
139
139
  }
140
140
 
@@ -148,15 +148,15 @@ export function parsePrimary(state) {
148
148
 
149
149
  if (tok.value === 'TRUE') {
150
150
  consume(state)
151
- return { type: 'literal', value: true, positionStart, positionEnd: lastPosition(state) }
151
+ return { type: 'literal', value: true, positionStart, positionEnd: state.lastPos }
152
152
  }
153
153
  if (tok.value === 'FALSE') {
154
154
  consume(state)
155
- return { type: 'literal', value: false, positionStart, positionEnd: lastPosition(state) }
155
+ return { type: 'literal', value: false, positionStart, positionEnd: state.lastPos }
156
156
  }
157
157
  if (tok.value === 'NULL') {
158
158
  consume(state)
159
- return { type: 'literal', value: null, positionStart, positionEnd: lastPosition(state) }
159
+ return { type: 'literal', value: null, positionStart, positionEnd: state.lastPos }
160
160
  }
161
161
  if (tok.value === 'EXISTS') {
162
162
  consume(state) // EXISTS
@@ -165,7 +165,7 @@ export function parsePrimary(state) {
165
165
  type: 'exists',
166
166
  subquery,
167
167
  positionStart,
168
- positionEnd: lastPosition(state),
168
+ positionEnd: state.lastPos,
169
169
  }
170
170
  }
171
171
  if (tok.value === 'CASE') {
@@ -212,7 +212,7 @@ export function parsePrimary(state) {
212
212
  whenClauses,
213
213
  elseResult,
214
214
  positionStart,
215
- positionEnd: lastPosition(state),
215
+ positionEnd: state.lastPos,
216
216
  }
217
217
  }
218
218
  if (tok.value === 'INTERVAL') {
@@ -293,7 +293,7 @@ function parseNot(state) {
293
293
  type: 'not exists',
294
294
  subquery,
295
295
  positionStart,
296
- positionEnd: lastPosition(state),
296
+ positionEnd: state.lastPos,
297
297
  }
298
298
  }
299
299
  const argument = parseNot(state)
@@ -412,5 +412,5 @@ function parseInterval(state) {
412
412
  }
413
413
  consume(state)
414
414
 
415
- return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: lastPosition(state) }
415
+ return { type: 'interval', value, unit: unitTok.value, positionStart, positionEnd: state.lastPos }
416
416
  }
@@ -1,7 +1,7 @@
1
- import { argCountParseError, syntaxError } from '../parseErrors.js'
1
+ import { ParseError, argCountParseError, syntaxError } from '../parseErrors.js'
2
2
  import { isAggregateFunc, validateFunctionArgCount } from '../validation.js'
3
3
  import { parseExpression } from './expression.js'
4
- import { consume, current, expect, lastPosition, match } from './state.js'
4
+ import { consume, current, expect, match } from './state.js'
5
5
 
6
6
  /**
7
7
  * @import { ExprNode, ParserState } from '../types.js'
@@ -41,7 +41,7 @@ export function parseFunctionCall(state, funcName, positionStart) {
41
41
  type: 'identifier',
42
42
  name: '*',
43
43
  positionStart: starTok.positionStart,
44
- positionEnd: lastPosition(state),
44
+ positionEnd: state.lastPos,
45
45
  })
46
46
  } else {
47
47
  args.push(parseExpression(state))
@@ -72,8 +72,25 @@ export function parseFunctionCall(state, funcName, positionStart) {
72
72
  expect(state, 'paren', ')')
73
73
  }
74
74
 
75
- // Validate argument count at parse time
75
+ // Validate star argument at parse time (only COUNT supports *)
76
76
  const funcNameUpper = funcName.toUpperCase()
77
+ const hasStar = args.length === 1 && args[0].type === 'identifier' && args[0].name === '*'
78
+ if (hasStar && isAggregateFunc(funcNameUpper) && funcNameUpper !== 'COUNT') {
79
+ throw new ParseError({
80
+ message: `${funcName} cannot be applied to "*"`,
81
+ positionStart,
82
+ positionEnd: state.lastPos,
83
+ })
84
+ }
85
+ if (hasStar && distinct) {
86
+ throw new ParseError({
87
+ message: 'COUNT(DISTINCT *) is not allowed',
88
+ positionStart,
89
+ positionEnd: state.lastPos,
90
+ })
91
+ }
92
+
93
+ // Validate argument count at parse time
77
94
  const validation = validateFunctionArgCount(funcNameUpper, args.length, state.functions)
78
95
  if (!validation.valid) {
79
96
  throw argCountParseError({
@@ -81,7 +98,7 @@ export function parseFunctionCall(state, funcName, positionStart) {
81
98
  expected: validation.expected,
82
99
  received: args.length,
83
100
  positionStart,
84
- positionEnd: lastPosition(state),
101
+ positionEnd: state.lastPos,
85
102
  })
86
103
  }
87
104
 
@@ -92,6 +109,6 @@ export function parseFunctionCall(state, funcName, positionStart) {
92
109
  distinct: distinct || undefined,
93
110
  filter,
94
111
  positionStart,
95
- positionEnd: lastPosition(state),
112
+ positionEnd: state.lastPos,
96
113
  }
97
114
  }