Skip to content

Commit 23c50f1

Browse files
authored
Add CASE expression support to tree-sitter grammar (#432)
Fixes #431 — CASE...END inside function arguments (e.g. COUNT(CASE WHEN x = 1 THEN 1 END)) previously caused parse errors because END was consumed as a bare identifier. Adds a structural case_expression rule that brackets CASE...END, preventing its interior tokens from leaking into the enclosing function_call.
1 parent 4712868 commit 23c50f1

2 files changed

Lines changed: 412 additions & 0 deletions

File tree

tree-sitter-ggsql/grammar.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ module.exports = grammar({
6060
$.from_clause,
6161
repeat(choice(
6262
$.window_function,
63+
$.case_expression,
6364
$.cast_expression,
6465
$.function_call,
6566
$.non_from_sql_keyword,
@@ -80,6 +81,7 @@ module.exports = grammar({
8081
select_body: $ => prec.left(repeat1(choice(
8182
$.from_clause,
8283
$.window_function, // Window functions like ROW_NUMBER() OVER (...)
84+
$.case_expression, // CASE WHEN ... THEN ... END
8385
$.cast_expression, // CAST(expr AS type), TRY_CAST(expr AS type)
8486
$.function_call, // Regular function calls like COUNT(), SUM()
8587
$.sql_keyword,
@@ -210,6 +212,7 @@ module.exports = grammar({
210212
// Token-by-token fallback for any other subquery content
211213
subquery_body: $ => repeat1(choice(
212214
$.window_function,
215+
$.case_expression,
213216
$.cast_expression,
214217
$.function_call,
215218
$.sql_keyword,
@@ -221,6 +224,41 @@ module.exports = grammar({
221224
token(/[^\s;(),'\"]+/)
222225
)),
223226

227+
// CASE expression: CASE ... END bracketed as a structural unit so that END
228+
// is consumed before the outer function_call's closing ')' can grab it.
229+
case_expression: $ => prec(3, seq(
230+
caseInsensitive('CASE'),
231+
repeat($.case_body_token),
232+
caseInsensitive('END')
233+
)),
234+
235+
case_body_token: $ => choice(
236+
caseInsensitive('WHEN'),
237+
caseInsensitive('THEN'),
238+
caseInsensitive('ELSE'),
239+
$.string,
240+
$.number,
241+
$.case_expression,
242+
$.cast_expression,
243+
$.function_call,
244+
$.subquery, // also handles IN-lists like ('a', 'b')
245+
token('='), token('!='), token('<>'), token('<='), token('>='),
246+
token('<'), token('>'),
247+
token('+'), token('-'), token('*'), token('/'), token('%'), token('||'), token('::'),
248+
caseInsensitive('AND'),
249+
caseInsensitive('OR'),
250+
caseInsensitive('NOT'),
251+
caseInsensitive('IN'),
252+
caseInsensitive('IS'),
253+
caseInsensitive('NULL'),
254+
caseInsensitive('LIKE'),
255+
caseInsensitive('ILIKE'),
256+
caseInsensitive('BETWEEN'),
257+
token(','),
258+
token('.'),
259+
$.identifier,
260+
),
261+
224262
// CAST/TRY_CAST expression: CAST(expr AS type) or TRY_CAST(expr AS type)
225263
// Higher precedence than function_call to win over treating CAST as a regular function
226264
cast_expression: $ => prec(3, seq(
@@ -359,6 +397,8 @@ module.exports = grammar({
359397
$.number,
360398
$.string,
361399
'*',
400+
// CASE expression
401+
$.case_expression,
362402
// CAST/TRY_CAST expression
363403
$.cast_expression,
364404
// Nested function call

0 commit comments

Comments
 (0)