Skip to content

Commit 017262f

Browse files
cpsievertclaude
andcommitted
fix(parser): support qualified names and nested functions in SQL
The tree-sitter grammar's `positional_arg` rule was too restrictive, only accepting simple identifiers, numbers, strings, and wildcards. This caused parse failures for common SQL patterns: 1. Table alias prefixes: `SELECT p.name, SUM(s.quantity)` 2. Cross-table arithmetic: `SUM(quantity * price)` 3. Nested function calls: `ROUND(AVG(price), 2)` 4. Full table qualifiers: `products.product_name` The fix extends `positional_arg` to also accept: - `qualified_name` for table.column references - `function_call` for nested functions - Binary arithmetic/comparison expressions - Parenthesized expressions Adds 6 regression tests covering all reported limitations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 41846fe commit 017262f

3 files changed

Lines changed: 120 additions & 6 deletions

File tree

src/parser/builder.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3134,4 +3134,104 @@ mod tests {
31343134
assert!(ok);
31353135
eprintln!("{:?}", palette);
31363136
}
3137+
3138+
// ========================================
3139+
// Parser Limitation Tests (from ggsql-parser-limitations.md)
3140+
// Tests to verify and fix reported parsing issues
3141+
// ========================================
3142+
3143+
#[test]
3144+
fn test_table_alias_prefixes_in_select() {
3145+
// Issue 1: Table alias prefixes in SELECT clause
3146+
// Query like `SELECT p.product_name FROM products p` should parse
3147+
let query = r#"
3148+
SELECT p.product_name, SUM(s.quantity) as total
3149+
FROM sales s JOIN products p ON s.product_id = p.product_id
3150+
GROUP BY p.product_name
3151+
VISUALISE
3152+
DRAW bar MAPPING product_name AS x, total AS y
3153+
"#;
3154+
3155+
let result = parse_test_query(query);
3156+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3157+
}
3158+
3159+
#[test]
3160+
fn test_cross_table_arithmetic() {
3161+
// Issue 2: Cross-table arithmetic expressions
3162+
// `quantity * price` across joined tables should work
3163+
let query = r#"
3164+
WITH t AS (
3165+
SELECT region, SUM(quantity * price) as revenue
3166+
FROM sales JOIN products ON sales.product_id = products.product_id
3167+
GROUP BY region
3168+
)
3169+
VISUALISE FROM t
3170+
DRAW bar MAPPING region AS x, revenue AS y
3171+
"#;
3172+
3173+
let result = parse_test_query(query);
3174+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3175+
}
3176+
3177+
#[test]
3178+
fn test_nested_function_calls() {
3179+
// Issue 3: Nested function calls
3180+
// `ROUND(AVG(price), 2)` should parse
3181+
let query = r#"
3182+
SELECT category, ROUND(AVG(price), 2) as avg_price
3183+
FROM products
3184+
GROUP BY category
3185+
VISUALISE
3186+
DRAW bar MAPPING category AS x, avg_price AS y
3187+
"#;
3188+
3189+
let result = parse_test_query(query);
3190+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3191+
}
3192+
3193+
#[test]
3194+
fn test_full_table_name_qualifiers() {
3195+
// Issue 4: Full table name qualifiers
3196+
// `products.product_name` (full table name, not alias) should work
3197+
let query = r#"
3198+
SELECT products.product_name, SUM(sales.quantity) as total
3199+
FROM sales
3200+
JOIN products ON sales.product_id = products.product_id
3201+
GROUP BY products.product_name
3202+
VISUALISE
3203+
DRAW bar MAPPING product_name AS x, total AS y
3204+
"#;
3205+
3206+
let result = parse_test_query(query);
3207+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3208+
}
3209+
3210+
#[test]
3211+
fn test_simple_cross_table_multiplication() {
3212+
// Simplified version of cross-table arithmetic
3213+
let query = r#"
3214+
SELECT a.x * b.y as result
3215+
FROM a JOIN b ON a.id = b.id
3216+
VISUALISE
3217+
DRAW point MAPPING result AS x
3218+
"#;
3219+
3220+
let result = parse_test_query(query);
3221+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3222+
}
3223+
3224+
#[test]
3225+
fn test_deeply_nested_functions() {
3226+
// Multiple levels of nesting
3227+
let query = r#"
3228+
SELECT COALESCE(NULLIF(TRIM(name), ''), 'Unknown') as clean_name
3229+
FROM data
3230+
VISUALISE
3231+
DRAW bar MAPPING clean_name AS x
3232+
"#;
3233+
3234+
let result = parse_test_query(query);
3235+
assert!(result.is_ok(), "Parse failed: {:?}", result);
3236+
}
31373237
}

tree-sitter-ggsql/grammar.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,25 @@ module.exports = grammar({
276276
field('value', $.positional_arg)
277277
),
278278

279-
positional_arg: $ => choice(
280-
$.identifier,
279+
// Positional argument: supports complex expressions including:
280+
// - Simple values: identifier, number, string, *
281+
// - Qualified names: table.column
282+
// - Nested function calls: ROUND(AVG(x), 2)
283+
// - Arithmetic expressions: quantity * price
284+
// - Type casts: value::type
285+
positional_arg: $ => prec.left(choice(
286+
// Simple values
287+
$.qualified_name, // Handles both simple identifiers and table.column
281288
$.number,
282289
$.string,
283-
'*'
284-
),
290+
'*',
291+
// Nested function call
292+
$.function_call,
293+
// Arithmetic/comparison expression (binary operators)
294+
seq($.positional_arg, choice('+', '-', '*', '/', '%', '||', '::', '<', '>', '<=', '>=', '=', '!=', '<>'), $.positional_arg),
295+
// Parenthesized expression
296+
seq('(', $.positional_arg, ')')
297+
)),
285298

286299
// Namespaced identifier: matches "namespace:name" pattern
287300
// Examples: ggsql:penguins, ggsql:airquality

tree-sitter-ggsql/test/corpus/basic.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,8 +1123,9 @@ DRAW line MAPPING x AS x, total AS y
11231123
(function_args
11241124
(function_arg
11251125
(positional_arg
1126-
(identifier
1127-
(bare_identifier)))))
1126+
(qualified_name
1127+
(identifier
1128+
(bare_identifier))))))
11281129
(window_specification
11291130
(window_order_clause
11301131
(order_item

0 commit comments

Comments
 (0)