From 9491a167e7ab39f23d9f76f288168ca04ae34c33 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 7 May 2026 05:22:52 -0500 Subject: [PATCH 1/5] Add CASE expression support to tree-sitter grammar (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tree-sitter-ggsql/grammar.js | 40 +++ tree-sitter-ggsql/test/corpus/basic.txt | 372 ++++++++++++++++++++++++ 2 files changed, 412 insertions(+) diff --git a/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index a1b53e87a..f3a9a8f6f 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 43bd28950..2afb1ce33 100644 --- a/tree-sitter-ggsql/test/corpus/basic.txt +++ b/tree-sitter-ggsql/test/corpus/basic.txt @@ -3224,3 +3224,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))))) From fbd69f31ed96137a212636abcc05d134db994a5b Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 18 May 2026 13:38:14 -0500 Subject: [PATCH 2/5] fix(writer): boxplot produces invalid Vega-Lite spec (#449) Skip the boxplot stat's internal "type" aesthetic when building Vega-Lite encoding channels and strip the y2 encoding on whisker/box layers down to just the field reference. Add a schema-validated boxplot test. Closes #448 --- src/writer/vegalite/layer.rs | 5 ++--- src/writer/vegalite/mod.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/writer/vegalite/layer.rs b/src/writer/vegalite/layer.rs index 8f0a7e580..1a31ed85c 100644 --- a/src/writer/vegalite/layer.rs +++ b/src/writer/vegalite/layer.rs @@ -1992,9 +1992,8 @@ impl BoxplotRenderer { // Build encoding templates for y and y2 fields let mut y_encoding = summary_prototype["encoding"][value_var1].clone(); y_encoding["field"] = json!(value_col); - let mut y2_encoding = summary_prototype["encoding"][value_var1].clone(); - y2_encoding["field"] = json!(value2_col); - y2_encoding["title"] = Value::Null; // Suppress y2 title to prevent "y, y2" axis label + // y2 is a secondary position channel — Vega-Lite only allows field/datum/value + let y2_encoding = json!({"field": value2_col}); // Lower whiskers (rule from y to y2, where y=q1 and y2=lower) let mut lower_whiskers = create_layer( diff --git a/src/writer/vegalite/mod.rs b/src/writer/vegalite/mod.rs index 2cefde1fa..8d78f4c9b 100644 --- a/src/writer/vegalite/mod.rs +++ b/src/writer/vegalite/mod.rs @@ -305,6 +305,12 @@ fn build_layer_encoding( continue; } + // Skip structural aesthetics that are consumed by renderers during data + // preparation but are not visual encoding channels in Vega-Lite. + if aesthetic == "geometry" || aesthetic == "type" { + continue; + } + let mut channel_name = map_aesthetic_name(aesthetic, &aesthetic_ctx, coord_kind); // Opacity is retargeted to the fill when fill is supported if channel_name == "opacity" && layer.mappings.contains_key("fill") { @@ -3194,4 +3200,28 @@ mod tests { ); } } + + #[test] + #[cfg(feature = "duckdb")] + fn test_boxplot_schema_validation() { + use crate::reader::{DuckDBReader, Reader}; + + let reader = DuckDBReader::from_connection_string("duckdb://memory").unwrap(); + reader + .execute_sql( + "CREATE TABLE box_data AS + SELECT 'A' AS grp, generate_series * 1.0 AS value FROM GENERATE_SERIES(1, 10) + UNION ALL + SELECT 'B' AS grp, generate_series * 1.0 + 4.0 AS value FROM GENERATE_SERIES(1, 10)", + ) + .unwrap(); + + let spec = reader + .execute("SELECT * FROM box_data VISUALISE grp AS x, value AS y DRAW boxplot") + .unwrap(); + + let writer = VegaLiteWriter::new(); + let json_str = writer.render(&spec).unwrap(); + assert_valid_vegalite(&json_str); + } } From c336c0cccc2901533b83e871a12b6046271ebe32 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sun, 24 May 2026 01:47:31 -0500 Subject: [PATCH 3/5] Support predicates in function arguments (#457) * fix(grammar): support AND/OR in function arguments * fix(grammar): support predicates in function arguments * test(grammar): cover predicate variants in function args * test(grammar): cover named predicate args * fix(grammar): wrap function arg expressions * Avoid parse conflict by left-factoring --------- Co-authored-by: George Stagg --- src/parser/builder.rs | 141 ++++ tree-sitter-ggsql/grammar.js | 77 ++- tree-sitter-ggsql/test/corpus/basic.txt | 859 ++++++++++++++++++++---- 3 files changed, 930 insertions(+), 147 deletions(-) diff --git a/src/parser/builder.rs b/src/parser/builder.rs index 737b376ab..11d7909ff 100644 --- a/src/parser/builder.rs +++ b/src/parser/builder.rs @@ -1935,6 +1935,147 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_boolean_operators_in_function_args() { + let query = r#" + SELECT IFF(x = 'a' OR x = 'b', 1, 0) AS rate + FROM t + VISUALISE rate AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_and_operator_in_function_args() { + let query = r#" + SELECT IFF(x > 0 AND x < 10, 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_between_in_function_args() { + let query = r#" + SELECT IFF(x BETWEEN 1 AND 10, 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_not_operator_in_function_args() { + let query = r#" + SELECT IFF(NOT x = 'a', 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_in_predicate_in_function_args() { + let query = r#" + SELECT IFF(x IN ('a', 'b'), 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_not_in_predicate_in_function_args() { + let query = r#" + SELECT IFF(x NOT IN ('a', 'b'), 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_is_null_in_function_args() { + let query = r#" + SELECT IFF(x IS NULL, 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_like_predicate_in_function_args() { + let query = r#" + SELECT IFF(x LIKE 'a%', 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_ilike_predicate_in_function_args() { + let query = r#" + SELECT IFF(x ILIKE 'a%', 1, 0) AS flag + FROM t + VISUALISE flag AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + + #[test] + fn test_extract_named_arg_predicate_value_spans_full_expression() { + let query = "SELECT FOO(flag => x = 'a' OR x = 'b') FROM t VISUALISE x AS x DRAW point"; + let source = make_source(query); + let root = source.root(); + let named_arg = source.find_node(&root, "(named_arg) @arg").unwrap(); + + let (_, value_node) = extract_name_value_nodes(&named_arg, "named_arg").unwrap(); + assert_eq!(source.get_text(&value_node), "x = 'a' OR x = 'b'"); + } + + #[test] + fn test_named_arg_predicate_in_function_args() { + let query = r#" + SELECT FOO(flag => x = 'a' OR x = 'b') + FROM t + VISUALISE x AS x + DRAW point + "#; + + let result = parse_test_query(query); + assert!(result.is_ok()); + } + // ======================================== // Negative Test Cases - Should Error // ======================================== diff --git a/tree-sitter-ggsql/grammar.js b/tree-sitter-ggsql/grammar.js index f3a9a8f6f..a5f158df1 100644 --- a/tree-sitter-ggsql/grammar.js +++ b/tree-sitter-ggsql/grammar.js @@ -376,13 +376,80 @@ module.exports = grammar({ // Function argument: position or named function_arg: $ => choice( $.named_arg, - $.position_arg + $.function_arg_expression ), named_arg: $ => seq( field('name', $.identifier), choice(':=', '=>'), - field('value', $.position_arg) + field('value', $.function_arg_expression) + ), + + // Function arguments support a slightly richer predicate grammar than the + // generic position_arg rule. + function_arg_expression: $ => prec.left(seq( + $._function_arg_and_expression, + repeat(seq(caseInsensitive('OR'), $._function_arg_and_expression)) + )), + + _function_arg_and_expression: $ => prec.left(seq( + $._function_arg_not_expression, + repeat(seq(caseInsensitive('AND'), $._function_arg_not_expression)) + )), + + _function_arg_not_expression: $ => choice( + $.not_predicate, + $._function_arg_predicate + ), + + not_predicate: $ => prec.right(seq( + caseInsensitive('NOT'), + $._function_arg_not_expression + )), + + _function_arg_predicate: $ => seq( + $.position_arg, + optional($._predicate_suffix) + ), + + _predicate_suffix: $ => choice( + $.between_suffix, + $.in_suffix, + $.is_suffix, + $.like_suffix, + ), + + between_suffix: $ => seq( + optional(caseInsensitive('NOT')), + caseInsensitive('BETWEEN'), + $.position_arg, + caseInsensitive('AND'), + $.position_arg + ), + + in_suffix: $ => seq( + optional(caseInsensitive('NOT')), + caseInsensitive('IN'), + choice($.in_value_list, $.scalar_subquery) + ), + + in_value_list: $ => seq( + '(', + $.position_arg, + repeat(seq(',', $.position_arg)), + ')' + ), + + is_suffix: $ => seq( + caseInsensitive('IS'), + optional(caseInsensitive('NOT')), + caseInsensitive('NULL') + ), + + like_suffix: $ => seq( + optional(caseInsensitive('NOT')), + choice(caseInsensitive('LIKE'), caseInsensitive('ILIKE')), + $.position_arg ), // Position argument: supports complex expressions including: @@ -406,7 +473,11 @@ module.exports = grammar({ // Scalar subquery: (SELECT ...) or (WITH ... SELECT ...) $.scalar_subquery, // Arithmetic/comparison expression (binary operators) - seq($.position_arg, choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), $.position_arg), + seq( + $.position_arg, + choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), + $.position_arg + ), // Parenthesized expression seq('(', $.position_arg, ')') )), diff --git a/tree-sitter-ggsql/test/corpus/basic.txt b/tree-sitter-ggsql/test/corpus/basic.txt index 2afb1ce33..ac0739dba 100644 --- a/tree-sitter-ggsql/test/corpus/basic.txt +++ b/tree-sitter-ggsql/test/corpus/basic.txt @@ -1360,10 +1360,11 @@ DRAW line MAPPING x AS x, total AS y (bare_identifier)) (function_args (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier)))))) + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier))))))) (window_specification (window_order_clause (order_item @@ -1484,14 +1485,16 @@ DRAW point MAPPING interval AS x (named_arg name: (identifier (bare_identifier)) - value: (position_arg - (number)))) + value: (function_arg_expression + (position_arg + (number))))) (function_arg (named_arg name: (identifier (bare_identifier)) - value: (position_arg - (number)))))) + value: (function_arg_expression + (position_arg + (number))))))) (identifier (bare_identifier)) (identifier @@ -2548,15 +2551,16 @@ SELECT SUM(TRY_CAST(price AS INTEGER)) as total FROM data VISUALISE DRAW bar MAP (bare_identifier)) (function_args (function_arg - (position_arg - (cast_expression - (position_arg - (qualified_name + (function_arg_expression + (position_arg + (cast_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (type_name (identifier - (bare_identifier)))) - (type_name - (identifier - (bare_identifier)))))))) + (bare_identifier))))))))) (identifier (bare_identifier)) (identifier @@ -2601,35 +2605,39 @@ DRAW point MAPPING x AS x (bare_identifier)) (function_args (function_arg - (position_arg - (function_call - (identifier - (bare_identifier)) - (function_args - (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier))))))))) + (function_arg_expression + (position_arg + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier))))))))))) (function_arg - (position_arg - (scalar_subquery - (select_statement - (select_body - (function_call - (identifier - (bare_identifier)) - (function_args - (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier))))))) - (from_clause - (table_ref - table: (qualified_name - (identifier - (bare_identifier)))))))))))) + (function_arg_expression + (position_arg + (scalar_subquery + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))))))) + (from_clause + (table_ref + table: (qualified_name + (identifier + (bare_identifier))))))))))))) (from_clause (table_ref table: (qualified_name @@ -3097,10 +3105,11 @@ SELECT COUNT(DISTINCT region) AS n FROM sales VISUALISE n AS x DRAW bar (set_quantifier) (function_args (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier))))))) + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))))))) (identifier (bare_identifier)) (identifier @@ -3144,10 +3153,11 @@ SELECT SUM(ALL price) AS total FROM sales VISUALISE total AS x DRAW bar (set_quantifier) (function_args (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier))))))) + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))))))) (identifier (bare_identifier)) (identifier @@ -3193,10 +3203,11 @@ SELECT x, COUNT(DISTINCT y) OVER (PARTITION BY x) AS n FROM data VISUALISE n AS (set_quantifier) (function_args (function_arg - (position_arg - (qualified_name - (identifier - (bare_identifier)))))) + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier))))))) (window_specification (window_partition_clause (identifier @@ -3225,7 +3236,6 @@ SELECT x, COUNT(DISTINCT y) OVER (PARTITION BY x) AS n FROM data VISUALISE n AS (draw_clause (geom_type))))) - ================================================================================ CASE expression inside aggregate function ================================================================================ @@ -3244,18 +3254,19 @@ SELECT COUNT(CASE WHEN x = 1 THEN 1 END) AS n FROM t VISUALISE n AS y DRAW bar (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))))))) + (function_arg_expression + (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 @@ -3298,21 +3309,22 @@ SELECT SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS n FROM t VISUALISE (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))))))) + (function_arg_expression + (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 @@ -3406,27 +3418,28 @@ SELECT COUNT(CASE WHEN CASE WHEN y = 1 THEN 'a' END = 'a' THEN 1 END) AS n FROM (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))))))) + (function_arg_expression + (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 @@ -3469,21 +3482,22 @@ SELECT COUNT(CASE WHEN status IN ('Charged Off', 'Default') THEN 1 END) AS n FRO (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))))))) + (function_arg_expression + (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 @@ -3528,38 +3542,42 @@ SELECT grade, ROUND(COUNT(CASE WHEN status = 'Default' THEN 1 END) * 100.0 / COU (bare_identifier)) (function_args (function_arg - (position_arg + (function_arg_expression (position_arg + (position_arg + (position_arg + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (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 - (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_expression + (position_arg))))))))) (function_arg - (position_arg - (number))))) + (function_arg_expression + (position_arg + (number)))))) (identifier (bare_identifier)) (identifier @@ -3596,3 +3614,556 @@ SELECT grade, ROUND(COUNT(CASE WHEN status = 'Default' THEN 1 END) * 100.0 / COU (viz_clause (draw_clause (geom_type))))) + +================================================================================ +Boolean operators in function arguments +================================================================================ + +SELECT IFF(x = 'a' OR x = 'b', 1, 0) AS rate FROM t +VISUALISE rate AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (position_arg + (string))) + (position_arg + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (position_arg + (string))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +NOT predicate in function arguments +================================================================================ + +SELECT IFF(NOT x = 'a', 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (not_predicate + (position_arg + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (position_arg + (string)))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +IN predicate in function arguments +================================================================================ + +SELECT IFF(x IN ('a', 'b'), 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (in_suffix + (in_value_list + (position_arg + (string)) + (position_arg + (string)))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +BETWEEN predicate in function arguments +================================================================================ + +SELECT IFF(x BETWEEN 1 AND 10, 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (between_suffix + (position_arg + (number)) + (position_arg + (number))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +IS NULL predicate in function arguments +================================================================================ + +SELECT IFF(x IS NULL, 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (is_suffix))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +LIKE predicate in function arguments +================================================================================ + +SELECT IFF(x LIKE 'a%', 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (like_suffix + (position_arg + (string))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +ILIKE predicate in function arguments +================================================================================ + +SELECT IFF(x ILIKE 'a%', 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (like_suffix + (position_arg + (string))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +NOT IN predicate in function arguments +================================================================================ + +SELECT IFF(x NOT IN ('a', 'b'), 1, 0) AS flag FROM t +VISUALISE flag AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (function_arg_expression + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (in_suffix + (in_value_list + (position_arg + (string)) + (position_arg + (string)))))) + (function_arg + (function_arg_expression + (position_arg + (number)))) + (function_arg + (function_arg_expression + (position_arg + (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))))) + +================================================================================ +Named argument with predicate expression +================================================================================ + +SELECT FOO(flag => x = 'a' OR x = 'b') FROM t +VISUALISE x AS x +DRAW point + +-------------------------------------------------------------------------------- + +(query + (sql_portion + (sql_statement + (select_statement + (select_body + (function_call + (identifier + (bare_identifier)) + (function_args + (function_arg + (named_arg + name: (identifier + (bare_identifier)) + value: (function_arg_expression + (position_arg + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (position_arg + (string))) + (position_arg + (position_arg + (qualified_name + (identifier + (bare_identifier)))) + (position_arg + (string)))))))) + (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))))) + From d539431be88ffc090f35c1dc0646c6ac14de2595 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Wed, 27 May 2026 13:23:15 +0100 Subject: [PATCH 4/5] Bump version to 0.3.3 --- Cargo.lock | 10 +++++----- Cargo.toml | 6 +++--- ggsql-jupyter/pyproject.toml | 2 +- ggsql-vscode/package-lock.json | 4 ++-- ggsql-vscode/package.json | 2 +- ggsql-wasm/demo/package-lock.json | 2 +- tree-sitter-ggsql/bindings/python/__init__.py | 2 +- tree-sitter-ggsql/package.json | 2 +- tree-sitter-ggsql/pyproject.toml | 2 +- tree-sitter-ggsql/tree-sitter.json | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92e97c16a..fbbe10a15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1165,7 +1165,7 @@ dependencies = [ [[package]] name = "ggsql" -version = "0.3.2" +version = "0.3.3" dependencies = [ "arrow", "bytes", @@ -1194,7 +1194,7 @@ dependencies = [ [[package]] name = "ggsql-cli" -version = "0.3.2" +version = "0.3.3" dependencies = [ "anyhow", "clap", @@ -1207,7 +1207,7 @@ dependencies = [ [[package]] name = "ggsql-jupyter" -version = "0.3.2" +version = "0.3.3" dependencies = [ "anyhow", "arrow", @@ -1231,7 +1231,7 @@ dependencies = [ [[package]] name = "ggsql-wasm" -version = "0.3.2" +version = "0.3.3" dependencies = [ "arrow", "getrandom 0.2.17", @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "tree-sitter-ggsql" -version = "0.3.2" +version = "0.3.3" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index 6efd7d5f5..38ec02c5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ default-members = [ resolver = "2" [workspace.package] -version = "0.3.2" +version = "0.3.3" edition = "2021" authors = ["ggsql Team"] license = "MIT" @@ -25,8 +25,8 @@ description = "A declarative visualization language that extends SQL with powerf [workspace.dependencies] # workspace packages -tree-sitter-ggsql = { path = "tree-sitter-ggsql", version = "0.3.2" } -ggsql = { path = "src", version = "0.3.2" } +tree-sitter-ggsql = { path = "tree-sitter-ggsql", version = "0.3.3" } +ggsql = { path = "src", version = "0.3.3" } # Parsing csscolorparser = "0.8.1" diff --git a/ggsql-jupyter/pyproject.toml b/ggsql-jupyter/pyproject.toml index d64486f27..20de6d3df 100644 --- a/ggsql-jupyter/pyproject.toml +++ b/ggsql-jupyter/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ggsql-jupyter" -version = "0.3.2" +version = "0.3.3" description = "Jupyter kernel for ggsql - SQL extension for declarative data visualization" readme = "README.md" license = { text = "MIT" } diff --git a/ggsql-vscode/package-lock.json b/ggsql-vscode/package-lock.json index ea56ff001..5db828808 100644 --- a/ggsql-vscode/package-lock.json +++ b/ggsql-vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "ggsql", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ggsql", - "version": "0.3.2", + "version": "0.3.3", "license": "MIT", "dependencies": { "toml": "^3.0.0" diff --git a/ggsql-vscode/package.json b/ggsql-vscode/package.json index b800f38ec..87d30f8ba 100644 --- a/ggsql-vscode/package.json +++ b/ggsql-vscode/package.json @@ -2,7 +2,7 @@ "name": "ggsql", "displayName": "ggsql", "description": "Syntax highlighting and language runtime for ggsql - SQL with declarative visualization", - "version": "0.3.2", + "version": "0.3.3", "publisher": "ggsql", "engines": { "vscode": "^1.75.0" diff --git a/ggsql-wasm/demo/package-lock.json b/ggsql-wasm/demo/package-lock.json index 47f378949..86e043c17 100644 --- a/ggsql-wasm/demo/package-lock.json +++ b/ggsql-wasm/demo/package-lock.json @@ -24,7 +24,7 @@ }, "../pkg": { "name": "ggsql-wasm", - "version": "0.3.2", + "version": "0.3.3", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { diff --git a/tree-sitter-ggsql/bindings/python/__init__.py b/tree-sitter-ggsql/bindings/python/__init__.py index 4f37f16f7..748952904 100644 --- a/tree-sitter-ggsql/bindings/python/__init__.py +++ b/tree-sitter-ggsql/bindings/python/__init__.py @@ -31,5 +31,5 @@ except Exception as e: raise ImportError(f"Could not load tree-sitter-ggsql language: {e}") -__version__ = "0.3.2" +__version__ = "0.3.3" __all__ = ["language"] diff --git a/tree-sitter-ggsql/package.json b/tree-sitter-ggsql/package.json index 7078cb29b..86773c7ea 100644 --- a/tree-sitter-ggsql/package.json +++ b/tree-sitter-ggsql/package.json @@ -1,6 +1,6 @@ { "name": "tree-sitter-ggsql", - "version": "0.3.2", + "version": "0.3.3", "description": "Tree-sitter grammar for ggsql visualization language", "main": "bindings/node", "repository": { diff --git a/tree-sitter-ggsql/pyproject.toml b/tree-sitter-ggsql/pyproject.toml index ef69dba2e..3efc97013 100644 --- a/tree-sitter-ggsql/pyproject.toml +++ b/tree-sitter-ggsql/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tree-sitter-ggsql" -version = "0.3.2" +version = "0.3.3" description = "Tree-sitter grammar for ggsql visualization language" readme = "README.md" license = {text = "MIT"} diff --git a/tree-sitter-ggsql/tree-sitter.json b/tree-sitter-ggsql/tree-sitter.json index deff9d0c2..a5bd7473c 100644 --- a/tree-sitter-ggsql/tree-sitter.json +++ b/tree-sitter-ggsql/tree-sitter.json @@ -14,7 +14,7 @@ } ], "metadata": { - "version": "0.3.2", + "version": "0.3.3", "license": "MIT", "description": "ggsql grammar for tree-sitter", "authors": [ From c188d4d3d7c08b44db09114625aae4a17698d2a0 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Wed, 27 May 2026 13:23:23 +0100 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49598aef4..040a5445c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## [Unreleased] +## 0.3.3 - 2026-05-27 + +### Fixed + +- Add CASE expression support to tree-sitter grammar (#432) +- Fix Vega-Lite spec emitted for boxplot (#449) +- Support predicates in function arguments (#457) + ## 0.3.2 - 2026-05-05 ### Fixed