Completion: narrow column candidates to the active table

Two related fixes:

1. \`update MyTable set \` was offering columns from every
   table in the project — completion fetched
   \`cache.for_source(IdentSource::Columns)\` which returns the
   flat \`cache.columns\` (union of every table's columns).
   The walker's WalkContext had \`current_table_columns\`
   populated (because the update-table-name slot is
   \`writes_table: true\`) but the completion engine never
   consulted it.

2. \`insert into MyTable (\` was offering nothing — the
   value-literal suppression fired because the expected set at
   this position contains both Form A column-list candidates
   (\`Ident{Columns}\`) and Form C bare-value-list literals
   (null/true/false/NumberLit/StringLit). \`is_value_literal_signature\`
   matched and the engine returned \`None\` before the column
   candidates were considered.

The fix threads the walker's \`current_table_columns\` through
to the completion engine and narrows the suppression rule:

**Walker:**
- New \`walker::CompletionProbe { expected, current_table_columns }\`
  struct.
- New \`walker::completion_probe(source, schema) -> CompletionProbe\`
  runs one schema-aware walk and reports both the expected
  set (or tail_expected on Match) and the resolved table-column
  snapshot.

**Completion engine:**
- \`candidates_at_cursor_with\` calls \`completion_probe\` and
  reads \`current_table_columns\` for the \`Columns\` ident
  source. Schemaless or unknown-table falls back to the flat
  \`cache.columns\` (preserves pre-fix behavior).
- Value-literal suppression now gated on
  \`!has_schema_ident\` — if the expected set also offers a
  schema-listable Ident, the user has actionable candidates
  beyond the misleading null/true/false trio and we shouldn't
  hide them.

Tests:
- \`update_set_offers_only_current_table_columns\` confirms
  Customers' columns appear while Orders' columns don't.
- \`update_where_offers_only_current_table_columns\` covers
  the where path.
- \`insert_into_open_paren_offers_current_table_columns\` and
  \`insert_into_open_paren_does_not_offer_unrelated_columns\`
  cover the Form A column-list position.
- \`drop_column_from_offers_only_current_table_columns\`
  documents the DDL fallback (drop-column's table-name slot
  doesn't currently \`writes_table\` — falls back to the flat
  list).

For the user: \`update MyTable set \` now offers only
MyTable's columns. \`insert into MyTable (\` offers all of
MyTable's columns so Form A is fully discoverable.

Tests: 859 passing, 0 failing, 1 ignored. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-15 19:07:46 +00:00
parent 5815918efb
commit 619a8bd707
2 changed files with 218 additions and 3 deletions
+57
View File
@@ -222,6 +222,63 @@ const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str
}
}
/// Completion-engine probe (ADR-0024 §Phase D §column-narrowing).
///
/// Runs a single schema-aware walk and returns the structured
/// pieces the completion engine needs: the expected set plus
/// the table-context snapshot the engine reads to narrow
/// column candidates to the active table.
#[derive(Debug, Clone)]
pub struct CompletionProbe {
pub expected: Vec<outcome::Expectation>,
/// Columns of `current_table` resolved at the cursor (set
/// by an `Ident { source: Tables, writes_table: true }`
/// earlier in the walk). `None` when the walker is
/// schemaless or the table didn't resolve.
pub current_table_columns: Option<Vec<crate::completion::TableColumn>>,
}
/// Run a schema-aware walk and report the completion-engine's
/// view (ADR-0024 §Phase D §column-narrowing).
#[must_use]
pub fn completion_probe(
source: &str,
schema: &crate::completion::SchemaCache,
) -> CompletionProbe {
use crate::dsl::grammar::REGISTRY;
if source.trim().is_empty() {
return CompletionProbe {
expected: REGISTRY
.iter()
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect(),
current_table_columns: None,
};
}
let mut ctx = context::WalkContext::with_schema(schema);
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
let Some(result) = result else {
return CompletionProbe {
expected: REGISTRY
.iter()
.map(|c| outcome::Expectation::Word(c.entry.primary))
.collect(),
current_table_columns: None,
};
};
let expected = match result.outcome {
outcome::WalkOutcome::Match { .. } => result.tail_expected,
outcome::WalkOutcome::Incomplete { expected, .. }
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
outcome::WalkOutcome::ValidationFailed { .. } => Vec::new(),
};
CompletionProbe {
expected,
current_table_columns: ctx.current_table_columns,
}
}
/// What the grammar would accept at the end of `source`
/// (ADR-0024 §architecture, Phase F walker-driven completion).
///