fix: SQL function-call names not flagged as columns

Two layered fixes for the same bug class: `select sum(Age) from
Customers` runs cleanly at the engine but the validator was treating
`sum` as a column reference. The grammar already admits function
calls structurally (ADR-0031 §1: "it does not know which names are
aggregates"); the validator needed to match.

1. Walker (schema_existence_diagnostics): the bare-column check on
   `sql_expr_ident` items now skips when the ident is immediately
   followed by `(` — it's a function-call name, not a column. New
   helper `is_followed_by_call_args` mirrors the existing
   `is_followed_by_qualified_ref` guard. Args inside the call are
   ordinary expressions and their idents still flow through the
   normal bare-column check on subsequent iterations.

   Cascades to: the [ERR] validity indicator (verdict derived from
   diagnostics), the red highlight overlay (renderer overlays
   diagnostic spans), and the ambient hint at complete inputs (the
   diagnostic-driven `pick_hint_diagnostic` path).

2. Typing-time (invalid_ident_at_cursor_in_mode): at any
   `sql_expr_ident` position the partial could resolve to either a
   column reference or a function-call name; without lookahead for a
   trailing `(` we can't tell. The check now returns early at
   `sql_expr_ident` positions. Submit-time still catches genuine
   column typos: the schema-existence diagnostic only skips when the
   ident *is* followed by `(`, so a bare unknown ident still trips
   and the hint surfaces it via `pick_hint_diagnostic`.

Trade-off worth flagging: typing `select Agx` (no FROM yet) is now
silent until FROM is added; previously the typing-time path flagged
it as "No such column". This makes typing-time consistent with
submit-time — the schema-existence pass already silently skips
no-FROM expressions ("no FROM in scope — engine catches"). For any
expression-with-scope (SELECT with FROM, WHERE, etc.) the
diagnostic-driven hint still fires for column typos; new test pins
this.

Tests added (5): walker positive (every standard aggregate plus
count(*), count(distinct …), nested calls, WHERE-clause functions,
and non-aggregate functions), walker negative (unknown column inside
call args), walker negative for DISTINCT-shielded arg, typing-time
positive (no false flag on partial function name), typing-time
trade-off lockdown (genuine column typo still hints when FROM is in
scope).

No grammar change; no ADR amendment (the fix matches ADR-0031 §1's
existing posture). Full suite 2040 passed / 0 failed / 0 unexpected
skips. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-28 22:03:31 +00:00
parent 6f87ad1842
commit 24c268541e
3 changed files with 206 additions and 0 deletions
+15
View File
@@ -1123,6 +1123,21 @@ pub fn invalid_ident_at_cursor_in_mode(
if expected.is_empty() {
return None;
}
// Issue #6 follow-on: at a SQL expression position the partial
// could resolve to either a column reference *or* a function-call
// name (the grammar admits the call shape structurally — see
// sql_expr.rs's `CALL_ARGS` comment, "it does not know which
// names are aggregates"). Without lookahead for a trailing `(`,
// we can't tell here, so we don't flag — submit-time validation
// still catches a genuine column typo via the `unknown_column`
// diagnostic, which only skips when the ident *is* followed by
// `(` (i.e. it really is a function call).
let has_sql_expr_slot = expected.iter().any(|e| {
matches!(e, Expectation::Ident { role: "sql_expr_ident", .. })
});
if has_sql_expr_slot {
return None;
}
// Find every schema-listable source in the expected list.
let sources: Vec<IdentSource> = expected
.iter()