Skip to content

Commit c336c0c

Browse files
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 <george.stagg@posit.co>
1 parent fbd69f3 commit c336c0c

3 files changed

Lines changed: 930 additions & 147 deletions

File tree

src/parser/builder.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,6 +1935,147 @@ mod tests {
19351935
assert!(result.is_ok());
19361936
}
19371937

1938+
#[test]
1939+
fn test_boolean_operators_in_function_args() {
1940+
let query = r#"
1941+
SELECT IFF(x = 'a' OR x = 'b', 1, 0) AS rate
1942+
FROM t
1943+
VISUALISE rate AS x
1944+
DRAW point
1945+
"#;
1946+
1947+
let result = parse_test_query(query);
1948+
assert!(result.is_ok());
1949+
}
1950+
1951+
#[test]
1952+
fn test_and_operator_in_function_args() {
1953+
let query = r#"
1954+
SELECT IFF(x > 0 AND x < 10, 1, 0) AS flag
1955+
FROM t
1956+
VISUALISE flag AS x
1957+
DRAW point
1958+
"#;
1959+
1960+
let result = parse_test_query(query);
1961+
assert!(result.is_ok());
1962+
}
1963+
1964+
#[test]
1965+
fn test_between_in_function_args() {
1966+
let query = r#"
1967+
SELECT IFF(x BETWEEN 1 AND 10, 1, 0) AS flag
1968+
FROM t
1969+
VISUALISE flag AS x
1970+
DRAW point
1971+
"#;
1972+
1973+
let result = parse_test_query(query);
1974+
assert!(result.is_ok());
1975+
}
1976+
1977+
#[test]
1978+
fn test_not_operator_in_function_args() {
1979+
let query = r#"
1980+
SELECT IFF(NOT x = 'a', 1, 0) AS flag
1981+
FROM t
1982+
VISUALISE flag AS x
1983+
DRAW point
1984+
"#;
1985+
1986+
let result = parse_test_query(query);
1987+
assert!(result.is_ok());
1988+
}
1989+
1990+
#[test]
1991+
fn test_in_predicate_in_function_args() {
1992+
let query = r#"
1993+
SELECT IFF(x IN ('a', 'b'), 1, 0) AS flag
1994+
FROM t
1995+
VISUALISE flag AS x
1996+
DRAW point
1997+
"#;
1998+
1999+
let result = parse_test_query(query);
2000+
assert!(result.is_ok());
2001+
}
2002+
2003+
#[test]
2004+
fn test_not_in_predicate_in_function_args() {
2005+
let query = r#"
2006+
SELECT IFF(x NOT IN ('a', 'b'), 1, 0) AS flag
2007+
FROM t
2008+
VISUALISE flag AS x
2009+
DRAW point
2010+
"#;
2011+
2012+
let result = parse_test_query(query);
2013+
assert!(result.is_ok());
2014+
}
2015+
2016+
#[test]
2017+
fn test_is_null_in_function_args() {
2018+
let query = r#"
2019+
SELECT IFF(x IS NULL, 1, 0) AS flag
2020+
FROM t
2021+
VISUALISE flag AS x
2022+
DRAW point
2023+
"#;
2024+
2025+
let result = parse_test_query(query);
2026+
assert!(result.is_ok());
2027+
}
2028+
2029+
#[test]
2030+
fn test_like_predicate_in_function_args() {
2031+
let query = r#"
2032+
SELECT IFF(x LIKE 'a%', 1, 0) AS flag
2033+
FROM t
2034+
VISUALISE flag AS x
2035+
DRAW point
2036+
"#;
2037+
2038+
let result = parse_test_query(query);
2039+
assert!(result.is_ok());
2040+
}
2041+
2042+
#[test]
2043+
fn test_ilike_predicate_in_function_args() {
2044+
let query = r#"
2045+
SELECT IFF(x ILIKE 'a%', 1, 0) AS flag
2046+
FROM t
2047+
VISUALISE flag AS x
2048+
DRAW point
2049+
"#;
2050+
2051+
let result = parse_test_query(query);
2052+
assert!(result.is_ok());
2053+
}
2054+
2055+
#[test]
2056+
fn test_extract_named_arg_predicate_value_spans_full_expression() {
2057+
let query = "SELECT FOO(flag => x = 'a' OR x = 'b') FROM t VISUALISE x AS x DRAW point";
2058+
let source = make_source(query);
2059+
let root = source.root();
2060+
let named_arg = source.find_node(&root, "(named_arg) @arg").unwrap();
2061+
2062+
let (_, value_node) = extract_name_value_nodes(&named_arg, "named_arg").unwrap();
2063+
assert_eq!(source.get_text(&value_node), "x = 'a' OR x = 'b'");
2064+
}
2065+
2066+
#[test]
2067+
fn test_named_arg_predicate_in_function_args() {
2068+
let query = r#"
2069+
SELECT FOO(flag => x = 'a' OR x = 'b')
2070+
FROM t
2071+
VISUALISE x AS x
2072+
DRAW point
2073+
"#;
2074+
2075+
let result = parse_test_query(query);
2076+
assert!(result.is_ok());
2077+
}
2078+
19382079
// ========================================
19392080
// Negative Test Cases - Should Error
19402081
// ========================================

