completion: §10.5 qualified-prefix + edit-scenario look-ahead

ADR-0032 §10.5 — at the cursor, an `<ident>.` prefix narrows
column candidates to that qualifier's binding columns. Resolves
through from_scope aliases first, then table names, then
cte_bindings (for `cte_alias.|`). Falls back to the schema cache
for DSL paths (`from <Table>.<col>`). Unresolved qualifier →
empty column list; the structural error path surfaces the
unresolved-prefix message.

Look-ahead probe — the "edit an existing query" workflow. When
the cursor is mid-projection but FROM exists after the cursor, a
second walk on the full input populates from_scope and the
column candidates narrow accordingly. Gated on the leading walk
producing no scope so cursor-past-FROM positions pay no cost.
The full input must parse for this to work; an unparseable
mid-edit state falls back to the §10.6 global posture.

CompletionProbe now exposes `from_scope` (top-frame table
bindings) and `cte_bindings` (union of in-scope CTE bindings,
innermost-first dedupe). The walker drains these at the cursor
position; the completion engine reads them for qualifier
resolution and unqualified narrowing.

Test totals: 1415 → 1424 passing (+9: 5 qualified-prefix +
4 look-ahead). Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-20 21:05:27 +00:00
parent fd259048da
commit 0fc7b082b2
2 changed files with 457 additions and 4 deletions
+40
View File
@@ -241,6 +241,17 @@ pub struct CompletionProbe {
/// WHERE-expression operand, which also accepts a column
/// reference — ADR-0026 §8).
pub pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
/// The active `from_scope` at the cursor (top frame on
/// the walker's scope stack). Empty when no FROM has been
/// reached or the walker is schemaless. Used by the
/// completion engine to narrow `cte.|` / `t.|` qualified-
/// prefix candidates to a single binding's columns
/// (ADR-0032 §10.5).
pub from_scope: Vec<context::TableBinding>,
/// CTE bindings visible at the cursor across all in-scope
/// frames (innermost to outermost). The same source the
/// qualified-prefix completion consults for `cte.|` shapes.
pub cte_bindings: Vec<context::CteBinding>,
}
/// Run a schema-aware walk and report the completion-engine's
@@ -282,6 +293,8 @@ pub fn completion_probe_in_mode(
expected: mode_filtered_entries(),
current_table_columns: None,
pending_hint_mode: None,
from_scope: Vec::new(),
cte_bindings: Vec::new(),
};
}
let mut ctx = context::WalkContext::with_schema(schema);
@@ -292,6 +305,8 @@ pub fn completion_probe_in_mode(
expected: mode_filtered_entries(),
current_table_columns: None,
pending_hint_mode: None,
from_scope: Vec::new(),
cte_bindings: Vec::new(),
};
};
let expected = match result.outcome {
@@ -318,10 +333,35 @@ pub fn completion_probe_in_mode(
// position.
outcome::WalkOutcome::ValidationFailed { .. } => result.tail_expected,
};
// Snapshot the cursor's lexical scope: top frame's
// from_scope and the union of every frame's cte_bindings
// (innermost first so a shadowing inner CTE wins on name
// collision per ADR-0032 §10.3).
let (from_scope, cte_bindings) = {
let top_from = ctx
.from_scope_stack
.last()
.map(|f| f.from_scope.clone())
.unwrap_or_default();
let mut ctes: Vec<context::CteBinding> = Vec::new();
for frame in ctx.from_scope_stack.iter().rev() {
for binding in &frame.cte_bindings {
if !ctes
.iter()
.any(|c| c.name.eq_ignore_ascii_case(&binding.name))
{
ctes.push(binding.clone());
}
}
}
(top_from, ctes)
};
CompletionProbe {
expected,
current_table_columns: ctx.current_table_columns,
pending_hint_mode: ctx.pending_hint_mode,
from_scope,
cte_bindings,
}
}