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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user