fix(completion): don't flag a table alias used before its FROM clause

In a SELECT, the projection can reference a table alias whose defining
FROM binding sits textually *after* the cursor — e.g.
`select sum(ol.count*p.price) … from … OrderLines ol …`. The candidate
engine already recovers that scope via the §10.6 full-input lookahead
(ADR-0032), but the typing-time validity indicator
(`invalid_ident_at_cursor`) walked only the text before the cursor,
found `ol` in no scope, and flagged it as an unknown column — a red
"ERR" overlay on an otherwise-valid query. (Other aliases escaped only
by coincidentally prefix-matching real columns.)

Give the validity check the same full-input lookahead: at a SQL
expression slot, recover the from-scope from the whole input and bail
when the partial prefix-matches a binding's alias or table name.
This commit is contained in:
claude@clouddev1
2026-06-12 12:19:55 +00:00
parent c3e010332c
commit 4cacb8261c
+43
View File
@@ -1243,6 +1243,27 @@ pub fn invalid_ident_at_cursor_in_mode(
if has_sql_expr_slot && crate::dsl::sql_functions::is_known_function_prefix(partial) {
return None;
}
// A bare ident at a SQL expression slot may be a **table alias / name**
// the user is mid-typing as a qualifier (`ol` in `sum(ol.count)`). The
// defining FROM clause can sit *after* the cursor — the projection
// references it — so the leading-only walk has an empty from-scope and
// would wrongly flag the alias as an unknown column. Recover the scope
// from the FULL input (mirrors the §10.6 edit-an-existing-query
// lookahead the candidate engine uses for column narrowing) and bail
// when the partial prefix-matches a binding's alias or table name.
if has_sql_expr_slot {
let full = crate::dsl::walker::completion_probe_in_mode(input, cache, mode);
let lowered = partial.to_lowercase();
let matches_qualifier = full.from_scope.iter().any(|b| {
b.alias
.as_deref()
.is_some_and(|a| a.to_lowercase().starts_with(&lowered))
|| b.table.to_lowercase().starts_with(&lowered)
});
if matches_qualifier {
return None;
}
}
// ADR-0048 D9: the `seed … set <col> as <gen>` slot is a curated
// vocabulary (`IdentSource::Generators`), not a schema source, so the
// schema-column check below would never see it. A partial that
@@ -2732,6 +2753,28 @@ mod tests {
);
}
#[test]
fn invalid_ident_does_not_flag_a_table_alias_used_before_its_from_clause() {
// Manual-testing bug: in `select … sum(ol.count*…) … from … OrderLines ol …`
// the projection references alias `ol` whose FROM binding sits
// *after* the cursor. The leading-only walk had an empty from-scope
// and wrongly flagged `ol` as an unknown column (a red "ERR" overlay
// on an otherwise-valid query). The full-input lookahead must
// recover the scope (ADR-0032 §10.6) so `ol` is not flagged.
use crate::dsl::types::Type;
let mut s = SchemaCache::default();
s.tables.push("OrderLines".into());
s.columns.push("count".into());
s.table_columns
.insert("OrderLines".into(), vec![TableColumn::new("count", Type::Int)]);
let input = "select sum(ol.count) from OrderLines ol";
let cursor = input.find("ol.count").unwrap() + 2; // right after `ol`
assert!(
invalid_ident_at_cursor_in_mode(input, cursor, &s, Mode::Advanced).is_none(),
"a table alias used before its FROM clause must not be flagged as a bad column",
);
}
#[test]
fn invalid_ident_fires_for_unknown_generator_after_as() {
// ADR-0048 D9: an unknown name at the `set <col> as <gen>` slot is