diff --git a/src/completion.rs b/src/completion.rs
index f5ad278..38bf3bd 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -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
as ` 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 as ` slot is