fix(completion): treat a bare in-scope table alias as an alias, not an unknown column (#31)

A bare table alias typed where a column is expected — `… GROUP BY o`,
with `o` aliasing `FROM Orders o` — was a blind spot: completion offered
nothing for `o`, and the hint panel called the in-scope alias an unknown
column (`no such column o on table Orders, ...`).

Completion now offers each FROM source's qualifier (alias-if-present-else
table-name) at a bare sql_expr_ident slot, folded into the column
candidate list; on an exact-qualifier partial the alias source steps
aside so the diagnostic can surface. The bare-reference diagnostic arm
emits a targeted `alias_used_as_column` / `table_used_as_column` hint
("`o` is a table alias — write `o.<column>` ...") after the
projection-alias check, so ORDER-BY alias refs still win and a genuine
unknown column still reports `unknown_column`.

Two guards keep the qualified-form advice correct: SQL only (role
`sql_expr_ident`, so the DSL `expr_column` path keeps `unknown_column`
since the DSL has no `table.column` syntax) and effective-qualifier
match (alias-if-present-else-table, so an aliased source referenced by
its shadowed real name falls through rather than being advised as
`name.<column>`). The diagnostic is a drop-in replacement for
`unknown_column` at the same span/Error severity, so verdict/overlay/hint
paths are unchanged.

ADR-0032 Amendment 3; +10 tests.
This commit is contained in:
claude@clouddev1
2026-06-12 14:03:00 +00:00
parent 82b9f7f9b9
commit 7e4bc122be
7 changed files with 417 additions and 1 deletions
+70
View File
@@ -1480,6 +1480,76 @@ accumulators), the per-keystroke re-walk (ADR-0027's
debounced cadence), and the ORDER BY no-fixup-needed
clarification.
## Amendment 3 — bare table aliases in expression slots (2026-06-12)
Issue #31. A bare in-scope table alias typed where the grammar
expects a column — `… GROUP BY o`, with `o` aliasing
`FROM Orders o` — was a blind spot in two surfaces:
- **Completion (§10).** §10.5 narrows columns *past* a
`qualifier .`, but the bare-ident slot before the dot offered
only columns and function names, never the aliases themselves.
A learner mid-typing `o` toward `o.<column>` got no Tab help.
- **Diagnostics (§11.2).** §11.2 added `projection_alias_misplaced`
for a *projection* alias used in a forbidden clause, but a bare
*table* alias fell through to the generic `unknown_column`
bare-reference check (§11.2's `matched.len() == 0` arm), which
reported `no such column \`o\` on table \`Orders, …\`` — calling
an in-scope alias an unknown column.
### What changes
1. **Completion offers in-scope FROM qualifiers at a bare
`sql_expr_ident` slot** (one not already past a `qualifier .`).
Each binding contributes its *qualifier* — the alias if it has
one, else the table name (an aliased source must be referenced
by its alias). Folded into the existing `IdentSource::Columns`
candidate list so it sorts / dedups / colours uniformly. When
the partial *exactly* matches an in-scope qualifier the alias
source steps aside: discoverability is already served, and
suppressing sibling aliases lets the diagnostic below surface
(rather than being hidden by the `typing_over_diag` path).
2. **A bare ident matching an in-scope qualifier now emits a
targeted diagnostic** instead of `unknown_column`, checked in
the `matched.len() == 0` arm *after* the projection-alias check
(so an ORDER-BY projection-alias reference still wins). It is a
drop-in replacement at the same span and `Error` severity — only
the message text changes — so the validity verdict, token
overlay, and hint-panel paths behave exactly as they did for
`unknown_column`:
- `diagnostic.alias_used_as_column` — `` `o` is a table alias —
write `o.<column>` to reference one of its columns `` (the
binding has an alias), or
- `diagnostic.table_used_as_column` — same shape, "is a table"
(an un-aliased table source).
Two guards keep the qualified-form advice correct (both covered
by regression tests):
- **SQL only.** The branch fires only for `role ==
"sql_expr_ident"`. The DSL `Expr` (role `expr_column`) reaches
the same arm but has no `table.column` syntax, so a DSL bare
table-name ref keeps the generic `unknown_column` — advising
the qualified form there would be wrong.
- **Effective-qualifier match.** It matches the binding's
*effective qualifier* — the alias if present, else the table
name — not the table name independently. An aliased source
must be referenced by its alias (`FROM a x … GROUP BY a` is
invalid SQL), so the shadowed real name `a` falls through to
`unknown_column` rather than being advised as `a.<column>`.
This mirrors the completion side's qualifier rule exactly.
A genuine unknown column (matching no alias, table, or column)
still reports `unknown_column` verbatim.
The message tail is deliberately clause-neutral ("to reference
one of its columns") rather than GROUP-BY-specific, because the
bare-reference arm fires across the projection, `WHERE`,
`GROUP BY`, and `HAVING`.
This is an additive refinement of §10 and §11.2; no grammar node
changes.
## See also
- ADR-0005 — the ten-type vocabulary §10 resolves back to.