diff --git a/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index 732cfdb6d..e6021a776 100644 --- a/tree-sitter-ggsql/grammar.js +++ b/tree-sitter-ggsql/grammar.js @@ -60,6 +60,7 @@ module.exports = grammar({ $.from_clause, repeat(choice( $.window_function, + $.case_expression, $.cast_expression, $.function_call, $.non_from_sql_keyword, @@ -80,6 +81,7 @@ module.exports = grammar({ select_body: $ => prec.left(repeat1(choice( $.from_clause, $.window_function, // Window functions like ROW_NUMBER() OVER (...) + $.case_expression, // CASE WHEN ... THEN ... END $.cast_expression, // CAST(expr AS type), TRY_CAST(expr AS type) $.function_call, // Regular function calls like COUNT(), SUM() $.sql_keyword, @@ -210,6 +212,7 @@ module.exports = grammar({ // Token-by-token fallback for any other subquery content subquery_body: $ => repeat1(choice( $.window_function, + $.case_expression, $.cast_expression, $.function_call, $.sql_keyword, @@ -221,6 +224,41 @@ module.exports = grammar({ token(/[^\s;(),'\"]+/) )), + // CASE expression: CASE ... END bracketed as a structural unit so that END + // is consumed before the outer function_call's closing ')' can grab it. + case_expression: $ => prec(3, seq( + caseInsensitive('CASE'), + repeat($.case_body_token), + caseInsensitive('END') + )), + + case_body_token: $ => choice( + caseInsensitive('WHEN'), + caseInsensitive('THEN'), + caseInsensitive('ELSE'), + $.string, + $.number, + $.case_expression, + $.cast_expression, + $.function_call, + $.subquery, // also handles IN-lists like ('a', 'b') + token('='), token('!='), token('<>'), token('<='), token('>='), + token('<'), token('>'), + token('+'), token('-'), token('*'), token('/'), token('%'), token('||'), token('::'), + caseInsensitive('AND'), + caseInsensitive('OR'), + caseInsensitive('NOT'), + caseInsensitive('IN'), + caseInsensitive('IS'), + caseInsensitive('NULL'), + caseInsensitive('LIKE'), + caseInsensitive('ILIKE'), + caseInsensitive('BETWEEN'), + token(','), + token('.'), + $.identifier, + ), + // CAST/TRY_CAST expression: CAST(expr AS type) or TRY_CAST(expr AS type) // Higher precedence than function_call to win over treating CAST as a regular function cast_expression: $ => prec(3, seq( @@ -359,6 +397,8 @@ module.exports = grammar({ $.number, $.string, '*', + // CASE expression + $.case_expression, // CAST/TRY_CAST expression $.cast_expression, // Nested function call diff --git a/tree-sitter-ggsql/test/corpus/basic.txt b/tree-sitter-ggsql/test/corpus/basic.txt index ec39fc830..e008b3307 100644 --- a/tree-sitter-ggsql/test/corpus/basic.txt +++ b/tree-sitter-ggsql/test/corpus/basic.txt @@ -3098,3 +3098,375 @@ SELECT x, COUNT(DISTINCT y) OVER (PARTITION BY x) AS n FROM data VISUALISE n AS (viz_clause (draw_clause (geom_type))))) + + +================================================================================ +CASE expression inside aggregate function +================================================================================ + +SELECT COUNT(CASE WHEN x = 1 THEN 1 END) AS n FROM t VISUALISE n AS y DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (number)) + (case_body_token) + (case_body_token + (number))))))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +CASE expression with ELSE inside aggregate function +================================================================================ + +SELECT SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS n FROM t VISUALISE n AS y DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (string)) + (case_body_token) + (case_body_token + (number)) + (case_body_token) + (case_body_token + (number))))))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +CASE expression in select list (not inside function) +================================================================================ + +SELECT CASE WHEN x = 1 THEN 'a' ELSE 'b' END AS lbl FROM t VISUALISE lbl AS x DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (number)) + (case_body_token) + (case_body_token + (string)) + (case_body_token) + (case_body_token + (string))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +Nested CASE expression inside function +================================================================================ + +SELECT COUNT(CASE WHEN CASE WHEN y = 1 THEN 'a' END = 'a' THEN 1 END) AS n FROM t VISUALISE n AS y DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (case_expression + (case_body_token) + (case_body_token + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (number)) + (case_body_token) + (case_body_token + (string)))) + (case_body_token) + (case_body_token + (string)) + (case_body_token) + (case_body_token + (number))))))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +CASE expression with IN list inside aggregate +================================================================================ + +SELECT COUNT(CASE WHEN status IN ('Charged Off', 'Default') THEN 1 END) AS n FROM t VISUALISE n AS y DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (subquery + (subquery_body + (string) + (string)))) + (case_body_token) + (case_body_token + (number))))))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type))))) + +================================================================================ +CASE inside ROUND inside aggregate (real-world pattern) +================================================================================ + +SELECT grade, ROUND(COUNT(CASE WHEN status = 'Default' THEN 1 END) * 100.0 / COUNT(*), 2) AS rate FROM t GROUP BY grade VISUALISE grade AS x, rate AS y DRAW bar + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (identifier + (bare_identifier)) + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (position_arg + (position_arg + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg + (case_expression + (case_body_token) + (case_body_token + (identifier + (bare_identifier))) + (case_body_token) + (case_body_token + (string)) + (case_body_token) + (case_body_token + (number)))))))) + (position_arg + (number))) + (position_arg + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (position_arg))))))) + (function_arg + (position_arg + (number))))) + (identifier + (bare_identifier)) + (identifier + (bare_identifier)) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))) + (sql_keyword + (non_from_sql_keyword)) + (sql_keyword + (non_from_sql_keyword)) + (identifier + (bare_identifier)))))) + (visualise_statement + (visualise_keyword) + (global_mapping + (mapping_list + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))) + (mapping_element + (explicit_mapping + value: (mapping_value + (column_reference + (identifier + (bare_identifier)))) + name: (aesthetic_name))))) + (viz_clause + (draw_clause + (geom_type)))))