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
+59
View File
@@ -1765,6 +1765,65 @@ mod tests {
cache
}
fn issue31_join_cache() -> crate::completion::SchemaCache {
use crate::completion::{SchemaCache, TableColumn};
use crate::dsl::types::Type;
let mut cache = SchemaCache::default();
let tables: &[(&str, &[(&str, Type)])] = &[
("Customers", &[("id", Type::Serial), ("name", Type::Text)]),
(
"Products",
&[("id", Type::Serial), ("name", Type::Text), ("price", Type::Decimal)],
),
(
"OrderLines",
&[
("id", Type::Serial),
("order_id", Type::Int),
("product_id", Type::Int),
("count", Type::Int),
],
),
(
"Orders",
&[("id", Type::Serial), ("customer_id", Type::Int), ("date", Type::Date)],
),
];
for (t, cols) in tables {
cache.tables.push((*t).to_string());
let tc: Vec<TableColumn> =
cols.iter().map(|(n, ty)| TableColumn::new(*n, *ty)).collect();
for c in &tc {
cache.columns.push(c.name.clone());
}
cache.table_columns.insert((*t).to_string(), tc);
}
cache
}
#[test]
fn issue31_group_by_partial_alias_shows_alias_hint() {
// Issue #31 end-to-end: the manual-testing query ended in
// `… group by o`, where `o` aliases `Orders`. The ambient
// hint must guide the learner to `o.<column>`, not claim
// `o` is an unknown column.
let cache = issue31_join_cache();
let input = "select c.name as customer_name, o.id as order_id, o.date, sum(ol.count*p.price) as total from Orders o join OrderLines ol on o.id=ol.order_id join Products p on p.id=ol.product_id join Customers c on c.id=o.customer_id group by o";
match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced) {
Some(AmbientHint::Prose(p)) => {
assert!(
p.contains("`o` is a table alias") && p.contains("o.<column>"),
"expected the alias hint; got: {p:?}",
);
assert!(
!p.contains("no such column"),
"must not show the misleading unknown-column message; got: {p:?}",
);
}
other => panic!("expected a Prose alias hint; got: {other:?}"),
}
}
#[test]
fn ambient_hint_at_insert_first_value_shows_int_prose() {
use crate::dsl::types::Type;