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