tree-sitter-ggsql/grammar.js

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,80 @@ module.exports = grammar({
376376
// Function argument: position or named
377377
function_arg: $ => choice(
378378
$.named_arg,
379-
$.position_arg
379+
$.function_arg_expression
380380
),
381381

382382
named_arg: $ => seq(
383383
field('name', $.identifier),
384384
choice(':=', '=>'),
385-
field('value', $.position_arg)
385+
field('value', $.function_arg_expression)
386+
),
387+
388+
// Function arguments support a slightly richer predicate grammar than the
389+
// generic position_arg rule.
390+
function_arg_expression: $ => prec.left(seq(
391+
$._function_arg_and_expression,
392+
repeat(seq(caseInsensitive('OR'), $._function_arg_and_expression))
393+
)),
394+
395+
_function_arg_and_expression: $ => prec.left(seq(
396+
$._function_arg_not_expression,
397+
repeat(seq(caseInsensitive('AND'), $._function_arg_not_expression))
398+
)),
399+
400+
_function_arg_not_expression: $ => choice(
401+
$.not_predicate,
402+
$._function_arg_predicate
403+
),
404+
405+
not_predicate: $ => prec.right(seq(
406+
caseInsensitive('NOT'),
407+
$._function_arg_not_expression
408+
)),
409+
410+
_function_arg_predicate: $ => seq(
411+
$.position_arg,
412+
optional($._predicate_suffix)
413+
),
414+
415+
_predicate_suffix: $ => choice(
416+
$.between_suffix,
417+
$.in_suffix,
418+
$.is_suffix,
419+
$.like_suffix,
420+
),
421+
422+
between_suffix: $ => seq(
423+
optional(caseInsensitive('NOT')),
424+
caseInsensitive('BETWEEN'),
425+
$.position_arg,
426+
caseInsensitive('AND'),
427+
$.position_arg
428+
),
429+
430+
in_suffix: $ => seq(
431+
optional(caseInsensitive('NOT')),
432+
caseInsensitive('IN'),
433+
choice($.in_value_list, $.scalar_subquery)
434+
),
435+
436+
in_value_list: $ => seq(
437+
'(',
438+
$.position_arg,
439+
repeat(seq(',', $.position_arg)),
440+
')'
441+
),
442+
443+
is_suffix: $ => seq(
444+
caseInsensitive('IS'),
445+
optional(caseInsensitive('NOT')),
446+
caseInsensitive('NULL')
447+
),
448+
449+
like_suffix: $ => seq(
450+
optional(caseInsensitive('NOT')),
451+
choice(caseInsensitive('LIKE'), caseInsensitive('ILIKE')),
452+
$.position_arg
386453
),
387454

388455
// Position argument: supports complex expressions including:
@@ -406,7 +473,11 @@ module.exports = grammar({
406473
// Scalar subquery: (SELECT ...) or (WITH ... SELECT ...)
407474
$.scalar_subquery,
408475
// Arithmetic/comparison expression (binary operators)
409-
seq($.position_arg, choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), $.position_arg),
476+
seq(
477+
$.position_arg,
478+
choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'),
479+
$.position_arg
480+
),
410481
// Parenthesized expression
411482
seq('(', $.position_arg, ')')
412483
)),

0 commit comments

Comments
 (0)