squirreling 0.10.2 → 0.11.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.
@@ -1,5 +1,5 @@
1
- import { isAggregateFunc, validateFunctionArgCount } from '../validation/functions.js'
2
- import { ParseError, syntaxError } from '../validation/parseErrors.js'
1
+ import { isAggregateFunc, isKnownFunction, niladicFuncs, validateFunctionArgs } from '../validation/functions.js'
2
+ import { ParseError, UnknownFunctionError } from '../validation/parseErrors.js'
3
3
  import { parseExpression } from './expression.js'
4
4
  import { consume, current, expect, match } from './state.js'
5
5
 
@@ -8,17 +8,33 @@ import { consume, current, expect, match } from './state.js'
8
8
  */
9
9
 
10
10
  /**
11
- * Parses a function call after the function name has been identified.
12
- * Expects the current token to be '('.
13
- *
14
11
  * @param {ParserState} state
15
- * @param {string} funcName - The function name
16
- * @param {number} positionStart - Start position of the function name
12
+ * @param {number} positionStart
17
13
  * @returns {ExprNode}
18
14
  */
19
- export function parseFunctionCall(state, funcName, positionStart) {
15
+ export function parseFunctionCall(state, positionStart) {
16
+ const funcTok = consume(state)
17
+ const funcName = funcTok.originalValue ?? funcTok.value
20
18
  const funcNameUpper = funcName.toUpperCase()
21
- consume(state) // '(' checked by caller
19
+
20
+ // Validate function existence early for better error messages
21
+ if (!isKnownFunction(funcNameUpper, state.functions)) {
22
+ throw new UnknownFunctionError({ funcName, ...funcTok })
23
+ }
24
+
25
+ // Niladic datetime functions (no parentheses required per ANSI SQL)
26
+ const parens = current(state)
27
+ if (niladicFuncs.includes(funcNameUpper) && parens.type !== 'paren' || parens.value !== '(') {
28
+ return {
29
+ type: 'function',
30
+ funcName,
31
+ args: [],
32
+ positionStart,
33
+ positionEnd: state.lastPos,
34
+ }
35
+ }
36
+
37
+ expect(state, 'paren', '(')
22
38
 
23
39
  /** @type {ExprNode[]} */
24
40
  const args = []
@@ -26,51 +42,30 @@ export function parseFunctionCall(state, funcName, positionStart) {
26
42
  let distinct
27
43
 
28
44
  // Check for DISTINCT or ALL keyword (for aggregate functions like COUNT(DISTINCT x))
29
- if (current(state).type === 'keyword' && current(state).value === 'DISTINCT') {
30
- consume(state)
45
+ if (match(state, 'keyword', 'DISTINCT')) {
31
46
  distinct = true
32
- } else if (current(state).type === 'keyword' && current(state).value === 'ALL') {
33
- consume(state)
47
+ } else {
48
+ match(state, 'keyword', 'ALL')
34
49
  }
35
50
 
36
- if (current(state).type !== 'paren' || current(state).value !== ')') {
37
- while (true) {
38
- // Handle COUNT(*) - treat * as a special identifier
39
- if (current(state).type === 'operator' && current(state).value === '*') {
40
- const starTok = current(state)
41
- consume(state)
42
- args.push({
43
- type: 'star',
44
- positionStart: starTok.positionStart,
45
- positionEnd: state.lastPos,
46
- })
47
- } else {
48
- args.push(parseExpression(state))
49
- }
50
- if (!match(state, 'comma')) break
51
- }
52
- }
53
- expect(state, 'paren', ')')
51
+ // Parse function arguments
52
+ while (true) {
53
+ const next = current(state)
54
+ if (next.type === 'paren' && next.value === ')') break
54
55
 
55
- // Check for FILTER clause (only valid for aggregate functions)
56
- /** @type {ExprNode | undefined} */
57
- let filter
58
- if (current(state).type === 'keyword' && current(state).value === 'FILTER') {
59
- if (!isAggregateFunc(funcNameUpper)) {
60
- throw syntaxError({
61
- expected: 'aggregate function for FILTER clause',
62
- received: `FILTER on non-aggregate function "${funcName}"`,
63
- positionStart: current(state).positionStart,
64
- positionEnd: current(state).positionEnd,
56
+ // Handle COUNT(*) - treat * as a special identifier
57
+ if (match(state, 'operator', '*')) {
58
+ args.push({
59
+ type: 'star',
60
+ positionStart: next.positionStart,
61
+ positionEnd: state.lastPos,
65
62
  })
63
+ } else {
64
+ args.push(parseExpression(state))
66
65
  }
67
- consume(state) // FILTER
68
- expect(state, 'paren', '(')
69
- expect(state, 'keyword', 'WHERE')
70
- filter = parseExpression(state)
71
- expect(state, 'paren', ')')
66
+ if (!match(state, 'comma')) break
72
67
  }
73
- const positionEnd = state.lastPos
68
+ expect(state, 'paren', ')')
74
69
 
75
70
  // Validate star argument at parse time (only COUNT supports *)
76
71
  const hasStar = args.length === 1 && args[0].type === 'star'
@@ -78,19 +73,36 @@ export function parseFunctionCall(state, funcName, positionStart) {
78
73
  throw new ParseError({
79
74
  message: `${funcName} cannot be applied to "*"`,
80
75
  positionStart,
81
- positionEnd,
76
+ positionEnd: state.lastPos,
82
77
  })
83
78
  }
84
79
  if (hasStar && distinct) {
85
80
  throw new ParseError({
86
81
  message: 'COUNT(DISTINCT *) is not allowed',
87
82
  positionStart,
88
- positionEnd,
83
+ positionEnd: state.lastPos,
89
84
  })
90
85
  }
91
86
 
92
87
  // Validate argument count at parse time
93
- validateFunctionArgCount(funcNameUpper, args.length, positionStart, positionEnd, state.functions)
88
+ validateFunctionArgs(funcNameUpper, args.length, positionStart, state.lastPos, state.functions)
89
+
90
+ // Check for FILTER clause (only valid for aggregate functions)
91
+ /** @type {ExprNode | undefined} */
92
+ let filter
93
+ const filterTok = current(state)
94
+ if (match(state, 'keyword', 'FILTER')) {
95
+ if (!isAggregateFunc(funcNameUpper)) {
96
+ throw new ParseError({
97
+ message: `FILTER cannot be applied to non-aggregate function "${funcName}"`,
98
+ ...filterTok,
99
+ })
100
+ }
101
+ expect(state, 'paren', '(')
102
+ expect(state, 'keyword', 'WHERE')
103
+ filter = parseExpression(state)
104
+ expect(state, 'paren', ')')
105
+ }
94
106
 
95
107
  return {
96
108
  type: 'function',
@@ -99,6 +111,6 @@ export function parseFunctionCall(state, funcName, positionStart) {
99
111
  distinct,
100
112
  filter,
101
113
  positionStart,
102
- positionEnd,
114
+ positionEnd: state.lastPos,
103
115
  }
104
116
  }
@@ -1,7 +1,7 @@
1
1
  import { expectNoAggregate } from '../validation/aggregates.js'
2
2
  import { parseExpression } from './expression.js'
3
3
  import { parseTableAlias } from './parse.js'
4
- import { consume, current, expect, expectIdentifier, match } from './state.js'
4
+ import { current, expect, match } from './state.js'
5
5
 
6
6
  /**
7
7
  * @import { ExprNode, JoinClause, JoinType, ParserState } from '../types.js'
@@ -22,45 +22,31 @@ export function parseJoins(state) {
22
22
  /** @type {JoinType} */
23
23
  let joinType = 'INNER'
24
24
 
25
- if (tok.type === 'keyword') {
26
- if (tok.value === 'INNER') {
27
- consume(state)
28
- joinType = 'INNER'
29
- } else if (tok.value === 'LEFT') {
30
- consume(state)
31
- match(state, 'keyword', 'OUTER') // LEFT OUTER JOIN
32
- joinType = 'LEFT'
33
- } else if (tok.value === 'RIGHT') {
34
- consume(state)
35
- match(state, 'keyword', 'OUTER') // RIGHT OUTER JOIN
36
- joinType = 'RIGHT'
37
- } else if (tok.value === 'FULL') {
38
- consume(state)
39
- match(state, 'keyword', 'OUTER') // FULL OUTER JOIN
40
- joinType = 'FULL'
41
- } else if (tok.value === 'POSITIONAL') {
42
- consume(state)
43
- joinType = 'POSITIONAL'
44
- } else if (tok.value === 'JOIN') {
45
- // Just JOIN (defaults to INNER)
46
- consume(state)
47
- } else {
48
- // Not a join keyword, stop parsing joins
49
- break
50
- }
51
-
52
- // If we consumed a join type keyword (INNER/LEFT/RIGHT/FULL), expect JOIN
53
- if (tok.value !== 'JOIN') {
54
- expect(state, 'keyword', 'JOIN')
55
- }
56
- } else {
57
- // No more joins
25
+ if (match(state, 'keyword', 'INNER')) {
26
+ joinType = 'INNER'
27
+ } else if (match(state, 'keyword', 'LEFT')) {
28
+ match(state, 'keyword', 'OUTER') // LEFT OUTER JOIN
29
+ joinType = 'LEFT'
30
+ } else if (match(state, 'keyword', 'RIGHT')) {
31
+ match(state, 'keyword', 'OUTER') // RIGHT OUTER JOIN
32
+ joinType = 'RIGHT'
33
+ } else if (match(state, 'keyword', 'FULL')) {
34
+ match(state, 'keyword', 'OUTER') // FULL OUTER JOIN
35
+ joinType = 'FULL'
36
+ } else if (match(state, 'keyword', 'POSITIONAL')) {
37
+ joinType = 'POSITIONAL'
38
+ } else if (!match(state, 'keyword', 'JOIN')) {
39
+ // Not a join keyword, stop parsing joins
58
40
  break
59
41
  }
60
42
 
43
+ // If we consumed a type keyword, expect JOIN next
44
+ if (tok.value !== 'JOIN') {
45
+ expect(state, 'keyword', 'JOIN')
46
+ }
47
+
61
48
  // Parse table name and optional alias
62
- const tableTok = expectIdentifier(state)
63
- const tableName = tableTok.value
49
+ const tableTok = expect(state, 'identifier')
64
50
  const tableAlias = parseTableAlias(state)
65
51
 
66
52
  // Parse ON condition (not for POSITIONAL joins)
@@ -74,10 +60,10 @@ export function parseJoins(state) {
74
60
 
75
61
  joins.push({
76
62
  joinType,
77
- table: tableName,
63
+ table: tableTok.value,
78
64
  alias: tableAlias,
79
65
  on: condition,
80
- positionStart: tableTok.positionStart,
66
+ positionStart: tok.positionStart,
81
67
  positionEnd: tableTok.positionEnd,
82
68
  })
83
69
  }